omegaee/my-fingerprint/main 58k tokens More Tools
```
├── .github/
   ├── FUNDING.yml
   ├── workflows/
      ├── build.yml (300 tokens)
├── .gitignore (100 tokens)
├── LICENSE (omitted)
├── README.md (1100 tokens)
├── README_EN.md (1600 tokens)
├── _locales/
   ├── en/
      ├── messages.json
   ├── zh/
      ├── messages.json
├── docs/
   ├── supporters.zh.md
├── example/
   ├── config/
      ├── template.json (200 tokens)
   ├── presets/
      ├── index.json (100 tokens)
      ├── known-issues.json
├── images/
   ├── en/
      ├── ui.png
   ├── wechat-code.png
   ├── zh/
      ├── ui.png
├── manifest.ts (400 tokens)
├── package-lock.json (omitted)
├── package.json (200 tokens)
├── plugins/
   ├── core-bundle.ts (300 tokens)
├── postcss.config.js
├── public/
   ├── config.json
   ├── logo-gray.png
   ├── logo.png
   ├── presets/
      ├── default.json (200 tokens)
      ├── incognito.json (200 tokens)
      ├── index.json (200 tokens)
   ├── settings/
      ├── client-hints.json (2.5k tokens)
      ├── gpu-info.json (600 tokens)
      ├── timezone.json (1000 tokens)
├── src/
   ├── api/
      ├── github.ts (200 tokens)
      ├── local.ts (200 tokens)
   ├── background/
      ├── badge.ts (200 tokens)
      ├── index.ts (700 tokens)
      ├── request.ts (1400 tokens)
      ├── script.ts (400 tokens)
      ├── storage.ts (1400 tokens)
   ├── components/
      ├── data/
         ├── highlight.tsx (200 tokens)
         ├── markdown.tsx (100 tokens)
         ├── tip-icon.tsx (200 tokens)
      ├── feedback/
         ├── var-popconfirm.tsx (200 tokens)
   ├── core/
      ├── .gitignore
      ├── core.ts (2.2k tokens)
      ├── index.ts (400 tokens)
      ├── output.d.ts (omitted)
      ├── tasks.ts (7k tokens)
      ├── utils.ts (1600 tokens)
   ├── locales/
      ├── en_US.json (1700 tokens)
      ├── index.ts (100 tokens)
      ├── zh_CN.json (1100 tokens)
   ├── popup/
      ├── App.tsx (1300 tokens)
      ├── config/
         ├── group/
            ├── prefs.tsx (400 tokens)
            ├── script.tsx (700 tokens)
            ├── strong.tsx (900 tokens)
            ├── weak.tsx (1300 tokens)
         ├── index.tsx (400 tokens)
         ├── item.tsx (600 tokens)
         ├── special/
            ├── client-hints.tsx (2.4k tokens)
            ├── gpu-info.tsx (900 tokens)
            ├── timezone.tsx (1000 tokens)
         ├── styles.ts (200 tokens)
      ├── css/
         ├── github-markdown.css (5.9k tokens)
      ├── index.css (600 tokens)
      ├── index.html (100 tokens)
      ├── main.tsx (100 tokens)
      ├── more/
         ├── config.tsx (400 tokens)
         ├── index.tsx (300 tokens)
         ├── permission.tsx (700 tokens)
         ├── preset.tsx (1300 tokens)
         ├── subscribe.tsx (500 tokens)
      ├── record/
         ├── fp-item.tsx (200 tokens)
         ├── fp.tsx (700 tokens)
         ├── iframe-item.tsx (100 tokens)
         ├── iframe.tsx (100 tokens)
         ├── index.tsx (400 tokens)
      ├── stores/
         ├── prefs.ts (600 tokens)
         ├── storage.ts (900 tokens)
      ├── whitelist/
         ├── index.tsx (900 tokens)
         ├── item.tsx (200 tokens)
   ├── scripts/
      ├── content.ts (400 tokens)
   ├── types/
      ├── base.d.ts (omitted)
      ├── browser.d.ts (omitted)
      ├── data.d.ts (omitted)
      ├── enum.ts
      ├── hook.d.ts (omitted)
      ├── message.d.ts (omitted)
      ├── storage.d.ts (omitted)
   ├── utils/
      ├── base.ts (1500 tokens)
      ├── browser.ts (200 tokens)
      ├── equipment.ts
      ├── hooks/
         ├── async.ts (100 tokens)
         ├── config.ts (500 tokens)
         ├── index.ts
         ├── options.tsx (200 tokens)
      ├── message.ts (200 tokens)
      ├── net.ts (400 tokens)
      ├── storage.ts (100 tokens)
      ├── style.ts
      ├── timer.ts (300 tokens)
   ├── vite-env.d.ts (omitted)
├── tailwind.config.js (300 tokens)
├── tsconfig.json (100 tokens)
├── tsconfig.node.json
├── updates.xml (100 tokens)
├── vite.config.ts (200 tokens)
```


## /.github/FUNDING.yml

```yml path="/.github/FUNDING.yml" 
ko_fi: omegaee
```

## /.github/workflows/build.yml

```yml path="/.github/workflows/build.yml" 
name: release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npx update-browserslist-db@latest

      - name: Build Chrome 
        run: npm run build || npm run build

      - name: Build Firefox
        run: npm run build:firefox || npm run build:firefox

      - name: Package Chrome artifacts
        run: |
          cd dist
          # Chrome
          zip -r ../my-fingerprint-chrome-${{ github.ref_name }}.zip .
          # Chrome Webstore
          jq 'del(.key, .update_url)' manifest.json > manifest.tmp.json && mv manifest.tmp.json manifest.json
          zip -r ../my-fingerprint-chrome-webstore-${{ github.ref_name }}.zip .
          cd ..

      - name: Package Firefox artifacts
        run: |
          cd dist-firefox && zip -r ../my-fingerprint-firefox-${{ github.ref_name }}.zip . && cd ..

      - name: Upload internal artifacts
        uses: actions/upload-artifact@v4
        with:
          name: store-uploads
          path: |
            my-fingerprint-chrome-webstore-${{ github.ref_name }}.zip
            my-fingerprint-firefox-${{ github.ref_name }}.zip

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          generate_release_notes: true
          draft: true
          files: |
            my-fingerprint-chrome-${{ github.ref_name }}.zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

```

## /.gitignore

```gitignore path="/.gitignore" 
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
dist-*/
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Release
dist.pem
releases
```

## /README.md

<h4 align="center">
简体中文 | <a href="./README_EN.md">English</a>
</h4>

<hr/>

<h1 align="center">My Fingerprint</h1>

<br/>

<p align="center">
<strong>
保护你的浏览器指纹,提升隐私安全。支持 <code>Chrome</code>、<code>Edge</code>、<code>Firefox</code>。
</strong>
</p>

<p align="center">
<strong>
轻量、零干扰的浏览器扩展,基于 Manifest V3 构建。
</strong>
</p>

<br/>

<p align="center">
<a href="https://github.com/omegaee/my-fingerprint/releases">
  <img alt="Latest Release" src="https://img.shields.io/github/v/release/omegaee/my-fingerprint?style=flat">
</a>
<a href="https://github.com/omegaee/my-fingerprint/stargazers">
  <img alt="Stars" src="https://img.shields.io/github/stars/omegaee/my-fingerprint?style=flat">
</a>
<a href="https://github.com/omegaee/my-fingerprint/issues">
  <img alt="Issues" src="https://img.shields.io/github/issues/omegaee/my-fingerprint?style=flat">
</a>
<a href="https://github.com/omegaee/my-fingerprint/blob/main/LICENSE">
  <img alt="License" src="https://img.shields.io/github/license/omegaee/my-fingerprint?style=flat">
</a>
</p>

<p align="center">
  <a href="https://microsoftedge.microsoft.com/addons/detail/mikeajonghdjobhfokpleagjockmmgdk">
    <img src="https://img.shields.io/badge/Edge-Addon-blue?logo=microsoftedge" alt="Edge Addon" />
  </a>
  <a href="https://addons.mozilla.org/firefox/addon/my-fingerprint/">
    <img src="https://img.shields.io/badge/Firefox-Addon-orange?logo=firefox" alt="Firefox Addon" />
  </a>
  <a href="https://addons.mozilla.org/android/addon/my-fingerprint/">
    <img src="https://img.shields.io/badge/Firefox%20for%20Android-Addon-orange?logo=firefoxbrowser" alt="Firefox for Android Addon" />
  </a>
  <img src="https://img.shields.io/badge/Chrome-Manual%20Install-lightgrey?logo=googlechrome" alt="Chrome Manual Install" />
</p>

<p align="center">
<a href="./docs/supporters.zh.md">
  <img src="https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1%E8%B5%9E%E8%B5%8F-%E6%94%AF%E6%8C%81%E4%BD%9C%E8%80%85-07C160?style=for-the-badge&logo=wechat" alt="微信赞赏支持作者" />
</a>
&nbsp;
<a href="https://ko-fi.com/omegaee">
  <img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support me on Ko-fi" />
</a>
</p>

---

<h5 align="center">
  <a href="#features">✨ 功能</a> |
  <a href="#fingerprint">🧬 指纹</a> |
  <a href="#installation">🧰 安装</a> |
  <a href="#configuration">⚙️ 配置</a> |
  <a href="#testing">🧪 测试</a> |
  <a href="#development">🛠️ 开发</a> |
  <a href="#faq">❓ 常见问题</a> |
  <a href="#community">🌱 社区</a> |
  <a href="#support">💝 支持</a> |
  <a href="#disclaimer">📜 声明</a> |
  <a href="#credits">🙏 鸣谢</a>
</h5>


## ✨ 功能特点 <a id="features"></a>

- 🚀 支持 Chrome、Edge、Firefox 浏览器
- ⚙️ 安装即生效,无需额外配置
- 📦 基于 Manifest V3,兼容性强
- 🔍 可监控页面对指纹 API 的访问情况
- 🧱 支持白名单控制与自定义配置
- 📤 提供配置导入导出与订阅功能
- 🧩 轻量级原生注入,零依赖,性能开销极低

## 🧬 指纹保护 <a id="fingerprint"></a>

- Canvas 指纹
- WebGL 指纹
- Audio 指纹
- Fonts 指纹
- WebRTC 保护
- WebGPU 指纹
- DomRect 指纹
- 语言与时区
- 图形驱动信息
- UserAgent
- 屏幕尺寸与分辨率

## 🧰 安装指南 <a id="installation"></a>

### 🧩 扩展商店安装

💡 扩展商店的更新可能因审核延迟而滞后于 GitHub。

