- Server-Neustart setzt hängengebliebene Jobs (running/queued) zurück (markStaleJobs) mit Hinweis-Nachricht; /abort räumt auch verwaiste Sessions auf - Löschen/Abbrechen/Modellwechsel ohne Owner-Beschränkung (konsistent zur offenen Sichtbarkeit im Ein-Nutzer-System) - Dashboard-HTML wird nicht mehr per Tool-Aufruf übertragen, sondern in der finalen Antwort als ```html```-Codeblock gestreamt (GLM scheiterte reproduzierbar am riesigen Tool-Call); Extraktion mit klaren Fehlern bei fehlendem/unvollständigem Codeblock - Reasoning-Effort pro Runde: volle Tiefe in Research-Runden, minimales Denken in der Dashboard-Liefer-Runde (DeepSeek: thinking aus, GLM: low) – Budget gehört dem HTML - GLM-Reasoning-Default high (max verbrennt das Completion-Budget), DeepSeek max_tokens auf 300k
129 lines
4.7 KiB
JavaScript
129 lines
4.7 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
const webpush = require('web-push');
|
||
|
||
const ENV_PATH = path.join(__dirname, '..', '.env');
|
||
|
||
function loadEnv() {
|
||
const env = {};
|
||
if (fs.existsSync(ENV_PATH)) {
|
||
for (const line of fs.readFileSync(ENV_PATH, 'utf8').split('\n')) {
|
||
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
||
if (m && !line.trim().startsWith('#')) env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
||
}
|
||
}
|
||
return env;
|
||
}
|
||
|
||
function saveEnv(env) {
|
||
const lines = [];
|
||
for (const [k, v] of Object.entries(env)) lines.push(`${k}=${v}`);
|
||
fs.writeFileSync(ENV_PATH, lines.join('\n') + '\n', 'utf8');
|
||
}
|
||
|
||
function ensure(key, generator) {
|
||
if (process.env[key] || env[key]) return process.env[key] || env[key];
|
||
const value = generator();
|
||
env[key] = value;
|
||
saveEnv(env);
|
||
return value;
|
||
}
|
||
|
||
const env = loadEnv();
|
||
|
||
function ensureVapidPair() {
|
||
if ((process.env.VAPID_PUBLIC_KEY || env.VAPID_PUBLIC_KEY) && (process.env.VAPID_PRIVATE_KEY || env.VAPID_PRIVATE_KEY)) {
|
||
return {
|
||
publicKey: process.env.VAPID_PUBLIC_KEY || env.VAPID_PUBLIC_KEY,
|
||
privateKey: process.env.VAPID_PRIVATE_KEY || env.VAPID_PRIVATE_KEY,
|
||
};
|
||
}
|
||
const pair = webpush.generateVAPIDKeys();
|
||
env.VAPID_PUBLIC_KEY = pair.publicKey;
|
||
env.VAPID_PRIVATE_KEY = pair.privateKey;
|
||
saveEnv(env);
|
||
return pair;
|
||
}
|
||
|
||
const vapid = ensureVapidPair();
|
||
|
||
const config = {
|
||
port: Number(process.env.PORT || env.PORT || 3000),
|
||
apiKey: process.env.DEEPSEEK_API_KEY || env.DEEPSEEK_API_KEY || '',
|
||
openrouterApiKey: process.env.OPENROUTER_API_KEY || env.OPENROUTER_API_KEY || '',
|
||
appPassword: process.env.APP_PASSWORD || env.APP_PASSWORD || '',
|
||
model: process.env.MODEL || env.MODEL || 'deepseek-v4-flash',
|
||
thinking: process.env.THINKING || env.THINKING || 'enabled',
|
||
maxTokens: Number(process.env.MAX_TOKENS || env.MAX_TOKENS || 64000),
|
||
baseUrl: process.env.DEEPSEEK_BASE_URL || env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',
|
||
loginMaxAttempts: Number(process.env.LOGIN_MAX_ATTEMPTS || env.LOGIN_MAX_ATTEMPTS || 5),
|
||
loginLockMinutes: Number(process.env.LOGIN_LOCK_MINUTES || env.LOGIN_LOCK_MINUTES || 5),
|
||
nominatimEmail: process.env.NOMINATIM_EMAIL || env.NOMINATIM_EMAIL || '',
|
||
osrmUrl: process.env.OSRM_URL || env.OSRM_URL || 'https://router.project-osrm.org',
|
||
sessionSecret: ensure('SESSION_SECRET', () => crypto.randomBytes(32).toString('hex')),
|
||
vapidPublicKey: vapid.publicKey,
|
||
vapidPrivateKey: vapid.privateKey,
|
||
};
|
||
|
||
const envOr = (key, def) => process.env[key] || env[key] || def;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Modell-Registry: DeepSeek direkt + OpenRouter-Modelle (z.B. GLM 5.3 Flash)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const MODELS = [
|
||
{
|
||
id: 'deepseek-v4-flash',
|
||
label: 'DeepSeek V4 Flash',
|
||
provider: 'deepseek',
|
||
vision: false,
|
||
// Max. Output laut Doku: 384K – DeepSeek hat 3x so viel Headroom wie GLM
|
||
maxTokens: 300000,
|
||
reasoningEffort: () => envOr('DEEPSEEK_REASONING_EFFORT', 'max'),
|
||
// Liefer-Runde des Dashboards: Denken komplett aus, Budget gehört dem HTML
|
||
deliveryEffort: 'none',
|
||
prices: { inputCacheHit: 0.014, inputCacheMiss: 0.44, output: 1.32 },
|
||
},
|
||
{
|
||
id: 'z-ai/glm-5.3-flash',
|
||
label: 'GLM 5.3 Flash (OpenRouter)',
|
||
provider: 'openrouter',
|
||
vision: true,
|
||
// Max. Completion laut OpenRouter: 131072
|
||
maxTokens: 131072,
|
||
// Research-Runden: 'high' (effort 'max' verbrennt das ganze Budget mit
|
||
// Denken; für lange Recherchen bleibt dann nichts fürs große HTML übrig)
|
||
reasoningEffort: () => envOr('GLM_REASONING_EFFORT', 'high'),
|
||
// Liefer-Runde des Dashboards: minimales Denken (GLM: Reasoning Pflicht)
|
||
deliveryEffort: 'low',
|
||
prices: { inputCacheHit: 0.015, inputCacheMiss: 0.075, output: 0.25 },
|
||
costNote: 'Schätzung (OpenRouter, Rabatt bis Sep 2026)',
|
||
},
|
||
];
|
||
|
||
/** Registry inkl. benutzerdefiniertem Modell aus der Env (MODEL), falls nicht bekannt. */
|
||
function getModels() {
|
||
if (MODELS.some((m) => m.id === config.model)) return MODELS;
|
||
return [
|
||
...MODELS,
|
||
{
|
||
id: config.model,
|
||
label: config.model,
|
||
provider: 'deepseek',
|
||
vision: false,
|
||
maxTokens: config.maxTokens,
|
||
reasoningEffort: () => envOr('DEEPSEEK_REASONING_EFFORT', 'max'),
|
||
prices: { inputCacheHit: 0.014, inputCacheMiss: 0.44, output: 1.32 },
|
||
},
|
||
];
|
||
}
|
||
|
||
/** Modell per ID auflösen (Fallback: Default-Modell aus Env). */
|
||
function getModel(id) {
|
||
const models = getModels();
|
||
return models.find((m) => m.id === id) || models.find((m) => m.id === config.model) || models[0];
|
||
}
|
||
|
||
module.exports = { config, MODELS, getModels, getModel };
|