// ==UserScript==
// @name Cookie管理器
// @namespace cookie_manager
// @version 1.2
// @description 支持Cookie跨机器同步,使用Github仓库作为远程存储(Cookie为敏感信息,不要使用公共仓库,请使用私有仓库)
// @author Gloduck
// @license MIT
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_cookie
// @grant GM_deleteValue
// @grant unsafeWindow
// @connect api.github.com
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @noframes
// ==/UserScript==
(function () {
'use strict';
// 配置存储键名
const CONFIG_KEYS = {
TOKEN: 'GITHUB_TOKEN',
OWNER: 'GITHUB_OWNER',
REPO: 'GITHUB_REPO',
BRANCH: 'GITHUB_BRANCH'
};
const DB_FILE = {
PATH: 'db',
FILE: 'cookie'
}
// 获取当前配置
async function getConfig() {
return {
token: await GM_getValue(CONFIG_KEYS.TOKEN, ''),
owner: await GM_getValue(CONFIG_KEYS.OWNER, ''),
repo: await GM_getValue(CONFIG_KEYS.REPO, ''),
branch: await GM_getValue(CONFIG_KEYS.BRANCH, 'main')
};
}
// 显示配置弹窗
async function showGitConfigDialog() {
const config = await getConfig();
const { value: formValues } = await Swal.fire({
title: 'GitHub 仓库设置',
html: `
<input id="owner" class="swal2-input" placeholder="仓库所有者" value="${config.owner}">
<input id="repo" class="swal2-input" placeholder="仓库名称" value="${config.repo}">
<input id="branch" class="swal2-input" placeholder="分支 (默认main)" value="${config.branch}">
<input id="token" class="swal2-input" placeholder="GitHub Personal Token" type="password" value="${config.token}">
`,
focusConfirm: false,
preConfirm: () => {
return {
owner: cument.getElementById('owner').value,
repo: document.getElementById('repo').value,
branch: document.getElementById('branch').value || 'main',
token: document.getElementById('token').value
};
},
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消'
});
if (formValues) {
await GM_setValue(CONFIG_KEYS.OWNER, formValues.owner);
await GM_setValue(CONFIG_KEYS.REPO, formValues.repo);
await GM_setValue(CONFIG_KEYS.BRANCH, formValues.branch);
await GM_setValue(CONFIG_KEYS.TOKEN, formValues.token);
Swal.fire('保存成功!', '仓库配置已更新', 'success');
}
}
async function clearGitConfig() {
const { isConfirmed } = await Swal.fire({
title: '确认清除',
text: '该操作将删除所有保存的GitHub配置',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消'
});
if (isConfirmed) {
await GM_deleteValue(CONFIG_KEYS.TOKEN);
await GM_deleteValue(CONFIG_KEYS.OWNER);
await GM_deleteValue(CONFIG_KEYS.REPO);
await GM_deleteValue(CONFIG_KEYS.BRANCH);
Swal.fire('已清除!', '所有配置已删除', 'success');
}
}
// GitHub API请求封装
async function githubApiRequest(method, endpoint, data = null) {
const config = await getConfig();
if (!config.token || !config.owner || !config.repo) {
throw new Error('请先配置GitHub仓库信息');
}
const url = `https://api.github.com/repos/${config.owner}/${config.repo}${endpoint}`;
const headers = {
"Authorization": `Bearer ${config.token}`,
"Accept": "application/vnd.github.v3+json",
"Content-Type": "application/json"
};
const options = {
method: method,
headers: headers,
body: data ? JSON.stringify(data) : null
};
try {
const response = await fetch(url, options);
// 处理非2xx响应
if (!response.ok) {
let errorBody;
try {
errorBody = await response.json();
} catch (e) {
errorBody = { message: `API请求失败: ${response.status} ${response.statusText}` };
}
throw {
status: response.status,
message: errorBody.message || 'API请求失败',
response: errorBody
};
}
// 处理204 No Content等空响应
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
return null;
}
return await response.json();
} catch (error) {
if (error.status) {
// 已处理的API错误
throw error;
}
// 网络错误
throw {
status: 0,
message: '网络请求失败',
error: error
};
}
}
// 1. 创建文件
async function createFile(path, content, message = "Created via Tampermonkey") {
const encodedContent = btoa(unescape(encodeURIComponent(content)));
return githubApiRequest('PUT', `/contents/${encodeURIComponent(path)}`, {
message,
content: encodedContent,
branch: (await getConfig()).branch
});
}
// 2. 更新文件
async function updateFile(path, content, message = "Updated via Tampermonkey") {
// 先获取文件当前SHA
const fileInfo = await getFileInfo(path);
const encodedContent = btoa(unescape(encodeURIComponent(content)));
return githubApiRequest('PUT', `/contents/${encodeURIComponent(path)}`, {
message,
content: encodedContent,
sha: fileInfo.sha,
branch: (await getConfig()).branch
});
}
// 3. 删除文件
async function deleteFile(path, message = "Deleted via Tampermonkey") {
// 先获取文件当前SHA
const fileInfo = await getFileInfo(path);
return githubApiRequest('DELETE', `/contents/${encodeURIComponent(path)}`, {
message,
sha: fileInfo.sha,
branch: (await getConfig()).branch
});
}
// 4. 获取文件信息(不包含内容)
async function getFileInfo(path) {
// 添加随机查询参数,强制绕过缓存
const ref = (await getConfig()).branch;
const cacheBuster = Date.now();
const fileInfo = await githubApiRequest('GET',
`/contents/${encodeURIComponent(path)}?ref=${ref}&_=${cacheBuster}`);
return fileInfo;
}
// 5. 获取文件内容
async function getFileContent(path) {
const fileInfo = await getFileInfo(path);
if (fileInfo.encoding === 'base64') {
return decodeURIComponent(escape(atob(fileInfo.content)));
}
return fileInfo.content;
}
// 6. 获取仓库所有文件列表(递归)
async function getAllFiles(path = '', files = []) {
const contents = await githubApiRequest('GET', `/contents/${encodeURIComponent(path)}?ref=${(await getConfig()).branch}`);
for (const item of contents) {
if (item.type === 'file') {
files.push({
path: item.path,
size: item.size,
sha: item.sha
});
} else if (item.type === 'dir') {
await getAllFiles(item.path, files);
}
}
return files;
}
class CsvUtils {
static parseCsvLine(line) {
const result = [];
let current = '';
let inQuotes = false;
let i = 0;
while (i < line.length) {
const char = line[i];
if (inQuotes) {
if (char === '"' && i + 1 < line.length && line[i + 1] === '"') {
current += '"';
i += 2;
continue;
} else if (char === '"') {
inQuotes = false;
i++;
continue;
} else {
current += char;
i++;
}
} else {
if (char === '"') {
inQuotes = true;
i++;
} else if (char === ',') {
result.push(CsvUtils.unescapeField(current));
current = '';
i++;
} else {
current += char;
i++;
}
}
}
result.push(CsvUtils.unescapeField(current));
return result;
}
static unescapeField(field) {
return field.replace(/\\"/g, '"')
.replace(/\\,/g, ',');
}
static escapeCsvField(field) {
if (field == null) return '';
if (typeof field !== 'string') field = String(field);
if (field.includes(',') || field.includes('"') || field.includes('\n')) {
return '"' + field.replace(/"/g, '""') + '"';
}
return field;
}
static compareValue(a, b) {
const numA = parseFloat(a);
const numB = parseFloat(b);
if (!isNaN(numA) && !isNaN(numB)) {
return numA - numB;
}
return a.localeCompare(b, undefined, { numeric: true });
}
}
function csvDataFilter() {
const _filters = []
function test(row) {
return _filters.every(f => f(row));
}
function eq(fieldName, value) {
const strValue = (value === null || value === undefined) ? null : String(value);
_filters.push(row => {
const v = row[fieldName];
if (v === null || v === undefined) {
return strValue === null;
}
if (strValue === null) {
return false;
}
return v === strValue;
});
}
function notEq(fieldName, value) {
const strValue = (value === null || value === undefined) ? null : String(value);
_filters.push(row => {
const v = row[fieldName];
if (v === null || v === undefined) {
return strValue !== null;
}
if (strValue === null) {
return true;
}
return v !== strValue;
});
}
function inValues(fieldName, ...values) {
const set = new Set(values.map(v => v == null ? null : String(v)));
_filters.push(row => {
const v = row[fieldName];
const valueToCheck = (v === undefined) ? null : v;
return set.has(valueToCheck);
});
}
function notIn(fieldName, ...values) {
const set = new Set(values.map(v => v == null ? null : String(v)));
_filters.push(row => {
const v = row[fieldName];
const valueToCheck = (v === undefined) ? null : v;
return !set.has(valueToCheck);
});
}
function like(fieldName, pattern) {
const regex = new RegExp('^' + pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/%/g, '.*')
.replace(/_/g, '.') + '$');
_filters.push(row => {
const v = row[fieldName] ?? '';
return regex.test(v);
});
}
function gt(fieldName, value) {
_cmpHelper(fieldName, value, cmpResult => cmpResult > 0);
}
function ge(fieldName, value) {
_cmpHelper(fieldName, value, cmpResult => cmpResult >= 0);
}
function lt(fieldName, value) {
_cmpHelper(fieldName, value, cmpResult => cmpResult < 0);
}
function le(fieldName, value) {
_cmpHelper(fieldName, value, cmpResult => cmpResult <= 0);
}
function _cmpHelper(fieldName, value, tester) {
const strValue = (value === null || value === undefined) ? null : String(value);
_filters.push(row => {
const v = row[fieldName];
if (v == null || strValue == null) {
return false;
}
const cmpResult = CsvUtils.compareValue(v, strValue);
return tester(cmpResult);
});
}
return {
test,
eq,
notEq,
inValues,
notIn,
like,
gt,
ge,
lt,
le
}
}
function csvDataFetcher() {
const handler = {
shouldHandleData(row) {
throw new Error("shouldHandleData must be implemented");
},
lineOffset() {
return 0;
},
lineLimit() {
return Number.MAX_VALUE;
},
orderField() {
return null;
},
orderDesc() {
return false;
},
selectField() {
return null;
}
}
function fetch(csvContent) {
const lines = csvContent.split('\n');
if (lines.length === 0) {
throw new Error("csv must contains header");
}
const headers = CsvUtils.parseCsvLine(lines[0]);
const records = [];
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = CsvUtils.parseCsvLine(lines[i]);
const row = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
if (!handler.shouldHandleData(row)) {
continue;
}
records.push(row);
}
const valueOrderFiled = handler.orderField();
const valueOrderDesc = handler.orderDesc();
if (valueOrderFiled != null) {
records.sort((a, b) => {
const v1 = a[valueOrderFiled];
const v2 = b[valueOrderFiled];
const cmpResult = CsvUtils.compareValue(v1, v2);
return valueOrderDesc ? -cmpResult : cmpResult;
});
}
const start = handler.lineOffset();
const end = start + handler.lineLimit();
const selectFields = handler.selectField();
if (selectFields == null) {
return records.slice(start, end);
} else {
return records.slice(start, end).map(row => {
const newRow = {};
selectFields.forEach(field => {
newRow[field] = row[field];
});
return newRow;
});
}
}
return {
fetch,
handler
}
}
function csvModifyHandler() {
const handler = {
appendRows() {
throw new Error("shouldHandleData must be implemented");
},
shouldHandleData(row) {
throw new Error("shouldHandleData must be implemented");
},
handleData(row) {
throw new Error("handleData must be implemented");
},
}
function execute(csvContent) {
const lines = csvContent.split('\n');
if (lines.length === 0) {
throw new Error("csv must contains header");
}
const headers = CsvUtils.parseCsvLine(lines[0]);
const records = [];
let affectedCount = 0;
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = CsvUtils.parseCsvLine(lines[i]);
const row = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
if (handler.shouldHandleData(row)) {
const newRow = handler.handleData({ ...row });
if (newRow !== null) {
records.push(prepareRecord(headers, newRow));
}
affectedCount++;
} else {
records.push(values);
}
}
for (const row of handler.appendRows()) {
records.push(prepareRecord(headers, row));
affectedCount++;
}
const newHeaders = headers.join(',');
const newCsv = [newHeaders, ...records.map(values =>
values.map(v => CsvUtils.escapeCsvField(v)).join(',')
)].join('\n');
return {
affectedCount: affectedCount,
csvContent: newCsv
};
}
function prepareRecord(headers, row) {
return headers.map(header => row[header] ?? '');
}
return {
handler,
execute,
};
}
function csvDb(csvPath) {
async function createIfNotExist(csvFileName, headers) {
const path = `${csvPath}/${csvFileName}.csv`;
try {
await getFileInfo(path);
return false;
} catch (error) {
if (error.status === 404) {
try {
await createFile(path, headers);
return true;
} catch (createError) {
throw createError;
}
} else {
throw error;
}
}
}
async function create(csvFileName, headers) {
const path = `${csvPath}/${csvFileName}.csv`;
const csvContent = headers.join(',') + '\n';
await createFile(path, csvContent);
}
function update(csvFileName) {
const updateFields = {};
const path = `${csvPath}/${csvFileName}.csv`;
const csvHandler = csvModifyHandler();
const csvFilter = csvDataFilter();
csvHandler.handler.shouldHandleData = (row) => {
return csvFilter.test(row);
};
csvHandler.handler.handleData = (row) => {
Object.entries(updateFields).forEach(([field, newVal]) => {
row[field] = (newVal === null || newVal === undefined) ? null : String(newVal);
});
return row;
};
csvHandler.handler.appendRows = () => [];
function set(field, value) {
updateFields[field] = value == null ? '' : String(value);
return this;
}
async function execute() {
const csvContent = await getFileContent(path);
const { affectedCount, csvContent: newCsvContent } = csvHandler.execute(csvContent);
await updateFile(path, newCsvContent);
return affectedCount;
}
return {
execute: execute,
set: set,
eq: function (fieldName, value) {
csvFilter.eq(fieldName, value);
return this;
},
notEq: function (fieldName, value) {
csvFilter.notEq(fieldName, value);
return this;
},
in: function (fieldName, ...values) {
csvFilter.inValues(fieldName, ...values);
return this;
},
notIn: function (fieldName, ...values) {
csvFilter.notIn(fieldName, ...values);
return this;
},
like: function (fieldName, pattern) {
csvFilter.like(fieldName, pattern);
return this;
},
gt: function (fieldName, value) {
csvFilter.gt(fieldName, value);
return this;
},
ge: function (fieldName, value) {
csvFilter.ge(fieldName, value);
return this;
},
lt: function (fieldName, value) {
csvFilter.lt(fieldName, value);
return this;
},
le: function (fieldName, value) {
csvFilter.le(fieldName, value);
return this;
}
}
}
function updateBy(csvFileName, fieldName) {
const updateDatas = {};
const path = `${csvPath}/${csvFileName}.csv`;
const csvHandler = csvModifyHandler();
csvHandler.handler.shouldHandleData = (row) => {
if (row[fieldName] === null || row[fieldName] === undefined) {
return false;
}
return updateDatas.hasOwnProperty(row[fieldName]);
}
csvHandler.handler.handleData = (row) => {
return updateDatas[row[fieldName]];
}
csvHandler.handler.appendRows = () => [];
function value(data) {
updateDatas[data[fieldName]] = data;
return this;
}
async function execute() {
const csvContent = await getFileContent(path);
const { affectedCount, csvContent: newCsvContent } = csvHandler.execute(csvContent);
await updateFile(path, newCsvContent);
return affectedCount;
}
return {
execute: execute,
value: value
}
}
function deleteFrom(csvFileName) {
const path = `${csvPath}/${csvFileName}.csv`;
const csvHandler = csvModifyHandler();
const csvFilter = csvDataFilter();
csvHandler.handler.shouldHandleData = (row) => {
return csvFilter.test(row);
};
csvHandler.handler.handleData = (row) => null;
csvHandler.handler.appendRows = () => [];
async function execute() {
const csvContent = await getFileContent(path);
const { affectedCount, csvContent: newCsvContent } = csvHandler.execute(csvContent);
await updateFile(path, newCsvContent);
return affectedCount;
}
return {
execute: execute,
eq: function (fieldName, value) {
csvFilter.eq(fieldName, value);
return this;
},
notEq: function (fieldName, value) {
csvFilter.notEq(fieldName, value);
return this;
},
in: function (fieldName, ...values) {
csvFilter.inValues(fieldName, ...values);
return this;
},
notIn: function (fieldName, ...values) {
csvFilter.notIn(fieldName, ...values);
return this;
},
like: function (fieldName, pattern) {
csvFilter.like(fieldName, pattern);
return this;
},
gt: function (fieldName, value) {
csvFilter.gt(fieldName, value);
return this;
},
ge: function (fieldName, value) {
csvFilter.ge(fieldName, value);
return this;
},
lt: function (fieldName, value) {
csvFilter.lt(fieldName, value);
return this;
},
le: function (fieldName, value) {
csvFilter.le(fieldName, value);
return this;
}
}
}
function insertInto(csvFileName) {
const path = `${csvPath}/${csvFileName}.csv`;
const csvHandler = csvModifyHandler();
const appendRows = [];
csvHandler.handler.shouldHandleData = () => false;
csvHandler.handler.handleData = (row) => row;
csvHandler.handler.appendRows = () => appendRows;
function value(data) {
appendRows.push(data);
return this;
}
async function execute() {
const csvContent = await getFileContent(path);
const { affectedCount, csvContent: newCsvContent } = csvHandler.execute(csvContent);
await updateFile(path, newCsvContent);
return affectedCount;
}
return {
value,
execute: execute
};
}
function selectFrom(csvFileName, ...fieldNames) {
const path = `${csvPath}/${csvFileName}.csv`;
const csvFetcher = csvDataFetcher();
const csvFilter = csvDataFilter();
csvFetcher.handler.shouldHandleData = (row) => {
return csvFilter.test(row);
};
csvFetcher.handler.selectField = () => {
return fieldNames.length === 0 ? null : fieldNames;
}
function offset(offset) {
if (offset < 0) throw new Error("Offset cannot be negative");
csvFetcher.handler.lineOffset = () => offset;
return this;
}
function limit(limit) {
if (limit < 0) throw new Error("Limit cannot be negative");
csvFetcher.handler.lineLimit = () => limit;
return this;
}
function order(fieldName, desc) {
csvFetcher.handler.orderField = () => fieldName;
csvFetcher.handler.orderDesc = () => desc;
return this;
}
async function fetch() {
const csvContent = await getFileContent(path);
return csvFetcher.fetch(csvContent);
}
async function fetchOne() {
const values = await fetch();
return values.length > 0 ? values[0] : null;
}
return {
offset,
limit,
fetch,
fetchOne,
order,
eq: function (fieldName, value) {
csvFilter.eq(fieldName, value);
return this;
},
notEq: function (fieldName, value) {
csvFilter.notEq(fieldName, value);
return this;
},
in: function (fieldName, ...values) {
csvFilter.inValues(fieldName, ...values);
return this;
},
notIn: function (fieldName, ...values) {
csvFilter.notIn(fieldName, ...values);
return this;
},
like: function (fieldName, pattern) {
csvFilter.like(fieldName, pattern);
return this;
},
gt: function (fieldName, value) {
csvFilter.gt(fieldName, value);
return this;
},
ge: function (fieldName, value) {
csvFilter.ge(fieldName, value);
return this;
},
lt: function (fieldName, value) {
csvFilter.lt(fieldName, value);
return this;
},
le: function (fieldName, value) {
csvFilter.le(fieldName, value);
return this;
}
}
}
return {
create,
createIfNotExist,
insertInto,
deleteFrom,
update,
updateBy,
selectFrom
}
}
function getRootDomain() {
const hostname = window.location.hostname;
if (!hostname) return '';
const specialSuffixes = [
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
'co.uk', 'org.uk', 'gov.uk', 'ac.uk',
'com.au', 'org.au', 'net.au',
'com.sg', 'org.sg', 'net.sg',
'co.jp', 'or.jp', 'go.jp', 'ac.jp',
'com.hk', 'org.hk', 'net.hk',
];
const parts = hostname.split('.');
const len = parts.length;
if (len <= 2) {
return hostname;
}
const lastTwoParts = `${parts[len - 2]}.${parts[len - 1]}`;
const lastThreeParts = `${parts[len - 3]}.${lastTwoParts}`;
if (specialSuffixes.includes(lastThreeParts)) {
return lastThreeParts;
} else if (specialSuffixes.includes(lastTwoParts)) {
return `${parts[len - 3]}.${lastTwoParts}`;
}
return `${parts[len - 2]}.${parts[len - 1]}`;
}
function getSupportCookieNames(fetchData) {
return fetchData && fetchData.supportNames && fetchData.supportNames.length != 0 ? fetchData.supportNames : null;
}
function showLoading(title) {
const loadingSwal = Swal.fire({
title: title,
allowOutsideClick: false,
showConfirmButton: false,
didOpen: () => {
Swal.showLoading();
}
});
return loadingSwal;
}
async function readCookie() {
const { isConfirmed } = await Swal.fire({
title: '确认读取',
text: '该操作将使用远程Cookie覆盖掉本地的Cookie',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消'
});
if (!isConfirmed) {
return;
}
let readLoading = null;
try {
const rootDomain = getRootDomain();
readLoading = showLoading('加载中...');
const fetchData = await csvDb(DB_FILE.PATH).selectFrom(DB_FILE.FILE).eq('domain', rootDomain).fetchOne();
await readLoading.close();
if (!fetchData) {
Swal.fire('读取失败', 'Cookie不存在,请先创建Cookie', 'error');
return;
}
const supportCookieNames = getSupportCookieNames(fetchData);
let cookies = JSON.parse(fetchData.cookies);
// 检查过期Cookie
const now = Math.floor(Date.now() / 1000); // 当前时间戳(秒)
const expiredCookies = [];
const validCookies = [];
cookies.forEach(cookie => {
if (supportCookieNames != null && !supportCookieNames.includes(cookie.name)) {
return;
}
if (cookie.expirationDate && cookie.expirationDate < now) {
expiredCookies.push(cookie);
} else {
validCookies.push(cookie);
}
});
// 处理过期Cookie
if (expiredCookies.length > 0) {
const expireCookieNames = expiredCookies.map(value => value.name).join(',');
const { isConfirmed } = await Swal.fire({
title: '存在过期Cookie',
html: `有 ${expiredCookies.length} 个Cookie已过期\n是否强制写入?\n${expireCookieNames}`,
icon: 'question',
showCancelButton: true,
confirmButtonText: '强制写入',
cancelButtonText: '取消操作',
});
if (!isConfirmed) {
return;
}
}
// 先删除原有Cookie
const deletePromises = cookies.map(cookie =>
new Promise((resolve, reject) => {
GM_cookie.delete({
name: cookie.name,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.httpOnly
}, error => {
error ? reject(error) : resolve();
});
})
);
await Promise.all(deletePromises);
const setCookiePromises = validCookies.map(cookie =>
new Promise((resolve, reject) => {
GM_cookie.set(cookie, (error) => {
error ? reject(error) : resolve();
});
})
);
await Promise.all(setCookiePromises);
Swal.fire({
title: '读取成功',
text: 'Cookie已成功写入,页面即将刷新',
icon: 'success',
confirmButtonText: '确认'
}).then(() => {
window.location.reload();
});
} catch (error) {
if (readLoading) {
await readLoading.close();
}
Swal.fire('读取失败', `错误信息: ${error.message || error}`, 'error');
}
}
async function createDbIfNotExist() {
let readLoading = null;
let success = false;
try {
readLoading = showLoading('检查数据库...');
const dbCreated = await csvDb(DB_FILE.PATH).createIfNotExist(DB_FILE.FILE, ['domain', 'supportNames', 'cookies', 'createTime', 'updateTime']);
await readLoading.close();
if (dbCreated) {
console.log('[Cookie管理器] 数据库不存在,已创建数据库');
}
success = true;
} catch (error) {
if (readLoading) {
await readLoading.close();
}
Swal.fire('创建数据库失败', `错误信息: ${error.message || error}`, 'error');
}
return success;
}
async function setSupportCookieNames() {
if (!await createDbIfNotExist()) {
return;
}
let readLoading = null;
let saveLoading = null;
try {
const domain = getRootDomain();
readLoading = showLoading('加载中...');
const existingRecord = await csvDb(DB_FILE.PATH)
.selectFrom(DB_FILE.FILE)
.eq('domain', domain)
.fetchOne();
await readLoading.close();
let supportCookieNames = existingRecord ? existingRecord.supportNames : '';
const { value, isConfirmed } = await Swal.fire({
title: '允许的Cookie名',
input: 'text',
inputValue: supportCookieNames,
inputLabel: '留空则同步所有Cookie,否则同步指定Cookie',
inputPlaceholder: '多个名称用逗号分隔,例如: session, token',
inputAttributes: {
'aria-label': '留空则同步所有Cookie,否则同步指定Cookie'
},
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消',
// 添加自定义按钮
showDenyButton: true,
denyButtonText: '解析必要Cookie',
preDeny: () => {
try {
Swal.getDenyButton().disabled = true;
const result = parseRequireCookie();
if (!result) {
Swal.showValidationMessage('无法解析当前网站必要Cookie');
}
Swal.getInput().value = result;
Swal.getDenyButton().disabled = false;
return false;
} catch (error) {
Swal.getDenyButton().disabled = false;
Swal.showValidationMessage(`解析失败: ${error.message || error}`);
return false;
}
}
});
if (!isConfirmed) {
return;
}
const now = Date.now();
saveLoading = showLoading('保存中...');
if (existingRecord) {
await csvDb(DB_FILE.PATH)
.update(DB_FILE.FILE)
.eq('domain', domain)
.set('supportNames', value)
.set('updateTime', now)
.execute();
} else {
await csvDb(DB_FILE.PATH)
.insertInto(DB_FILE.FILE)
.value({
domain,
cookies: '',
supportNames: value,
createTime: now,
updateTime: now
})
.execute();
}
await saveLoading.close();
Swal.fire('设置成功', '允许的Cookie名已成功保存到数据库', 'success');
} catch (error) {
if (readLoading) {
await readLoading.close();
}
if (saveLoading) {
await saveLoading.close();
}
Swal.fire('设置失败', `错误信息: ${error.message || error}`, 'error');
}
}
async function writeCookie() {
const { isConfirmed } = await Swal.fire({
title: '确认保存',
text: '该操作将保存当前网站Cookie到远程,如果已经存在则会覆盖',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消'
});
if (!isConfirmed) {
return;
}
if (!await createDbIfNotExist()) {
return;
}
let readLoading = null;
let saveLoading = null;
try {
const domain = getRootDomain();
const cookies = await new Promise((resolve, reject) => {
GM_cookie.list({}, (cookies, error) => {
if (error) {
reject(`获取Cookie失败: ${error}`);
return;
}
resolve(cookies);
});
});
readLoading = showLoading('加载中...');
const existingRecord = await csvDb(DB_FILE.PATH)
.selectFrom(DB_FILE.FILE)
.eq('domain', domain)
.fetchOne();
await readLoading.close();
const supportCookieNames = getSupportCookieNames(existingRecord);
const validCookies = [];
cookies.forEach(cookie => {
if (supportCookieNames != null && !supportCookieNames.includes(cookie.name)) {
return;
}
validCookies.push(cookie);
});
const cookiesStr = JSON.stringify(validCookies);
const now = Date.now();
saveLoading = showLoading('保存中...');
if (existingRecord) {
await csvDb(DB_FILE.PATH)
.update(DB_FILE.FILE)
.eq('domain', domain)
.set('cookies', cookiesStr)
.set('updateTime', now)
.execute();
} else {
await csvDb(DB_FILE.PATH)
.insertInto(DB_FILE.FILE)
.value({
domain,
cookies: cookiesStr,
supportNames: '',
createTime: now,
updateTime: now
})
.execute();
}
await readLoading.close();
Swal.fire('保存成功', 'Cookie已成功保存到数据库', 'success');
} catch (error) {
if (readLoading) {
await readLoading.close();
}
if (saveLoading) {
await saveLoading.close();
}
Swal.fire('保存失败', `错误信息: ${error.message || error}`, 'error');
}
}
async function clearLocalCookie() {
const { isConfirmed } = await Swal.fire({
title: '确认清空',
text: '该操作将清空本地所有的Cookie',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消'
});
if (!isConfirmed) {
return;
}
try {
const rootDomain = getRootDomain();
const allCookies = await new Promise((resolve, reject) => {
GM_cookie.list({ domain: rootDomain }, (cookies, error) => {
error ? reject(error) : resolve(cookies);
});
});
if (!allCookies || allCookies.length === 0) {
Swal.fire('清除成功', '当前域名下没有找到可清除的 Cookie', 'success');
return;
}
const deletePromises = allCookies.map(cookie =>
new Promise((resolve, reject) => {
GM_cookie.delete({
name: cookie.name,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.httpOnly
}, error => {
error ? reject(error) : resolve();
});
})
);
await Promise.all(deletePromises);
Swal.fire({
title: '清除成功',
text: `已成功删除 ${allCookies.length} 个 Cookie,页面即将刷新`,
icon: 'success',
confirmButtonText: '确认'
}).then(() => {
window.location.reload();
});
} catch (error) {
Swal.fire('清除失败', `错误信息: ${error.message || error}`, 'error');
}
}
async function showCookieManager() {
let readLoading = null;
try {
readLoading = showLoading('加载中...');
const cookies = await csvDb(DB_FILE.PATH).selectFrom(DB_FILE.FILE).fetch();
await readLoading.close();
let tableHTML = `
<style>
.cookie-manager-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.cookie-manager-table th,
.cookie-manager-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
border-right: 1px solid #ddd;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cookie-manager-table th {
background-color: #f2f2f2;
position: sticky;
top: 0;
font-weight: bold;
}
.cookie-manager-table tr:last-child td {
border-bottom: none;
}
.cookie-manager-table td:last-child,
.cookie-manager-table th:last-child {
border-right: none;
}
.cookie-manager-container {
max-height: 60vh;
overflow-y: auto;
}
.delete-btn {
background-color: #ff6b6b;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
}
.delete-btn:hover {
background-color: #ff5252;
}
.delete-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>
<div class="cookie-manager-container">
<table class="cookie-manager-table">
<thead>
<tr>
<th style="width: 20%;">域名</th>
<th style="width: 20%;">允许Cookie名</th>
<th style="width: 50%;">值</th>
<th style="width: 10%;">操作</th>
</tr>
</thead>
<tbody>
`;
cookies.forEach(cookie => {
tableHTML += `
<tr>
<td>${escapeHTML(cookie.domain)}</td>
<td>${getSupportCookieNames(cookie) ? escapeHTML(cookie.supportNames) : '全部'}</td>
<td>${escapeHTML(cookie.cookies)}</td>
<td>
<button class="delete-btn"
data-domain="${escapeHTML(cookie.domain)}">
删除
</button>
</td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
</div>
`;
const { isDismissed } = await Swal.fire({
title: 'Cookie管理',
html: tableHTML,
width: '80%',
showConfirmButton: false,
showCloseButton: true,
didOpen: () => {
document.querySelectorAll('.delete-btn').forEach(button => {
button.addEventListener('click', async (e) => {
const btn = e.currentTarget;
const targetDomain = btn.dataset.domain;
btn.textContent = '删除中...';
btn.disabled = true;
try {
const deleteCount = await csvDb(DB_FILE.PATH)
.deleteFrom(DB_FILE.FILE)
.eq('domain', targetDomain)
.execute();
if (deleteCount > 0) {
btn.closest('tr').remove();
}
} catch (error) {
btn.textContent = '删除';
btn.disabled = false;
Swal.fire('删除失败', `无法删除Cookie: ${error.message || error}`, 'error');
}
});
});
}
});
} catch (error) {
if (readLoading) {
await readLoading.close();
}
Swal.fire('加载失败', `无法获取Cookie列表: ${error.message || error}`, 'error');
}
}
function escapeHTML(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function parseRequireCookie() {
const chains = [
new DiscuzCookieFetcher(),
new A115CookieFetcher()
];
for (let i = 0; i < chains.length; i++) {
const fetcher = chains[i];
if (fetcher.support()) {
return fetcher.parseCookies().join(',');
}
}
return null;
}
class RequireCookieFetcher {
support() {
return false;
}
parseCookies() {
return null;
}
}
class DiscuzCookieFetcher extends RequireCookieFetcher {
support() {
const html = document.documentElement.outerHTML;
return /discuz_uid\s*=\s*(['"])?\d+\1/.test(html);
}
parseCookies() {
const html = document.documentElement.outerHTML;
const match = html.match(/cookiepre\s*=\s*(['"])([^'"]+)\1/);
if (match) {
return [
`${match[2]}auth`,
`${match[2]}saltkey`
];
}
return null;
}
}
class A115CookieFetcher extends RequireCookieFetcher {
support() {
return window.location.hostname.includes('115.com');
}
parseCookies() {
return ['UID', 'CID', 'SEID', 'KID']
}
}
GM_registerMenuCommand('⚙️ 设置GitHub仓库', showGitConfigDialog);
GM_registerMenuCommand('❌ 清除GitHub仓库配置', clearGitConfig);
GM_registerMenuCommand('👉保存网站Cookie到仓库', writeCookie);
GM_registerMenuCommand('👉从仓库读取网站Cookie', readCookie);
GM_registerMenuCommand('👉设置允许的Cookie名', setSupportCookieNames);
GM_registerMenuCommand('👉管理仓库Cookie', showCookieManager);
GM_registerMenuCommand('👉清空网站本地Cookie', clearLocalCookie);
// 添加样式
const style = document.createElement('style');
style.innerHTML = `
.swal2-popup {
font-size: 1.6rem !important;
}
.swal2-input, .swal2-file, .swal2-textarea {
font-size: 1.8rem !important;
}
`;
document.head.appendChild(style);
console.log('[Cookie管理器] 加载成功');
})();