- [Edge](https://microsoftedge.microsoft.com/addons/detail/mikeajonghdjobhfokpleagjockmmgdk)
- [Firefox](https://addons.mozilla.org/firefox/addon/my-fingerprint/)
- [Firefox for Android](https://addons.mozilla.org/android/addon/my-fingerprint/)
- Chrome: 尚未上架,可通过手动安装使用

### 📦 手动安装

#### Chrome / Edge

- 浏览器版本要求:`Chrome/Edge 102+`
- 推荐版本:120+
- [下载扩展包](https://github.com/omegaee/my-fingerprint/releases/latest) `.zip` 文件 → 拖入扩展管理页面 → 启用扩展
- 可选:启用无痕模式支持

#### Firefox

- 浏览器版本要求:`Firefox 136+`
- [下载扩展包](https://github.com/omegaee/my-fingerprint/releases/latest) `.xpi` 文件 → 拖入浏览器窗口 → 点击添加

## ⚙️ 配置模块 <a id="configuration"></a>

扩展支持灵活的配置选项,可通过图标进入设置面板进行调整:

- 强指纹组
  - 模拟高度唯一的用户特征,通常与其他指纹项或 IP 信息联合使用

- 弱指纹组
  - 获取重复率高的基础信息,适合轻度保护场景

- 脚本配置
  - 全局种子:用于“根据全局种子随机值”选项,确保生成结果一致性  
  - 注入模式:推荐启用“快速注入”以提升兼容性与性能

- 白名单机制
  - 支持编辑白名单列表  
  - 支持子域名匹配:如 `example.com` 可匹配 `*.example.com`、`*.*.example.com`

- 订阅机制
  - 可使用配置模板进行初始化(订阅一次后可关闭)  
    - [标准模式 - 默认配置](https://raw.githubusercontent.com/omegaee/my-fingerprint/main/example/config/template.json)  
  - 空值表示关闭订阅  
  - 支持手动订阅或扩展启动时自动拉取远程配置(JSON 格式)  
  - 订阅配置将覆盖原设置,并合并白名单内容

## 🧪 测试目标 <a id="testing"></a>

- [webbrowsertools.com](https://webbrowsertools.com/)
- [browserleaks.com](https://browserleaks.com/)
- [CreepJS](https://abrahamjuliot.github.io/creepjs/)
- [browserscan.net](https://www.browserscan.net/)
- [yalala.com](https://www.yalala.com/)
- [uutool.cn](https://uutool.cn/browser/)

## 🛠️ 开发 <a id="development"></a>

```bash
cd <project>
npm install
npm run dev          # Chrome / Edge
npm run dev:firefox  # Firefox
```


## ❓ 常见问题 <a id="faq"></a>

**Q: 某些页面扩展不生效或出现异常怎么办?**
> A: 可在脚本配置中启用“快速注入模式”以提升兼容性。若页面异常,可将其加入白名单,或尝试应用配置预设中的“已知问题列表”进行修复。

**Q: 为什么需要这个扩展?**
> A: 浏览器指纹可被网站用于跨站追踪,影响隐私。这个扩展通过伪装关键指纹信息,降低被识别和跟踪的风险。

**Q: 这个扩展和指纹浏览器有什么区别?**
> A: 一款优秀的指纹浏览器可以模拟完整环境实现深度伪装,更适合反检测场景;本扩展基于 JS 注入,轻量易用,兼容大部分指纹保护场景。

**Q: 浏览器指纹是用户的唯一标识么?**
> A: 指纹本身不是绝对唯一,但多个指纹项组合后可高度识别用户,尤其与 IP、浏览器存储 等数据结合时。

**Q: 配置保护的指纹越多越好么?**
> A: 不一定。多数情况下,仅修改一项关键指纹即可打断追踪链。过度伪装可能影响网页功能或暴露异常行为,建议按需选择保护项。

**Q: 强指纹和弱指纹有什么区别?**
> A: 强指纹具有高度唯一性,修改后能显著提升隐私保护;弱指纹识别力较低,多数情况下无需修改,避免影响兼容性。


## 🌱 社区 <a id="community"></a>

- 欢迎通过 Issues 和 Pull Requests 提交建议与反馈
- [![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-971379868-fedcba?style=flat-square&logo=qq&logoColor=white)](https://qm.qq.com/q/hxchiOUTtu)

## 💝 支持一下 <a id="support"></a>

- 如果你觉得项目有帮助,请点个 Star ⭐
- [微信赞赏-支持作者](./docs/supporters.zh.md)
- [Support me on Ko-fi](https://ko-fi.com/omegaee)

## 📜 声明 <a id="disclaimer"></a>

本项目仅用于学习与研究目的。请勿用于非法用途,开发者不对任何损失或问题负责。

## 🙏 鸣谢 <a id="credits"></a>

感谢所有贡献者与支持者。特别感谢开源社区的力量!


## /README_EN.md

<h4 align="center">
<a href="./README.md">简体中文</a> | English
</h4>

<hr/>

<h1 align="center">My Fingerprint</h1>

<br/>

<p align="center">
<strong>
Protect your browser fingerprints and enhance privacy. <code>Chrome</code>, <code>Edge</code>, <code>Firefox</code> supported.
</strong>
</p>

<p align="center">
<strong>
A lightweight, zero-disruption browser extension built on Manifest V3.
</strong>
</p>

<br/>

<p align="center">
<a href="https://github.com/omegaee/my-fingerprint/releases">
  <img alt="Latest Release" src="https://img.shields.io/github/v/release/omegaee/my-fingerprint?style=flat">
</a>
<a href="https://github.com/omegaee/my-fingerprint/stargazers">
  <img alt="Stars" src="https://img.shields.io/github/stars/omegaee/my-fingerprint?style=flat">
</a>
<a href="https://github.com/omegaee/my-fingerprint/issues">
  <img alt="Issues" src="https://img.shields.io/github/issues/omegaee/my-fingerprint?style=flat">
</a>
<a href="https://github.com/omegaee/my-fingerprint/blob/main/LICENSE">
  <img alt="License" src="https://img.shields.io/github/license/omegaee/my-fingerprint?style=flat">
</a>
</p>

<p align="center">
  <a href="https://microsoftedge.microsoft.com/addons/detail/mikeajonghdjobhfokpleagjockmmgdk">
    <img src="https://img.shields.io/badge/Edge-Addon-blue?logo=microsoftedge" alt="Edge Addon" />
  </a>
  <a href="https://addons.mozilla.org/firefox/addon/my-fingerprint/">
    <img src="https://img.shields.io/badge/Firefox-Addon-orange?logo=firefox" alt="Firefox Addon" />
  </a>
  <a href="https://addons.mozilla.org/android/addon/my-fingerprint/">
    <img src="https://img.shields.io/badge/Firefox%20for%20Android-Addon-orange?logo=firefoxbrowser" alt="Firefox for Android Addon" />
  </a>
  <img src="https://img.shields.io/badge/Chrome-Manual%20Install-lightgrey?logo=googlechrome" alt="Chrome Manual Install" />
</p>

<p align="center">
<a href="https://ko-fi.com/omegaee">
  <img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support me on Ko-fi" />
</a>
</p>

---

<h5 align="center">
  <a href="#features">✨ Features</a> |
  <a href="#fingerprint">🧬 Fingerprint</a> |
  <a href="#installation">🧰 Installation</a> |
  <a href="#configuration">⚙️ Configuration</a> |
  <a href="#testing">🧪 Testing</a> |
  <a href="#development">🛠️ Development</a> |
  <a href="#faq">❓ FAQ</a> |
  <a href="#support">💝 Support</a> |
  <a href="#disclaimer">📜 Disclaimer</a> |
  <a href="#credits">🙏 Credits</a>
</h5>


## ✨ Features <a id="features"></a>

- 🚀 Supports Chrome, Edge, and Firefox
- ⚙️ Works instantly upon installation, no configuration required
- 📦 Built on Manifest V3 for modern compatibility
- 🔍 Monitors fingerprint API access on web pages
- 🧱 Customizable protection rules and whitelist support
- 📤 Import/export configuration and subscription support
- 🧩 Lightweight native injection, zero dependencies, negligible performance cost

## 🧬 Fingerprint Protection <a id="fingerprint"></a>

- Canvas fingerprint
- Audio fingerprint
- Fonts fingerprint
- WebGL fingerprint
- WebRTC protection
- WebGPU fingerprint
- DomRect fingerprint
- Language and timezone
- Graphics driver info
- UserAgent series
- Screen size and resolution

## 🧰 Installation <a id="installation"></a>

### 🧩 Extension Store Installation

💡 Extension store updates may lag behind GitHub due to review delays.

- [Edge](https://microsoftedge.microsoft.com/addons/detail/mikeajonghdjobhfokpleagjockmmgdk)
- [Firefox](https://addons.mozilla.org/firefox/addon/my-fingerprint/)
- [Firefox for Android](https://addons.mozilla.org/android/addon/my-fingerprint/)
- Chrome: Not yet available. Please use manual installation.

### 📦 Manual Installation

#### Chrome / Edge

- Required version: `Chrome/Edge 102+`
- Recommended: 120+
- [Download](https://github.com/omegaee/my-fingerprint/releases/latest) `.zip` → Drag into extension manager → Enable
- Optional: Enable in Incognito/InPrivate mode

#### Firefox

- Required version: `Firefox 136+`
- [Download](https://github.com/omegaee/my-fingerprint/releases/latest) `.xpi` → Drag into browser window → Click “Add”

## ⚙️ Configuration Module <a id="configuration"></a>

This module provides flexible options for customizing fingerprint protection behavior:

- Strong Fingerprint Group
  - Simulates highly unique user characteristics  
  - Typically used in combination with other fingerprints or IP data

- Weak Fingerprint Group
  - Captures basic, high-repetition information  
  - Suitable for lightweight protection scenarios

- Script Settings
  - Global Seed: Used for the “Random by Global Seed” option to ensure consistent output  
  - Injection Mode: Recommended to enable “Fast Injection” for better compatibility and performance

- Whitelist Management
  - Supports editing whitelist entries  
  - Subdomain matching supported: e.g., `example.com` matches `*.example.com`, `*.*.example.com`

- Subscription Options
  - Use configuration templates for quick setup (subscription can be disabled after initial use)  
    - [Standard Mode – Default Template](https://raw.githubusercontent.com/omegaee/my-fingerprint/main/example/config/template.json)  
  - Empty value disables subscription  
  - Supports manual subscription or auto-fetching remote config (JSON format) on extension startup  
  - Subscription config will override existing settings and merge whitelist entries

## 🧪 Testing Targets <a id="testing"></a>

- [webbrowsertools.com](https://webbrowsertools.com/)
- [browserleaks.com](https://browserleaks.com/)
- [CreepJS](https://abrahamjuliot.github.io/creepjs/)
- [browserscan.net](https://www.browserscan.net/)
- [yalala.com](https://www.yalala.com/)
- [uutool.cn](https://uutool.cn/browser/)

## 🛠️ Development <a id="development"></a>

```bash
cd <project>
npm install
npm run dev          # Chrome / Edge
npm run dev:firefox  # Firefox
```


## ❓ FAQ <a id="faq"></a>

**Q: What should I do if the extension doesn't work properly or some pages behave abnormally?**  
> A: Try enabling **Fast Injection Mode** in the script settings to improve compatibility. For abnormal pages, you can either **add them to the whitelist** or apply the **Known Issues List** from the configuration presets to resolve common problems.

**Q: Why do I need this extension?**
> A: Browser fingerprints can be used for cross-site tracking, compromising user privacy. This extension disguises key fingerprint data to reduce the risk of identification and tracking.

**Q: How is this extension different from a fingerprint browser?**
> A: A well-designed fingerprint browser simulates a complete environment for deep obfuscation, ideal for anti-detection scenarios. This extension uses JS injection, offering lightweight protection suitable for everyday use and most fingerprint-related threats.

**Q: Are browser fingerprints a unique identifier?**
> A: Not strictly. A single fingerprint may not be unique, but when combined with IP address, browser storage, and other data, it can strongly identify a user.

**Q: Is it better to protect as many fingerprints as possible?**
> A: Not necessarily. In many cases, modifying just one key fingerprint is enough to break tracking. Overprotecting may cause site compatibility issues or expose abnormal behavior. Choose protection items based on actual needs.

**Q: What's the difference between strong and weak fingerprints?**
> A: Strong fingerprints are highly unique and often used for precise tracking—modifying them greatly enhances privacy. Weak fingerprints have low uniqueness and are usually safe to leave unchanged to maintain compatibility.


## 💝 Support <a id="support"></a>

- If you find this project helpful, give it a ⭐
- Your feedback helps make this project better. Every star counts!
- [Support me on Ko-fi](https://ko-fi.com/omegaee)

## 📜 Disclaimer <a id="disclaimer"></a>

This project is for educational and research purposes only. Do not use it for illegal activities. The developer is not responsible for any consequences.

## 🙏 Credits <a id="credits"></a>

Thanks to all contributors and the open-source community!


## /_locales/en/messages.json

```json path="/_locales/en/messages.json" 
{
  "ext_desc": {
    "message": "Protect Your Browser Fingerprints"
  }
}
```

## /_locales/zh/messages.json

```json path="/_locales/zh/messages.json" 
{
  "ext_desc": {
    "message": "保护你的浏览器指纹"
  }
}
```

## /docs/supporters.zh.md


## ❤️ 支持一下

你的支持是我最大的动力!

| 微信赞赏 |
| :---: |
| <img src='../images/wechat-code.png' title='微信赞赏' width='220px' height='220px'  /> |


## /example/config/template.json

```json path="/example/config/template.json" 
{
  "version": "2.7.0",
  "config": {
    "enable": true,
    "fp": {
      "navigator": {
        "hardwareConcurrency": {
          "type": 0
        },
        "languages": {
          "type": 0
        },
        "uaVersion": {
          "type": 0
        }
      },
      "normal": {
        "gpuInfo": {
          "type": 0
        }
      },
      "other": {
        "audio": {
          "type": 0
        },
        "canvas": {
          "type": 2
        },
        "domRect": {
          "type": 0
        },
        "font": {
          "type": 0
        },
        "timezone": {
          "type": 0
        },
        "webgl": {
          "type": 2
        },
        "webgpu": {
          "type": 0
        },
        "webrtc": {
          "type": 0
        }
      },
      "screen": {
        "depth": {
          "type": 0
        },
        "size": {
          "type": 0
        }
      }
    },
    "subscribe": {
      "url": "config.json"
    }
  },
  "whitelist": []
}
```

## /example/presets/index.json

```json path="/example/presets/index.json" 
{
  "presets": [
    {
      "file": "known-issues.json",
      "name": {
        "en": "Known Issues Whitelist",
        "zh": "已知问题白名单"
      },
      "description": {
        "en": "Provides preset whitelist rules for websites that may encounter compatibility issues under specific configurations. If a page behaves abnormally due to the extension, enabling this preset may help improve compatibility.",
        "zh": "针对在特定配置下可能出现兼容性问题的网站,提供预设白名单规则。遇到该扩展导致的页面异常时可尝试启用以提升兼容性。"
      }
    }
  ]
}
```

## /example/presets/known-issues.json

```json path="/example/presets/known-issues.json" 
{
  "whitelist": [
    "challenges.cloudflare.com"
  ]
}
```

## /images/en/ui.png

Binary file available at https://raw.githubusercontent.com/omegaee/my-fingerprint/refs/heads/main/images/en/ui.png

## /images/wechat-code.png

Binary file available at https://raw.githubusercontent.com/omegaee/my-fingerprint/refs/heads/main/images/wechat-code.png

## /images/zh/ui.png

Binary file available at https://raw.githubusercontent.com/omegaee/my-fingerprint/refs/heads/main/images/zh/ui.png

## /manifest.ts

```ts path="/manifest.ts" 
import { ManifestV3Export } from "@crxjs/vite-plugin"

const baseManifest: ManifestV3Export = {
  manifest_version: 3,
  version: '2.7.3',
  name: 'My Fingerprint',
  default_locale: 'en',
  description: '__MSG_ext_desc__',
  host_permissions: [
    '<all_urls>',
  ],
  icons: {
    128: 'logo.png',
  },
  action: {
    default_popup: "src/popup/index.html",
  },
  web_accessible_resources: []
}

const VALUES = {
  permissions: [
    'storage',
    'tabs',
    'activeTab',
    'webNavigation',
    'scripting',
    'declarativeNetRequest',
    'clipboardRead',
    'clipboardWrite',
  ] as chrome.runtime.ManifestPermissions[],
  optional_permissions: [
    "userScripts",
  ] as chrome.runtime.ManifestPermissions[],
  background: "src/background/index.ts",
  content: {
    matches: ["<all_urls>"],
    js: ["src/scripts/content.ts"],
    run_at: "document_start",
    match_about_blank: true,
    // all_frames: true,
  },
}

export const chromeManifest: ManifestV3Export = {
  ...baseManifest,
  minimum_chrome_version: '102',
  key: "b21lZ2FlZS9teS1maW5nZXJwcmludAo=",
  update_url: 'https://raw.githubusercontent.com/omegaee/my-fingerprint/refs/heads/main/updates.xml',
  permissions: [
    ...VALUES.permissions,
    ...VALUES.optional_permissions,
  ],
  background: {
    service_worker: VALUES.background,
  },
  content_scripts: [
    {
      // @ts-ignore
      world: "ISOLATED",
      ...VALUES.content,
    },
  ],
}

export const firefoxManifest: ManifestV3Export & { [key: string]: any } = {
  ...baseManifest,
  browser_specific_settings: {
    gecko: {
      id: "my-fingerprint@omegaee.addons",
      strict_min_version: "136.0"
    }
  },
  permissions: VALUES.permissions,
  optional_permissions: VALUES.optional_permissions,
  background: {
    scripts: [VALUES.background],
  },
  content_scripts: [
    VALUES.content,
  ],
}
```

## /package.json

```json path="/package.json" 
{
  "name": "my-fingerprint",
  "private": true,
  "version": "0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "dev:firefox": "vite build -- --firefox --no-minify",
    "build:firefox": "tsc && vite build -- --firefox",
    "build:all": "npm run build && npm run build:firefox"
  },
  "dependencies": {
    "antd": "^5.18.3",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-i18next": "^14.1.3",
    "react-markdown": "^9.1.0",
    "zustand": "^5.0.12"
  },
  "devDependencies": {
    "@crxjs/vite-plugin": "^2.4.0",
    "@rollup/plugin-commonjs": "^26.0.3",
    "@rollup/plugin-node-resolve": "^15.3.1",
    "@rollup/plugin-typescript": "^12.1.4",
    "@types/chrome": "^0.0.268",
    "@types/file-saver": "^2.0.7",
    "@types/node": "^20.14.2",
    "@types/react": "^18.0.26",
    "@types/react-dom": "^18.0.9",
    "@vitejs/plugin-react-swc": "^3.0.0",
    "autoprefixer": "^10.5.0",
    "postcss": "^8.5.13",
    "tailwindcss": "^3.4.19",
    "typescript": "^5.9.2",
    "vite": "^5.4.20"
  }
}

```

## /plugins/core-bundle.ts

```ts path="/plugins/core-bundle.ts" 
import { Plugin } from 'vite';
import { rollup } from 'rollup';
import { watch } from 'chokidar';
import { writeFileSync } from 'fs';

import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

type CoreBundleOptions = {
  inputFilePath: string
  outputFilePath: string
  functionName: string
  params: string
}

export const coreBundle = (options: CoreBundleOptions): Plugin => ({
  name: 'core-bundle',
  async buildStart() {
    await bundle(options);
  },
  configureServer(server) {
    const watcher = watch([options.inputFilePath], { persistent: true });
    watcher.on('change', async () => {
      await bundle(options);
      server.ws.send({ type: 'full-reload' });
    });
  }
})

async function bundle({ inputFilePath, outputFilePath, functionName, params }: CoreBundleOptions) {
  const bundle = await rollup({
    input: inputFilePath,
    plugins: [
      resolve(),
      commonjs(),
      typescript({
        compilerOptions: {
          isolatedModules: false,
          preserveConstEnums: false,
        },
      }),
    ],
  });
  const { output } = await bundle.generate({
    format: 'es',
    inlineDynamicImports: true,
  });
  const bundledCode = output[0].code;
  const wrappedCode = `export function ${functionName}(${params}) {\n${bundledCode}\n}`

  writeFileSync(outputFilePath, wrappedCode);
}

export default coreBundle;
```

## /postcss.config.js

```js path="/postcss.config.js" 
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

## /public/config.json

```json path="/public/config.json" 
{
  "config": {},
  "whitelist": []
}
```

## /public/logo-gray.png

Binary file available at https://raw.githubusercontent.com/omegaee/my-fingerprint/refs/heads/main/public/logo-gray.png

## /public/logo.png

Binary file available at https://raw.githubusercontent.com/omegaee/my-fingerprint/refs/heads/main/public/logo.png

## /public/presets/default.json

```json path="/public/presets/default.json" 
{
  "config": {
    "fp": {
      "navigator": {
        "hardwareConcurrency": {
          "type": 0
        },
        "languages": {
          "type": 0
        },
        "uaVersion": {
          "type": 0
        }
      },
      "normal": {
        "gpuInfo": {
          "type": 0
        }
      },
      "other": {
        "audio": {
          "type": 0
        },
        "canvas": {
          "type": 2
        },
        "domRect": {
          "type": 0
        },
        "font": {
          "type": 0
        },
        "timezone": {
          "type": 0
        },
        "webgl": {
          "type": 2
        },
        "webgpu": {
          "type": 0
        },
        "webrtc": {
          "type": 0
        },
        "serviceWorker": {
          "type": 0
        }
      },
      "screen": {
        "depth": {
          "type": 0
        },
        "size": {
          "type": 0
        }
      }
    }
  }
}
```

## /public/presets/incognito.json

```json path="/public/presets/incognito.json" 
{
  "config": {
    "fp": {
      "navigator": {
        "hardwareConcurrency": {
          "type": 0
        },
        "languages": {
          "type": 0
        },
        "uaVersion": {
          "type": 0
        }
      },
      "normal": {
        "gpuInfo": {
          "type": 0
        }
      },
      "other": {
        "audio": {
          "type": 2
        },
        "canvas": {
          "type": 2
        },
        "domRect": {
          "type": 0
        },
        "font": {
          "type": 0
        },
        "timezone": {
          "type": 0
        },
        "webgl": {
          "type": 2
        },
        "webgpu": {
          "type": 2
        },
        "webrtc": {
          "type": 7
        },
        "serviceWorker": {
          "type": 0
        }
      },
      "screen": {
        "depth": {
          "type": 0
        },
        "size": {
          "type": 0
        }
      }
    }
  }
}
```

## /public/presets/index.json

```json path="/public/presets/index.json" 
{
  "presets": [
    {
      "file": "default.json",
      "name": {
        "en": "Default Mode",
        "zh": "默认模式"
      },
      "description": {
        "en": "Enables basic fingerprint protection while ensuring page compatibility, suitable for everyday use with a balance between privacy and functionality.",
        "zh": "在确保页面兼容性的前提下,启用基础指纹防护,适合日常使用,兼顾隐私与功能体验。"
      }
    },
    {
      "file": "incognito.json",
      "name": {
        "en": "Incognito Mode",
        "zh": "隐身模式"
      },
      "description": {
        "en": "Activates additional verified fingerprint spoofing techniques and uses tab-level randomization, which may affect the functionality of certain websites.",
        "zh": "启用更多经过验证的指纹伪装项,并使用标签页随机机制,可能影响部分网站功能。"
      }
    }
  ]
}
```

## /public/settings/client-hints.json

```json path="/public/settings/client-hints.json" 
{
  "chromium": [
    {
      "key": "Chrome/140 MacOS/15.3.1 arm64",
      "title": "Chrome/140 MacOS/15.3.1 arm64",
      "ua": {
        "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
        "appVersion": "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
        "platform": "MacIntel"
      },
      "uaData": {
        "arch": "arm",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "macOS",
        "platformVersion": "15.3.1",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "140.0.7339.81",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "140.0.7339.81"
          },
          {
            "brand": "Not?A_Brand",
            "version": "8.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "140.0.7339.81"
          }
        ]
      }
    },
    {
      "key": "Chrome/140 Windows/10 arm64",
      "title": "Chrome/140 Windows/10 arm64",
      "ua": {
        "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
        "appVersion": "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
        "platform": "Win32"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "Windows",
        "platformVersion": "10.0.0",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "140.0.7339.81",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "140.0.7339.81"
          },
          {
            "brand": "Not?A_Brand",
            "version": "8.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "140.0.7339.81"
          }
        ]
      }
    },
    {
      "key": "Chrome/140 Android/14 Pixel 8 Pro",
      "title": "Chrome/140 Android/14 Pixel 8 Pro",
      "ua": {
        "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36",
        "appVersion": "5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36",
        "platform": "Linux arm64"
      },
      "uaData": {
        "arch": "arm",
        "bitness": "64",
        "mobile": true,
        "model": "Pixel 8 Pro",
        "platform": "Android",
        "platformVersion": "14",
        "formFactors": [
          "Mobile"
        ],
        "uaFullVersion": "140.0.7275.100",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "140.0.7275.100"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "140.0.7275.100"
          }
        ]
      }
    },
    {
      "key": "Chrome/138 Windows/11 x64",
      "title": "Chrome/138 Windows/11 x64",
      "ua": {
        "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
        "appVersion": "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
        "platform": "Win32"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "Windows",
        "platformVersion": "11.0.0",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "138.0.5971.80",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "138.0.5971.80"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "138.0.5971.80"
          }
        ]
      }
    },
    {
      "key": "Chrome/135 MacOS/14.4 Intel",
      "title": "Chrome/135 MacOS/14.4 Intel",
      "ua": {
        "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "appVersion": "5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "platform": "MacIntel"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "macOS",
        "platformVersion": "14.4",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "135.0.5672.63",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "135.0.5672.63"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "135.0.5672.63"
          }
        ]
      }
    },
    {
      "key": "Chrome/133 Linux/6.5 x64",
      "title": "Chrome/133 Linux/6.5 x64",
      "ua": {
        "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
        "appVersion": "5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
        "platform": "Linux x86_64"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "Linux",
        "platformVersion": "6.5.0",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "133.0.5612.87",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "133.0.5612.87"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "133.0.5612.87"
          }
        ]
      }
    },
    {
      "key": "Chrome/132 Windows/10 x64",
      "title": "Chrome/132 Windows/10 x64",
      "ua": {
        "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
        "appVersion": "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
        "platform": "Win32"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "Windows",
        "platformVersion": "10.0.0",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "132.0.5600.71",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "132.0.5600.71"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "132.0.5600.71"
          }
        ]
      }
    },
    {
      "key": "Chrome/130 Android/13 Galaxy S23 Ultra",
      "title": "Chrome/130 Android/13 Galaxy S23 Ultra",
      "ua": {
        "userAgent": "Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36",
        "appVersion": "5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36",
        "platform": "Linux arm64"
      },
      "uaData": {
        "arch": "arm",
        "bitness": "64",
        "mobile": true,
        "model": "SM-S918B",
        "platform": "Android",
        "platformVersion": "13",
        "formFactors": [
          "Mobile"
        ],
        "uaFullVersion": "130.0.5481.100",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "130.0.5481.100"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "130.0.5481.100"
          }
        ]
      }
    },
    {
      "key": "Chrome/129 MacOS/13.6 arm64",
      "title": "Chrome/129 MacOS/13.6 arm64",
      "ua": {
        "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
        "appVersion": "5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
        "platform": "MacIntel"
      },
      "uaData": {
        "arch": "arm",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "macOS",
        "platformVersion": "13.6",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "129.0.5450.90",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "129.0.5450.90"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "129.0.5450.90"
          }
        ]
      }
    },
    {
      "key": "Chrome/126 Windows/10 x86",
      "title": "Chrome/126 Windows/10 x86",
      "ua": {
        "userAgent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
        "appVersion": "5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
        "platform": "Win32"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "32",
        "mobile": false,
        "model": "",
        "platform": "Windows",
        "platformVersion": "10.0.0",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "126.0.5249.91",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "126.0.5249.91"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "126.0.5249.91"
          }
        ]
      }
    },
    {
      "key": "Chrome/124 Android/12 Xiaomi 13",
      "title": "Chrome/124 Android/12 Xiaomi 13",
      "ua": {
        "userAgent": "Mozilla/5.0 (Linux; Android 12; Xiaomi 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
        "appVersion": "5.0 (Linux; Android 12; Xiaomi 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
        "platform": "Linux arm64"
      },
      "uaData": {
        "arch": "arm",
        "bitness": "64",
        "mobile": true,
        "model": "Xiaomi 13",
        "platform": "Android",
        "platformVersion": "12",
        "formFactors": [
          "Mobile"
        ],
        "uaFullVersion": "124.0.5367.92",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "124.0.5367.92"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "124.0.5367.92"
          }
        ]
      }
    },
    {
      "key": "Chrome/121 Linux/5.15 x64",
      "title": "Chrome/121 Linux/5.15 x64",
      "ua": {
        "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
        "appVersion": "5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
        "platform": "Linux x86_64"
      },
      "uaData": {
        "arch": "x86",
        "bitness": "64",
        "mobile": false,
        "model": "",
        "platform": "Linux",
        "platformVersion": "5.15.0",
        "formFactors": [
          "Desktop"
        ],
        "uaFullVersion": "121.0.5122.75",
        "versions": [
          {
            "brand": "Google Chrome",
            "version": "121.0.5122.75"
          },
          {
            "brand": "Not?A_Brand",
            "version": "99.0.0.0"
          },
          {
            "brand": "Chromium",
            "version": "121.0.5122.75"
          }
        ]
      }
    }
  ]
}
```

## /public/settings/gpu-info.json

```json path="/public/settings/gpu-info.json" 
{
  "gpuInfo": [
    {
      "key": "RTX 3060 D3D11",
      "title": "RTX 3060 D3D11",
      "vendor": "Google Inc. (NVIDIA)",
      "renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Laptop GPU (0x00002560) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "GTX 1650 D3D11",
      "title": "GTX 1650 D3D11",
      "vendor": "Google Inc. (NVIDIA)",
      "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 1650 (0x00001F93) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "Intel UHD Graphics 620 D3D11",
      "title": "Intel UHD Graphics 620 D3D11",
      "vendor": "Google Inc. (Intel)",
      "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "AMD Radeon RX 580 D3D11",
      "title": "AMD Radeon RX 580 D3D11",
      "vendor": "Google Inc. (AMD)",
      "renderer": "ANGLE (AMD, Radeon RX 580 Series (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "Apple M1 Metal",
      "title": "Apple M1 Metal",
      "vendor": "Google Inc.",
      "renderer": "ANGLE (Apple, Apple M1, Metal)"
    },
    {
      "key": "Intel Iris Xe D3D11",
      "title": "Intel Iris Xe D3D11",
      "vendor": "Google Inc. (Intel)",
      "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "NVIDIA RTX 4090 Vulkan",
      "title": "NVIDIA RTX 4090 Vulkan",
      "vendor": "Google Inc. (NVIDIA)",
      "renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 (0x000027B6) Vulkan 1.3.236)"
    },
    {
      "key": "RTX 3080 D3D11",
      "title": "RTX 3080 D3D11",
      "vendor": "Google Inc. (NVIDIA)",
      "renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 (0x00002202) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "Intel HD Graphics 530 D3D11",
      "title": "Intel HD Graphics 530 D3D11",
      "vendor": "Google Inc. (Intel)",
      "renderer": "ANGLE (Intel, Intel(R) HD Graphics 530 (0x0000191B) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "AMD Radeon Vega 8 D3D11",
      "title": "AMD Radeon Vega 8 D3D11",
      "vendor": "Google Inc. (AMD)",
      "renderer": "ANGLE (AMD, AMD Radeon Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "NVIDIA Quadro P1000 D3D11",
      "title": "NVIDIA Quadro P1000 D3D11",
      "vendor": "Google Inc. (NVIDIA)",
      "renderer": "ANGLE (NVIDIA, NVIDIA Quadro P1000 (0x00001CB1) Direct3D11 vs_5_0 ps_5_0, D3D11)"
    },
    {
      "key": "Apple M2 Metal",
      "title": "Apple M2 Metal",
      "vendor": "Google Inc.",
      "renderer": "ANGLE (Apple, Apple M2, Metal)"
    },
    {
      "key": "Intel Arc A750 Vulkan",
      "title": "Intel Arc A750 Vulkan",
      "vendor": "Google Inc. (Intel)",
      "renderer": "ANGLE (Intel, Intel Arc A750 Graphics (0x0000A770) Vulkan 1.3.250)"
    },
    {
      "key": "AMD Radeon RX 7900 XTX Vulkan",
      "title": "AMD Radeon RX 7900 XTX Vulkan",
      "vendor": "Google Inc. (AMD)",
      "renderer": "ANGLE (AMD, AMD Radeon RX 7900 XTX (0x0000747F) Vulkan 1.3.250)"
    }
  ]
}
```

## /public/settings/timezone.json

```json path="/public/settings/timezone.json" 
{
  "timezone": [
    {
      "key": "LG",
      "title": {
        "en": "London",
        "zh": "伦敦"
      },
      "zone": "Europe/London",
      "locale": "en-GB",
      "offset": 0
    },
    {
      "key": "PAR",
      "title": {
        "en": "Paris",
        "zh": "巴黎"
      },
      "zone": "Europe/Paris",
      "locale": "fr-FR",
      "offset": 1
    },
    {
      "key": "BER",
      "title": {
        "en": "Berlin",
        "zh": "柏林"
      },
      "zone": "Europe/Berlin",
      "locale": "de-DE",
      "offset": 1
    },
    {
      "key": "CAI",
      "title": {
        "en": "Cairo",
        "zh": "开罗"
      },
      "zone": "Africa/Cairo",
      "locale": "ar-EG",
      "offset": 2
    },
    {
      "key": "MSK",
      "title": {
        "en": "Moscow",
        "zh": "莫斯科"
      },
      "zone": "Europe/Moscow",
      "locale": "ru-RU",
      "offset": 3
    },
    {
      "key": "DXB",
      "title": {
        "en": "Dubai",
        "zh": "迪拜"
      },
      "zone": "Asia/Dubai",
      "locale": "ar-AE",
      "offset": 4
    },
    {
      "key": "KZ",
      "title": {
        "en": "Almaty",
        "zh": "阿拉木图"
      },
      "zone": "Asia/Almaty",
      "locale": "zh-CN",
      "offset": 5
    },
    {
      "key": "KHI",
      "title": {
        "en": "Karachi",
        "zh": "卡拉奇"
      },
      "zone": "Asia/Karachi",
      "locale": "ur-PK",
      "offset": 5
    },
    {
      "key": "DAC",
      "title": {
        "en": "Dhaka",
        "zh": "达卡"
      },
      "zone": "Asia/Dhaka",
      "locale": "bn-BD",
      "offset": 6
    },
    {
      "key": "BKK",
      "title": {
        "en": "Bangkok",
        "zh": "曼谷"
      },
      "zone": "Asia/Bangkok",
      "locale": "th-TH",
      "offset": 7
    },
    {
      "key": "JKT",
      "title": {
        "en": "Jakarta",
        "zh": "雅加达"
      },
      "zone": "Asia/Jakarta",
      "locale": "id-ID",
      "offset": 7
    },
    {
      "key": "SH",
      "title": {
        "en": "Shanghai",
        "zh": "上海"
      },
      "zone": "Asia/Shanghai",
      "locale": "zh-CN",
      "offset": 8
    },
    {
      "key": "SG",
      "title": {
        "en": "Singapore",
        "zh": "新加坡"
      },
      "zone": "Asia/Singapore",
      "locale": "zh-SG",
      "offset": 8
    },
    {
      "key": "TY",
      "title": {
        "en": "Tokyo",
        "zh": "东京"
      },
      "zone": "Asia/Tokyo",
      "locale": "ja-JP",
      "offset": 9
    },
    {
      "key": "SE",
      "title": {
        "en": "Seoul",
        "zh": "首尔"
      },
      "zone": "Asia/Seoul",
      "locale": "ko-KR",
      "offset": 9
    },
    {
      "key": "SYD",
      "title": {
        "en": "Sydney",
        "zh": "悉尼"
      },
      "zone": "Australia/Sydney",
      "locale": "en-AU",
      "offset": 10
    },
    {
      "key": "NOU",
      "title": {
        "en": "Noumea",
        "zh": "努美阿"
      },
      "zone": "Pacific/Noumea",
      "locale": "fr-NC",
      "offset": 11
    },
    {
      "key": "AKL",
      "title": {
        "en": "Auckland",
        "zh": "奥克兰"
      },
      "zone": "Pacific/Auckland",
      "locale": "en-NZ",
      "offset": 12
    },
    {
      "key": "CVT",
      "title": {
        "en": "Cape Verde",
        "zh": "佛得角"
      },
      "zone": "Atlantic/Cape_Verde",
      "locale": "pt-CV",
      "offset": -1
    },
    {
      "key": "GST",
      "title": {
        "en": "Noronha",
        "zh": "诺罗尼亚"
      },
      "zone": "America/Noronha",
      "locale": "pt-BR",
      "offset": -2
    },
    {
      "key": "BRT",
      "title": {
        "en": "Sao Paulo",
        "zh": "圣保罗"
      },
      "zone": "America/Sao_Paulo",
      "locale": "pt-BR",
      "offset": -3
    },
    {
      "key": "AST",
      "title": {
        "en": "Caracas",
        "zh": "加拉加斯"
      },
      "zone": "America/Caracas",
      "locale": "es-VE",
      "offset": -4
    },
    {
      "key": "NY",
      "title": {
        "en": "New York",
        "zh": "纽约"
      },
      "zone": "America/New_York",
      "locale": "en-US",
      "offset": -5
    },
    {
      "key": "CHI",
      "title": {
        "en": "Chicago",
        "zh": "芝加哥"
      },
      "zone": "America/Chicago",
      "locale": "en-US",
      "offset": -6
    },
    {
      "key": "DEN",
      "title": {
        "en": "Denver",
        "zh": "丹佛"
      },
      "zone": "America/Denver",
      "locale": "en-US",
      "offset": -7
    },
    {
      "key": "LAX",
      "title": {
        "en": "Los Angeles",
        "zh": "洛杉矶"
      },
      "zone": "America/Los_Angeles",
      "locale": "en-US",
      "offset": -8
    },
    {
      "key": "AKST",
      "title": {
        "en": "Anchorage",
        "zh": "安克雷奇"
      },
      "zone": "America/Anchorage",
      "locale": "en-US",
      "offset": -9
    },
    {
      "key": "HNL",
      "title": {
        "en": "Honolulu",
        "zh": "夏威夷"
      },
      "zone": "Pacific/Honolulu",
      "locale": "en-US",
      "offset": -10
    },
    {
      "key": "NUT",
      "title": {
        "en": "Niue",
        "zh": "纽埃"
      },
      "zone": "Pacific/Niue",
      "locale": "en-NU",
      "offset": -11
    }
  ]
}
```

## /src/api/github.ts

```ts path="/src/api/github.ts" 

const repo = 'my-fingerprint'
const owner = 'omegaee'
const branch = 'main'

export type GithubContentItem = {
  name: string
  path: string
  sha: string
  size: number
  url: string
  html_url: string
  git_url: string
  download_url?: string
  type: 'file' | 'dir'
}

export const GithubApi = {
  asRawUrl(path: string) {
    if (path.startsWith('/')) path = path.slice(1);
    return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`
  },

  getContentList(path: string): Promise<GithubContentItem[]> {
    if (path.startsWith('/')) path = path.slice(1);
    return fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`).then(res => {
      if (!res.ok) throw new Error(res.statusText);
      return res.json()
    })
  },

  getJson<T = any>(path: string): Promise<T> {
    if (path.startsWith('/')) path = path.slice(1);
    return fetch(GithubApi.asRawUrl(path)).then(res => {
      if (!res.ok) throw new Error(res.statusText);
      return res.json();
    })
  }
}
```

## /src/api/local.ts

```ts path="/src/api/local.ts" 
import { appFetchJson } from "@/utils/net"

export type TimeZoneOption = TimeZoneInfo & {
  key: string
  title: I18nString
}

export type ClientHintsOption = ClientHintsInfo & {
  key: string
  title: I18nString
}

export type GpuInfoOption = GpuInfo & {
  key: string
  title: I18nString
}

export const LocalApi = {
  timezone: async () => {
    const url = chrome.runtime.getURL('settings/timezone.json')
    return appFetchJson(url)
      .then(v => v.timezone as TimeZoneOption[])
  },

  clientHints: async () => {
    const url = chrome.runtime.getURL('settings/client-hints.json')
    return appFetchJson(url)
      .then(v => v.chromium as ClientHintsOption[])
  },

  gpuInfo: async () => {
    const url = chrome.runtime.getURL('settings/gpu-info.json')
    return appFetchJson(url)
      .then(v => v.gpuInfo as GpuInfoOption[])
  }
}
```

## /src/background/badge.ts

```ts path="/src/background/badge.ts" 
const BADGE_COLOR = {
  whitelist: '#fff',
  low: '#7FFFD4',
  high: '#F4A460',
}

/**
 * 设置标识
 */
export const setBadgeContent = (tabId: number, text: string, level: number) => {
  chrome.action.setBadgeText({ tabId, text });
  chrome.action.setBadgeBackgroundColor({ tabId, color: level === 1 ? BADGE_COLOR.low : BADGE_COLOR.high });
}

/**
 * 设置白名单标识
 */
export const setBadgeWhitelist = (tabId: number) => {
  chrome.action.setBadgeText({ tabId, text: '-' }).catch(() => { })
  chrome.action.setBadgeTextColor({ tabId, color: BADGE_COLOR.whitelist }).catch(() => { })
  chrome.action.setBadgeBackgroundColor({ tabId, color: BADGE_COLOR.whitelist }).catch(() => { })
}

/**
 * 移除标识
 */
export const removeBadge = (tabId: number) => {
  chrome.action.setBadgeText({ tabId, text: '' }).catch(() => { })
}
```

## /src/background/index.ts

```ts path="/src/background/index.ts" 
import { applySubscribeStorage, cleanLocalWhitelist, getLocalStorage, initLocalStorage, reBrowserSeed, updateLocalConfig, updateLocalWhitelist } from "./storage";
import { removeBadge, setBadgeContent, setBadgeWhitelist } from "./badge";
import { injectScript, reRegisterScript } from './script';
import { tryUrl } from "@/utils/base";
import { reRequestHeader } from "./request";

let newVersion: string | undefined

/**
 * 获取最新版本号
 */
const getNewVersion = async () => {
  if (newVersion) {
    return newVersion
  } else {
    const data = await fetch('https://api.github.com/repos/omegaee/my-fingerprint/releases/latest').then(data => data.json())
    newVersion = data.tag_name
    return newVersion
  }
}

/**
 * 初次启动扩展时触发(浏览器更新、扩展更新触发)
 */
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === "install" || reason === "update") {
    initLocalStorage()
    reBrowserSeed()
  }
});

/**
 * 重启浏览器触发
 */
chrome.runtime.onStartup.addListener(() => {
  initLocalStorage()
  reBrowserSeed()
});

/**
 * 消息处理
 */
chrome.runtime.onMessage.addListener(((msg, sender, sendResponse) => {
  switch (msg?.type) {
    case 'config.set': {
      updateLocalConfig(msg.config).then((data) => {
        if (msg.result) sendResponse<'config.set'>(data);
      })
      return msg.result;
    }
    case 'config.subscribe': {
      const fun = async () => {
        if (msg.url != null) await updateLocalConfig({ subscribe: { url: msg.url.trim() } });
        if (await applySubscribeStorage()) {
          const [storage] = await getLocalStorage()
          sendResponse<'config.subscribe'>(storage)
        } else {
          sendResponse<'config.subscribe'>(undefined)
        }
      }
      fun()
      return true
    }
    case 'whitelist.update': {
      if (msg.clean) cleanLocalWhitelist();
      if (msg.data) updateLocalWhitelist(msg.data);
      return false;
    }
    case 'version.latest': {
      getNewVersion().then((version) => {
        sendResponse<'version.latest'>(version)
      })
      return true
    }
    case 'api.check': {
      if (msg.api === 'userScripts') {
        try {
          chrome.userScripts.getScripts()
          sendResponse<'api.check'>(true)
        } catch (e) {
          sendResponse<'api.check'>(e as string)
        }
      }
      return false
    }
    case 'badge.set': {
      const tabId = sender.tab?.id
      if (tabId == null) return;
      setBadgeContent(tabId, msg.text, msg.level)
      return false;
    }
  }
}) as BackgroundMessage.Listener)

/**
 * 监听tab变化
 */
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
  if (changeInfo.status === 'loading') {
    const [storage, { match }] = await getLocalStorage()

    // 兼容模式注入,内部判断是否需要注入
    injectScript(tabId, storage)

    const _url = tab.url ? tryUrl(tab.url) : undefined
    if (!_url?.hostname) return;

    if (match(_url.hostname)) {
      reRequestHeader(tabId)
      setBadgeWhitelist(tabId)
    } else {
      reRequestHeader(undefined, tabId)
    }
  }
});

/**
 * 监听tab关闭
 */
chrome.tabs.onRemoved.addListener((tabId) => {
  reRequestHeader(undefined, tabId)
  removeBadge(tabId)
})

// chrome.webNavigation.onCommitted.addListener((details) => {})

/**
 * 监听权限添加
 */
chrome.permissions.onAdded.addListener((perms) => {
  if (perms.permissions?.includes('userScripts')) {
    reRegisterScript()
  }
})

```

## /src/background/request.ts

```ts path="/src/background/request.ts" 
import { getBrowser } from "@/utils/equipment"
import { getLocalStorage } from "./storage"
import { HookType } from '@/types/enum'
import { isDefaultMode } from "@/utils/storage"

type RuleHeader = chrome.declarativeNetRequest.ModifyHeaderInfo
type RuleSignal = {
  isUpdate: boolean
}

const UA_NET_RULE_ID = 1

const MEMORY = {
  browser: getBrowser(navigator.userAgent),
  ua: undefined as Pair<string, readonly RuleHeader[]> | undefined,
  lang: undefined as Pair<string, readonly RuleHeader[]> | undefined,
  exIds: undefined as Set<number> | undefined,
  whitelistSet: undefined as Set<string> | undefined,
}

const isHookNetRequest = (storage: LocalStorage) => {
  const fp = storage.config.fp
  return !isDefaultMode([
    fp.navigator.clientHints,
    fp.navigator.languages,
  ])
}

/**
 * 获取已排除的tabID
 */
const getExcludeTabIds = async (singal: RuleSignal, excludeIds?: number | number[], passIds?: number | number[]) => {
  if (!MEMORY.exIds) {
    const rules = await chrome.declarativeNetRequest.getSessionRules()
    MEMORY.exIds = new Set(rules[0]?.condition?.excludedTabIds)
  }

  if (excludeIds !== undefined) {
    if (Array.isArray(excludeIds)) {
      for (const excludeTabId of excludeIds) {
        if (!MEMORY.exIds.has(excludeTabId)) {
          MEMORY.exIds.add(excludeTabId)
          singal.isUpdate = true
        }
      }
    } else {
      if (!MEMORY.exIds.has(excludeIds)) {
        MEMORY.exIds.add(excludeIds)
        singal.isUpdate = true
      }
    }
  }

  if (passIds !== undefined) {
    if (Array.isArray(passIds)) {
      for (const passTabId of passIds) {
        if (MEMORY.exIds.has(passTabId)) {
          MEMORY.exIds.delete(passTabId)
          singal.isUpdate = true
        }
      }
    } else {
      if (MEMORY.exIds.has(passIds)) {
        MEMORY.exIds.delete(passIds)
        singal.isUpdate = true
      }
    }
  }

  return MEMORY.exIds
}

/**
 * 获取seed
 */
const getSeedByMode = (config: LocalStorageConfig, mode: HookMode) => {
  switch (mode?.type) {
    case HookType.browser:
      return config.seed.browser
    case HookType.global:
      return config.seed.global
    default:
      return undefined
  }
}

const genUaRules = async ({ config }: LocalStorage, singal: RuleSignal): Promise<readonly RuleHeader[]> => {
  const uaMode = config.fp.navigator.clientHints

  const modeValue = (uaMode as any).value;
  const modeValueStr = typeof modeValue === 'object' ? JSON.stringify(modeValue) : modeValue;

  const key = `${uaMode.type}:${modeValueStr}`
  const mem = MEMORY.lang
  if (mem && mem[0] === key) return mem[1];

  singal.isUpdate = true;

  if (uaMode.type !== HookType.value) return [];

  const { ua, uaData } = uaMode.value;
  if (ua == null && uaData == null) return [];

  const fullVersionList = uaData.versions;
  const brands = fullVersionList?.map(v => ({
    ...v,
    version: v.version?.split('.')[0] ?? ''
  }))

  const makeRule = (header: string, value: string) => {
    return value == null ? undefined : {
      header,
      operation: "set",
      value,
    }
  }

  const res = [
    makeRule("User-Agent", ua.userAgent),
    makeRule("Sec-Ch-Ua-Arch", `"${uaData.arch}"`),
    makeRule("Sec-Ch-Ua-Bitness", `"${uaData.bitness}"`),
    makeRule("Sec-Ch-Ua-Platform", `"${uaData.platform}"`),
    makeRule("Sec-Ch-Ua-Platform-Version", `"${uaData.platformVersion}"`),
    makeRule("Sec-Ch-Ua-Mobile", uaData.mobile ? "?1" : "?0"),
    makeRule("Sec-Ch-Ua-Model", `"${uaData.model}"`),
    makeRule("Sec-Ch-Ua-Form-Factors", uaData.formFactors.map(v => `"${v}"`).join(", ")),
    makeRule("Sec-Ch-Ua-Full-Version", `"${uaData.uaFullVersion}"`),
    makeRule("Sec-Ch-Ua", brands.map((brand) => `"${brand.brand}";v="${brand.version}"`).join(", ")),
    makeRule("Sec-Ch-Ua-Full-Version-List", fullVersionList.map((brand) => `"${brand.brand}";v="${brand.version}"`).join(", ")),
  ].filter(Boolean) as RuleHeader[]

  MEMORY.ua = [key, res]
  return res;
}

const genLanguageRules = ({ config }: LocalStorage, singal: RuleSignal): readonly RuleHeader[] => {
  const langsMode = config.fp.navigator.languages

  const modeValue = (langsMode as any).value;
  const modeValueStr = typeof modeValue === 'object' ? JSON.stringify(modeValue) : modeValue;

  const key = `${langsMode.type}:${modeValueStr}`
  const mem = MEMORY.lang
  if (mem && mem[0] === key) return mem[1];

  singal.isUpdate = true;

  if (langsMode.type !== HookType.value) return [];

  const res: RuleHeader[] = []
  const langs = langsMode.value;

  if (langs?.length) {
    const [first, ...rest] = langs
    let qFactor = 1
    for (let i = 0; i < rest.length && qFactor > 0.1; i++) {
      qFactor -= 0.1
      rest[i] = `${rest[i]};q=${qFactor.toFixed(1)}`
    }
    res.push({
      header: "Accept-Language",
      operation: "set" as any,
      value: [first, ...rest].join(","),
    })
  }

  MEMORY.lang = [key, res]
  return res;
}

const checkWhitelistDiff = ({ whitelist }: LocalStorage, singal: RuleSignal) => {
  const mem = MEMORY.whitelistSet
  if (!mem || mem.size !== whitelist.length || whitelist.some((v) => !mem.has(v))) {
    MEMORY.whitelistSet = new Set(whitelist)
    singal.isUpdate = true
  }
}

/**
 * 删除请求头规则
 */
const removeRules = async () => {
  return await chrome.declarativeNetRequest.updateSessionRules({
    removeRuleIds: [UA_NET_RULE_ID],
  }).catch(() => { })
}

