feat: zentrale selfhost-dienste bereitstellen

This commit is contained in:
2026-08-02 16:34:10 +02:00
parent d3ad53bcf0
commit 8b8e0c97d3
30 changed files with 1818 additions and 74 deletions

View File

@@ -7,6 +7,7 @@ import { FastifyInstance } from "fastify";
import { storeExtractedTextForFile } from "./documentText";
import { loadFileBuffer } from "./fileBuffer";
import { secrets } from "./secrets";
import { centralServicesClient } from "../modules/push-server.client";
// Drizzle schema
import { vendors, accounts, tenants } from "../../db/schema";
@@ -21,6 +22,7 @@ const nullableNumber = z.number().nullable();
// INITIALIZE OPENAI
// ---------------------------------------------------------
export const initOpenAi = async () => {
if (secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured()) return;
openai = new OpenAI({
apiKey: secrets.OPENAI_API_KEY,
});
@@ -84,8 +86,9 @@ export const getInvoiceDataFromGPT = async function (
suppliedFileData?: Buffer,
) {
await initOpenAi();
const useCentralAi = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured());
if (!openai) {
if (!useCentralAi && !openai) {
throw new Error("OpenAI not initialized. Call initOpenAi() first.");
}
@@ -164,7 +167,7 @@ export const getInvoiceDataFromGPT = async function (
const completion = await openai.chat.completions.parse({
const completionRequest: any = {
model: "gpt-4o",
store: true,
response_format: zodResponseFormat(InstructionFormat as any, "instruction"),
@@ -190,9 +193,13 @@ export const getInvoiceDataFromGPT = async function (
"Keep invoice items in original order.\n",
},
],
});
const parsed = completion.choices[0].message.parsed;
};
const completion = useCentralAi
? await centralServicesClient.aiChatCompletions(completionRequest)
: await openai!.chat.completions.parse(completionRequest);
const parsed = useCentralAi
? InstructionFormat.parse(JSON.parse(completion.choices[0]?.message?.content || "{}"))
: completion.choices[0].message.parsed;
console.log(`🧾 Extracted invoice data for file ${file.id}`);

View File

@@ -5,6 +5,7 @@ import { zodResponseFormat } from "openai/helpers/zod";
import { and, desc, eq, gte } from "drizzle-orm";
import { FastifyInstance } from "fastify";
import { createHash } from "node:crypto";
import { centralServicesClient } from "../modules/push-server.client";
import {
bankaccounts,
@@ -382,9 +383,10 @@ const detectRecurringHeuristically = (statements: any[]): RecurringCandidate[] =
};
const detectRecurringWithAi = async (server: FastifyInstance, statements: any[]): Promise<RecurringCandidate[]> => {
if (!secrets.OPENAI_API_KEY || statements.length < 6) return [];
const useCentralAi = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured());
if ((!secrets.OPENAI_API_KEY && !useCentralAi) || statements.length < 6) return [];
const openai = new OpenAI({ apiKey: secrets.OPENAI_API_KEY });
const openai = useCentralAi ? null : new OpenAI({ apiKey: secrets.OPENAI_API_KEY });
const compactStatements = statements.slice(0, 220).map((statement) => ({
date: statement.valueDate || statement.date,
amount: roundMoney(Number(statement.amount || 0)),
@@ -393,7 +395,7 @@ const detectRecurringWithAi = async (server: FastifyInstance, statements: any[])
}));
try {
const completion = await openai.chat.completions.parse({
const completionRequest: any = {
model: "gpt-4o",
store: true,
response_format: zodResponseFormat(AiRecurringFormat as any, "liquidity_recurring_transactions"),
@@ -416,9 +418,15 @@ const detectRecurringWithAi = async (server: FastifyInstance, statements: any[])
}),
},
],
});
};
const completion = useCentralAi
? await centralServicesClient.aiChatCompletions(completionRequest)
: await openai!.chat.completions.parse(completionRequest);
const parsed = useCentralAi
? AiRecurringFormat.parse(JSON.parse(completion.choices[0]?.message?.content || "{}"))
: completion.choices[0].message.parsed;
return (completion.choices[0].message.parsed?.candidates || [])
return (parsed?.candidates || [])
.filter((candidate) => dayjs(candidate.nextDate).isValid())
.filter((candidate) => Number(candidate.amount || 0) < 0)
.map((candidate) => ({
@@ -832,7 +840,7 @@ export const generateLiquidityForecast = async (
},
points,
ai: {
enabled: Boolean(secrets.OPENAI_API_KEY),
enabled: Boolean(secrets.OPENAI_API_KEY || (secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured())),
candidates: aiRecurring.length,
},
};

View File

@@ -53,6 +53,7 @@ export let secrets = {
PUSH_SERVER_URL?: string
PUSH_SERVER_INSTANCE_ID?: string
PUSH_SERVER_SECRET?: string
FEDEO_CENTRAL_SERVICES_ENABLED?: boolean
}
const secretKeys = [
@@ -100,10 +101,11 @@ const secretKeys = [
"PUSH_SERVER_URL",
"PUSH_SERVER_INSTANCE_ID",
"PUSH_SERVER_SECRET",
"FEDEO_CENTRAL_SERVICES_ENABLED",
] as const
const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT"])
const booleanKeys = new Set(["DOKUBOX_IMAP_SECURE"])
const booleanKeys = new Set(["DOKUBOX_IMAP_SECURE", "FEDEO_CENTRAL_SERVICES_ENABLED"])
function normalizeEnvValue(key: string, value: string) {
if (numberKeys.has(key)) return Number(value)