mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 05:42:19 +03:00
Boarded with #11756 (a duplicate fix for the same underlying issue #11650). Compared both implementations directly: this one is technically superior — guards the json_extract() call with json_valid(metadata) so malformed/legacy metadata returns no match instead of throwing a 500, and covers genericBackend.ts/obsidianBackend.ts in addition to sqliteBackend.ts. #11756 only touched SQLite and had no malformed-JSON guard. Closing #11756 with credit. Resynced onto the updated release tip: the test file's `await import("../../src/lib/localDb.ts")` broke after #12055 deleted the barrel earlier this session (your branch forked before that migration) — fixed to import updateSettings directly from @/lib/db/settings, matching the pattern already used by other integration tests. typecheck:core, check:dashboard-typecheck, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-deps all green; 4/4 integration + 35/35 vitest pass after resync. Thanks for the thorough, well-tested fix.
120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
|
import { listMemories, createMemory, getMemoryTokensUsed } from "@/lib/memory/store";
|
|
import { memoryManager } from "@/lib/memory";
|
|
import { memoryCache } from "@/lib/memory/cache";
|
|
import { MemoryType } from "@/lib/memory/types";
|
|
import { parsePaginationParams, buildPaginatedResponse } from "@/shared/types/pagination";
|
|
import { z } from "zod";
|
|
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
|
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
|
|
|
const createMemorySchema = z.object({
|
|
content: z.string().min(1),
|
|
key: z.string().min(1),
|
|
type: z.nativeEnum(MemoryType).default(MemoryType.FACTUAL),
|
|
sessionId: z.string().default(""),
|
|
apiKeyId: z.string().default(""),
|
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
expiresAt: z.coerce.date().nullable().default(null),
|
|
});
|
|
|
|
export async function GET(request: Request) {
|
|
const authError = await requireManagementAuth(request);
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const url = new URL(request.url);
|
|
const { searchParams } = url;
|
|
|
|
const paginationParams = parsePaginationParams(searchParams);
|
|
const rawOffset = searchParams.get("offset");
|
|
const offset =
|
|
typeof rawOffset === "string" && rawOffset.trim().length > 0
|
|
? Math.max(0, Number.parseInt(rawOffset, 10) || 0)
|
|
: undefined;
|
|
const query = searchParams.get("q") || undefined;
|
|
|
|
const apiKeyId = searchParams.get("apiKeyId") || undefined;
|
|
const type = (searchParams.get("type") as any) || undefined;
|
|
const sessionId = searchParams.get("sessionId") || undefined;
|
|
const category = searchParams.get("category")?.trim() || undefined;
|
|
|
|
const result = await memoryManager.list({
|
|
apiKeyId,
|
|
type,
|
|
sessionId,
|
|
category,
|
|
query,
|
|
limit: paginationParams.limit,
|
|
offset:
|
|
offset ??
|
|
(offset === undefined ? undefined : (paginationParams.page - 1) * paginationParams.limit),
|
|
});
|
|
|
|
// Total tokens across all memories (computed in SQL inside the domain module
|
|
// to avoid loading every memory's content into process memory).
|
|
const tokensUsed = getMemoryTokensUsed(apiKeyId);
|
|
|
|
// Compute hit rate from memory cache
|
|
const cacheStats = memoryCache.stats();
|
|
const totalCacheRequests = cacheStats.hits + cacheStats.misses;
|
|
const hitRate = totalCacheRequests > 0 ? cacheStats.hits / totalCacheRequests : 0;
|
|
|
|
const stats = {
|
|
total: result.total,
|
|
byType: result.byType ?? {},
|
|
tokensUsed,
|
|
hitRate,
|
|
// Plan 21 / D18: MemoriesTab uses cacheStats to decide whether to render
|
|
// the Hit Rate card (only when hits + misses > 0). Without this field
|
|
// the card would never appear even when hitRate > 0.
|
|
cacheStats: { hits: cacheStats.hits, misses: cacheStats.misses },
|
|
};
|
|
|
|
const responsePagination =
|
|
offset === undefined
|
|
? paginationParams
|
|
: {
|
|
...paginationParams,
|
|
page: Math.floor(offset / paginationParams.limit) + 1,
|
|
};
|
|
|
|
const paginatedResponse = buildPaginatedResponse(result.data, result.total, responsePagination);
|
|
|
|
return NextResponse.json({
|
|
...paginatedResponse,
|
|
stats,
|
|
});
|
|
} catch (err: unknown) {
|
|
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
return NextResponse.json({ error: { message } }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const authError = await requireManagementAuth(request);
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const rawBody = await request.json();
|
|
const validation = validateBody(createMemorySchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json(validation.error, { status: 400 });
|
|
}
|
|
const memoryId = await memoryManager.create({
|
|
apiKeyId: validation.data.apiKeyId,
|
|
sessionId: validation.data.sessionId,
|
|
type: validation.data.type,
|
|
key: validation.data.key,
|
|
content: validation.data.content,
|
|
metadata: validation.data.metadata,
|
|
expiresAt: validation.data.expiresAt,
|
|
});
|
|
return NextResponse.json({ success: true, id: memoryId });
|
|
} catch (err: unknown) {
|
|
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
return NextResponse.json({ error: { message } }, { status: 400 });
|
|
}
|
|
}
|