// ==UserScript== // @name:zh-CN 魔搭社区限额查询 // @name ModelScope-Ratelimit-Check // @namespace https://github.com/RUnknown/modelscope-ratelimit-check // @version 3.0 // @description 用于查询 ModelScope API 用量与额度,自动保存查询记录 // @author RUnknown // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // ==================== 常量 ==================== const API_URL = 'https://api-inference.modelscope.cn/v1/chat/completions'; const STORAGE = { KEYS: 'modelscope_ratelimit_keys', ACTIVE_KEY_ID: 'modelscope_ratelimit_active_key_id', DARK: 'modelscope_ratelimit_dark_mode', LANG: 'modelscope_ratelimit_lang', API_KEY_HISTORY: 'modelscope_ratelimit_api_key_history', MODEL_HISTORY: 'modelscope_ratelimit_model_history' }; const DEFAULT_MODELS = [ 'deepseek-ai/DeepSeek-V4-Flash', 'deepseek-ai/DeepSeek-V4-Pro', 'ZhipuAI/GLM-5.1', 'MiniMax/MiniMax-M2.5', 'moonshotai/Kimi-K2.5' ]; // ==================== 国际化 ==================== const LANG = { zh: { title: '🔮 ModelScope 限额查询', save: '💾 保存', clearAll: '🗑️ 清空所有内容', resetModels: '🔄 恢复默认模型', darkMode: '深色模式', lightMode: '浅色模式', close: '关闭面板', apiKey: '🔑 API Key', query: '🚀 查询限额', addModel: '📦 添加模型', addBtn: '添加', accountLimit: '账户限额', accountRemaining: '账户剩余', used: '已使用', model: '模型', modelLimit: '模型限额', modelRemaining: '模型剩余', status: '状态', actions: '操作', duration: '耗时', notQueried: '未查询', querying: '查询中...', success: '✅ 成功', error: '❌ ', configSaved: '✅ 配置已保存', restoredDefaults: '✅ 已恢复默认模型', clearedAll: '✅ 已清空所有内容', queryComplete: '✨ 查询完成!', removeModelConfirm: '确定要移除模型', clearAllConfirm: '确定要清空所有内容吗?包括 API Key、模型列表和查询结果。', needApiKey: '请输入 API Key', needModel: '模型列表为空,请先恢复默认模型', modelExists: '模型已在列表中', copySuccess: '已复制', keyNamePlaceholder: '输入 Key 名称', addKey: '添加 Key', deleteKey: '删除当前 Key', renameKey: '重命名', keyName: 'Key 名称', enterKeyName: '请输入 Key 名称', keyNameExists: '名称已存在', langSwitch: 'EN', reminder: '注意:每次查询将消耗 1 次模型额度。' }, en: { title: '🔮 ModelScope Rate Limit Check', save: '💾 Save', clearAll: '🗑️ Clear All', resetModels: '🔄 Reset Default Models', darkMode: 'Dark Mode', lightMode: 'Light Mode', close: 'Close Panel', apiKey: '🔑 API Key', query: '🚀 Check Limits', addModel: '📦 Add Model', addBtn: 'Add', accountLimit: 'Account Limit', accountRemaining: 'Account Remaining', used: 'Used', model: 'Model', modelLimit: 'Model Limit', modelRemaining: 'Model Remaining', status: 'Status', actions: 'Actions', duration: 'Duration', notQueried: 'Not queried', querying: 'Querying...', success: '✅ Success', error: '❌ ', configSaved: '✅ Config saved', restoredDefaults: '✅ Default models restored', clearedAll: '✅ All cleared', queryComplete: '✨ Query complete!', removeModelConfirm: 'Are you sure you want to remove model ', clearAllConfirm: 'Are you sure you want to clear everything? (API Key, models, results)', needApiKey: 'Please enter an API Key', needModel: 'Model list is empty, please restore defaults', modelExists: 'Model already exists', copySuccess: 'Copied', keyNamePlaceholder: 'Enter key name', addKey: 'Add Key', deleteKey: 'Delete current key', renameKey: 'Rename', keyName: 'Key Name', enterKeyName: 'Enter key name', keyNameExists: 'Name already exists', langSwitch: '中文', reminder: 'Note: Each query consumes 1 model request quota.' } }; let currentLang = GM_getValue(STORAGE.LANG, 'zh'); function t(key) { return LANG[currentLang]?.[key] || key; } // ==================== 数据管理(多 Key) ==================== function getAllKeys() { const raw = GM_getValue(STORAGE.KEYS, '[]'); try { return JSON.parse(raw); } catch(e) { return []; } } function setAllKeys(keys) { GM_setValue(STORAGE.KEYS, JSON.stringify(keys)); } function getActiveKeyId() { return GM_getValue(STORAGE.ACTIVE_KEY_ID, null); } function setActiveKeyId(id) { GM_setValue(STORAGE.ACTIVE_KEY_ID, id); } function getActiveKeyData() { const keys = getAllKeys(); const activeId = getActiveKeyId(); if (activeId && keys.length) { return keys.find(k => k.id === activeId) || null; } return null; } function getCurrentApiKey() { return getActiveKeyData()?.key || ''; } function getCurrentModels() { const k = getActiveKeyData(); return k?.models || []; } function getCurrentResults() { const k = getActiveKeyData(); return k?.results || {}; } function updateCurrentKey(updates) { const keys = getAllKeys(); const activeId = getActiveKeyId(); const idx = keys.findIndex(k => k.id === activeId); if (idx !== -1) { keys[idx] = { ...keys[idx], ...updates }; setAllKeys(keys); } } // 历史记录 function getHistory(key) { const raw = GM_getValue(key, '[]'); try { return JSON.parse(raw); } catch(e) { return []; } } function addHistory(key, value) { if (!value) return; const arr = getHistory(key).filter(v => v !== value); arr.unshift(value); if (arr.length > 20) arr.pop(); GM_setValue(key, JSON.stringify(arr)); } // ==================== 面板 & UI ==================== let panel = null; let isDark = GM_getValue(STORAGE.DARK, false); let sortColumn = null; let sortAsc = true; GM_registerMenuCommand('ModelScope 限额查询', showPanel); function injectGlobalStyles() { if (document.getElementById('ms-ratelimit-style')) return; const style = document.createElement('style'); style.id = 'ms-ratelimit-style'; style.textContent = ` :root { --ms-blue: #1E88E5; --ms-blue-dark: #1565C0; --ms-blue-light: #64B5F6; --ms-gray-50: #F8FAFC; --ms-gray-100: #F1F5F9; --ms-gray-200: #E2E8F0; --ms-gray-600: #475569; --ms-gray-800: #1E293B; --ms-gray-900: #0F172A; } .ms-ratelimit-panel { --bg: #FFFFFF; --bg2: #F8FAFC; --input-bg: #F1F5F9; --text: #1E293B; --text2: #475569; --border: #E2E8F0; --accent: #1E88E5; --accent-hover: #1565C0; --table-head: #1E88E5; --table-head-text: #FFFFFF; --hover-row: rgba(30,136,229,0.04); --shadow: rgba(0,0,0,0.08); --group-bg: #F0F4F8; } .ms-ratelimit-panel.dark { --bg: #1E1E2E; --bg2: #2A2A40; --input-bg: #2D2D46; --text: #E2E8F0; --text2: #94A3B8; --border: #3B3B5C; --accent: #64B5F6; --accent-hover: #42A5F5; --table-head: #2D3A5C; --table-head-text: #E2E8F0; --hover-row: rgba(100,181,246,0.06); --shadow: rgba(0,0,0,0.4); --group-bg: #2A2A40; } .ms-ratelimit-panel { color: var(--text); background: var(--bg); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 20px 50px var(--shadow); font-family: 'Inter', system-ui, -apple-system, sans-serif; transition: background 0.3s, color 0.3s; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 840px; max-width: 94vw; max-height: 90vh; overflow: auto; z-index: 99999; padding: 24px; } .ms-ratelimit-panel * { box-sizing: border-box; } .ms-ratelimit-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .ms-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; } .ms-row label { font-size:13px; font-weight:500; color:var(--text2); white-space:nowrap; } .ms-input { background: var(--input-bg); border:1px solid var(--border); border-radius:8px; padding:8px 12px; font-size:14px; color:var(--text); flex:1; min-width:0; transition: border-color 0.2s; } .ms-input:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 2px rgba(30,136,229,0.15); } .ms-select { background: var(--input-bg); border:1px solid var(--border); border-radius:8px; padding:8px 12px; font-size:14px; color:var(--text); max-width:160px; } .ms-btn { display:inline-flex; align-items:center; justify-content:center; padding:8px 14px; border-radius:8px; font-size:13px; font-weight:500; border:none; cursor:pointer; transition:all 0.2s; white-space:nowrap; gap:4px; } .ms-btn-primary { background:var(--accent); color:#fff; } .ms-btn-primary:hover { background:var(--accent-hover); } .ms-btn-secondary { background:transparent; border:1px solid var(--border); color:var(--text); } .ms-btn-secondary:hover { background:var(--hover-row); } .ms-btn-icon { background:transparent; border:none; color:var(--text2); cursor:pointer; font-size:18px; padding:4px 8px; border-radius:6px; display:inline-flex; align-items:center; justify-content:center; } .ms-btn-icon:hover { background:var(--hover-row); color:var(--accent); } .ms-btn-icon.remove-btn { color:#e53935; } .ms-btn-icon.remove-btn:hover { background:rgba(229,57,53,0.1); } .ms-account-bar { display:flex; gap:24px; justify-content:center; padding:10px 16px; background:var(--bg2); border-radius:10px; margin-bottom:33px; font-size:14px; border:1px solid var(--border); } .ms-account-item { display:flex; align-items:center; gap:6px; } .ms-account-item span:first-child { color:var(--text2); font-weight:500; } .ms-account-item span:last-child { font-weight:600; color:var(--accent); } .ms-table { width:100%; border-collapse:separate; border-spacing:0; border-radius:10px; overflow:hidden; border:1px solid var(--border); font-size:13px; } .ms-table th { background:var(--table-head); color:var(--table-head-text); font-weight:500; padding:8px 10px; text-align:left; position:relative; cursor:pointer; user-select:none; } .ms-table th.sortable::after { content:' ↕'; opacity:0.5; font-size:12px; } .ms-table th.sort-asc::after { content:' ↑'; opacity:1; } .ms-table th.sort-desc::after { content:' ↓'; opacity:1; } .ms-table td { padding:8px 10px; border-bottom:1px solid var(--border); background:var(--bg); vertical-align:middle; } .ms-table tbody tr:last-child td { border-bottom:none; } .ms-table tbody tr:hover td { background:var(--hover-row); } /* 关键修复:强制单元格文本不换行,保持表格整洁 */ // .ms-table th, .ms-table td { .ms-table th { white-space: nowrap; } .model-clickable { cursor:pointer; transition:color 0.2s; } .model-clickable:hover { color:var(--accent); text-decoration:underline; } .group-row td { background:var(--group-bg); font-weight:600; cursor:pointer; padding:6px 10px; } .group-row td:hover { opacity:0.9; } .action-cell { display:flex; gap:4px; align-items:center; } .copy-tooltip { font-size:12px; margin-left:6px; color:var(--accent); } /* 新增:额度消耗提醒 */ .ms-reminder { text-align: center; padding: 8px 16px; margin: -20px auto 16px auto; /* 水平居中 */ background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; color: var(--text2); font-size: 13px; font-weight: 500; max-width: 80%; /* 限制最大宽度,视觉更舒适 */ box-sizing: border-box; } `; document.head.appendChild(style); } // ==================== 主面板构建 ==================== function showPanel() { if (panel) { panel.style.display = 'block'; panel.classList.toggle('dark', isDark); updateAccountBar(); return; } injectGlobalStyles(); // 确保至少有一个 Key let keys = getAllKeys(); if (!keys.length) { const defaultKey = { id: 'default', key: '', models: [...DEFAULT_MODELS], results: {} }; keys = [defaultKey]; setAllKeys(keys); setActiveKeyId('default'); } else if (!getActiveKeyId() || !keys.find(k => k.id === getActiveKeyId())) { setActiveKeyId(keys[0].id); } const activeKey = getActiveKeyData(); panel = document.createElement('div'); panel.className = 'ms-ratelimit-panel' + (isDark ? ' dark' : ''); // 标题栏 const header = document.createElement('div'); header.className = 'ms-ratelimit-header'; const h3 = document.createElement('h3'); h3.style.margin = '0'; h3.textContent = t('title'); header.appendChild(h3); const btnGroup = document.createElement('div'); btnGroup.style.cssText = 'display:flex; gap:6px; flex-wrap:wrap; align-items:center;'; // 语言切换 const langBtn = document.createElement('button'); langBtn.className = 'ms-btn-icon'; langBtn.textContent = t('langSwitch'); langBtn.title = 'Switch Language'; langBtn.onclick = () => { currentLang = currentLang === 'zh' ? 'en' : 'zh'; GM_setValue(STORAGE.LANG, currentLang); panel.remove(); panel = null; showPanel(); }; btnGroup.appendChild(langBtn); // Key 管理区域 const keySelect = document.createElement('select'); keySelect.className = 'ms-select'; keySelect.id = 'key-select'; function refreshKeySelect() { keySelect.innerHTML = ''; getAllKeys().forEach(k => { const opt = document.createElement('option'); opt.value = k.id; opt.textContent = k.id; keySelect.appendChild(opt); }); keySelect.value = getActiveKeyId(); } refreshKeySelect(); keySelect.onchange = () => { setActiveKeyId(keySelect.value); panel.remove(); panel = null; showPanel(); }; btnGroup.appendChild(keySelect); const addKeyBtn = document.createElement('button'); addKeyBtn.className = 'ms-btn-icon'; addKeyBtn.innerHTML = '➕'; addKeyBtn.title = t('addKey'); addKeyBtn.onclick = () => { const name = prompt(t('enterKeyName'), 'key_' + Date.now()); if (!name) return; const keys = getAllKeys(); if (keys.find(k => k.id === name)) { alert(t('keyNameExists')); return; } keys.push({ id: name, key: '', models: [...DEFAULT_MODELS], results: {} }); setAllKeys(keys); setActiveKeyId(name); panel.remove(); panel = null; showPanel(); }; btnGroup.appendChild(addKeyBtn); const delKeyBtn = document.createElement('button'); delKeyBtn.className = 'ms-btn-icon remove-btn'; delKeyBtn.innerHTML = '🗑️'; delKeyBtn.title = t('deleteKey'); delKeyBtn.onclick = () => { const id = getActiveKeyId(); if (!confirm(`Delete key "${id}"?`)) return; let keys = getAllKeys(); keys = keys.filter(k => k.id !== id); setAllKeys(keys); if (keys.length) { setActiveKeyId(keys[0].id); } else { const newKey = { id: 'default', key: '', models: [...DEFAULT_MODELS], results: {} }; setAllKeys([newKey]); setActiveKeyId('default'); } panel.remove(); panel = null; showPanel(); }; btnGroup.appendChild(delKeyBtn); // 清空所有内容按钮 const clearAllBtn = document.createElement('button'); clearAllBtn.className = 'ms-btn ms-btn-secondary'; clearAllBtn.textContent = t('clearAll'); clearAllBtn.onclick = () => { if (!confirm(t('clearAllConfirm'))) return; updateCurrentKey({ key: '', models: [], results: {} }); document.getElementById('ratelimit-api-key').value = ''; renderStoredResults(); updateAccountBar(); showStatus(t('clearedAll')); }; btnGroup.appendChild(clearAllBtn); // 恢复默认模型 const resetModelsBtn = document.createElement('button'); resetModelsBtn.className = 'ms-btn ms-btn-secondary'; resetModelsBtn.textContent = t('resetModels'); resetModelsBtn.onclick = () => { updateCurrentKey({ models: [...DEFAULT_MODELS], results: {} }); renderStoredResults(); updateAccountBar(); showStatus(t('restoredDefaults')); }; btnGroup.appendChild(resetModelsBtn); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.className = 'ms-btn ms-btn-secondary'; saveBtn.textContent = t('save'); saveBtn.onclick = () => saveAllConfig(); btnGroup.appendChild(saveBtn); // 深色模式 const darkBtn = document.createElement('button'); darkBtn.className = 'ms-btn-icon'; darkBtn.innerHTML = isDark ? '☀️' : '🌙'; darkBtn.title = isDark ? t('lightMode') : t('darkMode'); darkBtn.onclick = () => { isDark = !isDark; GM_setValue(STORAGE.DARK, isDark); panel.classList.toggle('dark', isDark); darkBtn.innerHTML = isDark ? '☀️' : '🌙'; }; btnGroup.appendChild(darkBtn); // 关闭 const closeBtn = document.createElement('button'); closeBtn.className = 'ms-btn-icon'; closeBtn.innerHTML = '✕'; closeBtn.title = t('close'); closeBtn.onclick = () => { panel.style.display = 'none'; }; btnGroup.appendChild(closeBtn); header.appendChild(btnGroup); panel.appendChild(header); // API Key 输入行 const apiRow = document.createElement('div'); apiRow.className = 'ms-row'; const apiLabel = document.createElement('label'); apiLabel.textContent = t('apiKey'); apiRow.appendChild(apiLabel); const apiKeyHistDatalist = document.createElement('datalist'); apiKeyHistDatalist.id = 'api-key-history'; function updateApiKeyDatalist() { apiKeyHistDatalist.innerHTML = ''; getHistory(STORAGE.API_KEY_HISTORY).forEach(v => { const opt = document.createElement('option'); opt.value = v; apiKeyHistDatalist.appendChild(opt); }); } updateApiKeyDatalist(); const keyInput = document.createElement('input'); keyInput.type = 'text'; keyInput.className = 'ms-input'; keyInput.id = 'ratelimit-api-key'; keyInput.value = getCurrentApiKey(); keyInput.placeholder = 'ms-xxxxxxxx'; keyInput.setAttribute('list', 'api-key-history'); apiRow.appendChild(keyInput); apiRow.appendChild(apiKeyHistDatalist); const queryBtn = document.createElement('button'); queryBtn.className = 'ms-btn ms-btn-primary'; queryBtn.textContent = t('query'); queryBtn.onclick = () => { const apiKey = keyInput.value.trim(); const models = getCurrentModels(); if (!apiKey) { alert(t('needApiKey')); return; } if (!models.length) { alert(t('needModel')); return; } updateCurrentKey({ key: apiKey }); addHistory(STORAGE.API_KEY_HISTORY, apiKey); updateApiKeyDatalist(); queryAllBalances(apiKey, models); }; apiRow.appendChild(queryBtn); panel.appendChild(apiRow); // 添加模型行 const addRow = document.createElement('div'); addRow.className = 'ms-row'; const addLabel = document.createElement('label'); addLabel.textContent = t('addModel'); addRow.appendChild(addLabel); const modelHistDatalist = document.createElement('datalist'); modelHistDatalist.id = 'model-history'; function updateModelDatalist() { modelHistDatalist.innerHTML = ''; const allModels = new Set([...DEFAULT_MODELS, ...getCurrentModels(), ...getHistory(STORAGE.MODEL_HISTORY)]); allModels.forEach(m => { const opt = document.createElement('option'); opt.value = m; modelHistDatalist.appendChild(opt); }); } updateModelDatalist(); const modelInput = document.createElement('input'); modelInput.type = 'text'; modelInput.className = 'ms-input'; modelInput.id = 'ratelimit-add-model'; modelInput.placeholder = '例如 Qwen/Qwen2.5-7B-Instruct'; modelInput.setAttribute('list', 'model-history'); addRow.appendChild(modelInput); addRow.appendChild(modelHistDatalist); const addBtn = document.createElement('button'); addBtn.className = 'ms-btn ms-btn-secondary'; addBtn.textContent = t('addBtn'); addBtn.onclick = () => { const newModel = modelInput.value.trim(); if (!newModel) return; const models = getCurrentModels(); if (models.includes(newModel)) { alert(t('modelExists')); return; } models.push(newModel); updateCurrentKey({ models: models }); addHistory(STORAGE.MODEL_HISTORY, newModel); updateModelDatalist(); modelInput.value = ''; addModelRowToTable(newModel); saveAllConfig(); }; addRow.appendChild(addBtn); panel.appendChild(addRow); // 账户概览 const accountBar = document.createElement('div'); accountBar.className = 'ms-account-bar'; accountBar.id = 'ratelimit-account-bar'; accountBar.innerHTML = `