sub2api测活浏览器插件(参考佬友)

sub2api测活浏览器插件(参考佬友)
sub2api测活浏览器插件(参考佬友)
摸鱼时间让Ai写了个sub2api自动测活工具 开发调优
感谢站里的佬友贡献的公益站 有时候佬们贡献公益站出现大量掉号的情况,请求的时候会挨个去轮询,所以下游可能会长时间导致持续链接却没有响应的状态。 然后,就有了这个自动测活工具脚本,测试过的账号会根据相应的结果自动开启或关闭账号 。 // ==UserScript== // @name Sub2API 账号模型巡检并自动下线 // @namespace https://s…
// ==UserScript==
// @name         Sub2API 账号模型巡检并自动下线
// @namespace    https://sinry.example
// @version      0.2.0
// @description  分批巡检账号模型;401 自动关闭并永久跳过;429 usage_limit_reached 视为正常;支持 5h 冷却和进度条
// @match        http://127.0.0.1:8317/admin/accounts*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const CONFIG = {
    apiBase: location.origin,
    pageSize: 100,
    defaultTimeoutMs: 45000,
    defaultBatchSize: 100,
    recentCheckWindowMs: 5 * 60 * 60 * 1000,
    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__',
    batchSizeStorageKey: '__sub2api_checker_batch_size__',
    cacheStorageKey: '__sub2api_checker_account_cache_v1__',
    cacheVersion: 1,
  };

  function storageGet(storage, key) {
    try {
      return storage.getItem(key);
    } catch (_) {
      return null;
    }
  }

  function storageSet(storage, key, value) {
    try {
      storage.setItem(key, value);
      return true;
    } catch (_) {
      return false;
    }
  }

  function normalizeTimestampMap(input) {
    const result = {};
    if (!input || typeof input !== 'object') return result;
    for (const [rawKey, rawValue] of Object.entries(input)) {
      const key = String(rawKey || '').trim();
      const value = Number(rawValue);
      if (!key || !Number.isFinite(value) || value <= 0) continue;
      result[key] = value;
    }
    return result;
  }

  function loadCheckerCache() {
    const fallback = {
      version: CONFIG.cacheVersion,
      recentChecks: {},
      unauthorizedAccounts: {},
    };

    try {
      const raw = storageGet(localStorage, CONFIG.cacheStorageKey);
      if (!raw) return fallback;
      const parsed = JSON.parse(raw);
      return {
        version: CONFIG.cacheVersion,
        recentChecks: normalizeTimestampMap(parsed?.recentChecks),
        unauthorizedAccounts: normalizeTimestampMap(parsed?.unauthorizedAccounts),
      };
    } catch (err) {
      console.warn('[sub2api-checker] failed to load cache:', err);
      return fallback;
    }
  }

  function persistCheckerCache() {
    storageSet(localStorage, CONFIG.cacheStorageKey, JSON.stringify(state.cache));
  }

  function getCachedAuthToken() {
    const raw =
      storageGet(localStorage, CONFIG.pageAuthTokenKey) ||
      storageGet(sessionStorage, CONFIG.pageAuthTokenKey) ||
      storageGet(localStorage, CONFIG.authStorageKey) ||
      '';
    return raw ? (raw.startsWith('Bearer ') ? raw : `Bearer ${raw}`) : '';
  }

  function getAccountKey(accountOrId) {
    const value = typeof accountOrId === 'object' ? accountOrId?.id : accountOrId;
    const id = Number(value);
    if (!Number.isFinite(id) || id <= 0) return '';
    return String(Math.trunc(id));
  }

  function pruneCache(now = Date.now()) {
    const cutoff = now - CONFIG.recentCheckWindowMs;
    let dirty = false;

    for (const [key, ts] of Object.entries(state.cache.recentChecks)) {
      if (!Number.isFinite(ts) || ts < cutoff) {
        delete state.cache.recentChecks[key];
        dirty = true;
      }
    }

    for (const [key, ts] of Object.entries(state.cache.unauthorizedAccounts)) {
      if (!Number.isFinite(ts) || ts <= 0) {
        delete state.cache.unauthorizedAccounts[key];
        dirty = true;
      }
    }

    if (dirty) persistCheckerCache();
  }

  function hasRecentCheck(accountOrId, now = Date.now()) {
    const key = getAccountKey(accountOrId);
    if (!key) return false;
    const ts = Number(state.cache.recentChecks[key] || 0);
    return Number.isFinite(ts) && now - ts < CONFIG.recentCheckWindowMs;
  }

  function markRecentCheck(accountOrId, now = Date.now()) {
    const key = getAccountKey(accountOrId);
    if (!key) return false;
    const wasRecent = hasRecentCheck(key, now);
    state.cache.recentChecks[key] = now;
    persistCheckerCache();
    return !wasRecent;
  }

  function isUnauthorizedAccount(accountOrId) {
    const key = getAccountKey(accountOrId);
    if (!key) return false;
    const ts = state.cache.unauthorizedAccounts[key];
    const value = Number(ts);
    return Number.isFinite(value) && value > 0;
  }

  function markUnauthorizedAccount(accountOrId, now = Date.now()) {
    const key = getAccountKey(accountOrId);
    if (!key) return false;
    const existed = isUnauthorizedAccount(key);
    state.cache.unauthorizedAccounts[key] = now;
    persistCheckerCache();
    return !existed;
  }

  const state = {
    authHeader: getCachedAuthToken(),
    timeoutMs: Number(storageGet(localStorage, CONFIG.timeoutStorageKey) || CONFIG.defaultTimeoutMs),
    testModel: storageGet(localStorage, CONFIG.testModelStorageKey) || CONFIG.defaultTestModel,
    batchSize: Number(storageGet(localStorage, CONFIG.batchSizeStorageKey) || CONFIG.defaultBatchSize),
    cache: loadCheckerCache(),
    running: false,
    stopRequested: false,
    panelReady: false,
    collapsed: true,
    stats: {
      total: 0,
      checked: 0,
      ok: 0,
      enabled: 0,
      disabled: 0,
      skipped: 0,
      failed: 0,
      excludedUnauthorized: 0,
    },
    progress: {
      totalPool: 0,
      checkedInWindow: 0,
      batchTotal: 0,
      batchDone: 0,
      excludedUnauthorized: 0,
      coolingCount: 0,
      eligibleCount: 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;
    storageSet(localStorage, 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;
    storageSet(localStorage, 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;
    storageSet(localStorage, CONFIG.testModelStorageKey, normalized);
    const input = document.querySelector('#sub2api-checker-test-model');
    if (input) input.value = normalized;
    return true;
  }

  function saveBatchSize(batchSize) {
    const n = Math.floor(Number(batchSize));
    if (!Number.isFinite(n) || n < 1) return false;
    state.batchSize = n;
    storageSet(localStorage, CONFIG.batchSizeStorageKey, String(n));
    const input = document.querySelector('#sub2api-checker-batch-size');
    if (input) input.value = String(n);
    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} | 401排除 ${s.excludedUnauthorized}`;
  }

  function updateProgress() {
    const p = state.progress;
    const progressSummary = document.querySelector('#sub2api-checker-progress-summary');
    const progressFill = document.querySelector('#sub2api-checker-progress-fill');
    const batchSummary = document.querySelector('#sub2api-checker-batch-summary');
    const batchFill = document.querySelector('#sub2api-checker-batch-fill');

    if (progressSummary) {
      progressSummary.textContent = `已巡检 ${p.checkedInWindow} / 全部账号 ${p.totalPool}`;
    }
    if (progressFill) {
      const ratio = p.totalPool > 0 ? Math.min(100, Math.max(0, (p.checkedInWindow / p.totalPool) * 100)) : 0;
      progressFill.style.width = `${ratio}%`;
    }
    if (batchSummary) {
      batchSummary.textContent = `本轮 ${p.batchDone} / ${p.batchTotal} | 401排除 ${p.excludedUnauthorized} | 5h冷却 ${p.coolingCount} | 可巡检 ${p.eligibleCount}`;
    }
    if (batchFill) {
      const ratio = p.batchTotal > 0 ? Math.min(100, Math.max(0, (p.batchDone / p.batchTotal) * 100)) : 0;
      batchFill.style.width = `${ratio}%`;
    }
  }

  function resetProgress() {
    state.progress = {
      totalPool: 0,
      checkedInWindow: 0,
      batchTotal: 0,
      batchDone: 0,
      excludedUnauthorized: 0,
      coolingCount: 0,
      eligibleCount: 0,
    };
    updateProgress();
  }

  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' : '460px';
    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:460px;
        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>
        <div style="display:flex;gap:8px;">
          <label style="display:flex;flex:1;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:1;flex-direction:column;gap:4px;">
            <span>本次巡检数量</span>
            <input id="sub2api-checker-batch-size" type="number" min="1" step="1" placeholder="100"
              style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" />
          </label>
        </div>
        <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 style="display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid #30363d;border-radius:8px;background:#0f1521;">
          <div id="sub2api-checker-progress-summary" style="color:#e6f4ff;">已巡检 0 / 全部账号 0</div>
          <div style="height:8px;border-radius:999px;background:#1f2430;overflow:hidden;">
            <div id="sub2api-checker-progress-fill" style="width:0%;height:100%;background:linear-gradient(90deg,#1677ff,#69b1ff);transition:width .2s ease;"></div>
          </div>
          <div id="sub2api-checker-batch-summary" style="color:#bfbfbf;">本轮 0 / 0 | 401排除 0 | 5h冷却 0 | 可巡检 0</div>
          <div style="height:8px;border-radius:999px;background:#1f2430;overflow:hidden;">
            <div id="sub2api-checker-batch-fill" style="width:0%;height:100%;background:linear-gradient(90deg,#52c41a,#b7eb8f);transition:width .2s ease;"></div>
          </div>
        </div>
        <div id="sub2api-checker-stats" style="color:#bfbfbf;">全部账号 0 | 已处理 0 | 正常 0 | 已启用 0 | 已关闭 0 | 跳过 0 | 异常 0 | 401排除 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 batchInput = root.querySelector('#sub2api-checker-batch-size');
    batchInput.value = String(Math.max(1, Math.floor(state.batchSize || CONFIG.defaultBatchSize)));
    batchInput.addEventListener('change', () => {
      const size = Number(batchInput.value || 0);
      if (!saveBatchSize(size)) {
        batchInput.value = String(Math.max(1, Math.floor(state.batchSize || CONFIG.defaultBatchSize)));
        log('巡检数量无效,需大于等于 1', 'error');
        return;
      }
      log(`已设置本次巡检数量 ${state.batchSize}`, '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;
      updateStats();
      updateProgress();
    }));
    root.querySelector('#sub2api-checker-stop').addEventListener('click', () => {
      state.stopRequested = true;
      log('已请求停止,当前请求结束后退出', 'warn');
    });

    updatePanelCollapsed();
    updateStats();
    updateProgress();
  }

  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);
    }
    return fetch(url, {
      ...options,
      headers,
      credentials: 'include',
    });
  }

  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];
  }

  function extractStatusCode(text) {
    const source = String(text || '');
    const matchers = [
      /API returned (\d{3})/i,
      /Authentication failed \((\d{3})\)/i,
      /\bHTTP (\d{3})\b/i,
      /\bstatus(?: code)?[:= ]+(\d{3})\b/i,
    ];

    for (const matcher of matchers) {
      const matched = source.match(matcher);
      if (matched) {
        const code = Number(matched[1]);
        if (Number.isFinite(code)) return code;
      }
    }
    return null;
  }

  function extractTrailingJson(text) {
    const source = String(text || '');
    const start = source.indexOf('{');
    const end = source.lastIndexOf('}');
    if (start < 0 || end <= start) return null;
    try {
      return JSON.parse(source.slice(start, end + 1));
    } catch (_) {
      return null;
    }
  }

  function classifyFailure(reasonText, explicitStatus) {
    const raw = String(reasonText || '').trim();
    const status = Number.isFinite(Number(explicitStatus)) ? Number(explicitStatus) : extractStatusCode(raw);
    const body = extractTrailingJson(raw);
    const errorType = String(body?.error?.type || body?.type || '').trim();
    const bodyMessage = String(body?.error?.message || body?.message || '').trim();
    const normalizedReason = bodyMessage || raw || '未知错误';

    if (
      status === 401 ||
      /Authentication failed \(401\)/i.test(raw) ||
      /API returned 401/i.test(raw) ||
      /\b401 Unauthorized\b/i.test(raw)
    ) {
      return {
        classification: 'unauthorized',
        rawStatus: 401,
        reason: normalizedReason,
        rawReason: raw || normalizedReason,
      };
    }

    if (
      status === 429 &&
      (
        errorType === 'usage_limit_reached' ||
        /usage_limit_reached/i.test(raw) ||
        /The usage limit has been reached/i.test(raw)
      )
    ) {
      return {
        classification: 'quota_exhausted',
        rawStatus: 429,
        reason: normalizedReason,
        rawReason: raw || normalizedReason,
      };
    }

    return {
      classification: 'other_failure',
      rawStatus: status,
      reason: raw || normalizedReason,
      rawReason: raw || normalizedReason,
    };
  }

  function isBackendUnauthorizedDisabled(account) {
    if (!account || account.schedulable !== false) return false;
    return classifyFailure(account.error_message || '').classification === 'unauthorized';
  }

  function shouldExcludeUnauthorized(account) {
    if (!account) return false;
    if (isUnauthorizedAccount(account.id)) return true;
    if (isBackendUnauthorizedDisabled(account)) {
      markUnauthorizedAccount(account.id);
      return true;
    }
    return false;
  }

  function buildRunPools(accounts, now = Date.now()) {
    pruneCache(now);

    const pool = [];
    let excludedUnauthorized = 0;
    let coolingCount = 0;

    for (const account of accounts) {
      if (!getAccountKey(account?.id)) continue;
      if (shouldExcludeUnauthorized(account)) {
        excludedUnauthorized += 1;
        continue;
      }
      if (CONFIG.onlyCheckSchedulable && !account.schedulable) {
        continue;
      }
      pool.push(account);
    }

    const eligible = [];
    for (const account of pool) {
      if (hasRecentCheck(account.id, now)) {
        coolingCount += 1;
      } else {
        eligible.push(account);
      }
    }

    return {
      pool,
      eligible,
      excludedUnauthorized,
      coolingCount,
      checkedInWindow: coolingCount,
    };
  }

  async function testModel(accountId, modelId) {
    const controller = new AbortController();
    let timer = null;
    const clearTimer = () => {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
    };
    const resetTimer = () => {
      clearTimer();
      timer = setTimeout(() => controller.abort(), 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) {
        clearTimer();
        const text = await resp.text();
        const classified = classifyFailure(text, resp.status);
        return {
          ok: false,
          classification: classified.classification,
          reason: classified.reason,
          rawReason: classified.rawReason,
          rawStatus: classified.rawStatus,
        };
      }

      const reader = resp.body?.getReader();
      if (!reader) {
        clearTimer();
        const text = await resp.text();
        return {
          ok: false,
          classification: 'other_failure',
          reason: `无响应流:${String(text || '').slice(0, 200)}`,
          rawReason: text,
          rawStatus: resp.status,
        };
      }

      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') {
              clearTimer();
              const classified = classifyFailure(event.error || '未知错误');
              return {
                ok: false,
                classification: classified.classification,
                reason: classified.reason,
                rawReason: classified.rawReason,
                rawStatus: classified.rawStatus,
              };
            }

            if (event.type === 'test_complete') {
              clearTimer();
              return {
                ok: !!event.success,
                classification: event.success ? 'success' : 'other_failure',
                reason: event.success ? 'success' : 'test_complete=false',
                rawReason: event.success ? 'success' : 'test_complete=false',
                rawStatus: resp.status,
              };
            }
          }
        }
      }

      clearTimer();
      return {
        ok: false,
        classification: 'other_failure',
        reason: '响应流结束但没有 test_complete',
        rawReason: '响应流结束但没有 test_complete',
        rawStatus: resp.status,
      };
    } catch (err) {
      clearTimer();
      return {
        ok: false,
        classification: 'other_failure',
        reason: err?.name === 'AbortError' ? '请求超时' : (err?.message || String(err)),
        rawReason: err?.message || String(err),
        rawStatus: null,
      };
    }
  }

  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,
      excludedUnauthorized: 0,
    };
    resetProgress();
    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 = window.prompt('没有自动捕获到 Authorization,请粘贴 Bearer token');
    if (!manual) return false;
    saveAuth(manual.trim());
    return true;
  }

  function markAccountCompleted(accountId) {
    const counted = markRecentCheck(accountId);
    state.stats.checked += 1;
    state.progress.batchDone += 1;
    if (counted) {
      state.progress.checkedInWindow += 1;
    }
    updateStats();
    updateProgress();
  }

  async function run() {
    if (state.running) {
      log('已有任务在运行', 'warn');
      return;
    }
    if (!(await ensureAuth())) {
      log('缺少 Authorization,已取消', 'error');
      return;
    }

    state.running = true;
    state.stopRequested = false;
    resetStats();
    pruneCache();

    try {
      state.collapsed = false;
      updatePanelCollapsed();

      const batchSize = Math.max(1, Math.floor(Number(state.batchSize) || CONFIG.defaultBatchSize));
      const now = Date.now();

      log('开始拉取账号列表');
      const accounts = await fetchAccounts();

      const pools = buildRunPools(accounts, now);
      const selectedAccounts = pools.eligible.slice(0, batchSize);

      state.stats.total = pools.pool.length;
      state.stats.excludedUnauthorized = pools.excludedUnauthorized;
      state.progress.totalPool = pools.pool.length;
      state.progress.checkedInWindow = pools.checkedInWindow;
      state.progress.batchTotal = selectedAccounts.length;
      state.progress.batchDone = 0;
      state.progress.excludedUnauthorized = pools.excludedUnauthorized;
      state.progress.coolingCount = pools.coolingCount;
      state.progress.eligibleCount = pools.eligible.length;
      updateStats();
      updateProgress();

      log(`共获取 ${accounts.length} 个账号`, 'success');
      log(`401 已排除 ${pools.excludedUnauthorized} 个;全部账号 ${pools.pool.length} 个;5h 内已巡检 ${pools.checkedInWindow} 个`, 'success');
      log(`本轮计划巡检 ${batchSize} 个,实际入选 ${selectedAccounts.length} 个`, 'success');

      if (!selectedAccounts.length) {
        log('当前没有可巡检账号:要么都在 5 小时冷却内,要么都已被 401 排除', 'warn');
        return;
      }

      for (const account of selectedAccounts) {
        if (state.stopRequested) break;

        const title = `#${account.id} ${account.name || '(未命名)'}`;
        const models = getModels(account);

        if (!models.length) {
          state.stats.failed += 1;
          log(`${title} 没有 model_mapping,准备关闭`, 'error');
          const off = await setAccountSchedulable(account.id, false);
          if (off.ok) {
            state.stats.disabled += 1;
            log(`${title} 已关闭 schedulable`, 'success');
          } else {
            log(`${title} 关闭失败:${off.reason}`, 'error');
          }
          markAccountCompleted(account.id);
          continue;
        }

        log(`${title} 开始测试 ${models.length} 个模型`);
        let accountOk = true;
        let failClassification = 'success';
        let failReason = '';
        let sawQuotaExhausted = false;
        let sawActualSuccess = false;
        let interrupted = false;

        for (let index = 0; index < models.length; index += 1) {
          if (state.stopRequested && index > 0) {
            interrupted = true;
            break;
          }

          const model = models[index];
          log(`${title} 测试模型 ${model}`);
          const result = await testModel(account.id, model);

          if (result.ok) {
            sawActualSuccess = true;
            log(`${title} 模型 ${model} 正常`, 'success');
            continue;
          }

          if (result.classification === 'quota_exhausted') {
            sawQuotaExhausted = true;
            log(`${title} 模型 ${model} 额度已用完,视为正常:${result.reason}`, 'warn');
            continue;
          }

          accountOk = false;
          failClassification = result.classification;
          failReason = `模型 ${model} 异常:${result.reason}`;
          log(`${title} ${failReason}`, 'error');
          if (CONFIG.stopOnFirstModelFailure) break;
        }

        if (interrupted) {
          log(`${title} 已停止,当前账号未完成全部模型测试,不记入本轮结果`, 'warn');
          break;
        }

        if (accountOk) {
          state.stats.ok += 1;

          if (!account.schedulable && sawActualSuccess) {
            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 if (!account.schedulable && sawQuotaExhausted && !sawActualSuccess) {
            log(`${title} 命中 usage_limit_reached,视为正常,但保持当前关闭状态不自动启用`, 'warn');
          } else if (sawQuotaExhausted && !sawActualSuccess) {
            log(`${title} 模型额度已用完,账号视为正常`, 'success');
          } else {
            log(`${title} 全部模型正常`, 'success');
          }

          markAccountCompleted(account.id);
          continue;
        }

        state.stats.failed += 1;

        if (failClassification === 'unauthorized') {
          if (account.schedulable) {
            const off = await setAccountSchedulable(account.id, false);
            if (off.ok) {
              state.stats.disabled += 1;
              markUnauthorizedAccount(account.id);
              log(`${title} 401 已关闭 schedulable,并标记为永久跳过(原因:${failReason})`, 'success');
            } else {
              log(`${title} 401 关闭失败:${off.reason}`, 'error');
            }
          } else {
            markUnauthorizedAccount(account.id);
            log(`${title} 401 账号已是关闭状态,已标记为永久跳过`, 'success');
          }
        } else {
          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');
          }
        }

        markAccountCompleted(account.id);
      }

      if (state.stopRequested) {
        log('任务已按要求停止', 'warn');
      } else {
        log('巡检完成', 'success');
      }
    } finally {
      state.running = false;
      updateStats();
      updateProgress();
    }
  }

  injectAuthSniffer();
  waitDomReady().then(() => {
    ensurePanel();
    if (state.authHeader) {
      log('脚本已就绪,已从本地缓存 auth_token 读取 Authorization', 'success');
    } else {
      log('脚本已就绪,未发现 auth_token;可刷新页面自动捕获或手动粘贴');
    }
  });
})();

使用方法:

:one:把// @match http://127.0.0.1:8317/admin/accounts\* 改成你自己的
:two:下载浏览器插件油猴导入代码

佬友的已经做的很好了
主要增添的功能
1. 分批巡检(5h之内不会重复巡检)

  1. 可视化巡检进度

  2. 429额度用完的不会关闭调度

  3. 已经检测为401的不会重复检测

    image

注意:尽量不要反复测活

9 个帖子 - 4 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文