Add RAG semantic search and proactive event notifications

Implement hybrid knowledge search using Cloudflare Vectorize + Workers AI
embeddings (bge-base-en-v1.5, 768d) merged with existing D1 LIKE queries,
with graceful degradation when Vectorize is unavailable. Add admin API
endpoints for batch/single article indexing.

Add 4 proactive notification cron jobs: server status changes, deposit
confirmation/rejection alerts, pending payment reminders (1h+), and bank
deposit matching notifications — all with DB-column-based deduplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-11 18:09:13 +09:00
parent 1d6b64c9e4
commit f7046f4c66
9 changed files with 768 additions and 34 deletions

View File

@@ -2,6 +2,7 @@ import { Hono } from 'hono';
import type { Env, User, Transaction } from '../types';
import { timingSafeEqual } from '../security';
import { getPendingActions } from '../services/pending-actions';
import { indexArticle, batchIndexArticles } from '../services/embedding';
import { sendMessage } from '../telegram';
import { createLogger } from '../utils/logger';
@@ -171,4 +172,67 @@ api.post('/broadcast', async (c) => {
}
});
// POST /api/knowledge/index - Batch index all knowledge articles
api.post('/knowledge/index', async (c) => {
try {
if (!c.env.VECTORIZE) {
return c.json({ error: 'VECTORIZE binding not configured' }, 400);
}
const body = await c.req.json<{ force_reindex?: boolean }>().catch(() => ({ force_reindex: false }));
const result = await batchIndexArticles(c.env, {
forceReindex: body.force_reindex ?? false,
});
logger.info('Knowledge batch indexing completed', {
indexed: result.indexed,
failed: result.failed,
});
return c.json(result);
} catch (error) {
logger.error('Knowledge batch indexing failed', error as Error);
return c.json({ error: 'Internal error' }, 500);
}
});
// POST /api/knowledge/:id/index - Index a single knowledge article
api.post('/knowledge/:id/index', async (c) => {
try {
if (!c.env.VECTORIZE) {
return c.json({ error: 'VECTORIZE binding not configured' }, 400);
}
const articleId = parseInt(c.req.param('id'));
if (isNaN(articleId)) {
return c.json({ error: 'Invalid article ID' }, 400);
}
const article = await c.env.DB
.prepare(
`SELECT id, category, title, content, tags
FROM knowledge_articles WHERE id = ? AND is_active = 1`
)
.bind(articleId)
.first<{ id: number; category: string; title: string; content: string; tags: string | null }>();
if (!article) {
return c.json({ error: 'Article not found' }, 404);
}
await indexArticle(
c.env,
article.id,
article.title,
article.content,
article.category,
article.tags ?? undefined
);
return c.json({ success: true, articleId: article.id });
} catch (error) {
logger.error('Knowledge article indexing failed', error as Error);
return c.json({ error: 'Internal error' }, 500);
}
});
export { api as apiRouter };