فهرست منبع

fix: enable Qdrant sync and add manual sync feature

- Fix Qdrant embedding workflow initialization by passing config to WebshopScraper
- Add POST /api/shops/:shopId/qdrant/sync endpoint for manual Qdrant sync
- Add "Sync to Qdrant" button to shop details page in web UI
- Button only shows when Qdrant is enabled and shop has custom_id
- Sync processes all content types in background with progress logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 8 ماه پیش
والد
کامیت
747bed57bb
4فایلهای تغییر یافته به همراه209 افزوده شده و 2 حذف شده
  1. 156 0
      src/api/components/QdrantEndpoint.ts
  2. 2 2
      src/index.ts
  3. 45 0
      web/src/components/pages/ShopDetailPage.ts
  4. 6 0
      web/src/services/ApiService.ts

+ 156 - 0
src/api/components/QdrantEndpoint.ts

@@ -452,6 +452,43 @@ export class QdrantEndpoint extends BaseEndpointComponent {
         }
         }
       ),
       ),
 
 
+      this.createEndpoint(
+        'POST',
+        '/api/shops/:shopId/qdrant/sync',
+        this.forceQdrantSync.bind(this),
+        {
+          summary: 'Force Qdrant sync',
+          description: 'Force synchronization of all shop content to Qdrant',
+          tags: ['Qdrant'],
+          requiresAuth: true,
+          parameters: [
+            {
+              name: 'shopId',
+              in: 'path',
+              type: 'string',
+              required: true,
+              description: 'Shop ID (internal UUID or custom UUID)'
+            }
+          ],
+          responses: {
+            '202': {
+              description: 'Sync started',
+              schema: {
+                type: 'object',
+                properties: {
+                  message: { type: 'string' },
+                  items_queued: { type: 'number' },
+                  shop_id: { type: 'string' }
+                }
+              }
+            },
+            '404': { description: 'Shop not found' },
+            '400': { description: 'Qdrant not enabled for shop or no custom_id set' },
+            '503': { description: 'Embedding workflow not available' }
+          }
+        }
+      ),
+
       // Error Management
       // Error Management
       this.createEndpoint(
       this.createEndpoint(
         'GET',
         'GET',
@@ -1005,4 +1042,123 @@ export class QdrantEndpoint extends BaseEndpointComponent {
       res.status(500).json({ error: 'Internal server error' });
       res.status(500).json({ error: 'Internal server error' });
     }
     }
   }
   }
+
+  private async forceQdrantSync(req: Request, res: Response): Promise<void> {
+    try {
+      const { shopId } = req.params;
+
+      // Get shop from database - try both internal ID and custom ID
+      let shop = this.db.getShopByAnyId(shopId);
+      if (!shop) {
+        res.status(404).json({ error: 'Shop not found' });
+        return;
+      }
+
+      // Check if Qdrant is enabled
+      if (!shop.qdrant_enabled) {
+        res.status(400).json({ error: 'Qdrant is not enabled for this shop' });
+        return;
+      }
+
+      // Check if shop has custom_id
+      if (!shop.custom_id) {
+        res.status(400).json({ error: 'Shop must have a custom_id to sync with Qdrant' });
+        return;
+      }
+
+      // Check if embedding workflow is available
+      if (!this.embeddingWorkflow) {
+        res.status(503).json({ error: 'Embedding workflow not available' });
+        return;
+      }
+
+      // Get all latest content for this shop
+      const latestContent = this.db.getLatestContentByType(shop.id);
+
+      // Prepare batch of content items to sync
+      const embeddingBatch: any[] = [];
+
+      // Process all content types
+      for (const content of latestContent.shipping) {
+        embeddingBatch.push({
+          contentId: content.id,
+          contentType: 'shipping' as const,
+          url: content.url,
+          content: content.content,
+          title: content.title,
+          contentHash: content.content_hash,
+          contentChanged: true // Force re-sync
+        });
+      }
+
+      for (const content of latestContent.contacts) {
+        embeddingBatch.push({
+          contentId: content.id,
+          contentType: 'contacts' as const,
+          url: content.url,
+          content: content.content,
+          title: content.title,
+          contentHash: content.content_hash,
+          contentChanged: true
+        });
+      }
+
+      for (const content of latestContent.terms) {
+        embeddingBatch.push({
+          contentId: content.id,
+          contentType: 'terms' as const,
+          url: content.url,
+          content: content.content,
+          title: content.title,
+          contentHash: content.content_hash,
+          contentChanged: true
+        });
+      }
+
+      for (const content of latestContent.faq) {
+        embeddingBatch.push({
+          contentId: content.id,
+          contentType: 'faq' as const,
+          url: content.url,
+          content: content.content,
+          title: content.title,
+          contentHash: content.content_hash,
+          contentChanged: true
+        });
+      }
+
+      if (embeddingBatch.length === 0) {
+        res.status(200).json({
+          message: 'No content available to sync',
+          items_queued: 0,
+          shop_id: shop.id
+        });
+        return;
+      }
+
+      // Start the sync process in background
+      logger.info(`Force syncing ${embeddingBatch.length} items to Qdrant for shop ${shop.id}`);
+
+      // Process the batch asynchronously (don't await)
+      this.embeddingWorkflow.processContentBatch(shop.id, embeddingBatch)
+        .then(results => {
+          const successCount = results.filter(r => r.embedded).length;
+          const failCount = results.filter(r => !r.embedded).length;
+          logger.info(`Qdrant sync completed for shop ${shop.id}: ${successCount} success, ${failCount} failed`);
+        })
+        .catch(error => {
+          logger.error(`Qdrant sync failed for shop ${shop.id}:`, error);
+        });
+
+      res.status(202).json({
+        message: 'Qdrant sync started',
+        items_queued: embeddingBatch.length,
+        shop_id: shop.id
+      });
+
+    } catch (error) {
+      logger.error('Failed to force Qdrant sync:', error);
+      res.status(500).json({ error: 'Internal server error' });
+    }
+  }
 }
 }