/**
 * 刷新请求头
 */
export const reRequestHeader = async (excludeTabIds?: number | number[], passTabIds?: number | number[]) => {
  const [storage] = await getLocalStorage()

  if (!storage.config.enable || !isHookNetRequest(storage)) {
    return await removeRules()
  }

  const singal: RuleSignal = { isUpdate: false }

  const uaRules = MEMORY.browser === 'firefox' ? [] : await genUaRules(storage, singal)
  const langRules = genLanguageRules(storage, singal)
  const exTabIds = await getExcludeTabIds(singal, excludeTabIds, passTabIds)
  checkWhitelistDiff(storage, singal)

  if (singal.isUpdate) {
    const rules = [
      ...uaRules,
      ...langRules,
    ]
    if (rules.length === 0) {
      return await removeRules()
    }
    return await chrome.declarativeNetRequest.updateSessionRules({
      removeRuleIds: [UA_NET_RULE_ID],
      addRules: [{
        id: UA_NET_RULE_ID,
        condition: {
          excludedInitiatorDomains: [...storage.whitelist],
          resourceTypes: Object.values(chrome.declarativeNetRequest.ResourceType),
          excludedTabIds: [...exTabIds],
        },
        action: {
          type: "modifyHeaders" as any,
          requestHeaders: rules,
        },
      }]
    }).catch(() => { })
  }
}
```

## /src/background/script.ts

```ts path="/src/background/script.ts" 
import { getLocalStorage, updateLocalConfig } from "./storage";
import { coreInject } from "@/core/output";

// // @ts-ignore
// import contentSrc from '@/scripts/content?script&module'

const REG_ID = 'core'
let mScriptCode: string | undefined = undefined

export const hasUserScripts = () => {
  try {
    if (chrome.userScripts) {
      chrome.userScripts.getScripts();
      return true;
    }
    return false;
  } catch (_) {
    return false
  }
}

/**
 * 确保 FastInject 配置正确,返回是否启用
 */
export const ensureFastInject = (storage: LocalStorage) => {
  if (!hasUserScripts()) {
    if (storage.config.action.fastInject) {
      // 若配置不同步则更新
      updateLocalConfig({ action: { fastInject: false } })
    }
    return false;
  }
  return storage.config.action.fastInject;
}

/**
 * 注入脚本(兼容模式)
 */
export const injectScript = async (tabId: number, storage: LocalStorage) => {
  if (!storage.config.enable || ensureFastInject(storage)) return;
  /* 注入脚本 */
  await chrome.scripting.executeScript({
    target: {
      tabId,
      allFrames: true,
    },
    world: 'MAIN',
    injectImmediately: true,
    args: [storage],
    func: coreInject,
  }).catch(() => { })
}

/**
 * 获取脚本文本
 */
const getRegScriptCode = (storage: LocalStorage) => {
  if (!mScriptCode) mScriptCode = coreInject.toString();
  return `(function(fun){fun({fun,storage:${JSON.stringify(storage)}});})(${mScriptCode});`
}

/**
 * 注册脚本(快速注入模式)
 */
export const reRegisterScript = async () => {
  const [storage] = await getLocalStorage()
  if (!ensureFastInject(storage)) return;

  if (storage.config.enable) {
    const scripts: chrome.userScripts.RegisteredUserScript[] = [{
      id: REG_ID,
      allFrames: true,
      runAt: 'document_start',
      world: 'MAIN',
      matches: ["*://*/*"],
      js: [{ code: getRegScriptCode(storage) }],
    }]

    try {
      await chrome.userScripts.update(scripts)
    } catch (_) {
      await chrome.userScripts.register(scripts)
    }
  } else {
    chrome.userScripts.unregister({ ids: [REG_ID] })
  }
}

```

## /src/background/storage.ts

```ts path="/src/background/storage.ts" 
import { genRandomSeed, existParentDomain } from "@/utils/base";
import { debounce, sharedAsync } from "@/utils/timer";
import { reRequestHeader } from "./request";
import { HookType } from '@/types/enum'
import { hasUserScripts, reRegisterScript } from "./script";

let mContent: LocalStorageContent | undefined

type LocalStorageWhitelist = {
  match: (v: string) => boolean
  add: (v: string) => void
  remove: (v: string) => void
  clean: () => void
}

type LocalStorageContent = [
  LocalStorage,
  LocalStorageWhitelist,
] & {
  storage: LocalStorage
  whitelist: LocalStorageWhitelist
}

/**
 * 格式化LocalStorage
 */
const genStorageContent = (storage: LocalStorage): LocalStorageContent => ({
  storage,
  whitelist: {
    match: (v: string) => existParentDomain(storage.whitelist, v),
    add(v: string) {
      storage.whitelist.push(v)
    },
    remove(v: string) {
      const index = storage.whitelist.indexOf(v)
      index !== -1 && storage.whitelist.splice(index, 1)
    },
    clean() {
      storage.whitelist = []
    }
  },
  [Symbol.iterator]() {
    return Object.values(this)[Symbol.iterator]();
  },
} as LocalStorageContent)

/**
 * 生成默认配置
 */
export const genDefaultLocalStorage = (): LocalStorage => {
  const manifest = chrome.runtime.getManifest()
  const sGlobal = genRandomSeed();
  return {
    version: manifest.version,
    config: {
      enable: true,
      seed: {
        browser: genRandomSeed(),
        global: sGlobal,
      },
      fp: {
        navigator: {
          clientHints: { type: HookType.default },
          languages: { type: HookType.default },
          hardwareConcurrency: { type: HookType.default },
        },
        screen: {
          size: { type: HookType.default },
          depth: { type: HookType.default },
        },
        normal: {
          gpuInfo: { type: HookType.default },
        },
        other: {
          timezone: { type: HookType.default },
          canvas: { type: HookType.page },
          audio: { type: HookType.default },
          webgl: { type: HookType.page },
          webrtc: { type: HookType.default },
          font: { type: HookType.default },
          webgpu: { type: HookType.default },
          domRect: { type: HookType.default },
          serviceWorker: { type: HookType.default },
        },
      },
      action: {
        fastInject: hasUserScripts() ? true : false,
      },
      input: {
        globalSeed: String(sGlobal),
      },
      subscribe: {
        url: 'config.json'
      },
      prefs: {
        language: navigator.language,
        theme: 'system',
      },
    },
    whitelist: []
  }
}

/**
 * 合并存储(dst覆盖src,合并到dst)
 * 会修改dst
 */
const mergeStorage = (src: LocalStorageConfig, dst?: DeepPartial<LocalStorageConfig>): LocalStorageConfig => {
  if (!dst) return src;
  for (const key in dst) {
    if (key === 'value') continue;
    // @ts-ignore
    if (!(key in src)) {
      // @ts-ignore
      delete dst[key];
    }
  }
  for (const key in src) {
    // @ts-ignore
    const srcValue = src[key];
    if (key in dst) {
      const dstValue = (dst as any)[key];

      if (dstValue === null) {
        delete (dst as any)[key];
        continue;
      }

      if (key === 'value') continue;

      if (typeof srcValue === 'object' && !Array.isArray(srcValue)) {
        // @ts-ignore
        mergeStorage(srcValue, dstValue);
      }
    } else {
      // @ts-ignore
      dst[key] = srcValue;
    }
  }
  // @ts-ignore
  return dst;
}

/**
 * 初始化默认配置
 */
export const initLocalStorage = sharedAsync(async () => {
  /* init config */
  const _curr = await chrome.storage.local.get() as LocalStorage
  let _new = genDefaultLocalStorage()
  const _config = mergeStorage(_new.config, _curr.config)
  /* clear */
  const rem = Object.keys(_curr).filter((key) => !(key in _new))
  if (rem.length) {
    chrome.storage.local.remove(rem)
  }
  /* set */
  const _storage: LocalStorage = {
    version: _new.version,
    config: _config,
    whitelist: _curr.whitelist ?? _new.whitelist,
  }
  mContent = genStorageContent(_storage)
  chrome.storage.local.set(_storage).then(() => {
    reRegisterScript()
    reRequestHeader()
    applySubscribeStorage()
  })
  return mContent
})

/**
 * 从url中拉取配置
 */
export const applySubscribeStorage = async () => {
  const [storage, { match }] = await getLocalStorage();

  let url = storage.config.subscribe.url.trim()
  if (url === '') return true;
  if (!url.includes("://")) url = chrome.runtime.getURL(url);

  /* 拉取内容 */
  const data = await fetch(url)
    .then(data => data.json() as DeepPartial<LocalStorage>)
    .catch(e => console.warn('Pull config failed: ' + e))

  if (!data) return false;
  /* 加载配置 */
  if (data.config && Object.keys(data.config).length) {
    // 移除不支持的配置
    delete data.config.prefs;
    delete data.config.action?.fastInject;
    // 更新配置
    await updateLocalConfig(data.config)
  }
  /* 加载白名单 */
  if (data.whitelist?.length) {
    const wlist = data.whitelist.filter(v => !match(v))  // 去重
    if (wlist.length) await updateLocalWhitelist({ add: wlist });
  }
  return true;
}

/**
 * 获取配置
 */
export const getLocalStorage = async () => {
  if (mContent !== undefined) {
    return mContent
  } else {
    return await initLocalStorage()
  }
}

/**
 * 存储配置
 */
export const saveLocalConfig = debounce((config: LocalStorageConfig) => {
  chrome.storage.local.set({ config })
}, 500)

/**
 * 存储白名单
 */
export const saveLocalWhitelist = debounce((whitelist: string[] | Set<string>) => {
  if (whitelist instanceof Set) {
    chrome.storage.local.set({ whitelist: [...whitelist] })
  } else {
    chrome.storage.local.set({ whitelist })
  }
}, 500)

/**
 * 修改配置
 */
export const updateLocalConfig = async (config: DeepPartial<LocalStorageConfig>) => {
  const [storage] = await getLocalStorage()
  storage.config = mergeStorage(storage.config, config)
  saveLocalConfig(storage.config)
  reRegisterScript()
  reRequestHeader()
  return storage.config
}

/**
 * 修改白名单
 */
export const updateLocalWhitelist = async (data: { add?: string[], del?: string[] }) => {
  const [storage, { add, remove }] = await getLocalStorage()
  data.del?.length && data.del.forEach(v => remove(v))
  data.add?.length && data.add.forEach(v => add(v))
  saveLocalWhitelist(storage.whitelist)
  reRegisterScript()
  reRequestHeader()
  return storage.whitelist
}

/**
 * 清理白名单
 */
export const cleanLocalWhitelist = async () => {
  const [storage, { clean }] = await getLocalStorage()
  clean()
  saveLocalWhitelist(storage.whitelist)
  reRegisterScript()
  reRequestHeader()
}

/**
 * 刷新浏览器种子
 */
export const reBrowserSeed = async () => {
  const [storage] = await getLocalStorage()
  storage.config.seed.browser = genRandomSeed()
  saveLocalConfig(storage.config)
  reRequestHeader()
}
```

## /src/components/data/highlight.tsx

```tsx path="/src/components/data/highlight.tsx" 
export type HighlightProps = {
  text: string
  keyword?: string | string[]
  ignoreCase?: boolean
  className?: string
}

/**
 * 文本高亮
 */
export const Highlight = function ({ className, text, keyword, ignoreCase }: HighlightProps) {
  if(keyword === undefined || keyword === null) return <span>{text}</span>
  if(Array.isArray(keyword)){
    if(keyword.length === 0) return <span className={className}>{text}</span>
    return <span className={className}>{text.split(new RegExp(keyword.map((item) => `${item}`).join('|'), ignoreCase ? "gi" : "g"))
    .map((str, index) => keyword.includes(str) ? <mark key={index}>{str}</mark> : str )}</span>
  }else{
    if(keyword === "") return <span className={className}>{text}</span>
    return <span className={className}>{text.split(new RegExp(`(${keyword})`, ignoreCase ? "gi" : "g"))
    .map((str, index) => str === keyword ? <mark key={index}>{str}</mark> : str )}</span>
  }
}

export default Highlight
```

## /src/components/data/markdown.tsx

```tsx path="/src/components/data/markdown.tsx" 
import Markdown from "react-markdown"

type MdProps = {
  className?: string
  children?: string | null | undefined;
}

export const Md = ({ className, children }: MdProps) => {
  return <Markdown className={'markdown-body max-w-64' + (className ?? '')}>{children}</Markdown>
}
```

## /src/components/data/tip-icon.tsx

```tsx path="/src/components/data/tip-icon.tsx" 
import { Popover, type PopoverProps } from "antd"
import {
  QuestionOutlined,
} from '@ant-design/icons';
import { cn } from "@/utils/style";

type Color = 'default' | 'warning' | 'danger'

const triggerColor: Record<Color, string> = {
  default: 'bg-default-100 hover:bg-default-200',
  warning: 'bg-warning-50 hover:bg-warning-100',
  danger: 'bg-danger-50 hover:bg-danger-100',
}

export type TipIconProps = {
  isIconOnly?: boolean
  color?: Color
  className?: string
  style?: React.CSSProperties
  children?: React.ReactNode
} & PopoverProps

export const TipIcon = ({ isIconOnly, color = 'default', className, style, children, ...props }: TipIconProps) => {
  return <Popover rootClassName="[&_.ant-popover-inner]:p-2" {...props}>
    <div
      className={cn(
        'flex justify-center items-center rounded duration-300 text-default-600 hover:text-default-900 select-none',
        isIconOnly && 'size-[22px]',
        triggerColor[color],
        className
      )}
      style={style}>{children}</div>
  </Popover>
}

TipIcon.Question = ({ content, ...props }: Omit<TipIconProps, 'icon'>) => {
  return <TipIcon isIconOnly content={content} {...props} ><QuestionOutlined /></TipIcon>
}

export default TipIcon
```

## /src/components/feedback/var-popconfirm.tsx

```tsx path="/src/components/feedback/var-popconfirm.tsx" 
import { Popconfirm, type PopconfirmProps, Tooltip, type TooltipProps } from "antd"
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

type VariablePopconfirmProps = {
  tooltip?: string | TooltipProps
}

export const VariablePopconfirm = ({ tooltip, ...props }: VariablePopconfirmProps & PopconfirmProps) => {
  const [t] = useTranslation()
  const [clicked, setClicked] = useState(false);
  const [hovered, setHovered] = useState(false);

  const tooltipProps = useMemo<TooltipProps>(() => {
    let res: TooltipProps = {
      placement: props.placement,
    }
    if (tooltip === undefined) {
      return res
    } else if (typeof tooltip === 'string') {
      res.title = tooltip
      return res
    } else {
      return { ...res, ...tooltip }
    }
  }, [tooltip])

  return <Tooltip {...tooltipProps}
    open={hovered}
    onOpenChange={(open: boolean) => {
      setHovered(open);
      setClicked(false);
    }}>
    <Popconfirm
      open={clicked}
      onOpenChange={(open: boolean) => {
        setHovered(false);
        setClicked(open);
      }}
      okText={t('g.confirm')}
      cancelText={t('g.cancel')}
      {...props} />
  </Tooltip>
}

export default VariablePopconfirm
```

## /src/core/.gitignore

```gitignore path="/src/core/.gitignore" 
output.js
```

## /src/core/core.ts

```ts path="/src/core/core.ts" 
import { HookType } from '@/types/enum'
import { seededRandom } from "@/utils/base";
import { genRandomSeed } from "../utils/base";
import { hookTasks } from "./tasks";
import { notify, notifyIframeOrigin } from './utils';

export type HookTask = {
  // 条件,为空则默认为true
  condition?: (ctx: FingerprintContext) => boolean | undefined
  // 钩子函数
  onEnable?: (ctx: FingerprintContext) => void
}

// export const WIN_KEY = Symbol('__my_fingerprint__')
export const WIN_KEY = 'my_fingerprint'

type SeedInfo = {
  page: number
  domain: number
  browser: number
  global: number
}

type ContextOptions = Pick<FingerprintContext, 'info' | 'conf'> & Partial<FingerprintContext>

export class FingerprintContext {
  public gthis: Window & WorkerGlobalScope & typeof globalThis
  public win?: Window & typeof globalThis
  public worker?: WorkerGlobalScope & typeof globalThis

  public info: WindowStorage
  public seed: SeedInfo
  public conf: LocalStorageConfig

  /// hook存储
  public myProxy: WeakSet<object>;
  public otherProxy: WeakSet<object>;

  public args: Record<string, any>;

  /// hook索引
  public symbol: {
    disown: symbol;
    raw: symbol;
    reflect: symbol;
  };

  private toProxyHandler = <T extends object>(handler: ProxyHandler<T>): ProxyHandler<T> => {
    return {
      ...handler,
      get: (target: any, prop: any, receiver: any) => {
        if (prop === this.symbol.raw) return target;
        if (prop === 'caller' || prop === 'arguments') return target[prop];
        const getter = handler.get ?? Reflect.get
        return getter(target, prop, receiver)
      },
      setPrototypeOf: (target: any, proto: any) => {
        const raw = this.useRaw(proto)
        if (target === raw && (this.isReg(proto) || this.isReg(proto.__proto__))) {
          if (proto[this.symbol.reflect]) {
            return Reflect.setPrototypeOf(target, raw);
          } else {
            try {
              return Object.setPrototypeOf(target, raw);
            } catch (e: any) {
              // 堆栈伪造
              if (this.info.browser !== 'firefox') {
                const es = e.stack.split('\n')
                es.splice(1, 2);
                e.stack = es.join('\n');
              }
              throw e;
            }
          }
        }
        return Reflect.setPrototypeOf(target, proto)
      },
    }
  }

  /**
   * 判断对象是否注册代理
   */
  public isReg = (target: any) => {
    return this.myProxy.has(target)
  }

  /**
   * 判断对象以及上游是否注册代理
   */
  public hasRaw = (target: any) => {
    if (target == null) return false;
    return target[this.symbol.raw] != null
  }

  /**
   * 获取原始值
   * @param target 目标
   */
  public useRaw = <T>(target: T): T => {
    if (target == null) return target;
    const raw = (target as any)[this.symbol.raw]
    return raw ?? target;
  }

  /**
   * 创建代理
   */
  public newProxy = <T extends object>(target: T, handler: ProxyHandler<T>): T => {
    const RawProxy = this.useRaw(Proxy)
    const proxy = new RawProxy(target, this.toProxyHandler(handler));
    this.myProxy.add(proxy);
    return proxy;
  }

  /**
   * 创建代理
   * @param target 目标对象
   * @param key 属性名
   * @param handler 处理对象 | (key) => 处理对象
   */
  public useProxy = <
    T extends object,
    K extends keyof T,
    H extends ProxyHandler<Extract<T[K], object>>,
  >(
    target: T,
    key: K | K[],
    handler: H | ((key: K) => H | void),
  ) => {
    if (Array.isArray(key)) {
      /* multi */
      for (const _k of key) {
        const _handler = typeof handler === 'function' ? handler(_k) : handler;
        if (_handler) {
          target[_k] = this.newProxy(target[_k] as any, _handler);
        }
      }
    } else {
      /* one */
      const _handler = typeof handler === 'function' ? handler(key) : handler;
      if (_handler) {
        target[key] = this.newProxy(target[key] as any, _handler);
      }
    }
  }

  /**
   * 定义属性描述符
   * @param target 目标对象 | [获取描述符对象, 写入对象]
   * @param key 属性名
   * @param attributes 属性描述符
   * @returns void
   */
  public useDefine = <
    T extends object,
    K extends keyof T,
    A extends (PropertyDescriptor & ThisType<any>),
    W = any,
  >(
    target: T | [T, W],
    key: K | K[],
    attributes: A | ((key: K, desc: PropertyDescriptor) => A | void)
  ) => {
    /* 处理target */
    let _read: T
    let _write: W | T
    if (Array.isArray(target)) {
      if (!target.length) return;
      _read = target[0]
      _write = target[1] ?? _read
    } else {
      _read = target
      _write = target
    }

    /* 定义属性 */
    if (Array.isArray(key)) {
      /* multi */
      for (const _k of key) {
        const desc = Object.getOwnPropertyDescriptor(_read, _k);
        if (!desc) continue;
        const attr = typeof attributes === 'function' ? attributes(_k, desc) : attributes;
        if (attr) {
          Object.defineProperty(_write, _k, attr);
        }
      }
    } else {
      /* one */
      const desc = Object.getOwnPropertyDescriptor(_read, key);
      if (!desc) return;
      const attr = typeof attributes === 'function' ? attributes(key, desc) : attributes;
      if (attr) {
        Object.defineProperty(_write, key, attr);
      }
    }
  }

  /**
   * 代理getter属性描述符
   * @param target 目标对象 | [获取描述符对象, 写入对象]
   * @param key 属性名
   * @param attributes 属性描述符代理 | ((key, getter) => 属性描述符代理 | 不进行代理)
   * @returns void
   */
  public useGetterProxy = <
    T extends object,
    K extends keyof T,
    H extends ProxyHandler<() => any>,
    W = any,
  >(
    target: T | [T, W],
    key: K | K[],
    handler: H | ((key: K, getter: () => any) => H | void)
  ) => {
    this.useDefine(target, key, (_k, desc) => {
      const getter = desc.get
      if (!getter) return;
      const _handler = typeof handler === 'function' ? handler(_k, getter) : handler
      if (!_handler) return;
      return {
        get: this.newProxy(getter, _handler)
      }
    })
  }

  public useSetterProxy = <
    T extends object,
    K extends keyof T,
    H extends ProxyHandler<() => any>,
    W = any,
  >(
    target: T | [T, W],
    key: K | K[],
    handler: H | ((key: K, setter: (v: any) => any) => H | void)
  ) => {
    this.useDefine(target, key, (_k, desc) => {
      const setter = desc.set
      if (!setter) return;
      const _handler = typeof handler === 'function' ? handler(_k, setter) : handler
      if (!_handler) return;
      return {
        set: this.newProxy(setter, _handler)
      }
    })
  }

  /**
   * 隐藏拥有属性
   */
  public useDisownKeys = <T>(src: T, keys: (keyof T)[]) => {
    if (src == null || typeof src !== 'object') return;
    const s = (src as any)[this.symbol.disown];
    if (s && s instanceof Set) {
      for (const k of keys) {
        s.add(k)
      }
    } else {
      (src as any)[this.symbol.disown] = new Set(keys);
    }
  }

  /**
   * 获取指定项的种子
   */
  public useSeed = (mode?: HookMode) => {
    switch (mode?.type) {
      case HookType.page:
        return this.seed.page
      case HookType.domain:
        return this.seed.domain
      case HookType.browser:
        return this.seed.browser
      case HookType.global:
        return this.seed.global
      default:
        return null
    }
  }

  /**
   * 获取指定项的种子或自定义值
   */
  public useHookMode = <V>(mode?: HookMode<V>): {
    isDefault?: boolean
    seed?: number
    value?: V
  } => {
    switch (mode?.type) {
      case HookType.default:
        return { isDefault: true };
      case HookType.value:
        return { value: mode.value };
      default:
        const seed = this.useSeed(mode)
        return seed == null ? {} : { seed };
    }
  }

  /**
   * 所有参数是否是默认模式
   * @example !isDefault([...]) // 至少一个元素是非默认值
   */
  public isDefault = (mode?: HookMode | HookMode[]) => {
    if (!mode) return true
    if (Array.isArray(mode)) {
      return mode.every(m => m.type === HookType.default)
    } else {
      return mode.type === HookType.default
    }
  }

  private constructor(gthis: any, opt: ContextOptions) {
    if (!gthis) throw new Error('gthis is required');

    const { info, conf } = opt;

    this.gthis = gthis
    this.info = info
    this.conf = conf

    this.myProxy = opt.myProxy ?? new WeakSet()
    this.otherProxy = opt.otherProxy ?? new WeakSet()
    this.seed = opt.seed ?? {
      page: info.seed,
      domain: Math.floor(seededRandom(info.host, Number.MAX_SAFE_INTEGER, 1)),
      browser: conf.seed.browser ?? genRandomSeed(),
      global: conf.seed.global ?? genRandomSeed(),
    }
    this.symbol = opt.symbol ?? {
      disown: Symbol('DisownProperty'),
      raw: Symbol('RawValue'),
      reflect: Symbol('Reflect'),
    }

    this.args = opt.args ?? {}

    if (typeof window !== "undefined") {
      this.win = gthis
    } else if (typeof self !== "undefined") {
      this.worker = gthis
    }

    this.runHook()
  }

  public static hookWindow = (win: Window, opt: ContextOptions) => {
    if (!win) throw new Error('win is required');

    const { info } = opt;

    if (win === window.top) {
      if (info.hooked) throw new Error('win is already hooked');
      info.hooked = true;
    } else {
      let hooked: boolean = false
      try {
        // @ts-ignore
        hooked = win[WIN_KEY]
      } catch (_) {
        throw new Error('unable to access cross source');
      }
      if (hooked) throw new Error('win is already hooked');
      // @ts-ignore
      win[WIN_KEY] = true;
    }

    const ctx = new FingerprintContext(win, opt);

    if (win !== window.top) {
      notify('other.iframe')
      notifyIframeOrigin(win.location.origin);
    }
    return ctx;
  }

  public static hookWorker = (worker: any, opt: ContextOptions) => {
    if (!worker) return;
    const ctx = new FingerprintContext(worker, opt);
    return ctx;
  }

  /**
   * 脚本是否启动
   */
  public isEnable() {
    return !!this.conf.enable
  }

  /**
   * hook内容
   */
  private runHook() {
    if (!this.isEnable()) return;
    for (const task of hookTasks) {
      if (!task.condition || task.condition(this) === true) {
        task.onEnable?.(this)
      }
    }
  }

  /**
   * hook target
   */
  public hookTarget = (target?: Window | HTMLIFrameElement | Node | null) => {
    if (!target) return;
    try {
      const _t: any = target;
      if (_t === _t.window) {
        return FingerprintContext.hookWindow(_t, this);
      }
      const cw = _t.contentWindow
      if (cw && cw === cw.window) {
        return FingerprintContext.hookWindow(cw, this)
      }
    } catch (_) { }
  }

  public makeScript = () => {
    const fun: (args: any) => string = this.args?.fun;
    if (!fun || typeof fun !== 'function') return;
    const options = JSON.stringify({
      info: this.info,
      conf: this.conf,
      seed: this.seed,
    })
    return `${fun.toString()};${fun.name}({options:${options}});`;
  }

}

```

## /src/core/index.ts

```ts path="/src/core/index.ts" 
import { FingerprintContext, WIN_KEY } from "./core";
import { genRandomSeed, existParentDomain } from "@/utils/base";
import { getBrowser } from "@/utils/equipment";
import { sendToWindow } from "@/utils/message";

// @ts-ignore
const args = _args;

// ------------
// script entry
// ------------
(() => {
  if (typeof window !== "undefined") {
    const storage: LocalStorage = args.storage;
    if (!window || !storage) return;

    const hook = (win: Window & typeof globalThis, data: WindowStorage | undefined) => {
      if (!data) return;
      if (existParentDomain(storage.whitelist, data.host)) return;
      if (win.location.hostname !== data.host && existParentDomain(storage.whitelist, win.location.hostname)) return;
      try {
        FingerprintContext.hookWindow(win, {
          info: data,
          conf: storage.config,
          args,
        });
      } catch (_) { }
    }

    if (window.top === window) {
      const data: WindowStorage = {
        url: location.href,
        host: location.hostname,
        seed: genRandomSeed(),
        hooked: false,
        browser: getBrowser(navigator.userAgent),
      }
      // @ts-ignore
      window[WIN_KEY] = data;
      window.addEventListener('message', ((ev) => {
        const msg = ev?.data?.__myfp__;
        if (!msg || !ev.source) return;
        if (msg.type === 'core.get-info') {
          sendToWindow(ev.source, {
            type: 'core.run',
            data,
          }, ev.origin)
        }
      }) as WindowMessage.Listener)
      hook(window, data)
      return;
    }

    try {
      /* 同源 */
      const top: any = window.top ?? window
      const data = top[WIN_KEY]
      hook(window, data)
    } catch (_) {
      /* 跨源 */
      window.addEventListener('message', ((ev) => {
        const msg = ev?.data?.__myfp__;
        if (msg?.type === 'core.run') {
          hook(window, msg.data)
        }
      }) as WindowMessage.Listener)
      sendToWindow(window.top ?? window, { type: 'core.get-info' })
    }
  }
  else if (typeof self !== "undefined") {
    const options = args.options;
    if (options) {
      FingerprintContext.hookWorker(self, options)
    }
  }
})()
```

## /src/core/tasks.ts

```ts path="/src/core/tasks.ts" 
import { HookType } from '@/types/enum'
import { type HookTask } from "./core";
import { seededRandom } from '@/utils/base';
import {
  notify,
  drawNoiseTo2d,
  drawNoiseToWebgl,
  proxyUserAgentData,
  randomCanvasNoise,
  randomFontNoise,
  randomWebglNoise,
  randomScreenSize,
} from './utils';

