1 Commits

Author SHA1 Message Date
abd0cb26f5 feat: IP-based daily account priority for save
- Added ip_daily_save_counts table to track per-IP daily usage

 - getAndValidateCredential now accepts ipAddress parameter

 - First 2 saves by same IP use primary account; 3rd+ round-robins to other accounts

 - Counter increments on each successful credential acquisition

 - All timestamps in Asia/Shanghai timezone
2026-05-15 06:19:04 +08:00
3 changed files with 97 additions and 9 deletions

View File

@@ -141,7 +141,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
}
// ── Unified credential validation ──
const credential = await getAndValidateCredential(cloudType);
const credential = await getAndValidateCredential(cloudType, ipAddress);
if (!credential.valid || !credential.config) {
return { success: false, message: credential.message };
}

View File

@@ -397,16 +397,79 @@ export interface CredentialValidationResult {
*
* Reference: search-ucmao get_and_validate_credential() pattern.
*/
export async function getAndValidateCredential(cloudType: string): Promise<CredentialValidationResult> {
export async function getAndValidateCredential(cloudType: string, ipAddress?: string): Promise<CredentialValidationResult> {
const db = getDb();
const config = db.prepare(
let config: CloudConfig | undefined;
if (!ipAddress) {
// No IP info — fallback to simple LUR
config = db.prepare(
`SELECT * FROM cloud_configs
WHERE cloud_type = ? AND is_active = 1
AND consecutive_failures < 5
ORDER BY last_used_at ASC NULLS FIRST
LIMIT 1`
).get(cloudType) as CloudConfig | undefined;
} else {
// Get today's date in Shanghai time
const today = (() => {
const now = new Date();
const shanghai = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
return shanghai.toISOString().slice(0, 10);
})();
// Count how many times this IP has saved today for this cloud type
const ipCountRow = db.prepare(
`SELECT COALESCE(SUM(save_count), 0) as total
FROM ip_daily_save_counts
WHERE ip_address = ? AND date = ? AND cloud_type = ?`
).get(ipAddress, today, cloudType) as { total: number };
const ipTodayCount = ipCountRow?.total || 0;
if (ipTodayCount < 3) {
// First 2 saves — use the primary account (least recently used, healthy)
config = db.prepare(
`SELECT * FROM cloud_configs
WHERE cloud_type = ? AND is_active = 1
AND consecutive_failures < 5
ORDER BY last_used_at ASC NULLS FIRST
LIMIT 1`
).get(cloudType) as CloudConfig | undefined;
} else {
// 3rd+ save — exclude accounts this IP has already used today,
// fall back to other available accounts round-robin
const usedConfigIds = db.prepare(
`SELECT DISTINCT config_id FROM ip_daily_save_counts
WHERE ip_address = ? AND date = ? AND cloud_type = ?
ORDER BY config_id`
).all(ipAddress, today, cloudType) as { config_id: number }[];
const usedIds = usedConfigIds.map(r => r.config_id);
const placeholders = usedIds.length > 0 ? usedIds.map(() => '?').join(',') : '-1';
config = db.prepare(
`SELECT * FROM cloud_configs
WHERE cloud_type = ? AND is_active = 1
AND consecutive_failures < 5
AND id NOT IN (${placeholders})
ORDER BY last_used_at ASC NULLS FIRST
LIMIT 1`
).get(cloudType, ...usedIds) as CloudConfig | undefined;
// If all accounts have been used by this IP, fall back to primary
if (!config) {
config = db.prepare(
`SELECT * FROM cloud_configs
WHERE cloud_type = ? AND is_active = 1
AND consecutive_failures < 5
ORDER BY last_used_at ASC NULLS FIRST
LIMIT 1`
).get(cloudType) as CloudConfig | undefined;
}
}
}
if (!config) {
return {
@@ -457,6 +520,21 @@ export async function getAndValidateCredential(cloudType: string): Promise<Crede
};
}
// Track IP daily usage count (if ipAddress provided)
if (ipAddress && config) {
const today = (() => {
const now = new Date();
const shanghai = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
return shanghai.toISOString().slice(0, 10);
})();
db.prepare(
`INSERT INTO ip_daily_save_counts (ip_address, date, cloud_type, config_id, save_count)
VALUES (?, ?, ?, ?, 1)
ON CONFLICT(ip_address, date, cloud_type, config_id)
DO UPDATE SET save_count = save_count + 1`
).run(ipAddress, today, cloudType, config.id);
}
return {
valid: true,
config: { ...config, cookie: decryptedCookie },

View File

@@ -119,6 +119,16 @@ function runMigrations(db: Database.Database): void {
source TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS ip_daily_save_counts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
date TEXT NOT NULL,
cloud_type TEXT NOT NULL,
config_id INTEGER NOT NULL,
save_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(ip_address, date, cloud_type, config_id)
);
`);
seedSystemConfigs(db);
migrateSaveRecords(db);