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:
1 parent
ad5b3cae72
commit
130eec751f
12 files changed
+1347
-72
No files matched your search
@@ -7,6 +7,15 @@ on:
|
||||
description: "版本号 (例如: 1.0.0)"
|
||||
required: true
|
||||
type: string
|
||||
platforms:
|
||||
description: "发布平台"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- chrome
|
||||
- firefox
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -20,16 +29,25 @@ jobs:
|
||||
- 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.json
|
||||
- name: 验证 manifest 文件
|
||||
run: |
|
||||
# 验证 manifest.json 是否为有效的 JSON
|
||||
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: 从输入设置版本
|
||||
id: version
|
||||
@@ -41,13 +59,22 @@ jobs:
|
||||
|
||||
- name: 更新 manifest 版本
|
||||
run: |
|
||||
# 更新 manifest.json 中的版本以匹配标签
|
||||
# 更新 Chrome manifest.json 中的版本
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
manifest.version = '${{ steps.version.outputs.version }}';
|
||||
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 版本
|
||||
@@ -68,7 +95,7 @@ jobs:
|
||||
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
|
||||
@@ -87,27 +114,38 @@ jobs:
|
||||
echo "✅ 版本更新已提交并推送到主分支,包含标签"
|
||||
fi
|
||||
|
||||
- name: 创建扩展包
|
||||
- name: 构建扩展包
|
||||
run: |
|
||||
# 创建通用扩展 zip 包(适用于所有基于 Chromium 的浏览器)
|
||||
zip -r extension.zip . \
|
||||
-x "*.git*" \
|
||||
-x "node_modules/*" \
|
||||
-x ".github/*" \
|
||||
-x "*.md" \
|
||||
-x "LICENSE" \
|
||||
-x "*.zip" \
|
||||
-x "*.crx"
|
||||
echo "🏗️ 开始构建扩展包..."
|
||||
|
||||
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 应用商店
|
||||
if: inputs.platforms == 'all' || inputs.platforms == 'chrome'
|
||||
continue-on-error: true
|
||||
id: chrome-publish
|
||||
uses: mnao305/chrome-extension-upload@v5.0.0
|
||||
with:
|
||||
file-path: extension.zip
|
||||
file-path: packages/xget-now-chrome-v${{ steps.version.outputs.version }}.zip
|
||||
extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
@@ -115,36 +153,96 @@ jobs:
|
||||
publish: true # 如果只想上传而不发布,设置为 false
|
||||
|
||||
- name: 发布到 Microsoft Edge 加载项
|
||||
if: inputs.platforms == 'all' || inputs.platforms == 'chrome'
|
||||
continue-on-error: true
|
||||
id: edge-publish
|
||||
uses: wdzeng/edge-addon@v2
|
||||
with:
|
||||
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 }}
|
||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||
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 发布
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
name: Xget Now ${{ steps.version.outputs.version }}
|
||||
files: |
|
||||
extension.zip
|
||||
packages/*.zip
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
此版本已自动发布到多个扩展商店。
|
||||
🎉 此版本已自动发布,现在支持多个浏览器平台!
|
||||
|
||||
## 📦 安装选项
|
||||
|
||||
### Chrome 系列浏览器
|
||||
- **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)
|
||||
- **手动安装**: 下载下面的 `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:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -152,29 +250,72 @@ jobs:
|
||||
run: |
|
||||
echo "📊 发布结果摘要:"
|
||||
echo "================================"
|
||||
echo "🏗️ 构建平台: ${{ inputs.platforms }}"
|
||||
echo "📦 版本: ${{ steps.version.outputs.version }}"
|
||||
echo ""
|
||||
|
||||
if [ "${{ steps.chrome-publish.outcome }}" = "success" ]; then
|
||||
echo "✅ Chrome 应用商店: 发布成功"
|
||||
elif [ "${{ steps.chrome-publish.outcome }}" = "failure" ]; then
|
||||
echo "❌ Chrome 应用商店: 发布失败"
|
||||
else
|
||||
echo "⚠️ Chrome 应用商店: 已跳过(缺少密钥)"
|
||||
if [ "${{ inputs.platforms }}" = "all" ] || [ "${{ inputs.platforms }}" = "chrome" ]; then
|
||||
if [ "${{ steps.chrome-publish.outcome }}" = "success" ]; then
|
||||
echo "✅ Chrome 应用商店: 发布成功"
|
||||
elif [ "${{ steps.chrome-publish.outcome }}" = "failure" ]; then
|
||||
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
|
||||
|
||||
if [ "${{ steps.edge-publish.outcome }}" = "success" ]; then
|
||||
echo "✅ Microsoft Edge 加载项: 发布成功"
|
||||
elif [ "${{ steps.edge-publish.outcome }}" = "failure" ]; then
|
||||
echo "❌ Microsoft Edge 加载项: 发布失败"
|
||||
else
|
||||
echo "⚠️ Microsoft Edge 加载项: 已跳过(缺少密钥)"
|
||||
if [ "${{ inputs.platforms }}" = "all" ] || [ "${{ inputs.platforms }}" = "firefox" ]; then
|
||||
if [ "${{ steps.firefox-prepare.outputs.firefox-package-exists }}" = "true" ]; then
|
||||
echo "✅ Firefox 扩展包: 已准备就绪,需要手动提交到 Mozilla"
|
||||
else
|
||||
echo "❌ Firefox 扩展包: 构建失败"
|
||||
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 "================================"
|
||||
|
||||
- name: 通知成功
|
||||
run: |
|
||||
echo "🎉 成功发布 Xget Now ${{ steps.version.outputs.version }} 到多个商店!"
|
||||
echo "🔗 Chrome 应用商店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}"
|
||||
echo "🔗 Microsoft Edge 加载项: 查看发布页面获取手动安装文件"
|
||||
echo ""
|
||||
echo "🎉 成功发布 Xget Now ${{ steps.version.outputs.version }}!"
|
||||
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 "📖 安装指南: https://github.com/${{ github.repository }}#installation"
|
||||
echo "================================"
|
||||
@@ -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
@@ -1,3 +1,12 @@
|
||||
// 导入兼容层
|
||||
importScripts("webext-compat.js");
|
||||
|
||||
// 确保兼容层可用
|
||||
if (typeof webext === "undefined") {
|
||||
// 在非模块环境中,兼容层应该已经通过脚本标签加载
|
||||
console.error("WebExt compatibility layer not found");
|
||||
}
|
||||
|
||||
// 平台配置
|
||||
const PLATFORMS = {
|
||||
// 代码托管平台
|
||||
@@ -201,20 +210,23 @@ const DEFAULT_SETTINGS = {
|
||||
};
|
||||
|
||||
// 初始化扩展
|
||||
chrome.runtime.onInstalled.addListener(async () => {
|
||||
webext.runtime.onInstalled.addListener(async () => {
|
||||
console.log("Xget Now 已安装");
|
||||
|
||||
// 使用本地存储而不是同步存储以确保兼容性
|
||||
const storageAPI = webext.storage.sync || webext.storage.local;
|
||||
|
||||
// 如果尚未设置,则设置默认设置
|
||||
const settings = await chrome.storage.sync.get(DEFAULT_SETTINGS);
|
||||
await chrome.storage.sync.set(settings);
|
||||
const settings = await storageAPI.get(DEFAULT_SETTINGS);
|
||||
await storageAPI.set(settings);
|
||||
});
|
||||
|
||||
// 防止监听器被多次注册
|
||||
let downloadListenerAdded = false;
|
||||
|
||||
// 监听下载事件
|
||||
if (!downloadListenerAdded) {
|
||||
chrome.downloads.onDeterminingFilename.addListener(handleDownload);
|
||||
if (!downloadListenerAdded && webext.downloads.onDeterminingFilename) {
|
||||
webext.downloads.onDeterminingFilename.addListener(handleDownload);
|
||||
downloadListenerAdded = true;
|
||||
}
|
||||
|
||||
@@ -228,7 +240,8 @@ function handleDownload(downloadItem, suggest) {
|
||||
|
||||
async function processDownloadRedirect(downloadItem) {
|
||||
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) {
|
||||
@@ -247,10 +260,10 @@ async function processDownloadRedirect(downloadItem) {
|
||||
|
||||
try {
|
||||
// 取消原始下载
|
||||
await chrome.downloads.cancel(downloadItem.id);
|
||||
await webext.downloads.cancel(downloadItem.id);
|
||||
|
||||
// 使用重定向的 URL 开始新下载
|
||||
await chrome.downloads.download({
|
||||
await webext.downloads.download({
|
||||
url: redirectedUrl,
|
||||
filename: downloadItem.filename || undefined,
|
||||
conflictAction: "uniquify",
|
||||
@@ -258,7 +271,7 @@ async function processDownloadRedirect(downloadItem) {
|
||||
|
||||
// 通过内容脚本显示通知
|
||||
try {
|
||||
await chrome.tabs.sendMessage(downloadItem.tabId, {
|
||||
await webext.tabs.sendMessage(downloadItem.tabId, {
|
||||
action: "showNotification",
|
||||
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") {
|
||||
chrome.storage.sync.get(DEFAULT_SETTINGS).then(sendResponse);
|
||||
storageAPI.get(DEFAULT_SETTINGS).then(sendResponse);
|
||||
return true;
|
||||
} else if (request.action === "saveSettings") {
|
||||
chrome.storage.sync.set(request.settings).then(async () => {
|
||||
storageAPI.set(request.settings).then(async () => {
|
||||
// 通知相关标签页刷新
|
||||
try {
|
||||
const tabs = await chrome.tabs.query({
|
||||
const tabs = await webext.tabs.query({
|
||||
url: Object.values(PLATFORMS).map((platform) => platform.base + "/*"),
|
||||
});
|
||||
|
||||
for (const tab of tabs) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, {
|
||||
await webext.tabs.sendMessage(tab.id, {
|
||||
action: "showNotification",
|
||||
message: "设置已更新!点击刷新页面",
|
||||
showRefreshButton: true,
|
||||
|
||||
@@ -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
@@ -1,5 +1,10 @@
|
||||
// 内容脚本,在支持的平台上注入下载拦截功能
|
||||
|
||||
// 确保兼容层可用
|
||||
if (typeof webext === "undefined") {
|
||||
console.error("WebExt compatibility layer not found in content script");
|
||||
}
|
||||
|
||||
// 平台配置
|
||||
const PLATFORMS = {
|
||||
// 代码托管平台
|
||||
@@ -141,7 +146,7 @@ const PLATFORMS = {
|
||||
console.log("Xget Now:内容脚本已加载");
|
||||
|
||||
// 监听来自后台脚本的消息
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === "showNotification") {
|
||||
showNotification(request.message, request.showRefreshButton);
|
||||
sendResponse({ success: true });
|
||||
@@ -170,7 +175,7 @@ const PLATFORMS = {
|
||||
async function getSettings() {
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ action: "getSettings" }, resolve);
|
||||
webext.runtime.sendMessage({ action: "getSettings" }, resolve);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取设置时出错:", error);
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -14,9 +14,7 @@
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"js": ["webext-compat.js", "content.js"],
|
||||
"matches": [
|
||||
"https://github.com/*",
|
||||
"https://gitlab.com/*",
|
||||
@@ -83,20 +81,12 @@
|
||||
},
|
||||
"manifest_version": 3,
|
||||
"name": "Xget Now",
|
||||
"permissions": [
|
||||
"downloads",
|
||||
"storage",
|
||||
"activeTab"
|
||||
],
|
||||
"permissions": ["downloads", "storage", "activeTab"],
|
||||
"version": "2.0.0",
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"resources": [
|
||||
"icons/*"
|
||||
]
|
||||
"matches": ["<all_urls>"],
|
||||
"resources": ["icons/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -576,6 +576,7 @@
|
||||
<div id="status" class="status" style="display: none"></div>
|
||||
</div>
|
||||
|
||||
<script src="webext-compat.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,10 @@
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// 确保兼容层可用
|
||||
if (typeof webext === "undefined") {
|
||||
console.error("WebExt compatibility layer not found in popup");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前设置
|
||||
await loadSettings();
|
||||
|
||||
@@ -8,7 +14,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const settings = await chrome.runtime.sendMessage({
|
||||
const settings = await webext.runtime.sendMessage({
|
||||
action: "getSettings",
|
||||
});
|
||||
|
||||
@@ -282,7 +288,7 @@ async function saveSettings() {
|
||||
settings.xgetDomain = cleanupDomain(settings.xgetDomain);
|
||||
}
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
const response = await webext.runtime.sendMessage({
|
||||
action: "saveSettings",
|
||||
settings: settings,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user