mirror of
https://github.com/complexcaresolutions/whatsapp-bot.git
synced 2026-03-17 18:34:07 +00:00
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:
parent
22eb3d3e5c
commit
f098cca1bd
5 changed files with 60 additions and 26 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Reference in a new issue