// ==UserScript== // @name 青龙面板数据采集器 - Cookie/LocalStorage管理器 // @namespace https://www.52pojie.cn/home.php?mod=space&uid=1034393 // @version 3.5 // @description 支持最新版青龙面板Client ID/Secret认证(Header方式),修复所有语法错误,支持HTTPS页面请求HTTP // @author auralife // @match https://developer.aliyun.com/* // @icon https://ts1.tc.mm.bing.net/th/id/ODF.b3kRz8VbzcklQgIluTHuYA?w=32&h=32&qlt=90&pcl=fffffa&o=6&pid=1.2 // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect * // @license MIT // ==/UserScript== (function() { 'use strict'; // ========== 用户配置区 ========== const CONFIG = { // 青龙面板配置(新版支持多种认证方式) QINGLONG: { URL: 'http://127.0.0.1:5700', // 你的青龙面板地址 // 方式1:使用Client ID + Client Secret(新版推荐,通过Header传递) CLIENT_ID: '', // 你的Client ID CLIENT_SECRET: '', // 你的Client Secret // // 方式2:使用Token(旧版) // TOKEN: '', // 例如:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // // 方式3:使用用户名密码(自动登录获取token) USERNAME: '', // 例如:'admin' PASSWORD: '' // 例如:'admin123' }, // 要保存的环境变量KEY ENV_KEY: 'COOKIE_ALIYUN', // 例如:'COOKIE_ALIYUN' // 插件显示位置 POSITION: 'top-right', // 认证方式优先级:client > token > password AUTH_PRIORITY: ['client', 'token', 'password'], // 调试模式:true会在控制台输出详细信息 DEBUG: false }; // =============================== // 状态管理 const state = { selectedCookies: new Set(), selectedLocal: new Set(), selectedSession: new Set(), editedValue: '', originalValue: '', currentData: { cookies: [], localStorage: [], sessionStorage: [] } }; // Token管理 const tokenManager = { token: null, expiry: null, // 调试日志 log: function(...args) { if (CONFIG.DEBUG) { console.log('[青龙编辑器]', ...args); } }, // 从Client ID/Secret获取token (正确的OpenAPI方式) // 从Client ID/Secret获取token (根据你的API文档修正) getTokenFromClient: async function() { if (!CONFIG.QINGLONG.CLIENT_ID || !CONFIG.QINGLONG.CLIENT_SECRET) { return null; } try { const baseUrl = CONFIG.QINGLONG.URL.replace(/\/$/, ''); const url = `${baseUrl}/open/auth/token?client_id=${encodeURIComponent(CONFIG.QINGLONG.CLIENT_ID)}&client_secret=${encodeURIComponent(CONFIG.QINGLONG.CLIENT_SECRET)}`; this.log('尝试Client认证 URL:', url); const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('请求超时(10秒)')); }, 10000); GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'application/json' }, onload: (res) => { clearTimeout(timeout); this.log('认证响应状态:', res.status); if (res.status >= 200 && res.status < 300) { try { const data = JSON.parse(res.responseText); this.log('解析后的数据:', data); let token = null; if (data.data && data.data.token) { token = data.data.token; } else if (data.token) { token = data.token; } if (token) { this.token = token; // 如果有过期时间字段,按实际情况处理 if (data.data && data.data.expiration) { this.expiry = data.data.expiration * 1000; } else { this.expiry = Date.now() + 7200 * 1000; // 默认2小时 } this.log('✅ Client认证成功,token:', token.substring(0, 20) + '...'); resolve(token); // ✅ 正确决议 } else { this.log('❌ 响应中没有找到token'); resolve(null); // ✅ 返回 null,让上层尝试其他方式 } } catch (e) { this.log('❌ 解析JSON失败:', e.message); resolve(null); // ✅ 解析失败也返回 null } } else { this.log('❌ HTTP错误:', res.status); resolve(null); // ✅ HTTP错误也返回 null } }, onerror: (err) => { clearTimeout(timeout); this.log('❌ 网络错误:', err); reject(new Error('网络请求失败')); // 网络错误可以 reject }, ontimeout: () => { clearTimeout(timeout); reject(new Error('请求超时')); // 超时 reject } }); }); return response; // 这里 response 就是 resolve 传出的 token 或 null } catch (e) { this.log('Client认证异常:', e.message); return null; // 发生异常(如超时、网络错误)时返回 null } }, // 从用户名密码登录获取token getTokenFromPassword: async function() { if (!CONFIG.QINGLONG.USERNAME || !CONFIG.QINGLONG.PASSWORD) { this.log('未配置用户名密码'); return null; } try { const baseUrl = CONFIG.QINGLONG.URL.replace(/\/$/, ''); const url = `${baseUrl}/api/login`; this.log('尝试密码登录:', url); const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ username: CONFIG.QINGLONG.USERNAME, password: CONFIG.QINGLONG.PASSWORD }), onload: function(res) { resolve({ ok: res.status >= 200 && res.status < 300, status: res.status, responseText: res.responseText, json: function() { try { return Promise.resolve(JSON.parse(res.responseText)); } catch (e) { return Promise.reject(e); } } }); }, onerror: function(err) { reject(err); } }); }); if (!response.ok) { this.log('密码登录HTTP错误:', response.status); return null; } const data = await response.json(); this.log('密码登录响应:', data); let token = null; if (data.data && data.data.token) { token = data.data.token; } else if (data.token) { token = data.token; } if (token) { this.token = token; this.log('✅ 密码登录成功'); return token; } return null; } catch (e) { this.log('密码登录异常:', e); return null; } }, // 从页面获取已有token getTokenFromPage: function() { // 从localStorage获取 let token = localStorage.getItem('token'); if (token) { if (token.startsWith('"') && token.endsWith('"')) { try { token = JSON.parse(token); } catch (e) { // 忽略解析错误 } } this.log('从localStorage获取token'); return token; } // 从cookie获取 const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'token') { this.log('从cookie获取token'); return decodeURIComponent(value); } } // 从配置中获取 if (CONFIG.QINGLONG.TOKEN) { this.log('从配置获取token'); return CONFIG.QINGLONG.TOKEN; } this.log('未找到token'); return null; }, // 获取有效token getValidToken: async function() { // 如果已有token且未过期,直接返回 if (this.token && this.expiry && this.expiry > Date.now()) { this.log('使用缓存的token,剩余时间:', Math.round((this.expiry - Date.now())/1000) + '秒'); return this.token; } this.log('获取新token,优先级:', CONFIG.AUTH_PRIORITY); // 按优先级尝试不同认证方式 for (const method of CONFIG.AUTH_PRIORITY) { this.log(`尝试认证方式: ${method}`); let token = null; try { switch (method) { case 'client': token = await this.getTokenFromClient(); break; case 'token': token = this.getTokenFromPage(); break; case 'password': token = await this.getTokenFromPassword(); break; } } catch (e) { this.log(`认证方式 ${method} 失败:`, e.message); continue; } if (token) { this.token = token; this.log(`✅ 使用 ${method} 方式认证成功`); return token; } } throw new Error('无法获取有效token,请检查认证配置'); }, // 清理token clearToken: function() { this.token = null; this.expiry = null; this.log('token已清理'); } }; // 带认证的请求函数 (使用GM_xmlhttpRequest) async function qinglongRequest(endpoint, options = {}) { options.retryCount = options.retryCount || 0; try { const token = await tokenManager.getValidToken(); if (!token) throw new Error('无法获取有效token'); tokenManager.log('使用token:', token.substring(0, 20) + '...'); const baseUrl = CONFIG.QINGLONG.URL.replace(/\/$/, ''); const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}/open${endpoint}`; tokenManager.log('请求URL:', url); tokenManager.log('请求方法:', options.method || 'GET'); return await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url: url, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json', ...options.headers }, data: options.body, onload: function(res) { tokenManager.log('响应状态码:', res.status); tokenManager.log('响应体预览:', res.responseText.substring(0, 200)); if (res.status === 401) { tokenManager.log('token已过期,尝试刷新'); tokenManager.clearToken(); if (options.retryCount < 1) { // 只重试一次 options.retryCount++; qinglongRequest(endpoint, options) .then(resolve) .catch(reject); } else { reject(new Error('多次401,认证失败,请检查token权限')); } return; } resolve({ ok: res.status >= 200 && res.status < 300, status: res.status, statusText: res.statusText, responseText: res.responseText, json: function() { try { return Promise.resolve(JSON.parse(res.responseText)); } catch (e) { return Promise.reject(e); } } }); }, onerror: function(err) { tokenManager.log('请求错误:', err); reject(new Error('网络请求失败')); } }); }); } catch (error) { tokenManager.log('请求失败:', error); throw error; } } // 添加样式 GM_addStyle(` .data-editor-panel { position: fixed; ${CONFIG.POSITION.includes('top') ? 'top: 20px;' : 'bottom: 20px;'} ${CONFIG.POSITION.includes('right') ? 'right: 20px;' : 'left: 20px;'} width: 700px; max-width: 90vw; background: white; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; } .data-editor-header { padding: 12px 16px; background: #f8f9fa; border-bottom: 1px solid #eee; border-radius: 8px 8px 0 0; font-weight: 600; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .data-editor-tabs { display: flex; border-bottom: 1px solid #dee2e6; background: #f8f9fa; padding: 0 16px; } .data-editor-tab { padding: 10px 16px; cursor: pointer; border: 1px solid transparent; border-bottom: none; margin-bottom: -1px; color: #495057; } .data-editor-tab.active { color: #007bff; background: white; border-color: #dee2e6 #dee2e6 white; border-radius: 4px 4px 0 0; font-weight: 500; } .data-editor-content { padding: 16px; max-height: 500px; overflow-y: auto; } .data-item { margin-bottom: 8px; padding: 8px; background: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef; transition: all 0.2s; } .data-item:hover { background: #e9ecef; } .data-item.selected { background: #e3f2fd; border-color: #2196f3; } .data-item-header { display: flex; align-items: center; gap: 8px; cursor: pointer; } .data-item-checkbox { margin: 0; width: 16px; height: 16px; cursor: pointer; } .data-item-key { font-weight: 600; color: #2196f3; flex: 1; } .data-item-value { color: #28a745; font-family: monospace; font-size: 12px; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .search-box { margin-bottom: 12px; padding: 8px; border: 1px solid #ced4da; border-radius: 4px; width: 100%; box-sizing: border-box; } .select-all-bar { padding: 8px 12px; background: #e9ecef; border-radius: 4px; margin-bottom: 12px; display: flex; align-items: center; gap: 12px; } .editor-section { margin-top: 16px; border: 1px solid #dee2e6; border-radius: 4px; } .editor-section-title { padding: 8px 12px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; font-weight: 500; display: flex; justify-content: space-between; align-items: center; } .editor-textarea { width: 100%; min-height: 200px; padding: 12px; border: none; border-bottom: 1px solid #dee2e6; font-family: monospace; font-size: 13px; resize: vertical; box-sizing: border-box; } .editor-textarea:focus { outline: none; background: #f8f9fa; } .button-group { padding: 12px; display: flex; gap: 8px; flex-wrap: wrap; background: #f8f9fa; border-top: 1px solid #dee2e6; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; transition: all 0.2s; white-space: nowrap; } .btn-primary { background: #007bff; color: white; } .btn-primary:hover { background: #0056b3; } .btn-success { background: #28a745; color: white; } .btn-success:hover { background: #218838; } .btn-info { background: #17a2b8; color: white; } .btn-info:hover { background: #138496; } .btn-warning { background: #ffc107; color: #212529; } .btn-warning:hover { background: #e0a800; } .btn-danger { background: #dc3545; color: white; } .btn-danger:hover { background: #c82333; } .btn-secondary { background: #6c757d; color: white; } .btn-secondary:hover { background: #5a6268; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .status-message { margin: 8px 16px 16px; padding: 8px 12px; border-radius: 4px; font-size: 13px; } .status-success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; } .status-error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; } .status-info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; } .badge { display: inline-block; padding: 3px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; } .badge-info { background: #17a2b8; color: white; } .float-btn { position: fixed; ${CONFIG.POSITION.includes('top') ? 'top: 80px;' : 'bottom: 80px;'} ${CONFIG.POSITION.includes('right') ? 'right: 20px;' : 'left: 20px;'} width: 48px; height: 48px; background: #007bff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 99998; font-size: 20px; transition: all 0.3s; } .float-btn:hover { transform: scale(1.1); background: #0056b3; } .auth-guide { padding: 16px; background: #e3f2fd; border-radius: 4px; margin-bottom: 16px; border-left: 4px solid #2196f3; } .auth-info { padding: 8px 12px; background: #f8f9fa; border-radius: 4px; margin: 8px 16px; font-size: 12px; border-left: 3px solid #28a745; } `); // 获取所有cookie function getAllCookies() { return document.cookie.split(';') .filter(c => c.trim()) .map(cookie => { const [key, ...valueParts] = cookie.trim().split('='); return { key: key.trim(), value: valueParts.join('='), type: 'cookie' }; }); } // 获取localStorage所有数据 function getAllLocalStorage() { const items = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); try { const value = localStorage.getItem(key); items.push({ key, value, type: 'local' }); } catch (e) { console.warn('读取localStorage失败', key, e); } } return items; } // 获取sessionStorage所有数据 function getAllSessionStorage() { const items = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); try { const value = sessionStorage.getItem(key); items.push({ key, value, type: 'session' }); } catch (e) { console.warn('读取sessionStorage失败', key, e); } } return items; } // 刷新数据 function refreshData() { state.currentData = { cookies: getAllCookies(), localStorage: getAllLocalStorage(), sessionStorage: getAllSessionStorage() }; } // 根据勾选生成数据 function generateDataFromSelection() { const selectedData = { cookies: state.currentData.cookies .filter(item => state.selectedCookies.has(item.key)) .map(({ key, value }) => ({ key, value })), localStorage: state.currentData.localStorage .filter(item => state.selectedLocal.has(item.key)) .map(({ key, value }) => ({ key, value })), sessionStorage: state.currentData.sessionStorage .filter(item => state.selectedSession.has(item.key)) .map(({ key, value }) => ({ key, value })), url: window.location.href, title: document.title, timestamp: new Date().toISOString() }; return JSON.stringify(selectedData, null, 2); } // 解析青龙返回的数据到勾选状态 function parseDataToSelection(data) { try { const parsed = typeof data === 'string' ? JSON.parse(data) : data; // 清空现有勾选 state.selectedCookies.clear(); state.selectedLocal.clear(); state.selectedSession.clear(); // 勾选cookies if (parsed.cookies && Array.isArray(parsed.cookies)) { parsed.cookies.forEach(cookie => { if (cookie.key) state.selectedCookies.add(cookie.key); }); } // 勾选localStorage if (parsed.localStorage && Array.isArray(parsed.localStorage)) { parsed.localStorage.forEach(item => { if (item.key) state.selectedLocal.add(item.key); }); } // 勾选sessionStorage if (parsed.sessionStorage && Array.isArray(parsed.sessionStorage)) { parsed.sessionStorage.forEach(item => { if (item.key) state.selectedSession.add(item.key); }); } return true; } catch (e) { console.error('解析数据失败', e); return false; } } // 从青龙获取环境变量 async function fetchFromQinglong() { try { const res = await qinglongRequest('/envs'); if (!res.ok) throw new Error(`请求失败: ${res.status}`); const data = await res.json(); tokenManager.log('获取环境变量响应:', data); // OpenAPI可能返回的格式:{code:200, data: [...]} // 或者:{code:200, data: {data: [...]}} let envList = []; if (data.code === 200) { if (Array.isArray(data.data)) { envList = data.data; } else if (data.data && Array.isArray(data.data.data)) { envList = data.data.data; } } tokenManager.log('解析后的环境变量列表:', envList); const targetEnv = envList.find(env => env.name === CONFIG.ENV_KEY); if (!targetEnv) throw new Error(`未找到环境变量 ${CONFIG.ENV_KEY}`); state.originalValue = targetEnv.value; parseDataToSelection(state.originalValue); return state.originalValue; } catch (error) { tokenManager.log('获取环境变量失败:', error); throw error; } } // 保存到青龙 // 替换整个 saveToQinglong 函数 async function saveToQinglong(value) { try { // 先获取所有环境变量 const listRes = await qinglongRequest('/envs'); if (!listRes.ok) throw new Error(`获取环境变量失败: ${listRes.status}`); const listData = await listRes.json(); tokenManager.log('获取环境变量列表响应:', listData); // 解析环境变量列表 let envList = []; if (listData.code === 200) { if (Array.isArray(listData.data)) { envList = listData.data; } else if (listData.data && Array.isArray(listData.data.data)) { envList = listData.data.data; } } const targetEnv = envList.find(env => env.name === CONFIG.ENV_KEY); if (targetEnv) { // 更新环境变量 const updateRes = await qinglongRequest('/envs', { method: 'PUT', body: JSON.stringify({ id: targetEnv.id, name: targetEnv.name, value: value, remarks: targetEnv.remarks || `更新于 ${new Date().toLocaleString()}` }) }); if (!updateRes.ok) throw new Error(`更新失败: ${updateRes.status}`); const updateData = await updateRes.json(); if (updateData.code !== 200) throw new Error(updateData.message); return { action: 'update', value }; } else { // 创建环境变量 const createRes = await qinglongRequest('/envs', { method: 'POST', body: JSON.stringify({ name: CONFIG.ENV_KEY, value: value, remarks: `创建于 ${new Date().toLocaleString()}` }) }); if (!createRes.ok) throw new Error(`创建失败: ${createRes.status}`); const createData = await createRes.json(); if (createData.code !== 200) throw new Error(createData.message); return { action: 'create', value }; } } catch (error) { tokenManager.log('保存到青龙失败:', error); throw error; } } // 渲染数据项列表 function renderDataItems(container, items, type) { if (!container) return; const selectedSet = type === 'cookie' ? state.selectedCookies : type === 'local' ? state.selectedLocal : state.selectedSession; let html = `
`; container.innerHTML = html; // 搜索功能 const searchInput = document.getElementById(`search-${type}`); const itemsContainer = document.getElementById(`items-${type}`); function filterItems(searchText) { const filtered = items.filter(item => item.key.toLowerCase().includes(searchText.toLowerCase()) || String(item.value).toLowerCase().includes(searchText.toLowerCase()) ); itemsContainer.innerHTML = filtered.map(item => `方式1:使用Client ID/Secret(推荐)
1. 登录青龙面板后台
2. 进入"系统设置" → "应用设置"
3. 点击"新建应用",填写名称
4. 获取 Client ID 和 Client Secret
5. 配置到脚本的 CONFIG.QINGLONG 中
方式2:使用Token
从浏览器 localStorage 中获取 token 直接配置
方式3:使用用户名密码
直接配置登录账号密码,脚本自动登录