export const hookTasks: HookTask[] = [
  /**
   * iframe html hook
   * 静态iframe注入
   */
  {
    onEnable: ({ win, hookTarget }) => {
      if (!win) return;

      // 监听DOM初始化
      const observer = new MutationObserver((mutations) => {
        // if (mutations.length == 1) return;
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node.nodeName === 'IFRAME') {
              hookTarget(node)
            }
          }
        }
      });
      observer.observe(win.document.documentElement, { childList: true, subtree: true });

      const closeObserver = () => {
        observer.disconnect()
        win.removeEventListener('DOMContentLoaded', closeObserver, { capture: true })
        win.removeEventListener('load', closeObserver, { capture: true })
      }
      win.addEventListener('DOMContentLoaded', closeObserver, { capture: true })
      win.addEventListener('load', closeObserver, { capture: true })
    },
  },

  /**
   * iframe script hook
   * 动态iframe注入
   */
  {
    onEnable: ({ win, hookTarget, useProxy, useSetterProxy }) => {
      if (!win) return;

      const mem = new WeakSet()

      const handler = {
        apply(target: any, thisArg: any, args: any) {
          const res: any = Reflect.apply(target, thisArg, args);
          const fs = thisArg.getElementsByTagName ?
            thisArg.getElementsByTagName('iframe') :
            thisArg.querySelectorAll?.('iframe');
          if (fs?.length) {
            for (const f of fs) {
              !mem.has(f) && hookTarget(f) && mem.add(f);
            }
          }
          return res;
        }
      }

      const pnKeys = ['append', 'prepend', 'replaceChildren'] as const

      useProxy(win.Node.prototype, [
        'appendChild', 'insertBefore', 'replaceChild'
      ], handler);
      useProxy(win.Element.prototype, [
        ...pnKeys, 'insertAdjacentElement', 'insertAdjacentHTML',
        'before', 'after', 'replaceWith', 'setHTMLUnsafe',
      ], handler);
      useProxy(win.Document.prototype, [
        ...pnKeys,
      ], handler);
      useProxy(win.ShadowRoot.prototype, [
        ...pnKeys, 'setHTMLUnsafe',
      ], handler);

      useSetterProxy(win.Element.prototype, ['innerHTML', 'outerHTML'], handler);
      useSetterProxy(win.ShadowRoot.prototype, ['innerHTML'], handler);
    }
  },

  /**
   * Worker
   */
  {
    onEnable: ({ win, conf, useProxy, makeScript }) => {
      if (!win) return;

      const blobMap = new Map<string, Blob>();

      useProxy(win.URL, 'createObjectURL', {
        apply(target, thisArg: URL, args: any) {
          const blob = args[0]
          if (blob instanceof Blob) {
            const url = Reflect.apply(target, thisArg, args);
            blobMap.set(url, blob);
            return url;
          }
          return Reflect.apply(target, thisArg, args)
        }
      })

      useProxy(win.URL, 'revokeObjectURL', {
        apply(target, thisArg: URL, args: any) {
          const url = args[0]
          blobMap.delete(url)
          return Reflect.apply(target, thisArg, args)
        }
      })

      let tsURLLookup: WeakMap<TrustedScriptURL, TrustedTypePolicy>
      if (win.TrustedTypePolicy) {
        tsURLLookup = new WeakMap()
        useProxy(win.TrustedTypePolicy.prototype, 'createScriptURL', {
          apply(target, thisArg: TrustedTypePolicy, args: any) {
            const res = Reflect.apply(target, thisArg, args);
            tsURLLookup.set(res, thisArg);
            return res;
          }
        })
      }

      function createScriptUrl(url: string) {
        if (url.toString().startsWith('blob:')) {
          const injected = makeScript();
          if (injected == null) return url;

          const blobScript = blobMap.get(url.toString());
          if (blobScript) {
            const blob = new Blob([
              `(function(){${injected}})();`, blobScript,
            ], { type: 'application/javascript' });
            return URL.createObjectURL(blob);
          }
        }
        return url;
      }

      {
        const makeHandler = (name: string) => ({
          construct(target: any, args: any, newTarget: any) {
            notify('other.worker.' + name)
            const url = args[0];

            if (win.TrustedScriptURL && url instanceof win.TrustedScriptURL && tsURLLookup) {
              const ttp = tsURLLookup.get(url);
              if (ttp) {
                args[0] = ttp.createScriptURL(
                  createScriptUrl(url.toString())
                );
              }
            } else if (typeof url === 'string' || url instanceof URL) {
              args[0] = createScriptUrl(url.toString());
            }

            return Reflect.construct(target, args, newTarget) as Worker;
          }
        })
        useProxy(win, 'Worker', makeHandler('web'));
        useProxy(win, 'SharedWorker', makeHandler('shared'));
      }

      {
        const isDisabled = conf.fp.other.serviceWorker.type === HookType.disabled;
        useProxy(win.ServiceWorkerContainer.prototype, 'register', {
          apply(target, thisArg, args) {
            notify('other.worker.service')
            if (isDisabled) {
              return Promise.reject(
                new DOMException("Service workers are disabled", "SecurityError")
              );
            }
            return Reflect.apply(target, thisArg, args);
          }
        });
      }
    }
  },

  /**
   * Navigator
   */
  {
    condition: ({ conf, isDefault }) => !isDefault(Object.values(conf.fp.navigator)),
    onEnable: (ctx) => {
      const { gthis, conf, useDisownKeys, useHookMode, useGetterProxy } = ctx;
      const fps = conf.fp.navigator;

      useDisownKeys(gthis.navigator, [
        'userAgent', 'appVersion', 'platform', 'userAgentData' as any, 'language', 'languages', 'hardwareConcurrency',
      ]);

      const prototype = (gthis.Navigator ?? gthis.WorkerNavigator).prototype;

      /* userAgent & userAgentData */
      const uaInfo = useHookMode(fps.clientHints).value;
      if (uaInfo != null) {
        if (uaInfo.ua != null) {
          useGetterProxy([prototype, gthis.navigator], [
            'userAgent', 'appVersion', 'platform',
          ], (key, getter) => {
            const value = uaInfo.ua[key]
            if (value == null) return;
            return {
              apply(target, thisArg: Navigator, args: any) {
                notify('weak.' + key)
                return value;
              }
            }
          })
        }
        if (uaInfo.uaData != null) {
          useGetterProxy([prototype, gthis.navigator], ('userAgentData' as any), (_, getter) => ({
            apply(target, thisArg: Navigator, args: any) {
              notify('weak.userAgentData')
              const result = getter.call(thisArg)
              return proxyUserAgentData(ctx, result, uaInfo);
            }
          }))
        }
      }

      /* other */
      useGetterProxy([prototype, gthis.navigator], [
        'languages', 'hardwareConcurrency',
      ], (key, getter) => {
        const value: any = useHookMode(fps[key] as any).value
        if (value == null) return;
        return {
          apply(target, thisArg: Navigator, args: any) {
            notify('weak.' + key)
            return value;
          }
        }
      })

      {
        const value = useHookMode(fps.languages).value?.[0]
        value != null && useGetterProxy([prototype, gthis.navigator], 'language', {
          apply(target, thisArg: Navigator, args: any) {
            notify('weak.languages')
            return value;
          }
        })
      }
    },
  },

  /**
   * Screen
   */
  {
    condition: ({ conf, isDefault }) => !isDefault(Object.values(conf.fp.screen)),
    onEnable: ({ win, conf, useDisownKeys, useHookMode, useGetterProxy }) => {
      if (!win) return;

      const fps = conf.fp.screen;
      const ws = win.screen;

      useDisownKeys(win.screen, [
        'colorDepth', 'pixelDepth', 'width', 'height', 'availWidth', 'availHeight',
      ]);

      /* Screen Depth */
      const depth = useHookMode(fps.depth).value
      if (depth) {
        const tasks: { [key in keyof Screen]?: number } = {
          'colorDepth': depth?.color,
          'pixelDepth': depth?.pixel,
        }
        useGetterProxy([win.Screen.prototype, win.screen],
          Object.keys(tasks) as (keyof Screen)[],
          (key, getter) => {
            const num = tasks[key]
            if (num == null) return;
            return {
              apply(target, thisArg: Screen, args: any) {
                notify('weak.' + key)
                return num;
              }
            }
          });
      }

      /* Screen Size */
      {
        const { value, seed } = useHookMode(fps.size)
        const size: any =
          seed != null ? randomScreenSize(ws, seed) :
            value != null ? value :
              null;

        if (size) {
          if (size.width != null) size.availWidth = size.width - (ws.width - ws.availWidth);
          if (size.height != null) size.availHeight = size.height - (ws.height - ws.availHeight);

          useGetterProxy([win.Screen.prototype, ws], [
            'width', 'height', 'availWidth', 'availHeight'
          ], (key, getter) => {
            const num = size[key]
            if (num == null) return;
            return {
              apply(target, thisArg: Screen, args: any) {
                notify('weak.' + key)
                return num;
              }
            }
          })
        }
      }

    },
  },

  /**
   * Canvas 2d
   */
  {
    condition: ({ conf }) => conf.fp.other.canvas.type !== HookType.default,
    onEnable: (ctx) => {
      const { win, conf, useSeed, useProxy } = ctx;
      if (!win) return;

      /* getContext */
      useProxy(win.HTMLCanvasElement.prototype, 'getContext', {
        apply: (target, thisArg, args: Parameters<typeof HTMLCanvasElement.prototype.getContext>) => {
          if (args[0] === '2d') {
            const option = args[1] ?? {};
            option.willReadFrequently = true;
            args[1] = option
          }
          return target.apply(thisArg, args);
        }
      })

      /* getImageData */
      {
        const seed = useSeed(conf.fp.other.canvas)
        if (seed != null) {
          const noise = randomCanvasNoise(seed)
          useProxy(win.CanvasRenderingContext2D.prototype, 'getImageData', {
            apply: (target, thisArg: CanvasRenderingContext2D, args: Parameters<typeof CanvasRenderingContext2D.prototype.getImageData>) => {
              notify('strong.canvas')
              return drawNoiseTo2d(ctx, noise, thisArg, ...args);
            }
          })
        }
      }
    },
  },

  /**
   * Canvas Webgl
   */
  {
    condition: ({ conf }) => conf.fp.other.webgl.type !== HookType.default,
    onEnable: ({ gthis, conf, useSeed, useProxy }) => {
      /* Image */
      {
        const seed = useSeed(conf.fp.other.webgl)
        if (seed != null) {
          const noise = randomWebglNoise(seed)
          const handler = {
            apply: (target: any, thisArg: WebGLRenderingContext | WebGL2RenderingContext, args: any) => {
              notify('strong.webgl')
              drawNoiseToWebgl(thisArg, noise)
              return target.apply(thisArg, args as any);
            }
          }
          useProxy(gthis.WebGLRenderingContext.prototype, 'readPixels', handler)
          useProxy(gthis.WebGL2RenderingContext.prototype, 'readPixels', handler)
        }
      }
    },
  },

  /**
   * Canvas Webgl参数信息
   */
  {
    condition: ({ conf, isDefault }) => !isDefault(conf.fp.normal.gpuInfo),
    onEnable: ({ gthis, conf, useHookMode, useProxy }) => {
      const fps = conf.fp.normal

      let ex: WEBGL_debug_renderer_info | null

      /* Report: Parameter */
      const info = useHookMode(fps.gpuInfo).value
      if (info) {
        const handler = {
          apply: (target: any, thisArg: WebGLRenderingContext, args: any) => {
            if (!ex) ex = thisArg.getExtension('WEBGL_debug_renderer_info');
            if (ex) {
              if (args[0] === ex.UNMASKED_VENDOR_WEBGL) {
                notify('weak.gpuInfo')
                // 模拟调用
                if (info.vendor && target.apply(thisArg, args)) {
                  return info.vendor;
                }
              } else if (args[0] === ex.UNMASKED_RENDERER_WEBGL) {
                notify('weak.gpuInfo')
                if (info.renderer && target.apply(thisArg, args)) {
                  return info.renderer;
                }
              }
            }
            return target.apply(thisArg, args);
          }
        }
        useProxy(gthis.WebGLRenderingContext.prototype, 'getParameter', handler);
        useProxy(gthis.WebGL2RenderingContext.prototype, 'getParameter', handler);
      }
    }
  },

  /**
   * Canvas Base
   */
  {
    condition: ({ conf, isDefault }) => !isDefault([conf.fp.other.canvas, conf.fp.other.webgl]),
    onEnable: (ctx) => {
      const { gthis, win, conf, useSeed, useProxy } = ctx;

      const seedCanvas = useSeed(conf.fp.other.canvas);
      const seedWebgl = useSeed(conf.fp.other.webgl);

      const noiseCanvas = seedCanvas == null ? null : randomCanvasNoise(seedCanvas);
      const noiseWebgl = seedWebgl == null ? null : randomWebglNoise(seedWebgl);

      const handler = {
        apply: (target: Function, thisArg: OffscreenCanvas | HTMLCanvasElement, args: any) => {
          /* 2d */
          if (noiseCanvas) {
            const c2d = thisArg.getContext('2d');
            if (c2d) {
              notify('strong.canvas');
              drawNoiseTo2d(
                ctx, noiseCanvas, c2d as any,
                0, 0, thisArg.width, thisArg.height
              );
              return Reflect.apply(target, thisArg, args);
            }
          }
          /* webgl */
          if (noiseWebgl) {
            const gl = thisArg.getContext('webgl') ?? thisArg.getContext('webgl2');
            if (gl) {
              notify('strong.webgl');
              drawNoiseToWebgl(gl as any, noiseWebgl);
              return Reflect.apply(target, thisArg, args);
            }
          }
          return Reflect.apply(target, thisArg, args);
        }
      }

      useProxy(gthis.OffscreenCanvas.prototype, 'convertToBlob', handler);

      if (win) {
        useProxy(win.HTMLCanvasElement.prototype, ['toDataURL', 'toBlob'], handler);
      }
    },
  },

  /**
   * Audio
   * 音频指纹
   */
  {
    condition: ({ conf }) => conf.fp.other.audio.type !== HookType.default,
    onEnable: ({ win, conf, useSeed, useProxy, useGetterProxy }) => {
      if (!win) return;

      const seed = useSeed(conf.fp.other.audio)
      if (seed == null) return;

      const mem = new WeakSet()

      useProxy(win.AudioBuffer.prototype, 'getChannelData', {
        apply: (target, thisArg: AudioBuffer, args: Parameters<typeof AudioBuffer.prototype.getChannelData>) => {
          notify('strong.audio')
          const data = target.apply(thisArg, args)
          if (mem.has(data)) return data;

          const step = data.length > 2000 ? 100 : 20;
          for (let i = 0; i < data.length; i += step) {
            const v = data[i]
            if (v !== 0 && Math.abs(v) > 1e-7) {
              data[i] += seededRandom(seed + i) * 1e-7;
            }
          }

          mem.add(data)
          return data;
        }
      })

      useProxy(win.AudioBuffer.prototype, [
        'copyFromChannel', 'copyToChannel',
      ], {
        apply: (target, thisArg: AudioBuffer, args: any) => {
          const channel = args[1]
          if (channel != null) {
            thisArg.getChannelData(channel)
          }
          return target.apply(thisArg, args)
        }
      })

      const dcNoise = seededRandom(seed) * 1e-7;
      useGetterProxy(win.DynamicsCompressorNode.prototype, 'reduction', (_, getter) => ({
        apply(target, thisArg, args: any) {
          notify('strong.audio')
          const res = getter.call(thisArg);
          return (typeof res === 'number' && res !== 0) ? res + dcNoise : res;
        }
      }))

    },
  },

  /**
   * Timezone
   * 时区
   */
  {
    condition: ({ conf }) => conf.fp.other.timezone.type !== HookType.default,
    onEnable: ({ gthis, conf, useHookMode, useProxy }) => {
      const tzValue = useHookMode(conf.fp.other.timezone).value
      if (!tzValue) return;

      const localOffsetMs = new Date().getTimezoneOffset() * 60000;
      const utcOffsetMs = tzValue.offset * 60 * 60 * 1000;
      const diffMs = utcOffsetMs + localOffsetMs;

      const _Date = gthis.Date;
      const _DateTimeFormat = gthis.Intl.DateTimeFormat;

      const dtFormatter = new _DateTimeFormat('en-US', {
        timeZone: tzValue.zone ?? 'Asia/Shanghai',
        weekday: 'short',
        month: 'short',
        day: 'numeric',
        year: 'numeric',
        hour: 'numeric',
        minute: 'numeric',
        second: 'numeric',
        fractionalSecondDigits: 3,
        hour12: false,
        timeZoneName: 'longOffset',
      })

      const weekdayShortToNumeric: Record<string, number> = {
        Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6,
      };

      const monthShortToNumeric: Record<string, number> = {
        Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11,
      };

      type TimeParts = Partial<Record<keyof Intl.DateTimeFormatPartTypesRegistry, string>>

      const getParts = (date: Date): TimeParts | undefined => {
        try {
          const plist = dtFormatter.formatToParts(date)
          return plist.reduce((acc: TimeParts, cur) => {
            acc[cur.type] = cur.value
            return acc
          }, {})
        } catch (e) { }
      }

      function isNumeric(v: any) {
        return !isNaN(v) && !isNaN(parseFloat(v));
      }

      /* DateTimeFormat */
      useProxy(gthis.Intl, 'DateTimeFormat', {
        construct: (target, args: Parameters<typeof Intl.DateTimeFormat>, newTarget) => {
          notify('weak.timezone')
          args[0] = args[0] ?? tzValue.locale
          args[1] = Object.assign({ timeZone: tzValue.zone }, args[1]);
          return new target(...args)
        },
        apply: (target, thisArg: Intl.DateTimeFormat, args: Parameters<typeof Intl.DateTimeFormat>) => {
          notify('weak.timezone')
          args[0] = args[0] ?? tzValue.locale
          args[1] = Object.assign({ timeZone: tzValue.zone }, args[1]);
          return target.apply(thisArg, args)
        },
      })

      /* Date */
      useProxy(gthis, 'Date', {
        construct: (target, args, newTarget) => {
          notify('weak.timezone')
          const raw = Reflect.construct(target, args, newTarget);

          if (typeof args[0] === 'string' && args[0].includes('/')) {
            return Reflect.construct(target, [raw.getTime() - diffMs], newTarget);
          }
          if (args[1] != null && isNumeric(args[1])) {
            return Reflect.construct(target, [raw.getTime() - diffMs], newTarget);
          }

          return raw;
        },
        apply: (target, thisArg: Date, args: Parameters<typeof Date>) => {
          return Reflect.construct(target, args).toString()
        }
      })

      /* Date getter */
      {
        type DateHandler = (parts: TimeParts, date: Date) => any

        const tasks: { [key in keyof Date]?: DateHandler } = {
          'getTimezoneOffset': () => tzValue.offset * -60,
          'toString': (p) => `${p.weekday} ${p.month} ${p.day?.padStart(2, '0')} ${p.year} ${p.hour?.padStart(2, '0')}:${p.minute?.padStart(2, '0')}:${p.second?.padStart(2, '0')} ${p.timeZoneName?.replace(':', '')}`,
          'toDateString': (p) => `${p.weekday} ${p.month} ${p.day?.padStart(2, '0')} ${p.year}`,
          'toTimeString': (p) => `${p.hour?.padStart(2, '0')}:${p.minute?.padStart(2, '0')}:${p.second?.padStart(2, '0')} ${p.timeZoneName?.replace(':', '')}`,
          'getHours': (p) => parseInt(p.hour ?? '0'),
          'getMinutes': (p) => parseInt(p.minute ?? '0'),
          'getSeconds': (p) => parseInt(p.second ?? '0'),
          'getFullYear': (p) => parseInt(p.year ?? '0'),
          'getDate': (p) => parseInt(p.day ?? '0'),
          'getMonth': (p) => monthShortToNumeric[p.month ?? ''] ?? 0,
          'getDay': (p) => weekdayShortToNumeric[p.weekday ?? ''] ?? 0,
        }

        useProxy(gthis.Date.prototype, Object.keys(tasks) as (keyof Date)[],
          (key) => {
            const task = tasks[key]
            return task && {
              apply: (target: any, thisArg: Date, args: Parameters<typeof Date.prototype.toString>) => {
                notify('weak.timezone')
                const parts = getParts(thisArg)
                if (parts) {
                  const res = task(parts, thisArg)
                  if (res != null) return res;
                }
                return Reflect.apply(target, thisArg, args)
              }
            }
          })
      }

      /* Date setter */
      useProxy(gthis.Date.prototype,
        ['setFullYear', 'setMonth', 'setDate', 'setHours', 'setMinutes', 'setSeconds'],
        {
          apply(target: any, thisArg: Date, args: any[]) {
            notify('weak.timezone');

            // to system
            const localDate = new _Date(thisArg.getTime() + diffMs);
            // set
            target.apply(localDate, args);
            // to custom
            const timeMs = localDate.getTime() - diffMs;
            thisArg.setTime(timeMs);

            return timeMs;
          }
        })

      /* toLocaleString */
      useProxy(gthis.Date.prototype, [
        'toLocaleString', 'toLocaleDateString', 'toLocaleTimeString'
      ], {
        apply: (target: any, thisArg: Date, args: Parameters<typeof Date.prototype.toLocaleString>) => {
          notify('weak.timezone')
          args[0] = args[0] ?? tzValue.locale
          args[1] = Object.assign({ timeZone: tzValue.zone }, args[1]);
          return target.apply(thisArg, args);
        }
      })
    },
  },

  /**
   * Webrtc
   */
  {
    condition: ({ conf }) => conf.fp.other.webrtc.type !== HookType.default,
    onEnable: ({ win, useDisownKeys, useDefine }) => {
      if (!win) return;

      {
        const keys: any = [
          'mediaDevices', 'getUserMedia', 'mozGetUserMedia', 'webkitGetUserMedia',
        ]
        useDisownKeys(win.navigator, keys);
        useDisownKeys(win.Navigator.prototype, keys);
        useDefine([win.Navigator.prototype, win.navigator], keys, {
          value: undefined,
          enumerable: false,
        });
        useDefine(win.Navigator.prototype, keys, {
          value: undefined,
          enumerable: false,
        });
      }

      [
        'RTCDataChannel',
        'RTCIceCandidate',
        'RTCConfiguration',
        'MediaStreamTrack',
        'RTCPeerConnection',
        'RTCSessionDescription',
        'mozMediaStreamTrack',
        'mozRTCPeerConnection',
        'mozRTCSessionDescription',
        'webkitMediaStreamTrack',
        'webkitRTCPeerConnection',
        'webkitRTCSessionDescription',
      ].forEach((key) => {
        // @ts-ignore
        if (win[key]) win[key] = undefined;
      });
    },
  },

  /**
   * Font
   * 字体指纹
   */
  {
    condition: ({ conf }) => conf.fp.other.font.type !== HookType.default,
    onEnable: ({ win, conf, useSeed, useProxy, useGetterProxy }) => {
      if (!win) return;

      const seed = useSeed(conf.fp.other.font)
      if (seed == null) return;

      useGetterProxy(win.HTMLElement.prototype, [
        'offsetHeight', 'offsetWidth'
      ], (key, getter) => ({
        apply(target: () => any, thisArg: HTMLElement, args: any) {
          notify('strong.fonts')
          const result = getter.call(thisArg);
          const mark = (thisArg.style?.fontFamily ?? key) + result;
          return result + randomFontNoise(seed, mark);
        }
      }))

      useProxy(win, 'FontFace', {
        construct: (target, args: ConstructorParameters<typeof FontFace>, newTarget) => {
          const source = args[1]
          if (typeof source === 'string' && source.startsWith('local(')) {
            notify('strong.fonts')
            const name = source.substring(source.indexOf('(') + 1, source.indexOf(')'));
            const rand = seededRandom(name + seed, 1, 0);
            if (rand < 0.02) {
              args[1] = `local("${rand}")`
            } else if (rand < 0.04) {
              args[1] = 'local("Arial")'
            }
          }
          return new target(...args)
        },
      })

    },
  },

  /**
   * Webgpu
   */
  {
    condition: ({ conf }) => conf.fp.other.webgpu.type !== HookType.default,
    onEnable: ({ win, conf, useSeed, useDefine, useProxy, newProxy }) => {
      if (!win) return;

      const seed = useSeed(conf.fp.other.webgpu)
      if (seed == null) return;

      /* GPUAdapter & GPUDevice */
      {
        const makeNoise = (raw: any, offset: number) => {
          notify('strong.webgpu')
          const rn = seededRandom(seed + (offset * 7), 64, 1)
          return raw ? raw - Math.floor(rn) : raw;
        }

        const handler = (_: any, desc: PropertyDescriptor) => {
          const getter = desc.get
          return getter && {
            get() {
              const limits = getter.call(this);
              return newProxy(limits, {
                get(target, prop) {
                  const value = target[prop];
                  switch (prop) {
                    case "maxBufferSize": return makeNoise(value, 0);
                    case "maxStorageBufferBindingSize": return makeNoise(value, 1);
                  }
                  return typeof value === "function" ? value.bind(target) : value;
                }
              })
            }
          }
        }

        // @ts-ignore
        win.GPUAdapter && useDefine(win.GPUAdapter.prototype, 'limits', handler)
        // @ts-ignore
        win.GPUDevice && useDefine(win.GPUDevice.prototype, 'limits', handler)
      }

      /*** GPUCommandEncoder ***/
      // @ts-ignore
      if (win.GPUCommandEncoder?.prototype?.beginRenderPass) {
        // @ts-ignore
        useProxy(win.GPUCommandEncoder.prototype, 'beginRenderPass', {
          apply(target, self, args) {
            notify('strong.webgpu')
            if (args?.[0]?.colorAttachments?.[0]?.clearValue) {
              try {
                const _clearValue = args[0].colorAttachments[0].clearValue
                let offset = 0
                for (let key in _clearValue) {
                  let value = _clearValue[key]
                  const noise = seededRandom(seed + (offset++ * 7), 0.01, 0.001)
                  value += value * noise * -1
                  _clearValue[key] = Math.abs(value)
                }
                args[0].colorAttachments[0].clearValue = _clearValue;
              } catch (e) { }
            }
            return Reflect.apply(target, self, args);
          }
        })
      }

      /*** GPUQueue ***/
      // @ts-ignore
      if (win.GPUQueue?.prototype?.writeBuffer) {
        // @ts-ignore
        useProxy(win.GPUQueue.prototype, 'writeBuffer', {
          apply(target, self, args) {
            notify('strong.webgpu')
            const _data = args?.[2]
            if (_data && _data instanceof Float32Array) {
              try {
                const count = Math.ceil(_data.length * 0.05)
                let offset = 0
                const selected = Array(_data.length)
                  .map((_, i) => i)
                  .sort(() => seededRandom(seed + (offset++ * 7), 1, -1))
                  .slice(0, count);

                offset = 0
                for (let i = 0; i < selected.length; i++) {
                  const index = selected[i];
                  let value = _data[index];
                  const noise = seededRandom(seed + (offset++ * 7), +0.0001, -0.0001)
                  _data[index] += noise * value;
                }
                // args[2] = _data;
              } catch (e) { }
            }
            return Reflect.apply(target, self, args);
          }
        })
      }
    },
  },

  /**
   * DomRect
   */
  {
    condition: ({ conf }) => conf.fp.other.domRect.type !== HookType.default,
    onEnable: ({ win, conf, useSeed, useProxy }) => {
      if (!win) return;

      const seed = useSeed(conf.fp.other.domRect)
      if (seed == null) return;

      const noise = seededRandom(seed, 1e-6, -1e-6);

      {
        const handler = {
          apply(target: () => DOMRect, thisArg: any, args: any) {
            notify('strong.domRect')
            const rect = Reflect.apply(target, thisArg, args);
            if (rect) {
              if (rect.x !== 0) rect.x += noise;
              if (rect.width !== 0) rect.width += noise;
            }
            return rect;
          }
        }
        useProxy(win.Element.prototype, 'getBoundingClientRect', handler)
        useProxy(win.Range.prototype, 'getBoundingClientRect', handler)
      }

      {
        const handler = {
          apply(target: () => DOMRectList, thisArg: any, args: any) {
            notify('strong.domRect')
            const rlist = Reflect.apply(target, thisArg, args);
            if (rlist) {
              for (let i = 0; i < rlist.length; i++) {
                const rect = rlist[i];
                if (rect.x !== 0) rect.x += noise;
                if (rect.width !== 0) rect.width += noise;
              }
            }
            return rlist;
          }
        }
        useProxy(win.Element.prototype, 'getClientRects', handler)
        useProxy(win.Range.prototype, 'getClientRects', handler)
      }
    }
  },

  /**
   * .ownProperties
   */
  {
    onEnable: ({ gthis, symbol, useProxy }) => {
      const symbolSet = new Set(Object.values(symbol))

      {
        useProxy(gthis.Object, 'getOwnPropertyNames', {
          apply(target, thisArg, args) {
            const res = Reflect.apply(target, thisArg, args) as string[];
            if (res) {
              const disown = args[0]?.[symbol.disown];
              if (disown && disown instanceof Set) {
                return res.filter((v) => !disown.has(v));
              }
            }
            return res;
          }
        });

        useProxy(gthis.Object, 'getOwnPropertyDescriptors', {
          apply(target, thisArg, args) {
            const res = Reflect.apply(target, thisArg, args);
            if (res) {
              for (let v of symbolSet) {
                delete res[v];
              }

              const disown = args[0]?.[symbol.disown];
              if (disown && disown instanceof Set) {
                for (let v of disown) {
                  delete res[v];
                }
              }
            }
            return res;
          }
        });

        useProxy(gthis.Object, 'getOwnPropertySymbols', {
          apply(target, thisArg, args) {
            const res = Reflect.apply(target, thisArg, args) as symbol[];
            return res?.filter((v) => !symbolSet.has(v));
          }
        });

        useProxy(gthis.Reflect, 'ownKeys', {
          apply(target, thisArg, args) {
            const res = Reflect.apply(target, thisArg, args) as (string | symbol)[];
            if (res) {
              const disown = args[0]?.[symbol.disown];
              if (disown && disown instanceof Set) {
                return res.filter((v) => typeof v === 'symbol' ? !symbolSet.has(v) : !disown.has(v));
              } else {
                return res.filter((v: any) => !symbolSet.has(v));
              }
            }
            return res;
          }
        });
      }

      {
        const handler = {
          apply(target: any, thisArg: any, args: any[]) {
            const obj = args[0];
            const key = args[1];
            if (key != null && typeof obj === 'object') {
              if (symbolSet.has(key)) return undefined;
              const disown = obj[symbol.disown];
              if (disown && disown instanceof Set) {
                if (disown.has(key)) return undefined;
              }
            }
            return Reflect.apply(target, thisArg, args);
          }
        }
        useProxy(gthis.Object, 'getOwnPropertyDescriptor', handler)
        useProxy(gthis.Reflect, 'getOwnPropertyDescriptor', handler)
      }
    }
  },

  /**
   * .setPrototypeOf
   */
  {
    onEnable: ({ gthis, symbol, hasRaw, useProxy }) => {
      useProxy(gthis.Reflect, 'setPrototypeOf', {
        apply(target: any, self: any, args: any[]) {
          const src = args[0]
          const dst = args[1]
          if (hasRaw(src) && dst != null) {
            dst[symbol.reflect] = true
            const res = Reflect.apply(target, self, args);
            delete dst[symbol.reflect]
            return res;
          }
          return Reflect.apply(target, self, args);
        }
      })
    }
  },

  /**
   * .toString
   */
  {
    onEnable: ({ gthis, info, symbol, myProxy, otherProxy, useProxy }) => {
      function isRealFunction(obj: any) {
        const target = Function.prototype.toString
        let proto = obj;
        while (proto = Object.getPrototypeOf(proto)) {
          if (proto.toString === target) {
            return !otherProxy.has(proto)
          }
        }
        return false;
      }

      useProxy(gthis.Function.prototype, 'toString', {
        apply(target: any, self: any, args: any[]) {
          try {
            if (self != null && myProxy.has(self)) {
              const raw = self[symbol.raw]
              if (raw) return Reflect.apply(target, raw, args);
            }
            return Reflect.apply(target, self, args);
          } catch (e: any) {
            // 堆栈伪造
            if (info.browser !== 'firefox') {
              const es = e.stack.split('\n')
              if (isRealFunction(self)) {
                es[1] = es[1].replace('Object', 'Function')
              }
              es.splice(2, 1);
              e.stack = es.join('\n');
            }
            throw e;
          }
        }
      })

    }
  },

  /**
   * Proxy
   */
  {
    onEnable: ({ gthis, otherProxy, useProxy }) => {
      useProxy(gthis, 'Proxy', {
        construct(target, args, newTarget) {
          const v = Reflect.construct(target, args, newTarget)
          otherProxy.add(v)
          return v;
        }
      })
    }
  },

];

```

## /src/core/utils.ts

```ts path="/src/core/utils.ts" 
import { hashNumberFromString, makeSeededRandom, seededRandom } from "@/utils/base";
import { sendToWindow } from "@/utils/message";
import { debounce } from "@/utils/timer";
import type { FingerprintContext } from "./core";

// 
// --- notification ---
// 
let fpNoticePool: Record<string, number> = {}
let iframeNoticePool: Record<string, number> = {}

/**
 * 记录指纹数量
 */
export const notify = (key: string) => {
  if (typeof window === 'undefined') return;
  fpNoticePool[key] = (fpNoticePool[key] ?? 0) + 1
  sendFpRecord()
}

