59 Commits
Author SHA1 Message Date
xixu-me 4ce464a6de fix(actions): avoid status event fan-out
测试构建 / test-build (push) Waiting to run
2026-08-18 11:29:41 +08:00
xixu-me 7df54ea584 fix(actions): retry Dependabot merge after checks 2026-08-18 11:25:32 +08:00
xixu-me 97d690b239 fix(ci): evaluate all Dependabot pull requests 2026-08-14 18:15:00 +08:00
github-actions[bot] 99f91c4f76 Merge pull request #35 from xixu-me/dependabot/npm_and_yarn/testing-fed66be72f
npm(deps-dev): bump playwright from 1.62.0 to 1.62.1 in the testing group
2026-08-03 09:07:58 +00:00
dependabot[bot] 0b598844c8 npm(deps-dev): bump playwright in the testing group
Bumps the testing group with 1 update: [playwright](https://github.com/microsoft/playwright).


Updates `playwright` from 1.62.0 to 1.62.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.0...v1.62.1)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: testing
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 09:06:40 +00:00
github-actions[bot] 50eec17043 Merge pull request #34 from xixu-me/dependabot/npm_and_yarn/testing-854fbf1c35
npm(deps-dev): bump playwright from 1.61.1 to 1.62.0 in the testing group
2026-07-27 09:07:17 +00:00
dependabot[bot] 8cf4480dd3 npm(deps-dev): bump playwright in the testing group
Bumps the testing group with 1 update: [playwright](https://github.com/microsoft/playwright).


Updates `playwright` from 1.61.1 to 1.62.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: testing
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 09:06:53 +00:00
github-actions[bot] da9a3a9b0d Merge pull request #33 from xixu-me/dependabot/github_actions/github-actions-abf59fe041
actions(deps): bump the github-actions group with 2 updates
2026-07-20 09:07:39 +00:00
dependabot[bot] d92ad2c485 actions(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/setup-python](https://github.com/actions/setup-python) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-python` from 6 to 7
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

Updates `actions/setup-node` from 6 to 7
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 09:07:05 +00:00
github-actions[bot] d1844cea31 Merge pull request #32 from xixu-me/dependabot/npm_and_yarn/testing-b1ff3c9868
npm(deps-dev): bump playwright from 1.61.0 to 1.61.1 in the testing group
2026-07-06 09:06:38 +00:00
dependabot[bot] 38310b688f npm(deps-dev): bump playwright in the testing group
Bumps the testing group with 1 update: [playwright](https://github.com/microsoft/playwright).


Updates `playwright` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.0...v1.61.1)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: testing
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 09:06:03 +00:00
xixu-me bf91b4f899 ci: trigger Dependabot auto-merge from workflow runs 2026-07-03 23:34:49 +08:00
xixu-me 7017af6949 ci: tolerate protected Dependabot workflow updates 2026-07-03 23:15:02 +08:00
github-actions[bot] dda9ac388a Merge pull request #30 from xixu-me/dependabot/npm_and_yarn/testing-80df46a43b
npm(deps-dev): bump playwright from 1.60.0 to 1.61.0 in the testing group
2026-07-03 15:09:16 +00:00
github-actions[bot] 9199dcc784 Merge pull request #31 from xixu-me/dependabot/github_actions/github-actions-640176b5ab
actions(deps): bump actions/checkout from 6 to 7 in the github-actions group
2026-07-03 15:09:06 +00:00
xixu-me 3cc7695b1c ci: add Dependabot auto merge workflow 2026-07-03 23:04:20 +08:00
dependabot[bot] 6d9689521c actions(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 09:06:32 +00:00
dependabot[bot] 68fb221d51 npm(deps-dev): bump playwright in the testing group
Bumps the testing group with 1 update: [playwright](https://github.com/microsoft/playwright).


Updates `playwright` from 1.60.0 to 1.61.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: testing
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 09:06:27 +00:00
xixu-me 80e27dfcef Merge pull request #29 from xixu-me/dependabot/npm_and_yarn/testing-a5755dd79e
npm(deps-dev): bump playwright from 1.59.1 to 1.60.0 in the testing group
2026-05-19 02:00:52 +08:00
dependabot[bot] 1bd473bf11 npm(deps-dev): bump playwright in the testing group
Bumps the testing group with 1 update: [playwright](https://github.com/microsoft/playwright).


Updates `playwright` from 1.59.1 to 1.60.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.59.1...v1.60.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: testing
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 16:04:58 +00:00
xixu-me 31ca503ae8 Merge pull request #28 from xixu-me/dependabot/github_actions/github-actions-dd04e95abd
actions(deps): bump the github-actions group with 3 updates
2026-04-13 17:26:38 +08:00
dependabot[bot] 95f6054b5c actions(deps): bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata), [actions/github-script](https://github.com/actions/github-script) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release).


Updates `dependabot/fetch-metadata` from 2 to 3
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2...v3)

Updates `actions/github-script` from 8 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

Updates `softprops/action-gh-release` from 2 to 3
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 09:25:25 +00:00
GitHub Action 5271287f45 chore: 更新版本到 2.1.0 [skip ci] 2026-04-11 16:25:52 +00:00
xixu-me f0335c4fc4 chore(release): prepare userscript publishing 2026-04-12 00:25:16 +08:00
xixu-me bbed3334c0 feat(userscript): add shared download core 2026-04-12 00:15:54 +08:00
xixu-me 3fec6dab96 docs: align readmes with create-readme 2026-04-11 21:54:58 +08:00
xixu-me 70fa0cb9c1 Update README with group invitation message
Added a welcome message for the Xget open source and AI group.
2026-04-07 23:42:30 +08:00
xixu-me f80543fbb6 Merge pull request #27 from xixu-me/dependabot/github_actions/github-actions-20a84abab2
actions(deps): bump mnao305/chrome-extension-upload from 5.0.0 to 6.0.0 in the github-actions group
2026-04-06 17:45:37 +08:00
dependabot[bot] 3345c943b3 actions(deps): bump mnao305/chrome-extension-upload
Bumps the github-actions group with 1 update: [mnao305/chrome-extension-upload](https://github.com/mnao305/chrome-extension-upload).


Updates `mnao305/chrome-extension-upload` from 5.0.0 to 6.0.0
- [Release notes](https://github.com/mnao305/chrome-extension-upload/releases)
- [Commits](https://github.com/mnao305/chrome-extension-upload/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: mnao305/chrome-extension-upload
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 09:12:24 +00:00
xixu-me 065cb2b396 ci: add dependabot auto-merge workflow 2026-03-11 13:36:47 +08:00
xixu-me 1335fb736e Merge pull request #25 from xixu-me/dependabot/github_actions/github-actions-674967a53d
actions(deps): bump actions/upload-artifact from 6 to 7 in the github-actions group
2026-03-06 14:13:36 +08:00
dependabot[bot] 545452be40 actions(deps): bump actions/upload-artifact in the github-actions group
Bumps the github-actions group with 1 update: [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `actions/upload-artifact` from 6 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 09:21:40 +00:00
xixu-me 2a6d5271c4 Merge pull request #22 from xixu-me/dependabot/github_actions/github-actions-88dfb18256
actions(deps): bump actions/upload-artifact from 5 to 6 in the github-actions group
2025-12-15 17:46:53 +08:00
dependabot[bot] d1013cfc42 actions(deps): bump actions/upload-artifact in the github-actions group
Bumps the github-actions group with 1 update: [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `actions/upload-artifact` from 5 to 6
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 09:25:28 +00:00
xixu-me a377e1db27 Merge pull request #19 from xixu-me/dependabot/github_actions/github-actions-76468cb07f
actions(deps): bump actions/checkout from 5 to 6 in the github-actions group
2025-11-24 21:23:14 +08:00
dependabot[bot] 0140103194 actions(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 10:19:03 +00:00
xixu-me 7850cbf790 Add English README and update main README with link
Introduced README.en.md providing comprehensive English documentation for the Xget Now extension. Updated the main README.md to include a prominent link to the English version.
2025-11-14 17:43:51 +08:00
xixu-me 94636cae36 Ignore non-code files in CI workflow triggers
Updated the GitHub Actions workflow to ignore changes to documentation, images, and other non-code files when triggering on push and pull_request events. This reduces unnecessary CI runs for changes that do not affect the codebase.
2025-11-14 16:40:57 +08:00
xixu-me 8b2402b14e Revise and simplify README instructions and features
Updated the README to clarify extension usage, simplify installation steps, and condense performance and service requirement sections. Removed redundant details about Xget's internal mechanisms and focused on essential deployment and usage information for end users.
2025-11-14 16:13:35 +08:00
xixu-me e389d9fc56 Update file paths for source structure refactor
Refactor all references to JavaScript, HTML, and compatibility files to use the new 'src/' directory structure in dev.py and the GitHub Actions workflow. This ensures consistency and correctness after reorganizing source files, and updates file existence checks, file watching, and linting logic accordingly.
2025-11-09 18:47:18 +08:00
xixu-me 36ef3969cf Remove required files check from CI workflow
The step that checked for the existence of required files in the test-build GitHub Actions workflow has been removed. This simplifies the workflow and may indicate that the check is no longer necessary or is handled elsewhere.
2025-11-09 18:34:42 +08:00
xixu-me 7dee5da5ed Refactor source structure and update build process
Moved source files into a structured src/ directory for better organization. Updated build.py to copy and flatten files from src/ during the build process for Chrome and Firefox, introducing a new flatten_for_browser function. Improved .gitignore to exclude additional build artifacts. Also reformatted and improved error handling in webext-compat.js.
2025-11-09 18:30:25 +08:00
GitHub Action 00ee7e6592 chore: 更新版本到 2.0.5 [skip ci] 2025-11-09 07:57:51 +00:00
xixu-me 1fb387a32d Add Firefox compatibility layer and improve API detection
Introduces firefox-compat.js to handle Firefox-specific messaging and storage, and updates background.js and popup.js to robustly detect and use the appropriate extension APIs for both Chrome and Firefox environments. Manifest and popup files are updated to load the new compatibility script, ensuring reliable settings management and messaging across browsers.
2025-11-09 15:56:34 +08:00
xixu-me 38e8d54129 Merge pull request #18 from xixu-me/dependabot/github_actions/github-actions-dc500464ed
actions(deps): bump actions/upload-artifact from 4 to 5 in the github-actions group
2025-10-27 21:00:51 +08:00
dependabot[bot] 4f6eff4173 actions(deps): bump actions/upload-artifact in the github-actions group
Bumps the github-actions group with 1 update: [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `actions/upload-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 09:58:46 +00:00
xixu-me bec2770f2e Merge pull request #16 from xixu-me/dependabot/github_actions/github-actions-5e51f74d5c
actions(deps): bump actions/setup-node from 5 to 6 in the github-actions group
2025-10-21 20:01:43 +08:00
dependabot[bot] ca45a98bab actions(deps): bump actions/setup-node in the github-actions group
Bumps the github-actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 5 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 10:25:31 +00:00
xixu-me bf3ef0427c Rename 'Microsoft Edge' to 'Edge' in docs and workflows
Updated all references from 'Microsoft Edge' to 'Edge' across issue templates, pull request templates, workflow files, privacy policy, and README for consistency and brevity.
2025-10-01 16:07:19 +08:00
xixu-me c68c2e0efa Merge pull request #14 from xixu-me/dependabot/github_actions/github-actions-9c09232849
actions(deps): bump the github-actions group with 2 updates
2025-09-08 19:06:16 +08:00
dependabot[bot] bdd7327292 actions(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/setup-python](https://github.com/actions/setup-python) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

Updates `actions/setup-node` from 4 to 5
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 10:07:20 +00:00
xixu-me 1682b76967 Update funding configuration in FUNDING.yml
Added custom sponsorship URL and Buy Me a Coffee username.
2025-08-24 17:42:10 +08:00
GitHub Action b2a96c9027 chore: 更新版本到 2.0.4 [skip ci] 2025-08-24 09:17:33 +00:00
xixu-me a6d62103c1 Move and restyle 'URL 转换器' link in header
The 'URL 转换器' link was moved from the bottom of the popup to the header section and given new styles for improved visibility and consistency with other header elements.
2025-08-24 17:16:36 +08:00
xixu-me 440b27b1b7 Add Chrome Web Store mirror link to README
A mirror link for the Chrome Web Store has been added to the 商店可用性 section to provide an alternative download source.
2025-08-19 19:19:18 +08:00
GitHub Action c0feccdd2b chore: 更新版本到 2.0.3 [skip ci] 2025-08-19 10:57:32 +00:00
xixu-me 1f74a391b7 Add URL converter link to popup menu
A new link to the URL converter (https://xuc.xi-xu.me/) has been added to the popup menu for easier access.
2025-08-19 18:56:54 +08:00
xixu-me a57fddb857 Merge pull request #10 from xixu-me/dependabot/github_actions/github-actions-a331d3ec2d
actions(deps): bump actions/checkout from 4 to 5 in the github-actions group
2025-08-18 21:53:50 +08:00
dependabot[bot] f9abababdb actions(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 13:53:05 +00:00
31 changed files with 3722 additions and 1427 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
custom: https://xi-xu.me/#sponsorships
buy_me_a_coffee: xixu
+1 -1
View File
@@ -24,7 +24,7 @@ body:
description: Which browser are you using?
options:
- Chrome
- Microsoft Edge
- Edge
- Brave
- Other Chromium-based browser
validations:
+1 -1
View File
@@ -40,7 +40,7 @@
## 浏览器测试
<!-- 检查你测试过的浏览器 -->
- [ ] Chrome
- [ ] Microsoft Edge
- [ ] Edge
- [ ] Brave
- [ ] 其他:____________
+129
View File
@@ -0,0 +1,129 @@
name: 自动合并 Dependabot 更新
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions:
contents: write
pull-requests: write
jobs:
enable-auto-merge:
name: 为符合条件的 Dependabot PR 开启自动合并
if: github.event.pull_request.user.login == 'dependabot[bot]' && !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: 读取 Dependabot 元数据
id: metadata
uses: dependabot/fetch-metadata@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: 判断是否允许自动合并
id: eligibility
env:
PACKAGE_ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }}
DEPENDENCY_TYPE: ${{ steps.metadata.outputs.dependency-type }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
run: |
auto_merge=false
reason="该 Dependabot 更新不在自动合并范围内"
if [ "$PACKAGE_ECOSYSTEM" = "github-actions" ]; then
auto_merge=true
reason="允许自动合并 GitHub Actions 更新"
elif [ "$PACKAGE_ECOSYSTEM" = "npm" ] && [ "$DEPENDENCY_TYPE" = "direct:development" ]; then
if [ "$UPDATE_TYPE" = "version-update:semver-patch" ] || [ "$UPDATE_TYPE" = "version-update:semver-minor" ]; then
auto_merge=true
reason="允许自动合并 npm 开发依赖的 patch/minor 更新"
fi
fi
echo "auto_merge=$auto_merge" >> "$GITHUB_OUTPUT"
echo "reason=$reason" >> "$GITHUB_OUTPUT"
echo "$reason"
- name: 自动批准符合条件的 PR
if: steps.eligibility.outputs.auto_merge == 'true'
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const pull_number = context.payload.pull_request.number;
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number,
per_page: 100,
});
const alreadyApproved = reviews.some(
(review) =>
review.user?.login === "github-actions[bot]" &&
review.state === "APPROVED"
);
if (alreadyApproved) {
core.info("PR 已经有 github-actions[bot] 的批准记录,跳过。");
return;
}
await github.rest.pulls.createReview({
owner,
repo,
pull_number,
event: "APPROVE",
body: "自动批准符合策略的 Dependabot 依赖更新。",
});
- name: 开启自动合并
if: steps.eligibility.outputs.auto_merge == 'true'
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pullRequestId = context.payload.pull_request.node_id;
try {
await github.graphql(
`
mutation EnableAutoMerge($pullRequestId: ID!) {
enablePullRequestAutoMerge(
input: {
pullRequestId: $pullRequestId
mergeMethod: SQUASH
}
) {
pullRequest {
number
autoMergeRequest {
enabledAt
}
}
}
}
`,
{ pullRequestId }
);
core.info("已为该 PR 开启自动合并,等待必需检查和分支保护条件满足。");
} catch (error) {
if (String(error.message).includes("Pull request is in clean status")) {
core.info("PR 已经满足条件并可能已启用自动合并,跳过重复设置。");
return;
}
throw error;
}
- name: 输出跳过原因
if: steps.eligibility.outputs.auto_merge != 'true'
run: echo "${{ steps.eligibility.outputs.reason }}"
+204
View File
@@ -0,0 +1,204 @@
name: Dependabot Auto Merge
on:
check_suite:
types: [completed]
workflow_run:
workflows:
- "CodeQL"
- "Dependabot Updates"
- "发布扩展到多个应用商店"
- "测试构建"
- "自动合并 Dependabot 更新"
types:
- completed
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
jobs:
merge:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Merge Dependabot PRs when checks pass
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const successfulCheckConclusions = new Set(["success", "skipped", "neutral"]);
const successfulStatusStates = new Set(["success"]);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const latestBy = (items, keyOf, timeOf) => {
const latest = new Map();
for (const item of items) {
const key = keyOf(item);
const itemTime = new Date(timeOf(item) || 0).getTime();
const existing = latest.get(key);
const existingTime = existing ? new Date(timeOf(existing) || 0).getTime() : -1;
if (!existing || itemTime >= existingTime) {
latest.set(key, item);
}
}
return [...latest.values()];
};
const findCandidatePulls = async () => {
const pulls = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
return pulls.filter((pr) => pr.user?.login === "dependabot[bot]");
};
const getMergeablePullRequest = async (pull_number) => {
for (let attempt = 1; attempt <= 6; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
if (pr.mergeable !== null) {
return pr;
}
core.info("PR #" + pull_number + " mergeability is still being computed; retry " + attempt + "/6.");
await wait(5000);
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
return pr;
};
const getSignalState = async (sha) => {
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const latestChecks = latestBy(
checkRuns.filter((run) => run.name !== "Dependabot Auto Merge"),
(run) => (run.app?.slug || "unknown") + ":" + run.name,
(run) => run.completed_at || run.started_at || run.created_at,
);
const latestStatuses = latestBy(statuses, (status) => status.context, (status) => status.updated_at || status.created_at);
const pendingChecks = latestChecks.filter((run) => run.status !== "completed");
const failedChecks = latestChecks.filter(
(run) => run.status === "completed" && !successfulCheckConclusions.has(String(run.conclusion || "").toLowerCase()),
);
const failedStatuses = latestStatuses.filter((status) => !successfulStatusStates.has(String(status.state || "").toLowerCase()));
return {
totalSignals: latestChecks.length + latestStatuses.length,
pendingChecks,
failedChecks,
failedStatuses,
};
};
const { data: repository } = await github.rest.repos.get({ owner, repo });
const mergeMethods = [];
if (repository.allow_merge_commit) mergeMethods.push("merge");
if (repository.allow_squash_merge) mergeMethods.push("squash");
if (repository.allow_rebase_merge) mergeMethods.push("rebase");
if (mergeMethods.length === 0) {
core.info("This repository has no enabled pull request merge methods.");
return;
}
const pulls = await findCandidatePulls();
if (pulls.length === 0) {
core.info("No open Dependabot PRs to evaluate.");
return;
}
for (const candidate of pulls) {
const pull_number = candidate.number;
const pr = await getMergeablePullRequest(pull_number);
if (pr.user?.login !== "dependabot[bot]") {
core.info("PR #" + pull_number + " is no longer a Dependabot PR.");
continue;
}
if (pr.state !== "open" || pr.draft) {
core.info("PR #" + pull_number + " is not an open, ready PR.");
continue;
}
if (pr.mergeable !== true) {
core.info("PR #" + pull_number + " is not currently mergeable.");
continue;
}
const signalState = await getSignalState(pr.head.sha);
if (signalState.totalSignals === 0) {
core.info("PR #" + pull_number + " has no checks or statuses yet; skipping.");
continue;
}
if (signalState.pendingChecks.length > 0) {
core.info("PR #" + pull_number + " still has pending checks: " + signalState.pendingChecks.map((run) => run.name).join(", ") + ".");
continue;
}
if (signalState.failedChecks.length > 0 || signalState.failedStatuses.length > 0) {
const failedChecks = signalState.failedChecks.map((run) => run.name + "=" + run.conclusion).join(", ");
const failedStatuses = signalState.failedStatuses.map((status) => status.context + "=" + status.state).join(", ");
core.info("PR #" + pull_number + " is not green. Checks: " + (failedChecks || "none") + ". Statuses: " + (failedStatuses || "none") + ".");
continue;
}
let merged = false;
let lastError = null;
for (const merge_method of mergeMethods) {
try {
await github.rest.pulls.merge({ owner, repo, pull_number, merge_method });
core.info("Merged Dependabot PR #" + pull_number + " with " + merge_method + ".");
merged = true;
break;
} catch (error) {
lastError = error;
if (error.status === 403 || error.status === 405 || error.status === 409) {
core.info("Cannot merge PR #" + pull_number + " with " + merge_method + ": " + error.message);
continue;
}
throw error;
}
}
if (!merged) {
core.info("PR #" + pull_number + " could not be merged: " + (lastError?.message || "unknown error") + ".");
continue;
}
if (pr.head.repo?.full_name === owner + "/" + repo) {
try {
await github.rest.git.deleteRef({ owner, repo, ref: "heads/" + pr.head.ref });
core.info("Deleted branch " + pr.head.ref + ".");
} catch (error) {
core.info("Could not delete branch " + pr.head.ref + ": " + error.message);
}
}
}
+60 -47
View File
@@ -21,30 +21,33 @@ jobs:
steps:
# 第一步:检出源代码
- name: 检出代码
uses: actions/checkout@v4
uses: actions/checkout@v7
# 第二步:设置 Python 环境用于构建脚本
- name: 设置 Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"
# 第三步:设置 Node.js 环境用于 JSON 验证
- name: 设置 Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: "18"
- name: 安装 Node.js 依赖
run: npm ci
# 第四步:验证 manifest 文件格式
- name: 验证 manifest 文件
run: |
# 验证 Chrome manifest.json 是否为有效的 JSON
node -e "JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'))"
echo "✅ Chrome manifest.json 格式有效"
echo "[OK] Chrome manifest.json 格式有效"
# 验证 Firefox manifest 是否为有效的 JSON
node -e "JSON.parse(require('fs').readFileSync('manifest-firefox.json', 'utf8'))"
echo "✅ Firefox manifest.json 格式有效"
echo "[OK] Firefox manifest.json 格式有效"
# 第五步:设置版本号
- name: 从输入设置版本
@@ -52,17 +55,23 @@ jobs:
run: |
# 从工作流输入获取版本号
VERSION="${{ inputs.version }}"
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "版本号格式非法: $VERSION"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "手动设置版本: $VERSION"
# 第六步:更新所有配置文件中的版本号
- name: 更新 manifest 版本
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# 更新 Chrome manifest.json 中的版本
node -e "
const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
manifest.version = '${{ steps.version.outputs.version }}';
manifest.version = process.env.VERSION;
fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
console.log('已更新 Chrome manifest.json 版本为:', manifest.version);
"
@@ -71,22 +80,18 @@ jobs:
node -e "
const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync('manifest-firefox.json', 'utf8'));
manifest.version = '${{ steps.version.outputs.version }}';
manifest.version = process.env.VERSION;
fs.writeFileSync('manifest-firefox.json', JSON.stringify(manifest, null, 2));
console.log('已更新 Firefox manifest.json 版本为:', manifest.version);
"
# 第七步:同步更新 package.json 版本
- name: 更新 package.json 版本
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# 更新 package.json 中的版本以保持一致性
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
console.log('已更新 package.json 版本为:', pkg.version);
"
npm version "$VERSION" --no-git-tag-version
echo "已同步 package.json 和 package-lock.json 版本为: $VERSION"
# 第八步:提交版本更改并创建 Git 标签
- name: 提交并推送版本更新
@@ -96,7 +101,7 @@ jobs:
git config --local user.name "GitHub Action"
# 添加更新的文件
git add manifest.json manifest-firefox.json package.json
git add manifest.json manifest-firefox.json package.json package-lock.json
# 检查是否有需要提交的更改
if git diff --staged --quiet; then
@@ -112,31 +117,32 @@ jobs:
git push origin HEAD:main
git push origin "v${{ steps.version.outputs.version }}"
echo "✅ 版本更新已提交并推送到主分支,包含标签"
echo "[OK] 版本更新已提交并推送到主分支,包含标签"
fi
- name: 构建扩展包
run: |
echo "🏗️ 开始构建扩展包..."
echo "开始构建扩展包..."
# 运行构建脚本构建所有平台
python build.py --platform all --package
echo "📦 扩展包构建完成"
echo "扩展包构建完成"
ls -la packages/ || echo "packages 目录不存在"
- name: 运行测试
run: |
echo "🧪 运行基础测试..."
echo "运行基础测试..."
python dev.py test
npm run test:unit
echo "🔍 运行代码检查..."
echo "运行代码检查..."
python dev.py lint
- name: 发布到 Chrome 应用商店
continue-on-error: true
id: chrome-publish
uses: mnao305/chrome-extension-upload@v5.0.0
uses: mnao305/chrome-extension-upload@v6.0.0
with:
file-path: packages/Xget-Now_${{ steps.version.outputs.version }}.chromium.zip
extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
@@ -145,7 +151,7 @@ jobs:
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
publish: true
- name: 发布到 Microsoft Edge 加载项
- name: 发布到 Edge 加载项
continue-on-error: true
id: edge-publish
uses: wdzeng/edge-addon@v2
@@ -171,20 +177,21 @@ jobs:
构建说明:直接从源代码打包,无需编译或转译。
- name: 创建 GitHub 发布
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ steps.version.outputs.version }}
name: Xget Now ${{ steps.version.outputs.version }}
files: |
packages/Xget-Now_${{ steps.version.outputs.version }}.chromium.zip
packages/Xget-Now_${{ steps.version.outputs.version }}.firefox.xpi
packages/Xget-Now_${{ steps.version.outputs.version }}.userscript.user.js
generate_release_notes: true
body: |
## 📦 扩展包下载
## 扩展包下载
### Chromium
- **文件**: `Xget-Now_${{ steps.version.outputs.version }}.chromium.zip`
- **支持**: Chrome、Microsoft Edge 等基于 Chromium 的浏览器
- **支持**: Chrome、Edge 等基于 Chromium 的浏览器
- **Manifest**: v3
### Firefox
@@ -192,11 +199,16 @@ jobs:
- **支持**: Firefox 或其他基于 Firefox 的浏览器
- **Manifest**: v2
## 🚀 安装方法
### Userscript
- **文件**: `Xget-Now_${{ steps.version.outputs.version }}.userscript.user.js`
- **支持**: 用户脚本管理器
- **能力**: GitHub、GitLab、Gitea、Codeberg、Hugging Face、PyPI、npm、SourceForge 下载加速
## 安装方法
详细的安装说明请参考:[手动安装指南](https://github.com/xixu-me/Xget-Now?tab=readme-ov-file#%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85)
## 📝 更新日志
## 更新日志
详细的更改内容请查看下方的自动生成的发布说明。
@@ -205,51 +217,52 @@ jobs:
- name: 检查发布结果
run: |
echo "📊 发布结果摘要:"
echo "发布结果摘要:"
echo "================================"
echo "📦 版本: ${{ steps.version.outputs.version }}"
echo "版本: ${{ steps.version.outputs.version }}"
echo ""
if [ "${{ steps.chrome-publish.outcome }}" = "success" ]; then
echo "✅ Chrome 应用商店: 发布成功"
echo "[OK] Chrome 应用商店: 发布成功"
elif [ "${{ steps.chrome-publish.outcome }}" = "failure" ]; then
echo "❌ Chrome 应用商店: 发布失败"
echo "[FAIL] Chrome 应用商店: 发布失败"
else
echo "⚠️ Chrome 应用商店: 已跳过(缺少密钥或其他原因)"
echo "[WARN] Chrome 应用商店: 已跳过(缺少密钥或其他原因)"
fi
if [ "${{ steps.edge-publish.outcome }}" = "success" ]; then
echo "✅ Microsoft Edge 加载项: 发布成功"
echo "[OK] Edge 加载项: 发布成功"
elif [ "${{ steps.edge-publish.outcome }}" = "failure" ]; then
echo "❌ Microsoft Edge 加载项: 发布失败"
echo "[FAIL] Edge 加载项: 发布失败"
else
echo "⚠️ Microsoft Edge 加载项: 已跳过(缺少密钥或其他原因)"
echo "[WARN] Edge 加载项: 已跳过(缺少密钥或其他原因)"
fi
if [ "${{ steps.firefox-publish.outcome }}" = "success" ]; then
echo "✅ Firefox 附加组件: 发布成功"
echo "[OK] Firefox 附加组件: 发布成功"
elif [ "${{ steps.firefox-publish.outcome }}" = "failure" ]; then
echo "❌ Firefox 附加组件: 发布失败"
echo "[FAIL] Firefox 附加组件: 发布失败"
else
echo "⚠️ Firefox 附加组件: 已跳过(缺少密钥或其他原因)"
echo "[WARN] Firefox 附加组件: 已跳过(缺少密钥或其他原因)"
fi
echo ""
echo "✅ GitHub 发布: 已创建包含所有扩展文件"
echo "[OK] GitHub 发布: 已创建包含所有扩展文件"
echo "================================"
- name: 通知成功
run: |
echo ""
echo "🎉 成功发布 Xget Now ${{ steps.version.outputs.version }}!"
echo "成功发布 Xget Now ${{ steps.version.outputs.version }}"
echo "================================"
echo "🔗 Chrome 应用商店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}"
echo "🔗 Microsoft Edge 加载项: 查看发布页面获取最新信息"
echo "🦊 Firefox 附加组件: 查看 Mozilla Add-ons 获取最新信息"
echo "📦 GitHub 发布: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}"
echo "📖 安装指南: https://github.com/${{ github.repository }}#installation"
echo "Chrome 应用商店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}"
echo "Edge 加载项: 查看发布页面获取最新信息"
echo "Firefox 附加组件: 查看 Mozilla Add-ons 获取最新信息"
echo "GitHub 发布: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}"
echo "安装指南: https://github.com/${{ github.repository }}#installation"
echo ""
echo "📁 发布的文件:"
echo "发布的文件:"
echo " - Xget-Now_${{ steps.version.outputs.version }}.chromium.zip (Chromium)"
echo " - Xget-Now_${{ steps.version.outputs.version }}.firefox.xpi (Firefox)"
echo " - Xget-Now_${{ steps.version.outputs.version }}.userscript.user.js (Userscript)"
echo "================================"
+98 -34
View File
@@ -5,8 +5,26 @@ permissions:
on:
push:
branches: [main, develop]
paths-ignore:
- "**.md"
- "docs/**"
- "LICENSE"
- ".gitignore"
- ".editorconfig"
- "images/**"
- "screenshots/**"
- ".vscode/**"
pull_request:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
- "LICENSE"
- ".gitignore"
- ".editorconfig"
- "images/**"
- "screenshots/**"
- ".vscode/**"
workflow_dispatch:
jobs:
@@ -15,18 +33,21 @@ jobs:
steps:
- name: 检出代码
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: 设置 Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: 设置 Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: "18"
- name: 安装 Node.js 依赖
run: npm ci
- name: 验证 manifest 文件
run: |
echo "验证 Chrome manifest..."
@@ -56,20 +77,25 @@ jobs:
required_files=(
"manifest.json"
"manifest-firefox.json"
"background.js"
"content.js"
"popup.js"
"popup.html"
"webext-compat.js"
"src/background/index.js"
"src/content/index.js"
"src/popup/popup.js"
"src/popup/index.html"
"src/shared/download-core.js"
"src/userscript/index.js"
"src/shared/compat/webext-compat.js"
"src/shared/compat/firefox-compat.js"
"src/shared/platform-detector.js"
"src/shared/platforms.js"
"build.py"
"dev.py"
)
for file in "${required_files[@]}"; do
if [ -f "$file" ]; then
echo "✅ $file"
echo "[OK] $file"
else
echo "❌ $file 缺失"
echo "[FAIL] $file 缺失"
exit 1
fi
done
@@ -78,6 +104,7 @@ jobs:
run: |
echo "运行基础测试..."
python dev.py test
npm run test:unit
- name: 运行代码检查
run: |
@@ -91,10 +118,10 @@ jobs:
# 验证构建结果
if [ -d "build/chrome" ]; then
echo "✅ Chrome 构建目录已创建"
echo "[OK] Chrome 构建目录已创建"
ls -la build/chrome/
else
echo "❌ Chrome 构建失败"
echo "[FAIL] Chrome 构建失败"
exit 1
fi
@@ -105,10 +132,23 @@ jobs:
# 验证构建结果
if [ -d "build/firefox" ]; then
echo "✅ Firefox 构建目录已创建"
echo "[OK] Firefox 构建目录已创建"
ls -la build/firefox/
else
echo "❌ Firefox 构建失败"
echo "[FAIL] Firefox 构建失败"
exit 1
fi
- name: 测试 userscript 构建
run: |
echo "测试 userscript 构建..."
python build.py --platform userscript
if [ -f "build/userscript/xget-now.user.js" ]; then
echo "[OK] userscript 构建文件已生成"
ls -la build/userscript/
else
echo "[FAIL] userscript 构建失败"
exit 1
fi
@@ -116,26 +156,36 @@ jobs:
run: |
echo "测试扩展包生成..."
python build.py --platform all --package
VERSION=$(node -p 'require("./package.json").version')
# 验证扩展包
if [ -d "packages" ]; then
echo "✅ 扩展包目录已创建"
echo "[OK] 扩展包目录已创建"
ls -la packages/
# 检查是否有 Chrome 和 Firefox 包
if [ -f packages/xget-now-chrome-v*.zip ]; then
echo "✅ Chrome 扩展包已生成"
# 检查是否有 Chrome、Firefox 和 userscript 包
if [ -f "packages/Xget-Now_${VERSION}.chromium.zip" ]; then
echo "[OK] Chrome 扩展包已生成"
else
echo "❌ Chrome 扩展包生成失败"
echo "[FAIL] Chrome 扩展包生成失败"
exit 1
fi
if [ -f packages/xget-now-firefox-v*.zip ]; then
echo "✅ Firefox 扩展包已生成"
if [ -f "packages/Xget-Now_${VERSION}.firefox.xpi" ]; then
echo "[OK] Firefox 扩展包已生成"
else
echo "❌ Firefox 扩展包生成失败"
echo "[FAIL] Firefox 扩展包生成失败"
exit 1
fi
if [ -f "packages/Xget-Now_${VERSION}.userscript.user.js" ]; then
echo "[OK] userscript 包已生成"
else
echo "[FAIL] userscript 包生成失败"
exit 1
fi
else
echo "❌ 扩展包生成失败"
echo "[FAIL] 扩展包生成失败"
exit 1
fi
@@ -172,8 +222,21 @@ jobs:
}
"
echo "检查 userscript 构建..."
node -e "
const fs = require('fs');
const userscript = fs.readFileSync('build/userscript/xget-now.user.js', 'utf8');
if (!userscript.includes('@grant GM_xmlhttpRequest')) {
throw new Error('userscript 构建缺少 GM_xmlhttpRequest grant');
}
if (!userscript.includes('@run-at document-start')) {
throw new Error('userscript 构建缺少 document-start 配置');
}
console.log('Userscript 元数据验证通过');
"
- name: 上传构建结果
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: extension-builds-${{ github.sha }}
path: |
@@ -184,15 +247,16 @@ jobs:
- name: 测试总结
run: |
echo ""
echo "🎉 所有测试通过!"
echo "所有测试通过"
echo "================================"
echo "✅ Manifest 文件验证"
echo "✅ 必需文件检查"
echo "✅ 基础测试"
echo "✅ 代码检查"
echo "✅ Chrome 构建"
echo "✅ Firefox 构建"
echo "✅ 扩展包生成"
echo "✅ 构建结果验证"
echo "[OK] Manifest 文件验证"
echo "[OK] 必需文件检查"
echo "[OK] 基础测试"
echo "[OK] 代码检查"
echo "[OK] Chrome 构建"
echo "[OK] Firefox 构建"
echo "[OK] userscript 构建"
echo "[OK] 扩展包生成"
echo "[OK] 构建结果验证"
echo "================================"
echo "🚀 准备发布!"
echo "准备发布"
+2
View File
@@ -7,7 +7,9 @@ yarn-error.log*
# Build outputs
dist/
build/
packages/
*.zip
*.xpi
extension.zip
# OS generated files
+1 -1
View File
@@ -46,7 +46,7 @@ Xget Now 扩展遵循“隐私优先”的设计原则,所有处理都在您
### 数据同步
如果您已登录 Chrome/Microsoft Edge 并启用了扩展同步,您的扩展设置可能会在您的设备之间同步。这是由 Google/Microsoft 的 Chrome/Microsoft Edge 同步服务处理的,不涉及我们的服务器。
如果您已登录 Chrome/Edge 并启用了扩展同步,您的扩展设置可能会在您的设备之间同步。这是由 Google/Microsoft 的 Chrome/Edge 同步服务处理的,不涉及我们的服务器。
## 数据处理
+244
View File
@@ -0,0 +1,244 @@
# Xget Now
***[汉语](README.md)***
A cross-browser extension for Chromium and Firefox that accelerates file downloads by seamlessly forwarding them to an [Xget](https://github.com/xixu-me/Xget) instance.
> [!NOTE]
> Quick start: install the store version, enter your Xget domain such as `xget.xi-xu.me`, and leave the target platform toggles enabled.
## Overview
Xget Now detects real download links on supported websites, rewrites them to your Xget domain, and keeps the browser's native download flow intact. You do not need to modify the original site or manually copy download URLs.
## Features
- Automatic download acceleration: seamlessly redirect downloads through Xget for faster speeds.
- Configurable settings: customize your Xget domain and platform preferences.
- Smart notifications: visual feedback when downloads are redirected.
- Privacy-first: all processing happens locally in your browser.
- Per-platform control: enable or disable acceleration for specific platforms.
- Cross-browser support: works on both Chromium and Firefox browsers.
## Supported Platforms
- Code hosting: GitHub, GitLab, Gitea, Codeberg, SourceForge, AOSP.
- AI / ML: Hugging Face.
- Package and distribution platforms: npm, PyPI, PyPI Files, Conda, Conda Community, Maven Central, Apache Downloads, Gradle Plugin Portal, RubyGems, CRAN, CPAN, CTAN, Go Modules, NuGet, Crates.io, Packagist.
- Other platforms: arXiv, F-Droid.
## Installation
### Store Availability
- [Chrome Web Store](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=en)
- [Chrome Web Store Mirror](https://chromewebstore.xi-xu.me/detail/ajiejgobfcifcikbahpijopolfjoodgf)
- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/xget-now/)
- [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc)
### Manual Installation
If you prefer manual installation, or if a store listing is unavailable, you can install Xget Now from GitHub releases or directly from source.
#### Option 1: Install from GitHub Releases
1. Go to the [Releases page](https://github.com/xixu-me/Xget-Now/releases/latest).
2. Download the package for your browser:
- `Xget-Now_x.x.x.chromium.zip`: for Chromium-based browsers such as Chrome, Edge, and Opera.
- `Xget-Now_x.x.x.firefox.xpi`: for Firefox.
3. Install in Chrome:
- Extract the Chromium `.zip` package.
- Open `chrome://extensions/`.
- Enable "Developer mode" in the top-right corner.
- Click "Load unpacked" and select the extracted folder.
4. Install in Firefox:
- Open `about:debugging`.
- Click "This Firefox".
- Click "Load Temporary Add-on".
- Select the Firefox `.xpi` file, or the `manifest.json` file from an extracted package.
5. Install in Edge:
- Extract the Chromium `.zip` package.
- Open `edge://extensions/`.
- Enable "Developer mode" in the left sidebar.
- Click "Load unpacked" and select the extracted folder.
6. Install in other Chromium-based browsers:
- Use the Chromium package.
- Enable that browser's developer mode first.
- Follow the browser-specific extension loading instructions.
#### Option 2: Install from Source
1. Clone the repository:
```bash
git clone https://github.com/xixu-me/Xget-Now.git
cd Xget-Now
```
2. Build the extension if needed:
`build.py` only uses the Python standard library, so Python 3.7+ is enough.
```bash
python build.py --platform chrome
python build.py --platform firefox
python build.py --platform all
python build.py --platform all --package
```
3. Load it in the browser:
- Chromium browsers:
- Open `chrome://extensions/` or `edge://extensions/`.
- Enable "Developer mode".
- Click "Load unpacked" and select either the repository root, or `build/chrome/` if you built the packaged version.
- Firefox:
- Open `about:debugging`.
- Click "This Firefox".
- Click "Load Temporary Add-on".
- Select `manifest-firefox.json` from the repository root, or `build/firefox/manifest.json` from a build output.
#### Verify Installation
After installation, you should see the Xget Now icon in the browser toolbar. Click it, configure your Xget domain, and start accelerating downloads.
## Configuration
1. Click the extension icon in the browser toolbar.
2. Enter your Xget domain, for example `xget.xi-xu.me`.
3. Enable the extension and toggle supported platforms on or off as needed.
4. Visit any supported platform and click download links as usual.
5. Downloads will automatically be routed through Xget.
## How It Works
1. Detection: the extension monitors download links on supported platforms.
2. Transformation: URLs are converted to an Xget-compatible format.
3. Redirection: downloads are routed through your configured Xget domain.
4. Acceleration: Xget's CDN and optimization features provide faster download speeds.
## Requirements
### Browser Support
- Chrome: 88+.
- Edge: 88+.
- Opera: 74+.
- Other Chromium-based browsers: versions with Manifest V3 support.
- Firefox: 109+.
- Firefox ESR: 109+.
### Xget Instance
Use the pre-deployed instance `xget.xi-xu.me`, or deploy your own by following the [Xget deployment documentation](https://github.com/xixu-me/Xget#-deployment).
## Privacy and Security
- Local processing: all URL transformations happen in your browser.
- No data collection: the extension does not collect or transmit personal data.
- Minimal permissions: it only requests permissions needed for the feature set.
- Open source: the full source code is available for inspection.
See the [Privacy Policy](PRIVACY_POLICY.md) for complete details.
## Troubleshooting
### Common Issues
**Extension not working?**
- Ensure you configured a valid Xget domain.
- Check whether the extension is enabled in the popup.
- Verify that the target platform is enabled.
- Refresh the page after changing settings.
**Downloads not being redirected?**
- Refresh the page after changing settings.
- Check the browser console for error messages (`F12` -> Console).
- Ensure the link is a recognized download type.
- Confirm that you clicked an actual download link rather than a navigation link.
**Xget domain issues?**
- Enter the domain without the `https://` protocol.
- Example: `xget.xi-xu.me`, not `https://xget.xi-xu.me`.
- Check whether the domain is reachable in your browser.
- Try the default pre-deployed instance first: `xget.xi-xu.me`.
**Performance issues?**
- Check your network connection.
- Try another Xget domain if one is available.
- Verify that the target platform is responding normally.
- Clear the browser cache and reload the extension.
**Firefox-specific issues?**
- Ensure Firefox is version 109 or newer.
- Check whether the extension is correctly loaded in `about:debugging`.
- Firefox may require a restart to fully apply extension settings.
- Review Firefox Privacy & Security settings to make sure they are not blocking extension behavior.
### Debug Mode
In Chromium browsers, open DevTools and inspect the Console tab. In Firefox, open `about:debugging`, click "This Firefox", find Xget Now, click "Inspect", and review the console output there.
Common debug messages:
- Extension loaded: `Xget Now: Content script loaded`
- Download redirect: `Redirecting download: [original] -> [Xget]`
- Settings changed: `Settings updated! Click to refresh page`
## FAQ
### Is this extension free?
Yes. The extension is completely free and open source under GPL-3.0.
### Can I use my own Xget server?
Yes. You can deploy your own Xget instance from the [Xget repository](https://github.com/xixu-me/Xget) and point the extension to your own domain.
### Why are some downloads still going through the original server?
The extension only redirects recognized download links. Navigation links, preview links, and some dynamic content may not be redirected.
### Will my browsing data be collected?
No. The extension runs entirely locally, and no browsing data is collected or transmitted. See the [Privacy Policy](PRIVACY_POLICY.md) for details.
### How much faster are the downloads?
Speed improvements depend on your location, network conditions, and file size. Typical gains range from roughly 2x to 10x.
### Can I disable the extension for specific websites?
Yes. Use the per-platform toggle switches in the popup to disable acceleration for specific supported platforms.
### Why are there different versions for Chrome and Firefox?
The two versions are optimized for different browser architectures:
- Chrome version: uses Manifest V3 and newer extension APIs.
- Firefox version: uses Manifest V2 for Firefox compatibility.
The core behavior remains the same, while the implementation details differ slightly.
### Can I use it in Chrome and Firefox at the same time?
Yes. You can install Xget Now in multiple browsers at once, and each browser keeps its own settings.
## Support
If Xget Now is useful to you, consider:
- Starring the repository.
- Leaving a review:
- [Chrome Web Store](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=en)
- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/xget-now/)
- [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc)
- Reporting bugs or suggesting features via [GitHub Issues](https://github.com/xixu-me/Xget-Now/issues).
- Sharing it with others who might benefit from faster downloads.
> [!NOTE]
> This repository is licensed under GPL-3.0. See [LICENSE](LICENSE) for the full text.
+148 -196
View File
@@ -1,257 +1,206 @@
# Xget Now
为 Chromium 和 Firefox 浏览器提供的 [Xget](https://github.com/xixu-me/Xget) 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速。
***[English](README.en.md)***
## 🚀 功能特性
一个适用于 Chromium 和 Firefox 的跨浏览器扩展,通过将文件下载无缝转交给 [Xget](https://github.com/xixu-me/Xget) 实例来加速下载。
- **🎯 自动下载加速**:无缝重定向下载通过 Xget 获得更快速度
- **⚙️ 可配置设置**:自定义你的 Xget 域名和平台偏好
- **🔔 智能通知**:下载重定向时的可视化反馈
- **🛡️ 隐私优先**:所有处理都在你的浏览器本地进行
- **🎛️ 按平台控制**:为特定平台启用/禁用加速
- **🌐 跨浏览器支持**:同时支持 Chromium 和 Firefox 浏览器
> [!TIP]
> 欢迎加入“Xget 开源与 AI 交流群”,一起交流开源项目、AI 应用、工程实践、效率工具和独立开发;如果你也在做产品、写代码、折腾项目或者对开源和 AI 感兴趣,欢迎[**进群**](https://file.xi-xu.me/QR%20Codes/%E7%BE%A4%E4%BA%8C%E7%BB%B4%E7%A0%81.png)认识更多认真做事、乐于分享的朋友。
## 📦 安装
> [!NOTE]
> 想快速开始:安装商店版本,填入你的 Xget 域名,例如 `xget.xi-xu.me`,保持目标平台开关开启即可。
### 商店可用性
## 概览
Xget Now 会在受支持的平台上识别真实下载链接,把请求重写到你的 Xget 域名,并继续使用浏览器原生下载流程。你不需要改动原始站点,也不需要手动复制链接。
## 功能特性
- 自动下载加速:无缝重定向下载,通过 Xget 获得更快速度。
- 可配置设置:自定义你的 Xget 域名和平台偏好。
- 智能通知:下载重定向时提供可视化反馈。
- 隐私优先:所有处理都在你的浏览器本地进行。
- 按平台控制:为特定平台启用或禁用加速。
- 跨浏览器支持:同时支持 Chromium 和 Firefox 浏览器。
## 支持的平台
- 代码托管平台:GitHub、GitLab、Gitea、Codeberg、SourceForge、AOSP。
- AI / ML 平台:Hugging Face。
- 包管理与分发平台:npm、PyPI、PyPI Files、Conda、Conda Community、Maven Central、Apache Downloads、Gradle Plugin Portal、RubyGems、CRAN、CPAN、CTAN、Go Modules、NuGet、Crates.io、Packagist。
- 其他平台:arXiv、F-Droid。
## 安装
### 商店安装
- [Chrome 应用商店](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=zh-CN)
- [Chrome 应用商店镜像](https://chromewebstore.xi-xu.me/detail/ajiejgobfcifcikbahpijopolfjoodgf)
- [Firefox 附加组件](https://addons.mozilla.org/zh-CN/firefox/addon/xget-now/)
- [Microsoft Edge 加载项](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
- [Edge 加载项](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
### 手动安装
如果你更喜欢手动安装扩展或应用商店版本不可用,你可以直接下载并安装:
如果你更喜欢手动安装扩展,或者应用商店版本不可用,可以直接下载发布包或从源码加载。
#### 选项 1:从 GitHub Releases 安装(推荐)
#### 方式 1:从 GitHub Releases 安装
1. **下载扩展**
- 前往 [Releases 页面](https://github.com/xixu-me/Xget-Now/releases/latest)
- 下载对应浏览器的扩展文件:
- `Xget-Now_x.x.x.chromium.zip` - 适用于所有基于 Chromium 的浏览器(Chrome、Microsoft Edge、Opera 等)
- `Xget-Now_x.x.x.firefox.zip` - 适用于 Firefox 浏览器
1. 前往 [Releases 页面](https://github.com/xixu-me/Xget-Now/releases/latest)。
2. 下载对应浏览器的扩展文件:
- `Xget-Now_x.x.x.chromium.zip`:适用于所有基于 Chromium 的浏览器,例如 Chrome、Edge、Opera。
- `Xget-Now_x.x.x.firefox.xpi`:适用于 Firefox 浏览器。
3. 在 Chrome 中安装:
- 解压 Chromium 版本 `.zip` 文件。
- 打开 `chrome://extensions/`。
- 启用右上角的“开发者模式”。
- 点击“加载已解压的扩展程序”,选择解压后的文件夹。
4. 在 Firefox 中安装:
- 打开 `about:debugging`。
- 点击“此 Firefox”。
- 点击“临时载入附加组件”。
- 选择下载的 Firefox `.xpi` 文件,或解压后目录中的 `manifest.json` 文件。
5. 在 Edge 中安装:
- 解压 Chromium 版本 `.zip` 文件。
- 打开 `edge://extensions/`。
- 启用左侧边栏中的“开发人员模式”。
- 点击“加载解压缩的扩展”,选择解压后的文件夹。
6. 在其他基于 Chromium 的浏览器中安装:
- 使用 Chromium 版本的扩展包。
- 先启用浏览器的“开发者模式”。
- 按照对应浏览器的扩展安装指南完成加载。
2. **在 Chrome 中安装**
- 解压下载的 Chromium 版本 ZIP 文件
- 打开 Chrome 并前往 `chrome://extensions/`
- 启用「开发者模式」(右上角的切换开关)
- 点击「加载已解压的扩展程序」并选择解压的文件夹
#### 方式 2:从源码安装
3. **在 Firefox 中安装**
- 打开 Firefox 并前往 `about:debugging`
- 点击「此 Firefox」
- 点击「临时载入附加组件」
- 选择下载的 Firefox 版本 ZIP 文件或解压后的 `manifest.json` 文件
4. **在 Microsoft Edge 中安装**
- 解压下载的 Chromium 版本 ZIP 文件
- 打开 Microsoft Edge 并前往 `edge://extensions/`
- 启用「开发人员模式」(左侧边栏的切换开关)
- 点击「加载解压缩的扩展」并选择解压的文件夹
5. **在其他基于 Chromium 的浏览器中安装**
- 使用 Chromium 版本的扩展包
- 遵循与 Chrome 或 Microsoft Edge 类似的步骤,确保首先启用「开发者模式」
- 按照相应浏览器的扩展安装指南进行操作
#### 选项 2:从源码安装
1. **克隆存储库**
1. 克隆仓库:
```bash
git clone https://github.com/xixu-me/Xget-Now.git
cd Xget-Now
```
2. **构建扩展(可选)**
2. 可选构建:
如果你想使用优化过的版本:
`build.py` 仅依赖 Python 标准库,安装 Python 3.7+ 后即可运行。
```bash
# 安装 Python 3.7+
pip install -r requirements.txt # 如果有依赖文件
# 构建特定浏览器版本
python build.py --platform chrome # 构建 Chrome 版本
python build.py --platform firefox # 构建 Firefox 版本
python build.py --platform all # 构建所有版本
python build.py --platform chrome
python build.py --platform firefox
python build.py --platform all
python build.py --platform all --package
```
3. **在浏览器中加载**
**Chromium 浏览器:**
- 打开浏览器的扩展管理页面(`chrome://extensions/` 或 `edge://extensions/`)
- 启用「开发者模式」
- 点击「加载已解压的扩展程序」并选择:
- 源码根目录(如果使用原始源码)
- `build/chrome/` 目录(如果使用构建版本)
**Firefox 浏览器:**
- 打开 `about:debugging`
- 点击「此 Firefox」
- 点击「临时载入附加组件」
- 选择以下文件之一:
- 源码根目录中的 `manifest-firefox.json`
- `build/firefox/` 目录中的 `manifest.json`(如果使用构建版本)
3. 在浏览器中加载:
- Chromium 浏览器:
- 打开 `chrome://extensions/` 或 `edge://extensions/`。
- 启用“开发者模式”。
- 点击“加载已解压的扩展程序”,选择源码根目录,或选择构建后的 `build/chrome/` 目录。
- Firefox 浏览器:
- 打开 `about:debugging`。
- 点击“此 Firefox”。
- 点击“临时载入附加组件”。
- 选择源码根目录中的 `manifest-firefox.json`,或构建后的 `build/firefox/manifest.json`。
#### 验证安装
安装后,你应该在浏览器工具栏中看到 Xget 扩展图标。点击它来配置你的 Xget 域名并开始加速下载!
安装后,你应该能在浏览器工具栏中看到 Xget Now 图标。点击图标,配置你的 Xget 域名并开始加速下载。
## ⚙️ 设置
## 上手配置
1. **配置 Xget 域名**
- 点击工具栏中的扩展图标
- 输入你的 Xget 域名(例如 `xget.xi-xu.me`)
- 启用扩展
1. 点击工具栏中的扩展图标。
2. 输入你的 Xget 域名,例如 `xget.xi-xu.me`。
3. 启用扩展,并按需打开或关闭各个平台的开关。
4. 访问任何受支持的平台,像往常一样点击下载链接。
5. 下载会自动通过 Xget 转发和加速。
2. **选择平台**
- 根据需要切换各个平台的开关
- 默认启用所有平台
## 工作原理
3. **开始下载**
- 访问任何受支持的平台
- 像往常一样点击下载链接
- 下载将自动通过 Xget 加速
1. 检测:扩展监控受支持平台上的下载链接。
2. 转换:URL 自动转换为 Xget 兼容格式。
3. 重定向:下载通过你配置的 Xget 域名路由。
4. 加速:Xget 的全球 CDN 和优化能力提供更快的下载速度。
## 🔧 工作原理
1. **检测**:扩展监控受支持平台上的下载链接
2. **转换**:URL 自动转换为 Xget 兼容格式
3. **重定向**:下载通过你配置的 Xget 域名路由
4. **加速**:Xget 的全球 CDN 和优化功能提供更快的下载速度
### 为什么 Xget 让下载更快
- **边缘缓存**:文件在 Cloudflare 的全球边缘位置进行缓存
- **HTTP/3 支持**:使用最新协议提高性能
- **智能路由**:自动通过最快的可用服务器路由
- **压缩**:自动内容压缩减少传输时间
- **连接优化**:持久连接和预连接功能
## 📈 性能优势
使用 Xget 的实际性能改进:
- **GitHub 发布**:下载速度提升高达 5 倍
- **大型存储库**:多 GB 存档的显著改进
- **国际用户**:通过边缘缓存显著加快下载速度
- **不稳定连接**:自动重试机制处理网络问题
- **并发下载**:与直接平台下载相比,没有速率限制
## 📋 要求
## 运行要求
### 浏览器支持
**Chromium 浏览器:**
- Chrome:88+。
- Edge:88+。
- Opera:74+。
- 其他基于 Chromium 的浏览器:支持 Manifest V3 的版本。
- Firefox:109+。
- Firefox ESR:109+。
- **Chrome**:版本 88+(Manifest V3 支持)
- **Microsoft Edge**:版本 88+
- **Opera**:版本 74+
- **其他基于 Chromium 的浏览器**:支持 Manifest V3 的版本
### Xget 实例
**Firefox 浏览器:**
你可以直接使用预部署实例 `xget.xi-xu.me`,也可以参考 [Xget 部署文档](https://github.com/xixu-me/Xget#-%E9%83%A8%E7%BD%B2) 自行部署。
- **Firefox**:版本 109+(Manifest V2 支持)
- **Firefox ESR**:版本 109+
## 隐私与安全
### Xget 服务要求
- 本地处理:所有 URL 转换都在你的浏览器中进行。
- 无数据收集:扩展不收集或传输个人数据。
- 最小权限:仅请求功能所需的必要权限。
- 开源可审计:完整源码可供检查。
- **Xget 服务**:访问 Xget 实例
- 使用公共实例:`xget.xi-xu.me`
- 或部署你自己的:[Xget 存储库](https://github.com/xixu-me/Xget)
查看 [隐私政策](PRIVACY_POLICY.md) 了解完整细节。
### 关于 Xget 服务
Xget 是基于 Cloudflare Workers 构建的高性能、安全的代理服务,提供:
- **全球边缘分发**:通过 Cloudflare 的全球网络加快下载速度
- **高级功能**:HTTP/3 支持、智能缓存、自动重试
- **安全性**:全面的安全标头和内容保护
- **可靠性**:内置超时保护和性能监控
在 [xget.xi-xu.me](https://xget.xi-xu.me) 或 [Xget 存储库](https://github.com/xixu-me/Xget) 了解更多。
## 🔒 隐私与安全
- **本地处理**:所有 URL 转换都在你的浏览器中进行
- **无数据收集**:扩展不收集或传输个人数据
- **最小权限**:仅请求功能所需的必要权限
- **开源**:完整源码可供检查
查看我们的 [隐私政策](PRIVACY_POLICY.md) 了解完整详情。
## 🐛 故障排除
## 故障排除
### 常见问题
**扩展不工作?**
- 确保你已配置有效的 Xget 域名
- 检查扩展在弹出窗口中是否已启用
- 验证目标平台是否已启用
- 更改设置后尝试刷新页面
- 确保你已配置有效的 Xget 域名。
- 检查扩展在弹出窗口中是否已启用。
- 验证目标平台是否已启用。
- 更改设置后尝试刷新页面。
**下载没有被重定向?**
- 更改设置后刷新页面
- 检查浏览器控制台的错误消息(F12 → 控制台)
- 确保链接是被识别的下载类型
- 验证你点击的是实际的下载链接,而不是导航链接
- 更改设置后刷新页面。
- 检查浏览器控制台的错误消息(`F12` -> “控制台”)。
- 确保链接是被识别的下载类型。
- 验证你点击的是实际下载链接,而不是导航链接。
**Xget 域名问题?**
- 域名应该不带 `https://` 协议
- 示例:`xget.xi-xu.me`(不是 `https://xget.xi-xu.me`)
- 检查域名在浏览器中是否可访问
- 尝试使用默认的公共实例:`xget.xi-xu.me`
- 域名应该不带 `https://` 协议。
- 示例:`xget.xi-xu.me`,而不是 `https://xget.xi-xu.me`。
- 检查域名在浏览器中是否可访问。
- 可以先尝试默认的预部署实例:`xget.xi-xu.me`。
**性能问题?**
- 检查你的网络连接
- 如果可用,尝试其他 Xget 域名
- 验证目标平台的服务器是否响应
- 清除浏览器缓存并重新加载扩展
- 检查你的网络连接。
- 如果可用,尝试其他 Xget 域名。
- 验证目标平台的服务器是否响应。
- 清除浏览器缓存并重新加载扩展。
**Firefox 特定问题?**
- 确保 Firefox 版本为 109+
- 在 `about:debugging` 中检查扩展是否正确加载
- Firefox 可能需要重启浏览器才能完全应用扩展设置
- 检查 Firefox 的「隐私和安全」设置是否阻止了扩展功能
- 确保 Firefox 版本为 109+。
- 在 `about:debugging` 中检查扩展是否正确加载。
- Firefox 可能需要重启浏览器才能完全应用扩展设置。
- 检查 Firefox 的“隐私和安全”设置是否阻止了扩展功能。
### 调试模式
**Chromium 浏览器:**
Chromium 浏览器中,打开开发者工具并查看 Console 选项卡。Firefox 中,打开 `about:debugging`,点击“此 Firefox”,找到 Xget Now 扩展并点击“检查”,然后查看控制台消息。
启用 Chrome 开发者工具并检查控制台选项卡中的调试消息:
常见调试消息:
**Firefox 浏览器:**
- 扩展加载:`Xget Now: Content script loaded`
- 下载重定向:`Redirecting download: [original] -> [Xget]`
- 设置更改:`Settings updated! Click to refresh page`
1. 打开 `about:debugging`
2. 点击「此 Firefox」
3. 找到 Xget Now 扩展并点击「检查」
4. 在打开的开发者工具中查看控制台消息
**通用调试消息:**
- 扩展加载:「Xget Now:内容脚本已加载」
- 下载重定向:「重定向下载:[原始] -> [Xget]」
- 设置更改:「设置已更新!点击刷新页面」
## 📄 许可证
本存储库采用 GPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## ❓ 常见问题
## 常见问题
### 这个扩展是免费的吗?
是的,扩展完全免费且在 GPL-3.0 许可证下开源。
是的,扩展完全免费,并且以 GPL-3.0 许可证开源。
### 我可以使用自己的 Xget 服务器吗?
当然可以!你可以使用 [Xget 存储库](https://github.com/xixu-me/Xget) 部署自己的 Xget 实例,并配置扩展使用你的域名。
当然可以。你可以使用 [Xget 存储库](https://github.com/xixu-me/Xget) 部署自己的 Xget 实例,并配置扩展使用你的域名。
### 为什么有些下载仍然通过原始服务器?
@@ -259,37 +208,40 @@ Xget 是基于 Cloudflare Workers 构建的高性能、安全的代理服务,
### 我的浏览数据会被收集吗?
不会,扩展完全在本地运行。不收集或传输浏览数据。查看我们的 [隐私政策](PRIVACY_POLICY.md) 了解详情。
不会。扩展完全在本地运行,不收集或传输浏览数据。更多说明见 [隐私政策](PRIVACY_POLICY.md)。
### 下载能快多少?
速度提升因你的位置、网络和文件大小而异。典型的改进范围从 2 倍到 10 倍。
速度提升取决于你的地理位置、网络状况和文件大小。典型改进范围约为 2 倍到 10 倍。
### 我可以为特定网站禁用扩展吗?
是的,使用扩展弹出窗口中的按平台切换开关来禁用特定平台的加速。
可以。使用扩展弹出窗口中的按平台开关,即可为特定平台禁用加速。
### 为什么 Chrome 和 Firefox 版本不同?
由于浏览器架构差异,我们针对不同浏览器进行了优化:
由于浏览器架构差异,我们分别做了适配:
- **Chrome 版本**:使用 Manifest V3,支持最新的扩展 API 和安全特性
- **Firefox 版本**:使用 Manifest V2,确保与 Firefox 扩展系统的最佳兼容性
- Chrome 版本:使用 Manifest V3,支持较新的扩展 API 和安全特性。
- Firefox 版本:使用 Manifest V2,以确保与 Firefox 扩展系统的兼容性。
两个版本的核心功能完全相同,只是技术实现略有不同。
两者的核心功能保持一致,差异主要在底层实现。
### 可以同时在 Chrome 和 Firefox 中使用吗?
当然可以!你可以在多个浏览器中同时安装和使用 Xget Now,它们会独立工作,各自维护自己的设置。
可以。你可以在多个浏览器中同时安装和使用 Xget Now,它们会独立工作,并分别保存各自的设置。
## 🌟 支持我们
## 支持与反馈
如果你觉得这个扩展有用,请:
如果你觉得这个扩展有用,可以:
- ⭐ 为此存储库点星
- 📝 留下评价:
- 为仓库点个 Star。
- 在应用商店留下评价:
- [Chrome 应用商店](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=zh-CN)
- [Firefox 附加组件](https://addons.mozilla.org/zh-CN/firefox/addon/xget-now/)
- [Microsoft Edge 加载项](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
- 🐛 通过 [GitHub Issues](https://github.com/xixu-me/Xget-Now/issues) 报告错误或建议功能
- 📢 与其他可能受益于更快下载的人分享
- [Edge 加载项](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
- 通过 [GitHub Issues](https://github.com/xixu-me/Xget-Now/issues) 报告错误或建议功能。
- 分享给其他可能受益于更快下载的人。
> [!NOTE]
> 本仓库采用 GPL-3.0 许可证。完整条款见 [LICENSE](LICENSE)。
-360
View File
@@ -1,360 +0,0 @@
/**
* Xget Now - 扩展后台脚本
*
* 功能:
* - 处理扩展设置管理
* - 监听标签页更新事件
* - 处理来自内容脚本的消息
* - 管理扩展状态和配置
*/
// 导入兼容层
importScripts("webext-compat.js");
// 确保兼容层可用
if (typeof webext === "undefined") {
// 在非模块环境中,兼容层应该已经通过脚本标签加载
console.error("WebExt compatibility layer not found");
}
/**
* 平台配置定义
* 支持的下载加速平台列表
*/
const PLATFORMS = {
// 代码托管平台
gh: {
base: "https://github.com",
name: "GitHub",
pattern: /^https:\/\/github\.com\//,
},
gl: {
base: "https://gitlab.com",
name: "GitLab",
pattern: /^https:\/\/gitlab\.com\//,
},
gitea: {
base: "https://gitea.com",
name: "Gitea",
pattern: /^https:\/\/gitea\.com\//,
},
codeberg: {
base: "https://codeberg.org",
name: "Codeberg",
pattern: /^https:\/\/codeberg\.org\//,
},
sf: {
base: "https://sourceforge.net",
name: "SourceForge",
pattern: /^https:\/\/sourceforge\.net\//,
},
aosp: {
base: "https://android.googlesource.com",
name: "AOSP",
pattern: /^https:\/\/android\.googlesource\.com\//,
},
// AI/ML 平台
hf: {
base: "https://huggingface.co",
name: "Hugging Face",
pattern: /^https:\/\/huggingface\.co\//,
},
// 包管理平台
npm: {
base: "https://registry.npmjs.org",
name: "npm",
pattern: /^https:\/\/registry\.npmjs\.org\//,
},
pypi: {
base: "https://pypi.org",
name: "PyPI",
pattern: /^https:\/\/pypi\.org\//,
},
"pypi-files": {
base: "https://files.pythonhosted.org",
name: "PyPI Files",
pattern: /^https:\/\/files\.pythonhosted\.org\//,
},
conda: {
base: "https://repo.anaconda.com",
name: "Conda",
pattern: /^https:\/\/repo\.anaconda\.com\//,
},
"conda-community": {
base: "https://conda.anaconda.org",
name: "Conda Community",
pattern: /^https:\/\/conda\.anaconda\.org\//,
},
maven: {
base: "https://repo1.maven.org",
name: "Maven",
pattern: /^https:\/\/repo1\.maven\.org\//,
},
apache: {
base: "https://downloads.apache.org",
name: "Apache",
pattern: /^https:\/\/downloads\.apache\.org\//,
},
gradle: {
base: "https://plugins.gradle.org",
name: "Gradle",
pattern: /^https:\/\/plugins\.gradle\.org\//,
},
rubygems: {
base: "https://rubygems.org",
name: "RubyGems",
pattern: /^https:\/\/rubygems\.org\//,
},
cran: {
base: "https://cran.r-project.org",
name: "CRAN",
pattern: /^https:\/\/cran\.r-project\.org\//,
},
cpan: {
base: "https://www.cpan.org",
name: "CPAN",
pattern: /^https:\/\/www\.cpan\.org\//,
},
ctan: {
base: "https://tug.ctan.org",
name: "CTAN",
pattern: /^https:\/\/tug\.ctan\.org\//,
},
golang: {
base: "https://proxy.golang.org",
name: "Go Modules",
pattern: /^https:\/\/proxy\.golang\.org\//,
},
nuget: {
base: "https://api.nuget.org",
name: "NuGet",
pattern: /^https:\/\/api\.nuget\.org\//,
},
crates: {
base: "https://crates.io",
name: "Crates.io",
pattern: /^https:\/\/crates\.io\//,
},
packagist: {
base: "https://repo.packagist.org",
name: "Packagist",
pattern: /^https:\/\/repo\.packagist\.org\//,
},
// 其他平台
arxiv: {
base: "https://arxiv.org",
name: "arXiv",
pattern: /^https:\/\/arxiv\.org\//,
},
fdroid: {
base: "https://f-droid.org",
name: "F-Droid",
pattern: /^https:\/\/f-droid\.org\//,
},
};
// 转换URL为Xget格式
function transformUrl(url, xgetDomain, enabledPlatforms) {
try {
const platform = detectPlatform(url);
if (!platform || !enabledPlatforms[platform]) {
return null;
}
const urlObj = new URL(url);
const path = urlObj.pathname + urlObj.search + urlObj.hash;
return `https://${xgetDomain}/${platform}${path}`;
} catch (error) {
console.error("转换 URL 时出错:", error);
return null;
}
}
// 检测平台
function detectPlatform(url) {
for (const [key, platform] of Object.entries(PLATFORMS)) {
if (platform.pattern.test(url)) {
return key;
}
}
return null;
}
const DEFAULT_SETTINGS = {
enabled: true,
xgetDomain: "xget.xi-xu.me",
enabledPlatforms: {
// 代码托管平台
gh: true,
gl: true,
gitea: true,
codeberg: true,
sf: true,
aosp: true,
// AI/ML 平台
hf: true,
// 包管理平台
npm: true,
pypi: true,
"pypi-files": true,
conda: true,
"conda-community": true,
maven: true,
apache: true,
gradle: true,
rubygems: true,
cran: true,
cpan: true,
ctan: true,
golang: true,
nuget: true,
crates: true,
packagist: true,
// 其他平台
arxiv: true,
fdroid: true,
},
};
// 初始化扩展
webext.runtime.onInstalled.addListener(async () => {
console.log("Xget Now 已安装");
// 使用本地存储而不是同步存储以确保兼容性
const storageAPI = webext.storage.sync || webext.storage.local;
// 如果尚未设置,则设置默认设置
const settings = await storageAPI.get(DEFAULT_SETTINGS);
await storageAPI.set(settings);
});
// 防止监听器被多次注册
let downloadListenerAdded = false;
// 监听下载事件
if (!downloadListenerAdded && webext.downloads.onDeterminingFilename) {
webext.downloads.onDeterminingFilename.addListener(handleDownload);
downloadListenerAdded = true;
}
function handleDownload(downloadItem, suggest) {
// 立即调用 suggest 来防止多次调用错误
suggest();
// 然后异步处理重定向逻辑
processDownloadRedirect(downloadItem);
}
async function processDownloadRedirect(downloadItem) {
try {
const storageAPI = webext.storage.sync || webext.storage.local;
const settings = await storageAPI.get(DEFAULT_SETTINGS);
// 检查扩展是否已启用并配置了域名
if (!settings.enabled || !settings.xgetDomain) {
return;
}
const url = downloadItem.url;
const redirectedUrl = transformUrl(
url,
settings.xgetDomain,
settings.enabledPlatforms
);
if (redirectedUrl && redirectedUrl !== url) {
console.log("重定向下载:", url, "->", redirectedUrl);
try {
// 取消原始下载
await webext.downloads.cancel(downloadItem.id);
// 使用重定向的 URL 开始新下载
await webext.downloads.download({
url: redirectedUrl,
filename: downloadItem.filename || undefined,
conflictAction: "uniquify",
});
// 通过内容脚本显示通知
try {
await webext.tabs.sendMessage(downloadItem.tabId, {
action: "showNotification",
message: "下载已通过 Xget 重定向",
});
} catch (error) {
console.log("无法向标签页发送通知");
}
} catch (error) {
console.error("重定向下载时出错:", error);
}
}
} catch (error) {
console.error("处理下载重定向时出错:", error);
}
}
function transformUrlLegacy(url, settings) {
try {
// 找到匹配的平台
for (const [platformKey, platform] of Object.entries(PLATFORMS)) {
if (!settings.enabledPlatforms[platformKey]) continue;
if (platform.pattern.test(url)) {
const urlObj = new URL(url);
const path = urlObj.pathname + urlObj.search + urlObj.hash;
// 使用 Xget 域名转换 URL(添加 https:// 协议)
const xgetUrl = `https://${settings.xgetDomain}/${platformKey}${path}`;
return xgetUrl;
}
}
return null; // 不需要转换
} catch (error) {
console.error("转换 URL 时出错:", error);
return null;
}
}
// 监听来自弹出窗口/选项的消息
webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
const storageAPI = webext.storage.sync || webext.storage.local;
if (request.action === "getSettings") {
storageAPI.get(DEFAULT_SETTINGS).then(sendResponse);
return true;
} else if (request.action === "saveSettings") {
storageAPI.set(request.settings).then(async () => {
// 通知相关标签页刷新
try {
const tabs = await webext.tabs.query({
url: Object.values(PLATFORMS).map((platform) => platform.base + "/*"),
});
for (const tab of tabs) {
try {
await webext.tabs.sendMessage(tab.id, {
action: "showNotification",
message: "设置已更新!点击刷新页面",
showRefreshButton: true,
});
} catch (error) {
// 标签页可能没有加载内容脚本,忽略
}
}
} catch (error) {
console.log("无法通知标签页设置更新");
}
sendResponse({ success: true });
});
return true;
}
});
+184 -18
View File
@@ -13,6 +13,7 @@
import argparse
import json
import os
import re
import shutil
import zipfile
from pathlib import Path
@@ -43,20 +44,22 @@ def copy_common_files(build_dir):
Args:
build_dir (Path): 目标构建目录
"""
common_files = [
"webext-compat.js",
"background.js",
"content.js",
"popup.js",
"popup.html",
"platforms.js",
# 创建 src 目录结构
src_dir = build_dir / "src"
# 复制源代码目录
if os.path.exists("src"):
shutil.copytree("src", src_dir, dirs_exist_ok=True)
# 复制根目录的文档文件
doc_files = [
"LICENSE",
"README.md",
"PRIVACY_POLICY.md",
"SECURITY.md",
]
for file in common_files:
for file in doc_files:
if os.path.exists(file):
shutil.copy2(file, build_dir)
@@ -65,6 +68,19 @@ def copy_common_files(build_dir):
shutil.copytree("icons", build_dir / "icons", dirs_exist_ok=True)
def read_package_metadata():
with open("package.json", encoding="utf-8") as f:
package = json.load(f)
return {
"name": package["name"],
"version": package["version"],
"description": package["description"],
"author": package["author"],
"homepage": package["homepage"],
}
def build_chrome():
"""
构建 Chromium 版本
@@ -81,8 +97,8 @@ def build_chrome():
# 复制 Chrome manifest
shutil.copy2("manifest.json", build_dir / "manifest.json")
# 修改 background.js 为 Chrome 优化
optimize_for_chrome(build_dir)
# 为 Chrome 创建扁平化的文件结构
flatten_for_browser(build_dir, "chrome")
print(f"Chrome 版本构建完成: {build_dir}")
return build_dir
@@ -104,13 +120,84 @@ def build_firefox():
# 复制 Firefox manifest
shutil.copy2("manifest-firefox.json", build_dir / "manifest.json")
# 优化文件以适配 Firefox
optimize_for_firefox(build_dir)
# 为 Firefox 创建扁平化的文件结构
flatten_for_browser(build_dir, "firefox")
print(f"Firefox 版本构建完成: {build_dir}")
return build_dir
def flatten_for_browser(build_dir, platform):
"""
将 src 目录中的文件扁平化到构建目录根
Args:
build_dir (Path): 构建目录路径
platform (str): 目标平台 (chrome/firefox)
"""
src_dir = build_dir / "src"
if not src_dir.exists():
print(f"警告: {src_dir} 不存在")
return
# 将文件从 src 子目录移动到根目录
# background/index.js -> background.js
bg_file = src_dir / "background" / "index.js"
if bg_file.exists():
shutil.copy2(bg_file, build_dir / "background.js")
# content/index.js -> content.js
content_file = src_dir / "content" / "index.js"
if content_file.exists():
shutil.copy2(content_file, build_dir / "content.js")
# popup/index.html -> popup.html
popup_html = src_dir / "popup" / "index.html"
if popup_html.exists():
shutil.copy2(popup_html, build_dir / "popup.html")
# popup/popup.js -> popup.js
popup_js = src_dir / "popup" / "popup.js"
if popup_js.exists():
shutil.copy2(popup_js, build_dir / "popup.js")
# shared/platforms.js -> platforms.js
platforms_file = src_dir / "shared" / "platforms.js"
if platforms_file.exists():
shutil.copy2(platforms_file, build_dir / "platforms.js")
# shared/download-core.js -> download-core.js
download_core_file = src_dir / "shared" / "download-core.js"
if download_core_file.exists():
shutil.copy2(download_core_file, build_dir / "download-core.js")
# shared/platform-detector.js -> platform-detector.js
detector_file = src_dir / "shared" / "platform-detector.js"
if detector_file.exists():
shutil.copy2(detector_file, build_dir / "platform-detector.js")
# shared/compat/webext-compat.js -> webext-compat.js
webext_compat = src_dir / "shared" / "compat" / "webext-compat.js"
if webext_compat.exists():
shutil.copy2(webext_compat, build_dir / "webext-compat.js")
# shared/compat/firefox-compat.js -> firefox-compat.js
firefox_compat = src_dir / "shared" / "compat" / "firefox-compat.js"
if firefox_compat.exists():
shutil.copy2(firefox_compat, build_dir / "firefox-compat.js")
# 删除 src 目录
if src_dir.exists():
shutil.rmtree(src_dir)
# 平台特定优化
if platform == "chrome":
optimize_for_chrome(build_dir)
elif platform == "firefox":
optimize_for_firefox(build_dir)
def optimize_for_chrome(build_dir):
"""
为 Chrome 优化文件
@@ -188,6 +275,79 @@ def create_package(build_dir, platform):
return package_path
def create_userscript_package(script_path):
package_dir = Path("packages")
package_dir.mkdir(exist_ok=True)
metadata = read_package_metadata()
package_name = f"Xget-Now_{metadata['version']}.userscript.user.js"
package_path = package_dir / package_name
shutil.copy2(script_path, package_path)
print(f"用户脚本包已创建: {package_path}")
return package_path
def extract_userscript_matches():
source = Path("src/shared/download-core.js").read_text(encoding="utf-8")
match = re.search(
r"const CORE_USERSCRIPT_MATCHES = \[(.*?)\];",
source,
re.DOTALL,
)
if not match:
raise ValueError("无法从 download-core.js 中提取 CORE_USERSCRIPT_MATCHES")
raw_items = match.group(1)
return re.findall(r'"([^"]+)"', raw_items)
def build_userscript():
print("构建 userscript 版本...")
build_dir = create_build_directory("userscript")
metadata = read_package_metadata()
matches = extract_userscript_matches()
metadata_lines = [
"// ==UserScript==",
"// @name Xget Now",
f"// @namespace {metadata['homepage']}",
f"// @version {metadata['version']}",
f"// @description {metadata['description']}",
f"// @author {metadata['author']}",
"// @license GPL-3.0",
]
metadata_lines.extend(f"// @match {pattern}" for pattern in matches)
metadata_lines.extend(
[
"// @grant GM_getValue",
"// @grant GM_setValue",
"// @grant GM_deleteValue",
"// @grant GM_registerMenuCommand",
"// @grant GM_xmlhttpRequest",
"// @connect *",
"// @run-at document-start",
"// ==/UserScript==",
"",
]
)
script_parts = [
"\n".join(metadata_lines),
Path("src/shared/download-core.js").read_text(encoding="utf-8"),
"",
Path("src/userscript/index.js").read_text(encoding="utf-8"),
"",
]
output_path = build_dir / "xget-now.user.js"
output_path.write_text("\n".join(script_parts), encoding="utf-8")
print(f"Userscript 构建完成: {output_path}")
return output_path
def validate_manifest(manifest_path, platform):
"""
验证 manifest 文件格式和内容
@@ -219,11 +379,11 @@ def validate_manifest(manifest_path, platform):
"applications" in manifest or "browser_specific_settings" in manifest
), "Firefox 需要应用ID"
print(f"✓ {platform} manifest 验证通过")
print(f"[OK] {platform} manifest 验证通过")
return True
except Exception as e:
print(f"✗ {platform} manifest 验证失败: {e}")
print(f"[FAIL] {platform} manifest 验证失败: {e}")
return False
@@ -231,7 +391,7 @@ def main():
parser = argparse.ArgumentParser(description="构建 Xget Now 扩展")
parser.add_argument(
"--platform",
choices=["chrome", "firefox", "all"],
choices=["chrome", "firefox", "userscript", "all"],
default="all",
help="要构建的平台",
)
@@ -244,7 +404,7 @@ def main():
platforms = []
if args.platform == "all":
platforms = ["chrome", "firefox"]
platforms = ["chrome", "firefox", "userscript"]
else:
platforms = [args.platform]
@@ -256,6 +416,12 @@ def main():
build_dir = build_chrome()
elif platform == "firefox":
build_dir = build_firefox()
elif platform == "userscript":
userscript_path = build_userscript()
build_results.append((platform, userscript_path))
if args.package:
create_userscript_package(userscript_path)
continue
# 验证 manifest
manifest_path = build_dir / "manifest.json"
@@ -274,10 +440,10 @@ def main():
# 总结
print("\n构建总结:")
for platform, build_dir in build_results:
print(f"✓ {platform}: {build_dir}")
print(f"[OK] {platform}: {build_dir}")
if args.package:
print("\n扩展包位于 packages/ 目录")
print("\n构建包位于 packages/ 目录")
if __name__ == "__main__":
-614
View File
@@ -1,614 +0,0 @@
/**
* Xget Now - 内容脚本
*
* 功能:
* - 在支持的平台页面上注入下载拦截功能
* - 监听页面上的下载链接点击事件
* - 将下载请求重定向到加速服务
* - 处理不同平台的特殊下载逻辑
*/
// 确保兼容层可用
if (typeof webext === "undefined") {
console.error("WebExt compatibility layer not found in content script");
}
/**
* 平台配置定义
* 与后台脚本保持同步的平台列表
*/
const PLATFORMS = {
// 代码托管平台
gh: {
base: "https://github.com",
name: "GitHub",
pattern: /^https:\/\/github\.com\//,
},
gl: {
base: "https://gitlab.com",
name: "GitLab",
pattern: /^https:\/\/gitlab\.com\//,
},
gitea: {
base: "https://gitea.com",
name: "Gitea",
pattern: /^https:\/\/gitea\.com\//,
},
codeberg: {
base: "https://codeberg.org",
name: "Codeberg",
pattern: /^https:\/\/codeberg\.org\//,
},
sf: {
base: "https://sourceforge.net",
name: "SourceForge",
pattern: /^https:\/\/sourceforge\.net\//,
},
aosp: {
base: "https://android.googlesource.com",
name: "AOSP",
pattern: /^https:\/\/android\.googlesource\.com\//,
},
// AI/ML 平台
hf: {
base: "https://huggingface.co",
name: "Hugging Face",
pattern: /^https:\/\/huggingface\.co\//,
},
// 包管理平台
npm: {
base: "https://registry.npmjs.org",
name: "npm",
pattern: /^https:\/\/registry\.npmjs\.org\//,
},
pypi: {
base: "https://pypi.org",
name: "PyPI",
pattern: /^https:\/\/pypi\.org\//,
},
"pypi-files": {
base: "https://files.pythonhosted.org",
name: "PyPI Files",
pattern: /^https:\/\/files\.pythonhosted\.org\//,
},
conda: {
base: "https://repo.anaconda.com",
name: "Conda",
pattern: /^https:\/\/repo\.anaconda\.com\//,
},
"conda-community": {
base: "https://conda.anaconda.org",
name: "Conda Community",
pattern: /^https:\/\/conda\.anaconda\.org\//,
},
maven: {
base: "https://repo1.maven.org",
name: "Maven",
pattern: /^https:\/\/repo1\.maven\.org\//,
},
apache: {
base: "https://downloads.apache.org",
name: "Apache",
pattern: /^https:\/\/downloads\.apache\.org\//,
},
gradle: {
base: "https://plugins.gradle.org",
name: "Gradle",
pattern: /^https:\/\/plugins\.gradle\.org\//,
},
rubygems: {
base: "https://rubygems.org",
name: "RubyGems",
pattern: /^https:\/\/rubygems\.org\//,
},
cran: {
base: "https://cran.r-project.org",
name: "CRAN",
pattern: /^https:\/\/cran\.r-project\.org\//,
},
cpan: {
base: "https://www.cpan.org",
name: "CPAN",
pattern: /^https:\/\/www\.cpan\.org\//,
},
ctan: {
base: "https://tug.ctan.org",
name: "CTAN",
pattern: /^https:\/\/tug\.ctan\.org\//,
},
golang: {
base: "https://proxy.golang.org",
name: "Go Modules",
pattern: /^https:\/\/proxy\.golang\.org\//,
},
nuget: {
base: "https://api.nuget.org",
name: "NuGet",
pattern: /^https:\/\/api\.nuget\.org\//,
},
crates: {
base: "https://crates.io",
name: "Crates.io",
pattern: /^https:\/\/crates\.io\//,
},
packagist: {
base: "https://repo.packagist.org",
name: "Packagist",
pattern: /^https:\/\/repo\.packagist\.org\//,
},
// 其他平台
arxiv: {
base: "https://arxiv.org",
name: "arXiv",
pattern: /^https:\/\/arxiv\.org\//,
},
fdroid: {
base: "https://f-droid.org",
name: "F-Droid",
pattern: /^https:\/\/f-droid\.org\//,
},
};
// 初始化内容脚本
(async function () {
console.log("Xget Now:内容脚本已加载");
// 监听来自后台脚本的消息
webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "showNotification") {
showNotification(request.message, request.showRefreshButton);
sendResponse({ success: true });
}
});
// 检查扩展是否已启用并配置
const settings = await getSettings();
if (!settings.enabled || !settings.xgetDomain) {
return;
}
// 找到当前平台
const currentPlatform = detectPlatform(window.location.href);
if (!currentPlatform || !settings.enabledPlatforms[currentPlatform]) {
return;
}
// 添加下载拦截
interceptDownloadLinks();
// 监控动态添加的内容
observePageChanges();
})();
async function getSettings() {
try {
return await new Promise((resolve) => {
webext.runtime.sendMessage({ action: "getSettings" }, resolve);
});
} catch (error) {
console.error("获取设置时出错:", error);
return { enabled: false };
}
}
function detectPlatform(url) {
for (const [key, platform] of Object.entries(PLATFORMS)) {
if (platform.pattern.test(url)) {
return key;
}
}
return null;
}
function interceptDownloadLinks() {
// 拦截下载链接的点击事件
document.addEventListener(
"click",
async (event) => {
const link = event.target.closest("a");
if (!link || !link.href) return;
// 检查这是否是下载链接
if (isDownloadLink(link)) {
event.preventDefault();
event.stopPropagation();
await handleDownloadLink(link.href);
}
},
true
);
}
function isDownloadLink(link) {
const href = link.href.toLowerCase();
const url = new URL(link.href);
const pathname = url.pathname.toLowerCase();
// 第一检查:明确的下载属性
if (link.download || link.hasAttribute("download")) {
return true;
}
// 第二检查:表示可下载文件的文件扩展名
const fileExtensions = [
".zip",
".tar.gz",
".tar.bz2",
".tar.xz",
".7z",
".rar",
".gz",
".bz2",
".exe",
".msi",
".dmg",
".pkg",
".deb",
".rpm",
".apk",
".jar",
".war",
".ear",
".iso",
".img",
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".mp4",
".avi",
".mkv",
".mov",
".wmv",
".flv",
".mp3",
".wav",
".flac",
".ogg",
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".tiff",
".svg",
".whl",
".egg",
".gem",
".nupkg",
];
// 检查 URL 是否以文件扩展名结尾
if (fileExtensions.some((ext) => pathname.endsWith(ext))) {
return true;
}
// 第三检查:GitHub 特定模式
const allowedGitHubHosts = ["github.com"];
try {
const parsedUrl = new URL(href);
if (allowedGitHubHosts.includes(parsedUrl.host)) {
// GitHub 发布资源下载 URL 遵循模式:/releases/download/
if (pathname.includes("/releases/download/")) {
return true;
}
// GitHub 存档下载 URL
if (
pathname.includes("/archive/") &&
(pathname.endsWith(".zip") || pathname.endsWith(".tar.gz"))
) {
return true;
}
// GitHub 原始文件 URL - 新增:支持原始文件链接
if (pathname.includes("/raw/")) {
return true;
}
// 排除导航到发布页面(仅 /releases 或 /releases/)
if (pathname.endsWith("/releases") || pathname.endsWith("/releases/")) {
return false;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第四检查:GitLab 特定模式
const allowedGitLabHosts = ["gitlab.com"];
try {
const parsedUrl = new URL(href);
if (allowedGitLabHosts.includes(parsedUrl.host)) {
// GitLab 存档下载
if (pathname.includes("/-/archive/")) {
return true;
}
// GitLab 发布下载
if (
pathname.includes("/-/releases/") &&
pathname.includes("/downloads/")
) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第五检查:Hugging Face 文件下载
const allowedHuggingFaceHosts = ["huggingface.co"];
try {
const parsedUrl = new URL(href);
if (allowedHuggingFaceHosts.includes(parsedUrl.host)) {
// HF 文件下载 URL 包含 /resolve/
if (pathname.includes("/resolve/")) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第六检查:npm 包下载
const allowedNpmHosts = ["registry.npmjs.org"];
try {
const parsedUrl = new URL(href);
if (allowedNpmHosts.includes(parsedUrl.host)) {
// npm tarball URL 包含 /-/
if (pathname.includes("/-/") && pathname.endsWith(".tgz")) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第七检查:PyPI 包下载
const allowedPypiHosts = ["pypi.org", "files.pythonhosted.org"];
try {
const parsedUrl = new URL(href);
if (allowedPypiHosts.includes(parsedUrl.host)) {
// PyPI 包文件下载
if (
pathname.includes("/packages/") &&
(pathname.endsWith(".tar.gz") ||
pathname.endsWith(".whl") ||
pathname.endsWith(".egg"))
) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第八检查:SourceForge 下载
const allowedSourceForgeHosts = ["sourceforge.net"];
try {
const parsedUrl = new URL(href);
if (allowedSourceForgeHosts.includes(parsedUrl.host)) {
// SourceForge 下载 URL 包含 /download
if (
pathname.includes("/download") ||
url.searchParams.get("use_mirror")
) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第九检查:Conda 包下载
const allowedCondaHosts = ["repo.anaconda.com", "conda.anaconda.org"];
try {
const parsedUrl = new URL(href);
if (allowedCondaHosts.includes(parsedUrl.host)) {
// Conda 包文件
if (pathname.endsWith(".conda") || pathname.endsWith(".tar.bz2")) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第十检查:其他包管理平台
const packageManagerHosts = [
"rubygems.org",
"cran.r-project.org",
"crates.io",
"repo.packagist.org",
"api.nuget.org",
"proxy.golang.org",
];
try {
const parsedUrl = new URL(href);
if (packageManagerHosts.includes(parsedUrl.host)) {
// 各种包管理器的下载文件
const packageExtensions = [
".gem",
".tar.gz",
".crate",
".zip",
".nupkg",
".tgz",
".tar.bz2",
];
if (packageExtensions.some((ext) => pathname.endsWith(ext))) {
return true;
}
}
} catch (e) {
console.error("无效的 URL:", href, e);
}
// 第十一检查:明确的下载文本指示器(更具体)
const downloadTextIndicators = ["download", "download file", "get file"];
const linkText = link.textContent.toLowerCase().trim();
if (
downloadTextIndicators.some(
(indicator) =>
linkText === indicator || linkText.startsWith(indicator + " ")
)
) {
return true;
}
// 默认:不是下载链接
return false;
}
async function handleDownloadLink(url) {
try {
const settings = await getSettings();
if (!settings.enabled || !settings.xgetDomain) return;
const transformedUrl = transformUrl(url, settings);
if (transformedUrl) {
// 显示通知
showNotification(`下载已通过 Xget 重定向`);
// 触发下载
window.location.href = transformedUrl;
}
} catch (error) {
console.error("处理下载时出错:", error);
}
}
function transformUrl(url, settings) {
try {
const platform = detectPlatform(url);
if (!platform || !settings.enabledPlatforms[platform]) {
return null;
}
const urlObj = new URL(url);
const path = urlObj.pathname + urlObj.search + urlObj.hash;
return `https://${settings.xgetDomain}/${platform}${path}`;
} catch (error) {
console.error("转换 URL 时出错:", error);
return null;
}
}
function observePageChanges() {
const observer = new MutationObserver(() => {
// 监控页面变化(无需额外操作)
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
function showNotification(message, showRefreshButton = false) {
try {
// 首先删除所有现有通知
const existingNotifications =
document.querySelectorAll(".xget-notification");
existingNotifications.forEach((notification) => notification.remove());
// 创建一个简单的通知
const notification = document.createElement("div");
notification.className = "xget-notification";
// 创建消息容器
const messageDiv = document.createElement("div");
messageDiv.textContent = message;
messageDiv.style.marginBottom = showRefreshButton ? "8px" : "0";
notification.appendChild(messageDiv);
// 如果需要,添加刷新按钮
if (showRefreshButton) {
const refreshButton = document.createElement("button");
refreshButton.textContent = "🔄 刷新页面";
refreshButton.style.cssText = `
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-top: 4px;
width: 100%;
transition: background 0.2s;
`;
refreshButton.addEventListener("mouseenter", () => {
refreshButton.style.background = "rgba(255, 255, 255, 0.3)";
});
refreshButton.addEventListener("mouseleave", () => {
refreshButton.style.background = "rgba(255, 255, 255, 0.2)";
});
refreshButton.addEventListener("click", () => {
window.location.reload();
});
notification.appendChild(refreshButton);
}
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 2147483647;
animation: xgetSlideIn 0.3s ease-out;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 280px;
word-wrap: break-word;
`;
// 如果尚未存在则添加动画样式
if (!document.getElementById("xget-notification-styles")) {
const style = document.createElement("style");
style.id = "xget-notification-styles";
style.textContent = `
@keyframes xgetSlideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes xgetSlideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(notification);
// 如果有刷新按钮则在较长时间后删除,否则在较短时间后删除
const removeDelay = showRefreshButton ? 8000 : 4000;
setTimeout(() => {
notification.style.animation = "xgetSlideOut 0.3s ease-in";
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}, removeDelay);
} catch (error) {
console.error("显示通知时出错:", error);
// 如果 DOM 操作失败则回退到控制台日志
console.log("Xget 通知:", message);
}
}
+57 -31
View File
@@ -54,20 +54,26 @@ def watch_files():
监控的文件包括:
- manifest.json / manifest-firefox.json
- JavaScript 文件 (background.js, content.js, popup.js)
- HTML 文件 (popup.html)
- 兼容层文件 (webext-compat.js)
- JavaScript 文件 (src/background/index.js, src/content/index.js, src/popup/popup.js)
- HTML 文件 (src/popup/index.html)
- 兼容层文件 (src/shared/compat/*.js)
- 平台检测文件 (src/shared/platform-detector.js, src/shared/platforms.js)
"""
import os
watch_files = [
"manifest.json",
"manifest-firefox.json",
"background.js",
"content.js",
"popup.js",
"popup.html",
"webext-compat.js",
"src/background/index.js",
"src/content/index.js",
"src/popup/popup.js",
"src/popup/index.html",
"src/shared/download-core.js",
"src/userscript/index.js",
"src/shared/compat/webext-compat.js",
"src/shared/compat/firefox-compat.js",
"src/shared/platform-detector.js",
"src/shared/platforms.js",
]
file_times = {}
@@ -97,9 +103,9 @@ def watch_files():
text=True,
)
if result.returncode == 0:
print("✓ 构建成功")
print("[OK] 构建成功")
else:
print(f"✗ 构建失败: {result.stderr}")
print(f"[FAIL] 构建失败: {result.stderr}")
time.sleep(1)
@@ -150,11 +156,11 @@ def run_tests():
assert manifest["manifest_version"] == 2, "Firefox 需要 Manifest V2"
assert "browser_action" in manifest, "Firefox 需要 browser_action 字段"
print(f"✓ {platform} manifest 测试通过")
print(f"[OK] {platform} manifest 测试通过")
return True
except Exception as e:
print(f"✗ {platform} manifest 测试失败: {e}")
print(f"[FAIL] {platform} manifest 测试失败: {e}")
return False
# 测试文件存在性
@@ -162,19 +168,24 @@ def run_tests():
required_files = [
"manifest.json",
"manifest-firefox.json",
"background.js",
"content.js",
"popup.js",
"popup.html",
"webext-compat.js",
"src/background/index.js",
"src/content/index.js",
"src/popup/popup.js",
"src/popup/index.html",
"src/shared/download-core.js",
"src/userscript/index.js",
"src/shared/compat/webext-compat.js",
"src/shared/compat/firefox-compat.js",
"src/shared/platform-detector.js",
"src/shared/platforms.js",
]
all_exist = True
for file in required_files:
if os.path.exists(file):
print(f"✓ {file} 存在")
print(f"[OK] {file} 存在")
else:
print(f"✗ {file} 不存在")
print(f"[FAIL] {file} 不存在")
all_exist = False
return all_exist
@@ -204,16 +215,21 @@ def lint_code():
特殊处理:
- webext-compat.js: 允许直接使用原生 API
- firefox-compat.js: 允许直接使用原生 API
- platform-detector.js: 允许直接使用原生 API
"""
print("运行代码检查...")
js_files = [
"background.js",
"content.js",
"popup.js",
"webext-compat.js",
"platform-detector.js",
"src/background/index.js",
"src/content/index.js",
"src/popup/popup.js",
"src/shared/download-core.js",
"src/userscript/index.js",
"src/shared/compat/webext-compat.js",
"src/shared/compat/firefox-compat.js",
"src/shared/platform-detector.js",
"src/shared/platforms.js",
]
for file in js_files:
@@ -228,34 +244,44 @@ def lint_code():
issues = []
# 底层文件允许直接使用原生 API
bottom_layer_files = ["webext-compat.js", "platform-detector.js"]
bottom_layer_files = [
"src/shared/compat/webext-compat.js",
"src/shared/compat/firefox-compat.js",
"src/shared/platform-detector.js",
]
if file not in bottom_layer_files:
if "chrome." in content and "webext." not in content:
issues.append("发现直接使用 chrome API,应使用 webext 兼容层")
elif file == "webext-compat.js":
elif "webext-compat.js" in file:
# 对于 webext-compat.js,检查是否正确导出了 webext 接口
if "window.webext" not in content and "module.exports" not in content:
issues.append("兼容层未正确导出 webext 接口")
elif file == "platform-detector.js":
elif "firefox-compat.js" in file:
# 对于 firefox-compat.js,检查 Firefox 特定的兼容性处理
if "browser." not in content and "chrome." not in content:
issues.append("Firefox 兼容层应处理 browser/chrome API")
elif "platform-detector.js" in file:
# 对于 platform-detector.js,检查是否正确导出了平台检测接口
if "platformDetector" not in content:
issues.append("平台检测器未正确定义 platformDetector 接口")
# 检查错误处理
if "console.log(" in content and "console.error(" not in content:
# 对于底层文件,console.warn 也是可以接受的
if file in bottom_layer_files and "console.warn(" in content:
# 对于底层文件和平台定义文件,console.warn 也是可以接受的
if (
file in bottom_layer_files or "platforms.js" in file
) and "console.warn(" in content:
pass # 底层文件使用 console.warn 是合理的
else:
issues.append("建议使用 console.error 处理错误")
if issues:
print(f" ⚠ {file} 发现问题:")
print(f" [WARN] {file} 发现问题:")
for issue in issues:
print(f" - {issue}")
else:
print(f" ✓ {file} 无明显问题")
print(f" [OK] {file} 无明显问题")
def main():
+31 -23
View File
@@ -1,8 +1,12 @@
{
"manifest_version": 2,
"name": "Xget Now",
"version": "2.0.2",
"description": "为 Firefox 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
"background": {
"persistent": true,
"scripts": [
"webext-compat.js",
"download-core.js",
"background.js"
]
},
"browser_action": {
"default_icon": {
"16": "icons/icon16.png",
@@ -13,21 +17,24 @@
"default_popup": "popup.html",
"default_title": "Xget Now"
},
"background": {
"scripts": [
"webext-compat.js",
"background.js"
],
"persistent": false
"browser_specific_settings": {
"gecko": {
"id": "i@xi-xu.me",
"strict_min_version": "109.0"
}
},
"content_scripts": [
{
"js": [
"webext-compat.js",
"download-core.js",
"content.js"
],
"matches": [
"https://github.com/*",
"https://raw.githubusercontent.com/*",
"https://codeload.github.com/*",
"https://objects.githubusercontent.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
@@ -56,11 +63,23 @@
"run_at": "document_end"
}
],
"description": "为 Firefox 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"manifest_version": 2,
"name": "Xget Now",
"permissions": [
"downloads",
"storage",
"activeTab",
"https://github.com/*",
"https://raw.githubusercontent.com/*",
"https://codeload.github.com/*",
"https://objects.githubusercontent.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
@@ -86,19 +105,8 @@
"https://arxiv.org/*",
"https://f-droid.org/*"
],
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"version": "2.1.0",
"web_accessible_resources": [
"icons/*"
],
"browser_specific_settings": {
"gecko": {
"id": "i@xi-xu.me",
"strict_min_version": "109.0"
}
}
]
}
+8 -1
View File
@@ -16,10 +16,14 @@
{
"js": [
"webext-compat.js",
"download-core.js",
"content.js"
],
"matches": [
"https://github.com/*",
"https://raw.githubusercontent.com/*",
"https://codeload.github.com/*",
"https://objects.githubusercontent.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
@@ -51,6 +55,9 @@
"description": "为 Chromium 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
"host_permissions": [
"https://github.com/*",
"https://raw.githubusercontent.com/*",
"https://codeload.github.com/*",
"https://objects.githubusercontent.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
@@ -89,7 +96,7 @@
"storage",
"activeTab"
],
"version": "2.0.2",
"version": "2.1.0",
"web_accessible_resources": [
{
"matches": [
+66
View File
@@ -0,0 +1,66 @@
{
"name": "xget-now",
"version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xget-now",
"version": "2.1.0",
"license": "GPL-3.0",
"devDependencies": {
"playwright": "^1.62.1"
},
"engines": {
"chrome": ">=88"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}
+11 -4
View File
@@ -56,8 +56,15 @@
"url": "https://github.com/xixu-me/Xget-Now.git"
},
"scripts": {
"build": "echo 'No build process needed for Chrome extension'",
"test": "echo 'No tests specified'"
"build": "python build.py --platform all",
"build:package": "python build.py --platform all --package",
"build:userscript": "python build.py --platform userscript --package",
"test": "node --test",
"test:unit": "node --test tests/unit/download-core.test.js",
"test:e2e": "node --test tests/e2e/userscript.e2e.test.js"
},
"version": "2.0.2"
}
"version": "2.1.0",
"devDependencies": {
"playwright": "^1.62.1"
}
}
+148
View File
@@ -0,0 +1,148 @@
if (typeof importScripts !== "undefined") {
try {
importScripts("webext-compat.js", "download-core.js");
} catch (error) {
console.log("Background dependencies already loaded by the browser");
}
}
if (typeof webext === "undefined") {
if (typeof browser !== "undefined") {
self.webext = browser;
self.webextFlavor = { soup: new Set(["firefox"]) };
} else if (typeof chrome !== "undefined") {
self.webext = chrome;
self.webextFlavor = { soup: new Set(["chromium"]) };
}
}
const core = self.XgetDownloadCore;
const DEFAULT_SETTINGS = core.normalizeSettings(core.DEFAULT_EXTENSION_SETTINGS);
function getStorageAPI() {
const isFirefox =
typeof webextFlavor !== "undefined" &&
webextFlavor.soup &&
webextFlavor.soup.has("firefox");
return isFirefox ? webext.storage.local : webext.storage.sync || webext.storage.local;
}
async function getSettings() {
const storageAPI = getStorageAPI();
const rawSettings = await storageAPI.get(DEFAULT_SETTINGS);
return core.normalizeSettings(rawSettings);
}
async function setSettings(nextSettings) {
const storageAPI = getStorageAPI();
const normalized = core.normalizeSettings(nextSettings);
await storageAPI.set(normalized);
return normalized;
}
async function notifyMatchingTabs(message, showRefreshButton) {
try {
const tabs = await webext.tabs.query({
url: core.getMatchPatterns(),
});
await Promise.all(
tabs.map(async (tab) => {
try {
await webext.tabs.sendMessage(tab.id, {
action: "showNotification",
message,
showRefreshButton,
});
} catch (error) {
return null;
}
})
);
} catch (error) {
console.log("Unable to notify tabs about settings changes");
}
}
async function processDownloadRedirect(downloadItem) {
const settings = await getSettings();
if (!settings.enabled || !settings.activeDomain) {
return;
}
const redirectedUrl = core.transformDownloadUrl(downloadItem.url, settings);
if (!redirectedUrl || redirectedUrl === downloadItem.url) {
return;
}
await webext.downloads.cancel(downloadItem.id);
await webext.downloads.download({
url: redirectedUrl,
filename: downloadItem.filename || undefined,
conflictAction: "uniquify",
});
if (typeof downloadItem.tabId === "number") {
try {
await webext.tabs.sendMessage(downloadItem.tabId, {
action: "showNotification",
message: "Download redirected via Xget",
showRefreshButton: false,
});
} catch (error) {
return null;
}
}
}
function handleDownload(downloadItem, suggest) {
suggest();
processDownloadRedirect(downloadItem).catch((error) => {
console.error("Failed to redirect browser download", error);
});
}
const messageListener = (request, sender, sendResponse) => {
if (request.action === "getSettings") {
getSettings()
.then((settings) => sendResponse(settings))
.catch((error) => {
console.error("Failed to load settings", error);
sendResponse(DEFAULT_SETTINGS);
});
return true;
}
if (request.action === "saveSettings") {
setSettings(request.settings || {})
.then(async (settings) => {
await notifyMatchingTabs("Settings updated. Refresh the page to use them.", true);
sendResponse({ success: true, settings });
})
.catch((error) => {
console.error("Failed to save settings", error);
sendResponse({ success: false, error: error.message });
});
return true;
}
return false;
};
webext.runtime.onInstalled.addListener(async () => {
try {
const settings = await getSettings();
await setSettings(settings);
} catch (error) {
console.error("Failed to initialize extension settings", error);
}
});
if (webext.downloads && webext.downloads.onDeterminingFilename) {
webext.downloads.onDeterminingFilename.addListener(handleDownload);
}
if (webext.runtime && webext.runtime.onMessage) {
webext.runtime.onMessage.addListener(messageListener);
}
+160
View File
@@ -0,0 +1,160 @@
if (typeof webext === "undefined") {
console.error("Xget Now content script: compatibility layer missing");
}
const core = globalThis.XgetDownloadCore;
let liveSettings = null;
async function getSettings() {
const settings = await new Promise((resolve) => {
webext.runtime.sendMessage({ action: "getSettings" }, resolve);
});
return core.normalizeSettings(settings);
}
function shouldHandleCurrentPage(settings) {
const currentPlatform = core.detectPlatform(window.location.href);
return (
Boolean(currentPlatform) &&
settings.enabled &&
Boolean(settings.activeDomain) &&
settings.enabledPlatforms[currentPlatform]
);
}
function createSyntheticLink(url, sourceLink) {
const link = document.createElement("a");
link.href = url;
link.rel = "noopener noreferrer";
link.target = sourceLink && sourceLink.target ? sourceLink.target : "_blank";
if (sourceLink && (sourceLink.download || sourceLink.hasAttribute("download"))) {
link.download = sourceLink.download || "";
}
link.style.display = "none";
return link;
}
function openViaSyntheticLink(url, sourceLink) {
const link = createSyntheticLink(url, sourceLink);
document.body.appendChild(link);
link.click();
link.remove();
}
function showNotification(message, showRefreshButton) {
const existing = document.querySelectorAll(".xget-notification");
existing.forEach((node) => node.remove());
const notification = document.createElement("div");
notification.className = "xget-notification";
notification.style.cssText = [
"position: fixed",
"top: 20px",
"right: 20px",
"z-index: 2147483647",
"max-width: 280px",
"padding: 12px 14px",
"border-radius: 10px",
"box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2)",
"background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)",
"color: #eff6ff",
"font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
].join("; ");
const text = document.createElement("div");
text.textContent = String(message);
notification.appendChild(text);
if (showRefreshButton) {
const button = document.createElement("button");
button.type = "button";
button.textContent = "Refresh";
button.style.cssText = [
"margin-top: 8px",
"border: 0",
"border-radius: 8px",
"padding: 6px 8px",
"background: rgba(255, 255, 255, 0.18)",
"color: inherit",
"cursor: pointer",
].join("; ");
button.addEventListener("click", () => window.location.reload());
notification.appendChild(button);
}
document.documentElement.appendChild(notification);
setTimeout(() => {
notification.remove();
}, showRefreshButton ? 8000 : 3000);
}
async function handleClick(event) {
const link = event.target && event.target.closest
? event.target.closest("a[href]")
: null;
if (!link) {
return;
}
const settings = liveSettings || (await getSettings());
liveSettings = settings;
if (!shouldHandleCurrentPage(settings)) {
return;
}
const platformKey = core.detectPlatform(link.href);
if (!platformKey || !settings.enabledPlatforms[platformKey]) {
return;
}
if (core.shouldBypassClick(event, link)) {
return;
}
if (
!core.isDownloadCandidate(link.href, {
hasDownloadAttribute: Boolean(link.download || link.hasAttribute("download")),
linkText: link.textContent || "",
})
) {
return;
}
event.preventDefault();
event.stopPropagation();
if (typeof event.stopImmediatePropagation === "function") {
event.stopImmediatePropagation();
}
const redirectedUrl = core.transformDownloadUrl(link.href, settings);
if (!redirectedUrl || redirectedUrl === link.href) {
showNotification("Unable to rewrite this URL. Opening the original link.");
openViaSyntheticLink(link.href, link);
return;
}
showNotification("Download redirected via Xget");
openViaSyntheticLink(redirectedUrl, link);
}
(async function init() {
liveSettings = await getSettings();
if (!shouldHandleCurrentPage(liveSettings)) {
return;
}
document.addEventListener("click", handleClick, true);
webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "showNotification") {
showNotification(request.message, request.showRefreshButton);
sendResponse({ success: true });
}
});
})();
+42 -2
View File
@@ -55,6 +55,38 @@
background: rgba(255, 255, 255, 0.05);
}
.header .link-item {
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
padding: 6px 12px;
font-size: 12px;
opacity: 0.8;
transition: opacity 0.2s ease;
border-radius: 6px;
background: var(--container-bg);
border: 1px solid var(--container-border);
text-decoration: none;
color: var(--text-color);
}
.header .link-item:hover {
opacity: 1;
background: var(--hover-bg);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.header .link-item .link-icon {
margin-right: 6px;
font-size: 14px;
}
.header .link-item .link-text {
font-size: 12px;
}
.header h1 {
margin: 0;
font-size: 20px;
@@ -371,6 +403,14 @@
</head>
<body>
<div class="header">
<a
href="https://xuc.xi-xu.me/"
target="_blank"
class="link-item"
style="margin-bottom: 10px; display: inline-block"
>
<span class="link-text">URL 转换器</span>
</a>
<h1>Xget Now</h1>
</div>
@@ -559,7 +599,6 @@
target="_blank"
class="link-item"
>
<span class="link-icon">🚀</span>
<span class="link-text">Xget 存储库</span>
</a>
<a
@@ -567,7 +606,6 @@
target="_blank"
class="link-item"
>
<span class="link-icon">📖</span>
<span class="link-text">扩展存储库</span>
</a>
</div>
@@ -577,6 +615,8 @@
</div>
<script src="webext-compat.js"></script>
<script src="download-core.js"></script>
<script src="firefox-compat.js"></script>
<script src="popup.js"></script>
</body>
</html>
+101 -25
View File
@@ -8,18 +8,33 @@
* - 提供平台开关控制
*/
const core = window.XgetDownloadCore;
document.addEventListener("DOMContentLoaded", async () => {
// 确保兼容层可用
if (typeof webext === "undefined") {
console.error("WebExt compatibility layer not found in popup");
// 确保有可用的API
if (
typeof webext === "undefined" &&
typeof browser === "undefined" &&
!window.firefoxAPI
) {
console.error("No compatible extension API found");
showStatus("扩展API加载失败", "error");
return;
}
// 加载当前设置
await loadSettings();
// 延迟执行以确保background脚本已准备就绪
setTimeout(async () => {
try {
// 加载当前设置
await loadSettings();
// 设置事件监听器
setupEventListeners();
// 设置事件监听器
setupEventListeners();
} catch (error) {
console.error("Initialization error:", error);
showStatus("初始化失败,请重新打开扩展", "error");
}
}, 100);
});
/**
@@ -27,9 +42,42 @@ document.addEventListener("DOMContentLoaded", async () => {
*/
async function loadSettings() {
try {
const settings = await webext.runtime.sendMessage({
action: "getSettings",
});
console.log("Loading settings...");
let settings;
// Firefox 特殊处理
if (window.firefoxAPI && typeof browser !== "undefined") {
console.log("Using Firefox direct storage API");
try {
// 首先尝试消息传递
settings = await window.firefoxAPI.sendMessage({
action: "getSettings",
});
} catch (msgError) {
console.log(
"Message passing failed, using direct storage access:",
msgError
);
// 如果消息传递失败,直接从存储读取
settings = await window.firefoxAPI.getSettings();
}
} else if (webext && webext.runtime && webext.runtime.sendMessage) {
// 标准 webext API
console.log("Using standard webext API");
settings = await webext.runtime.sendMessage({
action: "getSettings",
});
} else {
throw new Error("No compatible API available");
}
settings = core ? core.normalizeSettings(settings) : settings;
console.log("Settings received:", settings);
if (!settings) {
throw new Error("No response from background script");
}
// 使用当前设置更新 UI
const domainValue = settings.xgetDomain || "";
@@ -301,20 +349,49 @@ async function saveSettings() {
settings.xgetDomain = cleanupDomain(settings.xgetDomain);
}
const response = await webext.runtime.sendMessage({
action: "saveSettings",
settings: settings,
});
let response;
if (response.success) {
// Firefox 特殊处理
if (window.firefoxAPI && typeof browser !== "undefined") {
console.log("Saving settings using Firefox API");
try {
// 首先尝试消息传递
response = await window.firefoxAPI.sendMessage({
action: "saveSettings",
settings: settings,
});
} catch (msgError) {
console.log("Message passing failed, using direct storage:", msgError);
// 如果消息传递失败,直接写入存储
response = await window.firefoxAPI.saveSettings(settings);
}
} else if (webext && webext.runtime && webext.runtime.sendMessage) {
// 标准 webext API
console.log("Saving settings using standard webext API");
response = await webext.runtime.sendMessage({
action: "saveSettings",
settings: settings,
});
} else {
throw new Error("No compatible API available for saving settings");
}
if (response && response.success) {
// 显示适当的状态
if (!settings.xgetDomain && settings.enabled) {
showStatus("请配置你的 Xget 域名", "error");
} else if (settings.xgetDomain && settings.enabled) {
showStatus("✅ 设置已保存!查看页面通知中的刷新按钮", "success");
showStatus("设置已保存。查看页面通知中的刷新按钮。", "success");
} else {
showStatus("✅ 设置已保存!查看页面通知中的刷新按钮", "success");
showStatus("设置已保存。查看页面通知中的刷新按钮。", "success");
}
} else if (response && response.error) {
console.error("保存设置失败:", response.error);
showStatus("保存设置时出错: " + response.error, "error");
} else {
// 如果没有响应,可能是消息传递失败
console.error("未收到后台脚本的响应");
showStatus("保存设置时出错:无法与后台脚本通信", "error");
}
} catch (error) {
console.error("保存设置时出错:", error);
@@ -330,18 +407,17 @@ function validateDomain() {
}
function cleanupDomain(domain) {
// 如果意外包含协议,则删除它
domain = domain.replace(/^https?:\/\//, "");
if (core) {
return core.sanitizeDomain(domain);
}
// 删除末尾斜杠
domain = domain.replace(/\/$/, "");
return domain;
return String(domain || "")
.replace(/^https?:\/\//, "")
.replace(/\/$/, "");
}
function isValidDomain(domain) {
// 如果存在协议则删除它
domain = domain.replace(/^https?:\/\//, "");
domain = cleanupDomain(domain);
// 基本域名验证模式
const domainPattern =
+86
View File
@@ -0,0 +1,86 @@
/**
* Firefox 特定兼容性修复
* 直接使用 browser API 处理 Firefox 中的消息传递问题
*/
// 在 Firefox 中,直接使用 browser API
if (
typeof browser !== "undefined" &&
browser.runtime &&
browser.runtime.sendMessage
) {
console.log("Firefox environment detected, using native browser API");
// 为popup.js提供一个简化的API
window.firefoxAPI = {
sendMessage: function (message) {
return new Promise((resolve, reject) => {
try {
browser.runtime.sendMessage(message, (response) => {
if (browser.runtime.lastError) {
console.error(
"Firefox sendMessage error:",
browser.runtime.lastError
);
reject(browser.runtime.lastError);
} else {
resolve(response);
}
});
} catch (error) {
console.error("Firefox sendMessage exception:", error);
reject(error);
}
});
},
getSettings: async function () {
try {
return await browser.storage.local.get({
enabled: true,
xgetDomain: "xget.xi-xu.me",
enabledPlatforms: {
gh: true,
gl: true,
gitea: true,
codeberg: true,
sf: true,
aosp: true,
hf: true,
npm: true,
pypi: true,
"pypi-files": true,
conda: true,
"conda-community": true,
maven: true,
apache: true,
gradle: true,
rubygems: true,
cran: true,
cpan: true,
ctan: true,
golang: true,
nuget: true,
crates: true,
packagist: true,
arxiv: true,
fdroid: true,
},
});
} catch (error) {
console.error("Failed to get settings from storage:", error);
throw error;
}
},
saveSettings: async function (settings) {
try {
await browser.storage.local.set(settings);
return { success: true };
} catch (error) {
console.error("Failed to save settings to storage:", error);
return { success: false, error: error.message };
}
},
};
}
@@ -23,7 +23,7 @@
const detectBrowser = () => {
const ua = navigator.userAgent;
let manifest;
try {
manifest = chrome.runtime.getManifest();
} catch (error) {
@@ -101,7 +101,7 @@ const webext = (() => {
runtime: { getManifest: () => ({ version: "0.0.0" }) },
storage: { local: {}, sync: {} },
tabs: {},
downloads: {}
downloads: {},
};
}
@@ -162,7 +162,10 @@ const webext = (() => {
// sendMessage(message)
runtimeAPI.sendMessage(args[0], (response) => {
if (api.runtime.lastError) {
console.warn("SendMessage failed:", api.runtime.lastError.message);
console.warn(
"SendMessage failed:",
api.runtime.lastError.message
);
resolve(null);
} else {
resolve(response);
@@ -170,15 +173,18 @@ const webext = (() => {
});
} else if (args.length === 2) {
// 检查第二个参数是否是函数(回调)
if (typeof args[1] === 'function') {
if (typeof args[1] === "function") {
// sendMessage(message, callback) - 直接调用回调,不使用 Promise
runtimeAPI.sendMessage(args[0], args[1]);
resolve(undefined); // 立即解析 Promise
} else if (typeof args[0] === 'string' && args[0].length > 10) {
} else if (typeof args[0] === "string" && args[0].length > 10) {
// sendMessage(extensionId, message)
runtimeAPI.sendMessage(args[0], args[1], (response) => {
if (api.runtime.lastError) {
console.warn("SendMessage failed:", api.runtime.lastError.message);
console.warn(
"SendMessage failed:",
api.runtime.lastError.message
);
resolve(null);
} else {
resolve(response);
@@ -188,7 +194,10 @@ const webext = (() => {
// sendMessage(message, options)
runtimeAPI.sendMessage(args[0], args[1], (response) => {
if (api.runtime.lastError) {
console.warn("SendMessage failed:", api.runtime.lastError.message);
console.warn(
"SendMessage failed:",
api.runtime.lastError.message
);
resolve(null);
} else {
resolve(response);
@@ -197,7 +206,7 @@ const webext = (() => {
}
} else if (args.length === 3) {
// 检查第三个参数是否是函数(回调)
if (typeof args[2] === 'function') {
if (typeof args[2] === "function") {
// sendMessage(extensionId, message, callback) 或 sendMessage(message, options, callback)
runtimeAPI.sendMessage(args[0], args[1], args[2]);
resolve(undefined); // 立即解析 Promise
@@ -205,14 +214,17 @@ const webext = (() => {
// sendMessage(extensionId, message, options)
runtimeAPI.sendMessage(args[0], args[1], args[2], (response) => {
if (api.runtime.lastError) {
console.warn("SendMessage failed:", api.runtime.lastError.message);
console.warn(
"SendMessage failed:",
api.runtime.lastError.message
);
resolve(null);
} else {
resolve(response);
}
});
}
} else if (args.length === 4 && typeof args[3] === 'function') {
} else if (args.length === 4 && typeof args[3] === "function") {
// sendMessage(extensionId, message, options, callback)
runtimeAPI.sendMessage(args[0], args[1], args[2], args[3]);
resolve(undefined); // 立即解析 Promise
@@ -244,7 +256,10 @@ const webext = (() => {
// sendMessage(tabId, message)
tabsAPI.sendMessage(tabId, message, (response) => {
if (api.runtime.lastError) {
console.warn("Tabs sendMessage failed:", api.runtime.lastError.message);
console.warn(
"Tabs sendMessage failed:",
api.runtime.lastError.message
);
resolve(null);
} else {
resolve(response);
@@ -254,7 +269,10 @@ const webext = (() => {
// sendMessage(tabId, message, options)
tabsAPI.sendMessage(tabId, message, options, (response) => {
if (api.runtime.lastError) {
console.warn("Tabs sendMessage failed:", api.runtime.lastError.message);
console.warn(
"Tabs sendMessage failed:",
api.runtime.lastError.message
);
resolve(null);
} else {
resolve(response);
@@ -285,33 +303,35 @@ const webext = (() => {
},
// Tabs API
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: createTabsSendMessage(api.tabs),
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,
} : {
query: async () => [],
get: async () => null,
create: async () => null,
update: async () => null,
remove: async () => null,
sendMessage: async () => null,
executeScript: async () => null,
insertCSS: async () => null,
removeCSS: async () => null,
onUpdated: { addListener: () => {}, removeListener: () => {} },
onActivated: { addListener: () => {}, removeListener: () => {} },
onRemoved: { addListener: () => {}, removeListener: () => {} },
},
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: createTabsSendMessage(api.tabs),
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,
}
: {
query: async () => [],
get: async () => null,
create: async () => null,
update: async () => null,
remove: async () => null,
sendMessage: async () => null,
executeScript: async () => null,
insertCSS: async () => null,
removeCSS: async () => null,
onUpdated: { addListener: () => {}, removeListener: () => {} },
onActivated: { addListener: () => {}, removeListener: () => {} },
onRemoved: { addListener: () => {}, removeListener: () => {} },
},
// Storage API
storage: {
@@ -338,35 +358,43 @@ const webext = (() => {
},
// Downloads API
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,
onDeterminingFilename: api.downloads.onDeterminingFilename,
} : {
download: async () => null,
search: async () => [],
cancel: async () => null,
pause: async () => null,
resume: async () => null,
erase: async () => null,
removeFile: async () => null,
show: async () => null,
showDefaultFolder: async () => null,
onCreated: { addListener: () => {}, removeListener: () => {} },
onChanged: { addListener: () => {}, removeListener: () => {} },
onErased: { addListener: () => {}, removeListener: () => {} },
onDeterminingFilename: { addListener: () => {}, removeListener: () => {} },
},
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,
onDeterminingFilename: api.downloads.onDeterminingFilename,
}
: {
download: async () => null,
search: async () => [],
cancel: async () => null,
pause: async () => null,
resume: async () => null,
erase: async () => null,
removeFile: async () => null,
show: async () => null,
showDefaultFolder: async () => null,
onCreated: { addListener: () => {}, removeListener: () => {} },
onChanged: { addListener: () => {}, removeListener: () => {} },
onErased: { addListener: () => {}, removeListener: () => {} },
onDeterminingFilename: {
addListener: () => {},
removeListener: () => {},
},
},
// Browser Action / Action API (兼容不同版本)
action: (() => {
@@ -435,7 +463,9 @@ if (typeof module !== "undefined" && module.exports) {
// 服务工作器环境
self.webext = webext;
self.webextFlavor = webextFlavor;
console.log("WebExt compatibility layer loaded in service worker environment");
console.log(
"WebExt compatibility layer loaded in service worker environment"
);
} else {
// 全局环境
this.webext = webext;
+600
View File
@@ -0,0 +1,600 @@
(function (root, factory) {
const api = factory();
if (typeof module !== "undefined" && module.exports) {
module.exports = api;
}
root.XgetDownloadCore = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
const PLATFORM_DEFINITIONS = {
gh: {
base: "https://github.com",
name: "GitHub",
hosts: [
"github.com",
"raw.githubusercontent.com",
"codeload.github.com",
"objects.githubusercontent.com",
],
downloadPatterns: [
/\/releases\/download\//i,
/\/archive\/.*\.(zip|tar\.gz|tar)$/i,
/\/raw\//i,
/\/zip\//i,
/\/tar(\.gz)?\//i,
],
},
gl: {
base: "https://gitlab.com",
name: "GitLab",
hosts: ["gitlab.com"],
downloadPatterns: [/\/-\/archive\//i, /\/-\/releases\/.+\/downloads\//i],
},
gitea: {
base: "https://gitea.com",
name: "Gitea",
hosts: ["gitea.com"],
downloadPatterns: [/\/archive\//i, /\/raw\//i, /\/releases\/download\//i],
},
codeberg: {
base: "https://codeberg.org",
name: "Codeberg",
hosts: ["codeberg.org"],
downloadPatterns: [/\/archive\//i, /\/raw\//i, /\/releases\/download\//i],
},
sf: {
base: "https://sourceforge.net",
name: "SourceForge",
hosts: ["sourceforge.net"],
downloadPatterns: [/\/download$/i, /\/downloads\//i, /\/files\//i],
},
aosp: {
base: "https://android.googlesource.com",
name: "AOSP",
hosts: ["android.googlesource.com"],
downloadPatterns: [/\/\+archive\//i],
},
hf: {
base: "https://huggingface.co",
name: "Hugging Face",
hosts: ["huggingface.co"],
downloadPatterns: [/\/resolve\//i],
},
npm: {
base: "https://registry.npmjs.org",
name: "npm",
hosts: ["registry.npmjs.org"],
downloadPatterns: [/\/-\/.+\.tgz$/i],
},
pypi: {
base: "https://pypi.org",
name: "PyPI",
hosts: ["pypi.org"],
downloadPatterns: [/\/packages\//i],
},
"pypi-files": {
base: "https://files.pythonhosted.org",
name: "PyPI Files",
hosts: ["files.pythonhosted.org"],
downloadPatterns: [/\/packages\//i],
},
conda: {
base: "https://repo.anaconda.com",
name: "Conda",
hosts: ["repo.anaconda.com"],
downloadPatterns: [/\.(conda|tar\.bz2)$/i],
},
"conda-community": {
base: "https://conda.anaconda.org",
name: "Conda Community",
hosts: ["conda.anaconda.org"],
downloadPatterns: [/\.(conda|tar\.bz2)$/i],
},
maven: {
base: "https://repo1.maven.org",
name: "Maven Central",
hosts: ["repo1.maven.org"],
downloadPatterns: [/\.(jar|war|ear|pom|zip|tar\.gz)$/i],
},
apache: {
base: "https://downloads.apache.org",
name: "Apache Downloads",
hosts: ["downloads.apache.org"],
downloadPatterns: [/\.(zip|tar\.gz|tgz|bz2|xz)$/i],
},
gradle: {
base: "https://plugins.gradle.org",
name: "Gradle Plugin Portal",
hosts: ["plugins.gradle.org"],
downloadPatterns: [/\.(jar|zip)$/i],
},
rubygems: {
base: "https://rubygems.org",
name: "RubyGems",
hosts: ["rubygems.org"],
downloadPatterns: [/\.gem$/i],
},
cran: {
base: "https://cran.r-project.org",
name: "CRAN",
hosts: ["cran.r-project.org"],
downloadPatterns: [/\.(tar\.gz|zip)$/i],
},
cpan: {
base: "https://www.cpan.org",
name: "CPAN",
hosts: ["www.cpan.org"],
downloadPatterns: [/\.(tar\.gz|zip)$/i],
},
ctan: {
base: "https://tug.ctan.org",
name: "CTAN",
hosts: ["tug.ctan.org"],
downloadPatterns: [/\.(zip|tar\.xz|tar\.gz)$/i],
},
golang: {
base: "https://proxy.golang.org",
name: "Go Modules",
hosts: ["proxy.golang.org"],
downloadPatterns: [/\/@v\/.+\.(mod|zip|info)$/i],
},
nuget: {
base: "https://api.nuget.org",
name: "NuGet",
hosts: ["api.nuget.org"],
downloadPatterns: [/\.nupkg$/i],
},
crates: {
base: "https://crates.io",
name: "Crates.io",
hosts: ["crates.io"],
downloadPatterns: [/\/download$/i, /\.crate$/i],
},
packagist: {
base: "https://repo.packagist.org",
name: "Packagist",
hosts: ["repo.packagist.org"],
downloadPatterns: [/\.zip$/i],
},
arxiv: {
base: "https://arxiv.org",
name: "arXiv",
hosts: ["arxiv.org"],
downloadPatterns: [/\/pdf\//i, /\/e-print\//i],
},
fdroid: {
base: "https://f-droid.org",
name: "F-Droid",
hosts: ["f-droid.org"],
downloadPatterns: [/\.apk$/i, /\/FDroid\.apk$/i],
},
};
const HOST_TO_PLATFORM = Object.entries(PLATFORM_DEFINITIONS).reduce(
(map, [key, platform]) => {
platform.hosts.forEach((host) => {
map[host] = key;
});
return map;
},
{}
);
const CORE_USERSCRIPT_MATCHES = [
"https://github.com/*",
"https://raw.githubusercontent.com/*",
"https://codeload.github.com/*",
"https://objects.githubusercontent.com/*",
"https://gitlab.com/*",
"https://gitea.com/*",
"https://codeberg.org/*",
"https://sourceforge.net/*",
"https://huggingface.co/*",
"https://pypi.org/*",
"https://files.pythonhosted.org/*",
"https://registry.npmjs.org/*",
];
const CORE_USERSCRIPT_PLATFORM_KEYS = [
"gh",
"gl",
"gitea",
"codeberg",
"sf",
"hf",
"pypi",
"pypi-files",
"npm",
];
const DOWNLOADABLE_EXTENSIONS = [
".zip",
".tar.gz",
".tar.bz2",
".tar.xz",
".tgz",
".7z",
".rar",
".gz",
".bz2",
".xz",
".exe",
".msi",
".dmg",
".pkg",
".deb",
".rpm",
".apk",
".jar",
".war",
".ear",
".iso",
".img",
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".mp4",
".avi",
".mkv",
".mov",
".wmv",
".mp3",
".wav",
".flac",
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".svg",
".whl",
".egg",
".gem",
".nupkg",
".conda",
".crate",
".safetensors",
".bin",
".pt",
".pth",
".onnx",
".msgpack",
".tflite",
".h5",
];
const DEFAULT_EXTENSION_SETTINGS = {
enabled: true,
xgetDomain: "xget.xi-xu.me",
enabledPlatforms: Object.keys(PLATFORM_DEFINITIONS).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {}),
};
const DEFAULT_USERSCRIPT_SETTINGS = {
enabled: true,
activeProfileId: "default",
profiles: [
{ id: "default", name: "Xget Official", domain: "xget.xi-xu.me" },
],
enabledPlatforms: Object.keys(PLATFORM_DEFINITIONS).reduce((acc, key) => {
acc[key] = CORE_USERSCRIPT_PLATFORM_KEYS.includes(key);
return acc;
}, {}),
showNotifications: true,
autoProbe: true,
};
function sanitizeDomain(domain) {
return String(domain || "")
.trim()
.replace(/^https?:\/\//i, "")
.replace(/\/+$/g, "");
}
function getUrlObject(url) {
try {
return new URL(url);
} catch (error) {
return null;
}
}
function detectPlatform(url) {
const urlObject = getUrlObject(url);
if (!urlObject) {
return null;
}
return HOST_TO_PLATFORM[urlObject.hostname] || null;
}
function transformPath(path, platformKey) {
if (platformKey !== "crates") {
return path;
}
if (path === "/" || path.startsWith("/?")) {
return path.replace("/", "/api/v1/crates");
}
return `/api/v1/crates${path}`;
}
function isKnownDownloadPattern(urlObject, platformKey) {
const platform = PLATFORM_DEFINITIONS[platformKey];
if (!platform) {
return false;
}
if (platformKey === "gh" && urlObject.hostname !== "github.com") {
return true;
}
return platform.downloadPatterns.some((pattern) =>
pattern.test(urlObject.pathname)
);
}
function hasDownloadableExtension(pathname) {
const lowerPath = String(pathname || "").toLowerCase();
return DOWNLOADABLE_EXTENSIONS.some((extension) =>
lowerPath.endsWith(extension)
);
}
function looksLikeExplicitDownloadText(linkText) {
const normalized = String(linkText || "").trim().toLowerCase();
return (
normalized === "download" ||
normalized === "download file" ||
normalized === "get file" ||
normalized.startsWith("download ")
);
}
function isDownloadCandidate(url, context) {
const details = context || {};
const urlObject = getUrlObject(url);
if (!urlObject) {
return false;
}
const platformKey = detectPlatform(urlObject.href);
if (!platformKey) {
return false;
}
if (details.hasDownloadAttribute || details.downloadAttribute) {
return true;
}
if (isKnownDownloadPattern(urlObject, platformKey)) {
return true;
}
if (hasDownloadableExtension(urlObject.pathname)) {
return true;
}
if (
platformKey === "sf" &&
(urlObject.searchParams.has("use_mirror") ||
String(details.linkText || "").toLowerCase().includes("download"))
) {
return true;
}
return looksLikeExplicitDownloadText(details.linkText);
}
function resolveActiveDomain(settings) {
if (settings.activeDomain) {
return sanitizeDomain(settings.activeDomain);
}
if (settings.xgetDomain) {
return sanitizeDomain(settings.xgetDomain);
}
const profile = (settings.profiles || []).find(
(item) => item.id === settings.activeProfileId
);
if (profile && profile.domain) {
return sanitizeDomain(profile.domain);
}
const fallbackProfile = (settings.profiles || [])[0];
return sanitizeDomain(fallbackProfile && fallbackProfile.domain);
}
function transformDownloadUrl(url, rawSettings) {
const settings = normalizeSettings(rawSettings);
const platformKey = detectPlatform(url);
if (!platformKey || !settings.enabledPlatforms[platformKey]) {
return null;
}
const activeDomain = resolveActiveDomain(settings);
if (!activeDomain) {
return null;
}
const urlObject = getUrlObject(url);
if (!urlObject) {
return null;
}
const transformedPath = transformPath(
urlObject.pathname + urlObject.search + urlObject.hash,
platformKey
);
return `https://${activeDomain}/${platformKey}${transformedPath}`;
}
function shouldBypassClick(event, link) {
if (!event || !link || !link.href) {
return true;
}
if (event.defaultPrevented) {
return true;
}
if (event.button !== 0) {
return true;
}
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
return true;
}
const currentWindowName =
typeof window !== "undefined" ? String(window.name || "") : "";
if (
link.target &&
link.target !== "_self" &&
link.target !== "" &&
link.target !== currentWindowName
) {
return true;
}
return false;
}
function normalizeProfiles(profiles) {
const list = Array.isArray(profiles) ? profiles : [];
const normalized = list
.map((profile, index) => ({
id: String(profile && profile.id ? profile.id : `profile-${index + 1}`),
name: String(
profile && profile.name ? profile.name : `Profile ${index + 1}`
).trim(),
domain: sanitizeDomain(profile && profile.domain),
}))
.filter((profile) => profile.domain);
if (normalized.length > 0) {
return normalized;
}
return DEFAULT_USERSCRIPT_SETTINGS.profiles.map((profile) => ({ ...profile }));
}
function normalizeEnabledPlatforms(enabledPlatforms, mode) {
const source = enabledPlatforms || {};
const defaults =
mode === "userscript"
? DEFAULT_USERSCRIPT_SETTINGS.enabledPlatforms
: DEFAULT_EXTENSION_SETTINGS.enabledPlatforms;
return Object.keys(DEFAULT_EXTENSION_SETTINGS.enabledPlatforms).reduce(
(acc, key) => {
acc[key] =
typeof source[key] === "boolean" ? source[key] : defaults[key];
return acc;
},
{}
);
}
function normalizeSettings(rawSettings, options) {
const opts = options || {};
const input = rawSettings || {};
const mode =
opts.mode ||
(Array.isArray(input.profiles) || Object.prototype.hasOwnProperty.call(input, "activeProfileId")
? "userscript"
: "extension");
if (mode === "userscript") {
const profiles = normalizeProfiles(input.profiles);
const activeProfileId =
profiles.find((profile) => profile.id === input.activeProfileId)?.id ||
profiles[0].id;
const activeDomain = sanitizeDomain(
input.activeDomain ||
profiles.find((profile) => profile.id === activeProfileId)?.domain
);
return {
enabled:
typeof input.enabled === "boolean"
? input.enabled
: DEFAULT_USERSCRIPT_SETTINGS.enabled,
activeProfileId,
profiles,
enabledPlatforms: normalizeEnabledPlatforms(
input.enabledPlatforms,
"userscript"
),
showNotifications:
typeof input.showNotifications === "boolean"
? input.showNotifications
: DEFAULT_USERSCRIPT_SETTINGS.showNotifications,
autoProbe:
typeof input.autoProbe === "boolean"
? input.autoProbe
: DEFAULT_USERSCRIPT_SETTINGS.autoProbe,
activeDomain,
};
}
const xgetDomain = sanitizeDomain(input.xgetDomain);
return {
enabled:
typeof input.enabled === "boolean"
? input.enabled
: DEFAULT_EXTENSION_SETTINGS.enabled,
xgetDomain: xgetDomain || DEFAULT_EXTENSION_SETTINGS.xgetDomain,
enabledPlatforms: normalizeEnabledPlatforms(
input.enabledPlatforms,
"extension"
),
activeDomain: xgetDomain || DEFAULT_EXTENSION_SETTINGS.xgetDomain,
};
}
function getMatchPatterns(platformKeys) {
const allowed = Array.isArray(platformKeys) && platformKeys.length > 0
? new Set(platformKeys)
: null;
return Object.entries(PLATFORM_DEFINITIONS).flatMap(([key, platform]) => {
if (allowed && !allowed.has(key)) {
return [];
}
return platform.hosts.map((host) => `https://${host}/*`);
});
}
return {
CORE_USERSCRIPT_MATCHES,
CORE_USERSCRIPT_PLATFORM_KEYS,
DEFAULT_EXTENSION_SETTINGS,
DEFAULT_USERSCRIPT_SETTINGS,
DOWNLOADABLE_EXTENSIONS,
PLATFORM_DEFINITIONS,
detectPlatform,
getMatchPatterns,
hasDownloadableExtension,
isDownloadCandidate,
normalizeSettings,
sanitizeDomain,
shouldBypassClick,
transformDownloadUrl,
transformPath,
};
});
File renamed without changes.
File renamed without changes.
+593
View File
@@ -0,0 +1,593 @@
(function () {
const core = globalThis.XgetDownloadCore;
if (!core) {
console.error("Xget Now userscript: shared core missing");
return;
}
const STORAGE_KEY = "xget_now_userscript_settings";
const PROBE_CACHE_MS = 5 * 60 * 1000;
const state = {
settings: loadSettings(),
probeCache: {},
toastTimer: null,
};
registerMenuCommands();
document.addEventListener("click", onDocumentClick, true);
function loadSettings() {
return core.normalizeSettings(
typeof GM_getValue === "function" ? GM_getValue(STORAGE_KEY, null) : null,
{ mode: "userscript" }
);
}
function saveSettings(nextSettings) {
state.settings = core.normalizeSettings(nextSettings, { mode: "userscript" });
GM_setValue(STORAGE_KEY, state.settings);
return state.settings;
}
function registerMenuCommands() {
if (typeof GM_registerMenuCommand !== "function") {
return;
}
GM_registerMenuCommand(
state.settings.enabled ? "Disable Xget Now" : "Enable Xget Now",
() => {
const settings = saveSettings({
...state.settings,
enabled: !state.settings.enabled,
});
showToast(
settings.enabled ? "Xget Now userscript enabled" : "Xget Now userscript disabled"
);
}
);
GM_registerMenuCommand("Open Xget Now Settings", () => {
openSettingsModal();
});
GM_registerMenuCommand("Test Current Xget Node", async () => {
const activeProfile = getActiveProfile(state.settings);
if (!activeProfile) {
showToast("No active Xget profile configured", "error");
return;
}
showToast(`Testing ${activeProfile.domain}...`, "info");
const available = await probeDomain(activeProfile.domain, true);
showToast(
available
? `${activeProfile.domain} is reachable`
: `${activeProfile.domain} is unavailable`,
available ? "success" : "error"
);
});
}
function getActiveProfile(settings) {
return (settings.profiles || []).find(
(profile) => profile.id === settings.activeProfileId
);
}
async function probeDomain(domain, force) {
const normalizedDomain = core.sanitizeDomain(domain);
if (!normalizedDomain) {
return false;
}
const cached = state.probeCache[normalizedDomain];
if (
!force &&
cached &&
Date.now() - cached.checkedAt < PROBE_CACHE_MS
) {
return cached.available;
}
const available = await new Promise((resolve) => {
let settled = false;
const finish = (value) => {
if (!settled) {
settled = true;
resolve(value);
}
};
try {
GM_xmlhttpRequest({
method: "GET",
url: `https://${normalizedDomain}/gh/robots.txt`,
headers: {
Range: "bytes=0-0",
"Cache-Control": "no-cache",
},
timeout: 3000,
onload: (response) => {
finish(
response.status === 206 ||
(response.status >= 200 && response.status < 400)
);
},
onerror: () => finish(false),
ontimeout: () => finish(false),
});
} catch (error) {
finish(false);
}
});
state.probeCache[normalizedDomain] = {
available,
checkedAt: Date.now(),
};
return available;
}
function createSyntheticLink(url, sourceLink) {
const link = document.createElement("a");
link.href = url;
link.rel = "noopener noreferrer";
link.target = sourceLink && sourceLink.target ? sourceLink.target : "_blank";
if (sourceLink && (sourceLink.download || sourceLink.hasAttribute("download"))) {
link.download = sourceLink.download || "";
}
link.style.display = "none";
return link;
}
function openViaSyntheticLink(url, sourceLink) {
const link = createSyntheticLink(url, sourceLink);
document.body.appendChild(link);
link.click();
link.remove();
}
async function onDocumentClick(event) {
const link = event.target && event.target.closest
? event.target.closest("a[href]")
: null;
if (!link) {
return;
}
const settings = state.settings;
if (!settings.enabled) {
return;
}
const platformKey = core.detectPlatform(link.href);
if (!platformKey || !settings.enabledPlatforms[platformKey]) {
return;
}
if (core.shouldBypassClick(event, link)) {
return;
}
if (
!core.isDownloadCandidate(link.href, {
hasDownloadAttribute: Boolean(link.download || link.hasAttribute("download")),
linkText: link.textContent || "",
})
) {
return;
}
event.preventDefault();
event.stopPropagation();
if (typeof event.stopImmediatePropagation === "function") {
event.stopImmediatePropagation();
}
const activeProfile = getActiveProfile(settings);
if (!activeProfile) {
showToast("No active Xget profile configured", "error");
openViaSyntheticLink(link.href, link);
return;
}
if (settings.autoProbe) {
const available = await probeDomain(activeProfile.domain);
if (!available) {
showToast(
`${activeProfile.domain} is unavailable. Falling back to the original link.`,
"error"
);
openViaSyntheticLink(link.href, link);
return;
}
}
const acceleratedUrl = core.transformDownloadUrl(link.href, settings);
if (!acceleratedUrl || acceleratedUrl === link.href) {
showToast("Unable to rewrite this link. Falling back to the original URL.", "error");
openViaSyntheticLink(link.href, link);
return;
}
if (settings.showNotifications) {
showToast(`Redirecting download via ${activeProfile.domain}`, "success");
}
openViaSyntheticLink(acceleratedUrl, link);
}
function ensureToastContainer() {
let container = document.getElementById("xget-now-userscript-toast");
if (container) {
return container;
}
container = document.createElement("div");
container.id = "xget-now-userscript-toast";
container.style.cssText = [
"position: fixed",
"top: 20px",
"right: 20px",
"z-index: 2147483647",
"max-width: 320px",
"padding: 12px 14px",
"border-radius: 10px",
"box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18)",
"font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
"background: #0f172a",
"color: #f8fafc",
"opacity: 0",
"transform: translateY(-8px)",
"transition: opacity 120ms ease, transform 120ms ease",
"pointer-events: none",
].join("; ");
document.documentElement.appendChild(container);
return container;
}
function showToast(message, type) {
if (!state.settings.showNotifications && type !== "error" && type !== "info") {
return;
}
const container = ensureToastContainer();
container.textContent = String(message);
const palette = {
success: { background: "#0f766e", color: "#f0fdfa" },
error: { background: "#991b1b", color: "#fef2f2" },
info: { background: "#1d4ed8", color: "#eff6ff" },
};
const colors = palette[type] || palette.success;
container.style.background = colors.background;
container.style.color = colors.color;
container.style.opacity = "1";
container.style.transform = "translateY(0)";
if (state.toastTimer) {
clearTimeout(state.toastTimer);
}
state.toastTimer = setTimeout(() => {
container.style.opacity = "0";
container.style.transform = "translateY(-8px)";
}, type === "error" ? 5000 : 2500);
}
function createSwitch(label, checked) {
const wrapper = document.createElement("label");
wrapper.style.cssText =
"display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 0; cursor:pointer;";
const text = document.createElement("span");
text.textContent = label;
text.style.cssText = "font-size:13px; color:#0f172a;";
const input = document.createElement("input");
input.type = "checkbox";
input.checked = checked;
wrapper.append(text, input);
return { wrapper, input };
}
function createProfileRow(profile, isActive) {
const row = document.createElement("div");
row.className = "xget-profile-row";
row.dataset.profileId = profile.id;
row.style.cssText =
"display:grid; grid-template-columns: 18px minmax(0, 1fr) minmax(0, 1fr) auto; gap:10px; align-items:center; margin-bottom:8px;";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "xget-active-profile";
radio.checked = isActive;
radio.value = profile.id;
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = profile.name;
nameInput.placeholder = "Profile name";
nameInput.style.cssText = profileInputStyles();
nameInput.dataset.field = "name";
const domainInput = document.createElement("input");
domainInput.type = "text";
domainInput.value = profile.domain;
domainInput.placeholder = "xget.example.com";
domainInput.style.cssText = profileInputStyles();
domainInput.dataset.field = "domain";
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.textContent = "Delete";
deleteButton.style.cssText =
"border:0; border-radius:8px; background:#fee2e2; color:#991b1b; padding:8px 10px; cursor:pointer;";
deleteButton.addEventListener("click", () => {
const list = row.parentElement;
row.remove();
const remainingRows = list.querySelectorAll(".xget-profile-row");
if (remainingRows.length > 0 && !list.querySelector('input[type="radio"]:checked')) {
remainingRows[0].querySelector('input[type="radio"]').checked = true;
}
});
row.append(radio, nameInput, domainInput, deleteButton);
return row;
}
function profileInputStyles() {
return [
"width:100%",
"border:1px solid #cbd5e1",
"border-radius:8px",
"padding:8px 10px",
"font-size:13px",
"box-sizing:border-box",
].join("; ");
}
function createSectionTitle(text) {
const title = document.createElement("h3");
title.textContent = text;
title.style.cssText =
"margin:0 0 12px; font-size:14px; font-weight:700; color:#0f172a;";
return title;
}
function createPlatformGrid(settings) {
const grid = document.createElement("div");
grid.style.cssText =
"display:grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap:8px;";
core.CORE_USERSCRIPT_PLATFORM_KEYS.forEach((platformKey) => {
const platform = core.PLATFORM_DEFINITIONS[platformKey];
const switchView = createSwitch(
platform ? platform.name : platformKey,
Boolean(settings.enabledPlatforms[platformKey])
);
switchView.input.dataset.platformKey = platformKey;
switchView.wrapper.style.borderBottom = "1px solid #e2e8f0";
grid.appendChild(switchView.wrapper);
});
return grid;
}
function collectProfiles(profileList) {
return Array.from(profileList.querySelectorAll(".xget-profile-row"))
.map((row) => {
const radio = row.querySelector('input[type="radio"]');
const name = row.querySelector('input[data-field="name"]').value;
const domain = row.querySelector('input[data-field="domain"]').value;
const id = row.dataset.profileId || crypto.randomUUID();
return {
id,
name: String(name || "").trim() || "Unnamed profile",
domain: core.sanitizeDomain(domain),
active: Boolean(radio && radio.checked),
};
})
.filter((profile) => profile.domain);
}
function openSettingsModal() {
const existing = document.getElementById("xget-now-userscript-modal");
if (existing) {
existing.remove();
}
const overlay = document.createElement("div");
overlay.id = "xget-now-userscript-modal";
overlay.style.cssText = [
"position: fixed",
"inset: 0",
"z-index: 2147483646",
"background: rgba(15, 23, 42, 0.45)",
"display: flex",
"justify-content: center",
"align-items: center",
"padding: 20px",
].join("; ");
const panel = document.createElement("div");
panel.style.cssText = [
"width: min(900px, 100%)",
"max-height: min(90vh, 840px)",
"overflow: auto",
"background: #ffffff",
"border-radius: 16px",
"padding: 20px",
"box-shadow: 0 30px 80px rgba(15, 23, 42, 0.25)",
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
].join("; ");
const header = document.createElement("div");
header.style.cssText =
"display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px;";
const title = document.createElement("h2");
title.textContent = "Xget Now Userscript Settings";
title.style.cssText = "margin:0; font-size:18px; color:#0f172a;";
const close = document.createElement("button");
close.type = "button";
close.textContent = "Close";
close.style.cssText =
"border:0; border-radius:8px; background:#e2e8f0; color:#0f172a; padding:10px 12px; cursor:pointer;";
close.addEventListener("click", () => overlay.remove());
header.append(title, close);
panel.appendChild(header);
const togglesSection = document.createElement("section");
togglesSection.style.marginBottom = "18px";
togglesSection.appendChild(createSectionTitle("General"));
const enabledSwitch = createSwitch("Enable Xget Now", state.settings.enabled);
const notificationSwitch = createSwitch(
"Show notifications",
state.settings.showNotifications
);
const autoProbeSwitch = createSwitch(
"Probe node before redirecting downloads",
state.settings.autoProbe
);
togglesSection.append(
enabledSwitch.wrapper,
notificationSwitch.wrapper,
autoProbeSwitch.wrapper
);
panel.appendChild(togglesSection);
const profilesSection = document.createElement("section");
profilesSection.style.marginBottom = "18px";
profilesSection.appendChild(createSectionTitle("Xget Nodes"));
const profileList = document.createElement("div");
state.settings.profiles.forEach((profile) => {
profileList.appendChild(
createProfileRow(profile, profile.id === state.settings.activeProfileId)
);
});
profilesSection.appendChild(profileList);
const addProfile = document.createElement("button");
addProfile.type = "button";
addProfile.textContent = "Add node";
addProfile.style.cssText =
"border:0; border-radius:8px; background:#dbeafe; color:#1d4ed8; padding:10px 12px; cursor:pointer;";
addProfile.addEventListener("click", () => {
const profile = {
id:
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: `profile-${Date.now()}`,
name: "",
domain: "",
};
profileList.appendChild(
createProfileRow(profile, profileList.children.length === 0)
);
});
profilesSection.appendChild(addProfile);
panel.appendChild(profilesSection);
const probeRow = document.createElement("div");
probeRow.style.cssText =
"display:flex; align-items:center; gap:12px; margin: 14px 0 20px;";
const probeButton = document.createElement("button");
probeButton.type = "button";
probeButton.textContent = "Test selected node";
probeButton.style.cssText =
"border:0; border-radius:8px; background:#0f766e; color:#f0fdfa; padding:10px 12px; cursor:pointer;";
const probeResult = document.createElement("span");
probeResult.style.cssText = "font-size:13px; color:#334155;";
probeButton.addEventListener("click", async () => {
const profiles = collectProfiles(profileList);
const active = profiles.find((profile) => profile.active);
if (!active) {
probeResult.textContent = "Select a node first";
return;
}
probeResult.textContent = `Testing ${active.domain}...`;
const available = await probeDomain(active.domain, true);
probeResult.textContent = available
? `${active.domain} is reachable`
: `${active.domain} is unavailable`;
});
probeRow.append(probeButton, probeResult);
panel.appendChild(probeRow);
const platformsSection = document.createElement("section");
platformsSection.style.marginBottom = "20px";
platformsSection.appendChild(createSectionTitle("Platforms"));
platformsSection.appendChild(createPlatformGrid(state.settings));
panel.appendChild(platformsSection);
const footer = document.createElement("div");
footer.style.cssText =
"display:flex; justify-content:flex-end; gap:12px; border-top:1px solid #e2e8f0; padding-top:16px;";
const cancelButton = document.createElement("button");
cancelButton.type = "button";
cancelButton.textContent = "Cancel";
cancelButton.style.cssText =
"border:0; border-radius:8px; background:#e2e8f0; color:#0f172a; padding:10px 12px; cursor:pointer;";
cancelButton.addEventListener("click", () => overlay.remove());
const saveButton = document.createElement("button");
saveButton.type = "button";
saveButton.textContent = "Save";
saveButton.style.cssText =
"border:0; border-radius:8px; background:#1d4ed8; color:#eff6ff; padding:10px 12px; cursor:pointer;";
saveButton.addEventListener("click", () => {
const profiles = collectProfiles(profileList);
if (profiles.length === 0) {
showToast("Add at least one valid Xget node before saving.", "error");
return;
}
const activeProfile = profiles.find((profile) => profile.active) || profiles[0];
const enabledPlatforms = { ...state.settings.enabledPlatforms };
panel
.querySelectorAll("input[data-platform-key]")
.forEach((input) => {
enabledPlatforms[input.dataset.platformKey] = input.checked;
});
saveSettings({
enabled: enabledSwitch.input.checked,
showNotifications: notificationSwitch.input.checked,
autoProbe: autoProbeSwitch.input.checked,
activeProfileId: activeProfile.id,
profiles: profiles.map(({ active, ...profile }) => profile),
enabledPlatforms,
});
overlay.remove();
showToast("Xget Now userscript settings saved", "success");
});
footer.append(cancelButton, saveButton);
panel.appendChild(footer);
overlay.appendChild(panel);
overlay.addEventListener("click", (event) => {
if (event.target === overlay) {
overlay.remove();
}
});
document.documentElement.appendChild(overlay);
}
})();
+451
View File
@@ -0,0 +1,451 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const { chromium } = require("playwright");
const REPO_ROOT = path.resolve(__dirname, "..", "..");
const USER_SCRIPT_PATH = path.join(
REPO_ROOT,
"build",
"userscript",
"xget-now.user.js"
);
function buildUserscript() {
execFileSync("python", ["build.py", "--platform", "userscript"], {
cwd: REPO_ROOT,
stdio: "pipe",
});
assert.equal(
fs.existsSync(USER_SCRIPT_PATH),
true,
`Expected userscript artifact at ${USER_SCRIPT_PATH}`
);
return fs.readFileSync(USER_SCRIPT_PATH, "utf8");
}
function createUserscriptSettings(overrides = {}) {
return {
enabled: true,
activeProfileId: "test-profile",
profiles: [
{ id: "default", name: "Default", domain: "xget.xi-xu.me" },
{ id: "test-profile", name: "Test Profile", domain: "xget.test" },
],
enabledPlatforms: {
gh: true,
gl: true,
gitea: true,
codeberg: true,
sf: true,
hf: true,
pypi: true,
"pypi-files": true,
npm: true,
},
showNotifications: true,
autoProbe: true,
...overrides,
};
}
async function createHarness(storeOverrides = {}, xgetAvailable = true) {
const userscriptSource = buildUserscript();
const browser = await chromium.launch({
channel: "chromium",
headless: false,
});
const context = await browser.newContext({
viewport: { width: 1440, height: 1800 },
});
const seenRequests = [];
const probeRequests = [];
await context.exposeBinding("__xgetGMRequest", async (_source, request) => {
if (request.url === "https://xget.test/gh/robots.txt") {
probeRequests.push(request.url);
if (!xgetAvailable) {
return {
ok: false,
error: "Xget probe failed",
};
}
return {
ok: true,
status: 204,
responseText: "",
finalUrl: request.url,
};
}
const controller = new AbortController();
const timeoutMs = Number(request.timeout || 0);
let timeoutId = null;
if (timeoutMs > 0) {
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
}
try {
const response = await fetch(request.url, {
method: request.method || "GET",
headers: request.headers || {},
signal: controller.signal,
});
return {
ok: true,
status: response.status,
responseText: await response.text(),
finalUrl: request.url,
};
} catch (error) {
return {
ok: false,
error:
error && typeof error === "object" && "message" in error
? String(error.message)
: String(error),
};
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
});
await context.route("https://xget.test/**", async (route) => {
const request = route.request();
seenRequests.push(request.url());
if (!xgetAvailable) {
await route.abort("failed");
return;
}
await route.fulfill({
status: 200,
contentType: "text/html",
headers: {
"access-control-allow-origin": "*",
},
body: "<html><body>ok</body></html>",
});
});
await context.addInitScript(
({ initialStore }) => {
const store = { ...initialStore };
globalThis.GM_getValue = (key, fallback) =>
Object.prototype.hasOwnProperty.call(store, key) ? store[key] : fallback;
globalThis.GM_setValue = (key, value) => {
store[key] = value;
};
globalThis.GM_deleteValue = (key) => {
delete store[key];
};
globalThis.GM_registerMenuCommand = () => {};
globalThis.__XGET_TEST_STORE__ = store;
globalThis.GM_xmlhttpRequest = (options) => {
let aborted = false;
const request = {
url: options.url,
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 0,
};
globalThis.__xgetGMRequest(request)
.then((result) => {
if (aborted) {
return;
}
if (result.ok) {
options.onload?.({
status: result.status,
responseText: result.responseText,
finalUrl: result.finalUrl,
});
return;
}
options.onerror?.(new Error(result.error || "GM_xmlhttpRequest failed"));
})
.catch((error) => {
if (!aborted) {
options.onerror?.(error);
}
});
return {
abort() {
aborted = true;
},
};
};
},
{
initialStore: {
xget_now_userscript_settings: createUserscriptSettings(storeOverrides),
},
}
);
await context.addInitScript({ content: userscriptSource });
return { browser, context, probeRequests, seenRequests };
}
async function clickRoleAndWaitForRequest(page, seenRequests, roleName, expectedUrl) {
const locatorOptions =
typeof roleName === "string"
? { name: roleName, exact: true }
: { name: roleName };
await page.getByRole("link", locatorOptions).click();
await waitForRequest(seenRequests, expectedUrl);
}
async function clickSelectorAndWaitForRequest(page, seenRequests, selector, expectedUrl) {
await page.locator(selector).click();
await waitForRequest(seenRequests, expectedUrl);
}
async function waitForRequest(seenRequests, expectedUrl, timeoutMs = 15000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (seenRequests.includes(expectedUrl)) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
assert.fail(`Timed out waiting for request: ${expectedUrl}`);
}
test("GitHub release asset clicks rewrite to Xget", async () => {
const harness = await createHarness();
const { browser, context, seenRequests } = harness;
try {
const page = await context.newPage();
await page.goto("https://github.com/BurntSushi/ripgrep/releases/tag/14.1.1", {
waitUntil: "domcontentloaded",
});
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(5000);
const expectedUrl =
"https://xget.test/gh/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz";
await clickRoleAndWaitForRequest(
page,
seenRequests,
"ripgrep-14.1.1-aarch64-apple-darwin.tar.gz",
expectedUrl
);
assert.equal(
seenRequests.includes(expectedUrl),
true
);
} finally {
await browser.close();
}
});
test("GitHub source archive clicks rewrite to Xget", async () => {
const harness = await createHarness();
const { browser, context, seenRequests } = harness;
try {
const page = await context.newPage();
await page.goto("https://github.com/BurntSushi/ripgrep/releases/tag/14.1.1", {
waitUntil: "domcontentloaded",
});
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(5000);
const expectedUrl =
"https://xget.test/gh/BurntSushi/ripgrep/archive/refs/tags/14.1.1.zip";
await clickRoleAndWaitForRequest(page, seenRequests, "Source code (zip)", expectedUrl);
assert.equal(
seenRequests.includes(expectedUrl),
true
);
} finally {
await browser.close();
}
});
test("PyPI wheel clicks rewrite to Xget", async () => {
const harness = await createHarness();
const { browser, context, seenRequests } = harness;
try {
const page = await context.newPage();
await page.goto("https://pypi.org/project/requests/2.32.3/#files", {
waitUntil: "domcontentloaded",
});
await page.waitForTimeout(3000);
const expectedUrl =
"https://xget.test/pypi-files/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl";
await clickRoleAndWaitForRequest(
page,
seenRequests,
"requests-2.32.3-py3-none-any.whl",
expectedUrl
);
assert.equal(
seenRequests.includes(expectedUrl),
true
);
} finally {
await browser.close();
}
});
test("Hugging Face file clicks rewrite to Xget", async () => {
const harness = await createHarness();
const { browser, context, seenRequests } = harness;
try {
const page = await context.newPage();
await page.goto("https://huggingface.co/openai-community/gpt2/tree/main", {
waitUntil: "domcontentloaded",
});
await page.waitForTimeout(5000);
const expectedUrl =
"https://xget.test/hf/openai-community/gpt2/resolve/main/config.json?download=true";
await clickSelectorAndWaitForRequest(
page,
seenRequests,
'a[href="/openai-community/gpt2/resolve/main/config.json?download=true"]',
expectedUrl
);
assert.equal(
seenRequests.includes(expectedUrl),
true
);
} finally {
await browser.close();
}
});
test("SourceForge download clicks rewrite to Xget", async () => {
const harness = await createHarness();
const { browser, context, seenRequests } = harness;
try {
const page = await context.newPage();
await page.goto(
"https://sourceforge.net/projects/sevenzip/files/7-Zip/24.09/",
{ waitUntil: "domcontentloaded" }
);
await page.waitForTimeout(5000);
const expectedUrl =
"https://xget.test/sf/projects/sevenzip/files/7-Zip/24.09/7z2409-x64.exe/download";
await clickRoleAndWaitForRequest(
page,
seenRequests,
/7z2409-x64\.exe/i,
expectedUrl
);
assert.equal(
seenRequests.includes(expectedUrl),
true
);
} finally {
await browser.close();
}
});
test("modifier clicks bypass userscript interception", async () => {
const harness = await createHarness();
const { browser, context, seenRequests } = harness;
try {
const page = await context.newPage();
await context.route(
"https://github.com/BurntSushi/ripgrep/archive/refs/tags/14.1.1.zip",
async (route) => {
await route.fulfill({
status: 200,
contentType: "text/html",
body: "<html><body>original</body></html>",
});
}
);
await page.goto("https://github.com/BurntSushi/ripgrep/releases/tag/14.1.1", {
waitUntil: "domcontentloaded",
});
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(5000);
await page.getByRole("link", { name: "Source code (zip)" }).click({
modifiers: ["Control"],
});
await page.waitForTimeout(2000);
assert.equal(
seenRequests.some((url) =>
url.includes("xget.test/gh/BurntSushi/ripgrep/archive/refs/tags/14.1.1.zip")
),
false
);
} finally {
await browser.close();
}
});
test("falls back to the original URL when the Xget node is unavailable", async () => {
const harness = await createHarness({}, false);
const { browser, context, probeRequests, seenRequests } = harness;
try {
const page = await context.newPage();
let originalFallbackRequested = false;
await context.route(
"https://github.com/BurntSushi/ripgrep/archive/refs/tags/14.1.1.zip",
async (route) => {
originalFallbackRequested = true;
await route.fulfill({
status: 200,
contentType: "text/html",
body: "<html><body>original fallback</body></html>",
});
}
);
await page.goto("https://github.com/BurntSushi/ripgrep/releases/tag/14.1.1", {
waitUntil: "domcontentloaded",
});
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(5000);
await page.getByRole("link", { name: "Source code (zip)" }).click();
await page.waitForTimeout(2000);
assert.equal(
probeRequests.includes("https://xget.test/gh/robots.txt"),
true
);
assert.equal(
seenRequests.some((url) =>
url.includes("xget.test/gh/BurntSushi/ripgrep/archive/refs/tags/14.1.1.zip")
),
false
);
assert.equal(originalFallbackRequested, true);
assert.equal(
page.url(),
"https://github.com/BurntSushi/ripgrep/releases/tag/14.1.1"
);
} finally {
await browser.close();
}
});
+195
View File
@@ -0,0 +1,195 @@
const test = require("node:test");
const assert = require("node:assert/strict");
function loadCore() {
try {
return require("../../src/shared/download-core.js");
} catch (error) {
assert.fail(`download-core.js is not ready: ${error.message}`);
}
}
test("detectPlatform recognizes GitHub helper hosts", () => {
const core = loadCore();
assert.equal(
core.detectPlatform(
"https://objects.githubusercontent.com/github-production-release-asset-2e65be/123/file.zip"
),
"gh"
);
assert.equal(
core.detectPlatform("https://codeload.github.com/owner/repo/zip/refs/tags/v1.0.0"),
"gh"
);
assert.equal(
core.detectPlatform("https://raw.githubusercontent.com/owner/repo/main/file.txt"),
"gh"
);
});
test("isDownloadCandidate recognizes supported core platform downloads", () => {
const core = loadCore();
assert.equal(
core.isDownloadCandidate(
"https://github.com/owner/repo/releases/download/v1.0.0/app.zip",
{ linkText: "app.zip" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://github.com/owner/repo/archive/refs/tags/v1.0.0.tar.gz",
{ linkText: "Source code (tar.gz)" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://gitlab.com/owner/repo/-/archive/v1.0.0/repo-v1.0.0.zip",
{ linkText: "Download archive" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://codeberg.org/owner/repo/releases/download/v1.0.0/app.tar.gz",
{ linkText: "Download" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://huggingface.co/openai-community/gpt2/resolve/main/model.safetensors?download=true",
{ linkText: "548 MB" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://files.pythonhosted.org/packages/hash/app-1.0.0-py3-none-any.whl",
{ linkText: "app-1.0.0-py3-none-any.whl" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://registry.npmjs.org/react/-/react-19.0.0.tgz",
{ linkText: "react-19.0.0.tgz" }
),
true
);
assert.equal(
core.isDownloadCandidate(
"https://sourceforge.net/projects/sevenzip/files/7-Zip/24.09/7z2409-x64.exe/download",
{ linkText: "Download Latest Version" }
),
true
);
});
test("isDownloadCandidate rejects obvious non-download links", () => {
const core = loadCore();
assert.equal(
core.isDownloadCandidate("https://github.com/owner/repo/releases", {
linkText: "Releases",
}),
false
);
assert.equal(
core.isDownloadCandidate("https://pypi.org/project/requests/2.32.3/", {
linkText: "requests 2.32.3",
}),
false
);
});
test("transformDownloadUrl rewrites supported URLs to the active Xget domain", () => {
const core = loadCore();
const settings = core.normalizeSettings({
xgetDomain: "xget.example.com",
enabledPlatforms: { gh: true, pypi: true, "pypi-files": true, crates: true },
});
assert.equal(
core.transformDownloadUrl(
"https://github.com/owner/repo/releases/download/v1.0.0/app.zip",
settings
),
"https://xget.example.com/gh/owner/repo/releases/download/v1.0.0/app.zip"
);
assert.equal(
core.transformDownloadUrl(
"https://files.pythonhosted.org/packages/hash/app-1.0.0.tar.gz",
settings
),
"https://xget.example.com/pypi-files/packages/hash/app-1.0.0.tar.gz"
);
assert.equal(
core.transformDownloadUrl(
"https://crates.io/serde/1.0.0/download",
settings
),
"https://xget.example.com/crates/api/v1/crates/serde/1.0.0/download"
);
});
test("shouldBypassClick preserves modifier and non-left-click behavior", () => {
const core = loadCore();
const link = { href: "https://github.com/owner/repo/releases/download/v1.0.0/app.zip" };
assert.equal(
core.shouldBypassClick(
{ button: 1, defaultPrevented: false, ctrlKey: false, metaKey: false, shiftKey: false, altKey: false },
link
),
true
);
assert.equal(
core.shouldBypassClick(
{ button: 0, defaultPrevented: false, ctrlKey: true, metaKey: false, shiftKey: false, altKey: false },
link
),
true
);
assert.equal(
core.shouldBypassClick(
{ button: 0, defaultPrevented: false, ctrlKey: false, metaKey: false, shiftKey: false, altKey: false },
link
),
false
);
});
test("normalizeSettings supports extension and userscript shapes", () => {
const core = loadCore();
const extensionSettings = core.normalizeSettings({
xgetDomain: "https://xget.example.com/",
enabledPlatforms: { gh: true, gl: false },
});
assert.equal(extensionSettings.xgetDomain, "xget.example.com");
assert.equal(extensionSettings.enabledPlatforms.gh, true);
assert.equal(extensionSettings.enabledPlatforms.gl, false);
assert.equal(extensionSettings.activeDomain, "xget.example.com");
const userscriptSettings = core.normalizeSettings(
{
activeProfileId: "custom",
profiles: [
{ id: "default", name: "Default", domain: "xget.xi-xu.me" },
{ id: "custom", name: "Custom", domain: "https://xget.alt.example/" },
],
enabledPlatforms: { gh: true, hf: true },
autoProbe: false,
},
{ mode: "userscript" }
);
assert.equal(userscriptSettings.activeProfileId, "custom");
assert.equal(userscriptSettings.activeDomain, "xget.alt.example");
assert.equal(userscriptSettings.enabledPlatforms.hf, true);
assert.equal(userscriptSettings.autoProbe, false);
});