diff --git a/src/config.ts b/src/config.ts index 801f7d1..702905e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,7 +13,8 @@ const envSchema = z.object({ // Payload CMS PAYLOAD_API_URL: z.string().url().default('http://localhost:3001/api'), - PAYLOAD_API_KEY: z.string().optional(), + PAYLOAD_BOT_EMAIL: z.string().email().optional(), + PAYLOAD_BOT_PASSWORD: z.string().optional(), // PostgreSQL (optional — direct DB access for high-throughput mode) DATABASE_URL: z.string().optional(), diff --git a/src/llm/ClaudeProvider.ts b/src/llm/ClaudeProvider.ts index 35bbe02..0340aa7 100644 --- a/src/llm/ClaudeProvider.ts +++ b/src/llm/ClaudeProvider.ts @@ -40,7 +40,7 @@ export class ClaudeProvider implements LLMProvider { private client: Anthropic private model: string - constructor(apiKey: string, model = 'claude-3-5-haiku-20241022') { + constructor(apiKey: string, model = 'claude-haiku-4-5-20251001') { this.client = new Anthropic({ apiKey }) this.model = model } diff --git a/src/payload/PayloadClient.ts b/src/payload/PayloadClient.ts index 43df5c3..20db3ee 100644 --- a/src/payload/PayloadClient.ts +++ b/src/payload/PayloadClient.ts @@ -5,12 +5,54 @@ const log = getLogger('payload-client') export class PayloadClient { private baseUrl: string - private apiKey?: string + private token: string | null = null + private tokenExpiresAt = 0 + private email?: string + private password?: string constructor() { const config = getConfig() this.baseUrl = config.PAYLOAD_API_URL - this.apiKey = config.PAYLOAD_API_KEY + this.email = config.PAYLOAD_BOT_EMAIL + this.password = config.PAYLOAD_BOT_PASSWORD + } + + private get canAuth(): boolean { + return Boolean(this.email && this.password) + } + + async login(): Promise { + if (!this.canAuth) return false + + try { + const response = await fetch(`${this.baseUrl}/users/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: this.email, password: this.password }), + }) + + if (!response.ok) { + log.error({ status: response.status }, 'Payload login failed') + return false + } + + const data = (await response.json()) as { token: string; exp: number } + this.token = data.token + // Token expires in 2h, refresh 10 min early + this.tokenExpiresAt = Date.now() + 110 * 60 * 1000 + log.info('Payload API authenticated') + return true + } catch (err) { + log.error({ error: (err as Error).message }, 'Payload login error') + return false + } + } + + private async ensureAuth(): Promise { + if (!this.canAuth) return + if (!this.token || Date.now() > this.tokenExpiresAt) { + await this.login() + } } async find( @@ -25,8 +67,7 @@ export class PayloadClient { } const url = `${this.baseUrl}/${collection}?${params.toString()}` - const response = await this.request<{ docs: T[]; totalDocs: number }>(url) - return response + return this.request<{ docs: T[]; totalDocs: number }>(url) } async findByID(collection: string, id: number | string): Promise { @@ -48,7 +89,6 @@ export class PayloadClient { async isReachable(): Promise { try { const response = await fetch(this.baseUrl, { method: 'GET' }) - // Any HTTP response (even 401/403) means the API is alive return response.status < 500 } catch { return false @@ -56,11 +96,13 @@ export class PayloadClient { } private async request(url: string, init?: RequestInit): Promise { + await this.ensureAuth() + const headers: Record = { 'Content-Type': 'application/json', } - if (this.apiKey) { - headers['Authorization'] = `Bearer ${this.apiKey}` + if (this.token) { + headers['Authorization'] = `JWT ${this.token}` } const response = await fetch(url, { diff --git a/src/payload/RulesLoader.ts b/src/payload/RulesLoader.ts index 45a16a5..a30b194 100644 --- a/src/payload/RulesLoader.ts +++ b/src/payload/RulesLoader.ts @@ -1,6 +1,5 @@ import { getLogger } from '../lib/logger.js' import type { PayloadClient } from './PayloadClient.js' -import { getConfig } from '../config.js' const log = getLogger('rules-loader') @@ -13,7 +12,6 @@ interface CommunityRule { action: string replyTemplate?: string | { content: string } isActive: boolean - tenant?: number | { id: number } } export interface MatchedRule { @@ -45,7 +43,6 @@ export class RulesLoader { for (const rule of rules) { if (!rule.isActive) continue - // Check platform match (whatsapp or all) const platformName = typeof rule.platform === 'string' ? rule.platform @@ -57,7 +54,6 @@ export class RulesLoader { continue } - // Check trigger let matches = false switch (rule.triggerType) { @@ -100,11 +96,9 @@ export class RulesLoader { } try { - const tenantId = getConfig().CCS_TENANT_ID const result = await this.payloadClient.find( 'community-rules', { - 'where[tenant][equals]': tenantId, 'where[isActive][equals]': 'true', limit: '100', }, diff --git a/src/payload/TemplateResolver.ts b/src/payload/TemplateResolver.ts index 93c761a..6d1b37c 100644 --- a/src/payload/TemplateResolver.ts +++ b/src/payload/TemplateResolver.ts @@ -1,6 +1,5 @@ import { getLogger } from '../lib/logger.js' import type { PayloadClient } from './PayloadClient.js' -import { getConfig } from '../config.js' import { DEFAULT_HEALTHCARE_PROMPT } from '../llm/HealthcarePrompt.js' const log = getLogger('template-resolver') @@ -9,8 +8,7 @@ interface CommunityTemplate { id: number name: string category: string - content: string - tenant?: number | { id: number } + template: string } const TEMPLATE_CACHE_TTL = 600_000 // 10 minutes @@ -28,18 +26,18 @@ export class TemplateResolver { } try { - const tenantId = getConfig().CCS_TENANT_ID const result = await this.payloadClient.find( 'community-templates', { 'where[category][equals]': 'whatsapp_system_prompt', - 'where[tenant][equals]': tenantId, + 'where[isActive][equals]': 'true', limit: '1', + locale: 'de', }, ) - if (result.docs.length > 0) { - this.systemPromptCache = result.docs[0].content + if (result.docs.length > 0 && result.docs[0].template) { + this.systemPromptCache = result.docs[0].template this.lastFetch = now log.info('System prompt loaded from CMS') return this.systemPromptCache @@ -51,7 +49,6 @@ export class TemplateResolver { ) } - // Fallback to hardcoded prompt log.info('Using default healthcare prompt') return DEFAULT_HEALTHCARE_PROMPT } @@ -61,11 +58,11 @@ export class TemplateResolver { name?: string, ): Promise { try { - const tenantId = getConfig().CCS_TENANT_ID const query: Record = { 'where[category][equals]': category, - 'where[tenant][equals]': tenantId, + 'where[isActive][equals]': 'true', limit: '1', + locale: 'de', } if (name) { query['where[name][equals]'] = name @@ -77,7 +74,7 @@ export class TemplateResolver { query, ) - return result.docs[0]?.content ?? null + return result.docs[0]?.template ?? null } catch (err) { log.error( { category, name, error: (err as Error).message },