|
|
@@ -0,0 +1,602 @@
|
|
|
+import { Request, Response } from 'express';
|
|
|
+import { BaseEndpointComponent, EndpointMethod } from '../core/types';
|
|
|
+import { logger } from '../../utils/logger';
|
|
|
+import { ShopDatabase } from '../../database/Database';
|
|
|
+import { QdrantService } from '../../services/QdrantService';
|
|
|
+import { EmbeddingService } from '../../services/EmbeddingService';
|
|
|
+import { QdrantCleanupService } from '../../services/QdrantCleanupService';
|
|
|
+import { QdrantEmbeddingWorkflow } from '../../services/QdrantEmbeddingWorkflow';
|
|
|
+import type { AppConfig } from '../../config';
|
|
|
+
|
|
|
+export class QdrantEndpoint extends BaseEndpointComponent {
|
|
|
+ private qdrantService?: QdrantService;
|
|
|
+ private embeddingService?: EmbeddingService;
|
|
|
+ private cleanupService?: QdrantCleanupService;
|
|
|
+ private embeddingWorkflow?: QdrantEmbeddingWorkflow;
|
|
|
+
|
|
|
+ constructor(private db: ShopDatabase, private config: AppConfig) {
|
|
|
+ super();
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.qdrantService = new QdrantService(config);
|
|
|
+ this.embeddingService = new EmbeddingService(config);
|
|
|
+ this.cleanupService = new QdrantCleanupService(config, db);
|
|
|
+ this.embeddingWorkflow = new QdrantEmbeddingWorkflow(config, db);
|
|
|
+ } catch (error) {
|
|
|
+ logger.warn('Qdrant services not available:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getEndpoints(): EndpointMethod[] {
|
|
|
+ return [
|
|
|
+ // Shop Qdrant Settings
|
|
|
+ this.createEndpoint(
|
|
|
+ 'GET',
|
|
|
+ '/api/shops/:shopId/qdrant',
|
|
|
+ this.getShopQdrantStatus.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Get shop Qdrant status',
|
|
|
+ description: 'Get Qdrant embedding status and settings for a shop',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'shopId',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Shop ID'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ responses: {
|
|
|
+ '200': {
|
|
|
+ description: 'Shop Qdrant status',
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ enabled: { type: 'boolean' },
|
|
|
+ custom_id: { type: 'string', nullable: true },
|
|
|
+ collections: { type: 'array', items: { type: 'string' } },
|
|
|
+ embedding_stats: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ total_embeddings: { type: 'number' },
|
|
|
+ completed: { type: 'number' },
|
|
|
+ failed: { type: 'number' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ errors: { type: 'array', items: { type: 'object' } }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ '404': { description: 'Shop not found' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ this.createEndpoint(
|
|
|
+ 'PUT',
|
|
|
+ '/api/shops/:shopId/qdrant/toggle',
|
|
|
+ this.toggleShopQdrant.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Toggle shop Qdrant embedding',
|
|
|
+ description: 'Enable or disable Qdrant embedding for a shop',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'shopId',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Shop ID'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ requestBody: {
|
|
|
+ required: true,
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ enabled: { type: 'boolean' }
|
|
|
+ },
|
|
|
+ required: ['enabled']
|
|
|
+ }
|
|
|
+ },
|
|
|
+ responses: {
|
|
|
+ '200': { description: 'Qdrant status updated' },
|
|
|
+ '404': { description: 'Shop not found' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ this.createEndpoint(
|
|
|
+ 'PUT',
|
|
|
+ '/api/shops/:shopId/qdrant/custom-id',
|
|
|
+ this.updateShopCustomId.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Update shop custom ID',
|
|
|
+ description: 'Set or update the custom ID for Qdrant collection naming',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'shopId',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Shop ID'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ requestBody: {
|
|
|
+ required: true,
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ custom_id: { type: 'string' }
|
|
|
+ },
|
|
|
+ required: ['custom_id']
|
|
|
+ }
|
|
|
+ },
|
|
|
+ responses: {
|
|
|
+ '200': { description: 'Custom ID updated' },
|
|
|
+ '400': { description: 'Invalid custom ID or already in use' },
|
|
|
+ '403': { description: 'Cannot modify existing custom ID' },
|
|
|
+ '404': { description: 'Shop not found' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ // Collection Management
|
|
|
+ this.createEndpoint(
|
|
|
+ 'GET',
|
|
|
+ '/api/qdrant/collections',
|
|
|
+ this.listCollections.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'List Qdrant collections',
|
|
|
+ description: 'List all Qdrant collections with metadata',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ responses: {
|
|
|
+ '200': {
|
|
|
+ description: 'List of collections',
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ collections: {
|
|
|
+ type: 'array',
|
|
|
+ items: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ name: { type: 'string' },
|
|
|
+ vectors_count: { type: 'number' },
|
|
|
+ config: { type: 'object' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ this.createEndpoint(
|
|
|
+ 'DELETE',
|
|
|
+ '/api/qdrant/collections/:collectionName',
|
|
|
+ this.deleteCollection.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Delete Qdrant collection',
|
|
|
+ description: 'Delete a specific Qdrant collection',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'collectionName',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Collection name to delete'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ responses: {
|
|
|
+ '200': { description: 'Collection deleted' },
|
|
|
+ '404': { description: 'Collection not found' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ // Health Check
|
|
|
+ this.createEndpoint(
|
|
|
+ 'GET',
|
|
|
+ '/api/qdrant/health',
|
|
|
+ this.healthCheck.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Qdrant health check',
|
|
|
+ description: 'Check the health of Qdrant and embedding services',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ responses: {
|
|
|
+ '200': {
|
|
|
+ description: 'Health status',
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ healthy: { type: 'boolean' },
|
|
|
+ services: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ qdrant: { type: 'boolean' },
|
|
|
+ embedding: { type: 'boolean' },
|
|
|
+ database: { type: 'boolean' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ errors: { type: 'array', items: { type: 'string' } }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ // Embedding Management
|
|
|
+ this.createEndpoint(
|
|
|
+ 'POST',
|
|
|
+ '/api/shops/:shopId/qdrant/re-embed',
|
|
|
+ this.reEmbedShopContent.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Re-embed shop content',
|
|
|
+ description: 'Trigger re-embedding of all content for a shop',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'shopId',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Shop ID'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ requestBody: {
|
|
|
+ required: false,
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ content_type: {
|
|
|
+ type: 'string',
|
|
|
+ enum: ['shipping', 'contacts', 'terms', 'faq'],
|
|
|
+ description: 'Optional: re-embed only specific content type'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ responses: {
|
|
|
+ '202': { description: 'Re-embedding started' },
|
|
|
+ '404': { description: 'Shop not found' },
|
|
|
+ '400': { description: 'Qdrant not enabled for shop' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ // Error Management
|
|
|
+ this.createEndpoint(
|
|
|
+ 'GET',
|
|
|
+ '/api/shops/:shopId/qdrant/errors',
|
|
|
+ this.getShopQdrantErrors.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Get shop Qdrant errors',
|
|
|
+ description: 'Get recent Qdrant errors for a shop',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'shopId',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Shop ID'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'limit',
|
|
|
+ in: 'query',
|
|
|
+ type: 'number',
|
|
|
+ required: false,
|
|
|
+ description: 'Maximum number of errors to return (default: 50)'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ responses: {
|
|
|
+ '200': {
|
|
|
+ description: 'List of Qdrant errors',
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ errors: {
|
|
|
+ type: 'array',
|
|
|
+ items: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ id: { type: 'string' },
|
|
|
+ operation_type: { type: 'string' },
|
|
|
+ error_message: { type: 'string' },
|
|
|
+ metadata: { type: 'object' },
|
|
|
+ created_at: { type: 'string' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
+
|
|
|
+ this.createEndpoint(
|
|
|
+ 'DELETE',
|
|
|
+ '/api/shops/:shopId/qdrant/errors',
|
|
|
+ this.clearShopQdrantErrors.bind(this),
|
|
|
+ {
|
|
|
+ summary: 'Clear shop Qdrant errors',
|
|
|
+ description: 'Clear all Qdrant errors for a shop',
|
|
|
+ tags: ['Qdrant'],
|
|
|
+ requiresAuth: true,
|
|
|
+ parameters: [
|
|
|
+ {
|
|
|
+ name: 'shopId',
|
|
|
+ in: 'path',
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ description: 'Shop ID'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ responses: {
|
|
|
+ '200': { description: 'Errors cleared' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private async getShopQdrantStatus(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { shopId } = req.params;
|
|
|
+
|
|
|
+ const shop = this.db.getShopById(shopId);
|
|
|
+ if (!shop) {
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get collections for this shop
|
|
|
+ let collections: string[] = [];
|
|
|
+ if (this.qdrantService && shop.custom_id) {
|
|
|
+ try {
|
|
|
+ const allCollections = await this.qdrantService.listCollections();
|
|
|
+ collections = allCollections.filter(name => name.startsWith(shop.custom_id!));
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to list collections:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get embedding stats
|
|
|
+ const embeddings = this.db.getShopEmbeddingsStats(shopId);
|
|
|
+
|
|
|
+ // Get recent errors
|
|
|
+ const errors = this.db.getQdrantErrors(shopId, { limit: 10 });
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ enabled: shop.qdrant_enabled,
|
|
|
+ custom_id: shop.custom_id,
|
|
|
+ collections,
|
|
|
+ embedding_stats: embeddings,
|
|
|
+ errors
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to get shop Qdrant status:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async toggleShopQdrant(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { shopId } = req.params;
|
|
|
+ const { enabled } = req.body;
|
|
|
+
|
|
|
+ const shop = this.db.getShopById(shopId);
|
|
|
+ if (!shop) {
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.db.toggleShopQdrant(shopId, enabled);
|
|
|
+
|
|
|
+ res.json({ message: 'Qdrant status updated' });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to toggle shop Qdrant:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async updateShopCustomId(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { shopId } = req.params;
|
|
|
+ const { custom_id } = req.body;
|
|
|
+
|
|
|
+ // Validate custom_id format (alphanumeric, dashes, underscores)
|
|
|
+ if (!/^[a-zA-Z0-9_-]+$/.test(custom_id)) {
|
|
|
+ res.status(400).json({ error: 'Custom ID must contain only alphanumeric characters, dashes, and underscores' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if custom_id is already in use
|
|
|
+ if (this.db.isCustomIdInUse(custom_id, shopId)) {
|
|
|
+ res.status(400).json({ error: 'Custom ID already in use' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.db.updateShopCustomId(shopId, custom_id);
|
|
|
+ res.json({ message: 'Custom ID updated' });
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error.message.includes('Cannot modify existing custom_id')) {
|
|
|
+ res.status(403).json({ error: error.message });
|
|
|
+ } else if (error.message.includes('Shop not found')) {
|
|
|
+ res.status(404).json({ error: error.message });
|
|
|
+ } else {
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to update shop custom ID:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async listCollections(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ if (!this.qdrantService) {
|
|
|
+ res.status(503).json({ error: 'Qdrant service not available' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const collections = await this.qdrantService.listCollections();
|
|
|
+ const collectionsWithInfo = [];
|
|
|
+
|
|
|
+ for (const name of collections) {
|
|
|
+ try {
|
|
|
+ const info = await this.qdrantService.getCollectionInfo(name);
|
|
|
+ collectionsWithInfo.push({
|
|
|
+ name,
|
|
|
+ vectors_count: info.vectors_count,
|
|
|
+ config: info.config
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error(`Failed to get info for collection ${name}:`, error);
|
|
|
+ collectionsWithInfo.push({
|
|
|
+ name,
|
|
|
+ vectors_count: 0,
|
|
|
+ config: {}
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ res.json({ collections: collectionsWithInfo });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to list collections:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async deleteCollection(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { collectionName } = req.params;
|
|
|
+
|
|
|
+ if (!this.qdrantService) {
|
|
|
+ res.status(503).json({ error: 'Qdrant service not available' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const exists = await this.qdrantService.collectionExists(collectionName);
|
|
|
+ if (!exists) {
|
|
|
+ res.status(404).json({ error: 'Collection not found' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.qdrantService.deleteCollection(collectionName);
|
|
|
+
|
|
|
+ // Update database records
|
|
|
+ this.db.clearEmbeddingsByCollection(collectionName);
|
|
|
+
|
|
|
+ res.json({ message: 'Collection deleted' });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to delete collection:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async healthCheck(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ if (!this.embeddingWorkflow) {
|
|
|
+ res.json({
|
|
|
+ healthy: false,
|
|
|
+ services: { qdrant: false, embedding: false, database: false },
|
|
|
+ errors: ['Embedding workflow not available']
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const health = await this.embeddingWorkflow.healthCheck();
|
|
|
+ res.json(health);
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Health check failed:', error);
|
|
|
+ res.status(500).json({
|
|
|
+ healthy: false,
|
|
|
+ services: { qdrant: false, embedding: false, database: false },
|
|
|
+ errors: ['Health check failed']
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async reEmbedShopContent(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { shopId } = req.params;
|
|
|
+ const { content_type } = req.body || {};
|
|
|
+
|
|
|
+ const shop = this.db.getShopById(shopId);
|
|
|
+ if (!shop) {
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!shop.qdrant_enabled) {
|
|
|
+ res.status(400).json({ error: 'Qdrant not enabled for this shop' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get content to re-embed
|
|
|
+ const content = this.db.getShopContent(shopId, { contentType: content_type });
|
|
|
+
|
|
|
+ if (content.length === 0) {
|
|
|
+ res.status(200).json({ message: 'No content to re-embed' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Start re-embedding process
|
|
|
+ // Note: In a real system, this should be queued for background processing
|
|
|
+ logger.info(`Re-embedding ${content.length} content items for shop ${shopId}`);
|
|
|
+
|
|
|
+ res.status(202).json({
|
|
|
+ message: `Re-embedding started for ${content.length} content items`
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to start re-embedding:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async getShopQdrantErrors(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { shopId } = req.params;
|
|
|
+ const limit = parseInt(req.query.limit as string) || 50;
|
|
|
+
|
|
|
+ const errors = this.db.getQdrantErrors(shopId, { limit });
|
|
|
+
|
|
|
+ res.json({ errors });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to get shop Qdrant errors:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async clearShopQdrantErrors(req: Request, res: Response): Promise<void> {
|
|
|
+ try {
|
|
|
+ const { shopId } = req.params;
|
|
|
+
|
|
|
+ this.db.clearQdrantErrors(shopId);
|
|
|
+
|
|
|
+ res.json({ message: 'Errors cleared' });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to clear shop Qdrant errors:', error);
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|