'use strict'; const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'quota-monitor-')); const Config = require('../src/core/config'); Config.DATA_DIR = tempDir; Config.DB_PATH = path.join(tempDir, 'accounts.db'); const database = require('../src/core/database'); const { runQuotaMonitor, _private } = require('../src/services/quota_monitor_service'); const { sendFeishuQuotaReport } = require('../src/services/feishu_sync_service'); const schedulerPrivate = require('../src/services/quota_monitor_scheduler')._private; (async () => { try { database.initDB(); const tables = database.getDb().prepare(` SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'quota_monitor_%' ORDER BY name `).all().map(row => row.name); assert.deepStrictEqual(tables, ['quota_monitor_items', 'quota_monitor_runs']); assert(database.getDb().pragma('table_info(quota_monitor_runs)').some(row => row.name === 'skipped_401_count')); const scheduledWrites = []; const scheduledSession = JSON.stringify({ accessToken: 'old-session-access', sessionToken: 'scheduled-session-token' }); const scheduledSessionResult = JSON.stringify({ accessToken: 'new-session-access', sessionToken: 'scheduled-session-token' }); const scheduledOnly = await _private.queryAccount({ email: 'scheduled-session-only@gmail.com', openai_auth_json: null, chatgpt_session: scheduledSession, chatgpt_status: '已登录', proxy_ip: '', }, { database: { updateAccountOpenAIPlanStatus: (...args) => { scheduledWrites.push(['planStatus', ...args]); return true; }, updateAccountChatgptResult: (...args) => { scheduledWrites.push(['session', ...args]); return true; }, updateAccountOpenAIAuth: (...args) => { scheduledWrites.push(['auth', ...args]); return true; }, updateAccountOpenAIPlan: (...args) => { scheduledWrites.push(['plan', ...args]); return true; }, }, onLog: () => {}, delay: async () => {}, query: async (authJson, _proxy, _log, options) => { assert.deepStrictEqual(authJson, {}); assert.strictEqual(options.sessionText, scheduledSession); assert.strictEqual(options.expectedEmail, 'scheduled-session-only@gmail.com'); return { success: true, authJson: { access_token: 'new-session-access', usage: { rate_limit: { primary_window: { used_percent: 100 } } }, }, sessionText: scheduledSessionResult, details: { usage: { rate_limit: { primary_window: { used_percent: 100 } } }, errors: [] }, }; }, }); assert.strictEqual(scheduledOnly.success, true, '定时查询不能因缺少授权 JSON 跳过 Session 账号'); assert(scheduledWrites.some(call => call[0] === 'session' && call[3] === scheduledSessionResult)); assert(scheduledWrites.some(call => call[0] === 'auth')); const accounts = [ ['steady@example.com', 'steady-pass', 99, 'steady-session', 'proxy-a:1080:user:pass'], ['new-high@example.com', 'new-pass', 100, 'new-session', 'proxy-b:1080:user:pass'], ['new-high-two@example.com', 'new-pass-two', 100, 'new-session-two', 'proxy-b2:1080:user:pass'], ['already-high@example.com', 'already-pass', 100, 'already-session', 'proxy-b3:1080:user:pass'], ['steady-failure@example.com', 'failure-pass', null, 'failure-session', 'proxy-c:1080:user:pass'], ['stale-usage@example.com', 'stale-pass', null, 'stale-session', 'proxy-d:1080:user:pass'], ['below@example.com', 'below-pass', 98.9, 'below-session', 'proxy-e:1080:user:pass'], ['proxy-fallback@example.com', 'fallback-pass', 97, 'fallback-session', 'proxy-f:1080:user:pass'], ]; for (const [email, password, usage, session, proxy] of accounts) { database.upsertAccount({ email, password, recovery_email: `${email}.recovery`, secret_key: `2fa-${email}`, status: '已充值提取' }); database.updateAccountProxyIp(email, proxy); database.updateAccountChatgptResult(email, '已登录', JSON.stringify({ accessToken: session })); database.updateAccountOpenAIAuth(email, '已提取 JSON', JSON.stringify({ access_token: email, refresh_token: `refresh-${email}`, plan_type: 'plus', usage: usage == null ? { rate_limit: { primary_window: { used_percent: 100 } } } : { rate_limit: { primary_window: { used_percent: usage } } }, })); } database.updateAccountRefundStatus('steady@example.com', '已退款'); database.updateAccountRefundProcessStatus('already-high@example.com', '已发起'); const previousRunId = database.createQuotaMonitorRun({ taskId: 'previous', triggerType: 'scheduled', threshold: 99, eligibleCount: 2 }); database.saveQuotaMonitorItems(previousRunId, [ { email: 'steady@example.com', category: 'threshold', usagePercent: 99, isNew: true }, { email: 'steady-failure@example.com', category: 'failure', failureReason: 'Token失效(401)', isNew: true }, ]); database.completeQuotaMonitorRun(previousRunId, { successCount: 1, thresholdCount: 1, failureCount: 1, thresholdNewCount: 1, failureNewCount: 1, deliveryStatus: 'sent', }); const proxyCalls = []; const query = async (authJson, proxy, _log, options) => { proxyCalls.push([authJson.access_token, proxy, options.connectionMode]); if (authJson.access_token === 'steady-failure@example.com') { const error = new Error('access_token 无法查询套餐和用量'); error.code = 'OPENAI_ACCOUNT_QUERY_UNAUTHORIZED'; return { success: false, authJson, error }; } if (authJson.access_token === 'stale-usage@example.com') { return { success: true, authJson, details: { usage: null, errors: [{ source: 'usage', status: 503 }] } }; } if (authJson.access_token === 'proxy-fallback@example.com' && options.connectionMode === 'browser_proxy') { const error = new Error('账号代理连接失败'); error.code = 'OPENAI_PLAN_PROXY_CONNECTION_FAILED'; return { success: false, authJson, error }; } const source = accounts.find(item => item[0] === authJson.access_token); const usedPercent = source[2]; return { success: true, authJson: { ...authJson, usage: { rate_limit: { primary_window: { used_percent: usedPercent } } } }, details: { usage: { rate_limit: { primary_window: { used_percent: usedPercent } } }, errors: [] }, }; }; let sentReport = null; let syncedRefundEntries = null; const refundCalls = []; let activeRefunds = 0; let maxActiveRefunds = 0; const result = await runQuotaMonitor({ database, taskId: 'quota-current', triggerType: 'manual', threshold: 100, concurrency: 2, autoRefundEnabled: true, query, loadRefundCredentials: async () => ({ accessToken: 'owner-token', proxies: ['socks5://proxy:1080'] }), executeRefund: async payload => { activeRefunds += 1; maxActiveRefunds = Math.max(maxActiveRefunds, activeRefunds); refundCalls.push(payload.expectedEmail); await new Promise(resolve => setImmediate(resolve)); activeRefunds -= 1; return { success: true, cleanupOk: true }; }, sendReport: async payload => { sentReport = payload; return { success: true }; }, syncRefundStatuses: async payload => { syncedRefundEntries = payload.entries.map(item => ({ email: item.email, status: item.refundProcessStatus, result: item.refundResult, })); return { success: true, updatedCount: payload.entries.length, skippedCount: 0 }; }, delay: async () => {}, }); assert.strictEqual(result.success, true); assert.strictEqual(result.eligibleCount, 7); assert.strictEqual(result.successCount, 5); assert.strictEqual(result.thresholdCount, 3); assert.strictEqual(result.failureCount, 2); assert.strictEqual(result.thresholdPreviousCount, 1); assert.strictEqual(result.failurePreviousCount, 1); assert.strictEqual(result.thresholdNewCount, 3); assert.strictEqual(result.failureNewCount, 1); assert.strictEqual(result.thresholdRemovedCount, 1); assert.strictEqual(result.failureRecoveredCount, 0); assert.strictEqual(result.refundCandidateCount, 2); assert.strictEqual(result.refundAttemptedCount, 2); assert.strictEqual(result.refundSuccessCount, 2); assert.strictEqual(result.refundFailureCount, 0); assert.strictEqual(result.refundSkippedCount, 0); assert.deepStrictEqual(refundCalls, ['new-high@example.com', 'new-high-two@example.com']); assert.strictEqual(maxActiveRefunds, 1, '自动退款必须固定单路串行'); assert.deepStrictEqual(syncedRefundEntries, [ { email: 'new-high@example.com', status: '已发起', result: '成功' }, { email: 'new-high-two@example.com', status: '已发起', result: '成功' }, ], '额度自动退款结束后应只回写本轮已处理账号的退款流程状态'); assert.strictEqual(database.getAccount('new-high@example.com').refund_process_status, '已发起'); assert.strictEqual(database.getAccount('new-high-two@example.com').refund_process_status, '已发起'); assert.strictEqual(database.getAccount('already-high@example.com').refund_process_status, '已发起'); assert.strictEqual(database.getAccount('new-high@example.com').account_refund_status, '无状态', '自动退款不得修改账号退款字段'); assert(proxyCalls.some(call => call[0] === 'new-high@example.com' && call[1] === 'proxy-b:1080:user:pass')); assert(!proxyCalls.some(call => call[0] === 'steady@example.com'), '退款状态为已退款的账号必须跳过查询'); assert.strictEqual(proxyCalls.filter(call => call[0] === 'proxy-fallback@example.com' && call[2] === 'browser_proxy').length, 1, '明确的代理连接失败不应重复代理请求'); assert(proxyCalls.some(call => call[0] === 'proxy-fallback@example.com' && call[1] === '' && call[2] === 'direct'), '代理连接失败后必须自动直连'); assert(sentReport, '应发送飞书报告'); assert.strictEqual(sentReport.files.length, 1); assert(sentReport.files[0].name.startsWith('额度监控结果_')); const reportCsv = sentReport.files[0].content.toString('utf8'); assert(reportCsv.startsWith(`\uFEFF${_private.CSV_HEADERS.map(value => `"${value}"`).join(',')}`)); assert(!reportCsv.includes('steady@example.com'), '退款状态为已退款的账号不得进入报告'); assert(reportCsv.includes('"无状态","已发起","100%","new-high@example.com","new-pass"')); assert(reportCsv.includes('"new-high@example.com.recovery","2fa-new-high@example.com","{""accessToken"":""new-session""}","查询成功","成功","","是",""')); assert(reportCsv.includes('"查询成功","成功","","是",""')); assert(reportCsv.includes('"无状态","未发起","","steady-failure@example.com"')); assert(reportCsv.includes('Token失效(401)')); assert(reportCsv.includes('"stale-usage@example.com"')); assert(reportCsv.includes('本轮未返回额度数据')); assert(!reportCsv.includes('proxy-fallback@example.com'), '直连成功且额度未达到阈值的账号不应进入报告'); const currentRun = database.getQuotaMonitorRun(result.runId); assert.strictEqual(currentRun.status, 'completed'); assert.strictEqual(currentRun.delivery_status, 'sent'); const storedItems = database.getQuotaMonitorItems(result.runId); assert.strictEqual(storedItems.length, 5); assert.strictEqual(currentRun.auto_refund_enabled, 1); assert.strictEqual(currentRun.refund_success_count, 2); assert.strictEqual(Object.prototype.hasOwnProperty.call(storedItems[0], 'password'), false, '报告历史不得保存密码'); assert.strictEqual(database.getAccount('steady@example.com').status, '已充值提取'); const skip401Email = 'skip-401@example.com'; database.upsertAccount({ email: skip401Email, password: 'skip-pass', recovery_email: 'skip-recovery@example.com', secret_key: 'skip-2fa', status: '已充值提取', }); database.updateAccountOpenAIAuth(skip401Email, '已提取 JSON', JSON.stringify({ access_token: 'skip-access', refresh_token: 'skip-refresh', })); database.updateAccountOpenAIPlanStatus(skip401Email, 'Token失效(401)'); for (const account of database.getAccountsByStatuses(['已充值提取'])) { if (account.email !== skip401Email) database.updateAccountRefundStatus(account.email, '已退款'); } let skippedQueryCalls = 0; let skippedReport = null; const skip401Result = await runQuotaMonitor({ database, taskId: 'quota-skip-401', triggerType: 'manual', threshold: 100, concurrency: 2, skip401Enabled: true, autoRefundEnabled: false, query: async () => { skippedQueryCalls += 1; throw new Error('明确 401 账号不应进入查询'); }, sendReport: async payload => { skippedReport = payload; return { success: true }; }, delay: async () => {}, }); assert.strictEqual(skippedQueryCalls, 0, '开启设置后明确 401 账号不得再次发起套餐请求'); assert.strictEqual(skip401Result.eligibleCount, 0); assert.strictEqual(skip401Result.skipped401Count, 1); assert.strictEqual(skip401Result.failureCount, 0, '跳过 401 不应重复计入查询失败'); assert(skippedReport.text.includes('明确 401 跳过 1')); assert.strictEqual(database.getQuotaMonitorRun(skip401Result.runId).skipped_401_count, 1); const feishuRequests = []; const feishuHttpClient = { async request(config) { feishuRequests.push(config); if (config.url.endsWith('/auth/v3/tenant_access_token/internal')) { return { status: 200, data: { code: 0, tenant_access_token: 'tenant-token' } }; } if (config.url.endsWith('/im/v1/files')) { return { status: 200, data: { code: 0, data: { file_key: `file-${feishuRequests.length}` } } }; } if (config.url.endsWith('/im/v1/messages')) { return { status: 200, data: { code: 0, data: {} } }; } throw new Error(`unexpected request ${config.url}`); }, }; const sent = await sendFeishuQuotaReport({ database: { getSetting: (key, fallback) => ({ feishu_notify_mode: 'private', feishu_notify_receive_id_type: 'email', feishu_notify_receive_id: 'owner@example.com', }[key] || fallback) }, credentialStore: { loadFeishuCredentials: async () => ({ appId: 'cli_test', appSecret: 'secret' }) }, httpClient: feishuHttpClient, text: '监控完成', files: [{ name: 'report.csv', content: Buffer.from('test') }], }); assert.strictEqual(sent.fileCount, 1); const upload = feishuRequests.find(request => request.url.endsWith('/im/v1/files')); assert(upload && upload.data instanceof FormData); assert.strictEqual(Object.keys(upload.headers).some(key => key.toLowerCase() === 'content-type'), false, 'multipart 边界必须由运行时生成'); const messages = feishuRequests.filter(request => request.url.endsWith('/im/v1/messages')); assert.strictEqual(messages.length, 2); assert.strictEqual(messages[0].data.msg_type, 'text'); assert.strictEqual(messages[1].data.msg_type, 'file'); const fakeSettings = new Map([ ['quota_monitor_enabled', '1'], ['quota_monitor_interval_value', '2'], ['quota_monitor_interval_unit', 'hours'], ['quota_monitor_concurrency', '20'], ['quota_monitor_threshold', '100'], ['quota_monitor_skip_401_enabled', '1'], ['quota_monitor_auto_refund_enabled', '1'], ]); const fakeDb = { getSetting: (key, fallback) => fakeSettings.has(key) ? fakeSettings.get(key) : fallback, setSetting: (key, value) => fakeSettings.set(key, value), }; const config = schedulerPrivate.readConfig(fakeDb); assert.strictEqual(config.enabled, true); assert.strictEqual(config.intervalMs, 2 * 60 * 60 * 1000); assert.strictEqual(config.concurrency, 10); assert.strictEqual(config.threshold, 100); assert.strictEqual(config.skip401Enabled, true); assert.strictEqual(config.autoRefundEnabled, true); assert(schedulerPrivate.ensureNextRun(config, fakeDb, true) instanceof Date); const root = path.join(__dirname, '..'); const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8'); const app = fs.readFileSync(path.join(root, 'public', 'app.js'), 'utf8'); const server = fs.readFileSync(path.join(root, 'server.js'), 'utf8'); assert.match(html, /额度监控与自动退款/); assert.match(html, /@click="runQuotaMonitorNow\(\)"/); assert.match(html, /quota_monitor_concurrency/); assert.match(html, /quota_monitor_threshold/); assert.match(html, /quota_monitor_skip_401_enabled/); assert.match(html, /quota_monitor_auto_refund_enabled/); assert.match(html, /quota_monitor_interval_unit[\s\S]*settings-select/); assert.match(html, /feishu_notify_mode[\s\S]*settings-select/); assert.match(app, /\/api\/quota-monitor\/run/); assert.match(server, /\/api\/quota-monitor/); console.log('quota_monitor.test.js: PASS'); } finally { try { database.getDb().close(); } catch (_) {} fs.rmSync(tempDir, { recursive: true, force: true }); } })().catch(error => { console.error(error); process.exitCode = 1; });