Changeset 3763005
- Timestamp:
- 10/24/2026 09:26:11 AM (6 hours ago)
- Location:
- agentmatica-mcp-connector
- Files:
-
- 10 edited
- 1 copied
-
tags/0.7.2 (copied) (copied from agentmatica-mcp-connector/trunk )
-
tags/0.7.2/agentmatica.php (modified) (2 diffs)
-
tags/0.7.2/includes/class-ucp-catalog.php (modified) (7 diffs)
-
tags/0.7.2/includes/modules/woocommerce/class-cart.php (modified) (1 diff)
-
tags/0.7.2/includes/modules/woocommerce/class-catalog-search.php (modified) (1 diff)
-
tags/0.7.2/readme.txt (modified) (3 diffs)
-
trunk/agentmatica.php (modified) (2 diffs)
-
trunk/includes/class-ucp-catalog.php (modified) (7 diffs)
-
trunk/includes/modules/woocommerce/class-cart.php (modified) (1 diff)
-
trunk/includes/modules/woocommerce/class-catalog-search.php (modified) (1 diff)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
agentmatica-mcp-connector/tags/0.7.2/agentmatica.php
r3736588 r3763005 4 4 * Plugin URI: https://agentmatica.com/docs/integrations/wordpress 5 5 * Description: Free MCP connector for Agentmatica.com. Expose WordPress content and optional WooCommerce tools to Agentmatica visitor chat. 6 * Version: 0.7. 16 * Version: 0.7.2 7 7 * Requires at least: 6.9 8 * Requires PHP: 7.48 * Requires PHP: 8.0 9 9 * Author: Thought Bulb 10 10 * Author URI: https://thoughtbulb.dev … … 23 23 } 24 24 25 define( 'AGENTMATICA_VERSION', '0.7. 1' );25 define( 'AGENTMATICA_VERSION', '0.7.2' ); 26 26 define( 'AGENTMATICA_PLUGIN_FILE', __FILE__ ); 27 27 define( 'AGENTMATICA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
agentmatica-mcp-connector/tags/0.7.2/includes/class-ucp-catalog.php
r3736423 r3763005 44 44 45 45 if ( $query !== '' ) { 46 $products = self::query_published_products_by_search( $query, $limit ); 46 $found = self::query_published_products_by_search( $query, $limit ); 47 $products = $found['products']; 48 $total = $found['total']; 47 49 } else { 48 $products = self::query_all_published_products( $limit ); 49 } 50 51 $total_count = self::count_catalog_products(); 52 $payload = array(); 50 $found = self::query_all_published_products( $limit ); 51 $products = $found['products']; 52 $total = $found['total']; 53 } 54 55 $payload = array(); 53 56 54 57 foreach ( $products as $product ) { … … 73 76 } 74 77 75 return self::catalog_response( $payload, $total _count, $limit );78 return self::catalog_response( $payload, $total, $limit ); 76 79 } 77 80 … … 166 169 : $arguments; 167 170 $raw_id = isset( $catalog['id'] ) ? (string) $catalog['id'] : ''; 171 if ( $raw_id === '' && isset( $arguments['id'] ) ) { 172 $raw_id = (string) $arguments['id']; 173 } 168 174 169 175 $product = self::load_product_by_identifier( $raw_id ); 170 176 if ( ! $product ) { 177 $name = ''; 178 if ( isset( $catalog['name'] ) ) { 179 $name = sanitize_text_field( (string) $catalog['name'] ); 180 } elseif ( isset( $arguments['name'] ) ) { 181 $name = sanitize_text_field( (string) $arguments['name'] ); 182 } elseif ( isset( $catalog['query'] ) ) { 183 $name = sanitize_text_field( (string) $catalog['query'] ); 184 } 185 186 if ( $name !== '' ) { 187 return self::search_catalog( 188 array( 189 'query' => $name, 190 'pagination' => array( 'limit' => 5 ), 191 ) 192 ); 193 } 194 171 195 return self::catalog_response( array() ); 172 196 } … … 260 284 } 261 285 262 private static function count_catalog_products(): int { 263 if ( ! function_exists( 'wc_get_products' ) ) { 286 /** 287 * Published product count from post counts (no product hydration). 288 */ 289 private static function count_published_products(): int { 290 $counts = wp_count_posts( 'product' ); 291 if ( ! is_object( $counts ) || ! isset( $counts->publish ) ) { 264 292 return 0; 265 293 } 266 294 267 $count = 0; 268 foreach ( self::load_published_product_objects( -1 ) as $product ) { 269 if ( self::is_catalog_available( $product ) && self::format_product( $product ) ) { 270 ++$count; 271 } 272 } 273 274 return $count; 295 return (int) $counts->publish; 275 296 } 276 297 … … 308 329 * Load published products for catalog browse (available to buy only). 309 330 * 310 * @return list<WC_Product>331 * @return array{products: list<WC_Product>, total: int} 311 332 */ 312 333 private static function query_all_published_products( int $limit ): array { 334 $query = new WP_Query( 335 array( 336 'post_type' => 'product', 337 'post_status' => 'publish', 338 'posts_per_page' => $limit, 339 'paged' => 1, 340 'orderby' => 'date', 341 'order' => 'DESC', 342 'fields' => 'ids', 343 'no_found_rows' => false, 344 'ignore_sticky_posts' => true, 345 'update_post_meta_cache' => false, 346 'update_post_term_cache' => false, 347 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- instock browse only, paged. 348 array( 349 'key' => '_stock_status', 350 'value' => 'instock', 351 ), 352 ), 353 ) 354 ); 355 313 356 $products = array(); 314 foreach ( self::load_published_product_objects( -1 ) as $product ) { 357 foreach ( $query->posts as $product_id ) { 358 $product = wc_get_product( (int) $product_id ); 359 if ( ! $product instanceof WC_Product ) { 360 continue; 361 } 315 362 if ( ! self::is_catalog_available( $product ) ) { 316 363 continue; 317 364 } 318 319 365 $products[] = $product; 320 if ( count( $products ) >= $limit ) { 321 break; 322 } 323 } 324 325 return $products; 326 } 327 328 /** 329 * @return list<WC_Product> 330 */ 331 private static function load_published_product_objects( int $limit ): array { 332 $ids = get_posts( 333 array( 334 'post_type' => 'product', 335 'post_status' => 'publish', 336 'posts_per_page' => $limit, 337 'orderby' => 'date', 338 'order' => 'DESC', 339 'fields' => 'ids', 340 ) 341 ); 342 343 if ( ! is_array( $ids ) ) { 344 return array(); 345 } 346 347 $products = array(); 348 foreach ( $ids as $product_id ) { 349 $product = wc_get_product( (int) $product_id ); 350 if ( $product instanceof WC_Product ) { 351 $products[] = $product; 352 } 353 } 354 355 return $products; 356 } 357 358 /** 359 * Named search — may include unavailable matches so the agent can explain stock status. 366 } 367 368 $total = (int) $query->found_posts; 369 if ( $total < 1 ) { 370 $total = self::count_published_products(); 371 } 372 373 return array( 374 'products' => $products, 375 'total' => $total, 376 ); 377 } 378 379 /** 380 * Named search — SQL/Woo search plus a capped PHP re-rank. Never hydrates the full catalogue. 360 381 * 361 * @return list<WC_Product>382 * @return array{products: list<WC_Product>, total: int} 362 383 */ 363 384 private static function query_published_products_by_search( string $search, int $limit ): array { 364 385 $normalized = strtolower( trim( preg_replace( '/\s+/', ' ', $search ) ) ); 365 386 if ( $normalized === '' ) { 366 return array(); 367 } 387 return array( 388 'products' => array(), 389 'total' => 0, 390 ); 391 } 392 393 $candidate_limit = min( 100, max( $limit * 5, 40 ) ); 394 $found = self::search_published_product_ids( $normalized, $candidate_limit ); 395 $ids = $found['ids']; 396 $total = $found['total']; 368 397 369 398 $scored = array(); 370 371 foreach ( self::load_published_product_objects( -1 ) as $product ) { 399 foreach ( $ids as $product_id ) { 400 $product = wc_get_product( $product_id ); 401 if ( ! $product instanceof WC_Product ) { 402 continue; 403 } 404 372 405 $score = Agentmatica_Catalog_Search::product_match_score( $product, $search ); 373 406 if ( $score <= 0 ) { 374 continue;407 $score = 10; 375 408 } 376 409 377 410 $scored[] = array( 378 411 'score' => $score, 412 'id' => $product_id, 379 413 'product' => $product, 380 414 ); … … 384 418 $scored, 385 419 static function ( array $left, array $right ): int { 386 return $right['score'] <=> $left['score']; 420 $cmp = $right['score'] <=> $left['score']; 421 if ( 0 !== $cmp ) { 422 return $cmp; 423 } 424 425 return $left['id'] <=> $right['id']; 387 426 } 388 427 ); … … 396 435 } 397 436 398 return $products; 437 if ( $total < count( $products ) ) { 438 $total = count( $products ); 439 } 440 441 return array( 442 'products' => $products, 443 'total' => $total, 444 ); 445 } 446 447 /** 448 * @return array{ids: list<int>, total: int} 449 */ 450 private static function search_published_product_ids( string $search, int $limit ): array { 451 $ids = array(); 452 $total = 0; 453 454 $sku_id = function_exists( 'wc_get_product_id_by_sku' ) ? wc_get_product_id_by_sku( $search ) : 0; 455 if ( $sku_id ) { 456 $sku_product = wc_get_product( $sku_id ); 457 if ( $sku_product instanceof WC_Product_Variation ) { 458 $sku_id = (int) $sku_product->get_parent_id(); 459 } 460 // wc_get_product_id_by_sku() matches drafts and private products too. 461 if ( $sku_id > 0 && get_post_status( $sku_id ) === 'publish' ) { 462 $ids[] = $sku_id; 463 } 464 } 465 466 $query = new WP_Query( 467 array( 468 'post_type' => 'product', 469 'post_status' => 'publish', 470 'posts_per_page' => $limit, 471 's' => $search, 472 'fields' => 'ids', 473 'no_found_rows' => false, 474 'ignore_sticky_posts' => true, 475 'update_post_meta_cache' => false, 476 'update_post_term_cache' => false, 477 ) 478 ); 479 480 foreach ( $query->posts as $product_id ) { 481 $ids[] = (int) $product_id; 482 } 483 $total = max( $total, (int) $query->found_posts, count( $ids ) ); 484 485 $title_found = self::search_product_ids_by_title_tokens( $search, $limit ); 486 $ids = array_merge( $title_found['ids'], $ids ); 487 $total = max( $total, $title_found['total'] ); 488 489 $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) ); 490 491 return array( 492 'ids' => $ids, 493 'total' => $total, 494 ); 495 } 496 497 /** 498 * Title AND-match for all query tokens (older fixtures WP `s=` ranking can drop). 499 * 500 * @return array{ids: list<int>, total: int} 501 */ 502 private static function search_product_ids_by_title_tokens( string $search, int $limit ): array { 503 global $wpdb; 504 505 $words = preg_split( '/\s+/', $search ); 506 $tokens = array(); 507 foreach ( false === $words ? array() : $words as $word ) { 508 $word = trim( (string) $word ); 509 if ( strlen( $word ) < 3 ) { 510 continue; 511 } 512 $tokens[] = $word; 513 } 514 $tokens = array_values( array_unique( $tokens ) ); 515 if ( $tokens === array() ) { 516 return array( 517 'ids' => array(), 518 'total' => 0, 519 ); 520 } 521 522 $posts = $wpdb->posts; 523 $conditions = array( "post_type = 'product'", "post_status = 'publish'" ); 524 $values = array(); 525 foreach ( $tokens as $token ) { 526 $conditions[] = 'post_title LIKE %s'; 527 $values[] = '%' . $wpdb->esc_like( $token ) . '%'; 528 } 529 530 $where = implode( ' AND ', $conditions ); 531 $values[] = $limit; 532 533 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- `$posts` is `$wpdb->posts`; `$where` is LIKE %s placeholders. 534 $ids = $wpdb->get_col( 535 $wpdb->prepare( 536 "SELECT ID FROM {$posts} WHERE {$where} ORDER BY ID ASC LIMIT %d", 537 ...$values 538 ) 539 ); 540 541 $count_values = array_slice( $values, 0, -1 ); 542 $count = $wpdb->get_var( 543 $wpdb->prepare( 544 "SELECT COUNT(ID) FROM {$posts} WHERE {$where}", 545 ...$count_values 546 ) 547 ); 548 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare 549 550 return array( 551 'ids' => array_map( 'intval', is_array( $ids ) ? $ids : array() ), 552 'total' => (int) $count, 553 ); 399 554 } 400 555 -
agentmatica-mcp-connector/tags/0.7.2/includes/modules/woocommerce/class-cart.php
r3736423 r3763005 20 20 21 21 public static function init(): void { 22 add_action( ' init', array( __CLASS__, 'register_checkout_redirect_route' ) );23 add_action( ' init', array( __CLASS__, 'register_sync_storefront_cart_route' ) );24 add_action( ' init', array( __CLASS__, 'register_read_storefront_cart_route' ) );22 add_action( 'rest_api_init', array( __CLASS__, 'register_checkout_redirect_route' ) ); 23 add_action( 'rest_api_init', array( __CLASS__, 'register_sync_storefront_cart_route' ) ); 24 add_action( 'rest_api_init', array( __CLASS__, 'register_read_storefront_cart_route' ) ); 25 25 add_action( 'template_redirect', array( __CLASS__, 'maybe_handle_checkout_redirect' ) ); 26 26 } -
agentmatica-mcp-connector/tags/0.7.2/includes/modules/woocommerce/class-catalog-search.php
r3736423 r3763005 123 123 if ( str_contains( $title, $normalized ) ) { 124 124 return 90; 125 } 126 127 $words = preg_split( '/\s+/', $normalized ); 128 $words = false === $words ? array() : array_values( 129 array_filter( 130 $words, 131 static function ( string $word ): bool { 132 return strlen( $word ) >= 2; 133 } 134 ) 135 ); 136 if ( $words !== array() ) { 137 $title_hits = 0; 138 foreach ( $words as $word ) { 139 if ( self::word_matches_haystack( $word, $title ) ) { 140 ++$title_hits; 141 } 142 } 143 if ( $title_hits === count( $words ) ) { 144 return 85; 145 } 125 146 } 126 147 -
agentmatica-mcp-connector/tags/0.7.2/readme.txt
r3736588 r3763005 4 4 Requires at least: 6.9 5 5 Tested up to: 7.1 6 Requires PHP: 7.47 Stable tag: 0.7. 16 Requires PHP: 8.0 7 Stable tag: 0.7.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 97 97 == Changelog == 98 98 99 = 0.7.2 = 100 * WooCommerce `search_catalog` no longer loads the entire catalogue on each chat search (SQL + capped re-rank). Fixes timeouts on large shops. 101 * `get_product` can resolve by product name when `id` is missing. 102 * Checkout and storefront-cart REST routes register on `rest_api_init`. 103 * An exact SKU search only returns published products. 104 * Requires PHP 8.0. Earlier versions already needed it to load. 105 99 106 = 0.7.1 = 100 107 * Fixed: order attribution was sent to an address that does not exist, so sales made through the chat were not counted in Agentmatica Insights. … … 134 141 == Upgrade Notice == 135 142 143 = 0.7.2 = 144 Woo catalog search no longer times out on large inventories. Update recommended for Agentmatica chat. 145 136 146 = 0.7.1 = 137 147 Fixes order attribution so chat sales appear in Insights. Update recommended. -
agentmatica-mcp-connector/trunk/agentmatica.php
r3736588 r3763005 4 4 * Plugin URI: https://agentmatica.com/docs/integrations/wordpress 5 5 * Description: Free MCP connector for Agentmatica.com. Expose WordPress content and optional WooCommerce tools to Agentmatica visitor chat. 6 * Version: 0.7. 16 * Version: 0.7.2 7 7 * Requires at least: 6.9 8 * Requires PHP: 7.48 * Requires PHP: 8.0 9 9 * Author: Thought Bulb 10 10 * Author URI: https://thoughtbulb.dev … … 23 23 } 24 24 25 define( 'AGENTMATICA_VERSION', '0.7. 1' );25 define( 'AGENTMATICA_VERSION', '0.7.2' ); 26 26 define( 'AGENTMATICA_PLUGIN_FILE', __FILE__ ); 27 27 define( 'AGENTMATICA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
agentmatica-mcp-connector/trunk/includes/class-ucp-catalog.php
r3736423 r3763005 44 44 45 45 if ( $query !== '' ) { 46 $products = self::query_published_products_by_search( $query, $limit ); 46 $found = self::query_published_products_by_search( $query, $limit ); 47 $products = $found['products']; 48 $total = $found['total']; 47 49 } else { 48 $products = self::query_all_published_products( $limit ); 49 } 50 51 $total_count = self::count_catalog_products(); 52 $payload = array(); 50 $found = self::query_all_published_products( $limit ); 51 $products = $found['products']; 52 $total = $found['total']; 53 } 54 55 $payload = array(); 53 56 54 57 foreach ( $products as $product ) { … … 73 76 } 74 77 75 return self::catalog_response( $payload, $total _count, $limit );78 return self::catalog_response( $payload, $total, $limit ); 76 79 } 77 80 … … 166 169 : $arguments; 167 170 $raw_id = isset( $catalog['id'] ) ? (string) $catalog['id'] : ''; 171 if ( $raw_id === '' && isset( $arguments['id'] ) ) { 172 $raw_id = (string) $arguments['id']; 173 } 168 174 169 175 $product = self::load_product_by_identifier( $raw_id ); 170 176 if ( ! $product ) { 177 $name = ''; 178 if ( isset( $catalog['name'] ) ) { 179 $name = sanitize_text_field( (string) $catalog['name'] ); 180 } elseif ( isset( $arguments['name'] ) ) { 181 $name = sanitize_text_field( (string) $arguments['name'] ); 182 } elseif ( isset( $catalog['query'] ) ) { 183 $name = sanitize_text_field( (string) $catalog['query'] ); 184 } 185 186 if ( $name !== '' ) { 187 return self::search_catalog( 188 array( 189 'query' => $name, 190 'pagination' => array( 'limit' => 5 ), 191 ) 192 ); 193 } 194 171 195 return self::catalog_response( array() ); 172 196 } … … 260 284 } 261 285 262 private static function count_catalog_products(): int { 263 if ( ! function_exists( 'wc_get_products' ) ) { 286 /** 287 * Published product count from post counts (no product hydration). 288 */ 289 private static function count_published_products(): int { 290 $counts = wp_count_posts( 'product' ); 291 if ( ! is_object( $counts ) || ! isset( $counts->publish ) ) { 264 292 return 0; 265 293 } 266 294 267 $count = 0; 268 foreach ( self::load_published_product_objects( -1 ) as $product ) { 269 if ( self::is_catalog_available( $product ) && self::format_product( $product ) ) { 270 ++$count; 271 } 272 } 273 274 return $count; 295 return (int) $counts->publish; 275 296 } 276 297 … … 308 329 * Load published products for catalog browse (available to buy only). 309 330 * 310 * @return list<WC_Product>331 * @return array{products: list<WC_Product>, total: int} 311 332 */ 312 333 private static function query_all_published_products( int $limit ): array { 334 $query = new WP_Query( 335 array( 336 'post_type' => 'product', 337 'post_status' => 'publish', 338 'posts_per_page' => $limit, 339 'paged' => 1, 340 'orderby' => 'date', 341 'order' => 'DESC', 342 'fields' => 'ids', 343 'no_found_rows' => false, 344 'ignore_sticky_posts' => true, 345 'update_post_meta_cache' => false, 346 'update_post_term_cache' => false, 347 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- instock browse only, paged. 348 array( 349 'key' => '_stock_status', 350 'value' => 'instock', 351 ), 352 ), 353 ) 354 ); 355 313 356 $products = array(); 314 foreach ( self::load_published_product_objects( -1 ) as $product ) { 357 foreach ( $query->posts as $product_id ) { 358 $product = wc_get_product( (int) $product_id ); 359 if ( ! $product instanceof WC_Product ) { 360 continue; 361 } 315 362 if ( ! self::is_catalog_available( $product ) ) { 316 363 continue; 317 364 } 318 319 365 $products[] = $product; 320 if ( count( $products ) >= $limit ) { 321 break; 322 } 323 } 324 325 return $products; 326 } 327 328 /** 329 * @return list<WC_Product> 330 */ 331 private static function load_published_product_objects( int $limit ): array { 332 $ids = get_posts( 333 array( 334 'post_type' => 'product', 335 'post_status' => 'publish', 336 'posts_per_page' => $limit, 337 'orderby' => 'date', 338 'order' => 'DESC', 339 'fields' => 'ids', 340 ) 341 ); 342 343 if ( ! is_array( $ids ) ) { 344 return array(); 345 } 346 347 $products = array(); 348 foreach ( $ids as $product_id ) { 349 $product = wc_get_product( (int) $product_id ); 350 if ( $product instanceof WC_Product ) { 351 $products[] = $product; 352 } 353 } 354 355 return $products; 356 } 357 358 /** 359 * Named search — may include unavailable matches so the agent can explain stock status. 366 } 367 368 $total = (int) $query->found_posts; 369 if ( $total < 1 ) { 370 $total = self::count_published_products(); 371 } 372 373 return array( 374 'products' => $products, 375 'total' => $total, 376 ); 377 } 378 379 /** 380 * Named search — SQL/Woo search plus a capped PHP re-rank. Never hydrates the full catalogue. 360 381 * 361 * @return list<WC_Product>382 * @return array{products: list<WC_Product>, total: int} 362 383 */ 363 384 private static function query_published_products_by_search( string $search, int $limit ): array { 364 385 $normalized = strtolower( trim( preg_replace( '/\s+/', ' ', $search ) ) ); 365 386 if ( $normalized === '' ) { 366 return array(); 367 } 387 return array( 388 'products' => array(), 389 'total' => 0, 390 ); 391 } 392 393 $candidate_limit = min( 100, max( $limit * 5, 40 ) ); 394 $found = self::search_published_product_ids( $normalized, $candidate_limit ); 395 $ids = $found['ids']; 396 $total = $found['total']; 368 397 369 398 $scored = array(); 370 371 foreach ( self::load_published_product_objects( -1 ) as $product ) { 399 foreach ( $ids as $product_id ) { 400 $product = wc_get_product( $product_id ); 401 if ( ! $product instanceof WC_Product ) { 402 continue; 403 } 404 372 405 $score = Agentmatica_Catalog_Search::product_match_score( $product, $search ); 373 406 if ( $score <= 0 ) { 374 continue;407 $score = 10; 375 408 } 376 409 377 410 $scored[] = array( 378 411 'score' => $score, 412 'id' => $product_id, 379 413 'product' => $product, 380 414 ); … … 384 418 $scored, 385 419 static function ( array $left, array $right ): int { 386 return $right['score'] <=> $left['score']; 420 $cmp = $right['score'] <=> $left['score']; 421 if ( 0 !== $cmp ) { 422 return $cmp; 423 } 424 425 return $left['id'] <=> $right['id']; 387 426 } 388 427 ); … … 396 435 } 397 436 398 return $products; 437 if ( $total < count( $products ) ) { 438 $total = count( $products ); 439 } 440 441 return array( 442 'products' => $products, 443 'total' => $total, 444 ); 445 } 446 447 /** 448 * @return array{ids: list<int>, total: int} 449 */ 450 private static function search_published_product_ids( string $search, int $limit ): array { 451 $ids = array(); 452 $total = 0; 453 454 $sku_id = function_exists( 'wc_get_product_id_by_sku' ) ? wc_get_product_id_by_sku( $search ) : 0; 455 if ( $sku_id ) { 456 $sku_product = wc_get_product( $sku_id ); 457 if ( $sku_product instanceof WC_Product_Variation ) { 458 $sku_id = (int) $sku_product->get_parent_id(); 459 } 460 // wc_get_product_id_by_sku() matches drafts and private products too. 461 if ( $sku_id > 0 && get_post_status( $sku_id ) === 'publish' ) { 462 $ids[] = $sku_id; 463 } 464 } 465 466 $query = new WP_Query( 467 array( 468 'post_type' => 'product', 469 'post_status' => 'publish', 470 'posts_per_page' => $limit, 471 's' => $search, 472 'fields' => 'ids', 473 'no_found_rows' => false, 474 'ignore_sticky_posts' => true, 475 'update_post_meta_cache' => false, 476 'update_post_term_cache' => false, 477 ) 478 ); 479 480 foreach ( $query->posts as $product_id ) { 481 $ids[] = (int) $product_id; 482 } 483 $total = max( $total, (int) $query->found_posts, count( $ids ) ); 484 485 $title_found = self::search_product_ids_by_title_tokens( $search, $limit ); 486 $ids = array_merge( $title_found['ids'], $ids ); 487 $total = max( $total, $title_found['total'] ); 488 489 $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) ); 490 491 return array( 492 'ids' => $ids, 493 'total' => $total, 494 ); 495 } 496 497 /** 498 * Title AND-match for all query tokens (older fixtures WP `s=` ranking can drop). 499 * 500 * @return array{ids: list<int>, total: int} 501 */ 502 private static function search_product_ids_by_title_tokens( string $search, int $limit ): array { 503 global $wpdb; 504 505 $words = preg_split( '/\s+/', $search ); 506 $tokens = array(); 507 foreach ( false === $words ? array() : $words as $word ) { 508 $word = trim( (string) $word ); 509 if ( strlen( $word ) < 3 ) { 510 continue; 511 } 512 $tokens[] = $word; 513 } 514 $tokens = array_values( array_unique( $tokens ) ); 515 if ( $tokens === array() ) { 516 return array( 517 'ids' => array(), 518 'total' => 0, 519 ); 520 } 521 522 $posts = $wpdb->posts; 523 $conditions = array( "post_type = 'product'", "post_status = 'publish'" ); 524 $values = array(); 525 foreach ( $tokens as $token ) { 526 $conditions[] = 'post_title LIKE %s'; 527 $values[] = '%' . $wpdb->esc_like( $token ) . '%'; 528 } 529 530 $where = implode( ' AND ', $conditions ); 531 $values[] = $limit; 532 533 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- `$posts` is `$wpdb->posts`; `$where` is LIKE %s placeholders. 534 $ids = $wpdb->get_col( 535 $wpdb->prepare( 536 "SELECT ID FROM {$posts} WHERE {$where} ORDER BY ID ASC LIMIT %d", 537 ...$values 538 ) 539 ); 540 541 $count_values = array_slice( $values, 0, -1 ); 542 $count = $wpdb->get_var( 543 $wpdb->prepare( 544 "SELECT COUNT(ID) FROM {$posts} WHERE {$where}", 545 ...$count_values 546 ) 547 ); 548 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare 549 550 return array( 551 'ids' => array_map( 'intval', is_array( $ids ) ? $ids : array() ), 552 'total' => (int) $count, 553 ); 399 554 } 400 555 -
agentmatica-mcp-connector/trunk/includes/modules/woocommerce/class-cart.php
r3736423 r3763005 20 20 21 21 public static function init(): void { 22 add_action( ' init', array( __CLASS__, 'register_checkout_redirect_route' ) );23 add_action( ' init', array( __CLASS__, 'register_sync_storefront_cart_route' ) );24 add_action( ' init', array( __CLASS__, 'register_read_storefront_cart_route' ) );22 add_action( 'rest_api_init', array( __CLASS__, 'register_checkout_redirect_route' ) ); 23 add_action( 'rest_api_init', array( __CLASS__, 'register_sync_storefront_cart_route' ) ); 24 add_action( 'rest_api_init', array( __CLASS__, 'register_read_storefront_cart_route' ) ); 25 25 add_action( 'template_redirect', array( __CLASS__, 'maybe_handle_checkout_redirect' ) ); 26 26 } -
agentmatica-mcp-connector/trunk/includes/modules/woocommerce/class-catalog-search.php
r3736423 r3763005 123 123 if ( str_contains( $title, $normalized ) ) { 124 124 return 90; 125 } 126 127 $words = preg_split( '/\s+/', $normalized ); 128 $words = false === $words ? array() : array_values( 129 array_filter( 130 $words, 131 static function ( string $word ): bool { 132 return strlen( $word ) >= 2; 133 } 134 ) 135 ); 136 if ( $words !== array() ) { 137 $title_hits = 0; 138 foreach ( $words as $word ) { 139 if ( self::word_matches_haystack( $word, $title ) ) { 140 ++$title_hits; 141 } 142 } 143 if ( $title_hits === count( $words ) ) { 144 return 85; 145 } 125 146 } 126 147 -
agentmatica-mcp-connector/trunk/readme.txt
r3736588 r3763005 4 4 Requires at least: 6.9 5 5 Tested up to: 7.1 6 Requires PHP: 7.47 Stable tag: 0.7. 16 Requires PHP: 8.0 7 Stable tag: 0.7.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 97 97 == Changelog == 98 98 99 = 0.7.2 = 100 * WooCommerce `search_catalog` no longer loads the entire catalogue on each chat search (SQL + capped re-rank). Fixes timeouts on large shops. 101 * `get_product` can resolve by product name when `id` is missing. 102 * Checkout and storefront-cart REST routes register on `rest_api_init`. 103 * An exact SKU search only returns published products. 104 * Requires PHP 8.0. Earlier versions already needed it to load. 105 99 106 = 0.7.1 = 100 107 * Fixed: order attribution was sent to an address that does not exist, so sales made through the chat were not counted in Agentmatica Insights. … … 134 141 == Upgrade Notice == 135 142 143 = 0.7.2 = 144 Woo catalog search no longer times out on large inventories. Update recommended for Agentmatica chat. 145 136 146 = 0.7.1 = 137 147 Fixes order attribution so chat sales appear in Insights. Update recommended.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)