├── .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 | ![](https://s3.bmp.ovh/imgs/2022/06/11/1b85489b7deb62ed.png) 14 | 15 | ​ 启用脚本后: 16 | 17 | ![](https://s3.bmp.ovh/imgs/2022/06/11/3c63992d14f92645.png) 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 | ![](https://s3.bmp.ovh/imgs/2022/06/11/87a75c07a9063f1f.png) 24 | 25 | **使用脚本后:** 26 | 27 | ![](https://s3.bmp.ovh/imgs/2022/06/03/1395ea6e93c7bc09.png) 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 | ![](./image.png) 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 | ![example-search](./docs/example-search.webp) 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 | ![example-search](./docs/example-search.webp) 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 | 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 | --------------------------------------------------------------------------------