mirror of
https://github.com/complexcaresolutions/whatsapp-bot.git
synced 2026-03-17 17:24:06 +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 CMS
|
||||||
PAYLOAD_API_URL: z.string().url().default('http://localhost:3001/api'),
|
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)
|
// PostgreSQL (optional — direct DB access for high-throughput mode)
|
||||||
DATABASE_URL: z.string().optional(),
|
DATABASE_URL: z.string().optional(),
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export class ClaudeProvider implements LLMProvider {
|
||||||
private client: Anthropic
|
private client: Anthropic
|
||||||
private model: string
|
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.client = new Anthropic({ apiKey })
|
||||||
this.model = model
|
this.model = model
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,54 @@ const log = getLogger('payload-client')
|
||||||
|
|
||||||
export class PayloadClient {
|
export class PayloadClient {
|
||||||
private baseUrl: string
|
private baseUrl: string
|
||||||
private apiKey?: string
|
private token: string | null = null
|
||||||
|
private tokenExpiresAt = 0
|
||||||
|
private email?: string
|
||||||
|
private password?: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
this.baseUrl = config.PAYLOAD_API_URL
|
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>(
|
async find<T>(
|
||||||
|
|
@ -25,8 +67,7 @@ export class PayloadClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this.baseUrl}/${collection}?${params.toString()}`
|
const url = `${this.baseUrl}/${collection}?${params.toString()}`
|
||||||
const response = await this.request<{ docs: T[]; totalDocs: number }>(url)
|
return this.request<{ docs: T[]; totalDocs: number }>(url)
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByID<T>(collection: string, id: number | string): Promise<T> {
|
async findByID<T>(collection: string, id: number | string): Promise<T> {
|
||||||
|
|
@ -48,7 +89,6 @@ export class PayloadClient {
|
||||||
async isReachable(): Promise<boolean> {
|
async isReachable(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.baseUrl, { method: 'GET' })
|
const response = await fetch(this.baseUrl, { method: 'GET' })
|
||||||
// Any HTTP response (even 401/403) means the API is alive
|
|
||||||
return response.status < 500
|
return response.status < 500
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
|
@ -56,11 +96,13 @@ export class PayloadClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(url: string, init?: RequestInit): Promise<T> {
|
private async request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
|
await this.ensureAuth()
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
if (this.apiKey) {
|
if (this.token) {
|
||||||
headers['Authorization'] = `Bearer ${this.apiKey}`
|
headers['Authorization'] = `JWT ${this.token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getLogger } from '../lib/logger.js'
|
import { getLogger } from '../lib/logger.js'
|
||||||
import type { PayloadClient } from './PayloadClient.js'
|
import type { PayloadClient } from './PayloadClient.js'
|
||||||
import { getConfig } from '../config.js'
|
|
||||||
|
|
||||||
const log = getLogger('rules-loader')
|
const log = getLogger('rules-loader')
|
||||||
|
|
||||||
|
|
@ -13,7 +12,6 @@ interface CommunityRule {
|
||||||
action: string
|
action: string
|
||||||
replyTemplate?: string | { content: string }
|
replyTemplate?: string | { content: string }
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
tenant?: number | { id: number }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchedRule {
|
export interface MatchedRule {
|
||||||
|
|
@ -45,7 +43,6 @@ export class RulesLoader {
|
||||||
for (const rule of rules) {
|
for (const rule of rules) {
|
||||||
if (!rule.isActive) continue
|
if (!rule.isActive) continue
|
||||||
|
|
||||||
// Check platform match (whatsapp or all)
|
|
||||||
const platformName =
|
const platformName =
|
||||||
typeof rule.platform === 'string'
|
typeof rule.platform === 'string'
|
||||||
? rule.platform
|
? rule.platform
|
||||||
|
|
@ -57,7 +54,6 @@ export class RulesLoader {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check trigger
|
|
||||||
let matches = false
|
let matches = false
|
||||||
|
|
||||||
switch (rule.triggerType) {
|
switch (rule.triggerType) {
|
||||||
|
|
@ -100,11 +96,9 @@ export class RulesLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tenantId = getConfig().CCS_TENANT_ID
|
|
||||||
const result = await this.payloadClient.find<CommunityRule>(
|
const result = await this.payloadClient.find<CommunityRule>(
|
||||||
'community-rules',
|
'community-rules',
|
||||||
{
|
{
|
||||||
'where[tenant][equals]': tenantId,
|
|
||||||
'where[isActive][equals]': 'true',
|
'where[isActive][equals]': 'true',
|
||||||
limit: '100',
|
limit: '100',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getLogger } from '../lib/logger.js'
|
import { getLogger } from '../lib/logger.js'
|
||||||
import type { PayloadClient } from './PayloadClient.js'
|
import type { PayloadClient } from './PayloadClient.js'
|
||||||
import { getConfig } from '../config.js'
|
|
||||||
import { DEFAULT_HEALTHCARE_PROMPT } from '../llm/HealthcarePrompt.js'
|
import { DEFAULT_HEALTHCARE_PROMPT } from '../llm/HealthcarePrompt.js'
|
||||||
|
|
||||||
const log = getLogger('template-resolver')
|
const log = getLogger('template-resolver')
|
||||||
|
|
@ -9,8 +8,7 @@ interface CommunityTemplate {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
category: string
|
category: string
|
||||||
content: string
|
template: string
|
||||||
tenant?: number | { id: number }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEMPLATE_CACHE_TTL = 600_000 // 10 minutes
|
const TEMPLATE_CACHE_TTL = 600_000 // 10 minutes
|
||||||
|
|
@ -28,18 +26,18 @@ export class TemplateResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tenantId = getConfig().CCS_TENANT_ID
|
|
||||||
const result = await this.payloadClient.find<CommunityTemplate>(
|
const result = await this.payloadClient.find<CommunityTemplate>(
|
||||||
'community-templates',
|
'community-templates',
|
||||||
{
|
{
|
||||||
'where[category][equals]': 'whatsapp_system_prompt',
|
'where[category][equals]': 'whatsapp_system_prompt',
|
||||||
'where[tenant][equals]': tenantId,
|
'where[isActive][equals]': 'true',
|
||||||
limit: '1',
|
limit: '1',
|
||||||
|
locale: 'de',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (result.docs.length > 0) {
|
if (result.docs.length > 0 && result.docs[0].template) {
|
||||||
this.systemPromptCache = result.docs[0].content
|
this.systemPromptCache = result.docs[0].template
|
||||||
this.lastFetch = now
|
this.lastFetch = now
|
||||||
log.info('System prompt loaded from CMS')
|
log.info('System prompt loaded from CMS')
|
||||||
return this.systemPromptCache
|
return this.systemPromptCache
|
||||||
|
|
@ -51,7 +49,6 @@ export class TemplateResolver {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to hardcoded prompt
|
|
||||||
log.info('Using default healthcare prompt')
|
log.info('Using default healthcare prompt')
|
||||||
return DEFAULT_HEALTHCARE_PROMPT
|
return DEFAULT_HEALTHCARE_PROMPT
|
||||||
}
|
}
|
||||||
|
|
@ -61,11 +58,11 @@ export class TemplateResolver {
|
||||||
name?: string,
|
name?: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const tenantId = getConfig().CCS_TENANT_ID
|
|
||||||
const query: Record<string, unknown> = {
|
const query: Record<string, unknown> = {
|
||||||
'where[category][equals]': category,
|
'where[category][equals]': category,
|
||||||
'where[tenant][equals]': tenantId,
|
'where[isActive][equals]': 'true',
|
||||||
limit: '1',
|
limit: '1',
|
||||||
|
locale: 'de',
|
||||||
}
|
}
|
||||||
if (name) {
|
if (name) {
|
||||||
query['where[name][equals]'] = name
|
query['where[name][equals]'] = name
|
||||||
|
|
@ -77,7 +74,7 @@ export class TemplateResolver {
|
||||||
query,
|
query,
|
||||||
)
|
)
|
||||||
|
|
||||||
return result.docs[0]?.content ?? null
|
return result.docs[0]?.template ?? null
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(
|
log.error(
|
||||||
{ category, name, error: (err as Error).message },
|
{ category, name, error: (err as Error).message },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue