├── .gitignore
├── package.json
├── src
├── script
│ ├── JuejinCookie
│ │ ├── .gitignore
│ │ ├── .env.example
│ │ ├── config
│ │ │ └── index.js
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── config
│ │ │ │ └── index.js
│ │ │ ├── browser.js
│ │ │ ├── utils
│ │ │ │ └── index.js
│ │ │ └── cookie.js
│ │ ├── README.md
│ │ ├── index.js
│ │ └── pnpm-lock.yaml
│ ├── JuejinDailyPublish
│ │ ├── README.md
│ │ └── index.js
│ ├── JuejinDraw
│ │ ├── README.md
│ │ └── index.js
│ └── uToolsQuickCommands
│ │ └── README.md
└── userscript
│ ├── lock-screen
│ ├── README.md
│ └── index.user.js
│ ├── BJTU-Schedule-ics-csvGenerator
│ ├── image.png
│ ├── README.md
│ └── generator.user.js
│ ├── github-copilot-enhanced
│ ├── docs
│ │ └── example-search.webp
│ ├── README.zh-CN.md
│ └── README.md
│ ├── GithubRepoInfo
│ ├── README.md
│ └── userscript.user.js
│ ├── deepseek-snapshot
│ ├── README.md
│ └── index.user.js
│ ├── BiliAutoJudgement
│ ├── README.md
│ └── bili-auto-judgement.user.js
│ ├── snapshot-everything
│ ├── README.md
│ ├── index.user.js
│ └── overlay.vue
│ ├── BJTUCaptchaAutofill
│ ├── README.md
│ └── userscript.user.js
│ ├── tap-to-tab
│ ├── README.md
│ └── index.user.js
│ ├── McmodQuickSearch
│ ├── README.md
│ └── userscript.user.js
│ ├── WeChatArticleEX
│ ├── README.md
│ └── userscript.user.js
│ ├── highlight-translation
│ ├── README.zh-CN.md
│ └── README.md
│ ├── duplicate-tab-cleaner
│ ├── README.zh-CN.md
│ ├── README.md
│ └── index.user.js
│ ├── BJTUCourse
│ ├── README.md
│ └── userscript.user.js
│ └── websocket-hook
│ └── index.user.js
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | dist
4 | .DS_Store
5 | *.zip
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | data.txt
--------------------------------------------------------------------------------
/src/script/JuejinDailyPublish/README.md:
--------------------------------------------------------------------------------
1 | # 掘金每日发沸点
2 |
3 | 使用前需加入任一个`圈子`
--------------------------------------------------------------------------------
/src/userscript/lock-screen/README.md:
--------------------------------------------------------------------------------
1 | # Lock Screen
2 |
3 | A userscript that adds a password lock to specified websites.
--------------------------------------------------------------------------------
/src/userscript/BJTU-Schedule-ics-csvGenerator/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZiuChen/userscript/HEAD/src/userscript/BJTU-Schedule-ics-csvGenerator/image.png
--------------------------------------------------------------------------------
/src/script/JuejinCookie/.env.example:
--------------------------------------------------------------------------------
1 | # 用户信息 [{"mobile": "手机号", "password": "密码"}]
2 | # *注意: 所有key与value应当以双引号包裹
3 | USERS_INFO='[{"mobile":"18888888888","password":"**********"}]'
--------------------------------------------------------------------------------
/src/userscript/github-copilot-enhanced/docs/example-search.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZiuChen/userscript/HEAD/src/userscript/github-copilot-enhanced/docs/example-search.webp
--------------------------------------------------------------------------------
/src/script/JuejinDraw/README.md:
--------------------------------------------------------------------------------
1 | # 掘金梭哈抽奖脚本
2 |
3 | 修改自[iDerekLi](https://github.com/iDerekLi),调整了`lottery`函数内的实现
4 |
5 | - 默认十连抽梭哈
6 | - 默认延时3秒钟
7 |
8 | 使用方式:进入掘金,复制脚本并粘贴到网页控制台即可使用
--------------------------------------------------------------------------------
/src/userscript/GithubRepoInfo/README.md:
--------------------------------------------------------------------------------
1 | # GithubRepoInfo
2 |
3 | - 在浏览器上下文菜单中添加 Github 仓库信息显示功能
4 | - 纯净 在用户操作前不会向任何服务器发送请求
5 |
6 | 可能需要自行添加 Github Token:[https://github.com/settings/tokens](https://github.com/settings/tokens)
--------------------------------------------------------------------------------
/src/userscript/deepseek-snapshot/README.md:
--------------------------------------------------------------------------------
1 | # deepseek-snapshot
2 |
3 | Take snapshot for chat history on Deepseek.
4 |
5 | Added two menu command to context menu:
6 |
7 | - `Copy to clipboard`
8 | - `Download to file`
9 |
--------------------------------------------------------------------------------
/src/userscript/BiliAutoJudgement/README.md:
--------------------------------------------------------------------------------
1 | ## 全自动风纪委
2 |
3 | > 入口: [风纪委员会](https://www.bilibili.com/judgement/index)
4 | >
5 | > 原理: DOM 操作模拟点击,不会被检测异常
6 |
7 | ### 注意事项
8 |
9 | - 进入第一个案件后,按下 `Enter` 确认后即可自动开始审理案件
10 |
--------------------------------------------------------------------------------
/src/script/uToolsQuickCommands/README.md:
--------------------------------------------------------------------------------
1 | # uTools快捷命令合集
2 |
3 | 自己写的一些快捷命令脚本
4 |
5 | - 快速打开Typora中最近的文件或文件夹
6 | - 在线Photoshop
7 | - 获取当前日期时间
8 | - 生成随机字符串
9 | - 一言随机语录
10 | - 在Windows Terminal中打开
11 | - 获取当前WIFI密码
12 | - 推送消息到微信
13 | - 下载选中的链接
14 | - 新建文本文档
--------------------------------------------------------------------------------
/src/script/JuejinCookie/config/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | module.exports = {
4 | juejin: {
5 | login: 'https://juejin.cn/login',
6 | loginApi: '/passport/web/user/login',
7 | verifyApi: 'verify.snssdk.com/captcha/verify'
8 | },
9 | users: process.env.USERS_INFO
10 | }
11 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "juejin-cookie",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {},
7 | "author": "",
8 | "license": "MIT",
9 | "dependencies": {
10 | "dotenv": "^16.0.1",
11 | "puppeteer": "^15.5.0"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/src/config/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | module.exports = {
4 | juejin: {
5 | login: 'https://juejin.cn/login',
6 | loginApi: '/passport/web/user/login',
7 | verifyApi: 'verify.snssdk.com/captcha/verify'
8 | },
9 | users: process.env.USERS_INFO
10 | }
11 |
--------------------------------------------------------------------------------
/src/userscript/snapshot-everything/README.md:
--------------------------------------------------------------------------------
1 | # snapshot-everything
2 |
3 | Take snapshot on any site for any DOM.
4 |
5 | Added command to context menu:
6 |
7 | - `Take Snapshot`
8 |
9 | After the command is clicked, you need to select the DOM element you want to capture, and click the mouse to automatically capture and save it to the local.
10 |
--------------------------------------------------------------------------------
/src/userscript/BJTUCaptchaAutofill/README.md:
--------------------------------------------------------------------------------
1 | # 识别并自动填写MIS验证码
2 |
3 | 识别并自动填写北京交通大学MIS入口的验证码,使用前需自行申请讯飞印刷文字识别API
4 |
5 | [申请链接: xfyun.cn](https://www.xfyun.cn)
6 |
7 | - 使用前请先申请: 讯飞开放平台 印刷文字识别接口 申请链接 www.xfyun.cn 每天可免费识别 500 次
8 | - 控制台/我的应用/文字识别/印刷文字识别 获取到 APPID 和 APIKey
9 | - 接口名称为 **印刷文字识别接口** 不要申请错
10 | - 所有信息存储在本地不会上传
11 | - 只支持简单算式如单次/多次`+ - - /`的计算,
12 | - 每次识别都会在控制台输出日志,要修正识别结果可通过调整`reviseString()`函数中规则实现
13 | - 要支持其他站点,可以修改 `imgSelector` 与 `inputSelector`
14 |
--------------------------------------------------------------------------------
/src/userscript/tap-to-tab/README.md:
--------------------------------------------------------------------------------
1 | # Tap to Tab
2 |
3 | A userscript that supports opening new tabs in the background when double-clicking links.
4 |
5 | Since the extension [Tap to Tab](https://chromewebstore.google.com/detail/tap-to-tab/enhajhmncplakageabmopgpodkdgcodd) lacks updates, making it unusable on newer versions of Chrome:
6 |
7 | > This extension doesn't follow Chrome extensions best practices, so it may soon stop being supported.
8 |
9 | Therefore, we forked it as a Tampermonkey script for independent maintenance.
10 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/README.md:
--------------------------------------------------------------------------------
1 | # 批量获取掘金Cookie
2 |
3 | **脚本仅供学习交流使用, 请勿实际应用, 由使用此脚本导致的任何后果与开发者无关**
4 |
5 | ## 使用步骤
6 |
7 | ### 安装依赖
8 |
9 | 项目使用pnpm管理依赖,在终端内输入:
10 |
11 | ```sh
12 | # 进入项目根目录
13 | cd .\src\JuejinCookie\
14 | # 执行安装依赖
15 | pnpm install
16 | ```
17 |
18 | ### 配置参数
19 |
20 | 将根目录下`.env.example`按要求修改后,将其重命名为`.env`
21 |
22 | > 云函数部署下,可以直接在环境变量中定义`USERS_INFO`
23 |
24 | ### 运行脚本
25 |
26 | 运行`index.js`
27 |
28 | ```sh
29 | node index.js
30 | ```
31 |
32 | 脚本运行完毕, 将通过**控制台日志**输出Cookie数组
33 |
--------------------------------------------------------------------------------
/src/userscript/McmodQuickSearch/README.md:
--------------------------------------------------------------------------------
1 | ## Mcmod模组下载脚本
2 |
3 | ### ✨ 脚本简介
4 |
5 | 本脚本主要针对部分未在MCMOD.CN提供下载链接或在CurseForge下载速度过慢的模组开发。
6 |
7 | ### 🔨 使用脚本
8 |
9 | 确保脚本在脚本管理器中处于启用状态,在浏览器中打开模组介绍页面,以 Carpet 模组为例:[Carpet](https://www.mcmod.cn/class/2361.html)。
10 |
11 | 此模组MCMOD.CN未提供下载链接,启用脚本前:
12 |
13 | 
14 |
15 | 启用脚本后:
16 |
17 | 
18 |
19 | 在的模组界面中的“相关链接”中出现一个按钮,点击按钮后,通过读取模组页面英文名,跳转到现有[CurseForge模组检索网站](https://files.xmdhs.top/curseforge)检索,简单方便。
20 |
--------------------------------------------------------------------------------
/src/userscript/WeChatArticleEX/README.md:
--------------------------------------------------------------------------------
1 | ## 微信推送浏览功能拓展
2 |
3 | [脚本主页](https://github.com/ZiuChen/userscript)
4 |
5 | ### ✨ 主要功能:
6 |
7 | * **悬停快速预览/复制/跳转文章封面图**
8 | * **正文显示摘要,支持单击文本复制**
9 | * 设置默认展示时间格式
10 | * 隐藏了右侧悬浮的引导关注栏
11 | * 在网络中搜索当前文章
12 | * `ESC`临时显示/隐藏菜单
13 | * 所有功能可独立配置是否启用
14 |
15 | ### 🔨 效果演示:
16 |
17 | 确保脚本在脚本管理器中处于启用状态,在浏览器中随意打开一篇推送,例:[春节期间小程序及小游戏审核调整通知](https://mp.weixin.qq.com/s/SuBSIEg13yhOYQoZFiL_iQ)。
18 |
19 | *注:文章链接须以mp.weixin.qq.com开头*
20 |
21 | **使用脚本前:**
22 |
23 | 
24 |
25 | **使用脚本后:**
26 |
27 | 
28 |
--------------------------------------------------------------------------------
/src/userscript/highlight-translation/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | [中文](./README.zh-CN.md)
2 |
3 | ----
4 |
5 | # Highlight Translation
6 |
7 | 划词翻译用户脚本,支持多种翻译服务商,支持翻译源与用户设置跨设备同步。
8 |
9 | 1. 支持通过辅助键(双击 Ctrl/Cmd)唤起翻译浮窗,浮窗支持在固定位置展示
10 | 2. 内置了一系列翻译服务
11 | 1. 谷歌翻译(无需配置)
12 | 2. Chrome 内置翻译(无需配置)
13 | 3. GLM(无需配置)
14 | 4. 百度翻译
15 | 5. OpenAI 兼容的自定义端点
16 | 3. 翻译源与设置跟随 Tampermonkey 同步:可通过 Google Drive / Dropbox / OneDrive / WebDAV 同步
17 | 4. 支持自动检测语言,支持设置主要与次要语言
18 | 5. 界面简洁易用,适配深色模式
19 |
20 | ## 安装
21 | 1. 安装 [Tampermonkey](https://www.tampermonkey.net/) 浏览器扩展
22 | 2. [点此安装划词翻译脚本](https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/highlight-translation/index.user.js)
23 | 3. 安装完成后,在页面选中文本,双击 Ctrl/Cmd 即可唤起翻译浮窗
24 |
--------------------------------------------------------------------------------
/src/userscript/BJTU-Schedule-ics-csvGenerator/README.md:
--------------------------------------------------------------------------------
1 | ## 北交大iCalender课表生成
2 |
3 | ### 脚本功能:
4 |
5 | * 在Win/Android/IOS日历中登录Outlook邮箱导入课表,导入一次,多端同步!
6 | * 一键将课表导出为.ics文件,支持多端同步,与各系统原生日历应用完美结合。
7 | * 一键将课表导出为.csv文件,可以在Excel中编辑。
8 | * 一键将课表导出为.json文件,可以导入第三方软件。
9 |
10 | ### 使用效果:
11 |
12 | 
13 |
14 | ### 使用方法:
15 |
16 | [北交大iCalender课表生成使用指南](https://www.cnblogs.com/ziuc/articles/15152630.html)
17 |
18 | ### 注意事项:
19 |
20 | 每学期第一个教学周的第一个周一需要自行指定,点击 "校历" 按钮查看后,到脚本源代码修改变量`defaultStartMonday`的值即可。
21 |
22 | 考虑到疫情影响,课堂在**思源西楼** 与 **逸夫教学楼**的第二节课上下课时间已经做了修改,具体时间以开学时教务处公布时间为准,有能力的可以到源代码中自行修改变量`delayClassroom`的值,当其值为空时,所有日程都将为标准上下课时间。
23 |
24 | 目前仅对本科生 "选课课表" 与 "本学期课表" 两个网页做了支持,研究生选课界面尚未测试。
25 |
26 | ### 后续开发计划:
27 |
28 | 支持自定义日程描述部分格式;
29 |
30 | 使用过程中如遇到任何问题,欢迎到[脚本主页](https://greasyfork.org/zh-CN/scripts/430918)或[Github](https://github.com/ZiuChen/userscript)提交建议!
--------------------------------------------------------------------------------
/src/script/JuejinCookie/index.js:
--------------------------------------------------------------------------------
1 | const config = require('./src/config')
2 | const { getCookie } = require('./src/cookie')
3 | const { delay, randInt } = require('./src/utils')
4 | const fs = require('fs')
5 |
6 | const main = async () => {
7 | const users = []
8 | const cookies = {}
9 | // 从环境变量中读取`USERS_INFO`
10 | try {
11 | users.push(...JSON.parse(config.users))
12 | } catch (error) {
13 | console.log('请检查`USERS_INFO`格式是否正确')
14 | return
15 | }
16 | console.log(`共有${users.length}个账号`)
17 | // 遍历获取Cookie并存入`cookies`
18 | for (const [index, user] of users.entries()) {
19 | console.log(`第${index + 1}个账号`)
20 | await delay(randInt(0, 10) * 1000) // 10s 随机延迟
21 | const cookie = await getCookie(user.mobile, user.password)
22 | cookies[`COOKIE_${index + 1}`] = cookie
23 | }
24 | console.log(cookies)
25 | await fs.writeFileSync('data.txt', JSON.stringify(cookies), (err) => {
26 | if (err) console.log(err)
27 | })
28 | }
29 |
30 | main()
31 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/src/browser.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer')
2 | const getBrowser = async (options) => {
3 | if (!global._browser) {
4 | try {
5 | const browser = await puppeteer.launch(
6 | Object.assign({}, options, {
7 | headless: true,
8 | ignoreDefaultArgs: ['--disable-extensions'],
9 | args: [
10 | '--no-sandbox',
11 | '--disable-setuid-sandbox',
12 | '--use-gl=egl',
13 | '--disable-web-security',
14 | '--disable-features=IsolateOrigins,site-per-process'
15 | ]
16 | })
17 | )
18 | global._browser = browser
19 | } catch (error) {
20 | console.log(error.message || 'puppeteer启动失败')
21 | }
22 | }
23 |
24 | return global._browser || null
25 | }
26 |
27 | const closeBrowser = async () => {
28 | if (global._browser) {
29 | await global._browser.close()
30 | global._browser = null
31 | }
32 | }
33 | module.exports = {
34 | getBrowser,
35 | closeBrowser
36 | }
37 |
--------------------------------------------------------------------------------
/src/userscript/duplicate-tab-cleaner/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | [中文](./README.zh-CN.md)
2 |
3 | ----
4 |
5 | # Duplicate Tab Cleaner
6 |
7 | ## 功能摘要
8 | - 按规范化后的 URL 检测重复标签页。
9 | - 通过 GM tab-store 协调:选出保留页(winner),向重复页写入“请自关”请求。
10 | - 重复页尝试 window.close();若失败则显示可交互的提示栏,让用户手动关闭或保留。
11 | - 支持简体中文与英文界面,自动根据浏览器语言选择。
12 |
13 | ## 安装与使用
14 | - 在 Tampermonkey 中新建脚本,粘贴并保存本 userscript(默认匹配所有站点,可修改 @match)。
15 | - 在任意页面点击 Tampermonkey 脚本菜单中的“清理重复标签(dupe-cleaner)”(或英文 “Clean duplicate tabs (dupe-cleaner)”),即可触发一次去重。
16 | - 也可在控制台运行 window.__dupeCleaner.triggerCleanup() 触发。
17 |
18 | ## 要求(权限 / API)
19 | - 需要 GM.getTab / GM.saveTab / GM.getTabs / GM.setValue / GM.addValueChangeListener 等 API(脚本已兼容 GM_* 旧 API)。
20 | - 所有参与去重的标签页需安装并运行此脚本,否则不会出现在 GM 的 tab-store 中。
21 |
22 | ## 配置与扩展
23 | - 可在脚本内 CONFIG 区域修改:URL 规范化选项、自动跳转目标、提示延时等。
24 | - 可修改 @match 限制为仅在指定域名或路径启用。
25 | - 可扩展多语言词条、去重策略(保留最早/当前/用户标记)等。
26 |
27 | ## 限制与注意事项
28 | - 普通 userscript 无法强制关闭任意标签页(浏览器通常限制 window.close()),因此脚本采用“请求自关 + 提示用户”策略作为退路。
29 | - 若要完全自动强制关闭标签,需要开发浏览器扩展并申请 tabs 权限(不是 userscript 能做到的)。
30 | - 若遇到某些标签未被识别为已注册,请确保脚本在这些页面确实运行(没有被 CSP 或脚本管理器阻止)。
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 ZiuChen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/userscript/highlight-translation/README.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | [中文](./README.zh-CN.md)
2 |
3 | ----
4 |
5 | # Highlight Translation
6 |
7 | A text selection translation user script that supports multiple translation service providers and enables cross-device synchronization of translation sources and user settings.
8 |
9 | 1. Supports invoking the translation popup via a modifier key (double-click Ctrl/Cmd), with the option to display the popup in a fixed position.
10 | 2. Includes a variety of built-in translation services:
11 | 1. Google Translate (no configuration required)
12 | 2. Chrome's built-in translation (no configuration required)
13 | 3. GLM (no configuration required)
14 | 4. Baidu Translate
15 | 5. Custom endpoints compatible with OpenAI
16 | 3. Translation sources and settings sync with Tampermonkey: supports synchronization via Google Drive / Dropbox / OneDrive / WebDAV.
17 | 4. Supports automatic language detection and allows setting primary and secondary languages.
18 | 5. Features a clean and user-friendly interface with dark mode support.
19 |
20 | ## Installation
21 | 1. Install the [Tampermonkey](https://www.tampermonkey.net/) browser extension.
22 | 2. [Click here to install the text selection translation script](https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/highlight-translation/index.user.js).
23 | 3. After installation, select text on any webpage and double-click Ctrl/Cmd to bring up the translation popup.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Useful UserScript Set
2 |
3 | ## Userscript
4 |
5 | | Name | Description |
6 | |------|-------------|
7 | | [BiliAutoJudgement](./src/userscript/BiliAutoJudgement/README.md) | B站风纪委自动评分工具 |
8 | | [BJTU-Schedule-ics-csvGenerator](./src/userscript/BJTU-Schedule-ics-csvGenerator/README.md) | 北京交通大学生成 ICS/CSV 格式的课程表 |
9 | | [BJTUCaptchaAutofill](./src/userscript/BJTUCaptchaAutofill/README.md) | 北京交通大学MIS验证码自动识别&填写 |
10 | | [BJTUCourse](./src/userscript/BJTUCourse/README.md) | 北京交通大学抢课工具 |
11 | | [deepseek-snapshot](./src/userscript/deepseek-snapshot/README.md) | DeepSeek 网页快照工具 |
12 | | [duplicate-tab-cleaner](./src/userscript/duplicate-tab-cleaner/README.md) | 重复标签页清理工具 |
13 | | [Github Copilot Enhanced](./src/userscript/github-copilot-enhanced/README.md) | GitHub Copilot 增强 |
14 | | [GithubRepoInfo](./src/userscript/GithubRepoInfo/README.md) | GitHub 仓库信息展示工具 |
15 | | [highlight-translation](./src/userscript/highlight-translation/README.md) | 划词翻译 |
16 | | [lock-screen](./src/userscript/lock-screen/README.md) | 网页锁屏工具 |
17 | | [McmodQuickSearch](./src/userscript/McmodQuickSearch/README.md) | Minecraft 模组快速搜索工具 |
18 | | [snapshot-everything](./src/userscript/snapshot-everything/README.md) | 网页内容快照工具 |
19 | | [tap-to-tab](./src/userscript/tap-to-tab/README.md) | 点击转标签页工具 |
20 | | [websocket-hook](./src/userscript/websocket-hook/README.md) | 劫持网页的 WebSocket 消息并将其记录到全局数组,便于调试或分析 |
21 | | [WeChatArticleEX](./src/userscript/WeChatArticleEX/README.md) | 微信公众号文章增强工具 |
22 |
--------------------------------------------------------------------------------
/src/userscript/duplicate-tab-cleaner/README.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | [中文](./README.zh-CN.md)
2 |
3 | ----
4 |
5 | # Duplicate Tab Cleaner
6 |
7 | ## Summary
8 | - Detects duplicate tabs by normalized URL and coordinates which tab to keep.
9 | - Uses GM tab-store (GM.getTab / GM.saveTab / GM.getTabs) to share tab info across pages.
10 | - Sends per-tab close requests; recipients try window.close() and show a banner fallback if programmatic close fails.
11 | - UI in Simplified Chinese or English, auto-detected from browser language.
12 |
13 | ## Install & Use
14 | - Create a new Tampermonkey script and paste the userscript; save. Default @match is all sites—adjust as needed.
15 | - Trigger cleaning from the Tampermonkey menu item "Clean duplicate tabs (dupe-cleaner)" (or the localized label).
16 | - Or run window.__dupeCleaner.triggerCleanup() in the page console to trigger on-demand.
17 |
18 | ## Requirements (permissions / APIs)
19 | - Requires GM.getTab, GM.saveTab, GM.getTabs, GM.setValue, GM.addValueChangeListener (script includes legacy GM_* fallbacks).
20 | - All tabs participating in dedupe must have this userscript installed and running.
21 |
22 | ## Configuration & Extensibility
23 | - Edit the CONFIG block inside the script to change URL normalization, banner behavior, redirect target, etc.
24 | - Restrict @match to specific domains or paths if desired.
25 | - Extend localization (M dictionary) or change keep/selection policy (first, visible, user-marked).
26 |
27 | ## Limitations & Notes
28 | - Userscripts cannot reliably force-close arbitrary tabs due to browser restrictions on window.close(); this script requests tabs to close themselves and provides UI fallbacks.
29 | - Fully automatic forced close requires a browser extension with tabs permission (beyond userscript capabilities).
30 | - If some duplicates are not detected, ensure the script actually runs on those pages (CSP, script manager settings may block it).
31 |
--------------------------------------------------------------------------------
/src/userscript/McmodQuickSearch/userscript.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Mcmod快捷搜索
3 | // @namespace https://github.com/ZiuChen/userscript
4 | // @version 0.2.2
5 | // @description 一键跳转到CurseForge,全速下载模组
6 | // @author ZiuChen
7 | // @match *://www.mcmod.cn/class/*
8 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/McmodQuickSearch/userscript.user.js
9 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/McmodQuickSearch/userscript.user.js
10 | // @require http://libs.baidu.com/jquery/2.0.0/jquery.min.js
11 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
12 | // @grant none
13 | // @license MIT
14 | // ==/UserScript==
15 |
16 | $(".common-link-icon-frame-style-3:last").append(
17 | '
Search'
18 | )
19 | let ModName = $(".class-title h4").text() // 优先搜索小标题名
20 | if (ModName === "") {
21 | ModName = $(".class-title h3").text() // 小标题为空,则搜索主标题名
22 | }
23 | let trueurl = "https://files.xmdhs.top/curseforge/s?q=" + ModName // 注意,此处url须用//baidu.com这样的形式
24 | $("#AdditionIcon").attr("href", trueurl)
25 | let Xsite = $("#AdditionIcon").offset().left - 49
26 | let Ysite = $("#AdditionIcon").offset().top - 42
27 | let elePosition = "transform: translate(" + Xsite + "px, " + Ysite + "px);"
28 | $("#AdditionIcon").mouseenter(function () {
29 | $("body").append(
30 | ''
33 | )
34 | })
35 | $("#AdditionIcon").mouseleave(function () {
36 | $("#littleTip").remove()
37 | })
38 |
--------------------------------------------------------------------------------
/src/userscript/github-copilot-enhanced/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | [中文](./README.zh-CN.md)
2 |
3 | ----
4 |
5 | # Github Copilot Enhanced
6 |
7 | 一个用于增强 GitHub Copilot (Web) 的 UserScript。
8 |
9 | > [!WARNING]
10 | > 请注意,此脚本并非官方产品,使用时请确保了解其工作原理及潜在风险。
11 | >
12 | > 所有数据仅存储在本地浏览器中,不会上传到任何服务器。
13 |
14 | 
15 |
16 | ## 功能特性
17 |
18 | ### 🗄️ 长期存储
19 | 默认情况下,GitHub Copilot (Web) 仅保留 30 天内的聊天记录。
20 | 本脚本通过将聊天记录保存到浏览器的 IndexedDB 中,实现了更长时间的聊天记录保存功能。
21 |
22 | ## 🔍 搜索功能
23 | - **快捷键打开**: 按下 `Ctrl/Cmd+K` 快速打开搜索面板
24 | - **实时搜索**: 输入关键词时,实时显示匹配的聊天记录,支持模糊搜索
25 | - **键盘导航**: 使用方向键浏览搜索结果,按 Enter 键打开对应聊天记录
26 |
27 | ### ⚙️ 设置面板
28 | - **快捷访问**: 在搜索面板中点击齿轮图标打开设置
29 | - **配置管理**: 统一管理脚本的各项配置选项
30 | - **WebDAV 同步**: 配置 WebDAV 服务器实现跨浏览器/设备的数据同步
31 |
32 | ### 🔄 自动同步
33 | - **定期同步**: 每 30 分钟自动同步一次所有聊天记录
34 | - **首次加载**: 页面加载时立即执行一次同步
35 | - **后台运行**: 同步过程在后台进行,不影响正常使用
36 |
37 | ### ☁️ WebDAV 同步
38 | - **配置简单**: 在设置面板中输入 WebDAV 服务器地址、用户名和密码
39 | - **手动同步**: 支持手动触发上传和下载操作
40 | - **智能合并**: 下载时自动合并本地和远程数据,保留最新版本
41 | - **跨设备**: 通过 WebDAV 实现多设备间的数据同步
42 |
43 | ### 🔌 无缝集成
44 | - **请求拦截**: 自动拦截 GitHub Copilot 的 API 请求
45 | - **数据合并**: 将本地历史记录与服务器数据合并后返回
46 | - **透明操作**: 对用户完全透明,无需额外操作
47 |
48 | ## 使用方法
49 |
50 | ### 安装
51 | 1. 安装 Tampermonkey 或其他 UserScript 管理器
52 | 2. [点此安装](https://github.com/ZiuChen/userscript/raw/refs/heads/main/src/userscript/github-copilot-enhanced/index.user.js) 该脚本
53 |
54 | ### 配置 WebDAV 同步(可选)
55 | 1. 按下 `Ctrl/Cmd+K` 打开搜索面板
56 | 2. 点击右上角的齿轮图标打开设置面板
57 | 3. 在 "WebDAV 同步" 部分填写:
58 | - 服务器地址:如 `https://example.com/webdav`
59 | - 用户名和密码
60 | 4. 点击"保存配置"
61 | 5. 点击"测试连接"验证配置是否正确
62 | 6. 使用"上传数据"和"下载数据"按钮进行手动同步
63 |
64 | **注意事项**:
65 | - 首次上传前,如果远程已有文件,会自动先下载并合并
66 | - 下载时会自动合并本地和远程数据,保留时间戳较新的版本
67 | - 支持主流的 WebDAV 服务,如坚果云、Nextcloud 等
68 |
69 | ## 注意事项
70 |
71 | 1. **认证问题**: 项目使用浏览器已有的认证信息进行 API 请求,确保已登录 GitHub 并拥有访问 Copilot Pro 及以上的权限
72 | 2. **存储限制**: IndexedDB 受浏览器存储配额限制,过多数据可能导致存储失败
73 | 3. **隐私安全**:
74 | - 所有数据仅存储在本地浏览器的 IndexedDB 中
75 | - WebDAV 同步功能完全可选,不配置则不会上传任何数据
76 | - WebDAV 账号密码使用 GM_setValue 加密存储在本地
77 | 4. **兼容性**: 需要支持 IndexedDB 的现代浏览器
78 | 5. **同步频率**: 默认 30 分钟同步一次 GitHub Copilot 数据,可根据需要调整
79 | 6. **WebDAV 安全**:
80 | - 建议使用 HTTPS 协议的 WebDAV 服务器
81 | - 定期检查远程备份文件的完整性
82 | - 注意 WebDAV 服务器的存储空间限制
83 |
--------------------------------------------------------------------------------
/src/userscript/BiliAutoJudgement/bili-auto-judgement.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name 全自动风纪委
3 | // @description 进入评价界面自动开始提交风纪委评价
4 | // @namespace http://tampermonkey.net
5 | // @supportURL https://github.com/ZiuChen/userscript
6 | // @version 0.7.1
7 | // @author ZiuChen
8 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/BiliAutoJudgement/bili-auto-judgement.user.js
9 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/BiliAutoJudgement/bili-auto-judgement.user.js
10 | // @match https://www.bilibili.com/judgement*
11 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
12 | // @grant none
13 | // @license MIT
14 | // ==/UserScript==
15 |
16 | /* 原理:DOM操作模拟点击,不会被检测异常 */
17 | /* 使用方法:进入第一个案件后按下Enter,后续无需人工干预自动完成所有风纪委任务 */
18 | /* 仅供学习交流使用,安装后请于24小时内删除 */
19 |
20 | const CONFIG = {
21 | 是否合适: 0, // 0合适 好 | 1一般 普通 | 2不合适 差 | 3无法判断 无法判断
22 | 会观看吗: 1, // 0会观看 | 1不会观看
23 | 是否匿名: true // true匿名 | false非匿名
24 | }
25 |
26 | const randInt = (min, max) => {
27 | return parseInt(Math.random() * (max - min + 1) + min, 10)
28 | }
29 |
30 | const sleep = async (timeout) => {
31 | timeout += randInt(500, 1000) // 随机延迟
32 | return new Promise((resolve) => {
33 | setTimeout(() => {
34 | resolve()
35 | }, timeout)
36 | })
37 | }
38 |
39 | const btnClick = (selector, index = 0) => document.querySelectorAll(selector)[index]?.click()
40 |
41 | const callBackFn = async () => {
42 | // TODO: 添加跳出递归的条件
43 | return await sleep(2000)
44 | .then(() => {
45 | btnClick('.vote-btns .btn-group button', [CONFIG['是否合适']])
46 | btnClick('.vote-btns .will-you-watch button', [CONFIG['会观看吗']])
47 | CONFIG['是否匿名'] && btnClick('.vote-anonymous .v-check-box__label')
48 | })
49 | .then(() => btnClick('.vote-submit button')) // 提交
50 | .then(() => sleep(5000))
51 | .then(() => btnClick('.vote-result button')) // 跳转下一题
52 | .then(() => callBackFn())
53 | .catch((err) => confirm(`[全自动风纪委] 出错: ${err}, 点击确定刷新`) && location.reload())
54 | }
55 |
56 | document.addEventListener(
57 | 'keydown',
58 | // press `Enter` && in task page
59 | (e) =>
60 | e.keyCode === 13 &&
61 | location.href.indexOf('index') === -1 &&
62 | callBackFn() &&
63 | alert('点击确定启动脚本')
64 | )
65 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/src/utils/index.js:
--------------------------------------------------------------------------------
1 | // 计算登录滑块的移动距离
2 | const calcGapPosition = async (page, src) => {
3 | const distance = await page.evaluate(async (src) => {
4 | const findMost2 = (arr) => {
5 | // 定义一个空对象存储数据
6 | var h = {}
7 | // 假设频率高的数出现次数初始为0
8 | var maxNum = 0
9 | // 清空频率高的数
10 | var maxEle = null
11 | // 对数组从左往右遍历
12 | for (var i = 0; i < arr.length; i++) {
13 | // 对数组的每一个数据进行存储存于a
14 | var a = arr[i]
15 | // 判断存储的数字是否为默认值, 存在 对属性的值进行+1,不存在 往对象中重新添加属性赋值为1;
16 | h[a] === undefined ? (h[a] = 1) : h[a]++
17 | // 判断存入的数据是否大于初始的频率高数,如果满足将存入高频数和出现次数的覆盖前一次的。
18 | if (h[a] > maxNum) {
19 | maxEle = a
20 | maxNum = h[a]
21 | }
22 | }
23 | return {
24 | times: maxNum,
25 | value: maxEle
26 | }
27 | }
28 | const imageLoaded = (document, src) => {
29 | return new Promise((r, j) => {
30 | const image = document.createElement('img')
31 | image.setAttribute('src', src)
32 | image.crossOrigin = 'Anonymous'
33 | image.addEventListener('load', () => {
34 | r(image)
35 | })
36 | image.addEventListener('error', () => {
37 | j()
38 | })
39 | })
40 | }
41 | const image = await imageLoaded(document, src).catch((err) => {
42 | console.log('图片加载失败')
43 | })
44 | const canvas = document.createElement('canvas')
45 | canvas.width = image.width
46 | canvas.height = image.height
47 | const ctx = canvas.getContext('2d')
48 | ctx.drawImage(image, 0, 0)
49 | const imageInfo = ctx.getImageData(0, 0, image.width, image.height)
50 | const imageData = imageInfo.data
51 | const gap = 1
52 | let positionX = []
53 | for (var h = 0; h < image.height; h += gap) {
54 | for (var w = 0; w < image.width; w += gap) {
55 | var position = (image.width * h + w) * 4
56 | var r = imageData[position],
57 | g = imageData[position + 1],
58 | b = imageData[position + 2]
59 | let num = 0
60 | if (r >= 252) num += 1
61 | if (g >= 252) num += 1
62 | if (b >= 252) num += 1
63 | if (num >= 2) {
64 | positionX.push(w)
65 | }
66 | }
67 | }
68 | return findMost2(positionX)
69 | }, src)
70 | return (distance.value * 340) / 552
71 | }
72 |
73 | const delay = (timeout) => {
74 | return new Promise((res) => {
75 | setTimeout(() => {
76 | res()
77 | }, timeout)
78 | })
79 | }
80 |
81 | const randInt = (min, max = 10) => {
82 | return parseInt(Math.random() * (max - min + 1) + min, 10)
83 | }
84 |
85 | module.exports = { calcGapPosition, delay, randInt }
86 |
--------------------------------------------------------------------------------
/src/userscript/BJTUCourse/README.md:
--------------------------------------------------------------------------------
1 | # BJTU 抢课脚本
2 |
3 | **免责声明: 此脚本仅供学习交流使用, 请于下载后的 24 小时内删除, 禁止用于实际选课, 由使用此脚本造成的任何后果与开发者无关**
4 |
5 | 1. 为浏览器安装`Tampermonkey`拓展
6 | 2. 安装`userscript.user.js`
7 | 3. 在选课界面/脚本代码页面写入匹配规则
8 | 4. 点击`开始抢课`, 等待抢课成功通知
9 |
10 | **按下 F12 打开控制台查看抢课日志**
11 |
12 | ## 🔰 使用说明
13 |
14 | ### 🎨 匹配模式:
15 |
16 | - 简单匹配 (`config.simpleMatchPatterns`)
17 | - 支持匹配课程名, 支持泛选
18 | - 支持直接在选课界面设置, 课程名之间用英文逗号`,`分隔
19 | - 高级匹配 (`config.advanceMatchPatterns`)
20 | - 支持匹配表格内的部分字段
21 | - 不支持在选课界面内设置, 请手动编辑脚本中的变量值
22 |
23 | ### 📫 结果推送
24 |
25 | 当抢课成功时, 脚本将通过以下途径通知:
26 |
27 | - 系统通知弹出提示
28 | - 浏览器控制台相应日志输出
29 | - 支持将结果通过[PushPlus](http://www.pushplus.plus/)推送到微信
30 | - 使用前需申请`token`并填入`config.pushPlusToken`
31 |
32 | ## 🎯 匹配模式示例
33 |
34 | ### 📌 简单匹配示例
35 |
36 | 要匹配`人类的生育与健康 02`与`癌症知多少`
37 |
38 | ```js
39 | simpleMatchPatterns: ['人类的生育与健康 02', '癌症知多少']
40 | ```
41 |
42 | ### 📌 高级匹配示例
43 |
44 | > 高级匹配不支持在选课界面内设置, 请手动编辑脚本中的变量值
45 |
46 | 要匹配`晓明明`老师的`修身养性与大学生活`
47 |
48 | ```js
49 | advanceMatchPatterns: [{ cname: '修身养性与大学生活', tname: '晓明明' }]
50 | ```
51 |
52 | 要匹配`星期二 第2节`的`神奇的基因`、`星期日`的`生命科学纵横`
53 |
54 | ```js
55 | advanceMatchPatterns: [
56 | { cname: '神奇的基因', tap: '星期二 第2节' },
57 | { cname: '生命科学纵横', tap: '星期日' }
58 | ]
59 | ```
60 |
61 | 要抛出`中国历史文化概论 01`并匹配`中国历史文化概论 02`
62 |
63 | ```js
64 | advanceMatchPatterns: [{ cname: '中国历史文化概论 02', throw: '中国历史文化概论 01' }]
65 | ```
66 |
67 | 支持参数:
68 |
69 | - `cname` (课程名)
70 | - `credit` (学分)
71 | - `type` (考试类型)
72 | - `tname` (上课教师)
73 | - `tap` (时间地点)
74 | - `more` (选课限制说明)
75 | - `throw` (要抛出的课程名)
76 |
77 | ## 🚀 进阶用法
78 |
79 | 如果上述内容无法满足你的需求, 可以阅读此节, 了解脚本的进阶用法.
80 |
81 | ### 🔎 利用`advanceQuery`提高检索速度
82 |
83 | 可以为`config.advanceQuery`传入查询字符串, 将需要检索的课程量降低, 从而在一定程度上提高检索速度.
84 |
85 | 同时, 可以利用此属性减少需要设定的匹配参数: 例如, 只希望脚本检索所有的人文与艺术类课程, 可以为此属性传入`&gname2019=2`这样脚本取到的源数据将只有这类课程, 在此基础上设置的匹配规则可以更加简单.
86 |
87 | ### 🧲 利用`throw`属性实现抛-抢联动
88 |
89 | > 匹配到目标课程因已选同类课而无法选择时, 抛出已有的课程, 选择目标课程
90 |
91 | 在高级匹配模式下, 当匹配到课程时, 脚本通过`throw`的值检查已选中的课堂课表, 如有则先抛出`throw`值代表的课程, 随后在下一轮检索时发出网络请求, 抢由其他属性匹配到的课程.
92 |
93 | **注意, 在应用`throw`属性时, 请务必确保其他属性的值能让脚本唯一匹配你希望抢的那一门课, 而不是能匹配到其他同类课程**
94 |
95 | **注意, 由于选课是在下一轮检索进行的, 所以请避免设置较低的频率以免课程被抢**
96 |
97 | > `cname`的完整的格式为[课程号:课程名 课序号]
98 |
99 | ## 💡 脚本原理
100 |
101 | 用`fetch()`请求纯净的``元素, 使用 tabletojson 库将其转为 json, 用`setInterval()`无刷新定时重新获取最新数据, 用`isMatched()`匹配给定表达式, 一旦匹配成功触发`trialSubmit()`, 根据实际情况选择重新请求数据还是提交选课请求.
102 |
103 | 简单匹配与高级匹配`pattern`上的区别在调用`addMatchTask()`时被抹平, 由`toAdvancePattern()`将简单匹配的格式转为了包含`cname`属性的高级匹配格式.
104 |
105 | 针对`throw`属性, 它仅仅用于标识要抛出课程的`cname`, 在课程抛出后通过调用`modifyTask()`将其转为了不包含`throw`的高级匹配任务.
106 |
107 | ## 😳 注意事项
108 |
109 | 读取参数优先级: `config.simpleMatchPatterns` > `界面输入(本地存储)`
110 |
111 | 使用参数优先级: `config.advanceMatchPatterns` > `config.simpleMatchPatterns`
112 |
--------------------------------------------------------------------------------
/src/userscript/deepseek-snapshot/index.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Deepseek Snapshot
3 | // @namespace https://github.com/ZiuChen
4 | // @version 1.0.1
5 | // @description Take snapshot for chat history on Deepseek.
6 | // @author ZiuChen
7 | // @homepage https://github.com/ZiuChen
8 | // @supportURL https://github.com/ZiuChen/userscript/issues
9 | // @match https://chat.deepseek.com/*
10 | // @icon https://favicon.im/deepseek.com
11 | // @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
12 | // @updateURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/deepseek-snapshot/index.user.js
13 | // @downloadURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/deepseek-snapshot/index.user.js
14 | // @grant GM_registerMenuCommand
15 | // ==/UserScript==
16 |
17 | GM_registerMenuCommand('Copy to clipboard', async () => {
18 | const chat = document.querySelector('.dad65929')
19 | if (!chat) {
20 | console.error('Chat element not found')
21 | return
22 | }
23 |
24 | const blob = await snapshot(chat)
25 | copyImageToClipboard(blob)
26 | })
27 |
28 | GM_registerMenuCommand('Download to file', async () => {
29 | const chat = document.querySelector('.dad65929')
30 | if (!chat) {
31 | console.error('Chat element not found')
32 | return
33 | }
34 |
35 | const blob = await snapshot(chat)
36 | const filename = `deepseek_snapshot_${new Date().toISOString().slice(0, 10)}.png`
37 | downloadBlob(blob, filename)
38 | })
39 |
40 | async function snapshot(dom) {
41 | const preferredTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
42 | ? 'dark'
43 | : 'light'
44 |
45 | const canvas = await html2canvas(dom, {
46 | scale: 3,
47 | backgroundColor: preferredTheme === 'dark' ? '#000' : '#fff',
48 | useCORS: true,
49 | logging: false
50 | })
51 |
52 | if (!canvas) {
53 | console.error('Failed to create canvas')
54 | return
55 | }
56 |
57 | const toBlob = async (canvas) => {
58 | return new Promise((resolve) => {
59 | canvas.toBlob((blob) => {
60 | resolve(blob)
61 | }, 'image/png')
62 | })
63 | }
64 |
65 | // Convert canvas to blob
66 | const blob = await toBlob(canvas)
67 | if (!blob) {
68 | console.error('Failed to convert canvas to blob')
69 | return
70 | }
71 |
72 | return blob
73 | }
74 |
75 | function copyImageToClipboard(blob) {
76 | const item = new ClipboardItem({
77 | 'image/png': blob
78 | })
79 |
80 | navigator.clipboard.write([item]).then(
81 | () => {
82 | console.log('Image copied to clipboard')
83 | },
84 | (error) => {
85 | console.error('Failed to copy image to clipboard:', error)
86 | }
87 | )
88 | }
89 |
90 | function downloadBlob(blob, filename) {
91 | const url = URL.createObjectURL(blob)
92 | const a = document.createElement('a')
93 | a.href = url
94 | a.download = filename
95 | document.body.appendChild(a)
96 | a.click()
97 | setTimeout(() => {
98 | URL.revokeObjectURL(url)
99 | document.body.removeChild(a)
100 | }, 100)
101 | }
102 |
--------------------------------------------------------------------------------
/src/userscript/lock-screen/index.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Lock Screen
3 | // @namespace https://github.com/ZiuChen
4 | // @version 1.1.1
5 | // @description Locks given websites.
6 | // @author Sarfraz, ZiuChen
7 | // @homepage https://github.com/ZiuChen
8 | // @supportURL https://github.com/ZiuChen/userscript/issues
9 | // @run-at document-start
10 | // @updateURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/lock-screen/index.user.js
11 | // @downloadURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/lock-screen/index.user.js
12 | // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjNzQ3ZDhjIiBkPSJNMjAuNSAxM2EyLjUgMi41IDAgMCAxIDIuNSAyLjV2LjVhMSAxIDAgMCAxIDEgMXY0YTEgMSAwIDAgMS0xIDFoLTVhMSAxIDAgMCAxLTEtMXYtNGExIDEgMCAwIDEgMS0xdi0uNWEyLjUgMi41IDAgMCAxIDIuNS0yLjVtMCAxYTEuNSAxLjUgMCAwIDAtMS41IDEuNXYuNWgzdi0uNWExLjUgMS41IDAgMCAwLTEuNS0xLjVNMjAgNEgydjEyaDEzdjJoLTJ2MmgydjJIN3YtMmgydi0ySDJhMiAyIDAgMCAxLTItMlY0YzAtMS4xMS44OS0yIDItMmgxOGEyIDIgMCAwIDEgMiAydjcuNTNjLS41OS0uMzQtMS4yNy0uNTMtMi0uNTN6Ii8+PC9zdmc+
13 | // ==/UserScript==
14 |
15 | 'use strict'
16 |
17 | const CSS_CODE = /* css */ `body {
18 | filter: blur(10px) !important;
19 | }
20 |
21 | body[data-userscript-lock-screen="false"] {
22 | filter: none !important;
23 | }
24 |
25 | .userscript-lock-screen__mask {
26 | position: fixed;
27 | z-index: 9999;
28 | top: 0;
29 | left: 0;
30 | right: 0;
31 | bottom: 0;
32 | width: 100vw;
33 | height: 100vh;
34 | }
35 |
36 | .userscript-lock-screen__input {
37 | position: absolute;
38 | inset: 0;
39 | margin: auto;
40 | width: 150px;
41 | height: 30px;
42 | font-size: 20px;
43 | border: none;
44 | outline: none;
45 | background: transparent;
46 | }
47 |
48 | @media (prefers-color-scheme: light) {
49 | .userscript-lock-screen__mask {
50 | background: #f5f5f5;
51 | }
52 |
53 | .userscript-lock-screen__input {
54 | color: #141414;
55 | }
56 | }
57 |
58 | @media (prefers-color-scheme: dark) {
59 | .userscript-lock-screen__mask {
60 | background: #141414;
61 | }
62 |
63 | .userscript-lock-screen__input {
64 | color: #f5f5f5;
65 | }
66 | }`
67 |
68 | const PIN = localStorage.getItem('userscript-lock-screen/pwd')
69 |
70 | if (!PIN) {
71 | const result = prompt('Enter Password')
72 | if (!result) {
73 | alert('Password cannot be empty')
74 | return
75 | }
76 |
77 | localStorage.setItem('userscript-lock-screen/pwd', result)
78 | location.reload()
79 | }
80 |
81 | insertCSS(CSS_CODE)
82 |
83 | setTimeout(() => {
84 | hide()
85 | })
86 |
87 | window.addEventListener('blur', hide)
88 |
89 | function hide() {
90 | if (document.documentElement.querySelector('#userscript-lock-screen')) {
91 | // Already locked
92 | return
93 | }
94 |
95 | const container = document.createElement('div')
96 | container.id = 'userscript-lock-screen'
97 | container.className = 'userscript-lock-screen__mask'
98 | document.documentElement.appendChild(container)
99 |
100 | const input = document.createElement('input')
101 | input.type = 'password'
102 | input.className = 'userscript-lock-screen__input'
103 | input.autocomplete = 'off'
104 |
105 | container.appendChild(input)
106 |
107 | input.oninput = () => {
108 | if (input.value === PIN) {
109 | document.documentElement.removeChild(container)
110 | document.body.dataset.userscriptLockScreen = false
111 | }
112 | }
113 | }
114 |
115 | function insertCSS(code) {
116 | const style = document.createElement('style')
117 | style.textContent = code
118 | document.head.appendChild(style)
119 | }
120 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/src/cookie.js:
--------------------------------------------------------------------------------
1 | const config = require('./config')
2 | const { getBrowser, closeBrowser } = require('./browser')
3 | const { calcGapPosition } = require('./utils')
4 |
5 | const formatCookie = (cookies) => {
6 | const cookieItems = []
7 | for (let item of cookies) {
8 | cookieItems.push(item.name + '=' + item.value)
9 | }
10 | return cookieItems.join(';')
11 | }
12 |
13 | const getCookie = async (mobile, password) => {
14 | try {
15 | const browser = await getBrowser()
16 | const page = await browser.newPage()
17 | await page.goto(config.juejin.login)
18 | await page.waitForTimeout(1000)
19 | await page.waitForSelector('.clickable')
20 | await page.click('.clickable')
21 | await page.waitForTimeout(1000)
22 | await page.waitForSelector('input[name=loginPhoneOrEmail]')
23 | console.log('--------------------')
24 | console.log(`输入账号 ${mobile}`)
25 | await page.type('input[name=loginPhoneOrEmail]', mobile, { delay: 50 })
26 | console.log(`输入密码 ****`)
27 | await page.type('input[name=loginPassword]', password, { delay: 50 })
28 | await page.waitForTimeout(1000)
29 | await page.click('.btn')
30 | console.log(`开始登录`)
31 | await page.waitForSelector('#captcha-verify-image')
32 | await page.waitForTimeout(1000)
33 | let slideNum = 10 //最多尝试10次滑块验证
34 | let slideStatus = false
35 | let loginStatus = false
36 | let loginRes = null
37 | while (slideNum > 0 && slideStatus == false) {
38 | console.log(`开始第${10 - slideNum + 1}次验证`)
39 | const imageSrc = await page.$eval('#captcha-verify-image', (el) => el.src)
40 | const distance = await calcGapPosition(page, imageSrc)
41 | await page.hover('.secsdk-captcha-drag-icon')
42 | let ele = await page.$('.secsdk-captcha-drag-icon')
43 | let gapEle = await page.$('.captcha_verify_img_slide')
44 | let gapBlock = await gapEle.boundingBox()
45 | let block = await ele.boundingBox()
46 | await page.mouse.down()
47 | await page.mouse.move(
48 | gapBlock.x + distance + gapBlock.width - block.width / 2 - 5,
49 | block.y + block.y / 2,
50 | { steps: 50 }
51 | )
52 | await page.mouse.up()
53 | let verifyRes = await page.waitForResponse(
54 | (response) => response.url().includes(config.juejin.verifyApi) && response.status() === 200
55 | )
56 | let jsonRes = await verifyRes.json()
57 | if (jsonRes.code == 200) {
58 | // 验证通过
59 | slideStatus = true
60 | console.log('验证成功,登录中...')
61 | loginRes = await page.waitForResponse(
62 | (response) => response.url().includes(config.juejin.loginApi) && response.status() === 200
63 | )
64 | try {
65 | jsonRes = await loginRes.json()
66 | if (jsonRes.message && jsonRes.message == 'error') {
67 | console.log(jsonRes)
68 | console.log(`掘金登录失败[0]`)
69 | } else {
70 | loginRes = jsonRes.data || {}
71 | loginStatus = true
72 | }
73 | } catch (err) {
74 | console.log(err)
75 | console.log(`登录出错`)
76 | }
77 | } else {
78 | await page.waitForTimeout(5 * 1000)
79 | await page.$('.secsdk_captcha_refresh--text')
80 | }
81 | slideNum--
82 | }
83 | if (!loginStatus) {
84 | console.log(`掘金登录失败[1]`)
85 | return false
86 | }
87 | console.log(`登录成功`)
88 | console.log('--------------------')
89 | await page.waitForTimeout(2 * 1000)
90 | const cookie = await page.cookies()
91 | const cookieStr = formatCookie(cookie)
92 | await closeBrowser()
93 | return cookieStr
94 | } catch (error) {
95 | console.log(error)
96 | }
97 | }
98 |
99 | module.exports = {
100 | getCookie,
101 | formatCookie
102 | }
103 |
--------------------------------------------------------------------------------
/src/userscript/tap-to-tab/index.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Tap to Tab
3 | // @namespace https://github.com/ZiuChen
4 | // @version 1.1.2
5 | // @description Tap to Tab on Tampermonkey!
6 | // @author ZiuChen
7 | // @homepage https://github.com/ZiuChen
8 | // @supportURL https://github.com/ZiuChen/userscript/issues
9 | // @match *://*/*
10 | // @run-at document-start
11 | // @grant GM_openInTab
12 | // @grant GM_addStyle
13 | // @grant unsafeWindow
14 | // @updateURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/tap-to-tab/index.user.js
15 | // @downloadURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/tap-to-tab/index.user.js
16 | // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Im0xMS41IDExbDYuMzggNS4zN2wtLjg4LjE4bC0uNjQuMTJjLS42My4xMy0uOTkuODMtLjcxIDEuNGwuMjcuNThsMS4zNiAyLjk0bC0xLjQyLjY2bC0xLjM2LTIuOTNsLS4yNi0uNThhLjk4NS45ODUgMCAwIDAtMS41Mi0uMzZsLS41MS40bC0uNzEuNTd6bS0uNzQtMi4zMWEuNzYuNzYgMCAwIDAtLjc2Ljc2VjIwLjljMCAuNDIuMzQuNzYuNzYuNzZjLjE5IDAgLjM1LS4wNi40OC0uMTZsMS45MS0xLjU1bDEuNjYgMy42MmMuMTMuMjcuNC40My42OS40M2MuMTEgMCAuMjIgMCAuMzMtLjA4bDIuNzYtMS4yOGMuMzgtLjE4LjU2LS42NC4zNi0xLjAxTDE3LjI4IDE4bDIuNDEtLjQ1YS45LjkgMCAwIDAgLjQzLS4yNmMuMjctLjMyLjIzLS43OS0uMTItMS4wOGwtOC43NC03LjM1bC0uMDEuMDFhLjc2Ljc2IDAgMCAwLS40OS0uMThNMTUgMTBWOGg1djJ6bS0xLjE3LTUuMjRsMi44My0yLjgzbDEuNDEgMS40MWwtMi44MyAyLjgzek0xMCAwaDJ2NWgtMnpNMy45MyAxNC42NmwyLjgzLTIuODNsMS40MSAxLjQxbC0yLjgzIDIuODN6bTAtMTEuMzJsMS40MS0xLjQxbDIuODMgMi44M2wtMS40MSAxLjQxek03IDEwSDJWOGg1eiIvPjwvc3ZnPg==
17 | // ==/UserScript==
18 |
19 | 'use strict'
20 |
21 | let pending
22 | let lastNode
23 | let skipOver
24 | let delay = 300
25 |
26 | unsafeWindow.addEventListener(
27 | 'beforeunload',
28 | () => {
29 | if (pending) {
30 | clearTimeout(pending)
31 | pending = null
32 | }
33 | },
34 | false
35 | )
36 |
37 | function scheduleClick(e) {
38 | let n = e.target
39 | let props = {
40 | clientX: e.clientX,
41 | clientY: e.clientY,
42 | screenX: e.screenX,
43 | screenY: e.screenY,
44 | view: e.view,
45 | bubbles: true,
46 | cancelable: true
47 | }
48 | pending = setTimeout(() => {
49 | pending = null
50 | skipOver = n
51 | n.dispatchEvent(new MouseEvent('click', props))
52 | }, delay)
53 | }
54 |
55 | function stopEvent(e) {
56 | e.preventDefault()
57 | e.stopPropagation()
58 | e.stopImmediatePropagation()
59 | }
60 |
61 | unsafeWindow.addEventListener(
62 | 'click',
63 | (e) => {
64 | if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey || e.button !== 0) return
65 |
66 | let n = e.target
67 |
68 | if (n && skipOver === n) {
69 | //skip over our own dispatch
70 | skipOver = null
71 | return
72 | }
73 |
74 | for (let i = 4; i >= 0 && n && !n.href; i--) {
75 | n = n.parentNode
76 | }
77 | if (!n) return
78 |
79 | let last = lastNode
80 | lastNode = n
81 | let good =
82 | e.isTrusted &&
83 | n.href &&
84 | /^(https?|ftps?|file):/i.test(n.href) &&
85 | n.getAttribute('href') !== '#'
86 | //&& (n.getAttribute("target") !== "_self");
87 |
88 | if (pending) {
89 | if (n === last) {
90 | clearTimeout(pending)
91 | pending = null
92 | GM_openInTab(n.href, { active: false })
93 | stopEvent(e)
94 |
95 | if (n.style.animationName === 'bang') {
96 | n.style.animation = 'bong 1.3s ease-in'
97 | } else {
98 | n.style.animation = 'bang 1.3s ease-in'
99 | }
100 | } else if (good) {
101 | clearTimeout(pending)
102 | scheduleClick(e)
103 | stopEvent(e)
104 | }
105 | } else if (good) {
106 | scheduleClick(e)
107 | stopEvent(e)
108 | }
109 | },
110 | true
111 | )
112 |
113 | GM_addStyle(`@keyframes bang {
114 | from {
115 | opacity: 0;
116 | }
117 | }
118 |
119 | @keyframes bong {
120 | from {
121 | opacity: 0;
122 | }
123 | }`)
124 |
--------------------------------------------------------------------------------
/src/userscript/github-copilot-enhanced/README.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | [中文](./README.zh-CN.md)
2 |
3 | ----
4 |
5 | # Github Copilot Enhanced
6 |
7 | A UserScript for enhancing GitHub Copilot (Web).
8 |
9 | > [!WARNING]
10 | > Please note, this script is not an official product. Make sure you understand how it works and the potential risks before using.
11 | >
12 | > All data is stored only in your local browser and will not be uploaded to any server.
13 |
14 | 
15 |
16 | ## Feature Highlights
17 |
18 | ### 🗄️ Long-Term Storage
19 | By default, GitHub Copilot (Web) only retains chat history for 30 days.
20 | This script enables longer chat history retention by saving conversations to the browser's IndexedDB.
21 |
22 | ## 🔍 Search Functionality
23 | - **Shortcut Open**: Press `Ctrl/Cmd+K` to quickly open the search panel
24 | - **Live Search**: Shows matching chat records in real time as you type keywords, supports fuzzy search
25 | - **Keyboard Navigation**: Use arrow keys to browse search results, press Enter to open the selected conversation
26 |
27 | ### ⚙️ Settings Panel
28 | - **Quick Access**: Click the gear icon in the search panel to open settings
29 | - **Configuration Management**: Centrally manage all script configuration options
30 | - **WebDAV Sync**: Configure WebDAV server for cross-browser/device data synchronization
31 |
32 | ### 🔄 Auto Sync
33 | - **Periodic Sync**: Automatically syncs all chat history every 30 minutes
34 | - **Initial Load**: Executes one sync immediately when the page loads
35 | - **Background Operation**: Sync runs in the background without affecting normal usage
36 |
37 | ### ☁️ WebDAV Sync
38 | - **Simple Configuration**: Enter WebDAV server URL, username and password in settings panel
39 | - **Manual Sync**: Support manual upload and download operations
40 | - **Smart Merge**: Automatically merge local and remote data when downloading, keeping the latest version
41 | - **Cross-Device**: Sync data across multiple devices via WebDAV
42 |
43 | ### 🔌 Seamless Integration
44 | - **Request Interception**: Automatically intercepts GitHub Copilot API requests
45 | - **Data Merging**: Merges local history with server data before returning
46 | - **Transparent Operation**: Completely transparent to the user, no extra actions required
47 |
48 | ## Usage
49 |
50 | ### Installation
51 | 1. Install Tampermonkey or another UserScript manager
52 | 2. [Click here to install](https://github.com/ZiuChen/userscript/raw/refs/heads/main/src/userscript/github-copilot-enhanced/index.user.js) the script
53 |
54 | ### Configure WebDAV Sync (Optional)
55 | 1. Press `Ctrl/Cmd+K` to open the search panel
56 | 2. Click the gear icon in the top right corner to open settings panel
57 | 3. Fill in the "WebDAV Sync" section:
58 | - Server URL: e.g., `https://example.com/webdav`
59 | - Username and Password
60 | 4. Click "Save Config"
61 | 5. Click "Test Connection" to verify the configuration
62 | 6. Use "Upload Data" and "Download Data" buttons for manual sync
63 |
64 | **Notes**:
65 | - Before first upload, if remote file exists, it will automatically download and merge first
66 | - When downloading, local and remote data will be automatically merged, keeping the newer timestamp version
67 | - Supports mainstream WebDAV services like Nutstore, Nextcloud, etc.
68 |
69 | ## Notes
70 |
71 | 1. **Authentication Issues**: The project uses your browser's existing authentication info for API requests. Make sure you are logged into GitHub and have Copilot Pro or higher access
72 | 2. **Storage Limitations**: IndexedDB is subject to browser storage quotas; too much data may cause storage failures
73 | 3. **Privacy & Security**:
74 | - All data is stored only in your local browser's IndexedDB
75 | - WebDAV sync is completely optional, no data will be uploaded if not configured
76 | - WebDAV credentials are encrypted and stored locally using GM_setValue
77 | 4. **Compatibility**: Requires a modern browser that supports IndexedDB
78 | 5. **Sync Frequency**: Default is to sync GitHub Copilot data every 30 minutes, can be adjusted as needed
79 | 6. **WebDAV Security**:
80 | - Recommend using HTTPS protocol for WebDAV server
81 | - Regularly check the integrity of remote backup files
82 | - Be aware of WebDAV server storage space limitations
83 |
--------------------------------------------------------------------------------
/src/script/JuejinDailyPublish/index.js:
--------------------------------------------------------------------------------
1 | // HUAWEI P30 Pro 2340x1080
2 | const TIME_OUT = 1000
3 | const LOAD_TIME_OUT = 5000
4 | let title = ''
5 |
6 | // 解锁屏幕
7 | const unlock = () => {
8 | if (!device.isScreenOn()) {
9 | device.wakeUp()
10 | sleep(500)
11 | swipe(500, 2000, 500, 1000, 1500)
12 | }
13 | }
14 |
15 | const customBack = () => {
16 | back()
17 | sleep(TIME_OUT)
18 | }
19 |
20 | const customClick = (x, y) => {
21 | click(x, y)
22 | sleep(TIME_OUT)
23 | }
24 |
25 | const clickItemByBound = (item) => {
26 | const bounds = item.bounds()
27 | const flag = customClick(bounds.centerX(), bounds.centerY())
28 | return flag
29 | }
30 |
31 | const navBarClick = (navName) => {
32 | className('android.widget.FrameLayout')
33 | .desc(navName)
34 | .find()
35 | .forEach(function (tv) {
36 | clickItemByBound(tv)
37 | })
38 | }
39 |
40 | const clickText = (text) => {
41 | className('android.widget.TextView')
42 | .text(text)
43 | .find()
44 | .forEach(function (tv) {
45 | log(text)
46 | clickItemByBound(tv)
47 | })
48 | }
49 |
50 | const clickImage = (id) => {
51 | className('android.widget.ImageView')
52 | .id(id)
53 | .find()
54 | .forEach(function (tv) {
55 | log(id)
56 | clickItemByBound(tv)
57 | })
58 | }
59 |
60 | const generateMsg = (title) => {
61 | const date = new Date()
62 | const symbols = [', ', ',', '。', ' ', '\n']
63 | const msgList = [
64 | date.getMonth() + 1,
65 | '月',
66 | date.getDate(),
67 | '日 ',
68 | '每日阅读打卡',
69 | symbols[random(0, symbols.length - 1)],
70 | '今天阅读了《',
71 | title,
72 | '》,'
73 | ]
74 | const suffixs = [
75 | '看了这篇文章对自己收获很大,希望能有机会实践下来',
76 | '这篇文章实在太妙了!必须码住!',
77 | '干货满满,好文章值得分享',
78 | '总结的很系统很全面',
79 | '以前一直挺好奇的,现在终于有机会学习!受益匪浅!',
80 | '点赞,通俗易懂!',
81 | '值得好好阅读一番~',
82 | '文章内容太优秀了,佩服作者!',
83 | '值得一看,学习了!',
84 | '文章讲得很好,值得学习!',
85 | '文章质量很高,值得学习和收藏,很适合我这种菜鸡。',
86 | '光看是不行,最好跟着文章敲代码。',
87 | '感觉自己需要学习的太多啦',
88 | '从这篇文章可以学到很多知识点',
89 | '文章内容干货满满,值得学习,推荐!'
90 | ]
91 | const emojis = [
92 | '[思考]',
93 | '[奋斗]',
94 | '[偷笑]',
95 | '[委屈]',
96 | '[拥抱]',
97 | '[送心]',
98 | '[呲牙]',
99 | '[吃瓜群众]',
100 | '[睡]',
101 | '[憨笑]',
102 | '[机智]',
103 | '[力量]'
104 | ]
105 |
106 | const msg =
107 | msgList.join('') + suffixs[random(0, suffixs.length - 1)] + emojis[random(0, emojis.length - 1)]
108 | return msg
109 | }
110 |
111 | const main = () => {
112 | unlock()
113 |
114 | // 启动 App
115 | launchApp('稀土掘金')
116 |
117 | // 等待程序启动
118 | waitForActivity('im.juejin.android.ui.MainActivity')
119 |
120 | sleep(TIME_OUT)
121 |
122 | // 切换到`首页`
123 | navBarClick('首页')
124 |
125 | // 刷新推荐文章列表
126 | swipe(580, 850, 580, 850 + 850, TIME_OUT)
127 |
128 | // 休眠 2s + 5s 等待列表加载完成
129 | sleep(TIME_OUT + LOAD_TIME_OUT)
130 |
131 | // 进入第一篇推荐文章
132 | className('android.widget.TextView')
133 | .id('com.daimajia.gold:id/title')
134 | .find()
135 | .forEach((item, index) => {
136 | // 跳过第一个广告
137 | if (index === 1) {
138 | log('点击文章')
139 | log(item.text())
140 | clickItemByBound(item)
141 | return
142 | }
143 | })
144 |
145 | // 获取文章 title
146 | id('com.daimajia.gold:id/article_title')
147 | .find()
148 | .forEach(function (tv) {
149 | title = tv.text()
150 | })
151 |
152 | // 点击右上角三个点
153 | clickImage('com.daimajia.gold:id/iv_more')
154 |
155 | // 复制文章链接
156 | const copyParent = text('复制链接').findOne().parent()
157 | clickItemByBound(copyParent)
158 |
159 | customBack() // 返回主页
160 |
161 | sleep(TIME_OUT)
162 |
163 | // 底部导航栏 沸点
164 | navBarClick('沸点')
165 |
166 | // 我加入的圈子
167 | clickText('我加入的圈子')
168 |
169 | // 进入第一个圈子
170 | const groupParent = text('好文推荐').findOne().parent()
171 | clickItemByBound(groupParent)
172 |
173 | // 点击右下角悬浮按钮
174 | clickImage('com.daimajia.gold:id/btn_post')
175 |
176 | // 链接 按钮
177 | clickImage('com.daimajia.gold:id/iv_link_button')
178 |
179 | // paste() // 默认自动粘贴
180 |
181 | // 添加 解析
182 | clickText('添加')
183 |
184 | // 等待解析完成
185 | sleep(TIME_OUT + LOAD_TIME_OUT * 2)
186 |
187 | const message = generateMsg(title)
188 |
189 | // 将 msg 写入剪切板并粘贴
190 | setClip(message)
191 |
192 | paste()
193 |
194 | sleep(TIME_OUT)
195 |
196 | // 发布
197 | clickText('发布')
198 |
199 | // 返回主页
200 | customBack()
201 | customBack()
202 | customBack()
203 |
204 | // 退出 App
205 | customBack()
206 | customBack()
207 | }
208 |
209 | main()
210 |
--------------------------------------------------------------------------------
/src/userscript/GithubRepoInfo/userscript.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Github Repo Info
3 | // @name:en Github Repo Info
4 | // @name:zh-CN Github 仓库信息
5 | // @description Get Github Repo info like repo size, create time and etc.
6 | // @description:zh-CN 获取 Github 仓库信息,如仓库体积、创建事件等
7 | // @version 1.0.0
8 | // @author ZiuChen
9 | // @namespace https://github.com/ZiuChen
10 | // @source https://github.com/ZiuChen/userscript
11 | // @supportURL https://github.com/ZiuChen/userscript/issues
12 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/GithubRepoInfo/userscript.user.js
13 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/GithubRepoInfo/userscript.user.js
14 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
15 | // @match https://github.com/*
16 | // @grant GM_getValue
17 | // @grant GM_setValue
18 | // @grant GM_deleteValue
19 | // @grant GM_registerMenuCommand
20 | // ==/UserScript==
21 |
22 | const githubTokenRef = () =>
23 | GM_getValue(
24 | 'GITHUB_TOKEN',
25 | atob(
26 | 'Z2l0aHViX3BhdF8xMUFaRldORVEwZE5CRE1zalRRTG4zX3dua2NDeFNFR1lmeHJueWpiSjdLUE1WeG1PYlRVNFhYNHYzV1liZlFNWFU2N0hPN1I1UE5yUkt1SHY0'
27 | )
28 | )
29 |
30 | const MENU_COMMANDS = [
31 | {
32 | name: 'Show Repo Info',
33 | action: () => showRepoInfo(getCurrentRepoInfo())
34 | },
35 | {
36 | name: 'Fetch Repo Info',
37 | action: showRepoInfo
38 | },
39 | {
40 | name: 'Set Github Token',
41 | action: setGithubToken
42 | },
43 | {
44 | name: 'Reset Github Token',
45 | action: resetGithubToken
46 | },
47 | {
48 | name: 'View Current Github Token',
49 | action: () => {
50 | prompt('Current Github Token', githubTokenRef())
51 | }
52 | }
53 | ]
54 |
55 | // 注册菜单命令
56 | MENU_COMMANDS.forEach((command) => {
57 | GM_registerMenuCommand(
58 | command.name,
59 | function (event) {
60 | command.action()
61 | },
62 | { autoClose: true }
63 | )
64 | })
65 |
66 | /**
67 | * 获取 Repo 信息
68 | */
69 | async function showRepoInfo(repo) {
70 | if (!repo) {
71 | repo = prompt('Type in Repo info. like: `vuejs/core`', getCurrentRepoInfo())
72 | }
73 |
74 | if (!repo) return
75 |
76 | // 检查 repo 是否合法
77 | const repoReg = /^[\w-]+\/[\w-]+$/
78 |
79 | if (!repoReg.test(repo)) {
80 | alert('Repo info is invalid.')
81 | return
82 | }
83 |
84 | const repoInfo = await fetchRepoInfo(repo)
85 |
86 | repoInfo && alert(repoInfo)
87 | }
88 |
89 | /**
90 | * 设置 Token
91 | */
92 | function setGithubToken() {
93 | const token = prompt('Type in Github Token', githubTokenRef())
94 |
95 | if (!token) return
96 |
97 | GM_setValue('GITHUB_TOKEN', token)
98 | }
99 |
100 | /**
101 | * 重置 Token
102 | */
103 | function resetGithubToken() {
104 | GM_deleteValue('GITHUB_TOKEN')
105 | }
106 |
107 | /**
108 | * 获取 repo 信息
109 | */
110 | async function fetchRepoInfo(repo) {
111 | return fetch(`https://api.github.com/repos/${repo}`, {
112 | headers: {
113 | authorization: `token ${githubTokenRef()}`
114 | }
115 | })
116 | .then((res) => res.json())
117 | .then((res) => {
118 | console.log('Github Server Response:', res)
119 |
120 | const { size, created_at, updated_at, message } = res
121 |
122 | if (!size || !created_at) {
123 | throw new Error(
124 | 'Repo info is not available. Please checkout Github Token is valid. ' + message
125 | )
126 | }
127 |
128 | const sizeText = `Size: ${formatSize(size)}`
129 | const createdText = `Created: ${formatTime(created_at)}`
130 | const updatedText = `Updated: ${formatTime(updated_at)}`
131 |
132 | return `${sizeText}\n${createdText}\n${updatedText}`
133 | })
134 | .catch((err) => {
135 | alert(err)
136 | })
137 | }
138 |
139 | /**
140 | * 获取当前 repo 信息
141 | */
142 | function getCurrentRepoInfo() {
143 | // 从 url 中获取 repo 信息
144 | const repoReg = /github\.com\/(.+?)\/(.+?)(\/|$)/
145 | try {
146 | const [, owner, repo] = window.location.href.match(repoReg)
147 | return `${owner}/${repo}`
148 | } catch (error) {
149 | return ''
150 | }
151 | }
152 |
153 | /**
154 | * 格式化文件大小
155 | * 将文件大小转换为可读性更好的格式
156 | */
157 | function formatSize(bytes, decimals = 2) {
158 | if (bytes === 0) return '0 Bytes'
159 |
160 | const k = 1024
161 | const dm = decimals < 0 ? 0 : decimals
162 | const sizes = ['KB', 'MB', 'GB', 'TB', 'PB']
163 | const i = Math.floor(Math.log(bytes) / Math.log(k))
164 |
165 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
166 | }
167 |
168 | /**
169 | * 格式化时间
170 | */
171 | function formatTime(time) {
172 | return new Date(time).toLocaleString()
173 | }
174 |
--------------------------------------------------------------------------------
/src/script/JuejinDraw/index.js:
--------------------------------------------------------------------------------
1 | !(({ api, console, utils }) => {
2 | const state = {
3 | simulateSpeed: 3500, // ms/进行一次抽奖
4 | sumPoint: 0,
5 | pointCost: 0,
6 | supplyPoint: 0,
7 | freeCount: 0,
8 | luckyValue: 0,
9 | lottery: [],
10 | counter: 0,
11 | prize: {}
12 | }
13 |
14 | !(async () => {
15 | await utils.wait(100)
16 | console.clear()
17 |
18 | try {
19 | const checkInResult = await api.checkIn()
20 | console.log(checkInResult)
21 | const incrPoint = checkInResult.incr_point
22 | console.log(`签到成功 +${incrPoint} 矿石`)
23 |
24 | const sumPoint = checkInResult.sum_point
25 | state.sumPoint = sumPoint
26 | } catch (e) {
27 | console.log(e.message)
28 |
29 | const sumPoint = await api.getCurrentPoint()
30 | state.sumPoint = sumPoint
31 | }
32 |
33 | try {
34 | const luckyusersResult = await api.getLotteriesLuckyUsers()
35 | if (luckyusersResult.count > 0) {
36 | const no1LuckyUser = luckyusersResult.lotteries[0]
37 | const dipLuckyResult = await api.dipLucky(no1LuckyUser.history_id)
38 | if (dipLuckyResult.has_dip) {
39 | console.log(`今天你已经沾过喜气,明天再来吧!`)
40 | } else {
41 | console.log(`沾喜气 +${dipLuckyResult.dip_value} 幸运值`)
42 | }
43 | }
44 | } catch {}
45 |
46 | console.log(`当前余额:${state.sumPoint} 矿石`)
47 |
48 | const luckyResult = await api.getMyLucky()
49 | state.luckyValue = luckyResult.total_value
50 | console.log(`当前幸运值:${state.luckyValue}/6000`)
51 |
52 | const lotteryConfig = await api.getLotteryConfig()
53 | state.lottery = lotteryConfig.lottery
54 | state.pointCost = lotteryConfig.point_cost
55 | state.freeCount = lotteryConfig.free_count
56 | state.sumPoint += state.freeCount * state.pointCost
57 | console.log(`免费抽奖次数: ${state.freeCount}`)
58 |
59 | console.log(`准备梭哈!`)
60 |
61 | console.logGroupStart('奖品实况')
62 |
63 | const getSupplyPoint = (draw) => {
64 | const maybe = [
65 | ['lottery_id', '6981716980386496552'],
66 | ['lottery_name', '随机矿石'],
67 | ['lottery_type', 1]
68 | ]
69 | if (maybe.findIndex(([prop, value]) => draw[prop] === value) !== -1) {
70 | const supplyPoint = Number.parseInt(draw.lottery_name)
71 | if (!isNaN(supplyPoint)) {
72 | return supplyPoint
73 | }
74 | }
75 | return 0
76 | }
77 |
78 | const lottery = async () => {
79 | const result = await api.tenDrawLottery()
80 | const { LotteryBases, draw_lucky_value, total_lucky_value } = result
81 | // state.sumPoint -= state.pointCost
82 | // state.sumPoint += getSupplyPoint(result)
83 | // state.luckyValue += draw_lucky_value
84 | // state.counter += 10
85 | // state.prize[result.lottery_name] = (state.prize[result.lottery_name] || 0) + 1
86 | console.log(LotteryBases.map((item) => item.lottery_name).join('\n'))
87 | }
88 |
89 | while (state.freeCount > 0) {
90 | await lottery()
91 | state.freeCount--
92 | await utils.wait(state.simulateSpeed)
93 | }
94 |
95 | while (state.sumPoint >= state.pointCost) {
96 | await lottery()
97 | await utils.wait(state.simulateSpeed)
98 | }
99 |
100 | console.logGroupEnd('奖品实况')
101 |
102 | console.log(`弹药不足,当前余额:${state.sumPoint} 矿石`)
103 | console.log(`养精蓄锐来日再战!`)
104 |
105 | const recordInfo = []
106 | recordInfo.push('=====[战绩详情]=====')
107 | if (state.counter > 0) {
108 | const prizeList = []
109 | for (const key in state.prize) {
110 | prizeList.push(`${key}: ${state.prize[key]}`)
111 | }
112 | recordInfo.push(...prizeList)
113 | recordInfo.push('-------------------')
114 | recordInfo.push(`共计: ${state.counter}`)
115 | } else {
116 | recordInfo.push('暂无奖品')
117 | }
118 | recordInfo.push('+++++++++++++++++++')
119 | recordInfo.push(`幸运值: ${state.luckyValue}/6000`)
120 | recordInfo.push('===================')
121 | console.log(recordInfo.join('\n'))
122 | })()
123 | })(
124 | (() => {
125 | const cs = (() => {
126 | const result = []
127 | return {
128 | done: () => (typeof completion === 'function' && completion(result), (result.length = 0)),
129 | log: (msg) => (result.push(msg), console.log(msg)),
130 | clear: () => ((result.length = 0), console.clear()),
131 | logGroupStart: (name) => console.group(name),
132 | logGroupEnd: (name) => console.groupEnd(name)
133 | }
134 | })()
135 |
136 | const api = (() => {
137 | return {
138 | async fetch({ path, method, data }) {
139 | return fetch(`https://api.juejin.cn/growth_api/v1${path}`, {
140 | headers: {
141 | cookie: document.cookie
142 | },
143 | method: method,
144 | body: JSON.stringify(data),
145 | credentials: 'include'
146 | })
147 | .then((res) => res.json())
148 | .then((res) => {
149 | if (res.err_no) {
150 | throw new Error(res.err_msg)
151 | }
152 | return res.data
153 | })
154 | },
155 | async get(path) {
156 | return this.fetch({ path, method: 'GET' })
157 | },
158 | async post(path, data) {
159 | return this.fetch({ path, method: 'POST', data })
160 | },
161 | async getLotteryConfig() {
162 | return this.get('/lottery_config/get')
163 | },
164 | async getCurrentPoint() {
165 | return this.get('/get_cur_point')
166 | },
167 | async drawLottery() {
168 | return this.post('/lottery/draw')
169 | },
170 | async tenDrawLottery() {
171 | return this.post('/lottery/ten_draw')
172 | },
173 | async checkIn() {
174 | return this.post('/check_in')
175 | },
176 | async getLotteriesLuckyUsers() {
177 | return this.post('/lottery_history/global_big', {
178 | page_no: 1,
179 | page_size: 5
180 | })
181 | },
182 | async dipLucky(lottery_history_id) {
183 | return this.post('/lottery_lucky/dip_lucky', {
184 | lottery_history_id
185 | })
186 | },
187 | async getMyLucky() {
188 | return this.post('/lottery_lucky/my_lucky')
189 | }
190 | }
191 | })()
192 |
193 | const utils = (() => {
194 | return {
195 | async wait(time = 0) {
196 | return new Promise((resolve) => setTimeout(resolve, time))
197 | }
198 | }
199 | })()
200 |
201 | return { console: cs, api, utils }
202 | })()
203 | )
204 |
--------------------------------------------------------------------------------
/src/userscript/BJTUCaptchaAutofill/userscript.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name 识别并自动填写MIS验证码
3 | // @namespace https://github.com/ZiuChen/NO-FLASH-Upload
4 | // @version 1.1.0
5 | // @description 识别并自动填写北京交通大学MIS入口的验证码,使用前需自行申请讯飞印刷文字识别API
6 | // @author Ziu
7 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/NO-FLASH-Upload@master/plugins/fillCaptcha.user.js
8 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/NO-FLASH-Upload@master/plugins/fillCaptcha.user.js
9 | // @match https://cas.bjtu.edu.cn/*
10 | // @connect webapi.xfyun.cn
11 | // @grant GM_xmlhttpRequest
12 | // @grant GM_setValue
13 | // @grant GM_getValue
14 | // @require https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js
15 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
16 | // @license MIT
17 | // ==/UserScript==
18 |
19 | /**
20 | * 使用前请先申请: 讯飞开放平台 印刷文字识别接口 申请链接 www.xfyun.cn 每天可免费识别 500 次
21 | * 控制台/我的应用/文字识别/印刷文字识别 获取到 APPID 和 APIKey
22 | * apikey存储在本地不会上传到服务器
23 | */
24 | const global = {
25 | /**
26 | * 讯飞印刷文字识别接口地址
27 | */
28 | hostUrl: 'https://webapi.xfyun.cn/v1/service/v1/ocr/general',
29 | /**
30 | * APPID
31 | */
32 | appid: null,
33 | /**
34 | * APIKey
35 | */
36 | apiKey: null,
37 | /**
38 | * 验证码图片选择器
39 | */
40 | imgSelector: '.captcha',
41 | /**
42 | * 验证码输入框选择器
43 | */
44 | inputSelector: '#id_captcha_1'
45 | }
46 |
47 | const console = {
48 | log: window.console.log.bind(window.console, '[识别并自动填写MIS验证码]'),
49 | error: window.console.error.bind(window.console, '[识别并自动填写MIS验证码]'),
50 | warn: window.console.warn.bind(window.console, '[识别并自动填写MIS验证码]'),
51 | info: window.console.info.bind(window.console, '[识别并自动填写MIS验证码]')
52 | }
53 |
54 | /**
55 | * 获取验证码图片的base64编码
56 | */
57 | async function getCaptchaImage() {
58 | const img = document.querySelector(global.imgSelector)
59 | if (!img) {
60 | alert('未找到验证码图片')
61 | return
62 | }
63 |
64 | const canvas = document.createElement('canvas')
65 | canvas.width = img.width
66 | canvas.height = img.height
67 | const ctx = canvas.getContext('2d')
68 | ctx.drawImage(img, 0, 0)
69 | const base64 = canvas.toDataURL()
70 | return base64
71 | }
72 |
73 | /**
74 | * blob转base64
75 | */
76 | async function blobToBase64(blob) {
77 | return new Promise((resolve, reject) => {
78 | const reader = new FileReader()
79 | reader.onload = () => {
80 | resolve(reader.result)
81 | }
82 | reader.onerror = reject
83 | reader.readAsDataURL(blob)
84 | })
85 | }
86 |
87 | /**
88 | * 组装请求头
89 | */
90 | function getReqHeader() {
91 | const xParamStr = CryptoJS.enc.Base64.stringify(
92 | CryptoJS.enc.Utf8.parse(
93 | JSON.stringify({
94 | language: 'cn|en'
95 | })
96 | )
97 | )
98 | const timeStamp = parseInt(new Date().getTime() / 1000) // 获取当前时间戳
99 | const xCheckSum = CryptoJS.MD5(global.apiKey + timeStamp + xParamStr).toString()
100 | return {
101 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
102 | 'X-Appid': global.appid,
103 | 'X-CurTime': timeStamp + '',
104 | 'X-Param': xParamStr,
105 | 'X-CheckSum': xCheckSum
106 | }
107 | }
108 |
109 | /**
110 | * 修正识别字符串
111 | */
112 | function reviseString(ocrResult) {
113 | const rules = {
114 | '*': ['x', 'X', '×'],
115 | '/': ['.'],
116 | ' ': ['='] // remove
117 | }
118 | let res = ocrResult
119 | for (const symbol of Object.keys(rules)) {
120 | const rule = rules[symbol]
121 | rule.forEach((r) => {
122 | if (ocrResult.indexOf(r) !== -1) {
123 | res = res.replace(r, symbol)
124 | }
125 | })
126 | }
127 |
128 | // 修正后的字符串中仍然存在非数字字符
129 | if (res.match(/[^0-9\+\-\*\/\.]/g)) {
130 | res = res.replace(/[^0-9\+\-\*\/\.]/g, '')
131 | }
132 |
133 | console.log('originString: ' + ocrResult)
134 | console.log('rtnString: ' + res)
135 | return res
136 | }
137 |
138 | /**
139 | * 处理ocr识别传回的字符串
140 | * 执行计算并返回结果
141 | */
142 | function calcResult(string) {
143 | try {
144 | return eval(reviseString(string))
145 | } catch (error) {
146 | confirm('计算失败,点击确定重新识别') && location.reload()
147 | }
148 | }
149 |
150 | /**
151 | * GM_xmlhttpRequest 封装
152 | */
153 | function fetchWithGM(url, options) {
154 | return new Promise((resolve, reject) => {
155 | GM_xmlhttpRequest({
156 | method: options.method || 'GET',
157 | url,
158 | headers: options.headers,
159 | data: options.data,
160 | responseType: options.responseType || 'json',
161 | timeout: options.timeout || 10 * 1000,
162 | onload: resolve,
163 | onerror: reject,
164 | ontimeout: () => reject('请求超时')
165 | })
166 | })
167 | }
168 |
169 | /**
170 | * 参数预检查并填充到全局变量
171 | */
172 | function precheck() {
173 | // 优先从 GM_getValue 中获取
174 | global.appid = GM_getValue('xf_appid')
175 | global.apiKey = GM_getValue('xf_apiKey')
176 |
177 | if (!global.appid || !global.apiKey) {
178 | // 尝试从 localStorage 中获取
179 | global.appid = localStorage.getItem('xf_appid')
180 | global.apiKey = localStorage.getItem('xf_apiKey')
181 | }
182 |
183 | if (!global.appid || !global.apiKey) {
184 | const appid = prompt('[讯飞印刷文字识别] 请输入appid: ')
185 | if (appid) {
186 | global.appid = appid
187 | GM_setValue('xf_appid', appid)
188 | }
189 |
190 | const apiKey = prompt('[讯飞印刷文字识别] 请输入apiKey: ')
191 | if (apiKey) {
192 | global.apiKey = apiKey
193 | GM_setValue('xf_apiKey', apiKey)
194 | }
195 | }
196 |
197 | if (!global.appid || !global.apiKey) {
198 | return false
199 | }
200 |
201 | return true
202 | }
203 |
204 | /**
205 | * 识别并填充验证码
206 | * @param {*} image base64编码的验证码图片
207 | */
208 | async function captchAndFill(image) {
209 | // 将 base64 图片转换为讯飞识别接口所需的格式
210 | image = 'image=' + image.split('base64,')[1]
211 |
212 | if (!precheck()) {
213 | alert('初始化错误,请检查 Key 是否正确输入')
214 | }
215 |
216 | const input = document.querySelector(global.inputSelector)
217 | let inputPlaceholder = input?.placeholder
218 | inputPlaceholder && (input.placeholder = '正在识别验证码...')
219 |
220 | try {
221 | const res = await fetchWithGM(global.hostUrl, {
222 | method: 'POST',
223 | headers: getReqHeader(),
224 | data: image,
225 | responseType: 'json'
226 | })
227 | console.log('res', res)
228 |
229 | const ocrResult = res?.response?.data?.block[0]?.line[0]?.word[0]?.content
230 | if (!ocrResult) {
231 | throw new Error('ocrResult is invalid', ocrResult)
232 | }
233 |
234 | const numberResult = calcResult(ocrResult)
235 | if (numberResult === undefined) {
236 | throw new Error('numberResult is invalid', numberResult)
237 | }
238 |
239 | const target = document.querySelector(global.inputSelector)
240 | if (!target) {
241 | alert('未找到验证码输入框')
242 | return
243 | }
244 | target.value = numberResult // 填入输入框内
245 | } catch (error) {
246 | console.error(error)
247 | confirm('识别失败,点击确定重新识别') && location.reload()
248 | }
249 |
250 | input.placeholder = inputPlaceholder
251 | }
252 |
253 | ;(async () => {
254 | const base64 = await getCaptchaImage()
255 | captchAndFill(base64)
256 |
257 | // 点击验证码图片时重新识别
258 | document.querySelector(global.imgSelector).addEventListener('click', async () => {
259 | const base64 = await getCaptchaImage()
260 | captchAndFill(base64)
261 | })
262 | })()
263 |
--------------------------------------------------------------------------------
/src/userscript/websocket-hook/index.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Websocket Hook
3 | // @namespace https://github.com/ZiuChen
4 | // @version 1.0.0
5 | // @description Hook all Websocket message and log them to a global array for debugging or analysis.
6 | // @author ZiuChen
7 | // @homepage https://github.com/ZiuChen
8 | // @supportURL https://github.com/ZiuChen/userscript/issues
9 | // @match *://*/*
10 | // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0xMSA1SDhsNC00bDQgNGgtM3Y0LjQzYy0uNzUuNDYtMS40MiAxLjAzLTIgMS42OXptMTEgNmwtNC00djNhNi43NDcgNi43NDcgMCAwIDAtNyA2LjE3QTMuMDA2IDMuMDA2IDAgMCAwIDkuMTcgMjBBMy4wMDYgMy4wMDYgMCAwIDAgMTMgMjEuODNBMy4wMSAzLjAxIDAgMCAwIDE0LjgzIDE4Yy0uMy0uODYtLjk4LTEuNTMtMS44My0xLjgzYy40Ny00IDQuNDctNC4yIDQuOTUtNC4ydjN6bS0xMS4zNy41OUE3LjYzIDcuNjMgMCAwIDAgNiAxMFY3bC00IDRsNCA0di0zYzEuMzQuMDMgMi42My41IDMuNjQgMS40Yy4yNS0uNjQuNTgtMS4yNS45OS0xLjgxIi8+PC9zdmc+
11 | // @grant unsafeWindow
12 | // @run-at document-start
13 | // @updateURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/websocket-hook/index.user.js
14 | // @downloadURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/websocket-hook/index.user.js
15 | // ==/UserScript==
16 |
17 | // 日志默认关闭,设置为 true 可开启调试输出
18 | const LOG_ENABLED = false
19 |
20 | // 初始化消息记录数组
21 | const messages = []
22 |
23 | // 将消息记录数组挂载到 unsafeWindow,方便外部访问
24 | unsafeWindow.__ws_messages = unsafeWindow.__ws_messages || messages
25 |
26 | /** 保存原始 WebSocket */
27 | const OriginalWebSocket = unsafeWindow.WebSocket
28 |
29 | // 用于存储原始 listener -> 包装 listener 的映射,方便 removeEventListener
30 | const listenerMap = new WeakMap()
31 |
32 | /**
33 | * 复制静态属性从源构造函数到目标构造函数
34 | * @param {Function} targetCtor - 目标构造函数
35 | * @param {Function} sourceCtor - 源构造函数
36 | */
37 | function copyStaticProps(targetCtor, sourceCtor) {
38 | ;['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'].forEach((k) => {
39 | if (k in sourceCtor)
40 | try {
41 | targetCtor[k] = sourceCtor[k]
42 | } catch (e) {}
43 | })
44 | }
45 |
46 | /**
47 | * 将数据转换为字符串表示形式
48 | * 尽量安全,不做异步读取 Blob
49 | * @param {any} data - 要转换的数据
50 | * @returns {string} - 数据的字符串表示形式
51 | */
52 | function dataToString(data) {
53 | try {
54 | if (typeof data === 'string') return data
55 | if (data === null || data === undefined) return String(data)
56 | if (data instanceof ArrayBuffer) {
57 | const bytes = new Uint8Array(data)
58 | let binary = ''
59 | const chunkSize = 0x8000
60 | for (let i = 0; i < bytes.length; i += chunkSize) {
61 | binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize))
62 | }
63 | return 'ArrayBuffer(base64):' + btoa(binary)
64 | }
65 | if (typeof Blob !== 'undefined' && data instanceof Blob) {
66 | return `[Blob: size=${data.size} type=${data.type}]`
67 | }
68 | if (typeof data === 'object') {
69 | try {
70 | return JSON.stringify(data)
71 | } catch (e) {
72 | return Object.prototype.toString.call(data)
73 | }
74 | }
75 | return String(data)
76 | } catch (e) {
77 | try {
78 | return String(data)
79 | } catch (e2) {
80 | return '[unserializable]'
81 | }
82 | }
83 | }
84 |
85 | /**
86 | * 新的 WebSocket 构造函数
87 | * @param {string} url - WebSocket 连接的 URL
88 | * @param {string|string[]} [protocols] - 可选的子协议
89 | * @returns 原始 WebSocket 的 Proxy
90 | */
91 | function HookedWebSocket(url, protocols) {
92 | const realWs =
93 | protocols === undefined ? new OriginalWebSocket(url) : new OriginalWebSocket(url, protocols)
94 |
95 | const handler = {
96 | get(target, prop, receiver) {
97 | // 拦截 send 方法
98 | if (prop === 'send') {
99 | const originalSend = realWs.send.bind(realWs)
100 | return function (data) {
101 | // 累积到单个全局数组
102 | messages.push({
103 | type: 'sent',
104 | url: url,
105 | data,
106 | ts: Date.now()
107 | })
108 | if (LOG_ENABLED) console.debug('[WS Hook] send ->', url, data)
109 | return originalSend(data)
110 | }
111 | }
112 |
113 | // 拦截 addEventListener,以便拦截 'message' 事件 listener
114 | if (prop === 'addEventListener') {
115 | return function (type, listener, options) {
116 | if (type === 'message' && typeof listener === 'function') {
117 | const wrapped = function (event) {
118 | messages.push({
119 | type: 'recv',
120 | url: url,
121 | data: event.data,
122 | ts: Date.now()
123 | })
124 | if (LOG_ENABLED) console.debug('[WS Hook] recv <-', url, event.data)
125 | return listener.call(this, event)
126 | }
127 | listenerMap.set(listener, wrapped)
128 | return realWs.addEventListener.call(realWs, type, wrapped, options)
129 | } else {
130 | return realWs.addEventListener.call(realWs, type, listener, options)
131 | }
132 | }
133 | }
134 |
135 | // 拦截 removeEventListener,使用映射找到包装函数
136 | if (prop === 'removeEventListener') {
137 | return function (type, listener, options) {
138 | const wrapped = listenerMap.get(listener) || listener
139 | return realWs.removeEventListener.call(realWs, type, wrapped, options)
140 | }
141 | }
142 |
143 | // 拦截 onmessage 属性的读取与返回
144 | if (prop === 'onmessage') {
145 | return realWs.onmessage
146 | }
147 |
148 | // 对函数类型的属性,保证 this 绑定到真实 WebSocket 上
149 | const value = realWs[prop]
150 | if (typeof value === 'function') {
151 | return value.bind(realWs)
152 | }
153 | return value
154 | },
155 |
156 | set(target, prop, value, receiver) {
157 | // 当设置 onmessage handler 时,用包装函数包一层以便拦截接收消息
158 | if (prop === 'onmessage' && typeof value === 'function') {
159 | const wrapped = function (event) {
160 | messages.push({
161 | type: 'recv',
162 | url: url,
163 | data: event.data,
164 | ts: Date.now()
165 | })
166 | if (LOG_ENABLED) console.debug('[WS Hook] onmessage <-', url, event.data)
167 | return value.call(this, event)
168 | }
169 | listenerMap.set(value, wrapped)
170 | realWs.onmessage = wrapped
171 | return true
172 | }
173 | try {
174 | realWs[prop] = value
175 | return true
176 | } catch (e) {
177 | return false
178 | }
179 | },
180 |
181 | has(target, prop) {
182 | return prop in realWs
183 | },
184 | getOwnPropertyDescriptor(target, prop) {
185 | const desc = Object.getOwnPropertyDescriptor(realWs, prop)
186 | if (desc) return desc
187 | return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(realWs), prop)
188 | }
189 | }
190 |
191 | return new Proxy(realWs, handler)
192 | }
193 |
194 | try {
195 | HookedWebSocket.prototype = OriginalWebSocket.prototype
196 | copyStaticProps(HookedWebSocket, OriginalWebSocket)
197 |
198 | unsafeWindow.WebSocket = HookedWebSocket
199 | console.debug('[WS Hook] WebSocket hooked')
200 | } catch (e) {
201 | console.error('[WS Hook] failed to install hook', e)
202 | }
203 |
--------------------------------------------------------------------------------
/src/userscript/WeChatArticleEX/userscript.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name 微信文章浏览功能拓展
3 | // @description 快速预览/保存封面图与文章摘要以及更多
4 | // @namespace https://github.com/ZiuChen/userscript
5 | // @version 1.2.2
6 | // @author Ziu
7 | // @match *://mp.weixin.qq.com/s*
8 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/WeChatArticleEX/userscript.user.js
9 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/WeChatArticleEX/userscript.user.js
10 | // @require https://fastly.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
11 | // @require https://fastly.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js
12 | // @grant GM_registerMenuCommand
13 | // @grant GM_setValue
14 | // @grant GM_getValue
15 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
16 | // @license MIT
17 | // ==/UserScript==
18 |
19 | let summary_show_state
20 | let state = {
21 | url_state: true, // 是否启用封面图链接功能
22 | openNewWindow_state: true, // 是否在新窗口打开封面图链接
23 | datetype_state: true, // 是否修改默认时间显示模式
24 | summary_state: true, // 是否启用读取摘要功能
25 | clipboard_state: true, // 是否启用点击摘要复制到剪切板功能
26 | recommend_state: false, // 是否显示引导关注栏
27 | preview_state: true, // 封面图预览功能
28 | config_state: true // 是否默认显示设置侧边栏
29 | }
30 | // 设置框
31 | let configBox =
32 | "" +
33 | "
封面链接
" +
34 | "
文章摘要
" +
35 | "
复制链接
" +
36 | "
搜索文章
" +
37 | "
· · ·
" +
38 | "
"
39 | $("body").append(configBox)
40 | $(".unshownConfig").hide()
41 | $("#config_moreinfo").click(function () {
42 | $(".unshownConfig").fadeIn()
43 | $("#config_moreinfo").html(":D")
44 | $("#config_moreinfo").click(function () {
45 | window.open("https://greasyfork.org/zh-CN/users/605474")
46 | })
47 | })
48 | // 给设置窗口添加效果 移入透明度加深 移出透明度变浅
49 | $("#config_window").mouseenter(function () {
50 | $("#config_window").css("opacity", "1.0")
51 | $("#config_window > div").mouseenter(function () {
52 | var $this = $(this) // 把div元素转化成jQuery的对象
53 | $this.css("background-color", "#1b8262")
54 | $("#config_window > div").mouseleave(function () {
55 | $this.css("background-color", "#25ae84")
56 | })
57 | })
58 | $("#config_window>div").mousedown(function () {
59 | this.style.color = "#125540"
60 | })
61 | $("#config_window>div").mouseup(function () {
62 | this.style.color = "#FFF"
63 | })
64 | })
65 | $("#config_window").mouseleave(function () {
66 | $("#config_window").css("opacity", "0.25")
67 | })
68 | // 文章摘要
69 | $("#config_summary").click(function () {
70 | $("#summary").toggle("fast")
71 | state.summary_state = false
72 | })
73 | // 微信文章搜索
74 | $("#config_search").click(function () {
75 | let artname = $("#activity-name").text()
76 | let baseurl = "https://weixin.sogou.com/weixin?type=2&query="
77 | let trueurl = baseurl + artname
78 | if (state.openNewWindow_state === true) {
79 | window.open(trueurl)
80 | } else {
81 | window.location.href = trueurl
82 | }
83 | })
84 | // 封面图
85 | if (state.url_state === true) {
86 | let linkReg = /msg_cdn_url = "(.*)"/gi
87 | let data = $("*").html()
88 | let url = linkReg.exec(data)
89 | let trueurl = url[1]
90 | // 跳转
91 | $("#config_url").click(function () {
92 | if (state.openNewWindow_state === true) {
93 | window.open(trueurl)
94 | } else {
95 | window.location.href = trueurl
96 | }
97 | })
98 | // 预览封面图功能
99 | if (state.preview_state === true) {
100 | // 引入封面图并设置默认隐藏
101 | $("#config_url").append('
')
102 | $("#picture_url").attr("src", trueurl)
103 | $("#picture_url").css({
104 | zoom: "35%",
105 | position: "absolute",
106 | left: "140px",
107 | "border-radius": "8px",
108 | "box-shadow": "5px 5px 2px #555555"
109 | })
110 | $("#picture_url").hide()
111 | // 加入鼠标事件 实现预览功能
112 | $("#config_url").mouseenter(function () {
113 | $("#picture_url").fadeIn()
114 | })
115 | $("#config_url").mouseleave(function () {
116 | $("#picture_url").toggle()
117 | })
118 | }
119 | // 点击按钮复制封面链接到剪切板
120 | let clipboard = new ClipboardJS(".myclipboard") // 要使用 clipboard.js 需要声明一个clipboard实例
121 | $("#config_preview").attr("data-clipboard-text", trueurl)
122 | $("#config_preview").click(function () {
123 | let clipboard = new ClipboardJS("#config_preview") // 要使用 clipboard.js 需要声明一个clipboard实例
124 | alert("封面链接已复制到剪切板!")
125 | })
126 | }
127 | // 摘要
128 | if (state.summary_state === true) {
129 | let meta = $('meta[name="description"]')
130 | let contents = meta[0].content
131 | if (contents === "") {
132 | $("#config_summary").css("background-color", "#ffa758")
133 | $("#config_summary").html("摘要为空")
134 | $("#config_summary").attr("title", "此篇文章无摘要")
135 | }
136 | // 点击摘要复制到剪切板
137 | if (state.clipboard_state === true) {
138 | let clipboard = new ClipboardJS(".myclipboard") // 要使用 clipboard.js 需要声明一个clipboard实例
139 | }
140 | $("#meta_content").append(
141 | "afterend",
142 | ''
143 | )
144 | $("#summary").html("文章摘要:" + contents)
145 | $("#summary").attr("data-clipboard-text", contents)
146 | // 给摘要设置样式与效果事件
147 | $("#summary").css({ "text-align": "left", "font-size": "15px" }) // 设置摘要左居中
148 | $("#summary").mouseenter(function () {
149 | $("#summary").css("color", "#474747")
150 | })
151 | $("#summary").mousedown(function () {
152 | $("#summary").css("color", "#070707")
153 | })
154 | $("#summary").mouseup(function () {
155 | $("#summary").css("color", "#474747")
156 | $("#config_moreinfo").html("复制成功")
157 | $("#config_window").css("opacity", "1.0")
158 | $("#config_moreinfo").css("background-color", "#1b8262")
159 | })
160 | $("#summary").mouseleave(function () {
161 | $("#summary").css("color", "#b2b2b2")
162 | $("#config_window").css("opacity", "0.25")
163 | $("#config_moreinfo").css("background-color", "#25ae84")
164 | $("#config_moreinfo").html("···")
165 | })
166 | }
167 | // 设置摘要是否默认显示
168 | if (GM_getValue(summary_show_state) === false) {
169 | $("#summary").hide() // 设置默认隐藏摘要
170 | }
171 | //修改时间格式
172 | if (state.datetype_state === true) {
173 | $("#publish_time").trigger("click")
174 | }
175 | //隐藏引导关注栏
176 | if (state.recommend_state === false) {
177 | $(".qr_code_pc").hide()
178 | }
179 | // 默认显示设置侧边栏
180 | if (state.config_state === false) {
181 | $("#config_window").hide() // 默认隐藏设置框
182 | }
183 | // 给ESC按键添加事件:按下出现设置框
184 | $(document).keyup(function (event) {
185 | switch (event.keyCode) {
186 | case 27:
187 | $("#config_window").toggle()
188 | }
189 | })
190 | // 给设置菜单赋初值
191 | if (GM_getValue("summary_show_state") === null) {
192 | GM_setValue("summary_show_state", true)
193 | }
194 | // 默认显示摘要:设置菜单
195 | GM_registerMenuCommand("默认显示文章摘要", function () {
196 | if (GM_getValue(summary_show_state) === true) {
197 | GM_setValue(summary_show_state, false)
198 | alert("默认显示文章摘要:OFF")
199 | $("#summary").hide()
200 | console.log("默认显示文章摘要:" + GM_getValue(summary_show_state))
201 | return
202 | }
203 | if (GM_getValue(summary_show_state) === false) {
204 | GM_setValue(summary_show_state, true)
205 | alert("默认显示文章摘要:ON")
206 | $("#summary").show()
207 | console.log("默认显示文章摘要:" + GM_getValue(summary_show_state))
208 | return
209 | }
210 | })
211 |
--------------------------------------------------------------------------------
/src/userscript/snapshot-everything/index.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Snapshot Everything
3 | // @namespace https://github.com/ZiuChen
4 | // @version 1.0.1
5 | // @description Take snapshot on any site for any DOM.
6 | // @author ZiuChen
7 | // @homepage https://github.com/ZiuChen
8 | // @supportURL https://github.com/ZiuChen/userscript/issues
9 | // @match *://*/*
10 | // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik00IDRoM2wyLTJoNmwyIDJoM2EyIDIgMCAwIDEgMiAydjEyYTIgMiAwIDAgMS0yIDJINGEyIDIgMCAwIDEtMi0yVjZhMiAyIDAgMCAxIDItMm04IDNhNSA1IDAgMCAwLTUgNWE1IDUgMCAwIDAgNSA1YTUgNSAwIDAgMCA1LTVhNSA1IDAgMCAwLTUtNW0wIDJhMyAzIDAgMCAxIDMgM2EzIDMgMCAwIDEtMyAzYTMgMyAwIDAgMS0zLTNhMyAzIDAgMCAxIDMtMyIvPjwvc3ZnPg==
11 | // @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
12 | // @updateURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/snapshot-everything/index.user.js
13 | // @downloadURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/snapshot-everything/index.user.js
14 | // @grant GM_registerMenuCommand
15 | // @grant GM_notification
16 | // @grant GM_info
17 | // ==/UserScript==
18 |
19 | const USERSCRIPT_NAME = GM_info.script.name
20 |
21 | GM_registerMenuCommand('Take Snapshot', async () => {
22 | try {
23 | const element = await inspectElement()
24 | if (!element) {
25 | throw new Error('No element selected')
26 | }
27 |
28 | const blob = await snapshot(element)
29 | if (blob) {
30 | downloadBlob(blob, `SnapshotEverything_${new Date().toISOString().slice(0, 10)}.png`)
31 | }
32 | } catch (error) {
33 | GM_notification({
34 | title: `${USERSCRIPT_NAME} Error`,
35 | text: error.message,
36 | timeout: 3000
37 | })
38 | throw error
39 | }
40 | })
41 |
42 | async function inspectElement() {
43 | return new Promise((resolve, reject) => {
44 | try {
45 | const inspector = new DOMInspector({
46 | color: '#4b9bfa',
47 | borderWidth: '1px',
48 | showSize: true,
49 | onSelect: ({ element }) => {
50 | inspector.stop()
51 | resolve(element)
52 | }
53 | })
54 |
55 | // 开始检查
56 | inspector.start()
57 | } catch (error) {
58 | reject(error)
59 | }
60 | })
61 | }
62 |
63 | async function snapshot(dom) {
64 | const preferredTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
65 | ? 'dark'
66 | : 'light'
67 |
68 | const canvas = await html2canvas(dom, {
69 | scale: 3,
70 | backgroundColor: preferredTheme === 'dark' ? '#000' : '#fff',
71 | useCORS: true,
72 | logging: false
73 | })
74 |
75 | if (!canvas) {
76 | console.error('Failed to create canvas')
77 | return
78 | }
79 |
80 | const toBlob = async (canvas) => {
81 | return new Promise((resolve) => {
82 | canvas.toBlob((blob) => {
83 | resolve(blob)
84 | }, 'image/png')
85 | })
86 | }
87 |
88 | // Convert canvas to blob
89 | const blob = await toBlob(canvas)
90 | if (!blob) {
91 | console.error('Failed to convert canvas to blob')
92 | return
93 | }
94 |
95 | return blob
96 | }
97 |
98 | function copyImageToClipboard(blob) {
99 | const item = new ClipboardItem({
100 | 'image/png': blob
101 | })
102 |
103 | navigator.clipboard.write([item]).then(
104 | () => {
105 | console.log('Image copied to clipboard')
106 | },
107 | (error) => {
108 | console.error('Failed to copy image to clipboard:', error)
109 | }
110 | )
111 | }
112 |
113 | function downloadBlob(blob, filename) {
114 | const url = URL.createObjectURL(blob)
115 | const a = document.createElement('a')
116 | a.href = url
117 | a.download = filename
118 | document.body.appendChild(a)
119 | a.click()
120 | setTimeout(() => {
121 | URL.revokeObjectURL(url)
122 | document.body.removeChild(a)
123 | }, 100)
124 | }
125 |
126 | class DOMInspector {
127 | constructor(options = {}) {
128 | this.options = {
129 | color: '#f00',
130 | borderWidth: '2px',
131 | borderRadius: '4px',
132 | zIndex: 9999,
133 | showSize: true,
134 | ...options
135 | }
136 |
137 | this.currentElement = null
138 | this.overlay = null
139 | this.sizeInfo = null
140 | this.isInspecting = false
141 |
142 | this.init()
143 | }
144 |
145 | init() {
146 | this.createOverlay()
147 | this.createSizeInfo()
148 | }
149 |
150 | createOverlay() {
151 | this.overlay = document.createElement('div')
152 | Object.assign(this.overlay.style, {
153 | position: 'fixed',
154 | pointerEvents: 'none',
155 | border: `${this.options.borderWidth} solid ${this.options.color}`,
156 | borderRadius: this.options.borderRadius,
157 | backgroundColor: `${this.options.color}20`,
158 | zIndex: this.options.zIndex,
159 | display: 'none'
160 | })
161 | document.body.appendChild(this.overlay)
162 | }
163 |
164 | createSizeInfo() {
165 | if (!this.options.showSize) return
166 |
167 | this.sizeInfo = document.createElement('div')
168 | Object.assign(this.sizeInfo.style, {
169 | position: 'fixed',
170 | pointerEvents: 'none',
171 | backgroundColor: 'rgba(0, 0, 0, 0.7)',
172 | color: '#fff',
173 | fontSize: '12px',
174 | padding: '4px 8px',
175 | borderRadius: '4px',
176 | zIndex: this.options.zIndex + 1,
177 | display: 'none',
178 | fontFamily: 'monospace'
179 | })
180 | document.body.appendChild(this.sizeInfo)
181 | }
182 |
183 | start() {
184 | if (this.isInspecting) return
185 | this.isInspecting = true
186 |
187 | document.addEventListener('mousemove', this.handleMouseMove)
188 | document.addEventListener('click', this.handleClick, true)
189 | document.addEventListener('keydown', this.handleKeyDown)
190 | }
191 |
192 | stop() {
193 | if (!this.isInspecting) return
194 | this.isInspecting = false
195 |
196 | document.removeEventListener('mousemove', this.handleMouseMove)
197 | document.removeEventListener('click', this.handleClick, true)
198 | document.removeEventListener('keydown', this.handleKeyDown)
199 |
200 | this.overlay.style.display = 'none'
201 | if (this.sizeInfo) this.sizeInfo.style.display = 'none'
202 | this.currentElement = null
203 | }
204 |
205 | handleMouseMove = (e) => {
206 | const element = document.elementFromPoint(e.clientX, e.clientY)
207 | if (!element || element === this.currentElement) return
208 |
209 | this.currentElement = element
210 | this.highlightElement(element)
211 | }
212 |
213 | highlightElement(element) {
214 | const rect = element.getBoundingClientRect()
215 |
216 | // 设置高亮框位置和尺寸
217 | Object.assign(this.overlay.style, {
218 | display: 'block',
219 | left: `${rect.left}px`,
220 | top: `${rect.top}px`,
221 | width: `${rect.width}px`,
222 | height: `${rect.height}px`
223 | })
224 |
225 | // 显示尺寸信息
226 | if (this.sizeInfo) {
227 | Object.assign(this.sizeInfo.style, {
228 | display: 'block',
229 | left: `${rect.left}px`,
230 | top: `${rect.top - 30}px`,
231 | content: `${rect.width} × ${rect.height}`
232 | })
233 | this.sizeInfo.textContent = `${Math.round(rect.width)} × ${Math.round(rect.height)}`
234 | }
235 | }
236 |
237 | handleClick = (e) => {
238 | e.preventDefault()
239 | e.stopPropagation()
240 | e.stopImmediatePropagation()
241 |
242 | if (this.options.onSelect) {
243 | const selector = this.getElementSelector(this.currentElement)
244 | this.options.onSelect({
245 | element: this.currentElement,
246 | selector
247 | })
248 | }
249 | }
250 |
251 | handleKeyDown = (e) => {
252 | if (e.key === 'Escape') {
253 | this.stop()
254 | }
255 | }
256 |
257 | getElementSelector(element) {
258 | if (!element || !element.tagName) return ''
259 |
260 | const path = []
261 | let current = element
262 |
263 | while (current && current !== document.body) {
264 | let selector = current.tagName.toLowerCase()
265 |
266 | if (current.id) {
267 | selector += `#${current.id}`
268 | path.unshift(selector)
269 | break
270 | } else {
271 | let nth = 1
272 | let sibling = current.previousElementSibling
273 |
274 | while (sibling) {
275 | if (sibling.tagName === current.tagName) nth++
276 | sibling = sibling.previousElementSibling
277 | }
278 |
279 | if (nth !== 1) selector += `:nth-of-type(${nth})`
280 | }
281 |
282 | path.unshift(selector)
283 | current = current.parentElement
284 | }
285 |
286 | return path.join(' > ')
287 | }
288 |
289 | destroy() {
290 | this.stop()
291 | if (this.overlay && this.overlay.parentNode) {
292 | this.overlay.parentNode.removeChild(this.overlay)
293 | }
294 | if (this.sizeInfo && this.sizeInfo.parentNode) {
295 | this.sizeInfo.parentNode.removeChild(this.sizeInfo)
296 | }
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/src/userscript/BJTUCourse/userscript.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name BJTU抢课脚本
3 | // @description 北京交通大学抢课脚本
4 | // @version 0.0.3
5 | // @author Ziu
6 | // @source https://github.com/ZiuChen/userscript
7 | // @supportURL https://github.com/ZiuChen/userscript
8 | // @license MIT
9 | // @match https://aa.bjtu.edu.cn/course_selection/*
10 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/BJTUCourse/userscript.user.js
11 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/BJTUCourse/userscript.user.js
12 | // @namespace https://github.com/ZiuChen/userscript
13 | // @require https://fastly.jsdelivr.net/npm/table-to-json@1.0.0/lib/jquery.tabletojson.min.js
14 | // @connect pushplus.plus
15 | // @grant GM_notification
16 | // @grant GM_xmlhttpRequest
17 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
18 | // ==/UserScript==
19 |
20 | // 使用前请完整阅读 README.md
21 | const config = {
22 | simpleMatchPatterns: [], // (可选) 简单匹配 课程名 支持泛选
23 | advanceMatchPatterns: [], // (可选) 高级匹配 匹配表格内的任意属性 支持泛选
24 | advanceQuery: '', // (可选) 查询字符串 指定需要检索的数据源
25 | pushPlusToken: '', // (可选) PushPlusToken
26 | timeout: 2500 // (必须) 检索时间间隔(ms)
27 | }
28 |
29 | const GlobalTasks = [] // 代码环境下的匹配任务
30 |
31 | const URL = {
32 | selectPage: 'https://aa.bjtu.edu.cn/course_selection/courseselecttask/selects/',
33 | _base: 'https://aa.bjtu.edu.cn/course_selection/courseselecttask/selects_action/?action=',
34 | _main: 'load',
35 | _submit: 'submit',
36 | _delete: 'delete',
37 | _advanceQuery: config.advanceQuery || '',
38 | get main() {
39 | const suffix = '&iframe=school&page=1&perpage=1000'
40 | return this._base + this._main + this._advanceQuery + suffix
41 | },
42 | get submit() {
43 | return this._base + this._submit
44 | },
45 | get delete() {
46 | return this._base + this._delete
47 | }
48 | }
49 |
50 | const log = (content, type) => {
51 | const cMap = { success: 'green', warning: 'orange', error: 'red', info: 'blue', none: 'grey' }
52 | const color = cMap[type] === undefined ? cMap['none'] : cMap[type]
53 | const time = new Date().toLocaleTimeString()
54 | return console.log(`%c[${time}]` + `%c ${content}`, 'color: #005bac;', `color: ${color};`)
55 | }
56 |
57 | const parse2DOM = (text, selector) => {
58 | const p = new DOMParser()
59 | return p.parseFromString(text, 'text/html')?.querySelector(selector)
60 | }
61 |
62 | const parse2Json = (text) => {
63 | const table = parse2DOM(text, 'table')
64 | const toStatus = (string) => {
65 | if (string.indexOf('已选') !== -1) return 1 // 已选中
66 | else if (string.indexOf('无余量') !== -1) return -1 // 无余量
67 | else return 0 // 未选中
68 | }
69 | return $(table).tableToJSON({
70 | ignoreHiddenRows: false,
71 | headings: ['info', 'index', 'cname', 'remain', 'credit', 'type', 'tname', 'tap', 'more'],
72 | extractor: (cellIndex, $cell) => {
73 | return cellIndex === 0
74 | ? { status: toStatus($cell.text()), id: $cell.find('input').attr('value') }
75 | : $cell.text()
76 | }
77 | })
78 | }
79 |
80 | const req = async (url, options = {}) => {
81 | return fetch(url, { ...options }).catch((err) => {
82 | log('请求出错: ' + err)
83 | })
84 | }
85 |
86 | const submit = async (id) => {
87 | const f = new FormData()
88 | f.append('checkboxs', id) // 验证码: hashkey & answer
89 | return req(URL.submit, {
90 | method: 'POST',
91 | body: f
92 | })
93 | }
94 |
95 | const remove = async (id) => {
96 | const f = new FormData()
97 | f.append('select_id', id)
98 | return req(URL.delete, {
99 | method: 'POST',
100 | body: f
101 | })
102 | }
103 |
104 | const XHR = (XHROptions) => {
105 | // 仅用于跨域请求
106 | return new Promise((resolve) => {
107 | const onerror = (error) => resolve(undefined)
108 | XHROptions.timeout = 30 * 1000
109 | XHROptions.onload = (res) => resolve(res.response)
110 | XHROptions.onerror = onerror
111 | XHROptions.ontimeout = onerror
112 | GM_xmlhttpRequest(XHROptions)
113 | })
114 | }
115 |
116 | const sendMsg = async (info) => {
117 | config?.pushPlusToken &&
118 | XHR({
119 | anonymous: true,
120 | method: 'POST',
121 | url: `http://www.pushplus.plus/send`,
122 | data: JSON.stringify({
123 | token: config?.pushPlusToken,
124 | title: '抢课成功',
125 | content: info
126 | }),
127 | responseType: 'json'
128 | }).then(({ code, msg, data }) =>
129 | log(`PushPlus推送结果: [${code}] ${msg} 消息流水号: ${data}`, 'success')
130 | )
131 | GM_notification({
132 | title: '抢课成功',
133 | text: info,
134 | timeout: 10000,
135 | highlight: true
136 | })
137 | }
138 |
139 | const fetchSelectedTable = async () => {
140 | return req(URL.selectPage)
141 | .then((res) => res.text())
142 | .then((text) => parse2DOM(text, '.table'))
143 | .then((tableDOM) =>
144 | $(tableDOM).tableToJSON({
145 | ignoreHiddenRows: false,
146 | headings: ['id', 'cname', 'remain', 'credit', 'type', 'cstatus', 'tname', 'tap'],
147 | extractor: (cellIndex, $cell) => {
148 | return cellIndex === 0 ? $cell.find('a').attr('data-pk') : $cell.text()
149 | }
150 | })
151 | )
152 | }
153 |
154 | const modifyTask = (p) => {
155 | const index = GlobalTasks.indexOf(p)
156 | GlobalTasks[index].throw = undefined
157 | }
158 |
159 | const removeTask = (p) => {
160 | const index = GlobalTasks.indexOf(p)
161 | GlobalTasks.splice(index, 1)
162 | }
163 |
164 | const handleCourseThrow = async (p) => {
165 | const selectTable = await fetchSelectedTable()
166 | let flg = false
167 | for (const sc of selectTable) {
168 | if (sc.cname.indexOf(p.throw) !== -1) {
169 | flg = true
170 | remove(sc.id) // 抛掉 `throw` 指定的课程
171 | modifyTask(p)
172 | }
173 | }
174 | if (!flg) log('未匹配到throw课程', 'error')
175 | }
176 |
177 | const trialSubmit = async (c, p) => {
178 | const info = `[${c.cname} ${c.tname}] `
179 | const status = c.info.status
180 | if (status === 0) {
181 | log(info + '有余量, 发起抢课', 'warning')
182 | submit(c.info.id)
183 | } else if (status === -1) {
184 | // 无余量
185 | } else {
186 | if (!p?.throw) {
187 | log(info + '抢课成功', 'error')
188 | sendMsg(info)
189 | removeTask(p) // 移除当前匹配任务
190 | } else {
191 | // 配置了 `throw`属性 需要先抛课再执行抢课
192 | log(info + '抛出指定课堂', 'error')
193 | handleCourseThrow(p)
194 | }
195 | }
196 | }
197 |
198 | const isMatched = (c, p) => {
199 | const flags = []
200 | for (const [key, value] of Object.entries(p)) {
201 | // 遍历 pattern 的每个属性
202 | if (key === 'info') continue
203 | if (key === 'throw') continue
204 | flags.push(c[key].indexOf(value) !== -1)
205 | }
206 | return flags.indexOf(false) === -1
207 | }
208 |
209 | const advanceMatch = async (table, tasks) => {
210 | for (const c of table) {
211 | // 遍历 Table 中每个课程
212 | for (const p of tasks) {
213 | // 遍历 tasks 中每个匹配规则
214 | isMatched(c, p) && trialSubmit(c, p)
215 | }
216 | }
217 | }
218 |
219 | const toAdvancePattern = (matchNameTable) => {
220 | const ptns = []
221 | for (const cname of matchNameTable) {
222 | ptns.push({ cname })
223 | }
224 | return ptns
225 | }
226 |
227 | const cache = {
228 | set: (key, value) => window?.localStorage.setItem(key, JSON.stringify(value)),
229 | get: (key) => JSON.parse(window?.localStorage.getItem(key))
230 | }
231 |
232 | const addMatchTask = () => {
233 | const flgs = [config.advanceMatchPatterns.length, config.simpleMatchPatterns.length]
234 | const ptns = flgs[0]
235 | ? config.advanceMatchPatterns
236 | : flgs[1]
237 | ? toAdvancePattern(config.simpleMatchPatterns)
238 | : toAdvancePattern(cache.get('simple-match'))
239 | GlobalTasks.push(...ptns)
240 | }
241 |
242 | const start = () => {
243 | let count = 0
244 | addMatchTask()
245 | const fun = async () => {
246 | if (!GlobalTasks.length) {
247 | log('任务列表为空, 本次抢课结束', 'success')
248 | return clearInterval(interval)
249 | }
250 | log(`检索第${++count}次`)
251 | return req(URL.main)
252 | .then((res) => res.text())
253 | .then((text) => parse2Json(text))
254 | .then((table) => advanceMatch(table, GlobalTasks))
255 | }
256 | fun()
257 | const interval = setInterval(fun, config.timeout || 2500)
258 | }
259 |
260 | const viewInit = () => {
261 | const div = document.createElement('div')
262 | const icon = document.createElement('i')
263 | const span1 = document.createElement('span')
264 | const input1 = document.createElement('input')
265 | const startBtn = document.createElement('button')
266 | const pauseBtn = document.createElement('button')
267 | const childs = [icon, span1, input1, startBtn, pauseBtn]
268 | icon.className = 'fa fa-question bigger-125'
269 | icon.title = '如有多个则以英文逗号 , 分隔 \n更多信息, 请见README'
270 | icon.click = () => window.open()
271 | startBtn.className = 'btn btn-mini btn-danger'
272 | startBtn.innerText = '开始抢课'
273 | startBtn.onclick = start
274 | pauseBtn.className = 'btn btn-mini btn-success'
275 | pauseBtn.innerText = '中止抢课'
276 | pauseBtn.onclick = () => window.location.reload()
277 | span1.innerText = '简单匹配'
278 | input1.id = 'simple-match'
279 | input1.placeholder = '简单匹配'
280 | input1.style['width'] = '500px'
281 | input1.value = cache.get('simple-match')
282 | input1.onchange = (e) => cache.set('simple-match', e.target.value.split(','))
283 | div.style = 'display: flex; justify-content: flex-end; align-items: center;'
284 | childs.forEach((c) => (c.style['margin-right'] = '5px'))
285 | div.append(...childs)
286 | document.querySelector('form')?.after(div)
287 | }
288 |
289 | const main = () => {
290 | viewInit()
291 | }
292 |
293 | main()
294 |
--------------------------------------------------------------------------------
/src/userscript/duplicate-tab-cleaner/index.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Duplicate Tab Cleaner
3 | // @namespace https://github.com/ZiuChen/userscript
4 | // @version 1.0.0
5 | // @description Detects and cleans duplicate tabs (identified by URL), supports triggering cleanup via menu command.
6 | // @description:zh 检测并清理重复的标签页(通过 URL 识别),支持通过菜单命令触发清理操作。
7 | // @author ZiuChen
8 | // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik03LjUgMTFDNSAxMSAzIDEzIDMgMTUuNWMwIC44OC4yNSAxLjcxLjY5IDIuNEwuNjEgMjFMMiAyMi4zOWwzLjEyLTMuMDdjLjY5LjQzIDEuNTEuNjggMi4zOC42OGMyLjUgMCA0LjUtMiA0LjUtNC41UzEwIDExIDcuNSAxMW0wIDdhMi41IDIuNSAwIDAgMSAwLTVhMi41IDIuNSAwIDAgMSAwIDVNMjMgNXYxNGMwIDEuMTEtLjg5IDItMiAySDEwLjk1Yy44MS0uNSAxLjUtMS4xOSAyLjAyLTJIMjFWOWgtOFY1SDN2NS44MkMxLjc3IDEyIDEgMTMuNjYgMSAxNS41VjVjMC0xLjEuOS0yIDItMmgxOGEyIDIgMCAwIDEgMiAyIi8+PC9zdmc+
9 | // @match *://*/*
10 | // @grant GM.getTab
11 | // @grant GM.saveTab
12 | // @grant GM.getTabs
13 | // @grant GM.setValue
14 | // @grant GM.addValueChangeListener
15 | // @grant GM.getValue
16 | // @grant GM.addStyle
17 | // @grant GM_registerMenuCommand
18 | // @run-at document-idle
19 | // @updateURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/duplicate-tab-cleaner/index.user.js
20 | // @downloadURL https://cdn.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/duplicate-tab-cleaner/index.user.js
21 | // ==/UserScript==
22 |
23 | ;(async function () {
24 | 'use strict'
25 |
26 | // Compatibility wrappers (supports GM.* and legacy GM_* APIs)
27 | const gm = {
28 | async getTab() {
29 | if (typeof GM !== 'undefined' && GM.getTab) return await GM.getTab()
30 | return await new Promise((resolve) => GM_getTab(resolve))
31 | },
32 | async saveTab(tab) {
33 | if (typeof GM !== 'undefined' && GM.saveTab) return await GM.saveTab(tab)
34 | if (typeof GM_saveTab === 'function')
35 | return await new Promise((resolve) => GM_saveTab(tab, resolve))
36 | return
37 | },
38 | async getTabs() {
39 | if (typeof GM !== 'undefined' && GM.getTabs) return await GM.getTabs()
40 | return await new Promise((resolve) => GM_getTabs(resolve))
41 | },
42 | async setValue(key, val) {
43 | if (typeof GM !== 'undefined' && GM.setValue) return await GM.setValue(key, val)
44 | return await new Promise((resolve) => GM_setValue(key, val).then(resolve).catch(resolve))
45 | },
46 | addValueChangeListener(key, cb) {
47 | if (typeof GM !== 'undefined' && GM.addValueChangeListener)
48 | return GM.addValueChangeListener(key, cb)
49 | if (typeof GM_addValueChangeListener === 'function') return GM_addValueChangeListener(key, cb)
50 | return null
51 | },
52 | async getValue(key, def) {
53 | if (typeof GM !== 'undefined' && GM.getValue) return await GM.getValue(key, def)
54 | return await new Promise((resolve) => resolve(GM_getValue ? GM_getValue(key, def) : def))
55 | },
56 | registerMenuCommand(name, fn, accessKey) {
57 | try {
58 | if (typeof GM !== 'undefined' && GM.registerMenuCommand)
59 | return GM.registerMenuCommand(name, fn, accessKey)
60 | } catch (_) {}
61 | if (typeof GM_registerMenuCommand === 'function')
62 | return GM_registerMenuCommand(name, fn, accessKey)
63 | return null
64 | },
65 | addStyle(css) {
66 | try {
67 | if (typeof GM !== 'undefined' && GM.addStyle) return GM.addStyle(css)
68 | } catch (e) {}
69 | const s = document.createElement('style')
70 | s.textContent = css
71 | document.head.appendChild(s)
72 | }
73 | }
74 |
75 | // Config
76 | const CONFIG = {
77 | closeRequestPrefix: 'dupe-close:',
78 | keepFlagKey: 'dupe-keep',
79 | normalizeOptions: {
80 | removeHash: true,
81 | removeUtm: true,
82 | removeTrailingSlash: true
83 | },
84 | banner: {
85 | id: 'dupe-close-banner',
86 | autoRedirectDelayMs: 8000,
87 | redirectUrl: 'about:blank'
88 | }
89 | }
90 |
91 | // Language detection: prefer Simplified Chinese for zh-CN / zh-Hans, otherwise English
92 | function detectLang() {
93 | const langs = (navigator.languages && navigator.languages.join(',')) || navigator.language || ''
94 | const norm = langs.toLowerCase()
95 | // If explicit zh-CN or zh-hans present -> simplified
96 | if (/zh-(cn|hans?)/i.test(norm)) return 'zh'
97 | // If only zh present but no TW/HK/Hant -> treat as simplified
98 | if (/(^|,|\b)zh($|,|\b)/.test(norm) && !/(zh-(tw|hk|hant))/i.test(norm)) return 'zh'
99 | return 'en'
100 | }
101 | const LANG = detectLang()
102 |
103 | const M = {
104 | en: {
105 | menu: 'Clean duplicate tabs (dupe-cleaner)',
106 | noDuplicates: 'No duplicate tabs found for this URL.',
107 | noDuplicatesLosers: 'No duplicate tabs to close (losers excluded by keep flag).',
108 | foundConfirm: (sameCount, losersCount) =>
109 | `Found ${sameCount} tabs with same normalized URL.\nWill request ${losersCount} tab(s) to close. Proceed?`,
110 | sent: 'Close requests sent to duplicates (they will try to close themselves).',
111 | bannerMessage: 'This tab is a duplicate and was requested to close. Choose an action:',
112 | fallbackBannerMessage:
113 | 'Duplicate detected — cannot close programmatically. Click "Close now" or "Keep here".',
114 | btnCloseNow: 'Close now',
115 | btnKeepHere: 'Keep here',
116 | btnCloseRedirect: 'Close & Redirect',
117 | confirmOk: 'OK',
118 | confirmCancel: 'Cancel'
119 | },
120 | zh: {
121 | menu: '清理重复标签(dupe-cleaner)',
122 | noDuplicates: '未发现与当前 URL 重复的标签页。',
123 | noDuplicatesLosers: '没有需要关闭的重复标签(某些标签被标记为保留)。',
124 | foundConfirm: (sameCount, losersCount) =>
125 | `检测到 ${sameCount} 个相同(规范化后)URL 的标签页。\n将请求 ${losersCount} 个标签页自行关闭。继续?`,
126 | sent: '已向重复标签发送关闭请求(它们会尝试自行关闭)。',
127 | bannerMessage: '此标签为重复,已请求关闭。请选择操作:',
128 | fallbackBannerMessage: '检测到重复——无法以编程方式关闭。请点击“立即关闭”或“保留此页”。',
129 | btnCloseNow: '立即关闭',
130 | btnKeepHere: '保留此页',
131 | btnCloseRedirect: '关闭并跳转',
132 | confirmOk: '确定',
133 | confirmCancel: '取消'
134 | }
135 | }
136 |
137 | function t(k, ...args) {
138 | const dict = M[LANG] || M.en
139 | const v = dict[k]
140 | if (typeof v === 'function') return v(...args)
141 | return v
142 | }
143 |
144 | // Utilities
145 | function normalizeUrl(raw) {
146 | try {
147 | const u = new URL(raw, location.href)
148 | if (CONFIG.normalizeOptions.removeHash) u.hash = ''
149 | if (CONFIG.normalizeOptions.removeTrailingSlash) {
150 | if (u.pathname.length > 1 && u.pathname.endsWith('/')) {
151 | u.pathname = u.pathname.replace(/\/+$/, '')
152 | }
153 | }
154 | if (CONFIG.normalizeOptions.removeUtm) {
155 | const remove = []
156 | for (const k of u.searchParams.keys()) {
157 | if (/^utm_/i.test(k)) remove.push(k)
158 | }
159 | for (const k of remove) u.searchParams.delete(k)
160 | }
161 | return u.toString()
162 | } catch (e) {
163 | return raw
164 | }
165 | }
166 |
167 | function makeCloseKey(tabId) {
168 | return CONFIG.closeRequestPrefix + tabId
169 | }
170 |
171 | function addBanner(message, actions = []) {
172 | if (document.getElementById(CONFIG.banner.id)) return
173 | const style = `
174 | #${CONFIG.banner.id} {
175 | position: fixed;
176 | top: 6px;
177 | left: 50%;
178 | transform: translateX(-50%);
179 | background: rgba(20,20,20,0.92);
180 | color: #fff;
181 | padding: 10px 14px;
182 | border-radius: 6px;
183 | z-index: 999999;
184 | box-shadow: 0 4px 12px rgba(0,0,0,0.3);
185 | font-family: sans-serif;
186 | font-size: 13px;
187 | }
188 | #${CONFIG.banner.id} button {
189 | margin-left: 8px;
190 | background: #fff;
191 | color: #111;
192 | border: none;
193 | padding: 6px 8px;
194 | border-radius: 4px;
195 | cursor: pointer;
196 | }
197 | `
198 | gm.addStyle(style)
199 | const div = document.createElement('div')
200 | div.id = CONFIG.banner.id
201 | div.innerHTML = `${message}`
202 | for (const act of actions) {
203 | const b = document.createElement('button')
204 | b.textContent = act.label
205 | b.addEventListener('click', act.onClick)
206 | div.appendChild(b)
207 | }
208 | const closeX = document.createElement('button')
209 | closeX.textContent = '✕'
210 | closeX.style.marginLeft = '10px'
211 | closeX.addEventListener('click', () => {
212 | const el = document.getElementById(CONFIG.banner.id)
213 | if (el) el.remove()
214 | })
215 | div.appendChild(closeX)
216 | document.documentElement.appendChild(div)
217 | return div
218 | }
219 |
220 | // State
221 | let myTab = null
222 | let myId = null
223 | const normalized = normalizeUrl(location.href)
224 | let isClosingInProgress = false
225 |
226 | // Register tab info once at load
227 | async function registerTab() {
228 | myTab = await gm.getTab()
229 | if (!myTab) myTab = {}
230 | myTab.url = normalized
231 | myTab.ts = Date.now()
232 | if (typeof myTab[CONFIG.keepFlagKey] === 'undefined') myTab[CONFIG.keepFlagKey] = false
233 | await gm.saveTab(myTab)
234 |
235 | // find myId by comparing references
236 | const tabs = await gm.getTabs()
237 | for (const [id, t] of Object.entries(tabs || {})) {
238 | if (t && t === myTab) {
239 | myId = id
240 | break
241 | }
242 | }
243 | // heuristic fallback
244 | if (!myId) {
245 | for (const [id, t] of Object.entries(tabs || {})) {
246 | if (
247 | t &&
248 | t.url === myTab.url &&
249 | typeof t.ts !== 'undefined' &&
250 | Math.abs((t.ts || 0) - myTab.ts) < 2000
251 | ) {
252 | myId = id
253 | break
254 | }
255 | }
256 | }
257 | }
258 |
259 | // cleanup on unload
260 | async function cleanupOnUnload() {
261 | window.addEventListener('beforeunload', async () => {
262 | try {
263 | if (!myTab) myTab = await gm.getTab()
264 | if (myTab) {
265 | myTab.url = null
266 | myTab.ts = Date.now()
267 | myTab._closedByScript = true
268 | await gm.saveTab(myTab)
269 | }
270 | } catch (e) {
271 | /* ignore */
272 | }
273 | })
274 | }
275 |
276 | // evaluate duplicates and request close (invoked on-demand)
277 | async function evaluateAndRequestClose() {
278 | // refresh our tab info first
279 | try {
280 | if (!myTab) myTab = await gm.getTab()
281 | myTab.ts = Date.now()
282 | await gm.saveTab(myTab)
283 | } catch (e) {
284 | /* ignore */
285 | }
286 |
287 | const tabs = await gm.getTabs()
288 | const same = []
289 | for (const [id, t] of Object.entries(tabs || {})) {
290 | if (!t) continue
291 | if (t.url === normalized) {
292 | same.push({ id, tab: t, ts: t.ts || 0, keep: !!t[CONFIG.keepFlagKey] })
293 | }
294 | }
295 | if (same.length <= 1) {
296 | alert(t('noDuplicates'))
297 | return
298 | }
299 |
300 | // choose winner: prefer any with keep flag, else current visible tab, else earliest ts
301 | let winnerId = null
302 | const keepEntry = same.find((s) => s.keep)
303 | if (keepEntry) winnerId = keepEntry.id
304 | if (!winnerId) {
305 | if (document.visibilityState === 'visible' && myId) {
306 | const own = same.find((s) => s.id === myId)
307 | if (own) winnerId = myId
308 | }
309 | }
310 | if (!winnerId) {
311 | same.sort((a, b) => (a.ts || 0) - (b.ts || 0))
312 | winnerId = same[0].id
313 | }
314 |
315 | const losers = same.filter((s) => s.id !== winnerId && !s.keep)
316 | if (losers.length === 0) {
317 | alert(t('noDuplicatesLosers'))
318 | return
319 | }
320 |
321 | // confirm with user
322 | const doIt = confirm(t('foundConfirm', same.length, losers.length))
323 | if (!doIt) return
324 |
325 | // send close requests
326 | for (const ent of losers) {
327 | const key = makeCloseKey(ent.id)
328 | const payload = {
329 | by: winnerId,
330 | ts: Date.now(),
331 | url: normalized
332 | }
333 | try {
334 | await gm.setValue(key, JSON.stringify(payload))
335 | } catch (e) {
336 | /* ignore */
337 | }
338 | }
339 | alert(t('sent'))
340 | }
341 |
342 | // on receiving a close request targeted at this tab
343 | async function onCloseRequest(payload) {
344 | if (isClosingInProgress) return
345 | isClosingInProgress = true
346 |
347 | // respect keep flag
348 | if (myTab && myTab[CONFIG.keepFlagKey]) {
349 | isClosingInProgress = false
350 | return
351 | }
352 |
353 | try {
354 | window.close()
355 | await new Promise((res) => setTimeout(res, 500))
356 | // still open: show fallback banner
357 | if (!document.hidden) {
358 | addBanner(t('bannerMessage'), [
359 | {
360 | label: t('btnCloseNow'),
361 | onClick: () => {
362 | try {
363 | window.close()
364 | } catch (e) {}
365 | }
366 | },
367 | {
368 | label: t('btnKeepHere'),
369 | onClick: async () => {
370 | myTab[CONFIG.keepFlagKey] = true
371 | await gm.saveTab(myTab)
372 | const el = document.getElementById(CONFIG.banner.id)
373 | if (el) el.remove()
374 | }
375 | },
376 | {
377 | label: t('btnCloseRedirect'),
378 | onClick: () => {
379 | location.href = CONFIG.banner.redirectUrl
380 | }
381 | }
382 | ])
383 | if (CONFIG.banner.autoRedirectDelayMs > 0) {
384 | setTimeout(() => {
385 | if (
386 | document.getElementById(CONFIG.banner.id) &&
387 | !(myTab && myTab[CONFIG.keepFlagKey])
388 | ) {
389 | location.href = CONFIG.banner.redirectUrl
390 | }
391 | }, CONFIG.banner.autoRedirectDelayMs)
392 | }
393 | }
394 | } catch (e) {
395 | addBanner(t('fallbackBannerMessage'), [
396 | {
397 | label: t('btnCloseNow'),
398 | onClick: () => {
399 | try {
400 | window.close()
401 | } catch (ex) {}
402 | }
403 | },
404 | {
405 | label: t('btnKeepHere'),
406 | onClick: async () => {
407 | myTab[CONFIG.keepFlagKey] = true
408 | await gm.saveTab(myTab)
409 | const el = document.getElementById(CONFIG.banner.id)
410 | if (el) el.remove()
411 | }
412 | }
413 | ])
414 | } finally {
415 | isClosingInProgress = false
416 | }
417 | }
418 |
419 | // setup listener for close requests targeted at this tab
420 | function setupCloseListener() {
421 | if (!myId) return
422 | const key = makeCloseKey(myId)
423 | gm.addValueChangeListener(key, async (name, oldV, newV) => {
424 | if (!newV) return
425 | let payload = null
426 | try {
427 | payload = JSON.parse(newV)
428 | } catch (e) {
429 | payload = { raw: newV }
430 | }
431 | await onCloseRequest(payload)
432 | })
433 | }
434 |
435 | // register menu command to let user trigger cleaning on demand
436 | function registerCommand() {
437 | gm.registerMenuCommand(
438 | t('menu'),
439 | async () => {
440 | try {
441 | if (!myTab) myTab = await gm.getTab()
442 | myTab.ts = Date.now()
443 | await gm.saveTab(myTab)
444 | } catch (e) {}
445 | await evaluateAndRequestClose()
446 | },
447 | 'd'
448 | ) // access key 'd' (optional)
449 | }
450 |
451 | // initialize
452 | await registerTab()
453 | await cleanupOnUnload()
454 | setupCloseListener()
455 | registerCommand()
456 |
457 | // expose helper to console
458 | window.__dupeCleaner = {
459 | myId,
460 | myTab,
461 | triggerCleanup: evaluateAndRequestClose,
462 | getTabs: gm.getTabs,
463 | normalizeUrl
464 | }
465 | })()
466 |
--------------------------------------------------------------------------------
/src/userscript/BJTU-Schedule-ics-csvGenerator/generator.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name 北交大iCalender课表生成
3 | // @namespace https://github.com/ZiuChen/userscript
4 | // @version 1.5.1
5 | // @description 导出ics/csv/json格式的日程文件! 💻支持多端同步! 📝支持Excel编辑! 📆支持导入各系统原生日历!
6 | // @author ZiuChen
7 | // @updateURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/BJTU-Schedule-ics-csvGenerator/generator.js
8 | // @downloadURL https://fastly.jsdelivr.net/gh/ZiuChen/userscript@main/src/userscript/BJTU-Schedule-ics-csvGenerator/generator.js
9 | // @match https://aa.bjtu.edu.cn/course_selection/courseselect/stuschedule/*
10 | // @match https://aa.bjtu.edu.cn/course_selection/courseselecttask/schedule/
11 | // @require https://fastly.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js
12 | // @require https://fastly.jsdelivr.net/gh/nwcell/ics.js@dfec67f37a3c267b3f97dd229c9b6a3521222794/demo/ics.deps.min.js
13 | // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg
14 | // @grant none
15 | // @license MIT
16 | // ==/UserScript==
17 |
18 | "use strict"
19 |
20 | const defaultStartMonday = "2022-08-29" // 第一个教学周的第一个周一
21 |
22 | if (localStorage.getItem("defaultStartMonday") === null) {
23 | localStorage.setItem("defaultStartMonday", defaultStartMonday)
24 | }
25 |
26 | function buttonGenerate() {
27 | $(".widget-title").append(/* html */ `
28 |
29 |
30 |
31 |
32 |
33 |
34 | `)
35 | bindEvents()
36 | }
37 |
38 | function bindEvents() {
39 | $("#scheduleRedirect").click(() => {
40 | window.open("https://bksy.bjtu.edu.cn/Semester.html")
41 | })
42 | let pageFlag = 0
43 | if (window.location.href.search("/courseselect/stuschedule/") != -1) {
44 | // 本学期课表
45 | } else if (window.location.href.search("/courseselecttask/schedule/") != -1) {
46 | // 选课课表
47 | pageFlag = 1
48 | }
49 | $("#scheduleIcsGenerate").click(() => {
50 | icsmain(pageFlag)
51 | })
52 | $("#csvGenerate").click(() => {
53 | csvmain(pageFlag)
54 | })
55 | $("#jsonGenerate").click(() => {
56 | jsonmain(pageFlag)
57 | })
58 | $("#restoreSetting").click(() => {
59 | localStorage.clear()
60 | location.reload()
61 | })
62 | $("#startMonday")
63 | .change((e) => {
64 | localStorage.setItem("defaultStartMonday", e.target.value)
65 | })
66 | .val(localStorage.getItem("defaultStartMonday"))
67 | }
68 |
69 | // generateWeekTable() @github ygowill
70 | function generateWeekTable() {
71 | let startMondayString = $("#startMonday").val()
72 | if (validCheck(startMondayString) === false) return
73 | const startMonday = new Date(startMondayString)
74 | let weekDateTable = []
75 | for (let i = 0; i < 30; i++) {
76 | // 生成到30周
77 | let weekArr = []
78 | for (let j = 0; j < 7; j++) {
79 | let tmpDate = new Date(startMonday)
80 | tmpDate.setDate(tmpDate.getDate() + 7 * i + j)
81 | weekArr.push(tmpDate)
82 | }
83 | weekDateTable.push(weekArr)
84 | }
85 | return weekDateTable
86 | }
87 |
88 | function validCheck(startMondayString) {
89 | const re = new RegExp(/^\d{4}-\d{1,2}-\d{1,2}/)
90 | if (re.test(startMondayString) === false) {
91 | alert("输入日期值非法,请重新输入。")
92 | throw Error("输入日期值非法")
93 | } else {
94 | return true
95 | }
96 | }
97 |
98 | function tableTransfer(rowTable, isOrigin) {
99 | // 7*7行转列
100 | let tmpTable = []
101 | let columnTable = []
102 | for (let i = 0; i < 7; i++) {
103 | if (isOrigin) {
104 | for (let j = 0; j < 7; j++) {
105 | tmpTable.push(rowTable[j])
106 | }
107 | } else {
108 | for (let j = i; j < 49; j += 7) {
109 | tmpTable.push(rowTable[j])
110 | }
111 | }
112 | columnTable[i] = tmpTable
113 | tmpTable = []
114 | }
115 | return columnTable
116 | }
117 |
118 | function removeZero(iArr) {
119 | for (let i = 0; i < iArr.length; i++) {
120 | iArr[i] = parseInt(iArr[i], 10)
121 | }
122 | return iArr
123 | }
124 |
125 | function dateStr2Arr(dateStr) {
126 | let dateArr = []
127 | if (dateStr) {
128 | if (dateStr.indexOf("-") != -1) {
129 | // 第X-Y周
130 | let indexArr = dateStr.split("-")
131 | removeZero(indexArr)
132 | for (let i = indexArr[0]; i < indexArr[1] + 1; i++) {
133 | dateArr.push(i)
134 | }
135 | } else if (dateStr.indexOf(",") != -1) {
136 | // 单双周
137 | dateArr = dateStr.split(", ")
138 | removeZero(dateArr)
139 | } else dateArr.push(parseInt(dateStr, 10)) // 第X周
140 | }
141 | return dateArr
142 | }
143 |
144 | // courseList[x]示例:
145 | // allInfo: "国际贸易实务模拟 第03-06周思源东楼 SD401卜伟"
146 | // courseNum: 1
147 | // date: (4) [3, 4, 5, 6]
148 | // initInfo: "第03-06周"
149 | // location: "思源东楼 SD401"
150 | // name: "国际贸易实务模拟 "
151 | // teacher: "卜伟"
152 | // weekNum: 6
153 |
154 | function stuScheduleGetTable(isOrigin) {
155 | let courseListTmp = tableTransfer($("tr>td[style!='height:80px;']"), isOrigin)
156 | let courseList = []
157 | let courseTmp = {}
158 | for (let i = 0; i < 7; i++) {
159 | for (let j = 0; j < 7; j++) {
160 | for (
161 | let k = 0;
162 | k < courseListTmp[i][j].querySelectorAll('span[style="color:#000"]').length;
163 | k++
164 | ) {
165 | courseTmp.weekNum = i + 1
166 | courseTmp.courseNum = j + 1
167 | if (courseListTmp[i][j].querySelectorAll('span[style="color:#000"]')[k]) {
168 | courseTmp.name = courseListTmp[i][j]
169 | .querySelectorAll('span[style="color:#000"]')
170 | [k].innerText.split("[本")[0]
171 | courseTmp.location = courseListTmp[i][j].querySelectorAll('span[class="text-muted"]')[
172 | k
173 | ].innerText
174 | let dateStr = courseListTmp[i][j].querySelectorAll('div[style="max-width:120px;"]')[k]
175 | .innerText
176 | dateStr = dateStr.substring(dateStr.indexOf("第") + 1, dateStr.indexOf("周")) // 预处理
177 | courseTmp.initInfo = "第" + dateStr + "周"
178 | courseTmp.date = dateStr2Arr(dateStr)
179 | courseTmp.teacher = courseListTmp[i][j].querySelectorAll("i")[k].innerText
180 | courseTmp.allInfo =
181 | courseTmp.name +
182 | " " +
183 | courseTmp.initInfo +
184 | " " +
185 | courseTmp.location +
186 | " " +
187 | courseTmp.teacher
188 | courseList.push(courseTmp)
189 | courseTmp = {}
190 | }
191 | }
192 | }
193 | }
194 | return courseList
195 | }
196 |
197 | function scheduleGetTable(isOrigin) {
198 | let courseListTmp = tableTransfer($("tr>td[style!='height:80px;']"), isOrigin)
199 | let courseList = []
200 | let courseTmp = {}
201 | for (let i = 0; i < 7; i++) {
202 | for (let j = 0; j < 7; j++) {
203 | for (
204 | let k = 0;
205 | k < courseListTmp[i][j].querySelectorAll('div[style="max-width:120px;"]').length;
206 | k++
207 | ) {
208 | courseTmp.weekNum = i + 1
209 | courseTmp.courseNum = j + 1
210 | if (courseListTmp[i][j].querySelectorAll("span")[k]) {
211 | courseTmp.name = courseListTmp[i][j]
212 | .getElementsByTagName("span")
213 | [k * 3].innerText.split("\n")[1]
214 | courseTmp.location = courseListTmp[i][j].querySelectorAll('span[class="text-muted"]')[
215 | k
216 | ].innerText
217 | let dateStr = courseListTmp[i][j].querySelectorAll('div[style="max-width:120px;"]')[k]
218 | .innerText
219 | dateStr = dateStr.substring(dateStr.indexOf("第") + 1, dateStr.indexOf("周")) // 预处理
220 | courseTmp.initInfo = "第" + dateStr + "周"
221 | courseTmp.date = dateStr2Arr(dateStr)
222 | courseTmp.teacher = courseListTmp[i][j].querySelectorAll("i")[k]?.innerText || ""
223 | courseTmp.allInfo =
224 | courseTmp.name +
225 | " " +
226 | courseTmp.initInfo +
227 | " " +
228 | courseTmp.location +
229 | " " +
230 | courseTmp.teacher
231 | courseList.push(courseTmp)
232 | courseTmp = {}
233 | }
234 | }
235 | }
236 | }
237 | return courseList
238 | }
239 |
240 | function timeConstructor(weekTh, weekNum, courseNum, isStamp, isDelay) {
241 | let standardTimeTable = [
242 | ["08:00", "09:50"],
243 | ["10:10", "12:00"],
244 | ["12:10", "14:00"],
245 | ["14:10", "16:00"],
246 | ["16:20", "18:10"],
247 | ["19:00", "20:50"],
248 | ["21:00", "21:50"]
249 | ]
250 | let delayTimeTable = [
251 | ["08:00", "09:50"],
252 | ["10:30", "12:20"],
253 | ["12:10", "14:00"],
254 | ["14:10", "16:00"],
255 | ["16:20", "18:10"],
256 | ["19:00", "20:50"],
257 | ["21:00", "21:50"]
258 | ]
259 |
260 | let WeekTable = generateWeekTable()
261 | let DayTime = new Date(WeekTable[weekTh - 1][weekNum - 1])
262 | let rtnTime = []
263 | let startTimeStamp, endTimeStamp
264 | let delayClassroom = ["思源西楼", "逸夫"]
265 |
266 | for (let item of delayClassroom) {
267 | if (isDelay.search(item) != -1) {
268 | startTimeStamp = DayTime.setHours(
269 | delayTimeTable[courseNum - 1][0].split(":")[0],
270 | delayTimeTable[courseNum - 1][0].split(":")[1]
271 | )
272 | endTimeStamp = DayTime.setHours(
273 | delayTimeTable[courseNum - 1][1].split(":")[0],
274 | delayTimeTable[courseNum - 1][1].split(":")[1]
275 | )
276 | } else {
277 | startTimeStamp = DayTime.setHours(
278 | standardTimeTable[courseNum - 1][0].split(":")[0],
279 | standardTimeTable[courseNum - 1][0].split(":")[1]
280 | )
281 | endTimeStamp = DayTime.setHours(
282 | standardTimeTable[courseNum - 1][1].split(":")[0],
283 | standardTimeTable[courseNum - 1][1].split(":")[1]
284 | )
285 | }
286 | }
287 |
288 | if (isStamp === 1) {
289 | rtnTime.push(startTimeStamp)
290 | rtnTime.push(endTimeStamp)
291 | return rtnTime
292 | }
293 |
294 | let startTime = new Date(startTimeStamp)
295 | let endTime = new Date(endTimeStamp)
296 | startTime = startTime.toString()
297 | endTime = endTime.toString()
298 | rtnTime.push(startTime)
299 | rtnTime.push(endTime)
300 | return rtnTime
301 | }
302 |
303 | function icsConstructor(icsEventList) {
304 | let cal = ics()
305 | let today = new Date()
306 | today = today.toLocaleDateString()
307 | for (let i = 0; i < icsEventList.length; i++) {
308 | cal.addEvent(
309 | icsEventList[i].name,
310 | icsEventList[i].description,
311 | icsEventList[i].location,
312 | icsEventList[i].startTime,
313 | icsEventList[i].endTime
314 | )
315 | }
316 | cal.download("iCalender - 课表 - " + today)
317 | }
318 |
319 | function eventConstructor(courseList) {
320 | let icsEvent = {}
321 | let icsEventList = []
322 | for (let i = 0; i < courseList.length; i++) {
323 | for (let j = 0; j < courseList[i].date.length; j++) {
324 | let timeRst = timeConstructor(
325 | courseList[i].date[j],
326 | courseList[i].weekNum,
327 | courseList[i].courseNum,
328 | 0,
329 | courseList[i].location
330 | )
331 | let timeRstStamp = timeConstructor(
332 | courseList[i].date[j],
333 | courseList[i].weekNum,
334 | courseList[i].courseNum,
335 | 1,
336 | courseList[i].location
337 | )
338 | icsEvent.name = courseList[i].name
339 | icsEvent.description =
340 | courseList[i].location +
341 | " " +
342 | courseList[i].initInfo +
343 | " 任课教师:" +
344 | courseList[i].teacher
345 | icsEvent.location = courseList[i].location
346 | icsEvent.startTime = timeRst[0]
347 | icsEvent.endTime = timeRst[1]
348 | icsEvent.startTimeStamp = timeRstStamp[0]
349 | icsEvent.endTimeStamp = timeRstStamp[1]
350 | icsEventList.push(icsEvent)
351 | icsEvent = {}
352 | }
353 | }
354 | return icsEventList
355 | }
356 |
357 | function toExcelFormatter(courseList) {
358 | let standardTimeTable = [
359 | ["08:00", "09:50"],
360 | ["10:10", "12:00"],
361 | ["12:10", "14:00"],
362 | ["14:10", "16:00"],
363 | ["16:20", "18:10"],
364 | ["19:00", "20:50"],
365 | ["21:00", "21:50"]
366 | ]
367 | let jsonData = [
368 | {
369 | column1: "",
370 | column2: "",
371 | column3: "",
372 | column4: "",
373 | column5: "",
374 | column6: "",
375 | column7: ""
376 | },
377 | {
378 | column1: "",
379 | column2: "",
380 | column3: "",
381 | column4: "",
382 | column5: "",
383 | column6: "",
384 | column7: ""
385 | },
386 | {
387 | column1: "",
388 | column2: "",
389 | column3: "",
390 | column4: "",
391 | column5: "",
392 | column6: "",
393 | column7: ""
394 | },
395 | {
396 | column1: "",
397 | column2: "",
398 | column3: "",
399 | column4: "",
400 | column5: "",
401 | column6: "",
402 | column7: ""
403 | },
404 | {
405 | column1: "",
406 | column2: "",
407 | column3: "",
408 | column4: "",
409 | column5: "",
410 | column6: "",
411 | column7: ""
412 | },
413 | {
414 | column1: "",
415 | column2: "",
416 | column3: "",
417 | column4: "",
418 | column5: "",
419 | column6: "",
420 | column7: ""
421 | },
422 | {
423 | column1: "",
424 | column2: "",
425 | column3: "",
426 | column4: "",
427 | column5: "",
428 | column6: "",
429 | column7: ""
430 | }
431 | ]
432 | let charArr = ["第一节", "第二节", "第三节", "第四节", "第五节", "第六节", "第七节"]
433 | let objKeys = Object.keys(jsonData[0])
434 |
435 | for (let i = 0; i < 7; i++) {
436 | for (let j = 0; j < 7; j++) {
437 | let tmpKey = objKeys[j + 1]
438 | jsonData[i].column1 =
439 | charArr[i] + " [" + standardTimeTable[i][0] + " - " + standardTimeTable[i][0] + "]"
440 | for (let k = 0; k < courseList.length; k++) {
441 | if (courseList[k].courseNum == i + 1 && courseList[k].weekNum == j + 1) {
442 | jsonData[i][tmpKey] = jsonData[i][tmpKey] + " " + courseList[k].allInfo
443 | }
444 | }
445 | }
446 | }
447 | return jsonData
448 | }
449 |
450 | // tableToExcel() @csdn hhzzcc_
451 | function tableToExcel(jsonData) {
452 | let str = `课程|星期,星期一,星期二,星期三,星期四,星期五,星期六,星期日\n`
453 | for (let i = 0; i < jsonData.length; i++) {
454 | for (let key in jsonData[i]) {
455 | str += `${jsonData[i][key] + "\t"},`
456 | }
457 | str += "\n"
458 | }
459 | const uri = "data:text/csv;charset=utf-8,\ufeff" + encodeURIComponent(str)
460 | const link = document.createElement("a")
461 | link.href = uri
462 | link.download = "课程表.csv"
463 | link.click()
464 | }
465 |
466 | function icsmain(icase) {
467 | let icsEventList
468 | if (icase === 0) {
469 | icsEventList = eventConstructor(stuScheduleGetTable())
470 | } else if (icase === 1) {
471 | icsEventList = eventConstructor(scheduleGetTable())
472 | }
473 | icsConstructor(icsEventList)
474 | }
475 |
476 | function csvmain(icase) {
477 | let jsonData
478 | if (icase === 0) {
479 | jsonData = toExcelFormatter(stuScheduleGetTable())
480 | } else if (icase === 1) {
481 | jsonData = toExcelFormatter(scheduleGetTable())
482 | }
483 | tableToExcel(jsonData)
484 | }
485 |
486 | function jsonmain(icase) {
487 | let jsonData
488 | if (icase === 0) {
489 | jsonData = stuScheduleGetTable()
490 | } else if (icase === 1) {
491 | jsonData = scheduleGetTable()
492 | }
493 | const uri = "data:text/json;charset=utf-8,\ufeff" + encodeURIComponent(JSON.stringify(jsonData))
494 | const link = document.createElement("a")
495 | link.href = uri
496 | link.download = "课程表.json"
497 | link.click()
498 | }
499 |
500 | buttonGenerate()
501 |
--------------------------------------------------------------------------------
/src/userscript/snapshot-everything/overlay.vue:
--------------------------------------------------------------------------------
1 |
260 |
261 |
262 |
263 |
321 |
322 |
323 |
329 |
{{ linkParams.title }}:{{ linkParams.line }}:{{ linkParams.column }}
330 |
Click to go to the file
331 |
332 |
337 |
338 |
339 |
340 |
341 |
408 |
--------------------------------------------------------------------------------
/src/script/JuejinCookie/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: 5.4
2 |
3 | specifiers:
4 | dotenv: ^16.0.1
5 | puppeteer: ^15.5.0
6 |
7 | dependencies:
8 | dotenv: registry.npmmirror.com/dotenv/16.0.1
9 | puppeteer: registry.npmmirror.com/puppeteer/15.5.0
10 |
11 | packages:
12 |
13 | registry.npmmirror.com/@types/node/18.6.1:
14 | resolution: {integrity: sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/node/-/node-18.6.1.tgz}
15 | name: '@types/node'
16 | version: 18.6.1
17 | dev: false
18 | optional: true
19 |
20 | registry.npmmirror.com/@types/yauzl/2.10.0:
21 | resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.0.tgz}
22 | name: '@types/yauzl'
23 | version: 2.10.0
24 | requiresBuild: true
25 | dependencies:
26 | '@types/node': registry.npmmirror.com/@types/node/18.6.1
27 | dev: false
28 | optional: true
29 |
30 | registry.npmmirror.com/agent-base/6.0.2:
31 | resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz}
32 | name: agent-base
33 | version: 6.0.2
34 | engines: {node: '>= 6.0.0'}
35 | dependencies:
36 | debug: registry.npmmirror.com/debug/4.3.4
37 | transitivePeerDependencies:
38 | - supports-color
39 | dev: false
40 |
41 | registry.npmmirror.com/balanced-match/1.0.2:
42 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz}
43 | name: balanced-match
44 | version: 1.0.2
45 | dev: false
46 |
47 | registry.npmmirror.com/base64-js/1.5.1:
48 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz}
49 | name: base64-js
50 | version: 1.5.1
51 | dev: false
52 |
53 | registry.npmmirror.com/bl/4.1.0:
54 | resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz}
55 | name: bl
56 | version: 4.1.0
57 | dependencies:
58 | buffer: registry.npmmirror.com/buffer/5.7.1
59 | inherits: registry.npmmirror.com/inherits/2.0.4
60 | readable-stream: registry.npmmirror.com/readable-stream/3.6.0
61 | dev: false
62 |
63 | registry.npmmirror.com/brace-expansion/1.1.11:
64 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz}
65 | name: brace-expansion
66 | version: 1.1.11
67 | dependencies:
68 | balanced-match: registry.npmmirror.com/balanced-match/1.0.2
69 | concat-map: registry.npmmirror.com/concat-map/0.0.1
70 | dev: false
71 |
72 | registry.npmmirror.com/buffer-crc32/0.2.13:
73 | resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz}
74 | name: buffer-crc32
75 | version: 0.2.13
76 | dev: false
77 |
78 | registry.npmmirror.com/buffer/5.7.1:
79 | resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz}
80 | name: buffer
81 | version: 5.7.1
82 | dependencies:
83 | base64-js: registry.npmmirror.com/base64-js/1.5.1
84 | ieee754: registry.npmmirror.com/ieee754/1.2.1
85 | dev: false
86 |
87 | registry.npmmirror.com/chownr/1.1.4:
88 | resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz}
89 | name: chownr
90 | version: 1.1.4
91 | dev: false
92 |
93 | registry.npmmirror.com/concat-map/0.0.1:
94 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz}
95 | name: concat-map
96 | version: 0.0.1
97 | dev: false
98 |
99 | registry.npmmirror.com/cross-fetch/3.1.5:
100 | resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cross-fetch/-/cross-fetch-3.1.5.tgz}
101 | name: cross-fetch
102 | version: 3.1.5
103 | dependencies:
104 | node-fetch: registry.npmmirror.com/node-fetch/2.6.7
105 | transitivePeerDependencies:
106 | - encoding
107 | dev: false
108 |
109 | registry.npmmirror.com/debug/4.3.4:
110 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz}
111 | name: debug
112 | version: 4.3.4
113 | engines: {node: '>=6.0'}
114 | peerDependencies:
115 | supports-color: '*'
116 | peerDependenciesMeta:
117 | supports-color:
118 | optional: true
119 | dependencies:
120 | ms: registry.npmmirror.com/ms/2.1.2
121 | dev: false
122 |
123 | registry.npmmirror.com/devtools-protocol/0.0.1019158:
124 | resolution: {integrity: sha512-wvq+KscQ7/6spEV7czhnZc9RM/woz1AY+/Vpd8/h2HFMwJSdTliu7f/yr1A6vDdJfKICZsShqsYpEQbdhg8AFQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1019158.tgz}
125 | name: devtools-protocol
126 | version: 0.0.1019158
127 | dev: false
128 |
129 | registry.npmmirror.com/dotenv/16.0.1:
130 | resolution: {integrity: sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dotenv/-/dotenv-16.0.1.tgz}
131 | name: dotenv
132 | version: 16.0.1
133 | engines: {node: '>=12'}
134 | dev: false
135 |
136 | registry.npmmirror.com/end-of-stream/1.4.4:
137 | resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz}
138 | name: end-of-stream
139 | version: 1.4.4
140 | dependencies:
141 | once: registry.npmmirror.com/once/1.4.0
142 | dev: false
143 |
144 | registry.npmmirror.com/extract-zip/2.0.1:
145 | resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz}
146 | name: extract-zip
147 | version: 2.0.1
148 | engines: {node: '>= 10.17.0'}
149 | hasBin: true
150 | dependencies:
151 | debug: registry.npmmirror.com/debug/4.3.4
152 | get-stream: registry.npmmirror.com/get-stream/5.2.0
153 | yauzl: registry.npmmirror.com/yauzl/2.10.0
154 | optionalDependencies:
155 | '@types/yauzl': registry.npmmirror.com/@types/yauzl/2.10.0
156 | transitivePeerDependencies:
157 | - supports-color
158 | dev: false
159 |
160 | registry.npmmirror.com/fd-slicer/1.1.0:
161 | resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz}
162 | name: fd-slicer
163 | version: 1.1.0
164 | dependencies:
165 | pend: registry.npmmirror.com/pend/1.2.0
166 | dev: false
167 |
168 | registry.npmmirror.com/find-up/4.1.0:
169 | resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz}
170 | name: find-up
171 | version: 4.1.0
172 | engines: {node: '>=8'}
173 | dependencies:
174 | locate-path: registry.npmmirror.com/locate-path/5.0.0
175 | path-exists: registry.npmmirror.com/path-exists/4.0.0
176 | dev: false
177 |
178 | registry.npmmirror.com/fs-constants/1.0.0:
179 | resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz}
180 | name: fs-constants
181 | version: 1.0.0
182 | dev: false
183 |
184 | registry.npmmirror.com/fs.realpath/1.0.0:
185 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz}
186 | name: fs.realpath
187 | version: 1.0.0
188 | dev: false
189 |
190 | registry.npmmirror.com/get-stream/5.2.0:
191 | resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz}
192 | name: get-stream
193 | version: 5.2.0
194 | engines: {node: '>=8'}
195 | dependencies:
196 | pump: registry.npmmirror.com/pump/3.0.0
197 | dev: false
198 |
199 | registry.npmmirror.com/glob/7.2.3:
200 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz}
201 | name: glob
202 | version: 7.2.3
203 | dependencies:
204 | fs.realpath: registry.npmmirror.com/fs.realpath/1.0.0
205 | inflight: registry.npmmirror.com/inflight/1.0.6
206 | inherits: registry.npmmirror.com/inherits/2.0.4
207 | minimatch: registry.npmmirror.com/minimatch/3.1.2
208 | once: registry.npmmirror.com/once/1.4.0
209 | path-is-absolute: registry.npmmirror.com/path-is-absolute/1.0.1
210 | dev: false
211 |
212 | registry.npmmirror.com/https-proxy-agent/5.0.1:
213 | resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz}
214 | name: https-proxy-agent
215 | version: 5.0.1
216 | engines: {node: '>= 6'}
217 | dependencies:
218 | agent-base: registry.npmmirror.com/agent-base/6.0.2
219 | debug: registry.npmmirror.com/debug/4.3.4
220 | transitivePeerDependencies:
221 | - supports-color
222 | dev: false
223 |
224 | registry.npmmirror.com/ieee754/1.2.1:
225 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz}
226 | name: ieee754
227 | version: 1.2.1
228 | dev: false
229 |
230 | registry.npmmirror.com/inflight/1.0.6:
231 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz}
232 | name: inflight
233 | version: 1.0.6
234 | dependencies:
235 | once: registry.npmmirror.com/once/1.4.0
236 | wrappy: registry.npmmirror.com/wrappy/1.0.2
237 | dev: false
238 |
239 | registry.npmmirror.com/inherits/2.0.4:
240 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz}
241 | name: inherits
242 | version: 2.0.4
243 | dev: false
244 |
245 | registry.npmmirror.com/locate-path/5.0.0:
246 | resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz}
247 | name: locate-path
248 | version: 5.0.0
249 | engines: {node: '>=8'}
250 | dependencies:
251 | p-locate: registry.npmmirror.com/p-locate/4.1.0
252 | dev: false
253 |
254 | registry.npmmirror.com/minimatch/3.1.2:
255 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz}
256 | name: minimatch
257 | version: 3.1.2
258 | dependencies:
259 | brace-expansion: registry.npmmirror.com/brace-expansion/1.1.11
260 | dev: false
261 |
262 | registry.npmmirror.com/mkdirp-classic/0.5.3:
263 | resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz}
264 | name: mkdirp-classic
265 | version: 0.5.3
266 | dev: false
267 |
268 | registry.npmmirror.com/ms/2.1.2:
269 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz}
270 | name: ms
271 | version: 2.1.2
272 | dev: false
273 |
274 | registry.npmmirror.com/node-fetch/2.6.7:
275 | resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.7.tgz}
276 | name: node-fetch
277 | version: 2.6.7
278 | engines: {node: 4.x || >=6.0.0}
279 | peerDependencies:
280 | encoding: ^0.1.0
281 | peerDependenciesMeta:
282 | encoding:
283 | optional: true
284 | dependencies:
285 | whatwg-url: registry.npmmirror.com/whatwg-url/5.0.0
286 | dev: false
287 |
288 | registry.npmmirror.com/once/1.4.0:
289 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/once/-/once-1.4.0.tgz}
290 | name: once
291 | version: 1.4.0
292 | dependencies:
293 | wrappy: registry.npmmirror.com/wrappy/1.0.2
294 | dev: false
295 |
296 | registry.npmmirror.com/p-limit/2.3.0:
297 | resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz}
298 | name: p-limit
299 | version: 2.3.0
300 | engines: {node: '>=6'}
301 | dependencies:
302 | p-try: registry.npmmirror.com/p-try/2.2.0
303 | dev: false
304 |
305 | registry.npmmirror.com/p-locate/4.1.0:
306 | resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz}
307 | name: p-locate
308 | version: 4.1.0
309 | engines: {node: '>=8'}
310 | dependencies:
311 | p-limit: registry.npmmirror.com/p-limit/2.3.0
312 | dev: false
313 |
314 | registry.npmmirror.com/p-try/2.2.0:
315 | resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz}
316 | name: p-try
317 | version: 2.2.0
318 | engines: {node: '>=6'}
319 | dev: false
320 |
321 | registry.npmmirror.com/path-exists/4.0.0:
322 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz}
323 | name: path-exists
324 | version: 4.0.0
325 | engines: {node: '>=8'}
326 | dev: false
327 |
328 | registry.npmmirror.com/path-is-absolute/1.0.1:
329 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz}
330 | name: path-is-absolute
331 | version: 1.0.1
332 | engines: {node: '>=0.10.0'}
333 | dev: false
334 |
335 | registry.npmmirror.com/pend/1.2.0:
336 | resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz}
337 | name: pend
338 | version: 1.2.0
339 | dev: false
340 |
341 | registry.npmmirror.com/pkg-dir/4.2.0:
342 | resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz}
343 | name: pkg-dir
344 | version: 4.2.0
345 | engines: {node: '>=8'}
346 | dependencies:
347 | find-up: registry.npmmirror.com/find-up/4.1.0
348 | dev: false
349 |
350 | registry.npmmirror.com/progress/2.0.3:
351 | resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz}
352 | name: progress
353 | version: 2.0.3
354 | engines: {node: '>=0.4.0'}
355 | dev: false
356 |
357 | registry.npmmirror.com/proxy-from-env/1.1.0:
358 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz}
359 | name: proxy-from-env
360 | version: 1.1.0
361 | dev: false
362 |
363 | registry.npmmirror.com/pump/3.0.0:
364 | resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/pump/-/pump-3.0.0.tgz}
365 | name: pump
366 | version: 3.0.0
367 | dependencies:
368 | end-of-stream: registry.npmmirror.com/end-of-stream/1.4.4
369 | once: registry.npmmirror.com/once/1.4.0
370 | dev: false
371 |
372 | registry.npmmirror.com/puppeteer/15.5.0:
373 | resolution: {integrity: sha512-+vZPU8iBSdCx1Kn5hHas80fyo0TiVyMeqLGv/1dygX2HKhAZjO9YThadbRTCoTYq0yWw+w/CysldPsEekDtjDQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/puppeteer/-/puppeteer-15.5.0.tgz}
374 | name: puppeteer
375 | version: 15.5.0
376 | engines: {node: '>=14.1.0'}
377 | requiresBuild: true
378 | dependencies:
379 | cross-fetch: registry.npmmirror.com/cross-fetch/3.1.5
380 | debug: registry.npmmirror.com/debug/4.3.4
381 | devtools-protocol: registry.npmmirror.com/devtools-protocol/0.0.1019158
382 | extract-zip: registry.npmmirror.com/extract-zip/2.0.1
383 | https-proxy-agent: registry.npmmirror.com/https-proxy-agent/5.0.1
384 | pkg-dir: registry.npmmirror.com/pkg-dir/4.2.0
385 | progress: registry.npmmirror.com/progress/2.0.3
386 | proxy-from-env: registry.npmmirror.com/proxy-from-env/1.1.0
387 | rimraf: registry.npmmirror.com/rimraf/3.0.2
388 | tar-fs: registry.npmmirror.com/tar-fs/2.1.1
389 | unbzip2-stream: registry.npmmirror.com/unbzip2-stream/1.4.3
390 | ws: registry.npmmirror.com/ws/8.8.0
391 | transitivePeerDependencies:
392 | - bufferutil
393 | - encoding
394 | - supports-color
395 | - utf-8-validate
396 | dev: false
397 |
398 | registry.npmmirror.com/readable-stream/3.6.0:
399 | resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz}
400 | name: readable-stream
401 | version: 3.6.0
402 | engines: {node: '>= 6'}
403 | dependencies:
404 | inherits: registry.npmmirror.com/inherits/2.0.4
405 | string_decoder: registry.npmmirror.com/string_decoder/1.3.0
406 | util-deprecate: registry.npmmirror.com/util-deprecate/1.0.2
407 | dev: false
408 |
409 | registry.npmmirror.com/rimraf/3.0.2:
410 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz}
411 | name: rimraf
412 | version: 3.0.2
413 | hasBin: true
414 | dependencies:
415 | glob: registry.npmmirror.com/glob/7.2.3
416 | dev: false
417 |
418 | registry.npmmirror.com/safe-buffer/5.2.1:
419 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz}
420 | name: safe-buffer
421 | version: 5.2.1
422 | dev: false
423 |
424 | registry.npmmirror.com/string_decoder/1.3.0:
425 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz}
426 | name: string_decoder
427 | version: 1.3.0
428 | dependencies:
429 | safe-buffer: registry.npmmirror.com/safe-buffer/5.2.1
430 | dev: false
431 |
432 | registry.npmmirror.com/tar-fs/2.1.1:
433 | resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.1.tgz}
434 | name: tar-fs
435 | version: 2.1.1
436 | dependencies:
437 | chownr: registry.npmmirror.com/chownr/1.1.4
438 | mkdirp-classic: registry.npmmirror.com/mkdirp-classic/0.5.3
439 | pump: registry.npmmirror.com/pump/3.0.0
440 | tar-stream: registry.npmmirror.com/tar-stream/2.2.0
441 | dev: false
442 |
443 | registry.npmmirror.com/tar-stream/2.2.0:
444 | resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz}
445 | name: tar-stream
446 | version: 2.2.0
447 | engines: {node: '>=6'}
448 | dependencies:
449 | bl: registry.npmmirror.com/bl/4.1.0
450 | end-of-stream: registry.npmmirror.com/end-of-stream/1.4.4
451 | fs-constants: registry.npmmirror.com/fs-constants/1.0.0
452 | inherits: registry.npmmirror.com/inherits/2.0.4
453 | readable-stream: registry.npmmirror.com/readable-stream/3.6.0
454 | dev: false
455 |
456 | registry.npmmirror.com/through/2.3.8:
457 | resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/through/-/through-2.3.8.tgz}
458 | name: through
459 | version: 2.3.8
460 | dev: false
461 |
462 | registry.npmmirror.com/tr46/0.0.3:
463 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz}
464 | name: tr46
465 | version: 0.0.3
466 | dev: false
467 |
468 | registry.npmmirror.com/unbzip2-stream/1.4.3:
469 | resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz}
470 | name: unbzip2-stream
471 | version: 1.4.3
472 | dependencies:
473 | buffer: registry.npmmirror.com/buffer/5.7.1
474 | through: registry.npmmirror.com/through/2.3.8
475 | dev: false
476 |
477 | registry.npmmirror.com/util-deprecate/1.0.2:
478 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz}
479 | name: util-deprecate
480 | version: 1.0.2
481 | dev: false
482 |
483 | registry.npmmirror.com/webidl-conversions/3.0.1:
484 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz}
485 | name: webidl-conversions
486 | version: 3.0.1
487 | dev: false
488 |
489 | registry.npmmirror.com/whatwg-url/5.0.0:
490 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz}
491 | name: whatwg-url
492 | version: 5.0.0
493 | dependencies:
494 | tr46: registry.npmmirror.com/tr46/0.0.3
495 | webidl-conversions: registry.npmmirror.com/webidl-conversions/3.0.1
496 | dev: false
497 |
498 | registry.npmmirror.com/wrappy/1.0.2:
499 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz}
500 | name: wrappy
501 | version: 1.0.2
502 | dev: false
503 |
504 | registry.npmmirror.com/ws/8.8.0:
505 | resolution: {integrity: sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ws/-/ws-8.8.0.tgz}
506 | name: ws
507 | version: 8.8.0
508 | engines: {node: '>=10.0.0'}
509 | peerDependencies:
510 | bufferutil: ^4.0.1
511 | utf-8-validate: ^5.0.2
512 | peerDependenciesMeta:
513 | bufferutil:
514 | optional: true
515 | utf-8-validate:
516 | optional: true
517 | dev: false
518 |
519 | registry.npmmirror.com/yauzl/2.10.0:
520 | resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz}
521 | name: yauzl
522 | version: 2.10.0
523 | dependencies:
524 | buffer-crc32: registry.npmmirror.com/buffer-crc32/0.2.13
525 | fd-slicer: registry.npmmirror.com/fd-slicer/1.1.0
526 | dev: false
527 |
--------------------------------------------------------------------------------