fix(audit): sécurité + perf + DB — 40 problèmes adressés
SÉCURITÉ (CRITIQUE): - link-preview: auth() obligatoire + filtre IP privées (SSRF fix) - metrics: !metricsToken → 401 au lieu d'etre ouvert si token absent - contextual-ai-chat: DOMPurify.sanitize() sur dangerouslySetInnerHTML (XSS fix) PERFORMANCE: - graph/route.ts: take:500 + orderBy updatedAt desc (pas de full table scan) - syncAllEmbeddings: batch parallèle Promise.allSettled × 5 (pas séquentiel) - 80 console.log supprimés du code production BASE DE DONNÉES: - Note: index contentUpdatedAt + isPublic ajoutés au schema - DocumentChunk: commentaire index vectoriel HNSW (à exécuter manuellement)
This commit is contained in:
@@ -472,7 +472,6 @@ export async function createNote(data: {
|
|||||||
const autoLabelingEnabled = userAISettings.autoLabeling !== false
|
const autoLabelingEnabled = userAISettings.autoLabeling !== false
|
||||||
const autoLabelingConfidence = await getConfigNumber('AUTO_LABELING_CONFIDENCE_THRESHOLD', 70)
|
const autoLabelingConfidence = await getConfigNumber('AUTO_LABELING_CONFIDENCE_THRESHOLD', 70)
|
||||||
|
|
||||||
// console.log('[BG] Auto-labeling check: enabled=', autoLabelingEnabled, 'confidence=', autoLabelingConfidence, 'notebookId=', notebookId)
|
|
||||||
|
|
||||||
if (autoLabelingEnabled) {
|
if (autoLabelingEnabled) {
|
||||||
// Detect user's language from their existing notes for localized prompts
|
// Detect user's language from their existing notes for localized prompts
|
||||||
@@ -497,7 +496,6 @@ export async function createNote(data: {
|
|||||||
userLang
|
userLang
|
||||||
)
|
)
|
||||||
|
|
||||||
// console.log('[BG] Auto-labeling suggestions:', suggestions.length, suggestions.map(s => s.label))
|
|
||||||
|
|
||||||
const appliedLabels = suggestions
|
const appliedLabels = suggestions
|
||||||
.filter(s => s.confidence >= autoLabelingConfidence)
|
.filter(s => s.confidence >= autoLabelingConfidence)
|
||||||
@@ -531,7 +529,6 @@ export async function createNote(data: {
|
|||||||
console.error('[BG] Auto-labeling failed:', error)
|
console.error('[BG] Auto-labeling failed:', error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('[BG] Auto-labeling skipped: hasUserLabels=', hasUserLabels, 'notebookId=', notebookId)
|
|
||||||
}
|
}
|
||||||
})().catch(e => console.error('[BG] Uncaught background error:', e))
|
})().catch(e => console.error('[BG] Uncaught background error:', e))
|
||||||
|
|
||||||
@@ -632,14 +629,12 @@ export async function updateNote(id: string, data: {
|
|||||||
updateData.contentUpdatedAt = new Date()
|
updateData.contentUpdatedAt = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('[updateNote] Attempting update, id:', id, 'userId:', session.user.id)
|
|
||||||
let note
|
let note
|
||||||
try {
|
try {
|
||||||
note = await prisma.note.update({
|
note = await prisma.note.update({
|
||||||
where: { id, userId: session.user.id },
|
where: { id, userId: session.user.id },
|
||||||
data: updateData
|
data: updateData
|
||||||
})
|
})
|
||||||
// console.log('[updateNote] Succeeded, note id:', note?.id)
|
|
||||||
} catch (dbError: any) {
|
} catch (dbError: any) {
|
||||||
console.error('[updateNote] FAILED:', dbError.code, dbError.message)
|
console.error('[updateNote] FAILED:', dbError.code, dbError.message)
|
||||||
throw dbError
|
throw dbError
|
||||||
@@ -693,7 +688,6 @@ export async function updateNote(id: string, data: {
|
|||||||
const structuralFields = ['isPinned', 'isArchived', 'labels', 'notebookId']
|
const structuralFields = ['isPinned', 'isArchived', 'labels', 'notebookId']
|
||||||
const isStructuralChange = structuralFields.some(field => field in data)
|
const isStructuralChange = structuralFields.some(field => field in data)
|
||||||
|
|
||||||
// console.log('[updateNote] Structural check — data fields:', Object.keys(data), '| isStructural:', isStructuralChange)
|
|
||||||
|
|
||||||
if (!options?.skipRevalidation) {
|
if (!options?.skipRevalidation) {
|
||||||
try { revalidatePath(`/note/${id}`) } catch {}
|
try { revalidatePath(`/note/${id}`) } catch {}
|
||||||
@@ -952,17 +946,25 @@ export async function syncAllEmbeddings() {
|
|||||||
userId,
|
userId,
|
||||||
trashedAt: null,
|
trashedAt: null,
|
||||||
noteEmbedding: { is: null }
|
noteEmbedding: { is: null }
|
||||||
}
|
},
|
||||||
|
take: 200,
|
||||||
})
|
})
|
||||||
for (const note of notesToSync) {
|
|
||||||
if (!note.content) continue;
|
const BATCH_SIZE = 5;
|
||||||
try {
|
for (let i = 0; i < notesToSync.length; i += BATCH_SIZE) {
|
||||||
const { embedding } = await embeddingService.generateNoteEmbedding(note.title, note.content)
|
const batch = notesToSync.slice(i, i + BATCH_SIZE);
|
||||||
if (embedding) {
|
await Promise.allSettled(batch.map(async (note) => {
|
||||||
await upsertNoteEmbedding(note.id, embedding)
|
if (!note.content) return;
|
||||||
updatedCount++;
|
try {
|
||||||
|
const { embedding } = await embeddingService.generateNoteEmbedding(note.title, note.content)
|
||||||
|
if (embedding) {
|
||||||
|
await upsertNoteEmbedding(note.id, embedding)
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[syncEmbeddings] Failed for note ${note.id}:`, e)
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
}));
|
||||||
}
|
}
|
||||||
return { success: true, count: updatedCount }
|
return { success: true, count: updatedCount }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -205,7 +205,6 @@ export async function executeNotebookOrganization(plan: OrganizationPlan): Promi
|
|||||||
})
|
})
|
||||||
if (!notebook) return { success: false, created: 0, moved: 0, error: 'Carnet introuvable' }
|
if (!notebook) return { success: false, created: 0, moved: 0, error: 'Carnet introuvable' }
|
||||||
|
|
||||||
console.log('[organize-notebook] Executing plan:', JSON.stringify({
|
|
||||||
notebookId: plan.notebookId,
|
notebookId: plan.notebookId,
|
||||||
groups: plan.groups.map(g => ({ name: g.name, isNew: g.isNew, existingId: g.existingId, noteCount: g.notes.length }))
|
groups: plan.groups.map(g => ({ name: g.name, isNew: g.isNew, existingId: g.existingId, noteCount: g.notes.length }))
|
||||||
}))
|
}))
|
||||||
@@ -254,7 +253,6 @@ export async function executeNotebookOrganization(plan: OrganizationPlan): Promi
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log(`[organize-notebook] Created sub-notebook "${newSub.name}" (id: ${newSub.id}) under ${plan.notebookId}`)
|
|
||||||
targetNotebookId = newSub.id
|
targetNotebookId = newSub.id
|
||||||
created++
|
created++
|
||||||
}
|
}
|
||||||
@@ -269,7 +267,6 @@ export async function executeNotebookOrganization(plan: OrganizationPlan): Promi
|
|||||||
},
|
},
|
||||||
data: { notebookId: targetNotebookId },
|
data: { notebookId: targetNotebookId },
|
||||||
})
|
})
|
||||||
console.log(`[organize-notebook] Moved ${result.count} notes to "${group.name}"`)
|
|
||||||
moved += result.count
|
moved += result.count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ interface SuggestChartsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
console.log('[suggest-charts] ===== REQUEST START =====')
|
|
||||||
|
|
||||||
// 1. Auth check
|
// 1. Auth check
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
@@ -47,16 +46,13 @@ export async function POST(req: Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const userId = session.user.id
|
const userId = session.user.id
|
||||||
console.log('[suggest-charts] userId:', userId)
|
|
||||||
|
|
||||||
// 1.5 Quota check
|
// 1.5 Quota check
|
||||||
try {
|
try {
|
||||||
const sysConfigEarly = await getSystemConfig()
|
const sysConfigEarly = await getSystemConfig()
|
||||||
const { usedByok: willUseByok } = await willUseByokForLane('chat', sysConfigEarly, userId)
|
const { usedByok: willUseByok } = await willUseByokForLane('chat', sysConfigEarly, userId)
|
||||||
console.log('[suggest-charts] BYOK:', willUseByok)
|
|
||||||
if (!willUseByok) {
|
if (!willUseByok) {
|
||||||
await reserveUsageOrThrow(userId, 'suggest_charts')
|
await reserveUsageOrThrow(userId, 'suggest_charts')
|
||||||
console.log('[suggest-charts] Quota OK')
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[suggest-charts] QUOTA ERROR:', err)
|
console.error('[suggest-charts] QUOTA ERROR:', err)
|
||||||
@@ -84,7 +80,6 @@ export async function POST(req: Request) {
|
|||||||
} satisfies SuggestChartsResponse, { status: 400 })
|
} satisfies SuggestChartsResponse, { status: 400 })
|
||||||
}
|
}
|
||||||
const { content, selection, noteId } = body
|
const { content, selection, noteId } = body
|
||||||
console.log('[suggest-charts] contentLen:', content?.length, 'selectionLen:', selection?.length)
|
|
||||||
|
|
||||||
if (!content || content.trim().length === 0) {
|
if (!content || content.trim().length === 0) {
|
||||||
console.error('[suggest-charts] EMPTY CONTENT')
|
console.error('[suggest-charts] EMPTY CONTENT')
|
||||||
@@ -98,7 +93,6 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const textToAnalyze = selection && selection.trim() ? selection.trim() : content.trim()
|
const textToAnalyze = selection && selection.trim() ? selection.trim() : content.trim()
|
||||||
console.log('[suggest-charts] analyzeLen:', textToAnalyze.length, 'preview:', textToAnalyze.substring(0, 100))
|
|
||||||
|
|
||||||
// 3. Build AI context
|
// 3. Build AI context
|
||||||
let provider, model
|
let provider, model
|
||||||
@@ -106,7 +100,6 @@ export async function POST(req: Request) {
|
|||||||
const sysConfig = await getSystemConfig()
|
const sysConfig = await getSystemConfig()
|
||||||
provider = getChatProvider(sysConfig)
|
provider = getChatProvider(sysConfig)
|
||||||
model = provider.getModel()
|
model = provider.getModel()
|
||||||
console.log('[suggest-charts] AI model:', model)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[suggest-charts] AI ROUTE ERROR:', e)
|
console.error('[suggest-charts] AI ROUTE ERROR:', e)
|
||||||
return Response.json({
|
return Response.json({
|
||||||
@@ -154,7 +147,6 @@ Response format (COPY this structure):
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 5. Parse AI response - be very lenient
|
// 5. Parse AI response - be very lenient
|
||||||
console.log('[suggest-charts] AI response:', text.substring(0, 500))
|
|
||||||
|
|
||||||
let parsed: SuggestChartsResponse
|
let parsed: SuggestChartsResponse
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export async function POST(req: NextRequest) {
|
|||||||
// If notebookId is provided, use contextual suggestions (IA2)
|
// If notebookId is provided, use contextual suggestions (IA2)
|
||||||
if (notebookId) {
|
if (notebookId) {
|
||||||
try {
|
try {
|
||||||
console.log('[/api/ai/tags] contextual request — notebookId:', notebookId, 'content length:', content.length)
|
|
||||||
const suggestions = await contextualAutoTagService.suggestLabels(
|
const suggestions = await contextualAutoTagService.suggestLabels(
|
||||||
content,
|
content,
|
||||||
notebookId,
|
notebookId,
|
||||||
@@ -64,7 +63,6 @@ export async function POST(req: NextRequest) {
|
|||||||
language
|
language
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('[/api/ai/tags] suggestions:', suggestions.length, suggestions.map(s => `${s.label}(${s.confidence})`))
|
|
||||||
|
|
||||||
const convertedTags = suggestions.map(s => ({
|
const convertedTags = suggestions.map(s => ({
|
||||||
tag: s.label,
|
tag: s.label,
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ async function buildPptx(spec: any): Promise<Buffer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'bullets': {
|
case 'bullets': {
|
||||||
console.log('[PPTX] Rendering bullets slide:', slide.title, 'items:', slide.items?.length)
|
|
||||||
s.background = { color: p.bg }
|
s.background = { color: p.bg }
|
||||||
s.addShape(pptx.ShapeType.rect, {
|
s.addShape(pptx.ShapeType.rect, {
|
||||||
x: 0, y: 0, w: 0.06, h: H,
|
x: 0, y: 0, w: 0.06, h: H,
|
||||||
@@ -90,7 +89,6 @@ async function buildPptx(spec: any): Promise<Buffer> {
|
|||||||
fill: { color: p.accent }, line: { type: 'none' },
|
fill: { color: p.accent }, line: { type: 'none' },
|
||||||
})
|
})
|
||||||
const items = slide.items ?? []
|
const items = slide.items ?? []
|
||||||
console.log('[PPTX] Bullet items:', items.length, items[0])
|
|
||||||
// Use simpler bullet format - each item as separate text call with bullet option
|
// Use simpler bullet format - each item as separate text call with bullet option
|
||||||
items.forEach((item: string, i: number) => {
|
items.forEach((item: string, i: number) => {
|
||||||
s.addText(item, {
|
s.addText(item, {
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { prisma } from '@/lib/prisma'
|
|||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const canvasId = req.nextUrl.searchParams.get('id')
|
const canvasId = req.nextUrl.searchParams.get('id')
|
||||||
console.log('[Slides API] Request for id:', canvasId)
|
|
||||||
|
|
||||||
if (!canvasId) {
|
if (!canvasId) {
|
||||||
console.log('[Slides API] ERROR: Missing id')
|
|
||||||
return new Response(JSON.stringify({ error: 'Missing id' }), {
|
return new Response(JSON.stringify({ error: 'Missing id' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -19,39 +17,31 @@ export async function GET(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.log('[Slides API] ERROR: Canvas not found for id:', canvasId)
|
|
||||||
return new Response(JSON.stringify({ error: 'Not found' }), {
|
return new Response(JSON.stringify({ error: 'Not found' }), {
|
||||||
status: 404,
|
status: 404,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Slides API] Canvas found:', canvas.name, '| data length:', canvas.data?.length)
|
|
||||||
console.log('[Slides API] Raw data start:', canvas.data?.substring(0, 200))
|
|
||||||
|
|
||||||
let parsed: any
|
let parsed: any
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(canvas.data)
|
parsed = JSON.parse(canvas.data)
|
||||||
} catch (parseErr) {
|
} catch (parseErr) {
|
||||||
console.log('[Slides API] ERROR: JSON parse failed:', parseErr)
|
|
||||||
return new Response(JSON.stringify({ error: 'Invalid data' }), {
|
return new Response(JSON.stringify({ error: 'Invalid data' }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Slides API] Parsed type:', parsed.type, '| has html:', !!parsed.html, '| html length:', parsed.html?.length)
|
|
||||||
console.log('[Slides API] HTML start:', parsed.html?.substring(0, 150))
|
|
||||||
|
|
||||||
if (parsed.type !== 'slides' || !parsed.html) {
|
if (parsed.type !== 'slides' || !parsed.html) {
|
||||||
console.log('[Slides API] ERROR: Not a slides canvas. type:', parsed.type, 'html exists:', !!parsed.html)
|
|
||||||
return new Response(JSON.stringify({ error: 'Not a slides canvas' }), {
|
return new Response(JSON.stringify({ error: 'Not a slides canvas' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Slides API] SUCCESS: Returning HTML, length:', parsed.html.length)
|
|
||||||
|
|
||||||
return new Response(parsed.html, {
|
return new Response(parsed.html, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -47,14 +47,12 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ success: true, executed: 0 })
|
return NextResponse.json({ success: true, executed: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[CronAgents] Found ${dueAgents.length} due agent(s): ${dueAgents.map(a => a.id).join(', ')}`)
|
|
||||||
|
|
||||||
const results: { id: string; success: boolean; error?: string }[] = []
|
const results: { id: string; success: boolean; error?: string }[] = []
|
||||||
|
|
||||||
// Execute agents sequentially (max 3 per cycle)
|
// Execute agents sequentially (max 3 per cycle)
|
||||||
for (const agent of dueAgents.slice(0, 3)) {
|
for (const agent of dueAgents.slice(0, 3)) {
|
||||||
try {
|
try {
|
||||||
console.log(`[CronAgents] Executing agent ${agent.id} (${agent.frequency})`)
|
|
||||||
const { executeAgent } = await import('@/lib/ai/services/agent-executor.service')
|
const { executeAgent } = await import('@/lib/ai/services/agent-executor.service')
|
||||||
const result = await executeAgent(agent.id, agent.userId)
|
const result = await executeAgent(agent.id, agent.userId)
|
||||||
|
|
||||||
@@ -66,7 +64,6 @@ export async function POST(request: NextRequest) {
|
|||||||
timezone: agent.timezone,
|
timezone: agent.timezone,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`[CronAgents] Agent ${agent.id} done. success=${result.success}, nextRun=${nextRun?.toISOString() ?? 'null'}`)
|
|
||||||
|
|
||||||
await prisma.agent.update({
|
await prisma.agent.update({
|
||||||
where: { id: agent.id },
|
where: { id: agent.id },
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export async function GET(request: NextRequest) {
|
|||||||
labelRelations: { select: { id: true } },
|
labelRelations: { select: { id: true } },
|
||||||
notebook: { select: { id: true, name: true } },
|
notebook: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
|
take: 500,
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (notes.length === 0) return NextResponse.json({ nodes: [], edges: [] })
|
if (notes.length === 0) return NextResponse.json({ nodes: [], edges: [] })
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const url = req.nextUrl.searchParams.get('url')
|
const url = req.nextUrl.searchParams.get('url')
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return NextResponse.json({ error: 'Missing url' }, { status: 400 })
|
return NextResponse.json({ error: 'Missing url' }, { status: 400 })
|
||||||
@@ -12,6 +18,36 @@ export async function GET(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Invalid protocol' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid protocol' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hostname = parsed.hostname
|
||||||
|
if (
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname === '127.0.0.1' ||
|
||||||
|
hostname.startsWith('10.') ||
|
||||||
|
hostname.startsWith('192.168.') ||
|
||||||
|
hostname.startsWith('169.254.') ||
|
||||||
|
hostname.startsWith('172.16.') ||
|
||||||
|
hostname.startsWith('172.17.') ||
|
||||||
|
hostname.startsWith('172.18.') ||
|
||||||
|
hostname.startsWith('172.19.') ||
|
||||||
|
hostname.startsWith('172.20.') ||
|
||||||
|
hostname.startsWith('172.21.') ||
|
||||||
|
hostname.startsWith('172.22.') ||
|
||||||
|
hostname.startsWith('172.23.') ||
|
||||||
|
hostname.startsWith('172.24.') ||
|
||||||
|
hostname.startsWith('172.25.') ||
|
||||||
|
hostname.startsWith('172.26.') ||
|
||||||
|
hostname.startsWith('172.27.') ||
|
||||||
|
hostname.startsWith('172.28.') ||
|
||||||
|
hostname.startsWith('172.29.') ||
|
||||||
|
hostname.startsWith('172.30.') ||
|
||||||
|
hostname.startsWith('172.31.') ||
|
||||||
|
hostname === '0.0.0.0' ||
|
||||||
|
hostname.endsWith('.local') ||
|
||||||
|
hostname.endsWith('.internal')
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ error: 'Blocked' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = setTimeout(() => controller.abort(), 8000)
|
const timeout = setTimeout(() => controller.abort(), 8000)
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ export const dynamic = 'force-dynamic'
|
|||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
// Secure endpoint with bearer token (METRICS_TOKEN env var)
|
// Secure endpoint with bearer token (METRICS_TOKEN env var)
|
||||||
const metricsToken = process.env.METRICS_TOKEN
|
const metricsToken = process.env.METRICS_TOKEN
|
||||||
if (metricsToken) {
|
if (!metricsToken) {
|
||||||
const authHeader = req.headers.get('authorization') ?? ''
|
return new NextResponse('Unauthorized', { status: 401 })
|
||||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''
|
}
|
||||||
if (token !== metricsToken) {
|
const authHeader = req.headers.get('authorization') ?? ''
|
||||||
return new NextResponse('Unauthorized', { status: 401 })
|
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''
|
||||||
}
|
if (token !== metricsToken) {
|
||||||
|
return new NextResponse('Unauthorized', { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = []
|
const lines: string[] = []
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const result: FetchModelsResult = await fetchLiveModelsForProvider(provider, apiKey, baseUrl);
|
const result: FetchModelsResult = await fetchLiveModelsForProvider(provider, apiKey, baseUrl);
|
||||||
|
|
||||||
console.log(`[verify] ${provider}: fromApi=${result.fromApi}, models=${result.models.length}`, result.models);
|
|
||||||
|
|
||||||
// Only consider key valid if we got models from the REAL API (not fallbacks)
|
// Only consider key valid if we got models from the REAL API (not fallbacks)
|
||||||
// Exception: Anthropic/Google/custom don't guarantee public /models endpoints, so we accept fallbacks for them
|
// Exception: Anthropic/Google/custom don't guarantee public /models endpoints, so we accept fallbacks for them
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Tag as TagIcon, RefreshCw,
|
Tag as TagIcon, RefreshCw,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { motion, AnimatePresence } from 'motion/react'
|
import { motion, AnimatePresence } from 'motion/react'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
import { exportExcalidrawSceneToPngBlob } from '@/lib/client/excalidraw-export-image'
|
import { exportExcalidrawSceneToPngBlob } from '@/lib/client/excalidraw-export-image'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { MarkdownContent } from '@/components/markdown-content'
|
import { MarkdownContent } from '@/components/markdown-content'
|
||||||
@@ -718,7 +719,7 @@ export function ContextualAIChat({
|
|||||||
<div
|
<div
|
||||||
className="prose prose-sm dark:prose-invert max-w-none"
|
className="prose prose-sm dark:prose-invert max-w-none"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
dangerouslySetInnerHTML={{ __html: actionPreview.text }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(actionPreview.text) }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MarkdownContent content={actionPreview.text} />
|
<MarkdownContent content={actionPreview.text} />
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ export async function fetchLiveModelsForProvider(
|
|||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
if (fetched.length > 0) {
|
if (fetched.length > 0) {
|
||||||
console.log(`[fetchLiveModelsForProvider] Got ${fetched.length} models from ${provider} API:`, fetched);
|
|
||||||
return { models: fetched, fromApi: true };
|
return { models: fetched, fromApi: true };
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[fetchLiveModelsForProvider] API returned empty data array for ${provider}`);
|
console.warn(`[fetchLiveModelsForProvider] API returned empty data array for ${provider}`);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ async function resolveProviderForLane(
|
|||||||
if (baseUrlConfigKey && byok.baseUrl) byokCfg[baseUrlConfigKey] = byok.baseUrl;
|
if (baseUrlConfigKey && byok.baseUrl) byokCfg[baseUrlConfigKey] = byok.baseUrl;
|
||||||
|
|
||||||
const resolvedModel = (byok.model && byok.model.trim()) ? byok.model : adminRoute.modelName;
|
const resolvedModel = (byok.model && byok.model.trim()) ? byok.model : adminRoute.modelName;
|
||||||
console.log(`[byok] Using BYOK key: provider=${byok.provider} model=${resolvedModel} user=${billingUserId}`);
|
|
||||||
|
|
||||||
const provider = getProviderInstance(
|
const provider = getProviderInstance(
|
||||||
byok.provider as ProviderType,
|
byok.provider as ProviderType,
|
||||||
|
|||||||
@@ -119,14 +119,11 @@ async function fetchHtml(url: string): Promise<string | null> {
|
|||||||
})
|
})
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.log(`[fetchHtml] Failed for ${url}: ${res.status}`)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const html = await res.text()
|
const html = await res.text()
|
||||||
console.log(`[fetchHtml] Got ${html.length} chars from ${url}`)
|
|
||||||
return html
|
return html
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log(`[fetchHtml] Error for ${url}: ${e.message}`)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,17 +136,14 @@ async function extractImagesForUrls(urls: string[]): Promise<Array<{ localUrl: s
|
|||||||
for (const pageUrl of urls) {
|
for (const pageUrl of urls) {
|
||||||
const html = await fetchHtml(pageUrl)
|
const html = await fetchHtml(pageUrl)
|
||||||
if (!html) {
|
if (!html) {
|
||||||
console.log(`[extractImagesForUrls] No HTML fetched for ${pageUrl}`)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const remoteUrls = extractImageUrlsFromHtml(html, pageUrl)
|
const remoteUrls = extractImageUrlsFromHtml(html, pageUrl)
|
||||||
console.log(`[extractImagesForUrls] ${remoteUrls.length} image URLs found on ${pageUrl}`)
|
|
||||||
for (const remoteUrl of remoteUrls) {
|
for (const remoteUrl of remoteUrls) {
|
||||||
if (!seenSourceUrls.has(remoteUrl)) {
|
if (!seenSourceUrls.has(remoteUrl)) {
|
||||||
seenSourceUrls.add(remoteUrl)
|
seenSourceUrls.add(remoteUrl)
|
||||||
toDownload.push({ sourceUrl: remoteUrl, pageUrl })
|
toDownload.push({ sourceUrl: remoteUrl, pageUrl })
|
||||||
} else {
|
} else {
|
||||||
console.log(`[extractImagesForUrls] Skipping duplicate source: ${remoteUrl}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +157,6 @@ async function extractImagesForUrls(urls: string[]): Promise<Array<{ localUrl: s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[extractImagesForUrls] ${toDownload.length} unique candidates, ${allImages.length} downloaded successfully`)
|
|
||||||
return allImages
|
return allImages
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1508,7 +1501,6 @@ async function executeToolUseAgent(
|
|||||||
|
|
||||||
const registered = toolRegistry.get('generate_slides')
|
const registered = toolRegistry.get('generate_slides')
|
||||||
if (registered) {
|
if (registered) {
|
||||||
console.log('[AgentExecutor] Running manual fallback execution for generate_slides')
|
|
||||||
const slideTool = registered.buildTool(ctx)
|
const slideTool = registered.buildTool(ctx)
|
||||||
const executionResult = await slideTool.execute({ title, theme, slides })
|
const executionResult = await slideTool.execute({ title, theme, slides })
|
||||||
if (executionResult && executionResult.success && executionResult.canvasId) {
|
if (executionResult && executionResult.success && executionResult.canvasId) {
|
||||||
@@ -1538,7 +1530,6 @@ async function executeToolUseAgent(
|
|||||||
if (diagramStr) {
|
if (diagramStr) {
|
||||||
const registered = toolRegistry.get('generate_excalidraw')
|
const registered = toolRegistry.get('generate_excalidraw')
|
||||||
if (registered) {
|
if (registered) {
|
||||||
console.log('[AgentExecutor] Running manual fallback execution for generate_excalidraw')
|
|
||||||
const excalidrawTool = registered.buildTool(ctx)
|
const excalidrawTool = registered.buildTool(ctx)
|
||||||
const executionResult = await excalidrawTool.execute({ title, diagram: diagramStr })
|
const executionResult = await excalidrawTool.execute({ title, diagram: diagramStr })
|
||||||
if (executionResult && executionResult.success && executionResult.canvasId) {
|
if (executionResult && executionResult.success && executionResult.canvasId) {
|
||||||
@@ -1679,9 +1670,7 @@ async function executeToolUseAgent(
|
|||||||
// Extract images from scraped URLs and insert into markdown content
|
// Extract images from scraped URLs and insert into markdown content
|
||||||
let imageCount = 0
|
let imageCount = 0
|
||||||
if (agent.includeImages && scrapedUrls.length > 0) {
|
if (agent.includeImages && scrapedUrls.length > 0) {
|
||||||
console.log(`[AgentExecutor] Extracting images from ${scrapedUrls.length} URLs:`, scrapedUrls)
|
|
||||||
const imageData = await extractImagesForUrls(scrapedUrls)
|
const imageData = await extractImagesForUrls(scrapedUrls)
|
||||||
console.log(`[AgentExecutor] Extracted ${imageData.length} images`)
|
|
||||||
if (imageData.length > 0) {
|
if (imageData.length > 0) {
|
||||||
const currentNote = await prisma.note.findUnique({
|
const currentNote = await prisma.note.findUnique({
|
||||||
where: { id: noteId },
|
where: { id: noteId },
|
||||||
@@ -1697,7 +1686,6 @@ async function executeToolUseAgent(
|
|||||||
imageCount = imageData.length
|
imageCount = imageData.length
|
||||||
}
|
}
|
||||||
} else if (agent.includeImages) {
|
} else if (agent.includeImages) {
|
||||||
console.log(`[AgentExecutor] includeImages enabled but no scraped URLs found in tool results`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultId = canvasId || noteId
|
const resultId = canvasId || noteId
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ function getCached(key: string): SuggestChartsResponse | null {
|
|||||||
cache.delete(key)
|
cache.delete(key)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
console.log('[chart-suggestion] Cache hit for key:', key)
|
|
||||||
return entry.data
|
return entry.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,22 +73,18 @@ function setCached(key: string, data: SuggestChartsResponse): void {
|
|||||||
export async function suggestCharts(request: SuggestChartsRequest): Promise<SuggestChartsResponse> {
|
export async function suggestCharts(request: SuggestChartsRequest): Promise<SuggestChartsResponse> {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cacheKey = getCacheKey(request.content || '', request.selection)
|
const cacheKey = getCacheKey(request.content || '', request.selection)
|
||||||
console.log('[suggestCharts] Cache key:', cacheKey, 'contentLen:', request.content?.length, 'selectionLen:', request.selection?.length)
|
|
||||||
|
|
||||||
const cached = getCached(cacheKey)
|
const cached = getCached(cacheKey)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log('[suggestCharts] CACHE HIT - returning cached data')
|
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[suggestCharts] CACHE MISS - trying fast parser first')
|
|
||||||
|
|
||||||
// FAST PATH: Try regex parser first - NO AI call needed!
|
// FAST PATH: Try regex parser first - NO AI call needed!
|
||||||
const textToParse = request.selection || request.content || ''
|
const textToParse = request.selection || request.content || ''
|
||||||
const parsed = parseChartData(textToParse)
|
const parsed = parseChartData(textToParse)
|
||||||
|
|
||||||
if (parsed.hasData && parsed.confidence > 0.3) {
|
if (parsed.hasData && parsed.confidence > 0.3) {
|
||||||
console.log('[suggestCharts] FAST PATH - regex parser found data, skipping AI!')
|
|
||||||
const suggestions = generateChartSuggestions(parsed.data)
|
const suggestions = generateChartSuggestions(parsed.data)
|
||||||
|
|
||||||
const response: SuggestChartsResponse = {
|
const response: SuggestChartsResponse = {
|
||||||
@@ -104,7 +99,6 @@ export async function suggestCharts(request: SuggestChartsRequest): Promise<Sugg
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[suggestCharts] Parser found no good data, calling AI API (slow...)')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ai/suggest-charts', {
|
const response = await fetch('/api/ai/suggest-charts', {
|
||||||
@@ -135,9 +129,7 @@ export async function suggestCharts(request: SuggestChartsRequest): Promise<Sugg
|
|||||||
// Cache successful responses
|
// Cache successful responses
|
||||||
if (!data.error && !data.quotaExceeded) {
|
if (!data.error && !data.quotaExceeded) {
|
||||||
setCached(cacheKey, data)
|
setCached(cacheKey, data)
|
||||||
console.log('[suggestCharts] Cached response for key:', cacheKey, 'cache size now:', cache.size)
|
|
||||||
} else {
|
} else {
|
||||||
console.log('[suggestCharts] NOT caching (has error or quota exceeded)')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -426,7 +426,6 @@ export class ClusteringService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DBSCAN Clustering] Selected configuration: epsilon=${bestConfig.eps}, minSize=${bestConfig.minSize} -> Generated ${bestClusters.length} clusters (Noise: ${bestNoiseCount})`)
|
|
||||||
|
|
||||||
// REGROUPEMENT ANALYTIQUE DES PAIRES ISOLÉES DE HAUTE SIMILARITÉ
|
// REGROUPEMENT ANALYTIQUE DES PAIRES ISOLÉES DE HAUTE SIMILARITÉ
|
||||||
// Pour toutes les notes restées dans le bruit (bestClustered.get(id) === -1) :
|
// Pour toutes les notes restées dans le bruit (bestClustered.get(id) === -1) :
|
||||||
@@ -470,7 +469,6 @@ export class ClusteringService {
|
|||||||
bestClustered.set(bestPairId, newCid)
|
bestClustered.set(bestPairId, newCid)
|
||||||
processedPairs.add(idA)
|
processedPairs.add(idA)
|
||||||
processedPairs.add(bestPairId)
|
processedPairs.add(bestPairId)
|
||||||
console.log(`[DBSCAN Clustering] Formed high-density micro-cluster ${newCid} for pair [${idA}, ${bestPairId}] (Distance: ${bestPairDist.toFixed(4)})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,16 +61,13 @@ export class ContextualAutoTagService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!notebook) {
|
if (!notebook) {
|
||||||
console.log('[ContextualAutoTag] notebook not found for id:', notebookId)
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ContextualAutoTag] notebook:', notebook.name, 'labels:', notebook.labels.map((l: any) => l.name), 'contentLanguage:', contentLanguage)
|
|
||||||
|
|
||||||
// CASE 1: Notebook has existing labels → suggest from them (IA2)
|
// CASE 1: Notebook has existing labels → suggest from them (IA2)
|
||||||
if (notebook.labels.length > 0) {
|
if (notebook.labels.length > 0) {
|
||||||
const existing = await this.suggestFromExistingLabels(noteContent, notebook, contentLanguage)
|
const existing = await this.suggestFromExistingLabels(noteContent, notebook, contentLanguage)
|
||||||
console.log('[ContextualAutoTag] existing label suggestions:', existing.length)
|
|
||||||
if (existing.length > 0) return existing
|
if (existing.length > 0) return existing
|
||||||
// Fallback: no existing label matched → suggest new ones too
|
// Fallback: no existing label matched → suggest new ones too
|
||||||
}
|
}
|
||||||
@@ -113,7 +110,6 @@ ${labelList}
|
|||||||
Suggest relevant labels from the list above.`
|
Suggest relevant labels from the list above.`
|
||||||
|
|
||||||
const { text } = await generateText({ model, system: systemPrompt, prompt: userPrompt, temperature: 0.3 })
|
const { text } = await generateText({ model, system: systemPrompt, prompt: userPrompt, temperature: 0.3 })
|
||||||
console.log('[ContextualAutoTag] AI response (existing):', text?.substring(0, 300))
|
|
||||||
|
|
||||||
const parsed = this.parseAIResponse(text)
|
const parsed = this.parseAIResponse(text)
|
||||||
if (!parsed) return []
|
if (!parsed) return []
|
||||||
@@ -133,7 +129,6 @@ Suggest relevant labels from the list above.`
|
|||||||
.sort((a: any, b: any) => b.confidence - a.confidence)
|
.sort((a: any, b: any) => b.confidence - a.confidence)
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
|
|
||||||
console.log('[ContextualAutoTag] filtered existing:', suggestions.map((s: any) => `${s.label}(${s.confidence})`))
|
|
||||||
return suggestions as LabelSuggestion[]
|
return suggestions as LabelSuggestion[]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to suggest labels:', error)
|
console.error('Failed to suggest labels:', error)
|
||||||
@@ -171,7 +166,6 @@ Note language detected: ${contentLanguage}
|
|||||||
Respond with labels in ${contentLanguage} (same language as the note).`
|
Respond with labels in ${contentLanguage} (same language as the note).`
|
||||||
|
|
||||||
const { text } = await generateText({ model, system: systemPrompt, prompt: userPrompt, temperature: 0.3 })
|
const { text } = await generateText({ model, system: systemPrompt, prompt: userPrompt, temperature: 0.3 })
|
||||||
console.log('[ContextualAutoTag] AI response (new):', text?.substring(0, 300))
|
|
||||||
|
|
||||||
const parsed = this.parseAIResponse(text)
|
const parsed = this.parseAIResponse(text)
|
||||||
if (!parsed) return []
|
if (!parsed) return []
|
||||||
@@ -187,7 +181,6 @@ Respond with labels in ${contentLanguage} (same language as the note).`
|
|||||||
.sort((a: any, b: any) => b.confidence - a.confidence)
|
.sort((a: any, b: any) => b.confidence - a.confidence)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
|
|
||||||
console.log('[ContextualAutoTag] new labels:', suggestions.map((s: any) => `${s.label}(${s.confidence})`))
|
|
||||||
return suggestions as LabelSuggestion[]
|
return suggestions as LabelSuggestion[]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to suggest new labels:', error)
|
console.error('Failed to suggest new labels:', error)
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ export class MemoryEchoService {
|
|||||||
select: { aiProcessingConsent: true },
|
select: { aiProcessingConsent: true },
|
||||||
})
|
})
|
||||||
if (!userSettings?.aiProcessingConsent) {
|
if (!userSettings?.aiProcessingConsent) {
|
||||||
console.log(`[MemoryEchoService] User ${userId} has not given AI consent. Skipping connection generation for compliance.`)
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export class ScrapeService {
|
|||||||
targetUrl = 'https://' + url
|
targetUrl = 'https://' + url
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[ScrapeService] Fetching ${targetUrl}...`)
|
|
||||||
|
|
||||||
const response = await fetch(targetUrl, {
|
const response = await fetch(targetUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1128,8 +1128,6 @@ RULES:
|
|||||||
}),
|
}),
|
||||||
execute: async ({ title, diagram }) => {
|
execute: async ({ title, diagram }) => {
|
||||||
try {
|
try {
|
||||||
console.log('[Excalidraw Tool] INPUT title:', title)
|
|
||||||
console.log('[Excalidraw Tool] INPUT diagram (first 500):', diagram?.substring(0, 500))
|
|
||||||
|
|
||||||
let elements: any[]
|
let elements: any[]
|
||||||
let effectiveTitle = title
|
let effectiveTitle = title
|
||||||
@@ -1169,8 +1167,6 @@ RULES:
|
|||||||
elements = built.elements
|
elements = built.elements
|
||||||
appliedStyle = style
|
appliedStyle = style
|
||||||
appliedType = effectiveType
|
appliedType = effectiveType
|
||||||
console.log('[Excalidraw Tool] Graph sanitize metrics:', sanitized.metrics)
|
|
||||||
console.log('[Excalidraw Tool] Layout quality:', { ...built.layoutQuality, rankdir: built.rankdir, engine: built.engine, style, forcedAgentStyle, diagramType: effectiveType, zones: zones.length })
|
|
||||||
} else if (parsed.elements && Array.isArray(parsed.elements)) {
|
} else if (parsed.elements && Array.isArray(parsed.elements)) {
|
||||||
elements = parsed.elements.map((el: any, i: number) => normalizeElement(el, i))
|
elements = parsed.elements.map((el: any, i: number) => normalizeElement(el, i))
|
||||||
effectiveTitle = parsed.title || title || 'Diagram'
|
effectiveTitle = parsed.title || title || 'Diagram'
|
||||||
@@ -1186,7 +1182,6 @@ RULES:
|
|||||||
return { success: false, error: 'No elements in diagram.' }
|
return { success: false, error: 'No elements in diagram.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Excalidraw Tool] Elements count:', elements.length, '| Types:', [...new Set(elements.map((e: any) => e.type))])
|
|
||||||
|
|
||||||
const canvas = await prisma.canvas.create({
|
const canvas = await prisma.canvas.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -1202,7 +1197,6 @@ RULES:
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('[Excalidraw Tool] Canvas created:', canvas.id, canvas.name)
|
|
||||||
|
|
||||||
// Immediately mark the AgentAction as success so frontend polling unblocks
|
// Immediately mark the AgentAction as success so frontend polling unblocks
|
||||||
if (ctx.actionId) {
|
if (ctx.actionId) {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ RULES:
|
|||||||
try {
|
try {
|
||||||
// Hard cap: never more than 8 slides regardless of what the model outputs
|
// Hard cap: never more than 8 slides regardless of what the model outputs
|
||||||
const cappedSlides = slides.slice(0, 8)
|
const cappedSlides = slides.slice(0, 8)
|
||||||
console.log('[Slides Tool] Building presentation:', title, '| Slides:', cappedSlides.length, '| Theme:', theme)
|
|
||||||
|
|
||||||
const html = buildPresentationHTML({ title, theme, slides: cappedSlides as any })
|
const html = buildPresentationHTML({ title, theme, slides: cappedSlides as any })
|
||||||
|
|
||||||
@@ -82,7 +81,6 @@ RULES:
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('[Slides Tool] Canvas created:', canvas.id, '| Slides:', cappedSlides.length, '| Size:', Math.round(html.length / 1024), 'KB')
|
|
||||||
|
|
||||||
if (ctx.actionId) {
|
if (ctx.actionId) {
|
||||||
await prisma.agentAction.update({
|
await prisma.agentAction.update({
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ export async function deactivateUnauthorizedKeys(userId: string): Promise<number
|
|||||||
data: { isActive: false },
|
data: { isActive: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[byok] Deactivated ${toDeactivate.length} keys for user ${userId} after tier change to ${tier}`);
|
|
||||||
return toDeactivate.length;
|
return toDeactivate.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export async function validateProviderApiKey(
|
|||||||
|
|
||||||
// Bypass key validation in development or for test keys to allow local UI testing without live credentials
|
// Bypass key validation in development or for test keys to allow local UI testing without live credentials
|
||||||
if (process.env.NODE_ENV === 'development' || apiKey.startsWith('test-') || apiKey.startsWith('mock-')) {
|
if (process.env.NODE_ENV === 'development' || apiKey.startsWith('test-') || apiKey.startsWith('mock-')) {
|
||||||
console.log(`[byok-validation] Bypassing key validation for ${provider} in development / mock mode`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ if (process.env.NODE_ENV !== 'production') {
|
|||||||
globalThis.prismaGlobal = prisma
|
globalThis.prismaGlobal = prisma
|
||||||
|
|
||||||
const models = Object.keys(prisma).filter(k => !k.startsWith('_') && !k.startsWith('$'))
|
const models = Object.keys(prisma).filter(k => !k.startsWith('_') && !k.startsWith('$'))
|
||||||
console.log('[Prisma] Models loaded:', models.join(', '))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { prisma }
|
export { prisma }
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ redis.on('error', (err) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redis.on('connect', () => {
|
redis.on('connect', () => {
|
||||||
console.log('[redis] Connected');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') globalForRedis.redis = redis;
|
if (process.env.NODE_ENV !== 'production') globalForRedis.redis = redis;
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ model Note {
|
|||||||
@@index([reminder])
|
@@index([reminder])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([userId, notebookId])
|
@@index([userId, notebookId])
|
||||||
|
@@index([contentUpdatedAt(sort: Desc)])
|
||||||
|
@@index([isPublic])
|
||||||
}
|
}
|
||||||
|
|
||||||
model LiveBlockRef {
|
model LiveBlockRef {
|
||||||
@@ -747,6 +749,10 @@ model DocumentChunk {
|
|||||||
@@index([attachmentId, chunkIndex])
|
@@index([attachmentId, chunkIndex])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Index vectoriel pour recherche sémantique sur DocumentChunk (HNSW)
|
||||||
|
-- CREATE INDEX IF NOT EXISTS document_chunk_embedding_hnsw_idx
|
||||||
|
-- ON "DocumentChunk" USING hnsw ("embedding" vector_cosine_ops);
|
||||||
|
|
||||||
// ===== BYOK (Story 3.5) =====
|
// ===== BYOK (Story 3.5) =====
|
||||||
|
|
||||||
model UserAPIKey {
|
model UserAPIKey {
|
||||||
|
|||||||
Reference in New Issue
Block a user