Add per-profile AI comment action limits
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -1145,6 +1145,14 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tageslimit pro Profil für den AI-Kommentar-Button</label>
|
||||
<p class="form-help">
|
||||
Gilt nur für die Aktion <code>AI - generiere automatisch einen passenden Kommentar</code> im Tracker. <code>0</code> bedeutet kein Limit.
|
||||
</p>
|
||||
<div id="aiProfileCommentLimits"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="testBtn">
|
||||
🧪 Kommentar testen
|
||||
|
||||
103
web/settings.js
103
web/settings.js
@@ -41,6 +41,7 @@ const PROVIDER_INFO = {
|
||||
apiKeyHelp: 'Für lokale OpenAI-kompatible Server (z.B. Ollama) kannst du den Schlüssel leer lassen.'
|
||||
}
|
||||
};
|
||||
const AI_AUTO_COMMENT_DAILY_LIMIT_MAX = 500;
|
||||
|
||||
let credentials = [];
|
||||
let currentSettings = null;
|
||||
@@ -173,6 +174,7 @@ async function loadSettings() {
|
||||
document.getElementById('activeCredential').value = currentSettings.active_credential_id || '';
|
||||
document.getElementById('aiPromptPrefix').value = currentSettings.prompt_prefix ||
|
||||
'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n';
|
||||
renderAIProfileLimitInputs();
|
||||
}
|
||||
|
||||
async function loadHiddenSettings() {
|
||||
@@ -425,6 +427,94 @@ function normalizeSimilarityImageThresholdInput(value) {
|
||||
return Math.min(64, Math.max(0, parsed));
|
||||
}
|
||||
|
||||
function normalizeAIProfileDailyLimitInput(value) {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(AI_AUTO_COMMENT_DAILY_LIMIT_MAX, parsed);
|
||||
}
|
||||
|
||||
function getDefaultAIProfileLimitEntry(profileNumber) {
|
||||
return {
|
||||
profile_number: profileNumber,
|
||||
profile_name: `Profil ${profileNumber}`,
|
||||
daily_limit: 0,
|
||||
used_today: 0,
|
||||
remaining_today: null
|
||||
};
|
||||
}
|
||||
|
||||
function getAIProfileLimitEntries() {
|
||||
const rows = Array.isArray(currentSettings?.profile_limits)
|
||||
? currentSettings.profile_limits
|
||||
: [];
|
||||
const byProfile = new Map();
|
||||
rows.forEach((row) => {
|
||||
const profileNumber = parseInt(row?.profile_number, 10);
|
||||
if (!Number.isNaN(profileNumber) && profileNumber >= 1 && profileNumber <= 5) {
|
||||
byProfile.set(profileNumber, {
|
||||
...getDefaultAIProfileLimitEntry(profileNumber),
|
||||
...row,
|
||||
daily_limit: normalizeAIProfileDailyLimitInput(row?.daily_limit),
|
||||
used_today: Math.max(0, parseInt(row?.used_today, 10) || 0)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from({ length: 5 }, (_unused, index) => {
|
||||
const profileNumber = index + 1;
|
||||
return byProfile.get(profileNumber) || getDefaultAIProfileLimitEntry(profileNumber);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAIProfileLimitInputs() {
|
||||
const container = document.getElementById('aiProfileCommentLimits');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = getAIProfileLimitEntries();
|
||||
container.innerHTML = '';
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-group';
|
||||
wrapper.style.marginBottom = '12px';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'form-label';
|
||||
label.setAttribute('for', `aiProfileLimit${entry.profile_number}`);
|
||||
label.textContent = entry.profile_name || `Profil ${entry.profile_number}`;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.min = '0';
|
||||
input.max = String(AI_AUTO_COMMENT_DAILY_LIMIT_MAX);
|
||||
input.step = '1';
|
||||
input.id = `aiProfileLimit${entry.profile_number}`;
|
||||
input.className = 'form-input';
|
||||
input.value = String(normalizeAIProfileDailyLimitInput(entry.daily_limit));
|
||||
input.placeholder = '0 = kein Limit';
|
||||
input.addEventListener('blur', () => {
|
||||
input.value = String(normalizeAIProfileDailyLimitInput(input.value));
|
||||
});
|
||||
|
||||
const help = document.createElement('p');
|
||||
help.className = 'form-help';
|
||||
const dailyLimit = normalizeAIProfileDailyLimitInput(entry.daily_limit);
|
||||
const usedToday = Math.max(0, parseInt(entry.used_today, 10) || 0);
|
||||
help.textContent = dailyLimit > 0
|
||||
? `Heute verwendet: ${usedToday} / ${dailyLimit}`
|
||||
: `Heute verwendet: ${usedToday} · aktuell kein Limit aktiv`;
|
||||
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(input);
|
||||
wrapper.appendChild(help);
|
||||
container.appendChild(wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
function applySimilaritySettingsUI() {
|
||||
const textInput = document.getElementById('similarityTextThreshold');
|
||||
const imageInput = document.getElementById('similarityImageThreshold');
|
||||
@@ -998,7 +1088,15 @@ async function saveSettings(e, { silent = false } = {}) {
|
||||
const data = {
|
||||
enabled: document.getElementById('aiEnabled').checked,
|
||||
active_credential_id: parseInt(document.getElementById('activeCredential').value) || null,
|
||||
prompt_prefix: document.getElementById('aiPromptPrefix').value
|
||||
prompt_prefix: document.getElementById('aiPromptPrefix').value,
|
||||
profile_limits: Array.from({ length: 5 }, (_unused, index) => {
|
||||
const profileNumber = index + 1;
|
||||
const input = document.getElementById(`aiProfileLimit${profileNumber}`);
|
||||
return {
|
||||
profile_number: profileNumber,
|
||||
daily_limit: normalizeAIProfileDailyLimitInput(input ? input.value : 0)
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const res = await apiFetch(`${API_URL}/ai-settings`, {
|
||||
@@ -1013,6 +1111,7 @@ async function saveSettings(e, { silent = false } = {}) {
|
||||
}
|
||||
|
||||
currentSettings = await res.json();
|
||||
renderAIProfileLimitInputs();
|
||||
if (!silent) {
|
||||
showSuccess('✅ Einstellungen erfolgreich gespeichert');
|
||||
}
|
||||
@@ -1058,7 +1157,7 @@ async function generateTest() {
|
||||
const res = await apiFetch(`${API_URL}/ai/generate-comment`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({postText: text, profileNumber})
|
||||
body: JSON.stringify({postText: text, profileNumber, traceSource: 'settings-test'})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user