1. 新增用户凭证数据管理模块(user-data-manager.js),支持凭证关联、资源包计算和配置热更新 2. 实现资源包机制:每个健康凭证提供额外调用次数,支持有效期管理和自动过期清理 3. 新增系统配置API:支持动态调整默认限额、资源包次数和有效期 4. 新增批量操作API:批量应用限额和同步资源包状态到所有Key 5. 实现凭证健康检查:从主服务ProviderPoolManager同步凭证状态 6. 新增用户端API Key重置功能,支持数据自动迁移 7. 重构前端界面:采用GitHub风格深色主题,优化移动端响应式布局 8. 新增定时健康检查调度器,自动同步所有用户凭证状态
721 lines
22 KiB
JavaScript
721 lines
22 KiB
JavaScript
/**
|
||
* API 大锅饭 - 用户数据管理模块
|
||
* 管理用户关联的凭据文件路径和资源包
|
||
* 使用 Mutex 解决并发问题
|
||
*/
|
||
|
||
import { promises as fs } from 'fs';
|
||
import { existsSync, readFileSync, writeFileSync, watch } from 'fs';
|
||
import path from 'path';
|
||
|
||
// 配置文件路径
|
||
const USER_DATA_FILE = path.join(process.cwd(), 'configs', 'api-potluck-data.json');
|
||
|
||
// 默认配置值
|
||
const DEFAULT_CONFIG = {
|
||
defaultDailyLimit: 500,
|
||
bonusPerCredential: 300,
|
||
bonusValidityDays: 30,
|
||
persistInterval: 5000
|
||
};
|
||
|
||
// 内存缓存
|
||
let userDataStore = null;
|
||
let isDirty = false;
|
||
let isWriting = false;
|
||
let persistTimer = null;
|
||
let fileWatcher = null;
|
||
let currentPersistInterval = DEFAULT_CONFIG.persistInterval;
|
||
|
||
// ============ 简易 Mutex 实现 ============
|
||
class SimpleMutex {
|
||
constructor() {
|
||
this._locked = false;
|
||
this._waiting = [];
|
||
}
|
||
|
||
async acquire() {
|
||
return new Promise((resolve) => {
|
||
if (!this._locked) {
|
||
this._locked = true;
|
||
resolve();
|
||
} else {
|
||
this._waiting.push(resolve);
|
||
}
|
||
});
|
||
}
|
||
|
||
release() {
|
||
if (this._waiting.length > 0) {
|
||
const next = this._waiting.shift();
|
||
next();
|
||
} else {
|
||
this._locked = false;
|
||
}
|
||
}
|
||
|
||
async runExclusive(fn) {
|
||
await this.acquire();
|
||
try {
|
||
return await fn();
|
||
} finally {
|
||
this.release();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全局锁:用于资源包消耗操作
|
||
const bonusMutex = new SimpleMutex();
|
||
|
||
// ============ 配置管理 ============
|
||
|
||
/**
|
||
* 获取完整配置(支持热更新)
|
||
*/
|
||
function getFullConfig() {
|
||
ensureLoaded();
|
||
const config = userDataStore.config || {};
|
||
return {
|
||
defaultDailyLimit: config.defaultDailyLimit ?? DEFAULT_CONFIG.defaultDailyLimit,
|
||
bonusPerCredential: config.bonusPerCredential ?? DEFAULT_CONFIG.bonusPerCredential,
|
||
bonusValidityDays: config.bonusValidityDays ?? DEFAULT_CONFIG.bonusValidityDays,
|
||
persistInterval: config.persistInterval ?? DEFAULT_CONFIG.persistInterval
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取资源包配置(兼容旧接口)
|
||
*/
|
||
function getBonusConfig() {
|
||
const config = getFullConfig();
|
||
return {
|
||
bonusPerCredential: config.bonusPerCredential,
|
||
bonusValidityDays: config.bonusValidityDays
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 更新配置
|
||
* @param {Object} newConfig - 新配置
|
||
* @returns {Object} 更新后的完整配置
|
||
*/
|
||
export async function updateConfig(newConfig) {
|
||
ensureLoaded();
|
||
|
||
if (!userDataStore.config) {
|
||
userDataStore.config = {};
|
||
}
|
||
|
||
// 验证并更新各配置项
|
||
if (typeof newConfig.defaultDailyLimit === 'number' && newConfig.defaultDailyLimit > 0) {
|
||
userDataStore.config.defaultDailyLimit = newConfig.defaultDailyLimit;
|
||
}
|
||
if (typeof newConfig.bonusPerCredential === 'number' && newConfig.bonusPerCredential >= 0) {
|
||
userDataStore.config.bonusPerCredential = newConfig.bonusPerCredential;
|
||
}
|
||
if (typeof newConfig.bonusValidityDays === 'number' && newConfig.bonusValidityDays > 0) {
|
||
userDataStore.config.bonusValidityDays = newConfig.bonusValidityDays;
|
||
}
|
||
if (typeof newConfig.persistInterval === 'number' && newConfig.persistInterval >= 1000) {
|
||
userDataStore.config.persistInterval = newConfig.persistInterval;
|
||
// 更新持久化定时器
|
||
updatePersistTimer(newConfig.persistInterval);
|
||
}
|
||
|
||
markDirty();
|
||
await persistIfDirty();
|
||
|
||
const updatedConfig = getFullConfig();
|
||
console.log(`[API Potluck UserData] Config updated:`, updatedConfig);
|
||
return updatedConfig;
|
||
}
|
||
|
||
/**
|
||
* 更新持久化定时器间隔
|
||
*/
|
||
function updatePersistTimer(newInterval) {
|
||
if (newInterval === currentPersistInterval) return;
|
||
|
||
currentPersistInterval = newInterval;
|
||
if (persistTimer) {
|
||
clearInterval(persistTimer);
|
||
persistTimer = setInterval(persistIfDirty, currentPersistInterval);
|
||
console.log(`[API Potluck UserData] Persist interval updated to ${currentPersistInterval}ms`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前配置(对外暴露)
|
||
*/
|
||
export function getConfig() {
|
||
return getFullConfig();
|
||
}
|
||
|
||
/**
|
||
* 兼容旧接口:更新资源包配置
|
||
*/
|
||
export async function updateBonusConfig(newConfig) {
|
||
return updateConfig(newConfig);
|
||
}
|
||
|
||
/**
|
||
* 初始化:从文件加载数据到内存
|
||
*/
|
||
function ensureLoaded() {
|
||
if (userDataStore !== null) return;
|
||
try {
|
||
if (existsSync(USER_DATA_FILE)) {
|
||
const content = readFileSync(USER_DATA_FILE, 'utf8');
|
||
userDataStore = JSON.parse(content);
|
||
// 兼容旧数据:确保 config 和 users 存在
|
||
if (!userDataStore.config) {
|
||
userDataStore.config = {};
|
||
}
|
||
if (!userDataStore.users) {
|
||
userDataStore.users = {};
|
||
}
|
||
} else {
|
||
userDataStore = { config: {}, users: {} };
|
||
syncWriteToFile();
|
||
}
|
||
} catch (error) {
|
||
console.error('[API Potluck UserData] Failed to load user data:', error.message);
|
||
userDataStore = { config: {}, users: {} };
|
||
}
|
||
|
||
// 获取配置的持久化间隔
|
||
const config = userDataStore.config || {};
|
||
currentPersistInterval = config.persistInterval ?? DEFAULT_CONFIG.persistInterval;
|
||
|
||
// 启动定期持久化
|
||
if (!persistTimer) {
|
||
persistTimer = setInterval(persistIfDirty, currentPersistInterval);
|
||
}
|
||
// 启动文件监听(热更新)
|
||
startFileWatcher();
|
||
}
|
||
|
||
/**
|
||
* 同步写入文件(仅初始化时使用)
|
||
*/
|
||
function syncWriteToFile() {
|
||
try {
|
||
const dir = path.dirname(USER_DATA_FILE);
|
||
if (!existsSync(dir)) {
|
||
require('fs').mkdirSync(dir, { recursive: true });
|
||
}
|
||
writeFileSync(USER_DATA_FILE, JSON.stringify(userDataStore, null, 2), 'utf8');
|
||
} catch (error) {
|
||
console.error('[API Potluck UserData] Sync write failed:', error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 异步持久化(带写锁)
|
||
*/
|
||
async function persistIfDirty() {
|
||
if (!isDirty || isWriting || userDataStore === null) return;
|
||
isWriting = true;
|
||
try {
|
||
const dir = path.dirname(USER_DATA_FILE);
|
||
if (!existsSync(dir)) {
|
||
await fs.mkdir(dir, { recursive: true });
|
||
}
|
||
const tempFile = USER_DATA_FILE + '.tmp';
|
||
await fs.writeFile(tempFile, JSON.stringify(userDataStore, null, 2), 'utf8');
|
||
await fs.rename(tempFile, USER_DATA_FILE);
|
||
isDirty = false;
|
||
} catch (error) {
|
||
console.error('[API Potluck UserData] Persist failed:', error.message);
|
||
} finally {
|
||
isWriting = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 标记数据已修改
|
||
*/
|
||
function markDirty() {
|
||
isDirty = true;
|
||
}
|
||
|
||
/**
|
||
* 启动文件监听(热更新配置)
|
||
*/
|
||
let lastReloadTime = 0;
|
||
function startFileWatcher() {
|
||
if (fileWatcher) return;
|
||
|
||
try {
|
||
fileWatcher = watch(USER_DATA_FILE, { persistent: false }, (eventType) => {
|
||
if (eventType !== 'change') return;
|
||
|
||
// 防抖:忽略自己写入触发的事件
|
||
const now = Date.now();
|
||
if (now - lastReloadTime < 1000 || isWriting) return;
|
||
lastReloadTime = now;
|
||
|
||
// 重新加载配置部分
|
||
try {
|
||
const content = readFileSync(USER_DATA_FILE, 'utf8');
|
||
const newData = JSON.parse(content);
|
||
|
||
// 只热更新 config 部分,不覆盖内存中的 users 数据
|
||
if (newData.config) {
|
||
const oldConfig = userDataStore.config || {};
|
||
const newConfig = newData.config;
|
||
|
||
// 检查配置是否有变化
|
||
if (JSON.stringify(oldConfig) !== JSON.stringify(newConfig)) {
|
||
userDataStore.config = newConfig;
|
||
console.log('[API Potluck UserData] Config hot-reloaded:', getBonusConfig());
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[API Potluck UserData] Hot-reload failed:', error.message);
|
||
}
|
||
});
|
||
|
||
console.log('[API Potluck UserData] File watcher started for config hot-reload');
|
||
} catch (error) {
|
||
console.error('[API Potluck UserData] Failed to start file watcher:', error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止文件监听
|
||
*/
|
||
export function stopFileWatcher() {
|
||
if (fileWatcher) {
|
||
fileWatcher.close();
|
||
fileWatcher = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户数据
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @returns {Object|null}
|
||
*/
|
||
export function getUserData(apiKey) {
|
||
ensureLoaded();
|
||
return userDataStore.users[apiKey] || null;
|
||
}
|
||
|
||
/**
|
||
* 初始化用户数据(如果不存在)
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @returns {Object}
|
||
*/
|
||
export function ensureUserData(apiKey) {
|
||
ensureLoaded();
|
||
if (!userDataStore.users[apiKey]) {
|
||
userDataStore.users[apiKey] = {
|
||
credentials: [],
|
||
credentialBonuses: [],
|
||
createdAt: new Date().toISOString()
|
||
};
|
||
markDirty();
|
||
}
|
||
// 兼容旧数据:添加 credentialBonuses 数组
|
||
if (!userDataStore.users[apiKey].credentialBonuses) {
|
||
userDataStore.users[apiKey].credentialBonuses = [];
|
||
markDirty();
|
||
}
|
||
return userDataStore.users[apiKey];
|
||
}
|
||
|
||
/**
|
||
* 添加凭据路径到用户
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {Object} credentialInfo - 凭据信息
|
||
* @param {string} credentialInfo.path - 凭据文件路径
|
||
* @param {string} credentialInfo.provider - 提供商类型 (如 'claude-kiro-oauth')
|
||
* @param {string} [credentialInfo.authMethod] - 认证方式 (如 'builder-id', 'google', 'github')
|
||
* @returns {Object} 添加的凭据信息
|
||
*/
|
||
export async function addUserCredential(apiKey, credentialInfo) {
|
||
ensureLoaded();
|
||
const userData = ensureUserData(apiKey);
|
||
|
||
// 检查是否已存在相同路径
|
||
const existingIndex = userData.credentials.findIndex(c => c.path === credentialInfo.path);
|
||
|
||
// 只保留核心字段,健康状态从主服务实时获取
|
||
const credential = {
|
||
id: `cred_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
||
path: credentialInfo.path,
|
||
provider: credentialInfo.provider || 'claude-kiro-oauth',
|
||
authMethod: credentialInfo.authMethod || 'unknown',
|
||
addedAt: new Date().toISOString()
|
||
};
|
||
|
||
if (existingIndex >= 0) {
|
||
// 更新已存在的凭据,保留原有 id 和 addedAt
|
||
credential.id = userData.credentials[existingIndex].id;
|
||
credential.addedAt = userData.credentials[existingIndex].addedAt;
|
||
userData.credentials[existingIndex] = credential;
|
||
} else {
|
||
userData.credentials.push(credential);
|
||
}
|
||
|
||
markDirty();
|
||
await persistIfDirty();
|
||
|
||
return credential;
|
||
}
|
||
|
||
/**
|
||
* 移除用户凭据
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {string} credentialId - 凭据 ID
|
||
* @returns {boolean}
|
||
*/
|
||
export async function removeUserCredential(apiKey, credentialId) {
|
||
ensureLoaded();
|
||
const userData = userDataStore.users[apiKey];
|
||
if (!userData) return false;
|
||
|
||
const index = userData.credentials.findIndex(c => c.id === credentialId);
|
||
if (index === -1) return false;
|
||
|
||
userData.credentials.splice(index, 1);
|
||
markDirty();
|
||
await persistIfDirty();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取用户的所有凭据
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @returns {Array}
|
||
*/
|
||
export function getUserCredentials(apiKey) {
|
||
ensureLoaded();
|
||
const userData = userDataStore.users[apiKey];
|
||
return userData ? userData.credentials : [];
|
||
}
|
||
|
||
/**
|
||
* 通过路径查找凭据
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {string} credPath - 凭据文件路径
|
||
* @returns {Object|null}
|
||
*/
|
||
export function findCredentialByPath(apiKey, credPath) {
|
||
ensureLoaded();
|
||
const userData = userDataStore.users[apiKey];
|
||
if (!userData) return null;
|
||
|
||
return userData.credentials.find(c => c.path === credPath) || null;
|
||
}
|
||
|
||
/**
|
||
* 检查凭据路径是否已被任何用户使用
|
||
* @param {string} credPath - 凭据文件路径
|
||
* @returns {{exists: boolean, apiKey?: string}}
|
||
*/
|
||
export function isCredentialPathUsed(credPath) {
|
||
ensureLoaded();
|
||
for (const [apiKey, userData] of Object.entries(userDataStore.users)) {
|
||
const found = userData.credentials.find(c => c.path === credPath);
|
||
if (found) {
|
||
return { exists: true, apiKey };
|
||
}
|
||
}
|
||
return { exists: false };
|
||
}
|
||
|
||
/**
|
||
* 迁移用户凭据到新 Key(用于 Key 重置时)
|
||
* @param {string} oldApiKey - 旧 API Key
|
||
* @param {string} newApiKey - 新 API Key
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
export async function migrateUserCredentials(oldApiKey, newApiKey) {
|
||
ensureLoaded();
|
||
const oldUserData = userDataStore.users[oldApiKey];
|
||
if (!oldUserData) return false;
|
||
|
||
// 将旧用户数据迁移到新 Key
|
||
userDataStore.users[newApiKey] = {
|
||
...oldUserData,
|
||
migratedFrom: oldApiKey.substring(0, 12) + '...',
|
||
migratedAt: new Date().toISOString()
|
||
};
|
||
|
||
// 删除旧用户数据
|
||
delete userDataStore.users[oldApiKey];
|
||
|
||
markDirty();
|
||
await persistIfDirty();
|
||
|
||
console.log(`[API Potluck UserData] Migrated credentials from ${oldApiKey.substring(0, 12)}... to ${newApiKey.substring(0, 12)}...`);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取所有用户及其凭据(用于批量健康检查)
|
||
* @returns {Array<{apiKey: string, credentials: Array}>}
|
||
*/
|
||
export function getAllUsersCredentials() {
|
||
ensureLoaded();
|
||
const result = [];
|
||
for (const [apiKey, userData] of Object.entries(userDataStore.users)) {
|
||
if (userData.credentials && userData.credentials.length > 0) {
|
||
result.push({
|
||
apiKey,
|
||
credentials: userData.credentials
|
||
});
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ============ 凭证资源包管理 ============
|
||
|
||
/**
|
||
* 计算资源包过期时间(使用动态配置)
|
||
* @param {string} grantedAt - 授予时间
|
||
* @returns {Date}
|
||
*/
|
||
function calculateExpiresAt(grantedAt) {
|
||
const { bonusValidityDays } = getBonusConfig();
|
||
const granted = new Date(grantedAt);
|
||
return new Date(granted.getTime() + bonusValidityDays * 24 * 60 * 60 * 1000);
|
||
}
|
||
|
||
/**
|
||
* 检查资源包是否过期
|
||
* @param {Object} bonus - 资源包对象
|
||
* @returns {boolean}
|
||
*/
|
||
function isBonusExpired(bonus) {
|
||
const expiresAt = calculateExpiresAt(bonus.grantedAt);
|
||
return new Date() > expiresAt;
|
||
}
|
||
|
||
/**
|
||
* 为凭证添加资源包(凭证健康时调用)
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {string} credentialId - 凭证 ID
|
||
* @returns {Object|null} 添加的资源包信息
|
||
*/
|
||
export async function addCredentialBonus(apiKey, credentialId) {
|
||
ensureLoaded();
|
||
const userData = ensureUserData(apiKey);
|
||
|
||
// 检查是否已存在
|
||
const existing = userData.credentialBonuses.find(b => b.credentialId === credentialId);
|
||
if (existing) {
|
||
return existing;
|
||
}
|
||
|
||
const bonus = {
|
||
credentialId,
|
||
grantedAt: new Date().toISOString(),
|
||
usedCount: 0
|
||
};
|
||
|
||
userData.credentialBonuses.push(bonus);
|
||
markDirty();
|
||
|
||
console.log(`[API Potluck UserData] Added bonus for credential: ${credentialId}`);
|
||
return bonus;
|
||
}
|
||
|
||
/**
|
||
* 移除凭证资源包(凭证失效时调用)
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {string} credentialId - 凭证 ID
|
||
* @returns {boolean}
|
||
*/
|
||
export async function removeCredentialBonus(apiKey, credentialId) {
|
||
ensureLoaded();
|
||
const userData = userDataStore.users[apiKey];
|
||
if (!userData || !userData.credentialBonuses) return false;
|
||
|
||
const index = userData.credentialBonuses.findIndex(b => b.credentialId === credentialId);
|
||
if (index === -1) return false;
|
||
|
||
userData.credentialBonuses.splice(index, 1);
|
||
markDirty();
|
||
|
||
console.log(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 消耗资源包次数(FIFO 顺序,使用 Mutex 保证并发安全)
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @returns {boolean} 是否成功消耗
|
||
*/
|
||
export async function consumeBonus(apiKey) {
|
||
// 使用 Mutex 保证并发安全
|
||
return bonusMutex.runExclusive(async () => {
|
||
ensureLoaded();
|
||
const userData = userDataStore.users[apiKey];
|
||
if (!userData || !userData.credentialBonuses) return false;
|
||
|
||
const { bonusPerCredential } = getBonusConfig();
|
||
|
||
// 按 grantedAt 排序(FIFO)
|
||
const sortedBonuses = userData.credentialBonuses
|
||
.filter(b => !isBonusExpired(b))
|
||
.sort((a, b) => new Date(a.grantedAt) - new Date(b.grantedAt));
|
||
|
||
// 找到第一个有剩余次数的资源包
|
||
for (const bonus of sortedBonuses) {
|
||
const remaining = bonusPerCredential - bonus.usedCount;
|
||
if (remaining > 0) {
|
||
bonus.usedCount += 1;
|
||
markDirty();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 计算用户的剩余资源包总次数
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {Set<string>} [healthyCredentialIds] - 健康凭证 ID 集合(可选,用于过滤)
|
||
* @returns {number}
|
||
*/
|
||
export function calculateBonusRemaining(apiKey, healthyCredentialIds = null) {
|
||
ensureLoaded();
|
||
const userData = userDataStore.users[apiKey];
|
||
if (!userData || !userData.credentialBonuses) return 0;
|
||
|
||
const { bonusPerCredential } = getBonusConfig();
|
||
|
||
let total = 0;
|
||
for (const bonus of userData.credentialBonuses) {
|
||
// 检查是否过期
|
||
if (isBonusExpired(bonus)) continue;
|
||
|
||
// 如果提供了健康凭证集合,检查凭证是否健康
|
||
if (healthyCredentialIds && !healthyCredentialIds.has(bonus.credentialId)) continue;
|
||
|
||
const remaining = bonusPerCredential - bonus.usedCount;
|
||
if (remaining > 0) {
|
||
total += remaining;
|
||
}
|
||
}
|
||
|
||
return total;
|
||
}
|
||
|
||
/**
|
||
* 同步资源包状态(根据健康凭证列表)
|
||
* 兼容历史数据:为已有健康凭证创建资源包,使用凭证的 addedAt 作为 grantedAt
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @param {Array<{id: string, isHealthy: boolean, addedAt?: string}>} credentialsWithHealth - 带健康状态的凭证列表
|
||
* @returns {{added: number, removed: number, bonusRemaining: number}}
|
||
*/
|
||
export async function syncCredentialBonuses(apiKey, credentialsWithHealth) {
|
||
ensureLoaded();
|
||
const userData = ensureUserData(apiKey);
|
||
|
||
let added = 0, removed = 0;
|
||
|
||
// 获取健康凭证 ID 集合
|
||
const healthyIds = new Set(
|
||
credentialsWithHealth
|
||
.filter(c => c.isHealthy === true)
|
||
.map(c => c.id)
|
||
);
|
||
|
||
// 为新的健康凭证添加资源包
|
||
for (const cred of credentialsWithHealth) {
|
||
if (cred.isHealthy !== true) continue;
|
||
|
||
const exists = userData.credentialBonuses.some(b => b.credentialId === cred.id);
|
||
if (!exists) {
|
||
// 使用凭证的 addedAt 作为资源包授予时间(兼容历史数据)
|
||
const grantedAt = cred.addedAt || new Date().toISOString();
|
||
userData.credentialBonuses.push({
|
||
credentialId: cred.id,
|
||
grantedAt: grantedAt,
|
||
usedCount: 0
|
||
});
|
||
added++;
|
||
console.log(`[API Potluck UserData] Created bonus for credential ${cred.id}, grantedAt: ${grantedAt}`);
|
||
}
|
||
}
|
||
|
||
// 移除失效凭证的资源包
|
||
const toRemove = userData.credentialBonuses.filter(b => !healthyIds.has(b.credentialId));
|
||
for (const bonus of toRemove) {
|
||
const idx = userData.credentialBonuses.indexOf(bonus);
|
||
if (idx !== -1) {
|
||
userData.credentialBonuses.splice(idx, 1);
|
||
removed++;
|
||
}
|
||
}
|
||
|
||
// 清理过期资源包
|
||
const expiredCount = userData.credentialBonuses.filter(b => isBonusExpired(b)).length;
|
||
userData.credentialBonuses = userData.credentialBonuses.filter(b => !isBonusExpired(b));
|
||
|
||
if (added > 0 || removed > 0 || expiredCount > 0) {
|
||
markDirty();
|
||
}
|
||
|
||
// 计算剩余资源包次数
|
||
const bonusRemaining = calculateBonusRemaining(apiKey, healthyIds);
|
||
|
||
return { added, removed, bonusRemaining };
|
||
}
|
||
|
||
/**
|
||
* 获取用户的资源包详情
|
||
* @param {string} apiKey - 用户的 API Key
|
||
* @returns {Object}
|
||
*/
|
||
export function getBonusDetails(apiKey) {
|
||
ensureLoaded();
|
||
const { bonusPerCredential, bonusValidityDays } = getBonusConfig();
|
||
const userData = userDataStore.users[apiKey];
|
||
if (!userData) {
|
||
return {
|
||
bonuses: [],
|
||
totalRemaining: 0,
|
||
bonusPerCredential,
|
||
validityDays: bonusValidityDays
|
||
};
|
||
}
|
||
|
||
const bonuses = (userData.credentialBonuses || [])
|
||
.filter(b => !isBonusExpired(b))
|
||
.map(b => ({
|
||
credentialId: b.credentialId,
|
||
grantedAt: b.grantedAt,
|
||
expiresAt: calculateExpiresAt(b.grantedAt).toISOString(),
|
||
usedCount: b.usedCount,
|
||
remaining: bonusPerCredential - b.usedCount
|
||
}));
|
||
|
||
const totalRemaining = bonuses.reduce((sum, b) => sum + Math.max(0, b.remaining), 0);
|
||
|
||
return {
|
||
bonuses,
|
||
totalRemaining,
|
||
bonusPerCredential,
|
||
validityDays: bonusValidityDays
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取所有用户的 API Key 列表
|
||
* @returns {string[]}
|
||
*/
|
||
export function getAllUserApiKeys() {
|
||
ensureLoaded();
|
||
return Object.keys(userDataStore.users);
|
||
}
|
||
|
||
export { USER_DATA_FILE };
|