Add Firefox support and cross-browser compatibility layer

This update introduces full Firefox support using a new manifest-firefox.json (Manifest V2), adds a webext-compat.js compatibility layer for unified API usage, and refactors all extension scripts (background.js, content.js, popup.js) to use the new webext API. The build and development process is automated with new Python scripts (build.py, dev.py), and GitHub Actions workflows are updated to support multi-platform build, test, and release. Platform detection and capability abstraction are added, and all relevant files and manifests are updated for cross-browser compatibility.
This commit is contained in:
xixu-me committed 2025-08-10 13:44:08 +08:00
1 parent ad5b3cae72
commit 130eec751f
12 files changed
+1347 -72

No files matched your search

+180 -39
View File
@@ -7,6 +7,15 @@ on:
description: "版本号 (例如: 1.0.0)" description: "版本号 (例如: 1.0.0)"
required: true required: true
type: string type: string
platforms:
description: "发布平台"
required: true
default: "all"
type: choice
options:
- all
- chrome
- firefox
permissions: permissions:
contents: write contents: write
@@ -20,16 +29,25 @@ jobs:
- name: 检出代码 - name: 检出代码
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 设置 Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: 设置 Node.js - name: 设置 Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
- name: 验证 manifest.json - name: 验证 manifest 文件
run: | run: |
# 验证 manifest.json 是否为有效的 JSON # 验证 manifest.json 是否为有效的 JSON
node -e "JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'))" node -e "JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'))"
echo "✅ manifest.json 格式有效" echo "✅ Chrome manifest.json 格式有效"
# 验证 Firefox manifest
node -e "JSON.parse(require('fs').readFileSync('manifest-firefox.json', 'utf8'))"
echo "✅ Firefox manifest.json 格式有效"
- name: 从输入设置版本 - name: 从输入设置版本
id: version id: version
@@ -41,13 +59,22 @@ jobs:
- name: 更新 manifest 版本 - name: 更新 manifest 版本
run: | run: |
# 更新 manifest.json 中的版本以匹配标签 # 更新 Chrome manifest.json 中的版本
node -e " node -e "
const fs = require('fs'); const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8')); const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
manifest.version = '${{ steps.version.outputs.version }}'; manifest.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2)); fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
console.log('已更新 manifest.json 版本为:', manifest.version); console.log('已更新 Chrome manifest.json 版本为:', manifest.version);
"
# 更新 Firefox manifest.json 中的版本
node -e "
const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync('manifest-firefox.json', 'utf8'));
manifest.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('manifest-firefox.json', JSON.stringify(manifest, null, 2));
console.log('已更新 Firefox manifest.json 版本为:', manifest.version);
" "
- name: 更新 package.json 版本 - name: 更新 package.json 版本
@@ -68,7 +95,7 @@ jobs:
git config --local user.name "GitHub Action" git config --local user.name "GitHub Action"
# 添加更新的文件 # 添加更新的文件
git add manifest.json package.json git add manifest.json manifest-firefox.json package.json
# 检查是否有需要提交的更改 # 检查是否有需要提交的更改
if git diff --staged --quiet; then if git diff --staged --quiet; then
@@ -87,27 +114,38 @@ jobs:
echo "✅ 版本更新已提交并推送到主分支,包含标签" echo "✅ 版本更新已提交并推送到主分支,包含标签"
fi fi
- name: 创建扩展包 - name: 构建扩展包
run: | run: |
# 创建通用扩展 zip 包(适用于所有基于 Chromium 的浏览器) echo "🏗️ 开始构建扩展包..."
zip -r extension.zip . \
-x "*.git*" \
-x "node_modules/*" \
-x ".github/*" \
-x "*.md" \
-x "LICENSE" \
-x "*.zip" \
-x "*.crx"
echo "📦 已创建扩展包" # 运行构建脚本
ls -la *.zip if [ "${{ inputs.platforms }}" = "all" ]; then
python build.py --platform all --package
elif [ "${{ inputs.platforms }}" = "chrome" ]; then
python build.py --platform chrome --package
elif [ "${{ inputs.platforms }}" = "firefox" ]; then
python build.py --platform firefox --package
fi
echo "📦 扩展包构建完成"
ls -la packages/ || echo "packages 目录不存在"
ls -la build/ || echo "build 目录不存在"
- name: 运行测试
run: |
echo "🧪 运行基础测试..."
python dev.py test
echo "� 运行代码检查..."
python dev.py lint
- name: 发布到 Chrome 应用商店 - name: 发布到 Chrome 应用商店
if: inputs.platforms == 'all' || inputs.platforms == 'chrome'
continue-on-error: true continue-on-error: true
id: chrome-publish id: chrome-publish
uses: mnao305/chrome-extension-upload@v5.0.0 uses: mnao305/chrome-extension-upload@v5.0.0
with: with:
file-path: extension.zip file-path: packages/xget-now-chrome-v${{ steps.version.outputs.version }}.zip
extension-id: ${{ secrets.CHROME_EXTENSION_ID }} extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
client-id: ${{ secrets.CHROME_CLIENT_ID }} client-id: ${{ secrets.CHROME_CLIENT_ID }}
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
@@ -115,36 +153,96 @@ jobs:
publish: true # 如果只想上传而不发布,设置为 false publish: true # 如果只想上传而不发布,设置为 false
- name: 发布到 Microsoft Edge 加载项 - name: 发布到 Microsoft Edge 加载项
if: inputs.platforms == 'all' || inputs.platforms == 'chrome'
continue-on-error: true continue-on-error: true
id: edge-publish id: edge-publish
uses: wdzeng/edge-addon@v2 uses: wdzeng/edge-addon@v2
with: with:
product-id: ${{ secrets.EDGE_PRODUCT_ID }} product-id: ${{ secrets.EDGE_PRODUCT_ID }}
zip-path: extension.zip zip-path: packages/xget-now-chrome-v${{ steps.version.outputs.version }}.zip
api-key: ${{ secrets.EDGE_API_KEY }} api-key: ${{ secrets.EDGE_API_KEY }}
client-id: ${{ secrets.EDGE_CLIENT_ID }} client-id: ${{ secrets.EDGE_CLIENT_ID }}
notes-for-certification: "自动发布 v${{ steps.version.outputs.version }} - ${{ github.sha }}" notes-for-certification: "自动发布 v${{ steps.version.outputs.version }} - ${{ github.sha }}"
- name: 准备 Firefox 发布
if: inputs.platforms == 'all' || inputs.platforms == 'firefox'
continue-on-error: true
id: firefox-prepare
run: |
echo "🦊 准备 Firefox 扩展发布..."
# 检查 Firefox 扩展包是否存在
if [ -f "packages/xget-now-firefox-v${{ steps.version.outputs.version }}.zip" ]; then
echo "✅ Firefox 扩展包存在"
echo "firefox-package-exists=true" >> $GITHUB_OUTPUT
echo "firefox-package-path=packages/xget-now-firefox-v${{ steps.version.outputs.version }}.zip" >> $GITHUB_OUTPUT
else
echo "❌ Firefox 扩展包不存在"
echo "firefox-package-exists=false" >> $GITHUB_OUTPUT
fi
# 创建 Firefox 发布说明
cat > firefox-release-notes.txt << 'EOF'
此版本的 Xget Now 扩展已针对 Firefox 进行优化:
## 🆕 新功能
- 完全兼容 Firefox Manifest V2
- 支持所有主要功能:下载拦截、设置保存、通知显示
- 跨浏览器 API 兼容层确保稳定性
## 📦 安装方式
1. 临时安装:about:debugging → 临时载入附加组件
2. 手动安装:下载 .zip 文件并解压加载
3. 等待 Mozilla 审核通过后从 Add-ons 商店安装
## 🔧 技术改进
- 使用统一的 WebExtension API 兼容层
- 优化 Firefox 特有的权限和 API 调用
- 改进错误处理和用户体验
EOF
- name: 创建 GitHub 发布 - name: 创建 GitHub 发布
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{ steps.version.outputs.version }} tag_name: v${{ steps.version.outputs.version }}
name: Xget Now ${{ steps.version.outputs.version }} name: Xget Now ${{ steps.version.outputs.version }}
files: | files: |
extension.zip packages/*.zip
generate_release_notes: true generate_release_notes: true
body: | body: |
此版本已自动发布到多个扩展商店。 🎉 此版本已自动发布,现在支持多个浏览器平台!
## 📦 安装选项 ## 📦 安装选项
### Chrome 系列浏览器
- **Chrome 应用商店**: [从 Chrome 应用商店安装](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=zh-CN) - **Chrome 应用商店**: [从 Chrome 应用商店安装](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=zh-CN)
- **Microsoft Edge 加载项**: [从 Microsoft Edge 加载项安装](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN) - **Microsoft Edge 加载项**: [从 Microsoft Edge 加载项安装](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
- **手动安装**: 下载下面的 `extension.zip` 文件或查看 [安装指南](https://github.com/xixu-me/Xget-Now?tab=readme-ov-file#-%E5%AE%89%E8%A3%85) 获取详细说明 - **手动安装**: 下载 `xget-now-chrome-v${{ steps.version.outputs.version }}.zip`
### Firefox 浏览器
- **Firefox Add-ons**: 🚧 等待 Mozilla 审核(即将推出)
- **手动安装**: 下载 `xget-now-firefox-v${{ steps.version.outputs.version }}.zip` 并参考 [Firefox 安装指南](https://github.com/xixu-me/Xget-Now/blob/main/FIREFOX_INSTALL.md)
## 🆕 新功能亮点
- ✅ **Firefox 完全支持**: 现在同时支持 Chrome 和 Firefox 浏览器
- ✅ **统一功能体验**: 两个平台提供完全一致的功能
- ✅ **自动平台检测**: 扩展会自动适配当前浏览器环境
- ✅ **跨浏览器兼容**: 使用统一的 API 兼容层确保稳定性
## 📁 可用文件 ## 📁 可用文件
- `extension.zip` - 适用于所有基于 Chromium 的浏览器(Chrome、Edge、Brave、Opera 等) - `xget-now-chrome-v${{ steps.version.outputs.version }}.zip` - 适用于 Chrome、Edge、Brave、Opera 等基于 Chromium 的浏览器
- `xget-now-firefox-v${{ steps.version.outputs.version }}.zip` - 专为 Firefox 优化的版本
## 🛠️ 开发者信息
此版本引入了多平台支持架构:
- 使用 Manifest V3 (Chrome) 和 Manifest V2 (Firefox)
- 统一的 WebExtension API 兼容层
- 自动化构建和测试流程
详细技术文档请参考 [Firefox 支持说明](https://github.com/xixu-me/Xget-Now/blob/main/FIREFOX_SUPPORT.md)
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -152,29 +250,72 @@ jobs:
run: | run: |
echo "📊 发布结果摘要:" echo "📊 发布结果摘要:"
echo "================================" echo "================================"
echo "🏗️ 构建平台: ${{ inputs.platforms }}"
echo "📦 版本: ${{ steps.version.outputs.version }}"
echo ""
if [ "${{ steps.chrome-publish.outcome }}" = "success" ]; then if [ "${{ inputs.platforms }}" = "all" ] || [ "${{ inputs.platforms }}" = "chrome" ]; then
echo "✅ Chrome 应用商店: 发布成功" if [ "${{ steps.chrome-publish.outcome }}" = "success" ]; then
elif [ "${{ steps.chrome-publish.outcome }}" = "failure" ]; then echo "✅ Chrome 应用商店: 发布成功"
echo "❌ Chrome 应用商店: 发布失败" elif [ "${{ steps.chrome-publish.outcome }}" = "failure" ]; then
else echo "❌ Chrome 应用商店: 发布失败"
echo "⚠️ Chrome 应用商店: 已跳过(缺少密钥)" else
echo "⚠️ Chrome 应用商店: 已跳过(缺少密钥)"
fi
if [ "${{ steps.edge-publish.outcome }}" = "success" ]; then
echo "✅ Microsoft Edge 加载项: 发布成功"
elif [ "${{ steps.edge-publish.outcome }}" = "failure" ]; then
echo "❌ Microsoft Edge 加载项: 发布失败"
else
echo "⚠️ Microsoft Edge 加载项: 已跳过(缺少密钥)"
fi
fi fi
if [ "${{ steps.edge-publish.outcome }}" = "success" ]; then if [ "${{ inputs.platforms }}" = "all" ] || [ "${{ inputs.platforms }}" = "firefox" ]; then
echo "✅ Microsoft Edge 加载项: 发布成功" if [ "${{ steps.firefox-prepare.outputs.firefox-package-exists }}" = "true" ]; then
elif [ "${{ steps.edge-publish.outcome }}" = "failure" ]; then echo "✅ Firefox 扩展包: 已准备就绪,需要手动提交到 Mozilla"
echo "❌ Microsoft Edge 加载项: 发布失败" else
else echo "❌ Firefox 扩展包: 构建失败"
echo "⚠️ Microsoft Edge 加载项: 已跳过(缺少密钥)" fi
fi fi
echo "✅ GitHub 发布: 已创建扩展文件" echo ""
echo "✅ GitHub 发布: 已创建包含所有扩展文件"
echo "================================"
- name: Firefox 发布提醒
if: (inputs.platforms == 'all' || inputs.platforms == 'firefox') && steps.firefox-prepare.outputs.firefox-package-exists == 'true'
run: |
echo ""
echo "🦊 Firefox 发布提醒:"
echo "================================"
echo "Firefox 扩展已构建完成,但需要手动提交到 Mozilla Add-ons:"
echo ""
echo "1. 访问: https://addons.mozilla.org/developers/"
echo "2. 登录您的开发者账户"
echo "3. 上传文件: packages/xget-now-firefox-v${{ steps.version.outputs.version }}.zip"
echo "4. 填写发布信息并提交审核"
echo ""
echo "Mozilla 审核通常需要几天到几周时间。"
echo "在此期间,用户可以手动安装扩展文件。"
echo "================================" echo "================================"
- name: 通知成功 - name: 通知成功
run: | run: |
echo "🎉 成功发布 Xget Now ${{ steps.version.outputs.version }} 到多个商店!" echo ""
echo "🔗 Chrome 应用商店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}" echo "🎉 成功发布 Xget Now ${{ steps.version.outputs.version }}!"
echo "🔗 Microsoft Edge 加载项: 查看发布页面获取手动安装文件" echo "================================"
if [ "${{ inputs.platforms }}" = "all" ] || [ "${{ inputs.platforms }}" = "chrome" ]; then
echo "🔗 Chrome 应用商店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}"
echo "🔗 Microsoft Edge 加载项: 查看发布页面获取最新信息"
fi
if [ "${{ inputs.platforms }}" = "all" ] || [ "${{ inputs.platforms }}" = "firefox" ]; then
echo "🦊 Firefox: 手动提交到 Mozilla Add-ons(文件已准备就绪)"
fi
echo "📦 GitHub 发布: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}" echo "📦 GitHub 发布: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}"
echo "📖 安装指南: https://github.com/${{ github.repository }}#installation"
echo "================================"
+196
View File
@@ -0,0 +1,196 @@
name: 测试构建
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
test-build:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
- name: 验证 manifest 文件
run: |
echo "验证 Chrome manifest..."
node -e "
const manifest = JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'));
console.log('Chrome manifest 版本:', manifest.version);
console.log('Manifest 版本:', manifest.manifest_version);
if (manifest.manifest_version !== 3) {
throw new Error('Chrome manifest 应该使用 Manifest V3');
}
"
echo "验证 Firefox manifest..."
node -e "
const manifest = JSON.parse(require('fs').readFileSync('manifest-firefox.json', 'utf8'));
console.log('Firefox manifest 版本:', manifest.version);
console.log('Manifest 版本:', manifest.manifest_version);
if (manifest.manifest_version !== 2) {
throw new Error('Firefox manifest 应该使用 Manifest V2');
}
"
- name: 检查必需文件
run: |
echo "检查必需文件是否存在..."
required_files=(
"manifest.json"
"manifest-firefox.json"
"background.js"
"content.js"
"popup.js"
"popup.html"
"webext-compat.js"
"build.py"
"dev.py"
)
for file in "${required_files[@]}"; do
if [ -f "$file" ]; then
echo "✅ $file"
else
echo "❌ $file 缺失"
exit 1
fi
done
- name: 运行基础测试
run: |
echo "运行基础测试..."
python dev.py test
- name: 运行代码检查
run: |
echo "运行代码检查..."
python dev.py lint
- name: 测试 Chrome 构建
run: |
echo "测试 Chrome 构建..."
python build.py --platform chrome
# 验证构建结果
if [ -d "build/chrome" ]; then
echo "✅ Chrome 构建目录已创建"
ls -la build/chrome/
else
echo "❌ Chrome 构建失败"
exit 1
fi
- name: 测试 Firefox 构建
run: |
echo "测试 Firefox 构建..."
python build.py --platform firefox
# 验证构建结果
if [ -d "build/firefox" ]; then
echo "✅ Firefox 构建目录已创建"
ls -la build/firefox/
else
echo "❌ Firefox 构建失败"
exit 1
fi
- name: 测试扩展包生成
run: |
echo "测试扩展包生成..."
python build.py --platform all --package
# 验证扩展包
if [ -d "packages" ]; then
echo "✅ 扩展包目录已创建"
ls -la packages/
# 检查是否有 Chrome 和 Firefox 包
if [ -f packages/xget-now-chrome-v*.zip ]; then
echo "✅ Chrome 扩展包已生成"
else
echo "❌ Chrome 扩展包生成失败"
fi
if [ -f packages/xget-now-firefox-v*.zip ]; then
echo "✅ Firefox 扩展包已生成"
else
echo "❌ Firefox 扩展包生成失败"
fi
else
echo "❌ 扩展包生成失败"
exit 1
fi
- name: 验证构建的 manifest 文件
run: |
echo "验证构建的 manifest 文件..."
# 检查 Chrome 构建的 manifest
echo "检查 Chrome manifest..."
node -e "
const manifest = JSON.parse(require('fs').readFileSync('build/chrome/manifest.json', 'utf8'));
console.log('Chrome 构建版本:', manifest.version);
if (manifest.manifest_version !== 3) {
throw new Error('Chrome 构建应该使用 Manifest V3');
}
if (!manifest.action) {
throw new Error('Chrome 构建应该有 action 字段');
}
"
# 检查 Firefox 构建的 manifest
echo "检查 Firefox manifest..."
node -e "
const manifest = JSON.parse(require('fs').readFileSync('build/firefox/manifest.json', 'utf8'));
console.log('Firefox 构建版本:', manifest.version);
if (manifest.manifest_version !== 2) {
throw new Error('Firefox 构建应该使用 Manifest V2');
}
if (!manifest.browser_action) {
throw new Error('Firefox 构建应该有 browser_action 字段');
}
if (!manifest.applications && !manifest.browser_specific_settings) {
throw new Error('Firefox 构建应该有 applications 或 browser_specific_settings 字段');
}
"
- name: 上传构建结果
uses: actions/upload-artifact@v4
with:
name: extension-builds-${{ github.sha }}
path: |
build/
packages/
retention-days: 7
- name: 测试总结
run: |
echo ""
echo "🎉 所有测试通过!"
echo "================================"
echo "✅ Manifest 文件验证"
echo "✅ 必需文件检查"
echo "✅ 基础测试"
echo "✅ 代码检查"
echo "✅ Chrome 构建"
echo "✅ Firefox 构建"
echo "✅ 扩展包生成"
echo "✅ 构建结果验证"
echo "================================"
echo "🚀 准备发布!"
+29 -14
View File
@@ -1,3 +1,12 @@
// 导入兼容层
importScripts("webext-compat.js");
// 确保兼容层可用
if (typeof webext === "undefined") {
// 在非模块环境中,兼容层应该已经通过脚本标签加载
console.error("WebExt compatibility layer not found");
}
// 平台配置 // 平台配置
const PLATFORMS = { const PLATFORMS = {
// 代码托管平台 // 代码托管平台
@@ -201,20 +210,23 @@ const DEFAULT_SETTINGS = {
}; };
// 初始化扩展 // 初始化扩展
chrome.runtime.onInstalled.addListener(async () => { webext.runtime.onInstalled.addListener(async () => {
console.log("Xget Now 已安装"); console.log("Xget Now 已安装");
// 使用本地存储而不是同步存储以确保兼容性
const storageAPI = webext.storage.sync || webext.storage.local;
// 如果尚未设置,则设置默认设置 // 如果尚未设置,则设置默认设置
const settings = await chrome.storage.sync.get(DEFAULT_SETTINGS); const settings = await storageAPI.get(DEFAULT_SETTINGS);
await chrome.storage.sync.set(settings); await storageAPI.set(settings);
}); });
// 防止监听器被多次注册 // 防止监听器被多次注册
let downloadListenerAdded = false; let downloadListenerAdded = false;
// 监听下载事件 // 监听下载事件
if (!downloadListenerAdded) { if (!downloadListenerAdded && webext.downloads.onDeterminingFilename) {
chrome.downloads.onDeterminingFilename.addListener(handleDownload); webext.downloads.onDeterminingFilename.addListener(handleDownload);
downloadListenerAdded = true; downloadListenerAdded = true;
} }
@@ -228,7 +240,8 @@ function handleDownload(downloadItem, suggest) {
async function processDownloadRedirect(downloadItem) { async function processDownloadRedirect(downloadItem) {
try { try {
const settings = await chrome.storage.sync.get(DEFAULT_SETTINGS); const storageAPI = webext.storage.sync || webext.storage.local;
const settings = await storageAPI.get(DEFAULT_SETTINGS);
// 检查扩展是否已启用并配置了域名 // 检查扩展是否已启用并配置了域名
if (!settings.enabled || !settings.xgetDomain) { if (!settings.enabled || !settings.xgetDomain) {
@@ -247,10 +260,10 @@ async function processDownloadRedirect(downloadItem) {
try { try {
// 取消原始下载 // 取消原始下载
await chrome.downloads.cancel(downloadItem.id); await webext.downloads.cancel(downloadItem.id);
// 使用重定向的 URL 开始新下载 // 使用重定向的 URL 开始新下载
await chrome.downloads.download({ await webext.downloads.download({
url: redirectedUrl, url: redirectedUrl,
filename: downloadItem.filename || undefined, filename: downloadItem.filename || undefined,
conflictAction: "uniquify", conflictAction: "uniquify",
@@ -258,7 +271,7 @@ async function processDownloadRedirect(downloadItem) {
// 通过内容脚本显示通知 // 通过内容脚本显示通知
try { try {
await chrome.tabs.sendMessage(downloadItem.tabId, { await webext.tabs.sendMessage(downloadItem.tabId, {
action: "showNotification", action: "showNotification",
message: "下载已通过 Xget 重定向", message: "下载已通过 Xget 重定向",
}); });
@@ -298,21 +311,23 @@ function transformUrlLegacy(url, settings) {
} }
// 监听来自弹出窗口/选项的消息 // 监听来自弹出窗口/选项的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
const storageAPI = webext.storage.sync || webext.storage.local;
if (request.action === "getSettings") { if (request.action === "getSettings") {
chrome.storage.sync.get(DEFAULT_SETTINGS).then(sendResponse); storageAPI.get(DEFAULT_SETTINGS).then(sendResponse);
return true; return true;
} else if (request.action === "saveSettings") { } else if (request.action === "saveSettings") {
chrome.storage.sync.set(request.settings).then(async () => { storageAPI.set(request.settings).then(async () => {
// 通知相关标签页刷新 // 通知相关标签页刷新
try { try {
const tabs = await chrome.tabs.query({ const tabs = await webext.tabs.query({
url: Object.values(PLATFORMS).map((platform) => platform.base + "/*"), url: Object.values(PLATFORMS).map((platform) => platform.base + "/*"),
}); });
for (const tab of tabs) { for (const tab of tabs) {
try { try {
await chrome.tabs.sendMessage(tab.id, { await webext.tabs.sendMessage(tab.id, {
action: "showNotification", action: "showNotification",
message: "设置已更新!点击刷新页面", message: "设置已更新!点击刷新页面",
showRefreshButton: true, showRefreshButton: true,
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
构建脚本,用于生成不同浏览器版本的扩展包
支持 Chrome/Chromium(Manifest V3)和 Firefox(Manifest V2)
"""
import argparse
import json
import os
import shutil
import zipfile
from pathlib import Path
def create_build_directory(platform, clean=True):
"""创建构建目录"""
build_dir = Path(f"build/{platform}")
if clean and build_dir.exists():
shutil.rmtree(build_dir)
build_dir.mkdir(parents=True, exist_ok=True)
return build_dir
def copy_common_files(build_dir):
"""复制通用文件"""
common_files = [
"webext-compat.js",
"background.js",
"content.js",
"popup.js",
"popup.html",
"platforms.js",
"LICENSE",
"README.md",
"PRIVACY_POLICY.md",
"SECURITY.md",
]
for file in common_files:
if os.path.exists(file):
shutil.copy2(file, build_dir)
# 复制 icons 目录
if os.path.exists("icons"):
shutil.copytree("icons", build_dir / "icons", dirs_exist_ok=True)
def build_chrome():
"""构建 Chrome 版本"""
print("构建 Chrome 版本...")
build_dir = create_build_directory("chrome")
# 复制通用文件
copy_common_files(build_dir)
# 复制 Chrome manifest
shutil.copy2("manifest.json", build_dir / "manifest.json")
# 修改 background.js 为 Chrome 优化
optimize_for_chrome(build_dir)
print(f"Chrome 版本构建完成: {build_dir}")
return build_dir
def build_firefox():
"""构建 Firefox 版本"""
print("构建 Firefox 版本...")
build_dir = create_build_directory("firefox")
# 复制通用文件
copy_common_files(build_dir)
# 复制 Firefox manifest
shutil.copy2("manifest-firefox.json", build_dir / "manifest.json")
# 修改文件为 Firefox 优化
optimize_for_firefox(build_dir)
print(f"Firefox 版本构建完成: {build_dir}")
return build_dir
def optimize_for_chrome(build_dir):
"""为 Chrome 优化文件"""
# 移除 background.js 中的 importScripts 调用,因为在 Manifest V3 中不需要
bg_file = build_dir / "background.js"
if bg_file.exists():
content = bg_file.read_text(encoding="utf-8")
# 注释掉 importScripts 调用
content = content.replace(
"importScripts('webext-compat.js');",
"// importScripts('webext-compat.js'); // Chrome 不需要显式导入",
)
bg_file.write_text(content, encoding="utf-8")
def optimize_for_firefox(build_dir):
"""为 Firefox 优化文件"""
# Firefox 特有的优化可以在这里添加
pass
def create_package(build_dir, platform):
"""创建扩展包"""
package_dir = Path("packages")
package_dir.mkdir(exist_ok=True)
# 读取版本信息
manifest_file = build_dir / "manifest.json"
with open(manifest_file, encoding="utf-8") as f:
manifest = json.load(f)
version = manifest["version"]
package_name = f"xget-now-{platform}-v{version}.zip"
package_path = package_dir / package_name
# 创建 ZIP 包
with zipfile.ZipFile(package_path, "w", zipfile.ZIP_DEFLATED) as zipf:
for file_path in build_dir.rglob("*"):
if file_path.is_file():
arcname = file_path.relative_to(build_dir)
zipf.write(file_path, arcname)
print(f"扩展包已创建: {package_path}")
return package_path
def validate_manifest(manifest_path, platform):
"""验证 manifest 文件"""
try:
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
if platform == "chrome":
# 验证 Manifest V3 要求
assert manifest.get("manifest_version") == 3, "Chrome 需要 Manifest V3"
assert "action" in manifest, "Chrome 需要 action 字段"
assert "service_worker" in manifest.get("background", {}), (
"Chrome 需要 service_worker"
)
elif platform == "firefox":
# 验证 Manifest V2 要求
assert manifest.get("manifest_version") == 2, "Firefox 需要 Manifest V2"
assert "browser_action" in manifest, "Firefox 需要 browser_action 字段"
assert (
"applications" in manifest or "browser_specific_settings" in manifest
), "Firefox 需要应用ID"
print(f"✓ {platform} manifest 验证通过")
return True
except Exception as e:
print(f"✗ {platform} manifest 验证失败: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="构建 Xget Now 扩展")
parser.add_argument(
"--platform",
choices=["chrome", "firefox", "all"],
default="all",
help="要构建的平台",
)
parser.add_argument("--package", action="store_true", help="创建扩展包")
parser.add_argument(
"--clean", action="store_true", default=True, help="清理构建目录"
)
args = parser.parse_args()
platforms = []
if args.platform == "all":
platforms = ["chrome", "firefox"]
else:
platforms = [args.platform]
build_results = []
for platform in platforms:
try:
if platform == "chrome":
build_dir = build_chrome()
elif platform == "firefox":
build_dir = build_firefox()
# 验证 manifest
manifest_path = build_dir / "manifest.json"
if not validate_manifest(manifest_path, platform):
continue
build_results.append((platform, build_dir))
# 创建扩展包
if args.package:
create_package(build_dir, platform)
except Exception as e:
print(f"构建 {platform} 时出错: {e}")
# 总结
print("\n构建总结:")
for platform, build_dir in build_results:
print(f"✓ {platform}: {build_dir}")
if args.package:
print("\n扩展包位于 packages/ 目录")
if __name__ == "__main__":
main()
+7 -2
View File
@@ -1,5 +1,10 @@
// 内容脚本,在支持的平台上注入下载拦截功能 // 内容脚本,在支持的平台上注入下载拦截功能
// 确保兼容层可用
if (typeof webext === "undefined") {
console.error("WebExt compatibility layer not found in content script");
}
// 平台配置 // 平台配置
const PLATFORMS = { const PLATFORMS = {
// 代码托管平台 // 代码托管平台
@@ -141,7 +146,7 @@ const PLATFORMS = {
console.log("Xget Now:内容脚本已加载"); console.log("Xget Now:内容脚本已加载");
// 监听来自后台脚本的消息 // 监听来自后台脚本的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "showNotification") { if (request.action === "showNotification") {
showNotification(request.message, request.showRefreshButton); showNotification(request.message, request.showRefreshButton);
sendResponse({ success: true }); sendResponse({ success: true });
@@ -170,7 +175,7 @@ const PLATFORMS = {
async function getSettings() { async function getSettings() {
try { try {
return await new Promise((resolve) => { return await new Promise((resolve) => {
chrome.runtime.sendMessage({ action: "getSettings" }, resolve); webext.runtime.sendMessage({ action: "getSettings" }, resolve);
}); });
} catch (error) { } catch (error) {
console.error("获取设置时出错:", error); console.error("获取设置时出错:", error);
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
开发和测试脚本
"""
import http.server
import json
import os
import socketserver
import subprocess
import time
def start_dev_server(port=8000):
"""启动开发服务器用于测试"""
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=os.getcwd(), **kwargs)
def log_message(self, format, *args):
# 简化日志输出
print(f"[DEV] {format % args}")
with socketserver.TCPServer(("", port), Handler) as httpd:
print(f"开发服务器启动在 http://localhost:{port}")
print("按 Ctrl+C 停止服务器")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n开发服务器已停止")
def watch_files():
"""监控文件变化并自动重新构建"""
import os
watch_files = [
"manifest.json",
"manifest-firefox.json",
"background.js",
"content.js",
"popup.js",
"popup.html",
"webext-compat.js",
]
file_times = {}
for file in watch_files:
if os.path.exists(file):
file_times[file] = os.path.getmtime(file)
print("开始监控文件变化...")
print(f"监控文件: {', '.join(watch_files)}")
while True:
try:
changed = False
for file in watch_files:
if os.path.exists(file):
current_time = os.path.getmtime(file)
if file not in file_times or current_time > file_times[file]:
print(f"检测到文件变化: {file}")
file_times[file] = current_time
changed = True
if changed:
print("重新构建...")
result = subprocess.run(
["python", "build.py", "--platform", "all"],
capture_output=True,
text=True,
)
if result.returncode == 0:
print("✓ 构建成功")
else:
print(f"✗ 构建失败: {result.stderr}")
time.sleep(1)
except KeyboardInterrupt:
print("\n停止文件监控")
break
def run_tests():
"""运行测试"""
print("运行基础测试...")
# 测试 manifest 文件
def test_manifest(file_path, platform):
try:
with open(file_path, encoding="utf-8") as f:
manifest = json.load(f)
# 基础字段检查
required_fields = ["name", "version", "manifest_version"]
for field in required_fields:
assert field in manifest, f"缺少必需字段: {field}"
# 平台特定检查
if platform == "chrome":
assert manifest["manifest_version"] == 3, "Chrome 需要 Manifest V3"
assert "action" in manifest, "Chrome 需要 action 字段"
elif platform == "firefox":
assert manifest["manifest_version"] == 2, "Firefox 需要 Manifest V2"
assert "browser_action" in manifest, "Firefox 需要 browser_action 字段"
print(f"✓ {platform} manifest 测试通过")
return True
except Exception as e:
print(f"✗ {platform} manifest 测试失败: {e}")
return False
# 测试文件存在性
def test_files_exist():
required_files = [
"manifest.json",
"manifest-firefox.json",
"background.js",
"content.js",
"popup.js",
"popup.html",
"webext-compat.js",
]
all_exist = True
for file in required_files:
if os.path.exists(file):
print(f"✓ {file} 存在")
else:
print(f"✗ {file} 不存在")
all_exist = False
return all_exist
# 运行所有测试
results = []
results.append(test_files_exist())
results.append(test_manifest("manifest.json", "chrome"))
results.append(test_manifest("manifest-firefox.json", "firefox"))
# 总结
passed = sum(results)
total = len(results)
print(f"\n测试结果: {passed}/{total} 通过")
return passed == total
def lint_code():
"""代码检查"""
print("运行代码检查...")
js_files = ["background.js", "content.js", "popup.js", "webext-compat.js"]
for file in js_files:
if os.path.exists(file):
print(f"检查 {file}...")
# 简单的语法检查
with open(file, encoding="utf-8") as f:
content = f.read()
# 检查常见问题
issues = []
if "chrome." in content and "webext." not in content:
issues.append("发现直接使用 chrome API,应使用 webext 兼容层")
if "console.log(" in content and "console.error(" not in content:
issues.append("建议使用 console.error 处理错误")
if issues:
print(f" ⚠ {file} 发现问题:")
for issue in issues:
print(f" - {issue}")
else:
print(f" ✓ {file} 无明显问题")
def main():
import argparse
parser = argparse.ArgumentParser(description="Xget Now 开发工具")
parser.add_argument(
"command", choices=["test", "lint", "watch", "serve"], help="要执行的命令"
)
parser.add_argument(
"--port", type=int, default=8000, help="开发服务器端口 (默认: 8000)"
)
args = parser.parse_args()
if args.command == "test":
success = run_tests()
exit(0 if success else 1)
elif args.command == "lint":
lint_code()
elif args.command == "watch":
watch_files()
elif args.command == "serve":
start_dev_server(args.port)
if __name__ == "__main__":
main()
+110
View File
@@ -0,0 +1,110 @@
{
"manifest_version": 2,
"name": "Xget Now",
"version": "2.0.0",
"description": "为 Firefox 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
"applications": {
"gecko": {
"id": "xget-now@xixu.me",
"strict_min_version": "109.0"
}
},
"browser_action": {
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"default_popup": "popup.html",
"default_title": "Xget Now"
},
"background": {
"scripts": ["webext-compat.js", "background.js"],
"persistent": false
},
"content_scripts": [
{
"js": ["webext-compat.js", "content.js"],
"matches": [
"https://github.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
"https://sourceforge.net/*",
"https://android.googlesource.com/*",
"https://huggingface.co/*",
"https://registry.npmjs.org/*",
"https://pypi.org/*",
"https://files.pythonhosted.org/*",
"https://repo.anaconda.com/*",
"https://conda.anaconda.org/*",
"https://repo1.maven.org/*",
"https://downloads.apache.org/*",
"https://plugins.gradle.org/*",
"https://rubygems.org/*",
"https://cran.r-project.org/*",
"https://www.cpan.org/*",
"https://tug.ctan.org/*",
"https://proxy.golang.org/*",
"https://api.nuget.org/*",
"https://crates.io/*",
"https://repo.packagist.org/*",
"https://arxiv.org/*",
"https://f-droid.org/*"
],
"run_at": "document_end"
}
],
"permissions": [
"downloads",
"storage",
"activeTab",
"https://github.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
"https://sourceforge.net/*",
"https://android.googlesource.com/*",
"https://huggingface.co/*",
"https://registry.npmjs.org/*",
"https://pypi.org/*",
"https://files.pythonhosted.org/*",
"https://repo.anaconda.com/*",
"https://conda.anaconda.org/*",
"https://repo1.maven.org/*",
"https://downloads.apache.org/*",
"https://plugins.gradle.org/*",
"https://rubygems.org/*",
"https://cran.r-project.org/*",
"https://www.cpan.org/*",
"https://tug.ctan.org/*",
"https://proxy.golang.org/*",
"https://api.nuget.org/*",
"https://crates.io/*",
"https://repo.packagist.org/*",
"https://arxiv.org/*",
"https://f-droid.org/*"
],
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"web_accessible_resources": ["icons/*"],
"browser_specific_settings": {
"gecko": {
"id": "xget-now@xixu.me",
"strict_min_version": "109.0"
}
}
}
+5 -15
View File
@@ -14,9 +14,7 @@
}, },
"content_scripts": [ "content_scripts": [
{ {
"js": [ "js": ["webext-compat.js", "content.js"],
"content.js"
],
"matches": [ "matches": [
"https://github.com/*", "https://github.com/*",
"https://gitlab.com/*", "https://gitlab.com/*",
@@ -83,20 +81,12 @@
}, },
"manifest_version": 3, "manifest_version": 3,
"name": "Xget Now", "name": "Xget Now",
"permissions": [ "permissions": ["downloads", "storage", "activeTab"],
"downloads",
"storage",
"activeTab"
],
"version": "2.0.0", "version": "2.0.0",
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"matches": [ "matches": ["<all_urls>"],
"<all_urls>" "resources": ["icons/*"]
],
"resources": [
"icons/*"
]
} }
] ]
} }
+147
View File
@@ -0,0 +1,147 @@
/**
* 平台特定的配置和检测
*/
// 检测当前平台和环境
const platformDetector = {
/**
* 检测当前浏览器平台
*/
detect() {
const manifest =
typeof browser !== "undefined" && browser.runtime
? browser.runtime.getManifest()
: chrome.runtime.getManifest();
const ua = navigator.userAgent;
const extURL =
typeof browser !== "undefined" && browser.runtime
? browser.runtime.getURL("")
: chrome.runtime.getURL("");
let platform = "unknown";
let version = 0;
if (extURL.startsWith("moz-extension://")) {
platform = "firefox";
const match = /Firefox\/(\d+)/.exec(ua);
version = match ? parseInt(match[1], 10) : 115;
} else if (extURL.startsWith("safari-web-extension://")) {
platform = "safari";
const match = /Version\/(\d+)/.exec(ua);
version = match ? parseInt(match[1], 10) : 15;
} else if (/\bEdg\/\b/.test(ua)) {
platform = "edge";
const match = /Edg\/(\d+)/.exec(ua);
version = match ? parseInt(match[1], 10) : 120;
} else if (/\bOPR\/\b/.test(ua)) {
platform = "opera";
const match = /OPR\/(\d+)/.exec(ua);
version = match ? parseInt(match[1], 10) : 120;
} else {
platform = "chrome";
const match = /Chrome\/(\d+)/.exec(ua);
version = match ? parseInt(match[1], 10) : 120;
}
return {
platform,
version,
manifestVersion: manifest.manifest_version,
isMobile: /\bMobile\b/.test(ua),
isDevBuild: /^\d+\.\d+\.\d+\D/.test(manifest.version),
};
},
/**
* 获取平台特定的能力
*/
getCapabilities(platformInfo = null) {
const info = platformInfo || this.detect();
const capabilities = {
// 基础能力
downloads: true,
storage: true,
tabs: true,
// 高级能力
declarativeNetRequest: false,
offscreen: false,
scripting: false,
// 特殊能力
popupResize: true,
contextMenus: true,
notifications: true,
};
// Firefox 特定能力
if (info.platform === "firefox") {
capabilities.webRequest = true;
capabilities.contentSecurityPolicy = false; // 在 Manifest V2 中有限制
capabilities.popupResize = false; // Firefox 不支持动态调整弹窗大小
}
// Chrome/Chromium 特定能力
if (
info.platform === "chrome" ||
info.platform === "edge" ||
info.platform === "opera"
) {
if (info.manifestVersion === 3) {
capabilities.declarativeNetRequest = true;
capabilities.offscreen = info.version >= 109;
capabilities.scripting = true;
capabilities.webRequest = false; // MV3 中 webRequest 被限制
} else {
capabilities.webRequest = true;
}
}
// Safari 特定能力
if (info.platform === "safari") {
capabilities.notifications = false; // Safari 扩展通知支持有限
capabilities.contextMenus = false; // Safari 扩展上下文菜单支持有限
}
return capabilities;
},
/**
* 获取平台特定的限制
*/
getLimitations(platformInfo = null) {
const info = platformInfo || this.detect();
const limitations = {
maxStorageSize: Infinity,
maxDownloadConcurrency: 10,
requiresUserGesture: false,
contentScriptLimitations: [],
};
if (info.platform === "firefox") {
limitations.maxStorageSize = 5 * 1024 * 1024; // 5MB for local storage
limitations.contentScriptLimitations = [
"limited-eval", // eval() 受限
"no-dynamic-import", // 动态导入受限
];
}
if (info.platform === "safari") {
limitations.maxStorageSize = 10 * 1024 * 1024; // 10MB
limitations.requiresUserGesture = true; // 许多操作需要用户手势
limitations.maxDownloadConcurrency = 3; // 下载并发限制更严格
}
return limitations;
},
};
// 导出
if (typeof module !== "undefined" && module.exports) {
module.exports = { platformDetector };
} else {
window.platformDetector = platformDetector;
}
+1
View File
@@ -576,6 +576,7 @@
<div id="status" class="status" style="display: none"></div> <div id="status" class="status" style="display: none"></div>
</div> </div>
<script src="webext-compat.js"></script>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>
+8 -2
View File
@@ -1,4 +1,10 @@
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
// 确保兼容层可用
if (typeof webext === "undefined") {
console.error("WebExt compatibility layer not found in popup");
return;
}
// 加载当前设置 // 加载当前设置
await loadSettings(); await loadSettings();
@@ -8,7 +14,7 @@ document.addEventListener("DOMContentLoaded", async () => {
async function loadSettings() { async function loadSettings() {
try { try {
const settings = await chrome.runtime.sendMessage({ const settings = await webext.runtime.sendMessage({
action: "getSettings", action: "getSettings",
}); });
@@ -282,7 +288,7 @@ async function saveSettings() {
settings.xgetDomain = cleanupDomain(settings.xgetDomain); settings.xgetDomain = cleanupDomain(settings.xgetDomain);
} }
const response = await chrome.runtime.sendMessage({ const response = await webext.runtime.sendMessage({
action: "saveSettings", action: "saveSettings",
settings: settings, settings: settings,
}); });
+243
View File
@@ -0,0 +1,243 @@
/**
* WebExtension API 兼容层
* 提供跨浏览器的统一API接口
*/
// 检测当前浏览器环境
const detectBrowser = () => {
const ua = navigator.userAgent;
const manifest = chrome.runtime.getManifest();
const flavor = {
major: 0,
soup: new Set(["webext"]),
};
// 检测 Firefox
if (
typeof browser !== "undefined" &&
browser.runtime &&
browser.runtime.getURL("").startsWith("moz-extension://")
) {
flavor.soup.add("firefox");
const match = /Firefox\/(\d+)/.exec(ua);
flavor.major = match ? parseInt(match[1], 10) : 115;
// Firefox 特性检测
if (CSS.supports("selector(a:has(b))")) {
flavor.soup.add("native_css_has");
}
}
// 检测基于 Chromium 的浏览器
else {
const match = /\bChrom(?:e|ium)\/(\d+)/.exec(ua);
if (match !== null) {
flavor.soup.add("chromium");
}
flavor.major = match ? parseInt(match[1], 10) : 120;
// 检测具体浏览器
if (/\bEdg\/\b/.test(ua)) {
flavor.soup.add("edge");
} else if (/\bOPR\/\b/.test(ua)) {
flavor.soup.add("opera");
} else {
flavor.soup.add("chrome");
}
}
// 检测移动端
if (/\bMobile\b/.test(ua)) {
flavor.soup.add("mobile");
}
// 检测开发版本
if (/^\d+\.\d+\.\d+\D/.test(manifest.version)) {
flavor.soup.add("devbuild");
}
return flavor;
};
// 浏览器特性检测
const webextFlavor = detectBrowser();
// 统一的 WebExtension API
const webext = (() => {
// Firefox 使用 browser API,Chrome 使用 chrome API
const api =
typeof browser !== "undefined" && browser.runtime ? browser : chrome;
// Promise 化函数的包装器
const promisify = (context, methodName) => {
const method = context[methodName];
if (!method) {
throw new Error(`Method ${methodName} not found`);
}
return function (...args) {
return new Promise((resolve, reject) => {
method.call(context, ...args, (result) => {
if (api.runtime.lastError) {
reject(new Error(api.runtime.lastError.message));
} else {
resolve(result);
}
});
});
};
};
// 无失败的 Promise 包装器
const promisifyNoFail = (context, methodName, defaultValue = undefined) => {
try {
const promisified = promisify(context, methodName);
return async function (...args) {
try {
return await promisified(...args);
} catch (error) {
console.warn(`WebExt API call failed: ${methodName}`, error);
return defaultValue;
}
};
} catch (error) {
// 如果方法不存在,返回空函数
return async function () {
console.warn(`WebExt API method not available: ${methodName}`);
return defaultValue;
};
}
};
return {
// Runtime API
runtime: {
getManifest: () => api.runtime.getManifest(),
getURL: (path) => api.runtime.getURL(path),
sendMessage: promisifyNoFail(api.runtime, "sendMessage"),
connect: (connectInfo) => api.runtime.connect(connectInfo),
onMessage: api.runtime.onMessage,
onConnect: api.runtime.onConnect,
lastError: api.runtime.lastError,
id: api.runtime.id,
},
// Tabs API
tabs: {
query: promisifyNoFail(api.tabs, "query", []),
get: promisifyNoFail(api.tabs, "get"),
create: promisifyNoFail(api.tabs, "create"),
update: promisifyNoFail(api.tabs, "update"),
remove: promisifyNoFail(api.tabs, "remove"),
sendMessage: promisifyNoFail(api.tabs, "sendMessage"),
executeScript: promisifyNoFail(api.tabs, "executeScript"),
insertCSS: promisifyNoFail(api.tabs, "insertCSS"),
removeCSS: promisifyNoFail(api.tabs, "removeCSS"),
onUpdated: api.tabs.onUpdated,
onActivated: api.tabs.onActivated,
onRemoved: api.tabs.onRemoved,
},
// Storage API
storage: {
local: {
get: promisifyNoFail(api.storage.local, "get", {}),
set: promisifyNoFail(api.storage.local, "set"),
remove: promisifyNoFail(api.storage.local, "remove"),
clear: promisifyNoFail(api.storage.local, "clear"),
getBytesInUse: promisifyNoFail(api.storage.local, "getBytesInUse", 0),
},
sync: api.storage.sync
? {
get: promisifyNoFail(api.storage.sync, "get", {}),
set: promisifyNoFail(api.storage.sync, "set"),
remove: promisifyNoFail(api.storage.sync, "remove"),
clear: promisifyNoFail(api.storage.sync, "clear"),
getBytesInUse: promisifyNoFail(
api.storage.sync,
"getBytesInUse",
0
),
}
: null,
},
// Downloads API
downloads: {
download: promisifyNoFail(api.downloads, "download"),
search: promisifyNoFail(api.downloads, "search", []),
cancel: promisifyNoFail(api.downloads, "cancel"),
pause: promisifyNoFail(api.downloads, "pause"),
resume: promisifyNoFail(api.downloads, "resume"),
erase: promisifyNoFail(api.downloads, "erase"),
removeFile: promisifyNoFail(api.downloads, "removeFile"),
show: promisifyNoFail(api.downloads, "show"),
showDefaultFolder: promisifyNoFail(api.downloads, "showDefaultFolder"),
onCreated: api.downloads.onCreated,
onChanged: api.downloads.onChanged,
onErased: api.downloads.onErased,
},
// Browser Action / Action API (兼容不同版本)
action: (() => {
// Manifest V3 使用 action,V2 使用 browserAction
const actionAPI = api.action || api.browserAction;
if (!actionAPI) return null;
return {
setTitle: promisifyNoFail(actionAPI, "setTitle"),
getTitle: promisifyNoFail(actionAPI, "getTitle"),
setIcon: promisifyNoFail(actionAPI, "setIcon"),
setPopup: promisifyNoFail(actionAPI, "setPopup"),
getPopup: promisifyNoFail(actionAPI, "getPopup"),
setBadgeText: promisifyNoFail(actionAPI, "setBadgeText"),
getBadgeText: promisifyNoFail(actionAPI, "getBadgeText"),
setBadgeBackgroundColor: promisifyNoFail(
actionAPI,
"setBadgeBackgroundColor"
),
getBadgeBackgroundColor: promisifyNoFail(
actionAPI,
"getBadgeBackgroundColor"
),
setBadgeTextColor: promisifyNoFail(actionAPI, "setBadgeTextColor"),
getBadgeTextColor: promisifyNoFail(actionAPI, "getBadgeTextColor"),
enable: promisifyNoFail(actionAPI, "enable"),
disable: promisifyNoFail(actionAPI, "disable"),
onClicked: actionAPI.onClicked,
};
})(),
// Permissions API
permissions: {
contains: promisifyNoFail(api.permissions, "contains", false),
getAll: promisifyNoFail(api.permissions, "getAll", {
origins: [],
permissions: [],
}),
request: promisifyNoFail(api.permissions, "request", false),
remove: promisifyNoFail(api.permissions, "remove", false),
onAdded: api.permissions?.onAdded,
onRemoved: api.permissions?.onRemoved,
},
// i18n API
i18n: {
getMessage: (messageName, substitutions) =>
api.i18n.getMessage(messageName, substitutions),
getAcceptLanguages: promisifyNoFail(api.i18n, "getAcceptLanguages", []),
getUILanguage: () => api.i18n.getUILanguage(),
detectLanguage: promisifyNoFail(api.i18n, "detectLanguage"),
},
};
})();
// 导出接口
if (typeof module !== "undefined" && module.exports) {
// Node.js 环境
module.exports = { webext, webextFlavor };
} else {
// 浏览器环境
window.webext = webext;
window.webextFlavor = webextFlavor;
}