153 lines
4.4 KiB
JavaScript
153 lines
4.4 KiB
JavaScript
|
|
/**
|
|||
|
|
* 天眼配额守卫中间件
|
|||
|
|
* 拦截所有 API 调用请求,检查配额后放行或拒绝
|
|||
|
|
*
|
|||
|
|
* 指令:ZY-AGEOS-TOWER-2026-0326-001
|
|||
|
|
* 版权:国作登字-2026-A-00037559
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const QUOTA_CONFIG_PATH = path.join(__dirname, '..', 'skyeye', 'guards', 'quota-guard.json');
|
|||
|
|
const QUOTA_LOG_PATH = path.join(__dirname, '..', '.github', 'persona-brain', 'quota-daily.json');
|
|||
|
|
|
|||
|
|
let _quotaConfig = null;
|
|||
|
|
|
|||
|
|
function getQuotaConfig() {
|
|||
|
|
if (!_quotaConfig) {
|
|||
|
|
_quotaConfig = JSON.parse(fs.readFileSync(QUOTA_CONFIG_PATH, 'utf8'));
|
|||
|
|
}
|
|||
|
|
return _quotaConfig;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function loadDailyQuota() {
|
|||
|
|
const today = new Date().toISOString().split('T')[0];
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(fs.readFileSync(QUOTA_LOG_PATH, 'utf8'));
|
|||
|
|
if (data.date === today) return data;
|
|||
|
|
} catch (e) {
|
|||
|
|
// File missing or corrupted — will reset to fresh daily quota
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
date: today,
|
|||
|
|
total_used: 0,
|
|||
|
|
by_member: {},
|
|||
|
|
by_tier: { P0: 0, P1: 0, P2: 0 }
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveDailyQuota(daily) {
|
|||
|
|
const dir = path.dirname(QUOTA_LOG_PATH);
|
|||
|
|
if (!fs.existsSync(dir)) {
|
|||
|
|
fs.mkdirSync(dir, { recursive: true });
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync(QUOTA_LOG_PATH, JSON.stringify(daily, null, 2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getTier(devId) {
|
|||
|
|
const config = getQuotaConfig();
|
|||
|
|
for (const [tier, tierConfig] of Object.entries(config.priority_tiers)) {
|
|||
|
|
if (tierConfig.members && tierConfig.members.includes(devId)) return tier;
|
|||
|
|
}
|
|||
|
|
return null; // unknown developer — not in any tier
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function checkQuota(devId) {
|
|||
|
|
const config = getQuotaConfig();
|
|||
|
|
const daily = loadDailyQuota();
|
|||
|
|
const tier = getTier(devId);
|
|||
|
|
|
|||
|
|
if (!tier) {
|
|||
|
|
return {
|
|||
|
|
allowed: false,
|
|||
|
|
reason: '开发者 ' + devId + ' 未注册到任何配额层级',
|
|||
|
|
suggestion: '请联系管理员将你添加到配额层级中'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const tierConfig = config.priority_tiers[tier];
|
|||
|
|
|
|||
|
|
const totalDaily = config.quota_pool.daily_total;
|
|||
|
|
|
|||
|
|
// If daily_total is not yet configured, allow through
|
|||
|
|
if (totalDaily === 'auto-detect' || typeof totalDaily !== 'number') {
|
|||
|
|
return { allowed: true, remaining: -1, note: 'quota pool daily_total not yet configured' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Calculate tier allocation
|
|||
|
|
const tierAllocation = Math.floor(totalDaily * tierConfig.allocation_percent / 100);
|
|||
|
|
const tierUsed = daily.by_tier[tier] || 0;
|
|||
|
|
|
|||
|
|
if (tierUsed >= tierAllocation) {
|
|||
|
|
return {
|
|||
|
|
allowed: false,
|
|||
|
|
reason: `${tier} 层级今日配额已耗尽(${tierUsed}/${tierAllocation})`,
|
|||
|
|
suggestion: tier === 'P2' ? '请等待次日刷新或联系冰朔' : '请联系冰朔调整配额'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Per-member cap check
|
|||
|
|
const memberCount = tierConfig.members ? tierConfig.members.length : 1;
|
|||
|
|
const perMemberCap = Math.floor(tierAllocation / memberCount);
|
|||
|
|
const memberUsed = daily.by_member[devId] || 0;
|
|||
|
|
|
|||
|
|
if (memberUsed >= perMemberCap) {
|
|||
|
|
return {
|
|||
|
|
allowed: false,
|
|||
|
|
reason: `你的个人今日配额已耗尽(${memberUsed}/${perMemberCap})`,
|
|||
|
|
suggestion: '请等待次日刷新'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { allowed: true, remaining: perMemberCap - memberUsed };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function recordUsage(devId) {
|
|||
|
|
const daily = loadDailyQuota();
|
|||
|
|
const tier = getTier(devId);
|
|||
|
|
|
|||
|
|
daily.total_used = (daily.total_used || 0) + 1;
|
|||
|
|
daily.by_member[devId] = (daily.by_member[devId] || 0) + 1;
|
|||
|
|
daily.by_tier[tier] = (daily.by_tier[tier] || 0) + 1;
|
|||
|
|
|
|||
|
|
saveDailyQuota(daily);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function generateDailyReport() {
|
|||
|
|
const config = getQuotaConfig();
|
|||
|
|
const daily = loadDailyQuota();
|
|||
|
|
const totalDaily = config.quota_pool.daily_total;
|
|||
|
|
|
|||
|
|
const report = {
|
|||
|
|
date: daily.date,
|
|||
|
|
total: {
|
|||
|
|
used: daily.total_used || 0,
|
|||
|
|
limit: totalDaily,
|
|||
|
|
percent: totalDaily !== 'auto-detect' && typeof totalDaily === 'number'
|
|||
|
|
? Math.round(((daily.total_used || 0) / totalDaily) * 100)
|
|||
|
|
: 'N/A'
|
|||
|
|
},
|
|||
|
|
tiers: {},
|
|||
|
|
members: daily.by_member || {}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (const [tier, tierConfig] of Object.entries(config.priority_tiers)) {
|
|||
|
|
const tierUsed = daily.by_tier[tier] || 0;
|
|||
|
|
const allocation = typeof totalDaily === 'number'
|
|||
|
|
? Math.floor(totalDaily * tierConfig.allocation_percent / 100)
|
|||
|
|
: 'N/A';
|
|||
|
|
report.tiers[tier] = {
|
|||
|
|
name: tierConfig.name,
|
|||
|
|
used: tierUsed,
|
|||
|
|
allocation: allocation,
|
|||
|
|
percent: typeof allocation === 'number' ? Math.round((tierUsed / allocation) * 100) : 'N/A'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return report;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = { checkQuota, recordUsage, getTier, loadDailyQuota, generateDailyReport };
|