Add per-profile AI comment action limits

This commit is contained in:
2026-04-07 15:34:18 +02:00
parent 81dfb06f24
commit 66221f27c7
3 changed files with 329 additions and 7 deletions

View File

@@ -11,6 +11,8 @@ const app = express();
const PORT = process.env.PORT || 3000;
const MAX_PROFILES = 5;
const AI_AUTO_COMMENT_SOURCE = 'extension-ai-button';
const AI_AUTO_COMMENT_DAILY_LIMIT_MAX = 500;
const DEFAULT_PROFILE_NAMES = {
1: 'Profil 1',
2: 'Profil 2',
@@ -809,6 +811,167 @@ function getProfileName(profileNumber) {
return DEFAULT_PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`;
}
function normalizeAIAutoCommentDailyLimit(value) {
if (value === null || typeof value === 'undefined' || value === '') {
return 0;
}
const parsed = parseInt(value, 10);
if (Number.isNaN(parsed) || parsed < 0) {
return 0;
}
return Math.min(AI_AUTO_COMMENT_DAILY_LIMIT_MAX, parsed);
}
function getLocalDateKey(date = new Date()) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function getNextLocalMidnightIso(date = new Date()) {
const nextMidnight = new Date(date);
nextMidnight.setHours(24, 0, 0, 0);
return nextMidnight.toISOString();
}
function buildAIAutoCommentLimitPayload(profileNumber, dailyLimit, usedToday, date = new Date()) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (!normalizedProfileNumber) {
return null;
}
const normalizedDailyLimit = normalizeAIAutoCommentDailyLimit(dailyLimit);
const normalizedUsedToday = Math.max(0, parseInt(usedToday, 10) || 0);
return {
profile_number: normalizedProfileNumber,
profile_name: getProfileName(normalizedProfileNumber),
daily_limit: normalizedDailyLimit,
used_today: normalizedUsedToday,
remaining_today: normalizedDailyLimit > 0
? Math.max(0, normalizedDailyLimit - normalizedUsedToday)
: null,
blocked: normalizedDailyLimit > 0 && normalizedUsedToday >= normalizedDailyLimit,
resets_at: getNextLocalMidnightIso(date)
};
}
function listAIAutoCommentProfileLimits() {
const todayKey = getLocalDateKey();
const limitRows = db.prepare(`
SELECT profile_number, daily_limit
FROM ai_profile_auto_comment_limits
`).all();
const usageRows = db.prepare(`
SELECT profile_number, used_count
FROM ai_profile_auto_comment_usage
WHERE usage_date = ?
`).all(todayKey);
const dailyLimitByProfile = new Map();
limitRows.forEach((row) => {
const profileNumber = sanitizeProfileNumber(row.profile_number);
if (profileNumber) {
dailyLimitByProfile.set(profileNumber, normalizeAIAutoCommentDailyLimit(row.daily_limit));
}
});
const usageByProfile = new Map();
usageRows.forEach((row) => {
const profileNumber = sanitizeProfileNumber(row.profile_number);
if (profileNumber) {
usageByProfile.set(profileNumber, Math.max(0, parseInt(row.used_count, 10) || 0));
}
});
return Array.from({ length: MAX_PROFILES }, (_unused, index) => {
const profileNumber = index + 1;
return buildAIAutoCommentLimitPayload(
profileNumber,
dailyLimitByProfile.get(profileNumber) || 0,
usageByProfile.get(profileNumber) || 0
);
});
}
function getAIAutoCommentProfileLimit(profileNumber) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (!normalizedProfileNumber) {
return null;
}
const todayKey = getLocalDateKey();
const limitRow = db.prepare(`
SELECT daily_limit
FROM ai_profile_auto_comment_limits
WHERE profile_number = ?
`).get(normalizedProfileNumber);
const usageRow = db.prepare(`
SELECT used_count
FROM ai_profile_auto_comment_usage
WHERE profile_number = ?
AND usage_date = ?
`).get(normalizedProfileNumber, todayKey);
return buildAIAutoCommentLimitPayload(
normalizedProfileNumber,
limitRow ? limitRow.daily_limit : 0,
usageRow ? usageRow.used_count : 0
);
}
function saveAIAutoCommentProfileLimits(profileLimits) {
const rows = Array.isArray(profileLimits) ? profileLimits : [];
const normalizedByProfile = new Map();
rows.forEach((entry) => {
const profileNumber = sanitizeProfileNumber(entry && entry.profile_number);
if (!profileNumber) {
return;
}
normalizedByProfile.set(profileNumber, normalizeAIAutoCommentDailyLimit(entry.daily_limit));
});
const upsertLimit = db.prepare(`
INSERT INTO ai_profile_auto_comment_limits (profile_number, daily_limit, updated_at)
VALUES (@profile_number, @daily_limit, CURRENT_TIMESTAMP)
ON CONFLICT(profile_number) DO UPDATE SET
daily_limit = excluded.daily_limit,
updated_at = CURRENT_TIMESTAMP
`);
const persist = db.transaction(() => {
for (let profileNumber = 1; profileNumber <= MAX_PROFILES; profileNumber += 1) {
upsertLimit.run({
profile_number: profileNumber,
daily_limit: normalizedByProfile.has(profileNumber)
? normalizedByProfile.get(profileNumber)
: 0
});
}
});
persist();
return listAIAutoCommentProfileLimits();
}
function incrementAIAutoCommentProfileUsage(profileNumber) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (!normalizedProfileNumber) {
return null;
}
const todayKey = getLocalDateKey();
db.prepare(`
INSERT INTO ai_profile_auto_comment_usage (profile_number, usage_date, used_count, updated_at)
VALUES (?, ?, 1, CURRENT_TIMESTAMP)
ON CONFLICT(profile_number, usage_date) DO UPDATE SET
used_count = ai_profile_auto_comment_usage.used_count + 1,
updated_at = CURRENT_TIMESTAMP
`).run(normalizedProfileNumber, todayKey);
return getAIAutoCommentProfileLimit(normalizedProfileNumber);
}
function normalizeCreatorName(value) {
if (typeof value !== 'string') {
return null;
@@ -1580,6 +1743,24 @@ db.exec(`
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS ai_profile_auto_comment_limits (
profile_number INTEGER PRIMARY KEY,
daily_limit INTEGER NOT NULL DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS ai_profile_auto_comment_usage (
profile_number INTEGER NOT NULL,
usage_date TEXT NOT NULL,
used_count INTEGER NOT NULL DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (profile_number, usage_date)
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS search_seen_posts (
url TEXT PRIMARY KEY,
@@ -2049,6 +2230,8 @@ ensureColumn('posts', 'is_successful', 'is_successful INTEGER DEFAULT 0');
ensureColumn('ai_settings', 'active_credential_id', 'active_credential_id INTEGER');
ensureColumn('ai_settings', 'prompt_prefix', 'prompt_prefix TEXT');
ensureColumn('ai_settings', 'enabled', 'enabled INTEGER DEFAULT 0');
ensureColumn('ai_profile_auto_comment_limits', 'daily_limit', 'daily_limit INTEGER NOT NULL DEFAULT 0');
ensureColumn('ai_profile_auto_comment_usage', 'used_count', 'used_count INTEGER NOT NULL DEFAULT 0');
ensureColumn('ai_credentials', 'is_active', 'is_active INTEGER DEFAULT 1');
ensureColumn('ai_credentials', 'priority', 'priority INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'base_url', 'base_url TEXT');
@@ -6676,7 +6859,8 @@ app.get('/api/ai-settings', (req, res) => {
res.json({
...settings,
active_credential: activeCredential
active_credential: activeCredential,
profile_limits: listAIAutoCommentProfileLimits()
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -6685,7 +6869,7 @@ app.get('/api/ai-settings', (req, res) => {
app.put('/api/ai-settings', (req, res) => {
try {
const { active_credential_id, prompt_prefix, enabled } = req.body;
const { active_credential_id, prompt_prefix, enabled, profile_limits } = req.body;
const existing = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();
@@ -6702,6 +6886,10 @@ app.put('/api/ai-settings', (req, res) => {
`).run(active_credential_id || null, prompt_prefix, enabled ? 1 : 0);
}
const savedProfileLimits = Array.isArray(profile_limits)
? saveAIAutoCommentProfileLimits(profile_limits)
: listAIAutoCommentProfileLimits();
const updated = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();
let activeCredential = null;
@@ -6711,7 +6899,8 @@ app.put('/api/ai-settings', (req, res) => {
res.json({
...updated,
active_credential: activeCredential
active_credential: activeCredential,
profile_limits: savedProfileLimits
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -7317,6 +7506,8 @@ app.post('/api/ai/generate-comment', async (req, res) => {
try {
const { postText, profileNumber, preferredCredentialId } = requestBody;
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
let profileLimitInfo = null;
if (!postText) {
return respondWithTrackedError(400, 'postText is required');
@@ -7348,6 +7539,29 @@ app.post('/api/ai/generate-comment', async (req, res) => {
return respondWithTrackedError(400, 'No active AI credentials available');
}
if (traceSource === AI_AUTO_COMMENT_SOURCE) {
if (!normalizedProfileNumber) {
return respondWithTrackedError(400, 'Für die AI-Kommentar-Aktion ist ein gültiges Profil erforderlich');
}
const limitCheckStartedMs = timingStart();
const currentProfileLimit = getAIAutoCommentProfileLimit(normalizedProfileNumber);
if (currentProfileLimit && currentProfileLimit.blocked) {
timingEnd('profileLimitCheckMs', limitCheckStartedMs);
return respondWithTrackedError(
429,
`Tageslimit für ${currentProfileLimit.profile_name} erreicht (${currentProfileLimit.used_today}/${currentProfileLimit.daily_limit}). Die Aktion ist bis morgen gesperrt.`,
{
responseMeta: {
profileLimit: currentProfileLimit
}
}
);
}
profileLimitInfo = incrementAIAutoCommentProfileUsage(normalizedProfileNumber);
timingEnd('profileLimitCheckMs', limitCheckStartedMs);
}
let orderedCredentials = credentials;
if (typeof preferredCredentialId !== 'undefined' && preferredCredentialId !== null) {
const parsedPreferredId = Number(preferredCredentialId);
@@ -7362,7 +7576,6 @@ app.post('/api/ai/generate-comment', async (req, res) => {
const promptBuildStartedMs = timingStart();
let promptPrefix = settings.prompt_prefix || '';
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (normalizedProfileNumber) {
const friends = db.prepare('SELECT friend_names FROM profile_friends WHERE profile_number = ?').get(normalizedProfileNumber);
@@ -7433,7 +7646,8 @@ app.post('/api/ai/generate-comment', async (req, res) => {
responseMeta: {
usedCredential: credential.name,
usedCredentialId: credential.id,
attempts: attemptDetails
attempts: attemptDetails,
profileLimit: profileLimitInfo
},
totalDurationMs: backendTimings.totalMs
});
@@ -7447,6 +7661,7 @@ app.post('/api/ai/generate-comment', async (req, res) => {
usedCredentialId: credential.id,
attempts: attemptDetails,
rateLimitInfo: rateInfo || null,
profileLimitInfo,
traceId,
flowId,
timings: {