+ 2 - 2
src/index.ts

@@ -20,8 +20,8 @@ async function main() {
     // Initialize job queue
     // Initialize job queue
     const jobQueue = new JobQueue(config.maxConcurrentJobs);
     const jobQueue = new JobQueue(config.maxConcurrentJobs);
 
 
-    // Initialize scraper with database
-    const scraper = new WebshopScraper(db);
+    // Initialize scraper with database and config
+    const scraper = new WebshopScraper(db, config);
 
 
     // Initialize scheduler
     // Initialize scheduler
     const scheduler = new ScrapeScheduler(db, jobQueue);
     const scheduler = new ScrapeScheduler(db, jobQueue);

+ 45 - 0
web/src/components/pages/ShopDetailPage.ts

@@ -124,6 +124,12 @@ export class ShopDetailPage {
             <div class="w-4 h-4 mr-2" id="scrape-icon">${getIcon('play')}</div>
             <div class="w-4 h-4 mr-2" id="scrape-icon">${getIcon('play')}</div>
             <span id="scrape-text">Run Scrape</span>
             <span id="scrape-text">Run Scrape</span>
           </button>
           </button>
+          ${shop.qdrant_enabled && shop.custom_id ? `
+            <button type="button" class="btn btn-primary" id="trigger-qdrant-sync">
+              <div class="w-4 h-4 mr-2" id="sync-icon">${getIcon('search')}</div>
+              <span id="sync-text">Sync to Qdrant</span>
+            </button>
+          ` : ''}
           <button type="button" class="btn btn-danger" id="delete-shop">
           <button type="button" class="btn btn-danger" id="delete-shop">
             <div class="w-4 h-4 mr-2">${getIcon('delete')}</div>
             <div class="w-4 h-4 mr-2">${getIcon('delete')}</div>
             Delete Shop
             Delete Shop
@@ -383,6 +389,45 @@ export class ShopDetailPage {
       }
       }
     });
     });
 
 
+    // Trigger Qdrant sync
+    this.element.querySelector('#trigger-qdrant-sync')?.addEventListener('click', async (e) => {
+      if (!this.shopDetail) return;
+
+      const button = e.currentTarget as HTMLButtonElement;
+      const icon = this.element?.querySelector('#sync-icon');
+      const text = this.element?.querySelector('#sync-text');
+
+      // Prevent multiple clicks
+      if (button.disabled) return;
+
+      // Set loading state
+      button.disabled = true;
+      if (icon) icon.innerHTML = getIcon('loading');
+      if (text) text.textContent = 'Syncing...';
+
+      try {
+        const response = await this.apiService.forceQdrantSync(this.shopId);
+
+        if (response.success && response.data) {
+          this.toastService.success(
+            'Sync Started',
+            `Syncing ${response.data.items_queued} items to Qdrant`
+          );
+          // Reload the page after a short delay to show updated status
+          setTimeout(() => this.loadShopDetail(), 2000);
+        } else {
+          this.toastService.error('Sync Failed', response.error || 'Failed to start Qdrant sync');
+        }
+      } catch (error) {
+        this.toastService.error('Error', 'Failed to start Qdrant sync');
+      } finally {
+        // Reset button state
+        button.disabled = false;
+        if (icon) icon.innerHTML = getIcon('search');
+        if (text) text.textContent = 'Sync to Qdrant';
+      }
+    });
+
     // Delete shop
     // Delete shop
     this.element.querySelector('#delete-shop')?.addEventListener('click', async () => {
     this.element.querySelector('#delete-shop')?.addEventListener('click', async () => {
       const confirmed = confirm('Are you sure you want to delete this shop? This will remove all associated data and cannot be undone.');
       const confirmed = confirm('Are you sure you want to delete this shop? This will remove all associated data and cannot be undone.');

+ 6 - 0
web/src/services/ApiService.ts

@@ -274,6 +274,12 @@ export class ApiService {
     });
     });
   }
   }
 
 
+  async forceQdrantSync(shopId: string): Promise<ApiResponse<{ message: string; items_queued: number; shop_id: string }>> {
+    return this.request(`/api/shops/${shopId}/qdrant/sync`, {
+      method: 'POST',
+    });
+  }
+
   // MCP Management
   // MCP Management
   async getMcpStats(): Promise<ApiResponse<McpStats>> {
   async getMcpStats(): Promise<ApiResponse<McpStats>> {
     return this.request('/api/mcp/stats');
     return this.request('/api/mcp/stats');