const sendFpRecord = debounce(() => {
  sendToWindow((globalThis.top ?? globalThis) as any, {
    type: 'notice.push.fp',
    data: fpNoticePool,
  })
  fpNoticePool = {}
})

/**
 * 记录iframe数量
 */
export const notifyIframeOrigin = (key?: string) => {
  if (!key || key === 'null') key = 'about:blank';
  iframeNoticePool[key] = (iframeNoticePool[key] ?? 0) + 1
  sendIframeRecord()
}

const sendIframeRecord = debounce(() => {
  sendToWindow(window.top ?? window, {
    type: 'notice.push.iframe',
    data: iframeNoticePool,
  })
  iframeNoticePool = {}
})

// 
// --- random ---
// 

/**
 * 随机canvas噪音
 */
export const randomCanvasNoise = (seed: number) => {
  const rand = makeSeededRandom(seed, 255, 0)
  const noise: number[] = []
  for (let i = 0; i < 10; i++) {
    noise.push(Math.floor(rand()))
  }
  return noise
}

/**
 * 获取[x, y, ...],区间[-1, 1]
 */
export const randomWebglNoise = (seed: number): number[] => {
  const rand = makeSeededRandom(seed, 1, -1)
  const ps = []
  for (let i = 0; i < 20; i++) {
    ps.push(rand())
  }
  return ps;
}

/**
 * 获取随机字体噪音
 */
export const randomFontNoise = (seed: number, mark: string): number => {
  const random = seededRandom((seed + hashNumberFromString(mark)) % Number.MAX_SAFE_INTEGER, 3, 0)
  if ((random * 10) % 1 < 0.9) return 0;
  return Math.floor(random) - 1;
}

/**
 * 获取随机屏幕尺寸
 */
export const randomScreenSize = (screen: Screen, seed: number) => {
  const offset = Math.round(seededRandom(seed, 100, -100))
  const ratio = screen.width / screen.height;

  let width: number
  let height: number
  if (screen.width >= screen.height) {
    // 偏移宽度
    width = screen.width + offset;
    height = Math.round(width / ratio);
  } else {
    // 偏移高度
    height = screen.height + offset;
    width = Math.round(height * ratio);
  }

  return { width, height };
}


// 
// --- other ---
// 

type U8Array = Uint8ClampedArray | Uint8Array;

const isPixelEqual = (p1: U8Array, p2: U8Array) => {
  return p1[0] === p2[0] && p1[1] === p2[1] && p1[2] === p2[2] && p1[3] === p2[3];
}

const pixelCopy = (src: U8Array, dst: U8Array, index: number) => {
  dst[0] = src[index]
  dst[1] = src[index + 1]
  dst[2] = src[index + 2]
  dst[3] = src[index + 3]
}

/**
 * 在2d画布绘制噪音
 */
export const drawNoiseTo2d = (
  { useRaw }: FingerprintContext,
  noise: number[],
  target: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
  sx: number, sy: number, sw: number, sh: number, settings?: ImageDataSettings
) => {
  const imageData = useRaw(target.getImageData).call(target, sx, sy, sw, sh, settings)

  let noiseIndex = 0;
  let isChanged = false

  const Arr = Uint8ClampedArray;
  const center = new Arr(4)
  const up = new Arr(4)
  const down = new Arr(4)
  const left = new Arr(4)
  const right = new Arr(4)

  const pixelData = imageData.data

  outer: for (let row = 1; row < sh - 2; row += 2) {
    for (let col = 1; col < sw - 2; col += 2) {
      if (noise.length === noiseIndex) { break outer; }

      const index = (row * sw + col) * 4;
      pixelCopy(pixelData, center, index)

      pixelCopy(pixelData, up, ((row - 1) * sw + col) * 4)
      if (isPixelEqual(center, up)) continue;

      pixelCopy(pixelData, down, ((row + 1) * sw + col) * 4)
      if (isPixelEqual(center, down)) continue;

      pixelCopy(pixelData, left, (row * sw + (col - 1)) * 4)
      if (isPixelEqual(center, left)) continue;

      pixelCopy(pixelData, right, (row * sw + (col + 1)) * 4)
      if (isPixelEqual(center, right)) continue;

      pixelData[index + 3] = (noise[noiseIndex++] % 256)
      isChanged = true
    }
  }

  if (isChanged) {
    target.putImageData(imageData, sx, sy)
  }

  return imageData
}

/**
 * 代理UserAgentData
 */
export const proxyUserAgentData = (ctx: FingerprintContext, rawValue: any, proxyValue: ClientHintsInfo) => {
  if (!rawValue || !proxyValue) return rawValue;

  const uaData = proxyValue.uaData
  if (!uaData) return rawValue;

  const fullVersionList = uaData.versions
  const brands = fullVersionList?.map(v => ({
    ...v,
    version: v.version.split('.')[0]
  }))

  return ctx.newProxy(rawValue, {
    get: (target, key) => {
      const raw = target[key]
      switch (key) {
        case 'brands': return brands;
        case 'mobile': return uaData.mobile;
        case 'platform': return uaData.platform;
        case 'toJSON': {
          if (!raw) return raw;
          return ctx.newProxy(raw.bind(target), {
            apply: (target, thisArg, args) => {
              const res = target.apply(thisArg, args);
              res.brands = brands;
              res.mobile = uaData.mobile;
              res.platform = uaData.platform;
              return res;
            }
          })
        }
        case 'getHighEntropyValues': {
          if (!raw) return raw;
          return ctx.newProxy(raw.bind(target), {
            apply: (target, thisArg, args) => {
              return target.apply(thisArg, args)?.then((v: any) => {
                if (!v) return v;
                if (v.architecture != null) v.architecture = uaData.arch;
                if (v.bitness != null) v.bitness = uaData.bitness;
                if (v.model != null) v.model = uaData.model;
                if (v.mobile != null) v.mobile = uaData.mobile;
                if (v.platform != null) v.platform = uaData.platform;
                if (v.platformVersion != null) v.platformVersion = uaData.platformVersion;
                if (v.formFactors != null) v.formFactors = uaData.formFactors;
                if (v.uaFullVersion != null) v.uaFullVersion = uaData.uaFullVersion;
                if (v.brands != null) v.brands = brands;
                if (v.fullVersionList != null) v.fullVersionList = fullVersionList;
                return v;
              })
            }
          })
        }
      }
      return typeof raw === 'function' ? raw.bind(target) : raw
    }
  })
}

/**
 * 在webgl上下文绘制噪音点
 * @param noisePosition 区间[-1, 1]
 */
export const drawNoiseToWebgl = (gl: WebGLRenderingContext | WebGL2RenderingContext, noisePosition: number[]) => {
  const vertexShaderSource = `attribute vec4 noise;void main() {gl_Position = noise;gl_PointSize = 0.001;}`;
  const fragmentShaderSource = `void main() {gl_FragColor = vec4(0.0, 0.0, 0.0, 0.01);}`;

  const createShader = (gl: WebGLRenderingContext | WebGL2RenderingContext, type: GLenum, source: string) => {
    const shader = gl.createShader(type);
    if (!shader) return;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    return shader;
  }

  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  if (!vertexShader || !fragmentShader) return;

  const program = gl.createProgram();
  if (!program) return;
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  gl.useProgram(program);

  const positions = new Float32Array(noisePosition);
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

  const noise = gl.getAttribLocation(program, 'noise');
  gl.enableVertexAttribArray(noise);
  gl.vertexAttribPointer(noise, 2, gl.FLOAT, false, 0, 0);
  gl.drawArrays(gl.POINTS, 0, 1);
}
```

## /src/locales/en_US.json

```json path="/src/locales/en_US.json" 
{
  "g": {
    "enable": "Enable",
    "disable": "Disable",
    "enabled": "Enabled",
    "disabled": "Disabled",
    "preset": "Preset",
    "custom": "Custom",
    "special": "Special",
    "confirm": "Confirm",
    "cancel": "Cancel",
    "save": "Save",
    "random": "Random",
    "apply": "Apply",
    "local": "Local",
    "online": "Online",
    "reset": "Reset"
  },
  "e": {
    "enabled": "Extension Enabled",
    "disabled": "Extension Disabled",
    "whitelist": "Whitelist",
    "whitelist-in": "In Whitelist",
    "whitelist-not": "Not In Whitelist",
    "whitelist-click-in": "Click To Whitelist",
    "record": "Record",
    "config": "Config",
    "seed": "Seed",
    "more": "More",
    "fp-record": "Fingerprint",
    "iframe-record": "Iframe"
  },
  "label": {
    "config-file": "Config File",
    "config-import": "Import Config",
    "config-export": "Export Config",
    "config": {
      "strong": "Strong Fingerprint",
      "weak": "Weak Fingerprint",
      "script": "Script Config",
      "prefs": "Preferences"
    },
    "clipboard-export": "Export To Clipboard",
    "clipboard-import": "Import From Clipboard",
    "permission": "Permission",
    "subscribe": "Subscribe",
    "fp-notice": {
      "weak": "Weak",
      "strong": "Strong",
      "other": "Other"
    },
    "preset-panel": "Preset Panel"
  },
  "type": {
    "default": "Default",
    "value": "Custom value",
    "page": "Random by Tab Page",
    "browser": "Random by Browser Start",
    "domain": "Random by Domain",
    "global": "Random by Global Seed",
    "enabled": "Enabled",
    "disabled": "Disabled"
  },
  "item": {
    "label": {
      "glVendor": "Vendor",
      "glRenderer": "Renderer"
    },
    "title": {
      "clientHints": "ClientHints Info",
      "languages": "Browser Language",
      "hardwareConcurrency": "Number of logical processors",
      "size": "Screen Size",
      "depth": "Screen Color Depth",
      "timezone": "Timezone",
      "canvas": "Canvas Fingerprint",
      "audio": "Audio Fingerprint",
      "webgl": "WebGL Fingerprint",
      "webrtc": "WebRTC Interface",
      "fonts": "Fonts Fingerprint",
      "webgpu": "WebGPU Fingerprint",
      "gpuInfo": "Graphics Driver",
      "domRect": "DOMRect Fingerprint",
      "serviceWorker": "Service Worker Interface",
      "e-language": "Language",
      "theme": "Theme",
      "seed": "Global Seed",
      "fast-inject": "Fast Inject Mode"
    },
    "desc": {
      "clientHints": [
        "ClientHints information, including `UserAgent` and `UserAgentData` configuration",
        "`versions` should use the format `brand=version`, with multiple values separated by commas",
        "`formFactors` supports multiple values separated by commas"
      ],
      "languages": [
        "Browser language settings.",
        "Custom: separate multiple values with commas, e.g. `en-US,en,zh-CN,zh`. The first value is treated as the preferred language."
      ],
      "hardwareConcurrency": "Number of logical processors.",
      "size": "Screen Size.",
      "depth": "Screen Color Depth.",
      "timezone": "Browser Timezone. [More Timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)",
      "gpuInfo": "GPU graphics driver information.",
      "canvas": "Canvas fingerprinting detects device traits by analyzing subtle differences in how graphics are drawn.",
      "audio": "Audio fingerprinting identifies your device through unique patterns in audio signal processing.",
      "webgl": "WebGL fingerprinting captures device characteristics based on variations in 3D rendering behavior.",
      "webrtc": [
        "When disabled, websites are prevented from obtaining your real IP address via WebRTC (especially when using a proxy). When enabled, it helps prevent IP leaks.",
        "When disabled, some websites may behave abnormally or fail to function properly."
      ],
      "fonts": "Font fingerprinting reveals device details by checking which fonts are supported and how they’re rendered.",
      "webgpu": "WebGPU fingerprinting extracts device information by analyzing differences in GPU processing capabilities.",
      "domRect": "DOMRect fingerprinting gathers device data by measuring variations in element positioning and dimensions.",
      "serviceWorker": [
        "When disabled, websites are prevented from registering Service Workers, blocking them from using Service Workers to collect device information.",
        "When disabled, some websites may behave abnormally or fail to function properly."
      ],
      "e-language": "Language",
      "theme": "Theme",
      "seed": "Global Seed, Acts on **Random by Global Seed** Options",
      "fast-inject": [
        "When enabled, a faster script injection method will be used, requiring browser support; when disabled, compatibility mode is applied.",
        "---",
        "**Chrome Enable Conditions**",
        "- `Chrome 138+` Enable the **Allow running user scripts** option on the extension details page, no Developer Mode required",
        "- `Chrome 120+` Enable Developer Mode",
        "**Edge Enable Conditions**",
        "- `Edge 140+` Enable the **Allow user scripts** option on the extension details page, no Developer Mode required",
        "- `Edge 120+` Enable Developer Mode",
        "**Firefox Enable Conditions**",
        "- `Firefox 136+` Authorization allowed"
      ]
    },
    "theme": {
      "system": "System",
      "dark": "Dark",
      "light": "Light"
    },
    "warn": {
      "unsupported-net-hook": "Unsupported Net Request Hooks"
    },
    "sub": {
      "tz": {
        "offset": "Offset",
        "locale": "Locale",
        "zone": "Zone"
      }
    }
  },
  "tip": {
    "label": {
      "not-support-whitelist": "Not Supported",
      "no-auth-required": "No Auth Required",
      "json": "Json Format",
      "subscribe-save": "Save and Subscribe",
      "subscribe-test": "Subscription Target Detection",
      "no-fp-notice": "No records found",
      "select-content": "Please select content",
      "unsupport-content": "Unsupported content",
      "version-mismatch": "Version Mismatch"
    },
    "ok": {
      "config-import": "Import Config Success",
      "config-export": "Export Config Success",
      "subscribe-test": "Subscription Test Passed",
      "subscribe": "Subscription Success"
    },
    "err": {
      "input-hostname": "Please enter the correct domain name",
      "input-port": "Please enter the correct port number",
      "domain-exist": "Host already exists",
      "parent-domain-exist": "Parent domain already exists",
      "config-parse": "Config Parse Error",
      "config-import": "Import Config Error",
      "config-export": "Export Config Error",
      "config-unloaded": "Config Unloaded",
      "import-empty": "Import Config is Empty",
      "subscribe-test": "Subscription Test Failed",
      "subscribe": "Subscription Failed",
      "ns-fast-inject": "Enable failed, please view the \"?\" option description"
    },
    "if": {
      "remove-parent-domain": "Whether to remove its parent domain",
      "remove-child-domain": "Whether to remove its child domain",
      "remove-permission": "Whether to remove its permissions",
      "config-import": "Whether to overwrite the current config",
      "clean-whitelist": "Whether to clean the current whitelist"
    }
  },
  "perm": {
    "status": {
      "on": "Granted",
      "off": "Click to give permission",
      "unknown": "Not supported by current browser version"
    },
    "desc": {
      "userScripts": "Allow the extension to use the **userScripts** permission to make the extension adaptable to more sites"
    }
  },
  "desc": {
    "subscribe": [
      "When the extension loads, it will fetch configuration from the subscription target. It will overwrite the original settings and merge the whitelist. The configuration must be in JSON format. Refer to README.md for more details.",
      "Empty value - ` `",
      "Default - `config.json`",
      "Local - `file:///example/config.json`",
      "Remote - `http://example.com/config.json`"
    ],
    "record": [
      "**Fingerprint Record**",
      "Record the call of fingerprint api, some api are often used when rendering elements, so the number of records for some api will be very large",
      "**Embedded Frame Record**",
      "Record the source address and number of iframe loaded"
    ]
  },
  "tag": {
    "deprecated": "Deprecated",
    "unstable": "Unstable",
    "general": "General",
    "only-whitelist": "Only Whitelist",
    "unsupport-import": "Unsupport Import"
  }
}
```

## /src/locales/index.ts

```ts path="/src/locales/index.ts" 
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enUS from './en_US.json';
import zhCN from './zh_CN.json';

const resources = {
	'en-US': {
		translation: enUS,
	},
	'zh-CN': {
		translation: zhCN,
	},
};

i18n.use(initReactI18next).init({
	resources,
	lng: 'en-US',
	fallbackLng: 'en-US',
	interpolation: {
		escapeValue: false,
	},
});

export default i18n;

```

## /src/locales/zh_CN.json

```json path="/src/locales/zh_CN.json" 
{
  "g": {
    "enable": "开启",
    "disable": "关闭",
    "enabled": "已开启",
    "disabled": "已关闭",
    "preset": "预设",
    "custom": "自定义",
    "special": "特殊",
    "confirm": "确定",
    "cancel": "取消",
    "save": "保存",
    "random": "随机",
    "apply": "应用",
    "local": "本地",
    "online": "在线",
    "reset": "重置"
  },
  "e": {
    "enabled": "扩展已开启",
    "disabled": "扩展已关闭",
    "whitelist": "白名单",
    "whitelist-in": "在白名单中",
    "whitelist-not": "不在白名单",
    "whitelist-click-in": "点击加入白名单",
    "record": "记录",
    "config": "配置",
    "seed": "种子",
    "more": "更多",
    "fp-record": "指纹",
    "iframe-record": "内嵌框架"
  },
  "label": {
    "config-file": "配置文件",
    "config-import": "导入配置",
    "config-export": "导出配置",
    "config": {
      "strong": "强指纹",
      "weak": "弱指纹",
      "script": "脚本配置",
      "prefs": "个性化配置"
    },
    "clipboard-export": "复制到剪贴板",
    "clipboard-import": "从剪贴板导入",
    "permission": "权限",
    "subscribe": "订阅",
    "fp-notice": {
      "weak": "弱指纹",
      "strong": "强指纹",
      "other": "其他"
    },
    "preset-panel": "预设面板"
  },
  "type": {
    "default": "系统值",
    "value": "自定义值",
    "page": "每个标签页随机值",
    "browser": "每次启动浏览器随机值",
    "domain": "根据访问域名随机值",
    "global": "根据全局种子随机值",
    "enabled": "启用",
    "disabled": "禁用"
  },
  "item": {
    "label": {
      "glVendor": "供应商",
      "glRenderer": "渲染器"
    },
    "title": {
      "clientHints": "ClientHints信息",
      "languages": "浏览器语言",
      "hardwareConcurrency": "逻辑处理器数量",
      "size": "屏幕尺寸",
      "depth": "屏幕色彩深度",
      "timezone": "时区",
      "canvas": "Canvas指纹",
      "audio": "Audio指纹",
      "webgl": "WebGL指纹",
      "webrtc": "WebRTC接口",
      "fonts": "Fonts指纹",
      "webgpu": "WebGPU指纹",
      "gpuInfo": "图形驱动信息",
      "domRect": "DOMRect指纹",
      "serviceWorker": "Service Worker 接口",
      "e-language": "语言",
      "theme": "主题",
      "seed": "全局种子",
      "fast-inject": "快速注入模式"
    },
    "desc": {
      "clientHints": [
        "ClientHints信息,包含`UserAgent`和`UserAgentData`配置",
        "`versions`使用`brand=version`的格式,多个值用逗号分隔",
        "`formFactors`多个值用逗号分隔"
      ],
      "languages": [
        "浏览器语言",
        "自定义:多个值用逗号分隔,首个值作为首选语言,例如`zh-CN,zh,en-US,en`"
      ],
      "hardwareConcurrency": "逻辑处理器数量",
      "size": "屏幕尺寸",
      "depth": "屏幕颜色深度",
      "timezone": "浏览器时区,[更多时区](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)",
      "gpuInfo": "GPU图形驱动信息",
      "canvas": "Canvas画布指纹,通过绘图行为差异获取设备特征。",
      "audio": "Audio音频指纹,通过音频信号处理获取设备特征。",
      "webgl": "WebGL指纹,通过图形渲染差异获取设备特征。",
      "webrtc": [
        "禁用后, 阻止网站通过 WebRTC 获取真实 IP 地址(尤其在使用代理时)。开启后可防止 IP 泄露。",
        "禁用后, 可能会导致某些网站异常行为或无法正常工作。"
      ],
      "fonts": "Fonts字体指纹,通过检测字体支持情况获取设备特征。",
      "webgpu": "WebGPU指纹,通过设备图形处理参数差异获取设备特征。",
      "domRect": "DOMRect指纹,通过测量元素位置和尺寸差异获取设备特征。",
      "serviceWorker": [
        "禁用后, 阻止网站注册 Service Worker, 防止网站通过 Service Worker 获取设备信息。",
        "禁用后, 可能会导致某些网站异常行为或无法正常工作。"
      ],
      "e-language": "语言",
      "theme": "主题",
      "seed": "全局种子,作用于 **根据全局种子随机值** 选项",
      "fast-inject": [
        "开启后,会使用更快速的脚本注入方式,需要浏览器支持;关闭则为兼容模式",
        "---",
        "**Chrome 开启条件**",
        "- `Chrome 138+` 开启扩展详情页面的 **允许运行用户脚本** 选项, 不需要开发者模式",
        "- `Chrome 120+` 开启开发者模式",
        "**Edge 开启条件**",
        "- `Edge 140+` 开启扩展详情页面的 **允许用户脚本** 选项, 不需要开发者模式",
        "- `Edge 120+` 开启开发者模式",
        "**Firefox 开启条件**",
        "- `Firefox 136+` 允许授权"
      ]
    },
    "theme": {
      "system": "跟随系统",
      "dark": "暗色",
      "light": "浅色"
    },
    "warn": {
      "unsupported-net-hook": "不支持网络请求钩子"
    },
    "sub": {
      "tz": {
        "offset": "时间差",
        "locale": "语言环境",
        "zone": "时区名称"
      }
    }
  },
  "tip": {
    "label": {
      "not-support-whitelist": "当前标签页不支持",
      "no-auth-required": "无需授权",
      "json": "Json格式",
      "subscribe-save": "保存并订阅",
      "subscribe-test": "订阅目标检测",
      "no-fp-notice": "暂无记录",
      "select-content": "请选择内容",
      "unsupport-content": "内容不支持",
      "version-mismatch": "版本不匹配"
    },
    "ok": {
      "config-import": "配置导入成功",
      "config-export": "配置导出成功",
      "subscribe-test": "订阅测试通过",
      "subscribe": "订阅成功"
    },
    "err": {
      "input-hostname": "请输入正确的域名",
      "input-port": "请输入正确的端口号",
      "domain-exist": "域名已存在",
      "parent-domain-exist": "父域名已存在",
      "config-parse": "配置解析失败",
      "config-import": "配置导入失败",
      "config-export": "配置导出失败",
      "config-unloaded": "配置未加载",
      "import-empty": "导入配置为空",
      "subscribe-test": "订阅测试失败",
      "subscribe": "订阅失败",
      "ns-fast-inject": "开启失败,请查看选项的 \"?\" 配置描述"
    },
    "if": {
      "remove-parent-domain": "是否移除对应的父域名",
      "remove-child-domain": "是否移对应的除子域名",
      "remove-permission": "是否关闭对应的权限",
      "config-import": "是否覆盖当前配置",
      "clean-whitelist": "是否清空当前白名单"
    }
  },
  "perm": {
    "status": {
      "on": "已授权",
      "off": "点击授权",
      "unknown": "当前浏览器版本不支持"
    },
    "desc": {
      "userScripts": "允许该扩展使用**userScripts**权限,使该扩展能适应更多网站"
    }
  },
  "desc": {
    "subscribe": [
      "扩展加载时会从订阅目标上拉取配置,会覆盖原配置并合并白名单,配置内容必须是json格式。前往README.md查看更多信息",
      "空值 - ` `",
      "默认 - `config.json`",
      "本地 - `file:///example/config.json`",
      "网络 - `http://example.com/config.json`"
    ],
    "record": [
      "**指纹记录**",
      "记录指纹api的调用,部分api在元素渲染时经常用到,因此,有些api记录数量会非常多",
      "**内嵌框架记录**",
      "记录加载iframe的源地址和数量"
    ]
  },
  "tag": {
    "deprecated": "弃用",
    "unstable": "不稳定",
    "general": "通用",
    "only-whitelist": "仅白名单",
    "unsupport-import": "不支持导入"
  }
}
```

## /src/popup/App.tsx

```tsx path="/src/popup/App.tsx" 
import { Badge, Button, Divider, Layout, Popconfirm, Tabs, type TabsProps, Typography, message } from "antd"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"

import {
  GithubOutlined,
  CheckOutlined,
  CloseOutlined,
} from '@ant-design/icons';

import FConfig from "./config"
import WhitelistView from "./whitelist"

import { compareVersions, tryUrl, existParentDomain, selectParentDomains } from "@/utils/base"
import { useStorageStore } from "./stores/storage";
import MoreView from "./more";
import { useShallow } from "zustand/shallow";
import { sendToBackground } from "@/utils/message";
import { NoticePanel } from "./record";

function Application() {
  const [t, i18n] = useTranslation()
  const [enabled, setEnabled] = useState(false)
  const [tab, setTab] = useState<chrome.tabs.Tab>()
  const [hostname, setHostname] = useState<string>()
  const [whitelistMode, setWhitelistMode] = useState<'none' | 'self' | 'sub'>('none')
  const [hasNewVersion, setHasNewVersion] = useState(false)

  const [messageApi, contextHolder] = message.useMessage();

  const manifest = useMemo<chrome.runtime.Manifest>(() => chrome.runtime.getManifest(), [])

  const { config, whitelist, addWhitelist, deleteWhitelist } = useStorageStore(useShallow((state) => {
    state.config ?? state.loadStorage()
    return {
      config: state.config,
      whitelist: state.whitelist,
      addWhitelist: state.addWhitelist,
      deleteWhitelist: state.deleteWhitelist,
    }
  }))
  const isShowConfigBadge = !config?.action.fastInject;

  useEffect(() => {
    chrome.tabs.query({ active: true, currentWindow: true }).then(tabs => {
      const tab = tabs[0]
      setTab(tab)
      if (!tab || !tab.url || !tab.id) return;
      const _url = tryUrl(tab.url)
      if (_url && (_url.protocol === 'http:' || _url.protocol === 'https:')) {
        /* 允许白名单 */
        setHostname(_url.hostname)
      }
    })
    sendToBackground({ type: 'version.latest' }).then((version) => {
      if (!version) return
      setHasNewVersion(compareVersions(manifest.version, version) === -1)
    })
  }, [])

  useEffect(() => {
    if (!whitelist || !hostname) return;
    if (whitelist.includes(hostname)) {
      setWhitelistMode('self')
    } else if (existParentDomain(whitelist, hostname)) {
      setWhitelistMode('sub')
    }
  }, [whitelist, hostname])

  useEffect(() => {
    if (!config) return
    setEnabled(config.enable)
  }, [config])

  const switchEnable = () => {
    if (enabled) {
      sendToBackground({
        type: 'config.set',
        config: { enable: false },
      })
      setEnabled(false)
    } else {
      sendToBackground({
        type: 'config.set',
        config: { enable: true },
      })
      setEnabled(true)
    }
  }

  const switchWhitelist = () => {
    if (!hostname || !whitelist) return;
    if (whitelistMode === 'none') {
      /* 添加白名单 */
      addWhitelist(hostname)
      setWhitelistMode('self')
    } else if (whitelistMode === 'self') {
      /* 移除自身 */
      deleteWhitelist(hostname)
      setWhitelistMode('none')
    } else if (whitelistMode === 'sub') {
      /* 移除父域名 */
      deleteWhitelist(selectParentDomains(whitelist, hostname))
      setWhitelistMode('none')
    }
  }

  const tabItems = useMemo<TabsProps['items']>(() => {
    return [
      {
        label: t('e.record'),
        children: <NoticePanel tab={tab} />,
      },
      {
        label: <Badge dot={isShowConfigBadge}>{t('e.config')}</Badge>,
        children: <FConfig />,
      },
      {
        label: t('e.whitelist'),
        children: <WhitelistView msgApi={messageApi} />,
      },
      {
        label: t('e.more'),
        children: <MoreView />,
      }
    ].map((item, index) => ({ ...item, key: String(index) }))
  }, [i18n.language, tab, isShowConfigBadge])

  return (
    <Layout className="overflow-y-auto no-scrollbar p-2 w-80 flex flex-col">
      {contextHolder}

      <section className='relative'>
        <Typography.Text className="relative flex justify-center text-xl font-black">
          My Fingerprint
        </Typography.Text>
        <Badge dot={hasNewVersion} offset={[-2, 2]} style={{ width: '8px', height: '8px' }} className="absolute left-0 top-0 cursor-pointer">
          <GithubOutlined className="text-lg" onClick={() => window.open(hasNewVersion ? 'https://github.com/omegaee/my-fingerprint/releases' : 'https://github.com/omegaee/my-fingerprint')} />
        </Badge>
        <Typography.Text className="absolute right-0 bottom-0 text-xs font-mono font-bold">v{manifest.version}</Typography.Text>
      </section>

      <Divider style={{ margin: '8px 0' }} />

      <section className="flex items-stretch gap-2">

        {/* 白名单开关 */}
        <section className="grow flex flex-col items-center gap-1">
          <Popconfirm
            disabled={whitelistMode !== 'sub'}
            title={t('tip.if.remove-parent-domain')}
            placement='bottom'
            onConfirm={switchWhitelist}
            okText={t('g.confirm')}
            cancelText={t('g.cancel')}
            okType='danger' >
            <Button type={whitelistMode !== 'none' ? 'primary' : 'default'}
              danger={!hostname}
              className="font-mono font-bold"
              style={{ width: '100%' }}
              onClick={whitelistMode !== 'sub' ? switchWhitelist : undefined} >
              {hostname ?? t('tip.label.not-support-whitelist')}
            </Button>
          </Popconfirm>
          <Typography.Text className="text-[13px]">{whitelistMode !== 'none' ? t('e.whitelist-in') : t('e.whitelist-click-in')}</Typography.Text>
        </section>

        {/* 插件开关 */}
        <section className="flex flex-col items-center gap-1">
          <Button type={enabled ? 'primary' : 'default'} className="font-bold" onClick={switchEnable}>
            {enabled ? t('g.enabled') : t('g.disabled')}
          </Button>
          {/* <Switch value={enabled} onChange={setEnabled} /> */}
          <Typography.Text className="text-[13px]">{enabled ? t('e.enabled') : t('e.disabled')}</Typography.Text>
        </section>

      </section>

      <Divider style={{ margin: '8px 0 0 0' }} />

      <Tabs className="h-[450px] grow [&_.ant-tabs-tabpane]:animate-fadeIn"
        type="line" size='small' centered
        items={tabItems}
        tabBarStyle={{ marginBottom: '8px' }} />

    </Layout>
  )
}

export default Application

```

## /src/popup/config/group/prefs.tsx

```tsx path="/src/popup/config/group/prefs.tsx" 
import { useTranslation } from "react-i18next"
import { useStorageStore } from "@/popup/stores/storage"
import TipIcon from "@/components/data/tip-icon"
import { memo, useMemo } from "react";
import { Select, Spin } from "antd";
import { LoadingOutlined } from '@ant-design/icons'
import { usePrefsStore } from "@/popup/stores/prefs";
import { ConfigDesc, ConfigItemY } from "../item";
import { useShallow } from "zustand/shallow";

const LANG_OPTIONS = [
  {
    label: '中文',
    value: 'zh-CN'
  },
  {
    label: 'English',
    value: 'en-US'
  }
]

const nsImportTag = ['unsupport-import']

