fix: update Claude model, add JWT auth, fix CMS integration

- Update Claude model from claude-3-5-haiku-20241022 to claude-haiku-4-5-20251001
- Replace static API key auth with JWT login (auto-refresh before 2h expiry)
- Fix TemplateResolver: use 'template' field (not 'content'), remove non-existent tenant filter
- Fix RulesLoader: remove non-existent tenant filter from community-rules queries
- Update config: PAYLOAD_BOT_EMAIL/PASSWORD replace PAYLOAD_API_KEY

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS WhatsApp Bot 2026-03-02 15:18:35 +00:00
parent 22eb3d3e5c
commit f098cca1bd
5 changed files with 60 additions and 26 deletions

View file

@ -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(),

View file

@ -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
}

View file

@ -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<boolean> {
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<void> {
if (!this.canAuth) return
if (!this.token || Date.now() > this.tokenExpiresAt) {
await this.login()
}
}
async find<T>(
@ -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<T>(collection: string, id: number | string): Promise<T> {
@ -48,7 +89,6 @@ export class PayloadClient {
async isReachable(): Promise<boolean> {
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<T>(url: string, init?: RequestInit): Promise<T> {
await this.ensureAuth()
const headers: Record<string, string> = {
'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, {

View file

@ -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<CommunityRule>(
'community-rules',
{
'where[tenant][equals]': tenantId,
'where[isActive][equals]': 'true',
limit: '100',
},

View file

@ -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<CommunityTemplate>(
'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<string | null> {
try {
const tenantId = getConfig().CCS_TENANT_ID
const query: Record<string, unknown> = {
'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 },