- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { requireRole, toErrorResponse } from '@/lib/auth/account'
|
|
import { checkRateLimit, rateLimitResponse, RATE_LIMITS } from '@/lib/rate-limit'
|
|
import { loadEmbeddingsKey } from '@/lib/ai/config'
|
|
import { ingestDocument } from '@/lib/ai/knowledge'
|
|
import { AiError } from '@/lib/ai/types'
|
|
|
|
/**
|
|
* POST /api/ai/knowledge/reindex (admin+)
|
|
*
|
|
* Re-chunk and re-embed every document in the account. The main use is
|
|
* after adding an embeddings key: existing documents were stored
|
|
* lexical-only, and this backfills their vectors so semantic search
|
|
* turns on. Also recovers documents whose indexing failed earlier.
|
|
*/
|
|
export async function POST() {
|
|
try {
|
|
const { supabase, accountId, userId } = await requireRole('admin')
|
|
const limit = checkRateLimit(`ai-kb-reindex:${userId}`, RATE_LIMITS.adminAction)
|
|
if (!limit.success) return rateLimitResponse(limit)
|
|
|
|
const { data: docs, error } = await supabase
|
|
.from('ai_knowledge_documents')
|
|
.select('id, content')
|
|
.eq('account_id', accountId)
|
|
if (error) {
|
|
console.error('[ai/knowledge/reindex] fetch error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Failed to load documents' },
|
|
{ status: 500 },
|
|
)
|
|
}
|
|
|
|
const { key: embeddingsApiKey, corrupt } = await loadEmbeddingsKey(
|
|
supabase,
|
|
accountId,
|
|
)
|
|
// The whole point of Reindex is usually to backfill embeddings — so
|
|
// if a key is configured but can't be decrypted, don't quietly do a
|
|
// lexical-only pass and report success. Stop and tell the admin.
|
|
if (corrupt) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
reindexed: 0,
|
|
error:
|
|
'Your embeddings key could not be decrypted (check ENCRYPTION_KEY, then re-enter the key in Settings → AI Assistant). Nothing was reindexed.',
|
|
},
|
|
{ status: 200 },
|
|
)
|
|
}
|
|
|
|
let reindexed = 0
|
|
for (const doc of docs ?? []) {
|
|
try {
|
|
await ingestDocument(supabase, accountId, { embeddingsApiKey }, doc.id, doc.content)
|
|
reindexed += 1
|
|
} catch (err) {
|
|
// One bad document (e.g. a mid-run embeddings rate-limit) should
|
|
// not abort the whole batch.
|
|
const message = err instanceof AiError ? err.message : String(err)
|
|
console.error(`[ai/knowledge/reindex] doc ${doc.id} failed:`, message)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
reindexed,
|
|
total: (docs ?? []).length,
|
|
error: `Reindexed ${reindexed}, then hit an error: ${message}`,
|
|
},
|
|
{ status: 200 },
|
|
)
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ success: true, reindexed })
|
|
} catch (err) {
|
|
return toErrorResponse(err)
|
|
}
|
|
}
|