export const PrefsConfigGroup = memo(() => {
  const [t, i18n] = useTranslation()

  const { config, version } = useStorageStore(useShallow((s) => ({
    config: s.config,
    version: s.version,
  })))

  const prefs = usePrefsStore()

  const themeOptions = useMemo(() => [
    {
      label: t('item.theme.system'),
      value: 'system'
    }, {
      label: t('item.theme.light'),
      value: 'light'
    }, {
      label: t('item.theme.dark'),
      value: 'dark'
    }
  ], [i18n.language])

  return config ? <div key={version}>

    <ConfigItemY
      label={t('item.title.e-language')}
      endContent={<TipIcon.Question content={<ConfigDesc tags={nsImportTag} desc={t('item.desc.e-language')} />} />}
    >
      <Select
        options={LANG_OPTIONS}
        value={prefs.language}
        onChange={(value) => {
          config.prefs.language = value
          prefs.setLanguage(value)
        }}
      />
    </ConfigItemY>

    <ConfigItemY
      label={t('item.title.theme')}
      endContent={<TipIcon.Question content={<ConfigDesc tags={nsImportTag} desc={t('item.desc.theme')} />} />}
    >
      <Select
        options={themeOptions}
        value={prefs.theme}
        onChange={(value) => {
          config.prefs.theme = value
          prefs.setTheme(value)
        }}
      />
    </ConfigItemY>

  </div> : <Spin indicator={<LoadingOutlined spin />} />
})

export default PrefsConfigGroup
```

## /src/popup/config/group/script.tsx

```tsx path="/src/popup/config/group/script.tsx" 
import { useStorageStore } from "@/popup/stores/storage"
import { memo, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import TipIcon from "@/components/data/tip-icon"
import { genRandomSeed, hashNumberFromString } from "@/utils/base"
import { App, Badge, Button, Input, Spin, Switch, Tooltip } from "antd"
import { LoadingOutlined, RedoOutlined } from '@ant-design/icons'
import { getBrowserInfo, requestPermission } from "@/utils/browser"
import { ConfigDesc, ConfigItemX, ConfigItemY } from "../item"
import { sendToBackground } from "@/utils/message"
import { useShallow } from "zustand/shallow"
import { Md } from "@/components/data/markdown"

const nsImportTag = ['unsupport-import']

export const ScriptConfigGroup = memo(() => {
  const [t] = useTranslation()
  const [fastInject, setFastInject] = useState(false)
  const [globalSeed, setGlobalSeed] = useState('')

  const { message } = App.useApp()

  const { config, version } = useStorageStore(useShallow((s) => ({
    config: s.config,
    version: s.version,
  })))

  const isShowBadge = !config?.action.fastInject;

  const browserInfo = useMemo(() => getBrowserInfo(navigator.userAgent), [])

  useEffect(() => {
    if (!config) return;
    setGlobalSeed(config.input.globalSeed)
    setFastInject(config.action.fastInject)
  }, [config])

  /**
   * set global seed
   */
  const onSetGlobalSeed = (value: string) => {
    if (!config) return;
    if (value === '') {
      setGlobalSeed('')
      config.seed.global = 0
      config.input.globalSeed = ''
      return;
    }
    setGlobalSeed(value)
    config.input.globalSeed = value
    const _value = Number(value)
    if (isNaN(_value)) {
      config.seed.global = hashNumberFromString(value)
    } else {
      config.seed.global = _value
    }
  }

  /**
   * set fast inject mode
   */
  const onSetFastInject = async (checked: boolean) => {
    if (!config) return;
    /* 尝试启用 */
    if (checked === true) {
      if (await sendToBackground({
        type: 'api.check',
        api: 'userScripts',
      }) !== true) {
        message.warning(t('tip.err.ns-fast-inject'))
        return;
      }
    }
    /* set */
    setFastInject(checked)
    if (config.action.fastInject !== checked) {
      config.action.fastInject = checked
    }
  }

  return config ? <div key={version}>
    <ConfigItemY
      label={t('item.title.seed')}
      endContent={<TipIcon.Question content={<Md>{t('item.desc.seed')}</Md>} />}
    >
      <div className="flex gap-1">
        <Input
          className="grow"
          value={globalSeed}
          onInput={({ target }: any) => onSetGlobalSeed(target.value)}
        />
        <Tooltip title={t('g.random')}>
          <Button icon={<RedoOutlined />} onClick={() => {
            const seed = genRandomSeed()
            onSetGlobalSeed(seed.toString())
          }} />
        </Tooltip>
      </div>
    </ConfigItemY>

    <ConfigItemX
      label={<Badge dot={isShowBadge}>{t('item.title.fast-inject')}</Badge>}
      endContent={<TipIcon.Question content={<ConfigDesc tags={nsImportTag} desc={t('item.desc.fast-inject', { joinArrays: '\n\n' })} />} />}
    >
      <Switch
        className="[&_.ant-switch-inner>span]:font-bold"
        value={fastInject}
        onChange={async (v) => {
          await requestPermission('userScripts')
          onSetFastInject(v)
        }}
      />
    </ConfigItemX>

  </div> : <Spin indicator={<LoadingOutlined spin />} />
})

export default ScriptConfigGroup
```

## /src/popup/config/group/strong.tsx

```tsx path="/src/popup/config/group/strong.tsx" 
import { useStorageStore } from "@/popup/stores/storage"
import { useTranslation } from "react-i18next"
import { HookType } from '@/types/enum'
import { memo } from "react"
import { Spin } from "antd"
import { LoadingOutlined } from '@ant-design/icons'
import { ConfigDesc, ConfigItemY, HookModeContent } from "../item"
import TipIcon from "@/components/data/tip-icon"
import { selectStatusDotStyles as dotStyles } from "../styles"
import { useShallow } from "zustand/shallow"

const baseTypes = [HookType.default, HookType.page, HookType.browser, HookType.domain, HookType.global]
const disabledTypes = [HookType.default, HookType.disabled]

const unstableTag = ['unstable']

export const StrongFpConfigGroup = memo(() => {
  const [t] = useTranslation()

  const storage = useStorageStore(useShallow((s) => ({
    config: s.config,
    version: s.version,
  })))
  const fp = storage.config?.fp

  return fp ? <div key={storage.version}>

    <HookModeContent
      mode={fp.other.canvas}
      types={baseTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.canvas')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.canvas')} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.webgl}
      types={baseTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.webgl')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.webgl')} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.audio}
      types={baseTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.audio')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.audio')} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.webrtc}
      types={disabledTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.webrtc')}
        className={mode.isDefault ? '' : dotStyles.warning}
        endContent={<TipIcon.Question color='warning' content={<ConfigDesc tags={unstableTag} desc={t('item.desc.webrtc', { joinArrays: '\n\n' })} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.webgpu}
      types={baseTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.webgpu')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.webgpu')} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.font}
      types={baseTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.fonts')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.fonts')} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.domRect}
      types={baseTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.domRect')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.domRect')} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

    <HookModeContent
      mode={fp.other.serviceWorker}
      types={disabledTypes}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.serviceWorker')}
        className={mode.isDefault ? '' : dotStyles.warning}
        endContent={<TipIcon.Question color='warning' content={<ConfigDesc tags={unstableTag} desc={t('item.desc.serviceWorker', { joinArrays: '\n\n' })} />} />}
      >
        {select}
      </ConfigItemY>}
    </HookModeContent>

  </div> : <Spin indicator={<LoadingOutlined spin />} />
})

export default StrongFpConfigGroup
```

## /src/popup/config/group/weak.tsx

```tsx path="/src/popup/config/group/weak.tsx" 
import { memo, useMemo } from "react"
import { useStorageStore } from "@/popup/stores/storage"
import { HookType } from '@/types/enum'
import { useTranslation } from "react-i18next"
import { Form, Input, Spin } from "antd"
import { LoadingOutlined } from '@ant-design/icons'
import TimeZoneConfigItem from "../special/timezone"
import { ConfigDesc, ConfigItemY, HookModeContent } from "../item"
import TipIcon from "@/components/data/tip-icon"
import { getBrowserInfo } from "@/utils/browser"
import { selectStatusDotStyles as dotStyles } from "../styles"
import { useShallow } from "zustand/shallow"
import ClientHintsConfigItem from "../special/client-hints"
import GPUInfoConfigItem from "../special/gpu-info"

const baseTypes = [HookType.default, HookType.page, HookType.browser, HookType.domain, HookType.global]
const baseValueTypes = [...baseTypes, HookType.value]
const jsTypes = [HookType.default, HookType.browser, HookType.global]
const valueTypes = [HookType.default, HookType.value]

const deprecatedTag = ['deprecated']
const unstableTag = ['unstable']

export const WeakFpConfigGroup = memo(() => {
  const [t] = useTranslation()

  const storage = useStorageStore(useShallow((s) => ({
    config: s.config,
    version: s.version,
  })))
  const fp = storage.config?.fp

  const browserInfo = useMemo(() => getBrowserInfo(navigator.userAgent), [])

  return fp ? <div key={storage.version}>
    <TimeZoneConfigItem />

    {/* languages */}
    <HookModeContent
      mode={fp.navigator.languages}
      types={valueTypes}
      parser={{
        toInput: (v) => v?.join?.(',') ?? '',
        toValue: (v) => {
          const res = v.split(',').map(v => v.trim()).filter((v) => !!v)
          return res.length ? res : navigator.languages as string[]
        },
      }}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.languages')}
        className={mode.isDefault ? '' : dotStyles.success}
        endContent={<TipIcon.Question content={<ConfigDesc desc={t('item.desc.languages', { joinArrays: '\n\n' })} />} />}
      >
        {select}
        {mode.isValue && <>
          <Input
            placeholder={navigator.languages.join(',')}
            value={mode.input}
            onChange={({ target }) => mode.setInput(target.value)}
          />
        </>}
      </ConfigItemY>}
    </HookModeContent>

    {browserInfo.name !== 'firefox' && <ClientHintsConfigItem />}

    {/* gpuInfo */}
    <GPUInfoConfigItem />

    {/* screen size */}
    <HookModeContent
      mode={fp.screen.size}
      types={baseValueTypes}
      parser={{
        toInput: v => v ?? { width: screen.width, height: screen.height },
        toValue(v) {
          v.width ??= screen.width;
          v.height ??= screen.height;
          return v;
        },
      }}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.size')}
        className={mode.isDefault ? '' : dotStyles.warning}
        endContent={<TipIcon.Question color='warning' content={<ConfigDesc tags={unstableTag} desc={t('item.desc.size')} />} />}
      >
        {select}
        {mode.isValue && <>
          <Form.Item label='width'>
            <Input
              value={mode.input.width}
              onChange={({ target }) => mode.setInput({
                ...mode.input,
                width: target.value ? parseInt(target.value) : undefined,
              })}
            />
          </Form.Item>
          <Form.Item label='height'>
            <Input
              value={mode.input.height}
              onChange={({ target }) => mode.setInput({
                ...mode.input,
                height: target.value ? parseInt(target.value) : undefined,
              })}
            />
          </Form.Item>
        </>}
      </ConfigItemY>}
    </HookModeContent>

    {/* screen colorDepth */}
    <HookModeContent
      mode={fp.screen.depth}
      types={valueTypes}
      parser={{
        toInput: v => v ?? { color: screen.colorDepth, pixel: screen.pixelDepth },
        toValue(v) {
          v.color ??= screen.colorDepth;
          v.pixel ??= screen.pixelDepth;
          return v;
        },
      }}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.depth')}
        className={mode.isDefault ? '' : dotStyles.error}
        endContent={<TipIcon.Question color='danger' content={<ConfigDesc tags={deprecatedTag} desc={t('item.desc.depth')} />} />}
      >
        {select}
        {mode.isValue && <>
          <Form.Item label='colorDepth'>
            <Input
              value={mode.input.color}
              onChange={({ target }) => mode.setInput({
                ...mode.input,
                color: target.value ? parseInt(target.value) : undefined,
              })}
            />
          </Form.Item>
          <Form.Item label='pixelDepth'>
            <Input
              value={mode.input.pixel}
              onChange={({ target }) => mode.setInput({
                ...mode.input,
                pixel: target.value ? parseInt(target.value) : undefined,
              })}
            />
          </Form.Item>
        </>}
      </ConfigItemY>}
    </HookModeContent>

    {/* navigator hardwareConcurrency */}
    <HookModeContent
      mode={fp.navigator.hardwareConcurrency}
      types={valueTypes}
      parser={{
        toInput: v => String(v ?? navigator.hardwareConcurrency),
        toValue: (v) => isNaN(parseInt(v)) ? navigator.hardwareConcurrency : parseInt(v),
      }}
      selectClassName={dotStyles.base}
    >{(mode, { select }) =>
      <ConfigItemY
        label={t('item.title.hardwareConcurrency')}
        className={mode.isDefault ? '' : dotStyles.error}
        endContent={<TipIcon.Question color='danger' content={<ConfigDesc tags={deprecatedTag} desc={t('item.desc.hardwareConcurrency')} />} />}
      >
        {select}
        {mode.isValue && <>
          <Input
            placeholder={String(navigator.hardwareConcurrency)}
            value={mode.input}
            onChange={({ target }) => mode.setInput(target.value)}
          />
        </>}
      </ConfigItemY>}
    </HookModeContent>

  </div> : <Spin indicator={<LoadingOutlined spin />} />
})

export default WeakFpConfigGroup
```

## /src/popup/config/index.tsx

```tsx path="/src/popup/config/index.tsx" 
import { Badge, Collapse, Typography, type CollapseProps } from "antd"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import WeakFpConfigGroup from "./group/weak"
import StrongFpConfigGroup from "./group/strong"
import PrefsConfigGroup from "./group/prefs"
import ScriptConfigGroup from "./group/script"
import { usePrefsStore } from "../stores/prefs"
import { useShallow } from "zustand/shallow"
import { useStorageStore } from "../stores/storage"

export const FConfig = () => {
  const [t, i18n] = useTranslation()

  const prefs = usePrefsStore()
  const { config } = useStorageStore(useShallow((state) => ({ config: state.config })))

  const isShowScriptBadge = !config?.action.fastInject;

  const items = useMemo<CollapseProps['items']>(() => {
    return [
      {
        label: <Typography.Text className="font-bold">{t('label.config.strong')}</Typography.Text>,
        children: <StrongFpConfigGroup />,
      },
      {
        label: <Typography.Text className="font-bold">{t('label.config.weak')}</Typography.Text>,
        children: <WeakFpConfigGroup />,
      },
      {
        label: <Badge dot={isShowScriptBadge}>
          <Typography.Text className="font-bold">{t('label.config.script')}</Typography.Text>
        </Badge>,
        children: <ScriptConfigGroup />,
      },
      {
        label: <Typography.Text className="font-bold">{t('label.config.prefs')}</Typography.Text>,
        children: <PrefsConfigGroup />,
      },
    ].map((item, key) => ({
      ...item,
      key,
      className: 'mb-2 last:mb-0 py-1 !border-0 bg-[--ant-color-bg-container] !rounded-md',
    })) as CollapseProps['items']
  }, [i18n.language, prefs.theme, isShowScriptBadge])

  return <Collapse
    size='small'
    className="h-full bg-[--ant-layout-body-bg] overflow-auto no-scrollbar"
    bordered={false}
    accordion
    items={items} />
}

export default FConfig
```

## /src/popup/config/item.tsx

```tsx path="/src/popup/config/item.tsx" 
import { Md } from "@/components/data/markdown"
import { useHookMode, useHookTypeOptions } from "@/utils/hooks"
import { Divider, Select, Tag, type SelectProps } from "antd"
import { tagColors } from "./styles"
import { useTranslation } from "react-i18next"

type ConfigItemXProps = {
  children: React.ReactNode
  label?: React.ReactNode
  startContent?: React.ReactNode
  endContent?: React.ReactNode
}

export const ConfigItemX = ({ children, label, startContent, endContent }: ConfigItemXProps) => {
  return <div className="py-2 px-1 mb-1 last:mb-0 flex gap-2 items-center justify-between rounded duration-200">
    <div className="flex items-center gap-3">
      {label}
      {startContent}
    </div>
    <div className="flex items-center gap-3 [&_.ant-form-item]:mb-0 [&_.ant-form-item-label]:p-0">
      {endContent}
      {children}
    </div>
  </div>
}

type ConfigItemYProps = {
  children: React.ReactNode
  label?: React.ReactNode
  startContent?: React.ReactNode
  endContent?: React.ReactNode
  className?: string
}

export const ConfigItemY = ({ children, label, startContent, endContent, className }: ConfigItemYProps) => {
  return <div className={"mb-1 last:mb-0 flex flex-col gap-2 p-1 rounded duration-200 [&_.ant-form-item]:mb-0 [&_.ant-form-item-label]:p-0 " + (className ?? "")}>
    <div className="flex items-center justify-between">
      <div className="flex items-center gap-3">
        {label}
        {startContent}
      </div>
      {endContent && <div className="flex items-center gap-3">
        {endContent}
      </div>}
    </div>
    {children}
  </div>
}

/**
 * 配置描述
 */
export const ConfigDesc = ({ desc, tags }: {
  tags?: string[]
  desc: string
}) => {
  const [t] = useTranslation()
  return <>
    {tags?.length && <>
      <div className="flex items-center justify-center gap-2">
        {tags.map(tag => <Tag key={tag}
          className="mr-0"
          bordered={false}
          color={(tagColors as any)[tag]}>
          {t('tag.' + tag)}
        </Tag>)}
      </div>
      <Divider rootClassName="my-1" />
    </>}
    <Md>{desc}</Md>
  </>
}

/**
 * @param types 选择器可选类型
 * @param isMakeSelect 是否生成选择器
 */
type HookModeItemProps<V, I,> = {
  mode?: HookMode<V>
  parser?: Parameters<typeof useHookMode<V, I>>[1]
  types?: HookType[]
  isMakeSelect?: boolean
  selectClassName?: string
  children: (
    mode: ReturnType<typeof useHookMode<V, I>>,
    params: {
      options: SelectProps['options']
      select: React.ReactNode
    }
  ) => React.ReactNode
}

export const HookModeContent = <V, I,>({ mode, parser, types, isMakeSelect = true, selectClassName, children }: HookModeItemProps<V, I>) => {
  const hookMode = useHookMode<V, I>(mode, parser)
  const options = useHookTypeOptions(types ?? [])

  const select = isMakeSelect && <Select<HookType>
    className={selectClassName}
    options={options}
    value={hookMode.type}
    onChange={hookMode.setType}
  />

  return <>{children(hookMode, { options, select })}</>
}
```

## /src/popup/config/special/client-hints.tsx

```tsx path="/src/popup/config/special/client-hints.tsx" 
import { useStorageStore } from "@/popup/stores/storage"
import { ConfigDesc, ConfigItemY, HookModeContent } from "../item"
import { selectStatusDotStyles as dotStyles } from "../styles"
import TipIcon from "@/components/data/tip-icon"
import { useTranslation } from "react-i18next"
import { Button, Checkbox, Collapse, CollapseProps, Input, Select, Spin, Tooltip } from "antd"
import { LoadingOutlined, RedoOutlined } from '@ant-design/icons'
import { HookModeHandler, useI18n } from "@/utils/hooks"
import { cn } from "@/utils/style"
import { useEffect, useMemo, useState } from "react"
import { HookType } from '@/types/enum'
import { sharedAsync } from "@/utils/timer"
import { ClientHintsOption, LocalApi } from "@/api/local"

const unstableTag = ['unstable']

const userAgentData: any = (navigator as any).userAgentData;

type OptionType = (string & {}) | HookType
type ModeHandler = HookModeHandler<ClientHintsInfo, UserAgentInput>

type UserAgentInput = {
  ua: ClientHintsInfo['ua']
  uaData: Omit<ClientHintsInfo['uaData'], 'formFactors' | 'versions'> & {
    formFactors: string
    versions: string
  }
}

const versionsToArr = (v?: string) => {
  if (!v) return [];
  const arr = v.split(',').map(v => v.trim()).filter(Boolean)
  return arr.map(v => {
    const [brand, version] = v.split('=').map(v => v.trim())
    return {
      brand,
      version,
    }
  })
}

const versionsToStr = (v?: any[]) => {
  if (!v) return '';
  return v.map(v => `${v.brand}=${v.version ?? ''}`).join(',\n')
}

const formFactorsToArr = (v?: string) => {
  if (!v) return [];
  return v.split(',').map(v => v.trim()).filter(Boolean)
}

const getUaData = async () => {
  if (userAgentData) {
    const data = await userAgentData.getHighEntropyValues([
      "architecture",
      "bitness",
      "formFactor",
      "model",
      "platform",
      "platformVersion",
      'formFactors',
      "uaFullVersion",
      "brands",
      "fullVersionList",
    ])
    return data;
  }
}

const fetchClientHints = sharedAsync(LocalApi.clientHints)

const ClientHintsConfigItem = ({ }: {}) => {
  const [uaInfo, setUaInfo] = useState<ClientHintsInfo>()

  const config = useStorageStore((state) => state.config)
  const fp = config?.fp

  useEffect(() => {
    getUaData().then(data => {
      if (!data) return;
      setUaInfo({
        ua: {
          userAgent: navigator.userAgent,
          appVersion: navigator.appVersion,
          platform: navigator.platform,
        },
        uaData: {
          arch: data.architecture,
          bitness: data.bitness,
          mobile: data.mobile,
          model: data.model,
          platform: data.platform,
          platformVersion: data.platformVersion,
          formFactors: data.formFactors,
          uaFullVersion: data.uaFullVersion,
          versions: data.fullVersionList,
        }
      })
    });
  }, [])

  if (!userAgentData) {
    return <></>
  }

  return (fp && uaInfo) ? <>
    <HookModeContent<ClientHintsInfo, UserAgentInput>
      isMakeSelect={false}
      mode={fp.navigator.clientHints}
      parser={{
        toInput: (value) => {
          const input = value ?? { ...uaInfo }
          return {
            ua: input.ua,
            uaData: {
              ...input.uaData,
              formFactors: input.uaData.formFactors.join(', '),
              versions: versionsToStr(input.uaData.versions),
            }
          }
        },
        toValue: (input) => {
          const formFactors = formFactorsToArr(input.uaData.formFactors);
          const versions = versionsToArr(input.uaData.versions);
          return {
            ua: input.ua,
            uaData: {
              ...input.uaData,
              formFactors: formFactors.length ? formFactors : [...uaInfo.uaData.formFactors],
              versions: versions.length ? versions : [...uaInfo.uaData.versions],
            }
          }
        },
      }}
    >{(mode) => <ModeView mode={mode} defaultValues={uaInfo} />}</HookModeContent>
  </> : <Spin indicator={<LoadingOutlined spin />} />
}

/**
 * 配置模块
 */
const ModeView = ({ mode, defaultValues }: {
  mode: ModeHandler
  defaultValues: ClientHintsInfo
}) => {
  const { t, i18n, asLang } = useI18n()
  const [isOpen, setIsOpen] = useState(false)
  const [localPreset, setLocalPreset] = useState<ClientHintsOption[]>([])

  const presetKey = (mode.value as any)?.key;

  useEffect(() => {
    if (!isOpen || localPreset.length != 0) return;
    fetchClientHints()
      .then((data) => {
        setLocalPreset(data)
      }).catch((e) => {
        console.warn(e)
      })
  }, [isOpen])

  const options = useMemo(() => {
    return [
      {
        label: <span>{t('g.special')}</span>,
        title: 'special',
        options: [
          {
            value: HookType.default,
            label: t('type.' + HookType[HookType.default]),
          },
          {
            value: HookType.value,
            label: t('type.' + HookType[HookType.value]),
          },
        ],
      },
      localPreset && {
        label: <span>{t('g.preset')}</span>,
        title: 'preset',
        options: localPreset.map((v) => ({
          value: v.key,
          label: asLang(v.title),
        })),
      },
    ].filter(v => !!v)
  }, [i18n.language, localPreset])

  const onChange = (v: OptionType) => {
    if (v === HookType.default || v === HookType.value) {
      mode.setValue({ ...defaultValues })
      mode.setType(v);
    } else {
      const preset = localPreset?.find(item => item.key === v);
      if (preset) {
        const { title, ...rest } = preset;
        mode.setValue(rest)
      }
      mode.setType(HookType.value);
    }
  }

  return <ConfigItemY
    label={t('item.title.clientHints')}
    className={cn(!mode.isDefault && dotStyles.warning)}
    endContent={<TipIcon.Question color='warning' content={<ConfigDesc tags={unstableTag} desc={t('item.desc.clientHints', { joinArrays: '\n\n' })} />} />}
  >
    <Select<OptionType>
      open={isOpen}
      onOpenChange={setIsOpen}
      className={dotStyles.base}
      options={options}
      value={presetKey || mode.type}
      onChange={onChange}
    />
    {(mode.isValue && !presetKey) && <ConfigContentView mode={mode} defaultValues={defaultValues} />}
  </ConfigItemY>
}

/**
 * 配置组
 */
const ConfigContentView = ({ mode, defaultValues }: {
  mode: HookModeHandler<ClientHintsInfo, UserAgentInput>
  defaultValues: ClientHintsInfo
}) => {
  const resetUserAgent = () => {
    mode.setValue({
      ua: { ...defaultValues.ua },
      uaData: mode.value.uaData,
    })
  }

  const resetUserAgentData = () => {
    mode.setValue({
      ua: mode.value.ua,
      uaData: { ...defaultValues.uaData },
    })
  }

  const items = useMemo<CollapseProps['items']>(() => [
    {
      key: '1',
      label: 'UserAgent',
      children: <UserAgentView mode={mode} defaultValues={defaultValues} />,
      extra: <ResetButton onPress={resetUserAgent} />,
    },
    {
      key: '2',
      label: 'UserAgentData',
      children: <UserAgentDataView mode={mode} defaultValues={defaultValues} />,
      extra: <ResetButton onPress={resetUserAgentData} />,
    },
  ].map((v) => ({
    ...v,
    className: 'mb-2 last:mb-0 bg-default-100 !border-none !rounded [&_.ant-collapse-content]:bg-default-100',
  })), [mode]);

  return <div>
    <Collapse
      bordered={false}
      accordion
      size="small"
      className="bg-default-50 overflow-auto no-scrollbar border-none"
      items={items}
    />
  </div>
}

const ResetButton = ({ onPress }: {
  onPress?: () => void
}) => {
  const { t } = useTranslation()
  return <Tooltip title={t('g.reset')}>
    <Button
      size="small"
      className="group size-[22px]"
      icon={<RedoOutlined className="group-hover:rotate-180 duration-200" />}
      onClick={(e) => {
        e.stopPropagation()
        onPress?.()
      }}
    />
  </Tooltip>
}

const UserAgentView = ({ mode, defaultValues }: {
  mode: HookModeHandler<ClientHintsInfo, UserAgentInput>
  defaultValues: ClientHintsInfo
}) => {
  const raw = defaultValues.ua;
  const data = mode.input.ua;

  return <div className="grid grid-cols-[auto_1fr] gap-y-1 gap-x-2 items-center [&>label]:text-right">
    <label>userAgent</label>
    <Input
      placeholder={raw.userAgent}
      value={data.userAgent}
      onInput={({ target }: any) => {
        data.userAgent = target.value || raw.userAgent;
        mode.updateValue()
      }}
    />

    <label>appVersion</label>
    <Input
      placeholder={raw.appVersion}
      value={data.appVersion}
      onInput={({ target }: any) => {
        data.appVersion = target.value || raw.appVersion;
        mode.updateValue()
      }}
    />

    <label>platform</label>
    <Input
      placeholder={raw.platform}
      value={data.platform}
      onInput={({ target }: any) => {
        data.platform = target.value || raw.platform;
        mode.updateValue()
      }}
    />
  </div>
}

const UserAgentDataView = ({ mode, defaultValues }: {
  mode: HookModeHandler<ClientHintsInfo, UserAgentInput>
  defaultValues: ClientHintsInfo
}) => {
  const raw = defaultValues.uaData;
  const data = mode.input.uaData;

  return <div className="flex flex-col gap-1">
    <div className="grid grid-cols-[auto_1fr] gap-y-1 gap-x-2 items-center [&>label]:text-right">
      <label>mobile</label>
      <div>
        <Checkbox
          checked={data.mobile}
          onChange={({ target }: any) => {
            data.mobile = target.checked
            mode.updateValue()
          }}
        />
      </div>

      <label>arch</label>
      <Input
        placeholder={raw.arch}
        value={data.arch}
        onInput={({ target }: any) => {
          data.arch = target.value || raw.arch;
          mode.updateValue()
        }}
      />

      <label>bitness</label>
      <Input
        placeholder={raw.bitness}
        value={data.bitness}
        onInput={({ target }: any) => {
          data.bitness = target.value || raw.bitness;
          mode.updateValue()
        }}
      />

      <label>platform</label>
      <Input
        placeholder={raw.platform}
        value={data.platform}
        onInput={({ target }: any) => {
          data.platform = target.value || raw.platform;
          mode.updateValue()
        }}
      />

      <label>platformVersion</label>
      <Input
        placeholder={raw.platformVersion}
        value={data.platformVersion}
        onInput={({ target }: any) => {
          data.platformVersion = target.value || raw.platformVersion;
          mode.updateValue()
        }}
      />

      <label>model</label>
      <Input
        placeholder={raw.model}
        value={data.model}
        onInput={({ target }: any) => {
          data.model = target.value || raw.model;
          mode.updateValue()
        }}
      />

      <label>formFactors</label>
      <Input
        placeholder={raw.formFactors.join(', ')}
        value={data.formFactors}
        onInput={({ target }: any) => {
          data.formFactors = target.value;
          mode.updateValue()
        }}
      />

      <label>uaFullVersion</label>
      <Input
        placeholder={raw.uaFullVersion}
        value={data.uaFullVersion}
        onInput={({ target }: any) => {
          data.uaFullVersion = target.value || raw.uaFullVersion;
          mode.updateValue()
        }}
      />
    </div>

    <div>
      <label className="mb-1">versions</label>
      <Input.TextArea
        size="small"
        autoSize={{
          minRows: 3,
          maxRows: 3,
        }}
        placeholder={versionsToStr(raw.versions)}
        value={data.versions}
        onInput={({ target }: any) => {
          data.versions = target.value;
          mode.updateValue()
        }}
      />
    </div>
  </div>
}

export default ClientHintsConfigItem
```

## /src/popup/config/special/gpu-info.tsx

```tsx path="/src/popup/config/special/gpu-info.tsx" 
import { type GpuInfoOption, LocalApi } from "@/api/local"
import { useStorageStore } from "@/popup/stores/storage"
import { type HookModeHandler, useI18n } from "@/utils/hooks"
import { sharedAsync } from "@/utils/timer"
import { Form, Input, Select, Spin } from "antd"
import { useEffect, useMemo, useState } from "react"
import { LoadingOutlined } from '@ant-design/icons'
import { ConfigDesc, ConfigItemY, HookModeContent } from "../item"
import { selectStatusDotStyles as dotStyles } from "../styles"
import { cn } from "@/utils/style"
import TipIcon from "@/components/data/tip-icon"
import { useTranslation } from "react-i18next"
import { HookType } from '@/types/enum'

type ModeHandler = HookModeHandler<GpuInfo, GpuInfo>
type OptionType = (string & {}) | HookType

const unstableTag = ['unstable']

const fetchGPUInfo = sharedAsync(LocalApi.gpuInfo)

const GPUInfoConfigItem = ({ }: {
}) => {
  const config = useStorageStore((state) => state.config)
  const fp = config?.fp

  const gpuInfo = useMemo<GpuInfo>(() => {
    const cvs = document.createElement('canvas')
    const gl = cvs.getContext("webgl2") ?? cvs.getContext("webgl")
    if (!gl) return {};
    const ex = gl.getExtension('WEBGL_debug_renderer_info')
    if (!ex) return {};
    return {
      vendor: gl.getParameter(ex.UNMASKED_VENDOR_WEBGL),
      renderer: gl.getParameter(ex.UNMASKED_RENDERER_WEBGL),
    }
  }, [])

  return !fp ?
    <Spin indicator={<LoadingOutlined spin />} /> :
    <HookModeContent<GpuInfo, GpuInfo>
      isMakeSelect={false}
      mode={fp.normal.gpuInfo}
      parser={{
        toInput: (value) => value ?? { ...gpuInfo },
        toValue: (input) => input,
      }}
    >{(mode) => <ModeView mode={mode} defaultValues={gpuInfo} />}</HookModeContent>
}

const ModeView = ({ mode, defaultValues }: {
  mode: ModeHandler
  defaultValues: GpuInfo
}) => {
  const { t, i18n, asLang } = useI18n()
  const [isOpen, setIsOpen] = useState(false)
  const [localPreset, setLocalPreset] = useState<GpuInfoOption[]>([])

  const presetKey = (mode.value as any)?.key;

  useEffect(() => {
    if (!isOpen || localPreset.length != 0) return;
    fetchGPUInfo()
      .then((data) => {
        setLocalPreset(data)
      }).catch((e) => {
        console.warn(e)
      })
  }, [isOpen])

  const options = useMemo(() => {
    return [
      {
        label: <span>{t('g.special')}</span>,
        title: 'special',
        options: [
          {
            value: HookType.default,
            label: t('type.' + HookType[HookType.default]),
          },
          {
            value: HookType.value,
            label: t('type.' + HookType[HookType.value]),
          },
        ],
      },
      localPreset && {
        label: <span>{t('g.preset')}</span>,
        title: 'preset',
        options: localPreset.map((v) => ({
          value: v.key,
          label: asLang(v.title),
        })),
      },
    ].filter(v => !!v)
  }, [i18n.language, localPreset])

  const onChange = (v: OptionType) => {
    if (v === HookType.default || v === HookType.value) {
      mode.setValue({ ...defaultValues })
      mode.setType(v);
    } else {
      const preset = localPreset?.find(item => item.key === v);
      if (preset) {
        const { title, ...rest } = preset;
        mode.setValue(rest)
      }
      mode.setType(HookType.value);
    }
  }

  return <ConfigItemY
    label={t('item.title.gpuInfo')}
    className={cn(!mode.isDefault && dotStyles.warning)}
    endContent={<TipIcon.Question color='warning' content={<ConfigDesc tags={unstableTag} desc={t('item.desc.gpuInfo', { joinArrays: '\n\n' })} />} />}
  >
    <Select<OptionType>
      open={isOpen}
      onOpenChange={setIsOpen}
      className={dotStyles.base}
      options={options}
      value={presetKey || mode.type}
      onChange={onChange}
    />
    {(mode.isValue && !presetKey) && <CustomView mode={mode} defaultValues={defaultValues} />}
  </ConfigItemY>
}

const CustomView = ({ mode, defaultValues }: {
  mode: ModeHandler
  defaultValues: GpuInfo
}) => {
  const { t } = useTranslation()
  const input = mode.input;

  return <div>
    <Form.Item label={t('item.label.glVendor')}>
      <Input
        value={mode.input.vendor}
        onChange={({ target }) => {
          input.vendor = target.value || defaultValues.vendor;
          mode.updateValue()
        }}
      />
    </Form.Item>
    <Form.Item label={t('item.label.glRenderer')}>
      <Input
        value={mode.input.renderer}
        onChange={({ target }) => {
          input.renderer = target.value || defaultValues.renderer;
          mode.updateValue()
        }}
      />
    </Form.Item>
  </div>
}

export default GPUInfoConfigItem
```

## /src/popup/config/special/timezone.tsx

```tsx path="/src/popup/config/special/timezone.tsx" 
import { useStorageStore } from "@/popup/stores/storage"
import TipIcon from "@/components/data/tip-icon"
import Markdown from "react-markdown"
import { useEffect, useMemo, useState } from "react"
import { HookType } from '@/types/enum'
import { LoadingOutlined } from '@ant-design/icons'
import { Form, Input, InputNumber, Select, Spin } from "antd"
import { ConfigItemY, HookModeContent } from "../item"
import { selectStatusDotStyles as dotStyles } from "../styles"
import { sharedAsync } from "@/utils/timer"
import { LocalApi, type TimeZoneOption } from "@/api/local"
import { HookModeHandler, useI18n } from "@/utils/hooks"

type ModeHandler = HookModeHandler<TimeZoneInfo, TimeZoneInfo>
type OptionType = (string & {}) | HookType

const getCurrentTimeZoneInfo = (): TimeZoneInfo => {
  const opts = Intl.DateTimeFormat().resolvedOptions()
  return {
    locale: opts.locale,
    zone: opts.timeZone,
    offset: new Date().getTimezoneOffset() / -60
  }
}

const fetchTimezones = sharedAsync(LocalApi.timezone)

export const TimeZoneConfigItem = () => {
  const config = useStorageStore((state) => state.config)
  const fp = config?.fp

  const currentTz = useMemo(() => getCurrentTimeZoneInfo(), [])

  return !fp ? <Spin indicator={<LoadingOutlined spin />} /> : <>
    <HookModeContent<TimeZoneInfo, TimeZoneInfo>
      isMakeSelect={false}
      mode={fp.other.timezone}
      parser={{
        toInput: value => value ?? { ...currentTz },
        toValue: (input) => input,
      }}
    >{(mode) => <ModeView mode={mode} defaultValues={currentTz} />}
    </HookModeContent>
  </>
}

const ModeView = ({ mode, defaultValues }: {
  mode: ModeHandler
  defaultValues: TimeZoneInfo
}) => {
  const { t, i18n, asLang } = useI18n()
  const [isOpen, setIsOpen] = useState(false)
  const [localPreset, setLocalPreset] = useState<TimeZoneOption[]>([])

  const presetKey = (mode.value as any)?.key;
  const input = mode.input;

  useEffect(() => {
    if (!isOpen || localPreset.length != 0) return;
    console.log(111);
    
    fetchTimezones()
      .then(setLocalPreset)
      .catch((e) => {
        console.warn(e)
      })
  }, [isOpen])

  const options = useMemo(() => {
    return [
      {
        label: <span>{t('g.special')}</span>,
        title: 'special',
        options: [
          {
            value: HookType.default,
            label: t('type.' + HookType[HookType.default]),
          },
          {
            value: HookType.value,
            label: t('type.' + HookType[HookType.value]),
          },
        ],
      },
      localPreset && {
        label: <span>{t('g.preset')}</span>,
        title: 'preset',
        options: localPreset.map((tz) => ({
          value: tz.key,
          label: `(${tz.offset >= 0 ? '+' : ''}${tz.offset}) ${asLang(tz.title)}`,
        })),
      },
    ].filter(v => !!v)
  }, [i18n.language, localPreset])

  const onChange = (v: OptionType) => {
    if (v === HookType.default || v === HookType.value) {
      mode.setValue({ ...defaultValues })
      mode.setType(v);
    } else {
      const preset = localPreset?.find(item => item.key === v);
      if (preset) {
        const { title, ...rest } = preset;
        mode.setValue(rest)
      }
      mode.setType(HookType.value);
    }
  }

  return <ConfigItemY
    label={t('item.title.timezone')}
    className={mode.isDefault ? '' : dotStyles.success}
    endContent={<TipIcon.Question content={<Markdown>{t('item.desc.timezone')}</Markdown>} />}
  >
    <Select<OptionType>
      open={isOpen}
      onOpenChange={setIsOpen}
      className={dotStyles.base}
      options={options}
      value={presetKey || mode.type}
      onChange={onChange}
    />
    {(mode.isValue && !presetKey) && <>
      <Form.Item label={t('item.sub.tz.offset')}>
        <InputNumber
          min={-12} max={12}
          placeholder={`${defaultValues.offset}`}
          value={mode.input.offset}
          onChange={(offset) => {
            input.offset = offset ?? defaultValues.offset
            mode.updateValue()
          }}
        />
      </Form.Item>
      <Form.Item label={t('item.sub.tz.locale')}>
        <Input
          placeholder={defaultValues.locale}
          value={mode.input.locale}
          onChange={({ target }) => {
            input.locale = target.value || defaultValues.locale;
            mode.updateValue()
          }}
        />
      </Form.Item>
      <Form.Item label={t('item.sub.tz.zone')}>
        <Input
          placeholder={defaultValues.zone}
          value={mode.input.zone}
          onChange={({ target }) => {
            input.zone = target.value || defaultValues.zone;
            mode.updateValue()
          }}
        />
      </Form.Item>
    </>}
  </ConfigItemY>
}

export default TimeZoneConfigItem
```

## /src/popup/config/styles.ts

```ts path="/src/popup/config/styles.ts" 
export const selectStatusDotStyles = {
  base: "after:block after:absolute after:left-1 after:top-1/2 after:translate-y-[-50%] after:h-2 after:w-1 [&_.ant-select-selection-item]:ml-2 after:rounded-sm after:duration-300 after:bg-[--ant-color-fill-content] hover:after:bg-[--ant-color-fill-content-hover]",
  success: "[&_.ant-select]:after:bg-[--ant-color-success-border-hover] [&_.ant-select]:hover:after:bg-[--ant-color-success-text-hover]",
  error: "[&_.ant-select]:after:bg-[--ant-color-error-border-hover] [&_.ant-select]:hover:after:bg-[--ant-color-error-text-hover]",
  warning: "[&_.ant-select]:after:bg-[--ant-color-warning-border-hover] [&_.ant-select]:hover:after:bg-[--ant-color-warning-text-hover]",
}

export const tagColors = {
  unstable: 'orange',
  deprecated: 'red',
  'unsupport-import': 'cyan',
}
```

## /src/popup/css/github-markdown.css

```css path="/src/popup/css/github-markdown.css" 
.markdown-body {
  --base-size-4: 0.25rem;
  --base-size-8: 0.5rem;
  --base-size-16: 1rem;
  --base-size-24: 1.5rem;
  --base-size-40: 2.5rem;
  --base-text-weight-normal: 400;
  --base-text-weight-medium: 500;
  --base-text-weight-semibold: 600;
  --fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
  --fgColor-accent: Highlight;
}

[data-theme="dark"] .markdown-body {
  /* dark */
  color-scheme: dark;
  --focus-outlineColor: #1f6feb;
  --fgColor-default: #f0f6fc;
  --fgColor-muted: #9198a1;
  --fgColor-accent: #4493f8;
  --fgColor-success: #3fb950;
  --fgColor-attention: #d29922;
  --fgColor-danger: #f85149;
  --fgColor-done: #ab7df8;
  --bgColor-default: #0d1117;
  --bgColor-muted: #151b23;
  --bgColor-neutral-muted: #656c7633;
  --bgColor-attention-muted: #bb800926;
  --borderColor-default: #3d444d;
  --borderColor-muted: #3d444db3;
  --borderColor-neutral-muted: #3d444db3;
  --borderColor-accent-emphasis: #1f6feb;
  --borderColor-success-emphasis: #238636;
  --borderColor-attention-emphasis: #9e6a03;
  --borderColor-danger-emphasis: #da3633;
  --borderColor-done-emphasis: #8957e5;
  --color-prettylights-syntax-comment: #9198a1;
  --color-prettylights-syntax-constant: #79c0ff;
  --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
  --color-prettylights-syntax-entity: #d2a8ff;
  --color-prettylights-syntax-storage-modifier-import: #f0f6fc;
  --color-prettylights-syntax-entity-tag: #7ee787;
  --color-prettylights-syntax-keyword: #ff7b72;
  --color-prettylights-syntax-string: #a5d6ff;
  --color-prettylights-syntax-variable: #ffa657;
  --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
  --color-prettylights-syntax-brackethighlighter-angle: #9198a1;
  --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
  --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
  --color-prettylights-syntax-carriage-return-text: #f0f6fc;
  --color-prettylights-syntax-carriage-return-bg: #b62324;
  --color-prettylights-syntax-string-regexp: #7ee787;
  --color-prettylights-syntax-markup-list: #f2cc60;
  --color-prettylights-syntax-markup-heading: #1f6feb;
  --color-prettylights-syntax-markup-italic: #f0f6fc;
  --color-prettylights-syntax-markup-bold: #f0f6fc;
  --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
  --color-prettylights-syntax-markup-deleted-bg: #67060c;
  --color-prettylights-syntax-markup-inserted-text: #aff5b4;
  --color-prettylights-syntax-markup-inserted-bg: #033a16;
  --color-prettylights-syntax-markup-changed-text: #ffdfb6;
  --color-prettylights-syntax-markup-changed-bg: #5a1e02;
  --color-prettylights-syntax-markup-ignored-text: #f0f6fc;
  --color-prettylights-syntax-markup-ignored-bg: #1158c7;
  --color-prettylights-syntax-meta-diff-range: #d2a8ff;
  --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d;
}

[data-theme="light"] .markdown-body {
  /* light */
  color-scheme: light;
  --focus-outlineColor: #0969da;
  --fgColor-default: #1f2328;
  --fgColor-muted: #59636e;
  --fgColor-accent: #0969da;
  --fgColor-success: #1a7f37;
  --fgColor-attention: #9a6700;
  --fgColor-danger: #d1242f;
  --fgColor-done: #8250df;
  --bgColor-default: #ffffff;
  --bgColor-muted: #f6f8fa;
  --bgColor-neutral-muted: #818b981f;
  --bgColor-attention-muted: #fff8c5;
  --borderColor-default: #d1d9e0;
  --borderColor-muted: #d1d9e0b3;
  --borderColor-neutral-muted: #d1d9e0b3;
  --borderColor-accent-emphasis: #0969da;
  --borderColor-success-emphasis: #1a7f37;
  --borderColor-attention-emphasis: #9a6700;
  --borderColor-danger-emphasis: #cf222e;
  --borderColor-done-emphasis: #8250df;
  --color-prettylights-syntax-comment: #59636e;
  --color-prettylights-syntax-constant: #0550ae;
  --color-prettylights-syntax-constant-other-reference-link: #0a3069;
  --color-prettylights-syntax-entity: #6639ba;
  --color-prettylights-syntax-storage-modifier-import: #1f2328;
  --color-prettylights-syntax-entity-tag: #0550ae;
  --color-prettylights-syntax-keyword: #cf222e;
  --color-prettylights-syntax-string: #0a3069;
  --color-prettylights-syntax-variable: #953800;
  --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
  --color-prettylights-syntax-brackethighlighter-angle: #59636e;
  --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
  --color-prettylights-syntax-invalid-illegal-bg: #82071e;
  --color-prettylights-syntax-carriage-return-text: #f6f8fa;
  --color-prettylights-syntax-carriage-return-bg: #cf222e;
  --color-prettylights-syntax-string-regexp: #116329;
  --color-prettylights-syntax-markup-list: #3b2300;
  --color-prettylights-syntax-markup-heading: #0550ae;
  --color-prettylights-syntax-markup-italic: #1f2328;
  --color-prettylights-syntax-markup-bold: #1f2328;
  --color-prettylights-syntax-markup-deleted-text: #82071e;
  --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
  --color-prettylights-syntax-markup-inserted-text: #116329;
  --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
  --color-prettylights-syntax-markup-changed-text: #953800;
  --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
  --color-prettylights-syntax-markup-ignored-text: #d1d9e0;
  --color-prettylights-syntax-markup-ignored-bg: #0550ae;
  --color-prettylights-syntax-meta-diff-range: #8250df;
  --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98;
}


.markdown-body {
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  margin: 0;
  color: var(--fgColor-default);
  background-color: var(--bgColor-default);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
  font-size: 16px;
  line-height: 1.5;
  word-wrap: break-word;
}

.markdown-body .octicon {
  display: inline-block;
  fill: currentColor;
  vertical-align: text-bottom;
}

.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
  width: 16px;
  height: 16px;
  content: ' ';
  display: inline-block;
  background-color: currentColor;
  -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
  mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}

.markdown-body details,
.markdown-body figcaption,
.markdown-body figure {
  display: block;
}

.markdown-body summary {
  display: list-item;
}

.markdown-body [hidden] {
  display: none !important;
}

.markdown-body a {
  background-color: transparent;
  color: var(--fgColor-accent);
  text-decoration: none;
}

.markdown-body abbr[title] {
  border-bottom: none;
  -webkit-text-decoration: underline dotted;
  text-decoration: underline dotted;
}

.markdown-body b,
.markdown-body strong {
  font-weight: var(--base-text-weight-semibold, 600);
}

.markdown-body dfn {
  font-style: italic;
}

.markdown-body h1 {
  margin: .67em 0;
  font-weight: var(--base-text-weight-semibold, 600);
  padding-bottom: .3em;
  font-size: 2em;
  border-bottom: 1px solid var(--borderColor-muted);
}

.markdown-body mark {
  background-color: var(--bgColor-attention-muted);
  color: var(--fgColor-default);
}

.markdown-body small {
  font-size: 90%;
}

.markdown-body sub,
.markdown-body sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

.markdown-body sub {
  bottom: -0.25em;
}

.markdown-body sup {
  top: -0.5em;
}

.markdown-body img {
  border-style: none;
  max-width: 100%;
  box-sizing: content-box;
}

.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
  font-family: monospace;
  font-size: 1em;
}

.markdown-body figure {
  margin: 1em var(--base-size-40);
}

.markdown-body hr {
  box-sizing: content-box;
  overflow: hidden;
  background: transparent;
  border-bottom: 1px solid var(--borderColor-muted);
  height: .25em;
  padding: 0;
  margin: var(--base-size-24) 0;
  background-color: var(--borderColor-default);
  border: 0;
}

.markdown-body input {
  font: inherit;
  margin: 0;
  overflow: visible;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}

.markdown-body [type=button],
.markdown-body [type=reset],
.markdown-body [type=submit] {
  -webkit-appearance: button;
  appearance: button;
}

.markdown-body [type=checkbox],
.markdown-body [type=radio] {
  box-sizing: border-box;
  padding: 0;
}

.markdown-body [type=number]::-webkit-inner-spin-button,
.markdown-body [type=number]::-webkit-outer-spin-button {
  height: auto;
}

.markdown-body [type=search]::-webkit-search-cancel-button,
.markdown-body [type=search]::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

.markdown-body ::-webkit-input-placeholder {
  color: inherit;
  opacity: .54;
}

.markdown-body ::-webkit-file-upload-button {
  -webkit-appearance: button;
  appearance: button;
  font: inherit;
}

.markdown-body a:hover {
  text-decoration: underline;
}

.markdown-body ::placeholder {
  color: var(--fgColor-muted);
  opacity: 1;
}

.markdown-body hr::before {
  display: table;
  content: "";
}

.markdown-body hr::after {
  display: table;
  clear: both;
  content: "";
}

.markdown-body table {
  border-spacing: 0;
  border-collapse: collapse;
  display: block;
  width: max-content;
  max-width: 100%;
  overflow: auto;
  font-variant: tabular-nums;
}

.markdown-body td,
.markdown-body th {
  padding: 0;
}

.markdown-body details summary {
  cursor: pointer;
}

.markdown-body a:focus,
.markdown-body [role=button]:focus,
.markdown-body input[type=radio]:focus,
.markdown-body input[type=checkbox]:focus {
  outline: 2px solid var(--focus-outlineColor);
  outline-offset: -2px;
  box-shadow: none;
}

.markdown-body a:focus:not(:focus-visible),
.markdown-body [role=button]:focus:not(:focus-visible),
.markdown-body input[type=radio]:focus:not(:focus-visible),
.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
  outline: solid 1px transparent;
}

.markdown-body a:focus-visible,
.markdown-body [role=button]:focus-visible,
.markdown-body input[type=radio]:focus-visible,
.markdown-body input[type=checkbox]:focus-visible {
  outline: 2px solid var(--focus-outlineColor);
  outline-offset: -2px;
  box-shadow: none;
}

.markdown-body a:not([class]):focus,
.markdown-body a:not([class]):focus-visible,
.markdown-body input[type=radio]:focus,
.markdown-body input[type=radio]:focus-visible,
.markdown-body input[type=checkbox]:focus,
.markdown-body input[type=checkbox]:focus-visible {
  outline-offset: 0;
}

.markdown-body kbd {
  display: inline-block;
  padding: var(--base-size-4);
  font: 11px var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
  line-height: 10px;
  color: var(--fgColor-default);
  vertical-align: middle;
  background-color: var(--bgColor-muted);
  border: solid 1px var(--borderColor-neutral-muted);
  border-bottom-color: var(--borderColor-neutral-muted);
  border-radius: 6px;
  box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted);
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
  margin-top: var(--base-size-24);
  margin-bottom: var(--base-size-16);
  font-weight: var(--base-text-weight-semibold, 600);
  line-height: 1.25;
}

.markdown-body h2 {
  font-weight: var(--base-text-weight-semibold, 600);
  padding-bottom: .3em;
  font-size: 1.5em;
  border-bottom: 1px solid var(--borderColor-muted);
}

.markdown-body h3 {
  font-weight: var(--base-text-weight-semibold, 600);
  font-size: 1.25em;
}

.markdown-body h4 {
  font-weight: var(--base-text-weight-semibold, 600);
  font-size: 1em;
}

.markdown-body h5 {
  font-weight: var(--base-text-weight-semibold, 600);
  font-size: .875em;
}

.markdown-body h6 {
  font-weight: var(--base-text-weight-semibold, 600);
  font-size: .85em;
  color: var(--fgColor-muted);
}

.markdown-body p {
  margin-top: 0;
  margin-bottom: 10px;
}

.markdown-body blockquote {
  margin: 0;
  padding: 0 1em;
  color: var(--fgColor-muted);
  border-left: .25em solid var(--borderColor-default);
}

.markdown-body ul,
.markdown-body ol {
  margin-top: 0;
  margin-bottom: 0;
  padding-left: 2em;
}

.markdown-body ol ol,
.markdown-body ul ol {
  list-style-type: lower-roman;
}

.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
  list-style-type: lower-alpha;
}

.markdown-body dd {
  margin-left: 0;
}

.markdown-body tt,
.markdown-body code,
.markdown-body samp {
  font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
  font-size: 12px;
}

.markdown-body pre {
  margin-top: 0;
  margin-bottom: 0;
  font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
  font-size: 12px;
  word-wrap: normal;
}

.markdown-body .octicon {
  display: inline-block;
  overflow: visible !important;
  vertical-align: text-bottom;
  fill: currentColor;
}

.markdown-body input::-webkit-outer-spin-button,
.markdown-body input::-webkit-inner-spin-button {
  margin: 0;
  appearance: none;
}

.markdown-body .mr-2 {
  margin-right: var(--base-size-8, 8px) !important;
}

.markdown-body::before {
  display: table;
  content: "";
}

.markdown-body::after {
  display: table;
  clear: both;
  content: "";
}

.markdown-body>*:first-child {
  margin-top: 0 !important;
}

.markdown-body>*:last-child {
  margin-bottom: 0 !important;
}

.markdown-body a:not([href]) {
  color: inherit;
  text-decoration: none;
}

.markdown-body .absent {
  color: var(--fgColor-danger);
}

.markdown-body .anchor {
  float: left;
  padding-right: var(--base-size-4);
  margin-left: -20px;
  line-height: 1;
}

.markdown-body .anchor:focus {
  outline: none;
}

.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body details {
  margin-top: 0;
  margin-bottom: var(--base-size-16);
}

.markdown-body blockquote>:first-child {
  margin-top: 0;
}

.markdown-body blockquote>:last-child {
  margin-bottom: 0;
}

.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
  color: var(--fgColor-default);
  vertical-align: middle;
  visibility: hidden;
}

.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
  text-decoration: none;
}

.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
  visibility: visible;
}

.markdown-body h1 tt,
.markdown-body h1 code,
.markdown-body h2 tt,
.markdown-body h2 code,
.markdown-body h3 tt,
.markdown-body h3 code,
.markdown-body h4 tt,
.markdown-body h4 code,
.markdown-body h5 tt,
.markdown-body h5 code,
.markdown-body h6 tt,
.markdown-body h6 code {
  padding: 0 .2em;
  font-size: inherit;
}

.markdown-body summary h1,
.markdown-body summary h2,
.markdown-body summary h3,
.markdown-body summary h4,
.markdown-body summary h5,
.markdown-body summary h6 {
  display: inline-block;
}

.markdown-body summary h1 .anchor,
.markdown-body summary h2 .anchor,
.markdown-body summary h3 .anchor,
.markdown-body summary h4 .anchor,
.markdown-body summary h5 .anchor,
.markdown-body summary h6 .anchor {
  margin-left: -40px;
}

.markdown-body summary h1,
.markdown-body summary h2 {
  padding-bottom: 0;
  border-bottom: 0;
}

.markdown-body ul.no-list,
.markdown-body ol.no-list {
  padding: 0;
  list-style-type: none;
}

.markdown-body ol[type="a s"] {
  list-style-type: lower-alpha;
}

.markdown-body ol[type="A s"] {
  list-style-type: upper-alpha;
}

.markdown-body ol[type="i s"] {
  list-style-type: lower-roman;
}

.markdown-body ol[type="I s"] {
  list-style-type: upper-roman;
}

.markdown-body ol[type="1"] {
  list-style-type: decimal;
}

.markdown-body div>ol:not([type]) {
  list-style-type: decimal;
}

.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
  margin-top: 0;
  margin-bottom: 0;
}

.markdown-body li>p {
  margin-top: var(--base-size-16);
}

.markdown-body li+li {
  margin-top: .25em;
}

.markdown-body dl {
  padding: 0;
}

.markdown-body dl dt {
  padding: 0;
  margin-top: var(--base-size-16);
  font-size: 1em;
  font-style: italic;
  font-weight: var(--base-text-weight-semibold, 600);
}

.markdown-body dl dd {
  padding: 0 var(--base-size-16);
  margin-bottom: var(--base-size-16);
}

.markdown-body table th {
  font-weight: var(--base-text-weight-semibold, 600);
}

.markdown-body table th,
.markdown-body table td {
  padding: 6px 13px;
  border: 1px solid var(--borderColor-default);
}

.markdown-body table td>:last-child {
  margin-bottom: 0;
}

.markdown-body table tr {
  background-color: var(--bgColor-default);
  border-top: 1px solid var(--borderColor-muted);
}

.markdown-body table tr:nth-child(2n) {
  background-color: var(--bgColor-muted);
}

.markdown-body table img {
  background-color: transparent;
}

.markdown-body img[align=right] {
  padding-left: 20px;
}

.markdown-body img[align=left] {
  padding-right: 20px;
}

.markdown-body .emoji {
  max-width: none;
  vertical-align: text-top;
  background-color: transparent;
}

.markdown-body span.frame {
  display: block;
  overflow: hidden;
}

.markdown-body span.frame>span {
  display: block;
  float: left;
  width: auto;
  padding: 7px;
  margin: 13px 0 0;
  overflow: hidden;
  border: 1px solid var(--borderColor-default);
}

.markdown-body span.frame span img {
  display: block;
  float: left;
}

.markdown-body span.frame span span {
  display: block;
  padding: 5px 0 0;
  clear: both;
  color: var(--fgColor-default);
}

.markdown-body span.align-center {
  display: block;
  overflow: hidden;
  clear: both;
}

.markdown-body span.align-center>span {
  display: block;
  margin: 13px auto 0;
  overflow: hidden;
  text-align: center;
}

.markdown-body span.align-center span img {
  margin: 0 auto;
  text-align: center;
}

.markdown-body span.align-right {
  display: block;
  overflow: hidden;
  clear: both;
}

.markdown-body span.align-right>span {
  display: block;
  margin: 13px 0 0;
  overflow: hidden;
  text-align: right;
}

.markdown-body span.align-right span img {
  margin: 0;
  text-align: right;
}

.markdown-body span.float-left {
  display: block;
  float: left;
  margin-right: 13px;
  overflow: hidden;
}

.markdown-body span.float-left span {
  margin: 13px 0 0;
}

.markdown-body span.float-right {
  display: block;
  float: right;
  margin-left: 13px;
  overflow: hidden;
}

.markdown-body span.float-right>span {
  display: block;
  margin: 13px auto 0;
  overflow: hidden;
  text-align: right;
}

.markdown-body code,
.markdown-body tt {
  padding: .2em .4em;
  margin: 0;
  font-size: 85%;
  white-space: break-spaces;
  background-color: var(--bgColor-neutral-muted);
  border-radius: 6px;
}

.markdown-body code br,
.markdown-body tt br {
  display: none;
}

.markdown-body del code {
  text-decoration: inherit;
}

.markdown-body samp {
  font-size: 85%;
}

.markdown-body pre code {
  font-size: 100%;
}

.markdown-body pre>code {
  padding: 0;
  margin: 0;
  word-break: normal;
  white-space: pre;
  background: transparent;
  border: 0;
}

.markdown-body .highlight {
  margin-bottom: var(--base-size-16);
}

.markdown-body .highlight pre {
  margin-bottom: 0;
  word-break: normal;
}

.markdown-body .highlight pre,
.markdown-body pre {
  padding: var(--base-size-16);
  overflow: auto;
  font-size: 85%;
  line-height: 1.45;
  color: var(--fgColor-default);
  background-color: var(--bgColor-muted);
  border-radius: 6px;
}

.markdown-body pre code,
.markdown-body pre tt {
  display: inline;
  max-width: auto;
  padding: 0;
  margin: 0;
  overflow: visible;
  line-height: inherit;
  word-wrap: normal;
  background-color: transparent;
  border: 0;
}

.markdown-body .csv-data td,
.markdown-body .csv-data th {
  padding: 5px;
  overflow: hidden;
  font-size: 12px;
  line-height: 1;
  text-align: left;
  white-space: nowrap;
}

.markdown-body .csv-data .blob-num {
  padding: 10px var(--base-size-8) 9px;
  text-align: right;
  background: var(--bgColor-default);
  border: 0;
}

.markdown-body .csv-data tr {
  border-top: 0;
}

.markdown-body .csv-data th {
  font-weight: var(--base-text-weight-semibold, 600);
  background: var(--bgColor-muted);
  border-top: 0;
}

.markdown-body [data-footnote-ref]::before {
  content: "[";
}

.markdown-body [data-footnote-ref]::after {
  content: "]";
}

.markdown-body .footnotes {
  font-size: 12px;
  color: var(--fgColor-muted);
  border-top: 1px solid var(--borderColor-default);
}

.markdown-body .footnotes ol {
  padding-left: var(--base-size-16);
}

.markdown-body .footnotes ol ul {
  display: inline-block;
  padding-left: var(--base-size-16);
  margin-top: var(--base-size-16);
}

.markdown-body .footnotes li {
  position: relative;
}

