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