摸鱼时间让Ai写了个sub2api自动测活工具
- 内容介绍
- 文章标签
- 相关推荐
感谢站里的佬友贡献的公益站
有时候佬们贡献公益站出现大量掉号的情况,请求的时候会挨个去轮询,所以下游可能会长时间导致持续链接却没有响应的状态。
然后,就有了这个自动测活工具脚本,测试过的账号会根据相应的结果自动开启或关闭账号
。
// ==UserScript==
// @name Sub2API 账号模型巡检并自动下线
// @namespace https://sinry.example
// @version 0.1.0
// @description 批量测试账号模型;任一模型异常时自动关闭账号 schedulable
// @match http://XXXX/admin/accounts*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
apiBase: location.origin,
pageSize: 100,
defaultTimeoutMs: 45000,
prompt: 'hi',
onlyCheckSchedulable: false,
stopOnFirstModelFailure: true,
preferredModels: ['gpt-5.4', 'gpt-4o-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini'],
defaultTestModel: 'gpt-5.4',
pageAuthTokenKey: 'auth_token',
authStorageKey: '__sub2api_checker_auth__',
timeoutStorageKey: '__sub2api_checker_timeout_ms__',
testModelStorageKey: '__sub2api_checker_test_model__',
};
function getCachedAuthToken() {
const raw =
localStorage.getItem(CONFIG.pageAuthTokenKey) ||
sessionStorage.getItem(CONFIG.pageAuthTokenKey) ||
localStorage.getItem(CONFIG.authStorageKey) ||
'';
return raw ? (raw.startsWith('Bearer ') ? raw : `Bearer ${raw}`) : '';
}
const state = {
authHeader: getCachedAuthToken(),
timeoutMs: Number(localStorage.getItem(CONFIG.timeoutStorageKey) || CONFIG.defaultTimeoutMs),
testModel: localStorage.getItem(CONFIG.testModelStorageKey) || CONFIG.defaultTestModel,
running: false,
stopRequested: false,
panelReady: false,
collapsed: true,
stats: {
total: 0,
checked: 0,
ok: 0,
enabled: 0,
disabled: 0,
skipped: 0,
failed: 0,
},
};
function log(msg, type = 'info') {
const time = new Date().toLocaleTimeString();
const line = `[${time}] ${msg}`;
console[type === 'error' ? 'error' : 'log'](`[sub2api-checker] ${line}`);
const box = document.querySelector('#sub2api-checker-log');
if (!box) return;
const color =
type === 'error' ? '#ff7875' :
type === 'warn' ? '#ffd666' :
type === 'success' ? '#95de64' : '#d9d9d9';
const row = document.createElement('div');
row.style.color = color;
row.textContent = line;
box.appendChild(row);
box.scrollTop = box.scrollHeight;
}
function saveAuth(auth) {
if (!auth || typeof auth !== 'string') return;
const normalized = auth.startsWith('Bearer ') ? auth : `Bearer ${auth}`;
state.authHeader = normalized;
localStorage.setItem(CONFIG.authStorageKey, normalized);
const input = document.querySelector('#sub2api-checker-auth');
if (input && !input.value) input.value = normalized;
log('已捕获 Authorization', 'success');
}
function saveTimeoutMs(timeoutMs) {
const n = Number(timeoutMs);
if (!Number.isFinite(n) || n < 1000) return false;
state.timeoutMs = n;
localStorage.setItem(CONFIG.timeoutStorageKey, String(n));
const input = document.querySelector('#sub2api-checker-timeout');
if (input) input.value = String(Math.floor(n / 1000));
return true;
}
function saveTestModel(model) {
const normalized = String(model || '').trim();
if (!normalized) return false;
state.testModel = normalized;
localStorage.setItem(CONFIG.testModelStorageKey, normalized);
const input = document.querySelector('#sub2api-checker-test-model');
if (input) input.value = normalized;
return true;
}
function injectAuthSniffer() {
const script = document.createElement('script');
script.textContent = `
(() => {
const emit = (auth) => {
if (!auth) return;
document.dispatchEvent(new CustomEvent('__sub2api_checker_auth__', { detail: auth }));
};
const pickAuth = (headersLike) => {
try {
if (!headersLike) return '';
if (headersLike instanceof Headers) {
return headersLike.get('Authorization') || headersLike.get('authorization') || '';
}
if (Array.isArray(headersLike)) {
for (const [k, v] of headersLike) {
if (String(k).toLowerCase() === 'authorization') return v || '';
}
return '';
}
if (typeof headersLike === 'object') {
for (const key of Object.keys(headersLike)) {
if (key.toLowerCase() === 'authorization') return headersLike[key] || '';
}
}
} catch (_) {}
return '';
};
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function(input, init) {
const auth =
pickAuth(init && init.headers) ||
pickAuth(input && input.headers);
if (auth) emit(auth);
return origFetch.apply(this, arguments);
};
}
const origOpen = XMLHttpRequest.prototype.open;
const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function() {
this.__sub2apiAuth = '';
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
if (String(name).toLowerCase() === 'authorization' && value) {
this.__sub2apiAuth = value;
emit(value);
}
return origSetHeader.apply(this, arguments);
};
})();
`;
document.documentElement.appendChild(script);
script.remove();
document.addEventListener('__sub2api_checker_auth__', (event) => {
saveAuth(event.detail);
});
}
function updateStats() {
const el = document.querySelector('#sub2api-checker-stats');
if (!el) return;
const s = state.stats;
el.textContent = `总数 ${s.total} | 已处理 ${s.checked} | 正常 ${s.ok} | 已启用 ${s.enabled} | 已关闭 ${s.disabled} | 跳过 ${s.skipped} | 异常 ${s.failed}`;
}
function updatePanelCollapsed() {
const shell = document.querySelector('#sub2api-checker-shell');
const root = document.querySelector('#sub2api-checker-panel');
const toggle = document.querySelector('#sub2api-checker-toggle');
if (!root || !toggle || !shell) return;
root.style.width = state.collapsed ? '0px' : '420px';
root.style.opacity = state.collapsed ? '0' : '1';
root.style.marginRight = state.collapsed ? '0px' : '12px';
root.style.pointerEvents = state.collapsed ? 'none' : 'auto';
root.style.transform = state.collapsed ? 'translateX(12px)' : 'translateX(0)';
toggle.textContent = state.collapsed ? '账号巡检' : '收起';
toggle.style.borderRadius = state.collapsed ? '10px 0 0 10px' : '10px';
shell.style.pointerEvents = 'auto';
}
function ensurePanel() {
if (state.panelReady) return;
state.panelReady = true;
const shell = document.createElement('div');
shell.id = 'sub2api-checker-shell';
shell.style.cssText = `
position: fixed;
right: 0;
top: 120px;
z-index: 1000000;
display: flex;
flex-direction: row;
align-items: flex-start;
pointer-events: auto;
`;
document.body.appendChild(shell);
const toggle = document.createElement('button');
toggle.id = 'sub2api-checker-toggle';
toggle.style.cssText = `
padding: 10px 8px;
border: 0;
border-radius: 10px 0 0 10px;
background: #1677ff;
color: #fff;
cursor: pointer;
writing-mode: vertical-rl;
text-orientation: mixed;
box-shadow: 0 8px 24px rgba(0,0,0,.25);
transition: transform .28s ease, box-shadow .28s ease, border-radius .28s ease;
font: 12px/1.2 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,PingFang SC,Microsoft YaHei,sans-serif;
`;
toggle.addEventListener('mouseenter', () => {
toggle.style.transform = 'translateX(-2px)';
toggle.style.boxShadow = '0 10px 28px rgba(0,0,0,.32)';
});
toggle.addEventListener('mouseleave', () => {
toggle.style.transform = 'translateX(0)';
toggle.style.boxShadow = '0 8px 24px rgba(0,0,0,.25)';
});
toggle.addEventListener('click', () => {
state.collapsed = !state.collapsed;
updatePanelCollapsed();
});
shell.appendChild(toggle);
const root = document.createElement('div');
root.id = 'sub2api-checker-panel';
root.style.cssText = `
width: 0;
opacity: 0;
overflow: hidden;
transition: width .28s ease, opacity .22s ease, margin-right .28s ease, transform .28s ease;
transform: translateX(12px);
`;
root.innerHTML = `
<div id="sub2api-checker-panel-inner" style="
width:420px;
background:rgba(16, 18, 27, 0.96);
color:#fff;
border:1px solid #30363d;
border-radius:12px;
box-shadow:0 8px 24px rgba(0,0,0,.35);
font:12px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,PingFang SC,Microsoft YaHei,sans-serif;
overflow:hidden;
">
<div style="padding:12px 14px;border-bottom:1px solid #30363d;font-weight:700;">Sub2API 账号模型巡检</div>
<div style="padding:12px 14px;display:flex;flex-direction:column;gap:8px;">
<label style="display:flex;flex-direction:column;gap:4px;">
<span>Authorization(优先自动捕获,抓不到再手填)</span>
<input id="sub2api-checker-auth" type="text" placeholder="Bearer xxxxxx"
style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
</label>
<label style="display:flex;flex-direction:column;gap:4px;">
<span>单模型超时时间(秒)</span>
<input id="sub2api-checker-timeout" type="number" min="1" step="1" placeholder="45"
style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
</label>
<label style="display:flex;flex-direction:column;gap:4px;">
<span>测试模型</span>
<input id="sub2api-checker-test-model" type="text" placeholder="gpt-5.4"
style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
</label>
<div style="display:flex;gap:8px;align-items:center;">
<button id="sub2api-checker-start" style="flex:1;padding:8px 10px;border:0;border-radius:8px;background:#1677ff;color:#fff;cursor:pointer;">开始巡检</button>
<button id="sub2api-checker-stop" style="flex:1;padding:8px 10px;border:0;border-radius:8px;background:#fa541c;color:#fff;cursor:pointer;">停止</button>
</div>
<div id="sub2api-checker-stats" style="color:#bfbfbf;">总数 0 | 已处理 0 | 正常 0 | 已启用 0 | 已关闭 0 | 跳过 0 | 异常 0</div>
<div id="sub2api-checker-log" style="height:320px;overflow:auto;background:#0b0f17;border:1px solid #30363d;border-radius:8px;padding:8px;"></div>
</div>
</div>
`;
shell.appendChild(root);
const authInput = root.querySelector('#sub2api-checker-auth');
authInput.value = state.authHeader;
authInput.addEventListener('change', () => {
const v = authInput.value.trim();
if (v) saveAuth(v);
});
const timeoutInput = root.querySelector('#sub2api-checker-timeout');
timeoutInput.value = String(Math.floor(state.timeoutMs / 1000));
timeoutInput.addEventListener('change', () => {
const sec = Number(timeoutInput.value || 0);
if (!saveTimeoutMs(sec * 1000)) {
timeoutInput.value = String(Math.floor(state.timeoutMs / 1000));
log('超时时间无效,需大于等于 1 秒', 'error');
return;
}
log(`已设置单模型超时 ${sec} 秒`, 'success');
});
const testModelInput = root.querySelector('#sub2api-checker-test-model');
testModelInput.value = state.testModel;
testModelInput.addEventListener('change', () => {
const model = testModelInput.value.trim();
if (!saveTestModel(model)) {
testModelInput.value = state.testModel;
log('测试模型不能为空', 'error');
return;
}
log(`已设置测试模型 ${state.testModel}`, 'success');
});
root.querySelector('#sub2api-checker-start').addEventListener('click', () => run().catch((err) => {
log(`运行异常:${err.message}`, 'error');
state.running = false;
}));
root.querySelector('#sub2api-checker-stop').addEventListener('click', () => {
state.stopRequested = true;
log('已请求停止,当前请求结束后退出', 'warn');
});
updatePanelCollapsed();
}
async function waitDomReady() {
if (document.body) return;
await new Promise((resolve) => {
const timer = setInterval(() => {
if (document.body) {
clearInterval(timer);
resolve();
}
}, 50);
});
}
async function apiFetch(url, options = {}) {
const headers = new Headers(options.headers || {});
if (state.authHeader && !headers.has('Authorization')) {
headers.set('Authorization', state.authHeader);
}
const resp = await fetch(url, {
...options,
headers,
credentials: 'include',
});
return resp;
}
async function fetchAccounts() {
let page = 1;
const items = [];
while (true) {
const url = new URL('/api/v1/admin/accounts', CONFIG.apiBase);
url.searchParams.set('page', String(page));
url.searchParams.set('page_size', String(CONFIG.pageSize));
url.searchParams.set('platform', '');
url.searchParams.set('type', '');
url.searchParams.set('status', '');
url.searchParams.set('privacy_mode', '');
url.searchParams.set('group', '');
url.searchParams.set('search', '');
url.searchParams.set('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai');
const resp = await apiFetch(url.toString(), {
headers: { Accept: 'application/json, text/plain, */*' },
});
if (!resp.ok) throw new Error(`账号列表请求失败:HTTP ${resp.status}`);
const json = await resp.json();
if (json.code !== 0) throw new Error(`账号列表返回异常:${json.message || json.code}`);
const pageItems = json?.data?.items || [];
items.push(...pageItems);
const pages = Number(json?.data?.pages || 1);
if (page >= pages || pageItems.length === 0) break;
page += 1;
}
return items;
}
function getModels(account) {
const targetModel = String(state.testModel || '').trim();
if (targetModel) return [targetModel];
const mapping = account?.credentials?.model_mapping || {};
const keys = Object.keys(mapping).filter(Boolean);
if (keys.length <= 1) return keys;
const preferred = [];
for (const model of CONFIG.preferredModels) {
if (keys.includes(model)) preferred.push(model);
}
const rest = keys.filter((k) => !preferred.includes(k)).sort();
return [...preferred, ...rest];
}
async function testModel(accountId, modelId) {
const controller = new AbortController();
let timer = null;
const resetTimer = () => {
clearTimeout(timer);
timer = setTimeout(() => controller.abort(new Error(`模型 ${modelId} 流式超时`)), state.timeoutMs);
};
try {
resetTimer();
const resp = await apiFetch(`${CONFIG.apiBase}/api/v1/admin/accounts/${accountId}/test`, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ model_id: modelId, prompt: CONFIG.prompt }),
signal: controller.signal,
});
if (!resp.ok) {
clearTimeout(timer);
return { ok: false, reason: `HTTP ${resp.status}` };
}
const reader = resp.body?.getReader();
if (!reader) {
clearTimeout(timer);
const text = await resp.text();
return { ok: false, reason: `无响应流:${text.slice(0, 200)}` };
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
resetTimer();
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true }).replace(/\r/g, '');
let splitIndex;
while ((splitIndex = buffer.indexOf('\n\n')) >= 0) {
const chunk = buffer.slice(0, splitIndex);
buffer = buffer.slice(splitIndex + 2);
const dataLines = chunk
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trim());
for (const line of dataLines) {
if (!line) continue;
let event;
try {
event = JSON.parse(line);
} catch (_) {
continue;
}
if (event.type === 'error') {
clearTimeout(timer);
return { ok: false, reason: event.error || '未知错误' };
}
if (event.type === 'test_complete') {
clearTimeout(timer);
return { ok: !!event.success, reason: event.success ? 'success' : 'test_complete=false' };
}
}
}
}
clearTimeout(timer);
return { ok: false, reason: '响应流结束但没有 test_complete' };
} catch (err) {
clearTimeout(timer);
return {
ok: false,
reason: err?.name === 'AbortError' ? '请求超时' : (err?.message || String(err)),
};
}
}
async function setAccountSchedulable(accountId, schedulable) {
const resp = await apiFetch(`${CONFIG.apiBase}/api/v1/admin/accounts/${accountId}/schedulable`, {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ schedulable: !!schedulable }),
});
if (!resp.ok) {
return { ok: false, reason: `HTTP ${resp.status}` };
}
const json = await resp.json();
if (json.code !== 0) {
return { ok: false, reason: json.message || `code=${json.code}` };
}
return { ok: true, data: json.data };
}
function resetStats() {
state.stats = {
total: 0,
checked: 0,
ok: 0,
enabled: 0,
disabled: 0,
skipped: 0,
failed: 0,
};
updateStats();
const logBox = document.querySelector('#sub2api-checker-log');
if (logBox) logBox.innerHTML = '';
}
async function ensureAuth() {
const cached = getCachedAuthToken();
if (cached) {
saveAuth(cached);
return true;
}
if (state.authHeader) return true;
const fromInput = document.querySelector('#sub2api-checker-auth')?.value?.trim();
if (fromInput) {
saveAuth(fromInput);
return true;
}
const manual = prompt('没有自动捕获到 Authorization,请粘贴 Bearer token');
if (!manual) return false;
saveAuth(manual.trim());
return true;
}
async function run() {
if (state.running) {
log('已有任务在运行', 'warn');
return;
}
if (!(await ensureAuth())) {
log('缺少 Authorization,已取消', 'error');
return;
}
state.running = true;
state.stopRequested = false;
resetStats();
try {
state.collapsed = false;
updatePanelCollapsed();
log('开始拉取账号列表');
const accounts = await fetchAccounts();
state.stats.total = accounts.length;
updateStats();
log(`共获取 ${accounts.length} 个账号`, 'success');
for (const account of accounts) {
if (state.stopRequested) break;
const title = `#${account.id} ${account.name || '(未命名)'}`;
if (CONFIG.onlyCheckSchedulable && !account.schedulable) {
state.stats.checked += 1;
state.stats.skipped += 1;
updateStats();
log(`${title} 已是关闭状态,跳过`, 'warn');
continue;
}
const models = getModels(account);
if (!models.length) {
state.stats.failed += 1;
log(`${title} 没有 model_mapping,准备关闭`, 'error');
const off = await setAccountSchedulable(account.id, false);
state.stats.checked += 1;
if (off.ok) {
state.stats.disabled += 1;
log(`${title} 已关闭 schedulable`, 'success');
} else {
log(`${title} 关闭失败:${off.reason}`, 'error');
}
updateStats();
continue;
}
log(`${title} 开始测试 ${models.length} 个模型`);
let accountOk = true;
let failReason = '';
for (const model of models) {
if (state.stopRequested) break;
log(`${title} 测试模型 ${model}`);
const result = await testModel(account.id, model);
if (!result.ok) {
accountOk = false;
failReason = `模型 ${model} 异常:${result.reason}`;
log(`${title} ${failReason}`, 'error');
if (CONFIG.stopOnFirstModelFailure) break;
} else {
log(`${title} 模型 ${model} 正常`, 'success');
}
}
state.stats.checked += 1;
if (accountOk) {
state.stats.ok += 1;
if (!account.schedulable) {
const on = await setAccountSchedulable(account.id, true);
if (on.ok) {
state.stats.enabled += 1;
log(`${title} 全部模型正常,已重新启用 schedulable`, 'success');
} else {
log(`${title} 模型正常但重新启用失败:${on.reason}`, 'error');
}
} else {
log(`${title} 全部模型正常`, 'success');
}
} else {
state.stats.failed += 1;
const off = await setAccountSchedulable(account.id, false);
if (off.ok) {
state.stats.disabled += 1;
log(`${title} 已关闭 schedulable(原因:${failReason})`, 'success');
} else {
log(`${title} 关闭失败:${off.reason}`, 'error');
}
}
updateStats();
}
if (state.stopRequested) {
log('任务已按要求停止', 'warn');
} else {
log('巡检完成', 'success');
}
} finally {
state.running = false;
updateStats();
}
}
injectAuthSniffer();
waitDomReady().then(() => {
ensurePanel();
if (state.authHeader) {
log('脚本已就绪,已从本地缓存 auth_token 读取 Authorization', 'success');
} else {
log('脚本已就绪,未发现 auth_token;可刷新页面自动捕获或手动粘贴');
}
});
})();
网友解答:
--【壹】--:
感谢分享
--【贰】--:
有个全选批量编辑的功能呀
--【叁】--:
请教一下 这个油猴脚本的按钮在哪里?我好像没看见
是用的sub2api自己的定时测试吗
--【肆】--:
感谢大佬
--【伍】--:
感谢分享!
--【陆】--: ymzspirit:
试试sub2api和佬友脚本的效果如何
感谢大佬
--【柒】--:
感谢!一直用CLIproxyAPI也有类似的感受,佬友们用爱发电,稳定性肯定就差一些,如果能自动检测切换,使用体验会大大提升。
试试sub2api和佬友脚本的效果如何
--【捌】--:
佬
刚从 cpa 转过来,我想问问,导入账号以后是不是还得对每个账号进行编辑啊?
有没有快速的操作方法
image1144×1854 248 KB
--【玖】--:
感谢分享
感谢站里的佬友贡献的公益站
有时候佬们贡献公益站出现大量掉号的情况,请求的时候会挨个去轮询,所以下游可能会长时间导致持续链接却没有响应的状态。
然后,就有了这个自动测活工具脚本,测试过的账号会根据相应的结果自动开启或关闭账号
。
// ==UserScript==
// @name Sub2API 账号模型巡检并自动下线
// @namespace https://sinry.example
// @version 0.1.0
// @description 批量测试账号模型;任一模型异常时自动关闭账号 schedulable
// @match http://XXXX/admin/accounts*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
apiBase: location.origin,
pageSize: 100,
defaultTimeoutMs: 45000,
prompt: 'hi',
onlyCheckSchedulable: false,
stopOnFirstModelFailure: true,
preferredModels: ['gpt-5.4', 'gpt-4o-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini'],
defaultTestModel: 'gpt-5.4',
pageAuthTokenKey: 'auth_token',
authStorageKey: '__sub2api_checker_auth__',
timeoutStorageKey: '__sub2api_checker_timeout_ms__',
testModelStorageKey: '__sub2api_checker_test_model__',
};
function getCachedAuthToken() {
const raw =
localStorage.getItem(CONFIG.pageAuthTokenKey) ||
sessionStorage.getItem(CONFIG.pageAuthTokenKey) ||
localStorage.getItem(CONFIG.authStorageKey) ||
'';
return raw ? (raw.startsWith('Bearer ') ? raw : `Bearer ${raw}`) : '';
}
const state = {
authHeader: getCachedAuthToken(),
timeoutMs: Number(localStorage.getItem(CONFIG.timeoutStorageKey) || CONFIG.defaultTimeoutMs),
testModel: localStorage.getItem(CONFIG.testModelStorageKey) || CONFIG.defaultTestModel,
running: false,
stopRequested: false,
panelReady: false,
collapsed: true,
stats: {
total: 0,
checked: 0,
ok: 0,
enabled: 0,
disabled: 0,
skipped: 0,
failed: 0,
},
};
function log(msg, type = 'info') {
const time = new Date().toLocaleTimeString();
const line = `[${time}] ${msg}`;
console[type === 'error' ? 'error' : 'log'](`[sub2api-checker] ${line}`);
const box = document.querySelector('#sub2api-checker-log');
if (!box) return;
const color =
type === 'error' ? '#ff7875' :
type === 'warn' ? '#ffd666' :
type === 'success' ? '#95de64' : '#d9d9d9';
const row = document.createElement('div');
row.style.color = color;
row.textContent = line;
box.appendChild(row);
box.scrollTop = box.scrollHeight;
}
function saveAuth(auth) {
if (!auth || typeof auth !== 'string') return;
const normalized = auth.startsWith('Bearer ') ? auth : `Bearer ${auth}`;
state.authHeader = normalized;
localStorage.setItem(CONFIG.authStorageKey, normalized);
const input = document.querySelector('#sub2api-checker-auth');
if (input && !input.value) input.value = normalized;
log('已捕获 Authorization', 'success');
}
function saveTimeoutMs(timeoutMs) {
const n = Number(timeoutMs);
if (!Number.isFinite(n) || n < 1000) return false;
state.timeoutMs = n;
localStorage.setItem(CONFIG.timeoutStorageKey, String(n));
const input = document.querySelector('#sub2api-checker-timeout');
if (input) input.value = String(Math.floor(n / 1000));
return true;
}
function saveTestModel(model) {
const normalized = String(model || '').trim();
if (!normalized) return false;
state.testModel = normalized;
localStorage.setItem(CONFIG.testModelStorageKey, normalized);
const input = document.querySelector('#sub2api-checker-test-model');
if (input) input.value = normalized;
return true;
}
function injectAuthSniffer() {
const script = document.createElement('script');
script.textContent = `
(() => {
const emit = (auth) => {
if (!auth) return;
document.dispatchEvent(new CustomEvent('__sub2api_checker_auth__', { detail: auth }));
};
const pickAuth = (headersLike) => {
try {
if (!headersLike) return '';
if (headersLike instanceof Headers) {
return headersLike.get('Authorization') || headersLike.get('authorization') || '';
}
if (Array.isArray(headersLike)) {
for (const [k, v] of headersLike) {
if (String(k).toLowerCase() === 'authorization') return v || '';
}
return '';
}
if (typeof headersLike === 'object') {
for (const key of Object.keys(headersLike)) {
if (key.toLowerCase() === 'authorization') return headersLike[key] || '';
}
}
} catch (_) {}
return '';
};
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function(input, init) {
const auth =
pickAuth(init && init.headers) ||
pickAuth(input && input.headers);
if (auth) emit(auth);
return origFetch.apply(this, arguments);
};
}
const origOpen = XMLHttpRequest.prototype.open;
const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function() {
this.__sub2apiAuth = '';
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
if (String(name).toLowerCase() === 'authorization' && value) {
this.__sub2apiAuth = value;
emit(value);
}
return origSetHeader.apply(this, arguments);
};
})();
`;
document.documentElement.appendChild(script);
script.remove();
document.addEventListener('__sub2api_checker_auth__', (event) => {
saveAuth(event.detail);
});
}
function updateStats() {
const el = document.querySelector('#sub2api-checker-stats');
if (!el) return;
const s = state.stats;
el.textContent = `总数 ${s.total} | 已处理 ${s.checked} | 正常 ${s.ok} | 已启用 ${s.enabled} | 已关闭 ${s.disabled} | 跳过 ${s.skipped} | 异常 ${s.failed}`;
}
function updatePanelCollapsed() {
const shell = document.querySelector('#sub2api-checker-shell');
const root = document.querySelector('#sub2api-checker-panel');
const toggle = document.querySelector('#sub2api-checker-toggle');
if (!root || !toggle || !shell) return;
root.style.width = state.collapsed ? '0px' : '420px';
root.style.opacity = state.collapsed ? '0' : '1';
root.style.marginRight = state.collapsed ? '0px' : '12px';
root.style.pointerEvents = state.collapsed ? 'none' : 'auto';
root.style.transform = state.collapsed ? 'translateX(12px)' : 'translateX(0)';
toggle.textContent = state.collapsed ? '账号巡检' : '收起';
toggle.style.borderRadius = state.collapsed ? '10px 0 0 10px' : '10px';
shell.style.pointerEvents = 'auto';
}
function ensurePanel() {
if (state.panelReady) return;
state.panelReady = true;
const shell = document.createElement('div');
shell.id = 'sub2api-checker-shell';
shell.style.cssText = `
position: fixed;
right: 0;
top: 120px;
z-index: 1000000;
display: flex;
flex-direction: row;
align-items: flex-start;
pointer-events: auto;
`;
document.body.appendChild(shell);
const toggle = document.createElement('button');
toggle.id = 'sub2api-checker-toggle';
toggle.style.cssText = `
padding: 10px 8px;
border: 0;
border-radius: 10px 0 0 10px;
background: #1677ff;
color: #fff;
cursor: pointer;
writing-mode: vertical-rl;
text-orientation: mixed;
box-shadow: 0 8px 24px rgba(0,0,0,.25);
transition: transform .28s ease, box-shadow .28s ease, border-radius .28s ease;
font: 12px/1.2 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,PingFang SC,Microsoft YaHei,sans-serif;
`;
toggle.addEventListener('mouseenter', () => {
toggle.style.transform = 'translateX(-2px)';
toggle.style.boxShadow = '0 10px 28px rgba(0,0,0,.32)';
});
toggle.addEventListener('mouseleave', () => {
toggle.style.transform = 'translateX(0)';
toggle.style.boxShadow = '0 8px 24px rgba(0,0,0,.25)';
});
toggle.addEventListener('click', () => {
state.collapsed = !state.collapsed;
updatePanelCollapsed();
});
shell.appendChild(toggle);
const root = document.createElement('div');
root.id = 'sub2api-checker-panel';
root.style.cssText = `
width: 0;
opacity: 0;
overflow: hidden;
transition: width .28s ease, opacity .22s ease, margin-right .28s ease, transform .28s ease;
transform: translateX(12px);
`;
root.innerHTML = `
<div id="sub2api-checker-panel-inner" style="
width:420px;
background:rgba(16, 18, 27, 0.96);
color:#fff;
border:1px solid #30363d;
border-radius:12px;
box-shadow:0 8px 24px rgba(0,0,0,.35);
font:12px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,PingFang SC,Microsoft YaHei,sans-serif;
overflow:hidden;
">
<div style="padding:12px 14px;border-bottom:1px solid #30363d;font-weight:700;">Sub2API 账号模型巡检</div>
<div style="padding:12px 14px;display:flex;flex-direction:column;gap:8px;">
<label style="display:flex;flex-direction:column;gap:4px;">
<span>Authorization(优先自动捕获,抓不到再手填)</span>
<input id="sub2api-checker-auth" type="text" placeholder="Bearer xxxxxx"
style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
</label>
<label style="display:flex;flex-direction:column;gap:4px;">
<span>单模型超时时间(秒)</span>
<input id="sub2api-checker-timeout" type="number" min="1" step="1" placeholder="45"
style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
</label>
<label style="display:flex;flex-direction:column;gap:4px;">
<span>测试模型</span>
<input id="sub2api-checker-test-model" type="text" placeholder="gpt-5.4"
style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
</label>
<div style="display:flex;gap:8px;align-items:center;">
<button id="sub2api-checker-start" style="flex:1;padding:8px 10px;border:0;border-radius:8px;background:#1677ff;color:#fff;cursor:pointer;">开始巡检</button>
<button id="sub2api-checker-stop" style="flex:1;padding:8px 10px;border:0;border-radius:8px;background:#fa541c;color:#fff;cursor:pointer;">停止</button>
</div>
<div id="sub2api-checker-stats" style="color:#bfbfbf;">总数 0 | 已处理 0 | 正常 0 | 已启用 0 | 已关闭 0 | 跳过 0 | 异常 0</div>
<div id="sub2api-checker-log" style="height:320px;overflow:auto;background:#0b0f17;border:1px solid #30363d;border-radius:8px;padding:8px;"></div>
</div>
</div>
`;
shell.appendChild(root);
const authInput = root.querySelector('#sub2api-checker-auth');
authInput.value = state.authHeader;
authInput.addEventListener('change', () => {
const v = authInput.value.trim();
if (v) saveAuth(v);
});
const timeoutInput = root.querySelector('#sub2api-checker-timeout');
timeoutInput.value = String(Math.floor(state.timeoutMs / 1000));
timeoutInput.addEventListener('change', () => {
const sec = Number(timeoutInput.value || 0);
if (!saveTimeoutMs(sec * 1000)) {
timeoutInput.value = String(Math.floor(state.timeoutMs / 1000));
log('超时时间无效,需大于等于 1 秒', 'error');
return;
}
log(`已设置单模型超时 ${sec} 秒`, 'success');
});
const testModelInput = root.querySelector('#sub2api-checker-test-model');
testModelInput.value = state.testModel;
testModelInput.addEventListener('change', () => {
const model = testModelInput.value.trim();
if (!saveTestModel(model)) {
testModelInput.value = state.testModel;
log('测试模型不能为空', 'error');
return;
}
log(`已设置测试模型 ${state.testModel}`, 'success');
});
root.querySelector('#sub2api-checker-start').addEventListener('click', () => run().catch((err) => {
log(`运行异常:${err.message}`, 'error');
state.running = false;
}));
root.querySelector('#sub2api-checker-stop').addEventListener('click', () => {
state.stopRequested = true;
log('已请求停止,当前请求结束后退出', 'warn');
});
updatePanelCollapsed();
}
async function waitDomReady() {
if (document.body) return;
await new Promise((resolve) => {
const timer = setInterval(() => {
if (document.body) {
clearInterval(timer);
resolve();
}
}, 50);
});
}
async function apiFetch(url, options = {}) {
const headers = new Headers(options.headers || {});
if (state.authHeader && !headers.has('Authorization')) {
headers.set('Authorization', state.authHeader);
}
const resp = await fetch(url, {
...options,
headers,
credentials: 'include',
});
return resp;
}
async function fetchAccounts() {
let page = 1;
const items = [];
while (true) {
const url = new URL('/api/v1/admin/accounts', CONFIG.apiBase);
url.searchParams.set('page', String(page));
url.searchParams.set('page_size', String(CONFIG.pageSize));
url.searchParams.set('platform', '');
url.searchParams.set('type', '');
url.searchParams.set('status', '');
url.searchParams.set('privacy_mode', '');
url.searchParams.set('group', '');
url.searchParams.set('search', '');
url.searchParams.set('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai');
const resp = await apiFetch(url.toString(), {
headers: { Accept: 'application/json, text/plain, */*' },
});
if (!resp.ok) throw new Error(`账号列表请求失败:HTTP ${resp.status}`);
const json = await resp.json();
if (json.code !== 0) throw new Error(`账号列表返回异常:${json.message || json.code}`);
const pageItems = json?.data?.items || [];
items.push(...pageItems);
const pages = Number(json?.data?.pages || 1);
if (page >= pages || pageItems.length === 0) break;
page += 1;
}
return items;
}
function getModels(account) {
const targetModel = String(state.testModel || '').trim();
if (targetModel) return [targetModel];
const mapping = account?.credentials?.model_mapping || {};
const keys = Object.keys(mapping).filter(Boolean);
if (keys.length <= 1) return keys;
const preferred = [];
for (const model of CONFIG.preferredModels) {
if (keys.includes(model)) preferred.push(model);
}
const rest = keys.filter((k) => !preferred.includes(k)).sort();
return [...preferred, ...rest];
}
async function testModel(accountId, modelId) {
const controller = new AbortController();
let timer = null;
const resetTimer = () => {
clearTimeout(timer);
timer = setTimeout(() => controller.abort(new Error(`模型 ${modelId} 流式超时`)), state.timeoutMs);
};
try {
resetTimer();
const resp = await apiFetch(`${CONFIG.apiBase}/api/v1/admin/accounts/${accountId}/test`, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ model_id: modelId, prompt: CONFIG.prompt }),
signal: controller.signal,
});
if (!resp.ok) {
clearTimeout(timer);
return { ok: false, reason: `HTTP ${resp.status}` };
}
const reader = resp.body?.getReader();
if (!reader) {
clearTimeout(timer);
const text = await resp.text();
return { ok: false, reason: `无响应流:${text.slice(0, 200)}` };
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
resetTimer();
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true }).replace(/\r/g, '');
let splitIndex;
while ((splitIndex = buffer.indexOf('\n\n')) >= 0) {
const chunk = buffer.slice(0, splitIndex);
buffer = buffer.slice(splitIndex + 2);
const dataLines = chunk
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trim());
for (const line of dataLines) {
if (!line) continue;
let event;
try {
event = JSON.parse(line);
} catch (_) {
continue;
}
if (event.type === 'error') {
clearTimeout(timer);
return { ok: false, reason: event.error || '未知错误' };
}
if (event.type === 'test_complete') {
clearTimeout(timer);
return { ok: !!event.success, reason: event.success ? 'success' : 'test_complete=false' };
}
}
}
}
clearTimeout(timer);
return { ok: false, reason: '响应流结束但没有 test_complete' };
} catch (err) {
clearTimeout(timer);
return {
ok: false,
reason: err?.name === 'AbortError' ? '请求超时' : (err?.message || String(err)),
};
}
}
async function setAccountSchedulable(accountId, schedulable) {
const resp = await apiFetch(`${CONFIG.apiBase}/api/v1/admin/accounts/${accountId}/schedulable`, {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ schedulable: !!schedulable }),
});
if (!resp.ok) {
return { ok: false, reason: `HTTP ${resp.status}` };
}
const json = await resp.json();
if (json.code !== 0) {
return { ok: false, reason: json.message || `code=${json.code}` };
}
return { ok: true, data: json.data };
}
function resetStats() {
state.stats = {
total: 0,
checked: 0,
ok: 0,
enabled: 0,
disabled: 0,
skipped: 0,
failed: 0,
};
updateStats();
const logBox = document.querySelector('#sub2api-checker-log');
if (logBox) logBox.innerHTML = '';
}
async function ensureAuth() {
const cached = getCachedAuthToken();
if (cached) {
saveAuth(cached);
return true;
}
if (state.authHeader) return true;
const fromInput = document.querySelector('#sub2api-checker-auth')?.value?.trim();
if (fromInput) {
saveAuth(fromInput);
return true;
}
const manual = prompt('没有自动捕获到 Authorization,请粘贴 Bearer token');
if (!manual) return false;
saveAuth(manual.trim());
return true;
}
async function run() {
if (state.running) {
log('已有任务在运行', 'warn');
return;
}
if (!(await ensureAuth())) {
log('缺少 Authorization,已取消', 'error');
return;
}
state.running = true;
state.stopRequested = false;
resetStats();
try {
state.collapsed = false;
updatePanelCollapsed();
log('开始拉取账号列表');
const accounts = await fetchAccounts();
state.stats.total = accounts.length;
updateStats();
log(`共获取 ${accounts.length} 个账号`, 'success');
for (const account of accounts) {
if (state.stopRequested) break;
const title = `#${account.id} ${account.name || '(未命名)'}`;
if (CONFIG.onlyCheckSchedulable && !account.schedulable) {
state.stats.checked += 1;
state.stats.skipped += 1;
updateStats();
log(`${title} 已是关闭状态,跳过`, 'warn');
continue;
}
const models = getModels(account);
if (!models.length) {
state.stats.failed += 1;
log(`${title} 没有 model_mapping,准备关闭`, 'error');
const off = await setAccountSchedulable(account.id, false);
state.stats.checked += 1;
if (off.ok) {
state.stats.disabled += 1;
log(`${title} 已关闭 schedulable`, 'success');
} else {
log(`${title} 关闭失败:${off.reason}`, 'error');
}
updateStats();
continue;
}
log(`${title} 开始测试 ${models.length} 个模型`);
let accountOk = true;
let failReason = '';
for (const model of models) {
if (state.stopRequested) break;
log(`${title} 测试模型 ${model}`);
const result = await testModel(account.id, model);
if (!result.ok) {
accountOk = false;
failReason = `模型 ${model} 异常:${result.reason}`;
log(`${title} ${failReason}`, 'error');
if (CONFIG.stopOnFirstModelFailure) break;
} else {
log(`${title} 模型 ${model} 正常`, 'success');
}
}
state.stats.checked += 1;
if (accountOk) {
state.stats.ok += 1;
if (!account.schedulable) {
const on = await setAccountSchedulable(account.id, true);
if (on.ok) {
state.stats.enabled += 1;
log(`${title} 全部模型正常,已重新启用 schedulable`, 'success');
} else {
log(`${title} 模型正常但重新启用失败:${on.reason}`, 'error');
}
} else {
log(`${title} 全部模型正常`, 'success');
}
} else {
state.stats.failed += 1;
const off = await setAccountSchedulable(account.id, false);
if (off.ok) {
state.stats.disabled += 1;
log(`${title} 已关闭 schedulable(原因:${failReason})`, 'success');
} else {
log(`${title} 关闭失败:${off.reason}`, 'error');
}
}
updateStats();
}
if (state.stopRequested) {
log('任务已按要求停止', 'warn');
} else {
log('巡检完成', 'success');
}
} finally {
state.running = false;
updateStats();
}
}
injectAuthSniffer();
waitDomReady().then(() => {
ensurePanel();
if (state.authHeader) {
log('脚本已就绪,已从本地缓存 auth_token 读取 Authorization', 'success');
} else {
log('脚本已就绪,未发现 auth_token;可刷新页面自动捕获或手动粘贴');
}
});
})();
网友解答:
--【壹】--:
感谢分享
--【贰】--:
有个全选批量编辑的功能呀
--【叁】--:
请教一下 这个油猴脚本的按钮在哪里?我好像没看见
是用的sub2api自己的定时测试吗
--【肆】--:
感谢大佬
--【伍】--:
感谢分享!
--【陆】--: ymzspirit:
试试sub2api和佬友脚本的效果如何
感谢大佬
--【柒】--:
感谢!一直用CLIproxyAPI也有类似的感受,佬友们用爱发电,稳定性肯定就差一些,如果能自动检测切换,使用体验会大大提升。
试试sub2api和佬友脚本的效果如何
--【捌】--:
佬
刚从 cpa 转过来,我想问问,导入账号以后是不是还得对每个账号进行编辑啊?
有没有快速的操作方法
image1144×1854 248 KB
--【玖】--:
感谢分享