.markdown-body .footnotes li:target::before {
  position: absolute;
  top: calc(var(--base-size-8)*-1);
  right: calc(var(--base-size-8)*-1);
  bottom: calc(var(--base-size-8)*-1);
  left: calc(var(--base-size-24)*-1);
  pointer-events: none;
  content: "";
  border: 2px solid var(--borderColor-accent-emphasis);
  border-radius: 6px;
}

.markdown-body .footnotes li:target {
  color: var(--fgColor-default);
}

.markdown-body .footnotes .data-footnote-backref g-emoji {
  font-family: monospace;
}

.markdown-body body:has(:modal) {
  padding-right: var(--dialog-scrollgutter) !important;
}

.markdown-body .pl-c {
  color: var(--color-prettylights-syntax-comment);
}

.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
  color: var(--color-prettylights-syntax-constant);
}

.markdown-body .pl-e,
.markdown-body .pl-en {
  color: var(--color-prettylights-syntax-entity);
}

.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
  color: var(--color-prettylights-syntax-storage-modifier-import);
}

.markdown-body .pl-ent {
  color: var(--color-prettylights-syntax-entity-tag);
}

.markdown-body .pl-k {
  color: var(--color-prettylights-syntax-keyword);
}

.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
  color: var(--color-prettylights-syntax-string);
}

.markdown-body .pl-v,
.markdown-body .pl-smw {
  color: var(--color-prettylights-syntax-variable);
}

.markdown-body .pl-bu {
  color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
}

.markdown-body .pl-ii {
  color: var(--color-prettylights-syntax-invalid-illegal-text);
  background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
}

.markdown-body .pl-c2 {
  color: var(--color-prettylights-syntax-carriage-return-text);
  background-color: var(--color-prettylights-syntax-carriage-return-bg);
}

.markdown-body .pl-sr .pl-cce {
  font-weight: bold;
  color: var(--color-prettylights-syntax-string-regexp);
}

.markdown-body .pl-ml {
  color: var(--color-prettylights-syntax-markup-list);
}

.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
  font-weight: bold;
  color: var(--color-prettylights-syntax-markup-heading);
}

.markdown-body .pl-mi {
  font-style: italic;
  color: var(--color-prettylights-syntax-markup-italic);
}

.markdown-body .pl-mb {
  font-weight: bold;
  color: var(--color-prettylights-syntax-markup-bold);
}

.markdown-body .pl-md {
  color: var(--color-prettylights-syntax-markup-deleted-text);
  background-color: var(--color-prettylights-syntax-markup-deleted-bg);
}

.markdown-body .pl-mi1 {
  color: var(--color-prettylights-syntax-markup-inserted-text);
  background-color: var(--color-prettylights-syntax-markup-inserted-bg);
}

.markdown-body .pl-mc {
  color: var(--color-prettylights-syntax-markup-changed-text);
  background-color: var(--color-prettylights-syntax-markup-changed-bg);
}

.markdown-body .pl-mi2 {
  color: var(--color-prettylights-syntax-markup-ignored-text);
  background-color: var(--color-prettylights-syntax-markup-ignored-bg);
}

.markdown-body .pl-mdr {
  font-weight: bold;
  color: var(--color-prettylights-syntax-meta-diff-range);
}

.markdown-body .pl-ba {
  color: var(--color-prettylights-syntax-brackethighlighter-angle);
}

.markdown-body .pl-sg {
  color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
}

.markdown-body .pl-corl {
  text-decoration: underline;
  color: var(--color-prettylights-syntax-constant-other-reference-link);
}

.markdown-body [role=button]:focus:not(:focus-visible),
.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible),
.markdown-body button:focus:not(:focus-visible),
.markdown-body summary:focus:not(:focus-visible),
.markdown-body a:focus:not(:focus-visible) {
  outline: none;
  box-shadow: none;
}

.markdown-body [tabindex="0"]:focus:not(:focus-visible),
.markdown-body details-dialog:focus:not(:focus-visible) {
  outline: none;
}

.markdown-body g-emoji {
  display: inline-block;
  min-width: 1ch;
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  font-size: 1em;
  font-style: normal !important;
  font-weight: var(--base-text-weight-normal, 400);
  line-height: 1;
  vertical-align: -0.075em;
}

.markdown-body g-emoji img {
  width: 1em;
  height: 1em;
}

.markdown-body .task-list-item {
  list-style-type: none;
}

.markdown-body .task-list-item label {
  font-weight: var(--base-text-weight-normal, 400);
}

.markdown-body .task-list-item.enabled label {
  cursor: pointer;
}

.markdown-body .task-list-item+.task-list-item {
  margin-top: var(--base-size-4);
}

.markdown-body .task-list-item .handle {
  display: none;
}

.markdown-body .task-list-item-checkbox {
  margin: 0 .2em .25em -1.4em;
  vertical-align: middle;
}

.markdown-body ul:dir(rtl) .task-list-item-checkbox {
  margin: 0 -1.6em .25em .2em;
}

.markdown-body ol:dir(rtl) .task-list-item-checkbox {
  margin: 0 -1.6em .25em .2em;
}

.markdown-body .contains-task-list:hover .task-list-item-convert-container,
.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
  display: block;
  width: auto;
  height: 24px;
  overflow: visible;
  clip: auto;
}

.markdown-body ::-webkit-calendar-picker-indicator {
  filter: invert(50%);
}

.markdown-body .markdown-alert {
  padding: var(--base-size-8) var(--base-size-16);
  margin-bottom: var(--base-size-16);
  color: inherit;
  border-left: .25em solid var(--borderColor-default);
}

.markdown-body .markdown-alert>:first-child {
  margin-top: 0;
}

.markdown-body .markdown-alert>:last-child {
  margin-bottom: 0;
}

.markdown-body .markdown-alert .markdown-alert-title {
  display: flex;
  font-weight: var(--base-text-weight-medium, 500);
  align-items: center;
  line-height: 1;
}

.markdown-body .markdown-alert.markdown-alert-note {
  border-left-color: var(--borderColor-accent-emphasis);
}

.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title {
  color: var(--fgColor-accent);
}

.markdown-body .markdown-alert.markdown-alert-important {
  border-left-color: var(--borderColor-done-emphasis);
}

.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title {
  color: var(--fgColor-done);
}

.markdown-body .markdown-alert.markdown-alert-warning {
  border-left-color: var(--borderColor-attention-emphasis);
}

.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title {
  color: var(--fgColor-attention);
}

.markdown-body .markdown-alert.markdown-alert-tip {
  border-left-color: var(--borderColor-success-emphasis);
}

.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title {
  color: var(--fgColor-success);
}

.markdown-body .markdown-alert.markdown-alert-caution {
  border-left-color: var(--borderColor-danger-emphasis);
}

.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title {
  color: var(--fgColor-danger);
}

.markdown-body>*:first-child>.heading-element:first-child {
  margin-top: 0 !important;
}

.markdown-body .highlight pre:has(+.zeroclipboard-container) {
  min-height: 52px;
}
```

## /src/popup/index.css

```css path="/src/popup/index.css" 
@import 'css/github-markdown.css';

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  .no-scrollbar {
    /* 隐藏 IE, Edge 和 Firefox 的滚动条 */
    /* IE 和 Edge */
    -ms-overflow-style: none;
    /* Firefox */
    scrollbar-width: none;
  }

  /* 隐藏 Chrome, Safari 和 Opera 的滚动条 */
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }

  .flex-center {
    @apply flex justify-center items-center;
  }
}

:root {
  font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 20px;
  line-height: 24px;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #4a4a4a;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;

  --background: #FFFFFF;
  --foreground: #11181C;

  --default-50: #fafafa;
  --default-100: #f4f4f5;
  --default-200: #e4e4e7;
  --default-300: #d4d4d8;
  --default-400: #a1a1aa;
  --default-500: #71717a;
  --default-600: #52525b;
  --default-700: #3f3f46;
  --default-800: #27272a;
  --default-900: #18181b;

  --warning-50: #fefce8;
  --warning-100: #fdedd3;
  --warning-200: #fbdba7;
  --warning-300: #f9c97c;
  --warning-400: #f7b750;
  --warning-500: #f5a524;
  --warning-600: #c4841d;
  --warning-700: #936316;
  --warning-800: #62420e;
  --warning-900: #312107;

  --danger-50: #fee7ef;
  --danger-100: #fdd0df;
  --danger-200: #faa0bf;
  --danger-300: #f871a0;
  --danger-400: #f54180;
  --danger-500: #f31260;
  --danger-600: #c20e4d;
  --danger-700: #920b3a;
  --danger-800: #610726;
  --danger-900: #310413;
}

.dark {
  --background: #000000;
  --foreground: #ECEDEE;

  --default-50: #18181b;
  --default-100: #27272a;
  --default-200: #3f3f46;
  --default-300: #52525b;
  --default-400: #71717a;
  --default-500: #a1a1aa;
  --default-600: #d4d4d8;
  --default-700: #e4e4e7;
  --default-800: #f4f4f5;
  --default-900: #fafafa;

  --warning-50: #312107;
  --warning-100: #62420e;
  --warning-200: #936316;
  --warning-300: #c4841d;
  --warning-400: #f5a524;
  --warning-500: #f7b750;
  --warning-600: #f9c97c;
  --warning-700: #fbdba7;
  --warning-800: #fdedd3;
  --warning-900: #fefce8;

  --danger-50: #310413;
  --danger-100: #610726;
  --danger-200: #920b3a;
  --danger-300: #c20e4d;
  --danger-400: #f31260;
  --danger-500: #f54180;
  --danger-600: #f871a0;
  --danger-700: #faa0bf;
  --danger-800: #fdd0df;
  --danger-900: #fee7ef;
}

.ant-tabs .ant-tabs-content,
.ant-tabs .ant-tabs-tabpane {
  height: 100%;
}

.markdown-body {
  background-color: transparent !important;
}

.markdown-body {
  --base-size-4: 0.125rem;
  --base-size-8: 0.25rem;
  --base-size-16: 0.5rem;
  --base-size-24: 0.75rem;
  --base-size-40: 1rem;
}

.markdown-body ul,
.markdown-body ol {
  padding-left: 1em;
  margin-left: 0;
}

.markdown-body ul {
  list-style-type: disc;
  list-style-position: outside;
}

.markdown-body ol {
  list-style-type: decimal;
  list-style-position: outside;
}
```

## /src/popup/index.html

```html path="/src/popup/index.html" 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/logo.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/popup/main.tsx"></script>
  </body>
</html>

```

## /src/popup/main.tsx

```tsx path="/src/popup/main.tsx" 
import React, { useEffect } from 'react'
import ReactDOM from 'react-dom/client'
import Application from './App'
import './index.css'
import '@/locales'
import { App, ConfigProvider } from 'antd'
import { usePrefsStore } from './stores/prefs'

function MainApp() {
  const prefs = usePrefsStore()

  useEffect(() => {
    prefs.initLanguage()
  }, [])

  return <ConfigProvider theme={prefs.getThemeConfig()}>
    <App>
      <Application />
    </App>
  </ConfigProvider>
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <MainApp />
  </React.StrictMode>,
)

```

## /src/popup/more/config.tsx

```tsx path="/src/popup/more/config.tsx" 
import { App, Button, Tooltip } from "antd";
import {
  ExportOutlined,
  ImportOutlined,
} from '@ant-design/icons';
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/shallow";
import { useStorageStore } from "../stores/storage";
import VariablePopconfirm from "@/components/feedback/var-popconfirm";

export type MoreConfigViewProps = {
  className?: string
}

export const MoreConfigView = ({ className }: MoreConfigViewProps) => {
  const [t] = useTranslation()
  const { message } = App.useApp()

  const { storage, importStorage } = useStorageStore(useShallow((state) => ({
    storage: state.storage,
    importStorage: state.importStorage
  })))

  const clipboardExport = () => {
    navigator.clipboard.writeText(JSON.stringify(storage, null, 2))
      .then(() => message.success(t('tip.ok.config-export')))
      .catch((err) => message.error(`${t('tip.err.config-export')}: ${err}`))
  }

  const clipboardImport = () => {
    navigator.clipboard.readText()
      .then((text) => {
        try {
          const conf = JSON.parse(text)
          importStorage(conf)
            .then(() => message.success(t('tip.ok.config-import')))
            .catch((err) => message.warning(`${t('tip.err.config-import')}: ${err}`))
        } catch (err) {
          message.error(`${t('tip.err.config-parse')}: ${err}`)
        }
      })
      .catch((err) => {
        message.error(`${t('tip.err.config-parse')}: ${err}`)
      })
  }

  return <section className={className}>
    <Tooltip title={t('tip.label.json')}>
      <Button icon={<ExportOutlined />} onClick={clipboardExport}>{t('label.clipboard-export')}</Button>
    </Tooltip>
    <VariablePopconfirm
      tooltip={t('tip.label.json')}
      title={t('tip.if.config-import')}
      onConfirm={clipboardImport}>
      <Button icon={<ImportOutlined />}>{t('label.clipboard-import')}</Button>
    </VariablePopconfirm>
  </section>
}

export default MoreConfigView
```

## /src/popup/more/index.tsx

```tsx path="/src/popup/more/index.tsx" 
import { Divider } from "antd"
import { useTranslation } from "react-i18next";
import MoreConfigView from "./config";
import SubscribeView from "./subscribe";
import TipIcon from "@/components/data/tip-icon";
import { Md } from "@/components/data/markdown";
import PresetPanel from "./preset";

export type MoreViewProps = {
}

export const MoreView = ({ }: MoreViewProps) => {
  const [t] = useTranslation()

  return <div className="h-full overflow-y-auto no-scrollbar flex flex-col gap-2">
    <div className="p-2 bg-[--ant-color-bg-container] rounded-lg">
      <h3 className="mb-3 text-center text-sm">{t('label.config-file')}</h3>
      <MoreConfigView className="flex flex-wrap justify-center items-center gap-2" />
    </div>
    <div className="p-2 bg-[--ant-color-bg-container] rounded-lg">
      <div className="mb-3 flex justify-center items-center gap-2">
        <h3 className="text-sm">{t('label.subscribe')}</h3>
        <TipIcon.Question content={<Md>{t('desc.subscribe', { joinArrays: '\n\n' })}</Md>} />
      </div>
      <SubscribeView />
    </div>
    <div className="p-2 bg-[--ant-color-bg-container] rounded-lg">
      <h3 className="mb-2 text-center text-sm">{t('label.preset-panel')}</h3>
      <Divider className="m-0" />
      <PresetPanel />
    </div>
  </div>
}

export default MoreView
```

## /src/popup/more/permission.tsx

```tsx path="/src/popup/more/permission.tsx" 
import { Badge, Button, Popconfirm, Popover } from "antd";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";

type PermissionStatus = 'on' | 'off' | 'unknown'

const isAuthorized = async (permission: string): Promise<PermissionStatus> => {
  try {
    return await chrome.permissions.contains({ permissions: [permission] }) ? 'on' : 'off'
  } catch (err) {
    return 'unknown'
  }
}

export const PermissionView = ({ className }: {
  className?: string
}) => {
  const [t] = useTranslation()
  const [perms, setPerms] = useState<Record<string, PermissionStatus>>({})
  const [version, setVersion] = useState(0)

  useEffect(() => {
    const opts = chrome.runtime.getManifest().optional_permissions
    if (!opts?.length) return;

    const promises: Promise<PermissionStatus>[] = []
    for (const opt of opts) {
      promises.push(isAuthorized(opt))
    }

    Promise.all(promises).then((data) => {
      const res: Record<string, PermissionStatus> = {}
      for (let i = 0; i < opts.length; ++i) {
        res[opts[i]] = data[i]
      }
      setPerms(res)
    })
  }, [version])

  const permKeys = Object.keys(perms)

  return <section className={className}>
    {permKeys.length ?
      permKeys.map((perm) => <PermissionItem
        key={perm}
        name={perm}
        status={perms[perm]}
        onChange={() => setVersion(version + 1)}
      />) :
      <span className="text-[--ant-color-text-tertiary]">{t('tip.label.no-auth-required')}</span>}
  </section>
}

const PermissionItem = ({
  name, status, onChange
}: {
  name: string
  status: PermissionStatus
  onChange?: (status: PermissionStatus) => void
}) => {
  const [t] = useTranslation()
  const [clicked, setClicked] = useState(false);
  const [hovered, setHovered] = useState(false);

  const switchAuthStatus = useCallback(async () => {
    if (status === 'on') {
      if (await chrome.permissions.remove({ permissions: [name] })) {
        onChange?.('off')
      }
    } else if (status === 'off') {
      if (await chrome.permissions.request({ permissions: [name] })) {
        onChange?.('on')
      }
    }
  }, [])

  const btn = <Button
    className="font-mono font-bold"
    type={status === 'on' ? 'primary' : 'default'}
    danger={status === 'unknown'}
    onClick={status === 'off' ? switchAuthStatus : undefined}
    children={name} />

  let content: JSX.Element;
  switch (status) {
    case 'on':
      content = <Popconfirm
        title={t('tip.if.remove-permission')}
        okText={t('g.confirm')}
        cancelText={t('g.cancel')}
        open={clicked}
        onOpenChange={(open: boolean) => {
          setHovered(false);
          setClicked(open);
        }}
        onConfirm={switchAuthStatus}>{btn}
      </Popconfirm>
      break;
    case "off":
      content = <Badge dot>{btn}</Badge>
      break;
    case "unknown":
      content = btn
      break;
  }

  return <Popover
    content={<div className="flex flex-col justify-center items-center">
      <div>{t('perm.status.' + status)}</div>
      <Markdown>{t('perm.desc.' + name)}</Markdown>
    </div>}
    trigger="hover"
    open={hovered}
    onOpenChange={(open: boolean) => {
      setHovered(open);
      setClicked(false);
    }}>
    {content}
  </Popover>
}

export default PermissionView
```

## /src/popup/more/preset.tsx

```tsx path="/src/popup/more/preset.tsx" 
import { GithubApi } from "@/api/github"
import { sharedAsync } from "@/utils/timer"
import { App, Button, Divider, Popconfirm, Spin, Tag } from "antd"
import { useEffect, useMemo, useState } from "react"
import { LoadingOutlined } from '@ant-design/icons'
import { useShallow } from "zustand/shallow"
import { useStorageStore } from "../stores/storage"
import { useI18n } from "@/utils/hooks"

type PresetItem = {
  file: string
  name: I18nString
  description: I18nString
}

const getLocalPresets = sharedAsync(async () => {
  const url = chrome.runtime.getURL('presets/index.json')
  return await fetch(url).then(res => {
    if (!res.ok) throw new Error(res.statusText);
    return res.json()
  })
})

const getOnlinePresets = sharedAsync(async () => {
  return GithubApi.getJson('/example/presets/index.json')
})

/**
 * 预设内容面板
 */
const PresetPanel = ({ }: {
}) => {
  const { t, asLang } = useI18n()
  const { message } = App.useApp()

  const [localPresets, setLocalPresets] = useState<PresetItem[]>()
  const [onlinePresets, setOnlinePresets] = useState<PresetItem[]>()
  const [selectedKey, setSelectedKey] = useState<string>()

  const { importStorage } = useStorageStore(useShallow((state) => ({
    importStorage: state.importStorage
  })))

  useEffect(() => {
    getLocalPresets()
      .then(v => setLocalPresets(Array.isArray(v.presets) ? v.presets : []))
      .catch(() => setLocalPresets([]));
    getOnlinePresets()
      .then(v => setOnlinePresets(Array.isArray(v.presets) ? v.presets : []))
      .catch(() => setOnlinePresets([]));
  }, [])

  const onApply = (preset?: DeepPartial<LocalStorage>) => {
    if (!preset) return;
    importStorage(preset)
      .then(() => message.success(t('tip.ok.config-import')))
      .catch((err) => message.warning(`${t('tip.err.config-import')}: ${err}`))
  }

  const infoProps = useMemo(() => {
    if (!selectedKey) return;
    if (selectedKey.startsWith('local:')) {
      const key = selectedKey.split(':')[1]
      const item = localPresets?.find(v => v.file === key)
      return item && { item, mode: 'local' };
    }
    if (selectedKey.startsWith('online:')) {
      const key = selectedKey.split(':')[1]
      const item = onlinePresets?.find(v => v.file === key)
      return item && { item, mode: 'online' };
    }
  }, [selectedKey])

  return localPresets == null && onlinePresets == null ?
    <div className="w-full h-48 flex-center bg-[--ant-color-bg-container] rounded">
      <Spin indicator={<LoadingOutlined spin />} />
    </div> :
    <div className="w-full h-48 flex-center bg-[--ant-color-bg-container] rounded">
      <div className="w-full h-full p-2 flex animate-fadeIn">
        <div className="w-24 shrink-0 flex flex-col overflow-auto no-scrollbar">
          {localPresets && localPresets.length !== 0 && <div className="mb-1 last:mb-0">
            <div className="mb-1 font-bold">{t('g.local')}</div>
            {localPresets?.map(v => <PresetItemView
              key={v.file}
              title={asLang(v.name)}
              className={`local:${v.file}` === selectedKey ? 'bg-[--ant-color-primary-bg]' : undefined}
              onSelect={() => setSelectedKey(`local:${v.file}`)}
            />)}
          </div>}
          {onlinePresets && onlinePresets.length !== 0 && <div className="mb-1 last:mb-0">
            <div className="mb-1 font-bold">{t('g.online')}</div>
            {onlinePresets?.map(v => <PresetItemView
              key={v.file}
              title={asLang(v.name)}
              className={`online:${v.file}` === selectedKey ? 'bg-[--ant-color-primary-bg]' : undefined}
              onSelect={() => setSelectedKey(`online:${v.file}`)}
            />)}
          </div>}
        </div>
        <Divider className="h-full mx-2" type='vertical' />
        <div className="grow rounded">
          {infoProps == null ?
            <div className="h-full flex-center">{t('tip.label.select-content')}</div> :
            <PresetInfoView {...infoProps} onApply={onApply} />}
        </div>
      </div>
    </div>
}

/**
 * 预设项
 */
const PresetItemView = ({ title, onSelect, className }: {
  title?: string
  onSelect?: () => void
  className?: string
}) => {
  return <div
    className={"p-1 truncate rounded hover:bg-[--ant-color-primary-bg-hover] cursor-pointer duration-300 " + (className ?? '')}
    onClick={onSelect}
  >
    <span>{title ?? 'null'}</span>
  </div>
}

/**
 * 预设信息
 */
const PresetInfoView = ({ mode, item, onApply }: {
  mode: 'local' | 'online' | (string & {})
  item: PresetItem
  onApply?: (preset: DeepPartial<LocalStorage>) => void
}) => {
  const { t, asLang } = useI18n()

  const [isLoading, setIsLoading] = useState(false)
  const [preset, setPreset] = useState<DeepPartial<LocalStorage>>()

  const manifest = useMemo(() => chrome.runtime.getManifest(), [])

  useEffect(() => {
    if (!item?.file) return;
    const url = mode === 'local' ?
      chrome.runtime.getURL(`presets/${item.file}`) :
      mode === 'online' ?
        GithubApi.asRawUrl(`example/presets/${item.file}`) :
        null;

    if (!url) {
      setPreset(undefined)
      return;
    };

    setIsLoading(true)
    setPreset(undefined)
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json()
      })
      .then(setPreset)
      .catch(() => setPreset(undefined))
      .finally(() => setIsLoading(false))
  }, [mode, item])

  return isLoading ?
    <div className="h-full flex-center"><Spin indicator={<LoadingOutlined spin />} /></div> :
    preset == null ?
      <div className="h-full flex-center">{'tip.label.unsupport-content'}</div> :
      <div className="h-full flex flex-col gap-2 overflow-y-auto">
        <div>
          <Tag>{t('g.' + mode)}</Tag>
          {preset.version && <Tag>v{preset.version}</Tag>}
          {preset.version != null && manifest.version !== preset.version && <Tag color='error'>{t('tip.label.version-mismatch')}</Tag>}
          {!preset.config && preset.whitelist?.length && <Tag color='cyan'>{t('tag.only-whitelist')}</Tag>}
        </div>
        <p className="font-bold">{asLang(item.name) ?? 'null'}</p>
        <p>{asLang(item.description) ?? 'null'}</p>
        <Popconfirm
          title={t('g.apply')}
          description={t('tip.if.config-import')}
          onConfirm={() => onApply?.(preset)}
          okText={t('g.confirm')}
          showCancel={false}
        >
          <Button>{t('g.apply')}</Button>
        </Popconfirm>
      </div>
}

export default PresetPanel
```

## /src/popup/more/subscribe.tsx

```tsx path="/src/popup/more/subscribe.tsx" 
import { useTranslation } from "react-i18next"
import { useStorageStore } from "../stores/storage"
import { App, Button, Input, Tooltip } from "antd"
import { ApiOutlined, CheckOutlined } from '@ant-design/icons';
import { useEffect, useState } from "react";
import { useShallow } from "zustand/shallow";
import { sendToBackground } from "@/utils/message";

type SubscribeViewProps = {
  className?: string
}

export const SubscribeView = ({ className }: SubscribeViewProps) => {
  const [t] = useTranslation()
  const [input, setInput] = useState<string>('')
  const [saveable, setSaveable] = useState(true)
  const { message } = App.useApp()

  const { config, syncLoadStorage } = useStorageStore(useShallow((state) => ({
    config: state.config,
    syncLoadStorage: state.syncLoadStorage,
  })))

  useEffect(() => {
    const url = config?.subscribe.url
    if (input !== url) {
      setInput(url ?? '')
    }
  }, [config])

  /**
   * 测试订阅目标
   */
  const testTarget = () => {
    if (!config) return;
    let url = input
    if (!url.includes("://")) url = chrome.runtime.getURL(url);
    console.log(url);
    fetch(url)
      .then(v => v.json())
      .then(() => message.success(t('tip.ok.subscribe-test')))
      .catch(e => message.error(`${t('tip.err.subscribe-test')}: ${e}`))
  }

  const subscribeTarget = () => {
    if (!config) return;
    let url: string | undefined = undefined
    if (config.subscribe.url !== input.trim()) {
      url = input.trim()
      config.subscribe.url = url
    }
    sendToBackground({
      type: 'config.subscribe',
      url,
    }).then((v: LocalStorage | void) => {
      if (v) {
        syncLoadStorage(v)
        message.success(t('tip.ok.subscribe'))
      } else {
        message.error(t('tip.err.subscribe'))
      }
    })
  }

  return <section className={className}>
    <div className="flex justify-center items-center gap-1">
      <Input
        placeholder="config.json"
        value={input}
        onChange={({ target }) => setInput(target.value)}
      />
      <div>
        <Tooltip title={t('tip.label.subscribe-save')}>
          <Button icon={<CheckOutlined />} disabled={!saveable} onClick={() => {
            setTimeout(() => setSaveable(true), 1000);
            setSaveable(false);
            subscribeTarget()
          }} />
        </Tooltip>
      </div>
      <div>
        <Tooltip title={t('tip.label.subscribe-test')} >
          <Button
            icon={<ApiOutlined />}
            disabled={input.trim() === ''}
            onClick={testTarget}
          />
        </Tooltip>
      </div>
    </div>
  </section>
}

export default SubscribeView
```

## /src/popup/record/fp-item.tsx

```tsx path="/src/popup/record/fp-item.tsx" 
import { Tag } from "antd"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"

const RootKeys = ['weak', 'strong', 'other']

const KeyColors: Record<string, string> = {
  strong: 'orange',
  weak: 'green',
  other: 'blue'
}

type FpNoticeItemProps = {
  title: string
  count?: number
  isRoot?: boolean
}

export const FpNoticeItem = ({ title, count, isRoot }: FpNoticeItemProps) => {
  const [t, i18n] = useTranslation()

  const label = useMemo(() => {
    if (isRoot && RootKeys.includes(title)) {
      return t('label.fp-notice.' + title)
    }
    return title
  }, [isRoot, title, i18n.language])

  return isRoot ?
    <div className="py-[2px] flex justify-between items-center font-bold">
      <div>{label}</div>
      <Tag
        className="mr-0 text-[14px]"
        color={KeyColors[title]}>
        x {count}
      </Tag>
    </div>
    :
    <div className="flex justify-between items-center font-bold">
      <div>{label}</div>
      <div className="mr-2">x {count}</div>
    </div>
}
```

## /src/popup/record/iframe-item.tsx

```tsx path="/src/popup/record/iframe-item.tsx" 
export type IframeNoticeItemProps = {
  src: string
  count?: number
}

export const IframeNoticeItem = ({ src, count }: IframeNoticeItemProps) => {  
  return <div className="py-1 px-2 flex justify-between items-center font-bold rounded hover:bg-[--ant-color-primary-bg-hover] border-b border-b-[--ant-color-border] last:border-none">
    <div>{src}</div>
    <div className="">x {count}</div>
  </div>
}

export default IframeNoticeItem
```

## /src/popup/record/iframe.tsx

```tsx path="/src/popup/record/iframe.tsx" 
import { useTranslation } from "react-i18next"
import IframeNoticeItem from "./iframe-item"

export type IframeNoticePanelProps = {
  notice?: Record<string, number>
}

export const IframeNoticePanel = ({ notice }: IframeNoticePanelProps) => {
  const [t] = useTranslation()
  const keys = notice ? Object.keys(notice) : []

  return <div className='p-1 h-full flex flex-col bg-[--ant-color-bg-container]'>
    {keys.length === 0 ?
      <div className='grow flex justify-center items-center'>{t('tip.label.no-fp-notice')}</div> :
      <div>{keys.map((v) => <IframeNoticeItem key={v} src={v} count={notice?.[v]} />)}</div>}
  </div>
}

export default IframeNoticePanel
```

## /src/popup/whitelist/item.tsx

```tsx path="/src/popup/whitelist/item.tsx" 
import { Highlight } from '@/components/data/highlight'
import { Button, theme } from "antd"
import {
  DeleteOutlined,
} from '@ant-design/icons';
import { useState } from 'react';

export type WhitelistItemProps = {
  item: string
  filterValue: string
  onDelete?: (item: string) => void
}

export const WhitelistItem = ({ item, filterValue, onDelete }: WhitelistItemProps) => {
  const [active, setActive] = useState(false)
  const { token } = theme.useToken()

  return <section className='px-2 py-1 h-6 flex justify-between items-center border-solid border-b'
    style={{
      backgroundColor: active ? token.colorFillQuaternary : undefined,
      borderColor: token.colorBorderSecondary,
    }}
    onMouseEnter={() => setActive(true)}
    onMouseLeave={() => setActive(false)}>
    <Highlight className='font-mono' text={item} keyword={filterValue} ignoreCase />
    {active && <Button className='float-end' type='text' size='small' danger icon={<DeleteOutlined />} onClick={() => onDelete?.(item)} />}
  </section>
}

export default WhitelistItem
```

## /src/types/enum.ts

```ts path="/src/types/enum.ts" 
export enum HookType {
  default = 0,  // 系统值
  value = 1,  // 自定义值
  page = 2,  // 每个标签页随机
  browser = 3,  // 每次启动浏览器随机
  domain = 4,  // 根据域名随机
  global = 5,  // 根据全局种子随机
  enabled = 6,  // 启用
  disabled = 7,  // 禁用
}
```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!