diff --git a/README.md b/README.md
index 8c5b235..f122f46 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,11 @@ crm-new/
│ ├── export-kb-snapshot.sh # 导出 kb_docs 快照
│ ├── import-kb-snapshot.sh # 无旧 bridge 时导入 KB 快照
│ ├── sync-kb-online.js # 在线 FAQ 同步 + 重建 KB(不依赖旧 bridge)
+│ ├── build-site-kb.js # sitemap → kb/11-site-map.md
+│ ├── kb-version.js # KB 备份列表/回滚
+│ ├── eval-retrieval.js # RAG 检索评测
+│ ├── kb-version.js # KB 备份列表/回滚
+│ ├── build-site-kb.js # sitemap → 类目 KB
│ ├── smoke.sh
│ └── serve-web.sh # 仅本地预览 demo 页面
├── web/ # 站点接入:ai-snippet.js + demo.html(widget JWT 演示)
@@ -99,7 +104,7 @@ crm-new/
│ └── plan-a.md # 方案 A:bridge 接管 AI 编排进度
├── deploy/
│ ├── nginx.conf.example
-│ └── seeds/kb_docs.sql.gz # KB 快照(367 chunks:296 parent + 71 child)
+│ └── seeds/kb_docs.sql.gz # KB 快照(594 chunks:523 parent + 71 child)
services/public-gateway/ # 公网路径白名单反向代理(仅 webhook/帮助中心)
```
diff --git a/bridge/config/experiments.json b/bridge/config/experiments.json
new file mode 100644
index 0000000..1f39c2a
--- /dev/null
+++ b/bridge/config/experiments.json
@@ -0,0 +1,3 @@
+{
+ "experiments": []
+}
diff --git a/bridge/kb/11-site-map.md b/bridge/kb/11-site-map.md
index 25bb23b..f083d5d 100644
--- a/bridge/kb/11-site-map.md
+++ b/bridge/kb/11-site-map.md
@@ -1,491 +1,1172 @@
---
title: Yehwang Website Category Map (Site Map)
category: site-map
-source: www.yehwang.com sitemap.xml (auto-generated)
+source: https://www.yehwang.com/sitemap.xml
generated: true
source_url:
- https://www.yehwang.com/sitemap.xml
-last_updated: 2026-08-14
+last_updated: 2026-10-23
---
Every Yehwang category page has a direct URL. Use these links to send customers straight to the right page instead of a search query.
-### Q: Where can I find accessories?
-Browse our accessories category: https://www.yehwang.com/accessories
+### Q: Where can I find Accessories?
+Browse our Accessories category: https://www.yehwang.com/accessories
-### Q: Where can I find bags?
-Browse our bags category: https://www.yehwang.com/accessories/bags
+### Q: Where can I find Bags?
+Browse our Bags category: https://www.yehwang.com/accessories/bags
-### Q: Where can I find bag charms?
-Browse our bag charms category: https://www.yehwang.com/accessories/bags/bag-charms
+### Q: Where can I find Bag Charms?
+Browse our Bag Charms category: https://www.yehwang.com/accessories/bags/bag-charms
-### Q: Where can I find clutches?
-Browse our clutches category: https://www.yehwang.com/accessories/bags/clutches
+### Q: Where can I find Clutches?
+Browse our Clutches category: https://www.yehwang.com/accessories/bags/clutches
-### Q: Where can I find make up bags?
-Browse our make up bags category: https://www.yehwang.com/accessories/bags/make-up-bags
+### Q: Where can I find Make Up Bags?
+Browse our Make Up Bags category: https://www.yehwang.com/accessories/bags/make-up-bags
-### Q: Where can I find shopper bags?
-Browse our shopper bags category: https://www.yehwang.com/accessories/bags/shopper-bags
+### Q: Where can I find Shopper Bags?
+Browse our Shopper Bags category: https://www.yehwang.com/accessories/bags/shopper-bags
-### Q: Where can I find shoulder bags?
-Browse our shoulder bags category: https://www.yehwang.com/accessories/bags/shoulder-bags
+### Q: Where can I find Shoulder Bags?
+Browse our Shoulder Bags category: https://www.yehwang.com/accessories/bags/shoulder-bags
-### Q: Where can I find wholesale beach bags?
-Browse our wholesale beach bags category: https://www.yehwang.com/accessories/bags/wholesale-beach-bags
+### Q: Where can I find Wholesale Beach Bags?
+Browse our Wholesale Beach Bags category: https://www.yehwang.com/accessories/bags/wholesale-beach-bags
-### Q: Where can I find fan?
-Browse our fan category: https://www.yehwang.com/accessories/fan
+### Q: Where can I find Fan?
+Browse our Fan category: https://www.yehwang.com/accessories/fan
-### Q: Where can I find gloves?
-Browse our gloves category: https://www.yehwang.com/accessories/gloves
+### Q: Where can I find Gloves?
+Browse our Gloves category: https://www.yehwang.com/accessories/gloves
-### Q: Where can I find hair accessories?
-Browse our hair accessories category: https://www.yehwang.com/accessories/hair-accessories
+### Q: Where can I find Hair Accessories?
+Browse our Hair Accessories category: https://www.yehwang.com/accessories/hair-accessories
-### Q: Where can I find extensions?
-Browse our extensions category: https://www.yehwang.com/accessories/hair-accessories/extensions
+### Q: Where can I find Hair Bands?
+Browse our Hair Bands category: https://www.yehwang.com/accessories/hair-accessories/hair-bands
-### Q: Where can I find hair bands?
-Browse our hair bands category: https://www.yehwang.com/accessories/hair-accessories/hair-bands
+### Q: Where can I find Hair Clips?
+Browse our Hair Clips category: https://www.yehwang.com/accessories/hair-accessories/hair-clips
-### Q: Where can I find hair clips?
-Browse our hair clips category: https://www.yehwang.com/accessories/hair-accessories/hair-clips
+### Q: Where can I find Hair Ties?
+Browse our Hair Ties category: https://www.yehwang.com/accessories/hair-accessories/hair-ties
-### Q: Where can I find hair ties?
-Browse our hair ties category: https://www.yehwang.com/accessories/hair-accessories/hair-ties
+### Q: Where can I find Scrunchies?
+Browse our Scrunchies category: https://www.yehwang.com/accessories/hair-accessories/scrunchies
-### Q: Where can I find scrunchies?
-Browse our scrunchies category: https://www.yehwang.com/accessories/hair-accessories/scrunchies
+### Q: Where can I find Hats And Caps?
+Browse our Hats And Caps category: https://www.yehwang.com/accessories/hats-and-caps
-### Q: Where can I find hats and caps?
-Browse our hats and caps category: https://www.yehwang.com/accessories/hats-and-caps
+### Q: Where can I find Keychain?
+Browse our Keychain category: https://www.yehwang.com/accessories/keychain
-### Q: Where can I find keychain?
-Browse our keychain category: https://www.yehwang.com/accessories/keychain
+### Q: Where can I find Phone Accessories 226?
+Browse our Phone Accessories 226 category: https://www.yehwang.com/accessories/phone-accessories-226
-### Q: Where can I find phone cases?
-Browse our phone cases category: https://www.yehwang.com/accessories/phone-accessories-226/phone-cases
+### Q: Where can I find Phone Cases?
+Browse our Phone Cases category: https://www.yehwang.com/accessories/phone-accessories-226/phone-cases
-### Q: Where can I find phone cords?
-Browse our phone cords category: https://www.yehwang.com/accessories/phone-accessories-226/phone-cords
+### Q: Where can I find Phone Cords?
+Browse our Phone Cords category: https://www.yehwang.com/accessories/phone-accessories-226/phone-cords
-### Q: Where can I find scarves?
-Browse our scarves category: https://www.yehwang.com/accessories/scarves
+### Q: Where can I find Scarves?
+Browse our Scarves category: https://www.yehwang.com/accessories/scarves
-### Q: Where can I find scarves.htmls?
-Browse our scarves.htmls category: https://www.yehwang.com/accessories/scarves.htmls
+### Q: Where can I find Scarves.Htmls?
+Browse our Scarves.Htmls category: https://www.yehwang.com/accessories/scarves.htmls
-### Q: Where can I find scarf clips?
-Browse our scarf clips category: https://www.yehwang.com/accessories/scarves/scarf-clips
+### Q: Where can I find Scarf Clips?
+Browse our Scarf Clips category: https://www.yehwang.com/accessories/scarves/scarf-clips
-### Q: Where can I find summer scarves?
-Browse our summer scarves category: https://www.yehwang.com/accessories/scarves/summer-scarves
+### Q: Where can I find Summer Scarves?
+Browse our Summer Scarves category: https://www.yehwang.com/accessories/scarves/summer-scarves
-### Q: Where can I find winter scarves?
-Browse our winter scarves category: https://www.yehwang.com/accessories/scarves/winter-scarves
+### Q: Where can I find Winter Scarves?
+Browse our Winter Scarves category: https://www.yehwang.com/accessories/scarves/winter-scarves
-### Q: Where can I find shoe clips?
-Browse our shoe clips category: https://www.yehwang.com/accessories/shoe-clips
+### Q: Where can I find Socks?
+Browse our Socks category: https://www.yehwang.com/accessories/socks
-### Q: Where can I find socks?
-Browse our socks category: https://www.yehwang.com/accessories/socks
+### Q: Where can I find Bestsellers?
+Browse our Bestsellers category: https://www.yehwang.com/bestsellers
-### Q: Where can I find sunglasses?
-Browse our sunglasses category: https://www.yehwang.com/accessories/sunglasses
+### Q: Where can I find Bestsellers Accessories?
+Browse our Bestsellers Accessories category: https://www.yehwang.com/bestsellers/bestsellers-accessories
-### Q: Where can I find bestsellers?
-Browse our bestsellers category: https://www.yehwang.com/bestsellers
+### Q: Where can I find Bestsellers Clothes?
+Browse our Bestsellers Clothes category: https://www.yehwang.com/bestsellers/bestsellers-clothes
-### Q: Where can I find bestsellers accessories?
-Browse our bestsellers accessories category: https://www.yehwang.com/bestsellers/bestsellers-accessories
+### Q: Where can I find Bestsellers Jewelry?
+Browse our Bestsellers Jewelry category: https://www.yehwang.com/bestsellers/bestsellers-jewelry
-### Q: Where can I find bestsellers clothes?
-Browse our bestsellers clothes category: https://www.yehwang.com/bestsellers/bestsellers-clothes
+### Q: Where can I find Brands?
+Browse our Brands category: https://www.yehwang.com/brands
-### Q: Where can I find bestsellers jewelry?
-Browse our bestsellers jewelry category: https://www.yehwang.com/bestsellers/bestsellers-jewelry
+### Q: Where can I find Club Spice?
+Browse our Club Spice category: https://www.yehwang.com/brands/club-spice
-### Q: Where can I find brands?
-Browse our brands category: https://www.yehwang.com/brands
+### Q: Where can I find Lilli June?
+Browse our Lilli June category: https://www.yehwang.com/brands/lilli-june
-### Q: Where can I find club spice?
-Browse our club spice category: https://www.yehwang.com/brands/club-spice
+### Q: Where can I find Lume?
+Browse our Lume category: https://www.yehwang.com/brands/lume
-### Q: Where can I find lilli june?
-Browse our lilli june category: https://www.yehwang.com/brands/lilli-june
+### Q: Where can I find Yehwang?
+Browse our Yehwang category: https://www.yehwang.com/brands/yehwang
-### Q: Where can I find lume?
-Browse our lume category: https://www.yehwang.com/brands/lume
+### Q: Where can I find Clothes?
+Browse our Clothes category: https://www.yehwang.com/clothes
-### Q: Where can I find yehwang?
-Browse our yehwang category: https://www.yehwang.com/brands/yehwang
+### Q: Where can I find Blazers?
+Browse our Blazers category: https://www.yehwang.com/clothes/blazers
-### Q: Where can I find clothes?
-Browse our clothes category: https://www.yehwang.com/clothes
+### Q: Where can I find Blouses?
+Browse our Blouses category: https://www.yehwang.com/clothes/blouses
-### Q: Where can I find blazers?
-Browse our blazers category: https://www.yehwang.com/clothes/blazers
+### Q: Where can I find Clothing Collections?
+Browse our Clothing Collections category: https://www.yehwang.com/clothes/clothing-collections
-### Q: Where can I find blouses?
-Browse our blouses category: https://www.yehwang.com/clothes/blouses
+### Q: Where can I find Fall And Winter Clothes?
+Browse our Fall And Winter Clothes category: https://www.yehwang.com/clothes/clothing-collections/fall-and-winter-clothes
-### Q: Where can I find clothing collections?
-Browse our clothing collections category: https://www.yehwang.com/clothes/clothing-collections
+### Q: Where can I find Summer Clothes?
+Browse our Summer Clothes category: https://www.yehwang.com/clothes/clothing-collections/summer-clothes
-### Q: Where can I find fall and winter clothes?
-Browse our fall and winter clothes category: https://www.yehwang.com/clothes/clothing-collections/fall-and-winter-clothes
+### Q: Where can I find Denim?
+Browse our Denim category: https://www.yehwang.com/clothes/denim
-### Q: Where can I find summer clothes?
-Browse our summer clothes category: https://www.yehwang.com/clothes/clothing-collections/summer-clothes
+### Q: Where can I find Dresses?
+Browse our Dresses category: https://www.yehwang.com/clothes/dresses
-### Q: Where can I find denim?
-Browse our denim category: https://www.yehwang.com/clothes/denim
+### Q: Where can I find Midi Dresses?
+Browse our Midi Dresses category: https://www.yehwang.com/clothes/dresses/midi-dresses
-### Q: Where can I find dresses?
-Browse our dresses category: https://www.yehwang.com/clothes/dresses
+### Q: Where can I find Mini Dresses?
+Browse our Mini Dresses category: https://www.yehwang.com/clothes/dresses/mini-dresses
-### Q: Where can I find midi dresses?
-Browse our midi dresses category: https://www.yehwang.com/clothes/dresses/midi-dresses
+### Q: Where can I find Jackets?
+Browse our Jackets category: https://www.yehwang.com/clothes/jackets
-### Q: Where can I find mini dresses?
-Browse our mini dresses category: https://www.yehwang.com/clothes/dresses/mini-dresses
+### Q: Where can I find Jeans?
+Browse our Jeans category: https://www.yehwang.com/clothes/jeans
-### Q: Where can I find jackets?
-Browse our jackets category: https://www.yehwang.com/clothes/jackets
+### Q: Where can I find Jeans 3580?
+Browse our Jeans 3580 category: https://www.yehwang.com/clothes/jeans-3580
-### Q: Where can I find jeans?
-Browse our jeans category: https://www.yehwang.com/clothes/jeans
+### Q: Where can I find Ripped Jeans?
+Browse our Ripped Jeans category: https://www.yehwang.com/clothes/jeans-3580/ripped-jeans
-### Q: Where can I find ripped jeans?
-Browse our ripped jeans category: https://www.yehwang.com/clothes/jeans-3580/ripped-jeans
+### Q: Where can I find Straight Jeans?
+Browse our Straight Jeans category: https://www.yehwang.com/clothes/jeans/straight-jeans
-### Q: Where can I find straight jeans?
-Browse our straight jeans category: https://www.yehwang.com/clothes/jeans/straight-jeans
+### Q: Where can I find Wide Leg Jeans?
+Browse our Wide Leg Jeans category: https://www.yehwang.com/clothes/jeans/wide-leg-jeans
-### Q: Where can I find wide leg jeans?
-Browse our wide leg jeans category: https://www.yehwang.com/clothes/jeans/wide-leg-jeans
+### Q: Where can I find Jumpsuits?
+Browse our Jumpsuits category: https://www.yehwang.com/clothes/jumpsuits
-### Q: Where can I find jumpsuits?
-Browse our jumpsuits category: https://www.yehwang.com/clothes/jumpsuits
+### Q: Where can I find Knitwear And Sweaters?
+Browse our Knitwear And Sweaters category: https://www.yehwang.com/clothes/knitwear-and-sweaters
-### Q: Where can I find knitwear and sweaters?
-Browse our knitwear and sweaters category: https://www.yehwang.com/clothes/knitwear-and-sweaters
+### Q: Where can I find Cardigans 583617266?
+Browse our Cardigans 583617266 category: https://www.yehwang.com/clothes/knitwear-and-sweaters/cardigans-583617266
-### Q: Where can I find sweaters?
-Browse our sweaters category: https://www.yehwang.com/clothes/knitwear-and-sweaters/sweaters
+### Q: Where can I find Sweaters?
+Browse our Sweaters category: https://www.yehwang.com/clothes/knitwear-and-sweaters/sweaters
-### Q: Where can I find shorts?
-Browse our shorts category: https://www.yehwang.com/clothes/shorts
+### Q: Where can I find Shorts?
+Browse our Shorts category: https://www.yehwang.com/clothes/shorts
-### Q: Where can I find skirts?
-Browse our skirts category: https://www.yehwang.com/clothes/skirts
+### Q: Where can I find Skirts?
+Browse our Skirts category: https://www.yehwang.com/clothes/skirts
-### Q: Where can I find midi skirts?
-Browse our midi skirts category: https://www.yehwang.com/clothes/skirts/midi-skirts
+### Q: Where can I find Midi Skirts?
+Browse our Midi Skirts category: https://www.yehwang.com/clothes/skirts/midi-skirts
-### Q: Where can I find mini skirts?
-Browse our mini skirts category: https://www.yehwang.com/clothes/skirts/mini-skirts
+### Q: Where can I find Mini Skirts?
+Browse our Mini Skirts category: https://www.yehwang.com/clothes/skirts/mini-skirts
-### Q: Where can I find tops?
-Browse our tops category: https://www.yehwang.com/clothes/tops
+### Q: Where can I find Tops?
+Browse our Tops category: https://www.yehwang.com/clothes/tops
-### Q: Where can I find long sleeve tops?
-Browse our long sleeve tops category: https://www.yehwang.com/clothes/tops/long-sleeve-tops
+### Q: Where can I find Long Sleeve Tops?
+Browse our Long Sleeve Tops category: https://www.yehwang.com/clothes/tops/long-sleeve-tops
-### Q: Where can I find short sleeve tops?
-Browse our short sleeve tops category: https://www.yehwang.com/clothes/tops/short-sleeve-tops
+### Q: Where can I find Short Sleeve Tops?
+Browse our Short Sleeve Tops category: https://www.yehwang.com/clothes/tops/short-sleeve-tops
-### Q: Where can I find sleeveless tops?
-Browse our sleeveless tops category: https://www.yehwang.com/clothes/tops/sleeveless-tops
+### Q: Where can I find Sleeveless Tops?
+Browse our Sleeveless Tops category: https://www.yehwang.com/clothes/tops/sleeveless-tops
-### Q: Where can I find t shirts?
-Browse our t shirts category: https://www.yehwang.com/clothes/tops/t-shirts
+### Q: Where can I find T Shirts?
+Browse our T Shirts category: https://www.yehwang.com/clothes/tops/t-shirts
-### Q: Where can I find vest?
-Browse our vest category: https://www.yehwang.com/clothes/tops/vest
+### Q: Where can I find Vest?
+Browse our Vest category: https://www.yehwang.com/clothes/tops/vest
-### Q: Where can I find trousers?
-Browse our trousers category: https://www.yehwang.com/clothes/trousers
+### Q: Where can I find Trousers?
+Browse our Trousers category: https://www.yehwang.com/clothes/trousers
-### Q: Where can I find flaired pants?
-Browse our flaired pants category: https://www.yehwang.com/clothes/trousers/flaired-pants
+### Q: Where can I find Flaired Pants?
+Browse our Flaired Pants category: https://www.yehwang.com/clothes/trousers/flaired-pants
-### Q: Where can I find skinny pants?
-Browse our skinny pants category: https://www.yehwang.com/clothes/trousers/skinny-pants
+### Q: Where can I find Skinny Pants?
+Browse our Skinny Pants category: https://www.yehwang.com/clothes/trousers/skinny-pants
-### Q: Where can I find straight pants?
-Browse our straight pants category: https://www.yehwang.com/clothes/trousers/straight-pants
+### Q: Where can I find Straight Pants?
+Browse our Straight Pants category: https://www.yehwang.com/clothes/trousers/straight-pants
-### Q: Where can I find wide leg pants?
-Browse our wide leg pants category: https://www.yehwang.com/clothes/trousers/wide-leg-pants
+### Q: Where can I find Wide Leg Pants?
+Browse our Wide Leg Pants category: https://www.yehwang.com/clothes/trousers/wide-leg-pants
-### Q: Where can I find dropshipping?
-Browse our dropshipping category: https://www.yehwang.com/dropshipping
+### Q: Where can I find Dropshipping?
+Browse our Dropshipping category: https://www.yehwang.com/dropshipping
-### Q: Where can I find factory direct wholesale en?
-Browse our factory direct wholesale en category: https://www.yehwang.com/factory-direct-wholesale-en
+### Q: Where can I find Factory Direct Wholesale En?
+Browse our Factory Direct Wholesale En category: https://www.yehwang.com/factory-direct-wholesale-en
-### Q: Where can I find jewelry?
-Browse our jewelry category: https://www.yehwang.com/jewelry
+### Q: Where can I find 1053554669 Rings?
+Browse our 1053554669 Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings
-### Q: Where can I find anklets?
-Browse our anklets category: https://www.yehwang.com/jewelry/Anklets
+### Q: Where can I find Adjustable Rings?
+Browse our Adjustable Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/adjustable-rings
-### Q: Where can I find advent calendars?
-Browse our advent calendars category: https://www.yehwang.com/jewelry/advent-calendars
+### Q: Where can I find Beaded Rings?
+Browse our Beaded Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/beaded-rings
-### Q: Where can I find bracelets?
-Browse our bracelets category: https://www.yehwang.com/jewelry/bracelets
+### Q: Where can I find Gemstone Rings?
+Browse our Gemstone Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/gemstone-rings
-### Q: Where can I find bangles?
-Browse our bangles category: https://www.yehwang.com/jewelry/bracelets/bangles
+### Q: Where can I find Minimalist Rings?
+Browse our Minimalist Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/minimalist-rings
-### Q: Where can I find bangles and cuffs bracelet?
-Browse our bangles and cuffs bracelet category: https://www.yehwang.com/jewelry/bracelets/bangles-and-cuffs-bracelet
+### Q: Where can I find Rhinestone Rings?
+Browse our Rhinestone Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/rhinestone-rings
-### Q: Where can I find beaded bracelets?
-Browse our beaded bracelets category: https://www.yehwang.com/jewelry/bracelets/beaded-bracelets
+### Q: Where can I find Ring Sets?
+Browse our Ring Sets category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/ring-sets
-### Q: Where can I find bracelets sets?
-Browse our bracelets sets category: https://www.yehwang.com/jewelry/bracelets/bracelets-sets
+### Q: Where can I find Rings With Sizes?
+Browse our Rings With Sizes category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/rings-with-sizes
-### Q: Where can I find braided bracelets?
-Browse our braided bracelets category: https://www.yehwang.com/jewelry/bracelets/braided-bracelets
+### Q: Where can I find Signet Rings?
+Browse our Signet Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/signet-rings
-### Q: Where can I find chain bracelets?
-Browse our chain bracelets category: https://www.yehwang.com/jewelry/bracelets/chain-bracelets
+### Q: Where can I find Statement Rings?
+Browse our Statement Rings category: https://www.yehwang.com/factory-direct-wholesale-en/1053554669-rings/statement-rings
-### Q: Where can I find charm bracelets?
-Browse our charm bracelets category: https://www.yehwang.com/jewelry/bracelets/charm-bracelets
+### Q: Where can I find 1085018024 Accessories?
+Browse our 1085018024 Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories
-### Q: Where can I find chunky bracelets?
-Browse our chunky bracelets category: https://www.yehwang.com/jewelry/bracelets/chunky-bracelets
+### Q: Where can I find 1966767763 Sunglasses?
+Browse our 1966767763 Sunglasses category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/1966767763-sunglasses
-### Q: Where can I find hand chain bracelets?
-Browse our hand chain bracelets category: https://www.yehwang.com/jewelry/bracelets/hand-chain-bracelets
+### Q: Where can I find Bag Charms?
+Browse our Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms
-### Q: Where can I find brooches?
-Browse our brooches category: https://www.yehwang.com/jewelry/brooches
+### Q: Where can I find Animal Bag Charms?
+Browse our Animal Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms/animal-bag-charms
-### Q: Where can I find cufflinks?
-Browse our cufflinks category: https://www.yehwang.com/jewelry/cufflinks
+### Q: Where can I find Beaded Bag Charms?
+Browse our Beaded Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms/beaded-bag-charms
-### Q: Where can I find display set en?
-Browse our display set en category: https://www.yehwang.com/jewelry/display-set-en
+### Q: Where can I find Doll Bag Charms?
+Browse our Doll Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms/doll-bag-charms
-### Q: Where can I find anklets display set?
-Browse our anklets display set category: https://www.yehwang.com/jewelry/display-set-en/anklets-display-set
+### Q: Where can I find Flower Bag Charms?
+Browse our Flower Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms/flower-bag-charms
-### Q: Where can I find bracelets display set?
-Browse our bracelets display set category: https://www.yehwang.com/jewelry/display-set-en/bracelets-display-set
+### Q: Where can I find Fruit Bag Charms?
+Browse our Fruit Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms/fruit-bag-charms
-### Q: Where can I find earrings display set?
-Browse our earrings display set category: https://www.yehwang.com/jewelry/display-set-en/earrings-display-set
+### Q: Where can I find Heart Bag Charms?
+Browse our Heart Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-charms/heart-bag-charms
-### Q: Where can I find necklace display set?
-Browse our necklace display set category: https://www.yehwang.com/jewelry/display-set-en/necklace-display-set
+### Q: Where can I find Bag Straps?
+Browse our Bag Straps category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bag-straps
-### Q: Where can I find rings display set?
-Browse our rings display set category: https://www.yehwang.com/jewelry/display-set-en/rings-display-set
+### Q: Where can I find Bags?
+Browse our Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags
-### Q: Where can I find earrings?
-Browse our earrings category: https://www.yehwang.com/jewelry/earrings
+### Q: Where can I find Beach Bags?
+Browse our Beach Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/beach-bags
-### Q: Where can I find beads earrings?
-Browse our beads earrings category: https://www.yehwang.com/jewelry/earrings/beads-earrings
+### Q: Where can I find Clutches?
+Browse our Clutches category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/clutches
-### Q: Where can I find birthstone earrings?
-Browse our birthstone earrings category: https://www.yehwang.com/jewelry/earrings/birthstone-earrings
+### Q: Where can I find Crossbody Bags?
+Browse our Crossbody Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/crossbody-bags
-### Q: Where can I find drop earrings?
-Browse our drop earrings category: https://www.yehwang.com/jewelry/earrings/drop-earrings
+### Q: Where can I find Handbags?
+Browse our Handbags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/handbags
-### Q: Where can I find ear cuffs?
-Browse our ear cuffs category: https://www.yehwang.com/jewelry/earrings/ear-cuffs
+### Q: Where can I find Laptop Bag?
+Browse our Laptop Bag category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/laptop-bag
-### Q: Where can I find ear studs?
-Browse our ear studs category: https://www.yehwang.com/jewelry/earrings/ear-studs
+### Q: Where can I find Polka And Stripe Bags?
+Browse our Polka And Stripe Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/polka-and-stripe-bags
-### Q: Where can I find earring set?
-Browse our earring set category: https://www.yehwang.com/jewelry/earrings/earring-set
+### Q: Where can I find Shopping Bags?
+Browse our Shopping Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/shopping-bags
-### Q: Where can I find earrings with stone?
-Browse our earrings with stone category: https://www.yehwang.com/jewelry/earrings/earrings-with-stone
+### Q: Where can I find Shoulder Bags?
+Browse our Shoulder Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/shoulder-bags
-### Q: Where can I find hoop earrings?
-Browse our hoop earrings category: https://www.yehwang.com/jewelry/earrings/hoop-earrings
+### Q: Where can I find Striped Bags?
+Browse our Striped Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/striped-bags
-### Q: Where can I find party earrings?
-Browse our party earrings category: https://www.yehwang.com/jewelry/earrings/party-earrings
+### Q: Where can I find Suede And Studded Bags?
+Browse our Suede And Studded Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/suede-and-studded-bags
-### Q: Where can I find studs?
-Browse our studs category: https://www.yehwang.com/jewelry/earrings/studs
+### Q: Where can I find Tote Bags?
+Browse our Tote Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/tote-bags
-### Q: Where can I find jewelry collections?
-Browse our jewelry collections category: https://www.yehwang.com/jewelry/jewelry-collections
+### Q: Where can I find Wallets And Card Holders?
+Browse our Wallets And Card Holders category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/wallets-and-card-holders
-### Q: Where can I find acrylic and resin collection?
-Browse our acrylic and resin collection category: https://www.yehwang.com/jewelry/jewelry-collections/acrylic-and-resin-collection
+### Q: Where can I find Woven Bags?
+Browse our Woven Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/bags/woven-bags
-### Q: Where can I find beach collection?
-Browse our beach collection category: https://www.yehwang.com/jewelry/jewelry-collections/beach-collection
+### Q: Where can I find Earphone Case?
+Browse our Earphone Case category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/earphone-case
-### Q: Where can I find flowers collection?
-Browse our flowers collection category: https://www.yehwang.com/jewelry/jewelry-collections/flowers-collection
+### Q: Where can I find Hats?
+Browse our Hats category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats
-### Q: Where can I find fruits and plants collection?
-Browse our fruits and plants collection category: https://www.yehwang.com/jewelry/jewelry-collections/fruits-and-plants-collection
+### Q: Where can I find Baseball Cap?
+Browse our Baseball Cap category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/baseball-cap
-### Q: Where can I find hearts collection?
-Browse our hearts collection category: https://www.yehwang.com/jewelry/jewelry-collections/hearts-collection
+### Q: Where can I find Beanie?
+Browse our Beanie category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/beanie
-### Q: Where can I find jewelry parts?
-Browse our jewelry parts category: https://www.yehwang.com/jewelry/jewelry-parts
+### Q: Where can I find Berets?
+Browse our Berets category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/berets
-### Q: Where can I find jewelry beads?
-Browse our jewelry beads category: https://www.yehwang.com/jewelry/jewelry-parts/jewelry-beads
+### Q: Where can I find Bucket Hats?
+Browse our Bucket Hats category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/bucket-hats
-### Q: Where can I find jewelry charms?
-Browse our jewelry charms category: https://www.yehwang.com/jewelry/jewelry-parts/jewelry-charms
+### Q: Where can I find Crochet Cap?
+Browse our Crochet Cap category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/crochet-cap
-### Q: Where can I find jewelry working supplies?
-Browse our jewelry working supplies category: https://www.yehwang.com/jewelry/jewelry-parts/jewelry-working-supplies
+### Q: Where can I find Earflap Hat?
+Browse our Earflap Hat category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/earflap-hat
-### Q: Where can I find men s bracelets?
-Browse our men s bracelets category: https://www.yehwang.com/jewelry/men-s-bracelets
+### Q: Where can I find Straw Hats?
+Browse our Straw Hats category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/hats/straw-hats
-### Q: Where can I find men s jewelry?
-Browse our men s jewelry category: https://www.yehwang.com/jewelry/men-s-jewelry
+### Q: Where can I find Keychain?
+Browse our Keychain category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/keychain
-### Q: Where can I find men s necklaces?
-Browse our men s necklaces category: https://www.yehwang.com/jewelry/men-s-necklaces
+### Q: Where can I find Pendants?
+Browse our Pendants category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/pendants
-### Q: Where can I find navel piercings?
-Browse our navel piercings category: https://www.yehwang.com/jewelry/navel-piercings
+### Q: Where can I find Fruit Charms And Beaded?
+Browse our Fruit Charms And Beaded category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/pendants/fruit-charms-and-beaded
-### Q: Where can I find necklaces?
-Browse our necklaces category: https://www.yehwang.com/jewelry/necklaces
+### Q: Where can I find Heart Charms And Beaded?
+Browse our Heart Charms And Beaded category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/pendants/heart-charms-and-beaded
-### Q: Where can I find beaded necklaces?
-Browse our beaded necklaces category: https://www.yehwang.com/jewelry/necklaces/beaded-necklaces
+### Q: Where can I find Stainless Steel Pendants?
+Browse our Stainless Steel Pendants category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/pendants/stainless-steel-pendants
-### Q: Where can I find braided necklaces?
-Browse our braided necklaces category: https://www.yehwang.com/jewelry/necklaces/braided-necklaces
+### Q: Where can I find Phone Accessories?
+Browse our Phone Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/phone-accessories
-### Q: Where can I find chain necklaces?
-Browse our chain necklaces category: https://www.yehwang.com/jewelry/necklaces/chain-necklaces
+### Q: Where can I find Phone Bags?
+Browse our Phone Bags category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/phone-bags
-### Q: Where can I find layered necklace?
-Browse our layered necklace category: https://www.yehwang.com/jewelry/necklaces/layered-necklace
+### Q: Where can I find Scarves?
+Browse our Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/scarves
-### Q: Where can I find long necklaces?
-Browse our long necklaces category: https://www.yehwang.com/jewelry/necklaces/long-necklaces
+### Q: Where can I find Socks?
+Browse our Socks category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/socks
-### Q: Where can I find medium necklaces?
-Browse our medium necklaces category: https://www.yehwang.com/jewelry/necklaces/medium-necklaces
+### Q: Where can I find Winter Scarves?
+Browse our Winter Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/1085018024-accessories/winter-scarves
-### Q: Where can I find necklaces sets?
-Browse our necklaces sets category: https://www.yehwang.com/jewelry/necklaces/necklaces-sets
+### Q: Where can I find 1410460788 Necklaces?
+Browse our 1410460788 Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces
-### Q: Where can I find pendant necklaces?
-Browse our pendant necklaces category: https://www.yehwang.com/jewelry/necklaces/pendant-necklaces
+### Q: Where can I find 10877353 Pendant Necklaces?
+Browse our 10877353 Pendant Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/10877353-pendant-necklaces
-### Q: Where can I find rings?
-Browse our rings category: https://www.yehwang.com/jewelry/rings
+### Q: Where can I find Beaded Necklaces?
+Browse our Beaded Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/beaded-necklaces
-### Q: Where can I find birthstone rings?
-Browse our birthstone rings category: https://www.yehwang.com/jewelry/rings/birthstone-rings
+### Q: Where can I find Braided Necklaces?
+Browse our Braided Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/braided-necklaces
-### Q: Where can I find gemstone rings?
-Browse our gemstone rings category: https://www.yehwang.com/jewelry/rings/gemstone-rings
+### Q: Where can I find Chain Necklaces?
+Browse our Chain Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/chain-necklaces
-### Q: Where can I find minimalist rings?
-Browse our minimalist rings category: https://www.yehwang.com/jewelry/rings/minimalist-rings
+### Q: Where can I find Chokers?
+Browse our Chokers category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/chokers
-### Q: Where can I find rhinestone rings?
-Browse our rhinestone rings category: https://www.yehwang.com/jewelry/rings/rhinestone-rings
+### Q: Where can I find Layered Necklaces?
+Browse our Layered Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/layered-necklaces
-### Q: Where can I find rings with sizes?
-Browse our rings with sizes category: https://www.yehwang.com/jewelry/rings/rings-with-sizes
+### Q: Where can I find Long Necklaces?
+Browse our Long Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/long-necklaces
-### Q: Where can I find stainless steel jewelry?
-Browse our stainless steel jewelry category: https://www.yehwang.com/jewelry/stainless-steel-jewelry
+### Q: Where can I find Necklaces Sets?
+Browse our Necklaces Sets category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/necklaces-sets
-### Q: Where can I find new?
-Browse our new category: https://www.yehwang.com/new
+### Q: Where can I find Short Necklaces?
+Browse our Short Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/1410460788-necklaces/short-necklaces
-### Q: Where can I find pre order?
-Browse our pre order category: https://www.yehwang.com/new/pre-order
+### Q: Where can I find 401529937 Men S Jewelry?
+Browse our 401529937 Men S Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/401529937-men-s-jewelry
-### Q: Where can I find packing and display?
-Browse our packing and display category: https://www.yehwang.com/packing-and-display
+### Q: Where can I find 4731332 Men S Bracelets?
+Browse our 4731332 Men S Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/401529937-men-s-jewelry/4731332-men-s-bracelets
-### Q: Where can I find wholesale display materials?
-Browse our wholesale display materials category: https://www.yehwang.com/packing-and-display/wholesale-display-materials
+### Q: Where can I find 477150573 Men S Necklaces?
+Browse our 477150573 Men S Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/401529937-men-s-jewelry/477150573-men-s-necklaces
-### Q: Where can I find displays for bracelets?
-Browse our displays for bracelets category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-bracelets
+### Q: Where can I find 428146655 Bracelets?
+Browse our 428146655 Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets
-### Q: Where can I find displays for earrings?
-Browse our displays for earrings category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-earrings
+### Q: Where can I find Bangles?
+Browse our Bangles category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/bangles
-### Q: Where can I find displays for necklaces?
-Browse our displays for necklaces category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-necklaces
+### Q: Where can I find Beaded Bracelets?
+Browse our Beaded Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/beaded-bracelets
-### Q: Where can I find displays for rings?
-Browse our displays for rings category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-rings
+### Q: Where can I find Bracelets Sets?
+Browse our Bracelets Sets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/bracelets-sets
-### Q: Where can I find jewelry trays?
-Browse our jewelry trays category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/jewelry-trays
+### Q: Where can I find Braided Bracelets?
+Browse our Braided Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/braided-bracelets
-### Q: Where can I find wholesale packing materials?
-Browse our wholesale packing materials category: https://www.yehwang.com/packing-and-display/wholesale-packing-materials
+### Q: Where can I find Chain Bracelets?
+Browse our Chain Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/chain-bracelets
-### Q: Where can I find restock request?
-Browse our restock request category: https://www.yehwang.com/restock-request
+### Q: Where can I find Charm Bracelet?
+Browse our Charm Bracelet category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/charm-bracelet
-### Q: Where can I find top deal?
-Browse our top deal category: https://www.yehwang.com/top-deal
+### Q: Where can I find Cuff Bracelets?
+Browse our Cuff Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/cuff-bracelets
-### Q: Where can I find 0 12 zone?
-Browse our 0 12 zone category: https://www.yehwang.com/top-deal/12-12-sale/0-12-zone
+### Q: Where can I find Hand Chain?
+Browse our Hand Chain category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/hand-chain
-### Q: Where can I find daily deals?
-Browse our daily deals category: https://www.yehwang.com/top-deal/daily-deals
+### Q: Where can I find Statement Bracelets?
+Browse our Statement Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/428146655-bracelets/statement-bracelets
-### Q: Where can I find new in sale?
-Browse our new in sale category: https://www.yehwang.com/top-deal/new-in-sale
+### Q: Where can I find 80945701 Earrings?
+Browse our 80945701 Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings
-### Q: Where can I find 1040045430 50 off?
-Browse our 1040045430 50 off category: https://www.yehwang.com/top-deal/new-in-sale/1040045430-50-off
+### Q: Where can I find Charm Earrings?
+Browse our Charm Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings.html/charm-earrings
-### Q: Where can I find 20 30 off?
-Browse our 20 30 off category: https://www.yehwang.com/top-deal/new-in-sale/20-30-off
+### Q: Where can I find Beads Earrings?
+Browse our Beads Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/beads-earrings
-### Q: Where can I find 2113133623 20 off?
-Browse our 2113133623 20 off category: https://www.yehwang.com/top-deal/new-in-sale/2113133623-20-off
+### Q: Where can I find Dangle Earrings?
+Browse our Dangle Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/dangle-earrings
-### Q: Where can I find 30 40 off?
-Browse our 30 40 off category: https://www.yehwang.com/top-deal/new-in-sale/30-40-off
+### Q: Where can I find Ear Cuffs?
+Browse our Ear Cuffs category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/ear-cuffs
-### Q: Where can I find 40 50 off?
-Browse our 40 50 off category: https://www.yehwang.com/top-deal/new-in-sale/40-50-off
+### Q: Where can I find Earring Sets?
+Browse our Earring Sets category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/earring-sets
-### Q: Where can I find sale en?
-Browse our sale en category: https://www.yehwang.com/top-deal/sale-en
+### Q: Where can I find Earrings With Stone?
+Browse our Earrings With Stone category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/earrings-with-stone
-### Q: Where can I find accessories?
-Browse our accessories category: https://www.yehwang.com/top-deal/sale-en/accessories
+### Q: Where can I find Hoop Earrings?
+Browse our Hoop Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/hoop-earrings
-### Q: Where can I find jewelry?
-Browse our jewelry category: https://www.yehwang.com/top-deal/sale-en/jewelry
+### Q: Where can I find Party Earrings?
+Browse our Party Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/party-earrings
-### Q: Where can I find packing and display?
-Browse our packing and display category: https://www.yehwang.com/top-deal/sale-en/packing-and-display
+### Q: Where can I find Stud Earrings?
+Browse our Stud Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/80945701-earrings/stud-earrings
-### Q: Where can I find wholesale service?
-Browse our wholesale service category: https://www.yehwang.com/wholesale-service
+### Q: Where can I find Accessories?
+Browse our Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/accessories
-### Q: Where can I find private label?
-Browse our private label category: https://www.yehwang.com/wholesale-service/private-label
+### Q: Where can I find Belts?
+Browse our Belts category: https://www.yehwang.com/factory-direct-wholesale-en/accessories/belts
+
+### Q: Where can I find Phone Chain?
+Browse our Phone Chain category: https://www.yehwang.com/factory-direct-wholesale-en/accessories/phone-accessories/phone-chain
+
+### Q: Where can I find Gloves?
+Browse our Gloves category: https://www.yehwang.com/factory-direct-wholesale-en/apparel-accessories/gloves
+
+### Q: Where can I find Scarves?
+Browse our Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/apparel-accessories/scarves
+
+### Q: Where can I find Bag And Charms?
+Browse our Bag And Charms category: https://www.yehwang.com/factory-direct-wholesale-en/bag-and-charms
+
+### Q: Where can I find Makeup Bags?
+Browse our Makeup Bags category: https://www.yehwang.com/factory-direct-wholesale-en/bags-and-carriers/makeup-bags
+
+### Q: Where can I find Beauty And Makeup Tools?
+Browse our Beauty And Makeup Tools category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools
+
+### Q: Where can I find Beauty And Makeup Tools.Htm?
+Browse our Beauty And Makeup Tools.Htm category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools.htm
+
+### Q: Where can I find Combs?
+Browse our Combs category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools/combs
+
+### Q: Where can I find Eyelash Tools?
+Browse our Eyelash Tools category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools/eyelash-tools
+
+### Q: Where can I find Makeup Bags?
+Browse our Makeup Bags category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools/makeup-bags
+
+### Q: Where can I find Makeup Brushes?
+Browse our Makeup Brushes category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools/makeup-brushes
+
+### Q: Where can I find Makeup Puffs?
+Browse our Makeup Puffs category: https://www.yehwang.com/factory-direct-wholesale-en/beauty-and-makeup-tools/makeup-puffs
+
+### Q: Where can I find Bestsellers?
+Browse our Bestsellers category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers
+
+### Q: Where can I find Bag Charms?
+Browse our Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/bag-charms
+
+### Q: Where can I find Bags?
+Browse our Bags category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/bags
+
+### Q: Where can I find Beads?
+Browse our Beads category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/beads
+
+### Q: Where can I find Bestsellers Accessories?
+Browse our Bestsellers Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/bestsellers-accessories
+
+### Q: Where can I find Bestsellers Jewelry?
+Browse our Bestsellers Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/bestsellers-jewelry
+
+### Q: Where can I find Body Jewelry?
+Browse our Body Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/body-jewelry
+
+### Q: Where can I find Bracelets?
+Browse our Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/bracelets
+
+### Q: Where can I find Clothes?
+Browse our Clothes category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/clothes
+
+### Q: Where can I find Earrings?
+Browse our Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/earrings
+
+### Q: Where can I find Glasses?
+Browse our Glasses category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/glasses
+
+### Q: Where can I find Hair Accessories?
+Browse our Hair Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/hair-accessories
+
+### Q: Where can I find Hats?
+Browse our Hats category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/hats
+
+### Q: Where can I find Jewelry Sets?
+Browse our Jewelry Sets category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/jewelry-sets
+
+### Q: Where can I find Keychain?
+Browse our Keychain category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/keychain
+
+### Q: Where can I find Making Supplies?
+Browse our Making Supplies category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/making-supplies
+
+### Q: Where can I find Necklaces?
+Browse our Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/necklaces
+
+### Q: Where can I find Packing And Display?
+Browse our Packing And Display category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/packing-and-display
+
+### Q: Where can I find Pendants?
+Browse our Pendants category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/pendants
+
+### Q: Where can I find Piercing Jewelry?
+Browse our Piercing Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/piercing-jewelry
+
+### Q: Where can I find Rings?
+Browse our Rings category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/rings
+
+### Q: Where can I find Summer Scarves?
+Browse our Summer Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/summer-scarves
+
+### Q: Where can I find Trending Styles?
+Browse our Trending Styles category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/trending-styles
+
+### Q: Where can I find Winter Scarves?
+Browse our Winter Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/bestsellers/winter-scarves
+
+### Q: Where can I find Body Jewelry?
+Browse our Body Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/body-jewelry
+
+### Q: Where can I find 806600864 Brooches?
+Browse our 806600864 Brooches category: https://www.yehwang.com/factory-direct-wholesale-en/body-jewelry/806600864-brooches
+
+### Q: Where can I find Anklet?
+Browse our Anklet category: https://www.yehwang.com/factory-direct-wholesale-en/body-jewelry/anklet
+
+### Q: Where can I find Swimwear?
+Browse our Swimwear category: https://www.yehwang.com/factory-direct-wholesale-en/body-jewelry/swimwear
+
+### Q: Where can I find Waist Chains 3462?
+Browse our Waist Chains 3462 category: https://www.yehwang.com/factory-direct-wholesale-en/body-jewelry/waist-chains-3462
+
+### Q: Where can I find Clothes?
+Browse our Clothes category: https://www.yehwang.com/factory-direct-wholesale-en/clothes
+
+### Q: Where can I find Bottoms?
+Browse our Bottoms category: https://www.yehwang.com/factory-direct-wholesale-en/clothes/bottoms
+
+### Q: Where can I find Dresses?
+Browse our Dresses category: https://www.yehwang.com/factory-direct-wholesale-en/clothes/dresses
+
+### Q: Where can I find Tops?
+Browse our Tops category: https://www.yehwang.com/factory-direct-wholesale-en/clothes/tops
+
+### Q: Where can I find Women And 039 S Knitwear?
+Browse our Women And 039 S Knitwear category: https://www.yehwang.com/factory-direct-wholesale-en/clothes/women-and-039-s-knitwear
+
+### Q: Where can I find Diy Letter And Cord Jewelry?
+Browse our Diy Letter And Cord Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/diy-letter-and-cord-jewelry
+
+### Q: Where can I find Diy Pendants?
+Browse our Diy Pendants category: https://www.yehwang.com/factory-direct-wholesale-en/diy-pendants
+
+### Q: Where can I find Chains?
+Browse our Chains category: https://www.yehwang.com/factory-direct-wholesale-en/diy-pendants/chains
+
+### Q: Where can I find Diy Tools?
+Browse our Diy Tools category: https://www.yehwang.com/factory-direct-wholesale-en/diy-pendants/diy-tools
+
+### Q: Where can I find Jewelry Beads?
+Browse our Jewelry Beads category: https://www.yehwang.com/factory-direct-wholesale-en/diy-pendants/jewelry-beads
+
+### Q: Where can I find Factory Direct Wholesale Sale?
+Browse our Factory Direct Wholesale Sale category: https://www.yehwang.com/factory-direct-wholesale-en/factory-direct-wholesale-sale
+
+### Q: Where can I find Fall And Winter Collection?
+Browse our Fall And Winter Collection category: https://www.yehwang.com/factory-direct-wholesale-en/fall-and-winter-collection
+
+### Q: Where can I find F W Clothing?
+Browse our F W Clothing category: https://www.yehwang.com/factory-direct-wholesale-en/fall-and-winter-collection/f-w-clothing
+
+### Q: Where can I find Fw Bags?
+Browse our Fw Bags category: https://www.yehwang.com/factory-direct-wholesale-en/fall-and-winter-collection/fw-bags
+
+### Q: Where can I find Fw Hats?
+Browse our Fw Hats category: https://www.yehwang.com/factory-direct-wholesale-en/fall-and-winter-collection/fw-hats
+
+### Q: Where can I find Fw Jewelry?
+Browse our Fw Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/fall-and-winter-collection/fw-jewelry
+
+### Q: Where can I find Winter Scarves?
+Browse our Winter Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/fall-and-winter-collection/winter-scarves
+
+### Q: Where can I find Hair Accessories?
+Browse our Hair Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories
+
+### Q: Where can I find Combs?
+Browse our Combs category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories/combs
+
+### Q: Where can I find Hair Bands?
+Browse our Hair Bands category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories/hair-bands
+
+### Q: Where can I find Hair Claws?
+Browse our Hair Claws category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories/hair-claws
+
+### Q: Where can I find Hair Clips?
+Browse our Hair Clips category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories/hair-clips
+
+### Q: Where can I find Hair Ties?
+Browse our Hair Ties category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories/hair-ties
+
+### Q: Where can I find Scrunchies?
+Browse our Scrunchies category: https://www.yehwang.com/factory-direct-wholesale-en/hair-accessories/scrunchies
+
+### Q: Where can I find Glasses?
+Browse our Glasses category: https://www.yehwang.com/factory-direct-wholesale-en/hats-and-scarves/glasses
+
+### Q: Where can I find Aviator Glasses?
+Browse our Aviator Glasses category: https://www.yehwang.com/factory-direct-wholesale-en/hats-and-scarves/glasses/aviator-glasses
+
+### Q: Where can I find Non Prescription Glasses?
+Browse our Non Prescription Glasses category: https://www.yehwang.com/factory-direct-wholesale-en/hats-and-scarves/glasses/non-prescription-glasses
+
+### Q: Where can I find Retro Cat Eye?
+Browse our Retro Cat Eye category: https://www.yehwang.com/factory-direct-wholesale-en/hats-and-scarves/glasses/retro-cat-eye
+
+### Q: Where can I find Sunglasses Accessories?
+Browse our Sunglasses Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/hats-and-scarves/glasses/sunglasses-accessories
+
+### Q: Where can I find High End Jewelry?
+Browse our High End Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/high-end-jewelry
+
+### Q: Where can I find 925 Sterling Silver Jewelry?
+Browse our 925 Sterling Silver Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/high-end-jewelry/925-sterling-silver-jewelry
+
+### Q: Where can I find Moissanite Jewelry?
+Browse our Moissanite Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/high-end-jewelry/moissanite-jewelry
+
+### Q: Where can I find Beaded Jewelry?
+Browse our Beaded Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/hot-collection/beaded-jewelry
+
+### Q: Where can I find Diamond Series?
+Browse our Diamond Series category: https://www.yehwang.com/factory-direct-wholesale-en/hot-collection/diamond-series
+
+### Q: Where can I find Gold Plated Jewelry?
+Browse our Gold Plated Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/hot-collection/gold-plated-jewelry
+
+### Q: Where can I find Heart Jewelry?
+Browse our Heart Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/hot-collection/heart-jewelry
+
+### Q: Where can I find Pearl Jewelry 3432?
+Browse our Pearl Jewelry 3432 category: https://www.yehwang.com/factory-direct-wholesale-en/hot-collection/pearl-jewelry-3432
+
+### Q: Where can I find Jewelry?
+Browse our Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry
+
+### Q: Where can I find Jewelry Sets?
+Browse our Jewelry Sets category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry-sets
+
+### Q: Where can I find Bracelets?
+Browse our Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry/bracelets
+
+### Q: Where can I find Earrings?
+Browse our Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry/earrings
+
+### Q: Where can I find Statement Earrings?
+Browse our Statement Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry/earrings/statement-earrings
+
+### Q: Where can I find Necklaces?
+Browse our Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry/necklaces
+
+### Q: Where can I find Rings?
+Browse our Rings category: https://www.yehwang.com/factory-direct-wholesale-en/jewelry/rings
+
+### Q: Where can I find New By Date?
+Browse our New By Date category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date
+
+### Q: Where can I find Bag Charms?
+Browse our Bag Charms category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/bag-charms
+
+### Q: Where can I find Bags?
+Browse our Bags category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/bags
+
+### Q: Where can I find Beads?
+Browse our Beads category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/beads
+
+### Q: Where can I find Body Jewelry?
+Browse our Body Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/body-jewelry
+
+### Q: Where can I find Clothes?
+Browse our Clothes category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/clothes
+
+### Q: Where can I find Glasses?
+Browse our Glasses category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/glasses
+
+### Q: Where can I find Hair Accessories?
+Browse our Hair Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/hair-accessories
+
+### Q: Where can I find Hats?
+Browse our Hats category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/hats
+
+### Q: Where can I find Jewelry Sets?
+Browse our Jewelry Sets category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/jewelry-sets
+
+### Q: Where can I find Keychain?
+Browse our Keychain category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/keychain
+
+### Q: Where can I find Making Supplies?
+Browse our Making Supplies category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/making-supplies
+
+### Q: Where can I find New In Accessories?
+Browse our New In Accessories category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/new-in-accessories
+
+### Q: Where can I find New In Jewelry?
+Browse our New In Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/new-in-jewelry
+
+### Q: Where can I find Packing And Display?
+Browse our Packing And Display category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/packing-and-display
+
+### Q: Where can I find Pendants?
+Browse our Pendants category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/pendants
+
+### Q: Where can I find Piercing Jewelry?
+Browse our Piercing Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/piercing-jewelry
+
+### Q: Where can I find Socks?
+Browse our Socks category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/socks
+
+### Q: Where can I find Summer Scarves?
+Browse our Summer Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/summer-scarves
+
+### Q: Where can I find Winter Scarves?
+Browse our Winter Scarves category: https://www.yehwang.com/factory-direct-wholesale-en/new-by-date/winter-scarves
+
+### Q: Where can I find Packing And Display?
+Browse our Packing And Display category: https://www.yehwang.com/factory-direct-wholesale-en/packing-and-display
+
+### Q: Where can I find Festive Packaging?
+Browse our Festive Packaging category: https://www.yehwang.com/factory-direct-wholesale-en/packing-and-display/festive-packaging
+
+### Q: Where can I find Packaging Cards?
+Browse our Packaging Cards category: https://www.yehwang.com/factory-direct-wholesale-en/packing-and-display/packaging-cards
+
+### Q: Where can I find Packaging Materials?
+Browse our Packaging Materials category: https://www.yehwang.com/factory-direct-wholesale-en/packing-and-display/packaging-materials
+
+### Q: Where can I find Party Packaging?
+Browse our Party Packaging category: https://www.yehwang.com/factory-direct-wholesale-en/packing-and-display/party-packaging
+
+### Q: Where can I find Piercing Jewelry?
+Browse our Piercing Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/piercing-jewelry
+
+### Q: Where can I find Nose Stud?
+Browse our Nose Stud category: https://www.yehwang.com/factory-direct-wholesale-en/piercing-jewelry/nose-stud
+
+### Q: Where can I find Piercings Earrings?
+Browse our Piercings Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/piercing-jewelry/piercings-earrings
+
+### Q: Where can I find Cute Jewelry?
+Browse our Cute Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/so-cute-hair-accessories-10-off/cute-jewelry
+
+### Q: Where can I find Sports And Outdoor?
+Browse our Sports And Outdoor category: https://www.yehwang.com/factory-direct-wholesale-en/sports-and-outdoor
+
+### Q: Where can I find Stationery And Office Supplies?
+Browse our Stationery And Office Supplies category: https://www.yehwang.com/factory-direct-wholesale-en/stationery-and-office-supplies
+
+### Q: Where can I find Trends?
+Browse our Trends category: https://www.yehwang.com/factory-direct-wholesale-en/trends
+
+### Q: Where can I find Acrylic And Resin Jewelry?
+Browse our Acrylic And Resin Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/trends/acrylic-and-resin-jewelry
+
+### Q: Where can I find Back To School?
+Browse our Back To School category: https://www.yehwang.com/factory-direct-wholesale-en/trends/back-to-school
+
+### Q: Where can I find Lace Collection?
+Browse our Lace Collection category: https://www.yehwang.com/factory-direct-wholesale-en/trends/lace-collection
+
+### Q: Where can I find Stainless Steel Zircon Jewelry?
+Browse our Stainless Steel Zircon Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/trends/stainless-steel-zircon-jewelry
+
+### Q: Where can I find Statement Jewelry?
+Browse our Statement Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/trends/statement-jewelry
+
+### Q: Where can I find Weekly Jewelry Offers?
+Browse our Weekly Jewelry Offers category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers
+
+### Q: Where can I find Braided Jewelry?
+Browse our Braided Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/braided-jewelry
+
+### Q: Where can I find Stainless Steel Jewelry?
+Browse our Stainless Steel Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/stainless-steel-jewelry
+
+### Q: Where can I find Stainless Steel Anklets?
+Browse our Stainless Steel Anklets category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/stainless-steel-jewelry/stainless-steel-anklets
+
+### Q: Where can I find Stainless Steel Bracelets?
+Browse our Stainless Steel Bracelets category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/stainless-steel-jewelry/stainless-steel-bracelets
+
+### Q: Where can I find Stainless Steel Earrings?
+Browse our Stainless Steel Earrings category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/stainless-steel-jewelry/stainless-steel-earrings
+
+### Q: Where can I find Stainless Steel Necklaces?
+Browse our Stainless Steel Necklaces category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/stainless-steel-jewelry/stainless-steel-necklaces
+
+### Q: Where can I find Stainless Steel Rings?
+Browse our Stainless Steel Rings category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/stainless-steel-jewelry/stainless-steel-rings
+
+### Q: Where can I find Zircon Jewelry?
+Browse our Zircon Jewelry category: https://www.yehwang.com/factory-direct-wholesale-en/weekly-jewelry-offers/zircon-jewelry
+
+### Q: Where can I find Jewelry?
+Browse our Jewelry category: https://www.yehwang.com/jewelry
+
+### Q: Where can I find Advent Calendars?
+Browse our Advent Calendars category: https://www.yehwang.com/jewelry/advent-calendars
+
+### Q: Where can I find Anklets?
+Browse our Anklets category: https://www.yehwang.com/jewelry/Anklets
+
+### Q: Where can I find Bracelets?
+Browse our Bracelets category: https://www.yehwang.com/jewelry/bracelets
+
+### Q: Where can I find Bangles?
+Browse our Bangles category: https://www.yehwang.com/jewelry/bracelets/bangles
+
+### Q: Where can I find Bangles And Cuffs Bracelet?
+Browse our Bangles And Cuffs Bracelet category: https://www.yehwang.com/jewelry/bracelets/bangles-and-cuffs-bracelet
+
+### Q: Where can I find Beaded Bracelets?
+Browse our Beaded Bracelets category: https://www.yehwang.com/jewelry/bracelets/beaded-bracelets
+
+### Q: Where can I find Bracelets Sets?
+Browse our Bracelets Sets category: https://www.yehwang.com/jewelry/bracelets/bracelets-sets
+
+### Q: Where can I find Braided Bracelets?
+Browse our Braided Bracelets category: https://www.yehwang.com/jewelry/bracelets/braided-bracelets
+
+### Q: Where can I find Chain Bracelets?
+Browse our Chain Bracelets category: https://www.yehwang.com/jewelry/bracelets/chain-bracelets
+
+### Q: Where can I find Charm Bracelets?
+Browse our Charm Bracelets category: https://www.yehwang.com/jewelry/bracelets/charm-bracelets
+
+### Q: Where can I find Chunky Bracelets?
+Browse our Chunky Bracelets category: https://www.yehwang.com/jewelry/bracelets/chunky-bracelets
+
+### Q: Where can I find Hand Chain Bracelets?
+Browse our Hand Chain Bracelets category: https://www.yehwang.com/jewelry/bracelets/hand-chain-bracelets
+
+### Q: Where can I find Brooches?
+Browse our Brooches category: https://www.yehwang.com/jewelry/brooches
+
+### Q: Where can I find Cufflinks?
+Browse our Cufflinks category: https://www.yehwang.com/jewelry/cufflinks
+
+### Q: Where can I find Display Set En?
+Browse our Display Set En category: https://www.yehwang.com/jewelry/display-set-en
+
+### Q: Where can I find Anklets Display Set?
+Browse our Anklets Display Set category: https://www.yehwang.com/jewelry/display-set-en/anklets-display-set
+
+### Q: Where can I find Bracelets Display Set?
+Browse our Bracelets Display Set category: https://www.yehwang.com/jewelry/display-set-en/bracelets-display-set
+
+### Q: Where can I find Earrings Display Set?
+Browse our Earrings Display Set category: https://www.yehwang.com/jewelry/display-set-en/earrings-display-set
+
+### Q: Where can I find Necklace Display Set?
+Browse our Necklace Display Set category: https://www.yehwang.com/jewelry/display-set-en/necklace-display-set
+
+### Q: Where can I find Rings Display Set?
+Browse our Rings Display Set category: https://www.yehwang.com/jewelry/display-set-en/rings-display-set
+
+### Q: Where can I find Earrings?
+Browse our Earrings category: https://www.yehwang.com/jewelry/earrings
+
+### Q: Where can I find Beads Earrings?
+Browse our Beads Earrings category: https://www.yehwang.com/jewelry/earrings/beads-earrings
+
+### Q: Where can I find Birthstone Earrings?
+Browse our Birthstone Earrings category: https://www.yehwang.com/jewelry/earrings/birthstone-earrings
+
+### Q: Where can I find Drop Earrings?
+Browse our Drop Earrings category: https://www.yehwang.com/jewelry/earrings/drop-earrings
+
+### Q: Where can I find Ear Cuffs?
+Browse our Ear Cuffs category: https://www.yehwang.com/jewelry/earrings/ear-cuffs
+
+### Q: Where can I find Ear Studs?
+Browse our Ear Studs category: https://www.yehwang.com/jewelry/earrings/ear-studs
+
+### Q: Where can I find Earring Set?
+Browse our Earring Set category: https://www.yehwang.com/jewelry/earrings/earring-set
+
+### Q: Where can I find Earrings With Stone?
+Browse our Earrings With Stone category: https://www.yehwang.com/jewelry/earrings/earrings-with-stone
+
+### Q: Where can I find Hoop Earrings?
+Browse our Hoop Earrings category: https://www.yehwang.com/jewelry/earrings/hoop-earrings
+
+### Q: Where can I find Party Earrings?
+Browse our Party Earrings category: https://www.yehwang.com/jewelry/earrings/party-earrings
+
+### Q: Where can I find Studs?
+Browse our Studs category: https://www.yehwang.com/jewelry/earrings/studs
+
+### Q: Where can I find Jewelry Collections?
+Browse our Jewelry Collections category: https://www.yehwang.com/jewelry/jewelry-collections
+
+### Q: Where can I find Acrylic And Resin Collection?
+Browse our Acrylic And Resin Collection category: https://www.yehwang.com/jewelry/jewelry-collections/acrylic-and-resin-collection
+
+### Q: Where can I find Beach Collection?
+Browse our Beach Collection category: https://www.yehwang.com/jewelry/jewelry-collections/beach-collection
+
+### Q: Where can I find Flowers Collection?
+Browse our Flowers Collection category: https://www.yehwang.com/jewelry/jewelry-collections/flowers-collection
+
+### Q: Where can I find Fruits And Plants Collection?
+Browse our Fruits And Plants Collection category: https://www.yehwang.com/jewelry/jewelry-collections/fruits-and-plants-collection
+
+### Q: Where can I find Hearts Collection?
+Browse our Hearts Collection category: https://www.yehwang.com/jewelry/jewelry-collections/hearts-collection
+
+### Q: Where can I find Jewelry Parts?
+Browse our Jewelry Parts category: https://www.yehwang.com/jewelry/jewelry-parts
+
+### Q: Where can I find Jewelry Beads?
+Browse our Jewelry Beads category: https://www.yehwang.com/jewelry/jewelry-parts/jewelry-beads
+
+### Q: Where can I find Jewelry Charms?
+Browse our Jewelry Charms category: https://www.yehwang.com/jewelry/jewelry-parts/jewelry-charms
+
+### Q: Where can I find Jewelry Working Supplies?
+Browse our Jewelry Working Supplies category: https://www.yehwang.com/jewelry/jewelry-parts/jewelry-working-supplies
+
+### Q: Where can I find Men S Bracelets?
+Browse our Men S Bracelets category: https://www.yehwang.com/jewelry/men-s-bracelets
+
+### Q: Where can I find Men S Jewelry?
+Browse our Men S Jewelry category: https://www.yehwang.com/jewelry/men-s-jewelry
+
+### Q: Where can I find Men S Necklaces?
+Browse our Men S Necklaces category: https://www.yehwang.com/jewelry/men-s-necklaces
+
+### Q: Where can I find Navel Piercings?
+Browse our Navel Piercings category: https://www.yehwang.com/jewelry/navel-piercings
+
+### Q: Where can I find Necklaces?
+Browse our Necklaces category: https://www.yehwang.com/jewelry/necklaces
+
+### Q: Where can I find Beaded Necklaces?
+Browse our Beaded Necklaces category: https://www.yehwang.com/jewelry/necklaces/beaded-necklaces
+
+### Q: Where can I find Braided Necklaces?
+Browse our Braided Necklaces category: https://www.yehwang.com/jewelry/necklaces/braided-necklaces
+
+### Q: Where can I find Chain Necklaces?
+Browse our Chain Necklaces category: https://www.yehwang.com/jewelry/necklaces/chain-necklaces
+
+### Q: Where can I find Layered Necklace?
+Browse our Layered Necklace category: https://www.yehwang.com/jewelry/necklaces/layered-necklace
+
+### Q: Where can I find Long Necklaces?
+Browse our Long Necklaces category: https://www.yehwang.com/jewelry/necklaces/long-necklaces
+
+### Q: Where can I find Medium Necklaces?
+Browse our Medium Necklaces category: https://www.yehwang.com/jewelry/necklaces/medium-necklaces
+
+### Q: Where can I find Necklaces Sets?
+Browse our Necklaces Sets category: https://www.yehwang.com/jewelry/necklaces/necklaces-sets
+
+### Q: Where can I find Pendant Necklaces?
+Browse our Pendant Necklaces category: https://www.yehwang.com/jewelry/necklaces/pendant-necklaces
+
+### Q: Where can I find Short Necklaces 5516?
+Browse our Short Necklaces 5516 category: https://www.yehwang.com/jewelry/necklaces/short-necklaces-5516
+
+### Q: Where can I find Y Necklaces?
+Browse our Y Necklaces category: https://www.yehwang.com/jewelry/necklaces/y-necklaces
+
+### Q: Where can I find Rings?
+Browse our Rings category: https://www.yehwang.com/jewelry/rings
+
+### Q: Where can I find Adjustable Rings 5530?
+Browse our Adjustable Rings 5530 category: https://www.yehwang.com/jewelry/rings/adjustable-rings-5530
+
+### Q: Where can I find Birthstone Rings?
+Browse our Birthstone Rings category: https://www.yehwang.com/jewelry/rings/birthstone-rings
+
+### Q: Where can I find Gemstone Rings?
+Browse our Gemstone Rings category: https://www.yehwang.com/jewelry/rings/gemstone-rings
+
+### Q: Where can I find Minimalist Rings?
+Browse our Minimalist Rings category: https://www.yehwang.com/jewelry/rings/minimalist-rings
+
+### Q: Where can I find Rhinestone Rings?
+Browse our Rhinestone Rings category: https://www.yehwang.com/jewelry/rings/rhinestone-rings
+
+### Q: Where can I find Rings With Sizes?
+Browse our Rings With Sizes category: https://www.yehwang.com/jewelry/rings/rings-with-sizes
+
+### Q: Where can I find Stainless Steel Jewelry?
+Browse our Stainless Steel Jewelry category: https://www.yehwang.com/jewelry/stainless-steel-jewelry
+
+### Q: Where can I find New?
+Browse our New category: https://www.yehwang.com/new
+
+### Q: Where can I find Accessories 670685702?
+Browse our Accessories 670685702 category: https://www.yehwang.com/new/accessories-670685702
+
+### Q: Where can I find Backinstock 3536?
+Browse our Backinstock 3536 category: https://www.yehwang.com/new/backinstock-3536
+
+### Q: Where can I find Clothes 297809159?
+Browse our Clothes 297809159 category: https://www.yehwang.com/new/clothes-297809159
+
+### Q: Where can I find Jewelry 918809724?
+Browse our Jewelry 918809724 category: https://www.yehwang.com/new/jewelry-918809724
+
+### Q: Where can I find Pre Order?
+Browse our Pre Order category: https://www.yehwang.com/new/pre-order
+
+### Q: Where can I find Packing And Display?
+Browse our Packing And Display category: https://www.yehwang.com/packing-and-display
+
+### Q: Where can I find Wholesale Display Materials?
+Browse our Wholesale Display Materials category: https://www.yehwang.com/packing-and-display/wholesale-display-materials
+
+### Q: Where can I find Displays For Bracelets?
+Browse our Displays For Bracelets category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-bracelets
+
+### Q: Where can I find Displays For Earrings?
+Browse our Displays For Earrings category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-earrings
+
+### Q: Where can I find Displays For Necklaces?
+Browse our Displays For Necklaces category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-necklaces
+
+### Q: Where can I find Displays For Rings?
+Browse our Displays For Rings category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-rings
+
+### Q: Where can I find Jewelry Trays?
+Browse our Jewelry Trays category: https://www.yehwang.com/packing-and-display/wholesale-display-materials/jewelry-trays
+
+### Q: Where can I find Wholesale Packing Materials?
+Browse our Wholesale Packing Materials category: https://www.yehwang.com/packing-and-display/wholesale-packing-materials
+
+### Q: Where can I find Restock Request?
+Browse our Restock Request category: https://www.yehwang.com/restock-request
+
+### Q: Where can I find Top Deal?
+Browse our Top Deal category: https://www.yehwang.com/top-deal
+
+### Q: Where can I find 0 12 Zone?
+Browse our 0 12 Zone category: https://www.yehwang.com/top-deal/12-12-sale/0-12-zone
+
+### Q: Where can I find Daily Deals?
+Browse our Daily Deals category: https://www.yehwang.com/top-deal/daily-deals
+
+### Q: Where can I find New In Sale?
+Browse our New In Sale category: https://www.yehwang.com/top-deal/new-in-sale
+
+### Q: Where can I find 1040045430 50 Off?
+Browse our 1040045430 50 Off category: https://www.yehwang.com/top-deal/new-in-sale/1040045430-50-off
+
+### Q: Where can I find 20 30 Off?
+Browse our 20 30 Off category: https://www.yehwang.com/top-deal/new-in-sale/20-30-off
+
+### Q: Where can I find 2113133623 20 Off?
+Browse our 2113133623 20 Off category: https://www.yehwang.com/top-deal/new-in-sale/2113133623-20-off
+
+### Q: Where can I find 30 40 Off?
+Browse our 30 40 Off category: https://www.yehwang.com/top-deal/new-in-sale/30-40-off
+
+### Q: Where can I find 40 50 Off?
+Browse our 40 50 Off category: https://www.yehwang.com/top-deal/new-in-sale/40-50-off
+
+### Q: Where can I find Sale En?
+Browse our Sale En category: https://www.yehwang.com/top-deal/sale-en
+
+### Q: Where can I find Accessories?
+Browse our Accessories category: https://www.yehwang.com/top-deal/sale-en/accessories
+
+### Q: Where can I find Clothes 686?
+Browse our Clothes 686 category: https://www.yehwang.com/top-deal/sale-en/clothes-686
+
+### Q: Where can I find Jewelry?
+Browse our Jewelry category: https://www.yehwang.com/top-deal/sale-en/jewelry
+
+### Q: Where can I find Packing And Display?
+Browse our Packing And Display category: https://www.yehwang.com/top-deal/sale-en/packing-and-display
+
+### Q: Where can I find Wholesale Service?
+Browse our Wholesale Service category: https://www.yehwang.com/wholesale-service
+
+### Q: Where can I find Private Label?
+Browse our Private Label category: https://www.yehwang.com/wholesale-service/private-label
diff --git a/bridge/src/agent.js b/bridge/src/agent.js
index e490c22..10b58db 100644
--- a/bridge/src/agent.js
+++ b/bridge/src/agent.js
@@ -12,10 +12,17 @@ const { markdownToHtml } = require('./markdown');
const { guardReply } = require('./output-guard');
const { quickClassify, detectEmotion, INTENTS } = require('./intent');
const { createContextManager } = require('./context');
+const { createReranker } = require('./reranker');
const { detectInjection, markToolResult, INJECTION_CAUTION_PROMPT } = require('./security');
const { hasBuyingIntent, missingLeadContacts, detectLanguage, appendLeadFollowup, alreadyAsksForLead } = require('./lead-intent');
const { createLlmCache } = require('./llm-cache');
+const { checkCitation } = require('./citation-checker');
+const { createFlywheelWriter } = require('./flywheel');
+const { languageMandate } = require('./language');
+const { createEscalation } = require('./escalation');
+const { createExperimentManager } = require('./experiments');
const crypto = require('crypto');
+const prom = require('./prom');
const MAX_TOOL_STEPS = 4;
@@ -40,7 +47,18 @@ function createAgent(cfg, deps = {}) {
const metrics = deps.metrics || createMetrics(cfg);
const contextManager = deps.contextManager || createContextManager();
const llmCache = deps.llmCache || createLlmCache(cfg);
- const tools = deps.tools || createTools(cfg, { verification, retriever, client });
+ const escalation = deps.escalation || createEscalation({ client, handoffUserId: cfg.handoffUserId || 1 });
+ const experiments = deps.experiments || createExperimentManager(cfg);
+ const flywheel = deps.flywheel || createFlywheelWriter({
+ pool: metrics.pool,
+ callLLM: async (messages, system, maxTokens) => {
+ const msgs = system ? [{ role: 'system', content: system }, ...messages] : messages;
+ const res = await llm.chat({ messages: msgs, maxTokens: maxTokens || 300 });
+ return { text: res.content || '' };
+ },
+ });
+ const reranker = deps.reranker || createReranker(llm);
+ const tools = deps.tools || createTools(cfg, { verification, retriever, client, reranker });
const leadStore = deps.leadStore || (tools && tools.leadStore) || null;
const byName = new Map(tools.map((t) => [t.name, t]));
const toolDefs = tools.map((t) => ({
@@ -87,6 +105,7 @@ function createAgent(cfg, deps = {}) {
const startedAt = Date.now();
const stats = { llmCalls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, toolCalls: [], toolResults: [] };
const profile = resolveSiteProfile(cfg, { inboxId: String(conv.inbox_id || '') });
+ const variant = experiments.getVariant(conv.uuid);
const identity = await identityFor(conv);
const ctx = {
verified: identity.verified,
@@ -97,6 +116,7 @@ function createAgent(cfg, deps = {}) {
};
const currentText = (webhookMessage.text_content || stripHtml(webhookMessage.content)).trim();
+ ctx.language = detectLanguage(currentText);
const messages = await buildMessages(conv, webhookMessage);
if (!messages.length) return null;
const injection = detectInjection(currentText);
@@ -107,6 +127,8 @@ function createAgent(cfg, deps = {}) {
channel: conv.channel || 'livechat',
}) + (injection.level === 'high' ? INJECTION_CAUTION_PROMPT : '');
+ systemContent += languageMandate(currentText);
+
// 购买意向:LLM 之前先给硬指令,LLM 之后还有程序化兜底
let leadState = null;
if (leadStore) {
@@ -126,6 +148,8 @@ function createAgent(cfg, deps = {}) {
// 预检索:政策问题先查 KB,商品问题先查商品,避免模型跳过工具直接编。
const intent = quickClassify(currentText);
const emotion = detectEmotion(currentText);
+ const emotionStrategy = escalation.applyEmotionStrategy(emotion);
+ const maxSteps = emotionStrategy.maxToolTurns || MAX_TOOL_STEPS;
if (emotion !== 'neutral') {
const toneLine = emotion === 'angry'
? 'TONE: The customer is angry. Acknowledge their frustration, be empathetic, and escalate/hand off if needed.'
@@ -180,12 +204,19 @@ function createAgent(cfg, deps = {}) {
toolCalls: stats.toolCalls, audit: { ...auditAnswer({ finalContent: cached, toolResults: stats.toolResults, toolCalls: stats.toolCalls }), cache_hit: true },
replyLength: cached.length,
});
+ prom.inc('bridge_ai_cache_hits_total');
console.log(JSON.stringify({ ts: new Date().toISOString(), agent: 'cache_hit', conversation: conv.uuid, length: cached.length }));
return cached;
}
}
- for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
- const response = await llm.chat({ messages, tools: toolDefs, temperature: 0.3, maxTokens: 1200 });
+ for (let step = 0; step < maxSteps; step += 1) {
+ prom.inc('bridge_ai_llm_calls_total');
+ const response = await llm.chat({
+ messages,
+ tools: toolDefs,
+ temperature: (variant.overriddenParams && variant.overriddenParams.temperature) ?? 0.3,
+ maxTokens: (variant.overriddenParams && variant.overriddenParams.maxTokens) ?? 1200,
+ });
stats.llmCalls += 1;
if (response.usage) {
stats.promptTokens += Number(response.usage.prompt_tokens) || 0;
@@ -210,6 +241,7 @@ function createAgent(cfg, deps = {}) {
} catch (err) {
result = { error: err.code || err.message || 'tool_failed', message: err.message || String(err) };
}
+ prom.inc('bridge_ai_tool_calls_total', { tool: name, ok: !(result && result.error) });
stats.toolCalls.push({ name, ms: Date.now() - started, ok: !(result && result.error) });
stats.toolResults.push(result);
console.log(JSON.stringify({
@@ -256,6 +288,7 @@ function createAgent(cfg, deps = {}) {
toolCalls: stats.toolCalls,
lang: detectLanguage(currentText),
});
+ if (guard.blocked) prom.inc('bridge_ai_guard_blocked_total', { reason: (guard.reasons || []).join('|') || 'unknown' });
if (guard.blocked || guard.removed_urls.length) {
console.log(JSON.stringify({
ts: new Date().toISOString(),
@@ -270,6 +303,57 @@ function createAgent(cfg, deps = {}) {
}
finalContent = guard.content;
+ // 引用一致性检查(语义/数字覆盖)+ 飞轮
+ const kbHits = [];
+ const kbSources = [];
+ (stats.toolCalls || []).forEach((call, i) => {
+ if (call.name !== 'search_kb') return;
+ const result = stats.toolResults[i];
+ for (const hit of (result && result.results) || []) {
+ kbHits.push(hit);
+ for (const u of hit.source_urls || []) kbSources.push(u);
+ }
+ });
+ const citation = checkCitation(finalContent, kbHits, intent, [...new Set(kbSources)]);
+
+ // 三级升级 + 会话状态
+ let escalationLevel = 0;
+ let escalationReason = null;
+ try {
+ const latestState = (leadStore && await leadStore.get(conv.uuid)) || {};
+ const state = {
+ ...latestState,
+ messagesProcessed: (Number(latestState.messagesProcessed) || 0) + 1,
+ consecutiveFails: Number(latestState.consecutiveFails) || 0,
+ };
+ const topScore = kbHits.length ? Number(kbHits[0].score) || 0 : 0;
+ const kbConfidence = topScore >= 0.7 ? 'high' : topScore >= 0.55 ? 'medium' : topScore > 0 ? 'low' : 'none';
+ const decision = escalation.shouldEscalate(
+ latestState,
+ intent,
+ kbConfidence,
+ Number(latestState.consecutiveFails) || 0,
+ kbHits.length
+ );
+ escalationLevel = decision.level;
+ prom.inc('bridge_ai_escalations_total', { level: decision.level });
+ escalationReason = decision.reason;
+ if (decision.level === 1 && !/handoff|human|人工/i.test(finalContent)) {
+ finalContent = escalation.appendEscalationOption(finalContent, currentText);
+ } else if (decision.level === 2) {
+ await escalation.escalateToHuman({ convUuid: conv.uuid, trigger: decision.reason, history: messages.slice(-6) });
+ const zh = /[\u4e00-\u9fff]/.test(currentText);
+ const line = zh ? '\n\n已为您转接人工同事,请稍候。' : '\n\nI have transferred you to a human colleague.';
+ if (!finalContent.includes(line.trim())) finalContent += line;
+ }
+ state.consecutiveFails = (kbConfidence === 'low' || kbConfidence === 'none' || citation.hallucination_flag === true) ? state.consecutiveFails + 1 : 0;
+ state.kbConfidence = kbConfidence;
+ if (leadStore && leadStore.saveState) await leadStore.saveState(conv.uuid, state);
+ else if (leadStore && leadStore.save) await leadStore.save(conv.uuid, state.slots || {});
+ } catch (err) {
+ console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `escalation failed: ${err.message}` }));
+ }
+
const alreadyHtml = /<(p|br|a|ul|ol|strong|div)\b/i.test(finalContent);
const outboundContent = alreadyHtml ? finalContent : markdownToHtml(finalContent);
await client.sendMessage(conv.uuid, outboundContent, { echoId: `bridge-ai-${webhookMessage.uuid}` });
@@ -284,8 +368,40 @@ function createAgent(cfg, deps = {}) {
reply: finalContent,
});
}
+ if (citation.hallucination_flag === true || (citation.citation_score !== null && citation.citation_score < 0.5)) {
+ try {
+ const sampleId = await flywheel.captureSample({
+ convId: conv.uuid,
+ query: currentText,
+ reply: finalContent,
+ intent,
+ kbHits: kbHits.slice(0, 10),
+ signal: citation.hallucination_flag === true ? 'hallucination' : 'low_citation',
+ citationScore: citation.citation_score,
+ });
+ if (sampleId) {
+ setImmediate(async () => {
+ try {
+ const review = await flywheel.preLabel({ query: currentText, reply: finalContent, kbHits });
+ if (review) await flywheel.updateReview(sampleId, review);
+ } catch { /* noop */ }
+ });
+ }
+ await client.sendPrivateNote(conv.uuid, `[⚠️ Citation Alert] score=${citation.citation_score}, severity=${citation.severity || 'low'}. This reply may need review.`).catch(() => {});
+ console.log(JSON.stringify({ ts: new Date().toISOString(), agent: 'citation_alert', conversation: conv.uuid, citation_score: citation.citation_score, severity: citation.severity }));
+ } catch (err) {
+ console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `citation/flywheel failed: ${err.message}` }));
+ }
+ }
+
const audit = auditAnswer({ finalContent, toolResults: stats.toolResults, toolCalls: stats.toolCalls });
audit.injection_level = injection.level;
+ audit.escalation_level = escalationLevel;
+ audit.escalation_reason = escalationReason;
+ audit.experiment = { id: variant.experiment_id, variant: variant.variant };
+ audit.citation_score = citation.citation_score;
+ audit.hallucination_flag = citation.hallucination_flag;
+ audit.uncovered_entities = citation.uncovered_entities;
audit.lead_followup = leadFollowup;
audit.output_guard = { blocked: guard.blocked, rewritten: guard.rewritten, reasons: guard.reasons, removed_urls: guard.removed_urls, evidence_missing: guard.evidence_missing };
await metrics.record({
@@ -303,6 +419,7 @@ function createAgent(cfg, deps = {}) {
audit,
replyLength: finalContent.length,
});
+ prom.inc('bridge_ai_runs_total');
console.log(JSON.stringify({ ts: new Date().toISOString(), agent: 'replied', conversation: conv.uuid, length: finalContent.length, audit }));
return finalContent;
}
diff --git a/bridge/src/chat-log.js b/bridge/src/chat-log.js
new file mode 100644
index 0000000..55e540c
--- /dev/null
+++ b/bridge/src/chat-log.js
@@ -0,0 +1,36 @@
+'use strict';
+
+function stripHtml(value) {
+ return String(value || '')
+ .replace(/
/gi, '\n')
+ .replace(/<[^>]+>/g, ' ')
+ .replace(/ /g, ' ')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/\s+/g, ' ')
+ .trim();
+}
+
+// 把完整会话记录作为 Note 挂到商机(旧 bridge crm/chatLogger 的 LibreDesk 版)。
+async function attachChatToOpportunity({ client, twenty, convUuid, opportunityId, maxMessages = 100 }) {
+ if (!client || !twenty || !convUuid || !opportunityId) return null;
+ const messages = await client.getMessages(convUuid, maxMessages);
+ const lines = (messages || [])
+ .filter((m) => m && !m.private && (m.text_content || m.content))
+ .sort((a, b) => Number(a.id) - Number(b.id))
+ .map((m) => {
+ const role = m.sender_type === 'contact' ? 'Customer' : 'Agent';
+ return `[${role}] ${stripHtml(m.text_content || m.content).slice(0, 300)}`;
+ })
+ .filter((line) => line.trim().length > 12);
+ if (!lines.length) return null;
+ const text = lines.join('\n\n---\n\n').slice(0, 20000);
+ const noteId = await twenty.createNote(text);
+ if (noteId) await twenty.linkNoteToOpportunity(noteId, opportunityId);
+ console.log(JSON.stringify({ ts: new Date().toISOString(), crm: 'chat_note', conversation: convUuid, opportunity: opportunityId, note_id: noteId }));
+ return noteId;
+}
+
+module.exports = { attachChatToOpportunity, stripHtml };
diff --git a/bridge/src/citation-checker.js b/bridge/src/citation-checker.js
new file mode 100644
index 0000000..29d7b40
--- /dev/null
+++ b/bridge/src/citation-checker.js
@@ -0,0 +1,314 @@
+// citation_checker.js — Entity extraction + coverage calculation + hallucination flagging
+// Shared between offline eval and online scoring paths
+
+const EU_COUNTRIES = new Set([
+ 'austria', 'belgium', 'bulgaria', 'croatia', 'cyprus', 'czech republic', 'czechia',
+ 'denmark', 'estonia', 'finland', 'france', 'germany', 'greece', 'hungary', 'ireland',
+ 'italy', 'latvia', 'lithuania', 'luxembourg', 'malta', 'netherlands', 'poland',
+ 'portugal', 'romania', 'slovakia', 'slovenia', 'spain', 'sweden',
+ // Also common forms / abbreviations
+ 'uk', 'united kingdom', 'england', 'scotland', 'wales', 'northern ireland',
+ // German names
+ 'deutschland', 'österreich', 'schweiz',
+ // French names
+ 'allemagne', 'pays-bas', 'espagne', 'italie', 'belgique',
+ // Dutch names
+ 'duitsland', 'nederland', 'belgië', 'spanje', 'italië', 'frankrijk',
+]);
+
+const POLICY_TERMS_EN = [
+ 'refund', 'return', 'shipping', 'discount', 'vat', 'membership', 'dropship',
+ 'factory-direct', 'pre-order', 'preorder', 'backorder', 'cancellation', 'cancel',
+ 'exchange', 'warranty', 'guarantee', 'minimum order', 'bulk order', 'wholesale',
+ 'retail', 'reseller', 'business', 'corporate', 'invoice', 'payment', 'delivery',
+ 'pickup', 'lead time', 'turnaround', 'restock', 'reorder', 'custom', 'personalize',
+ 'engrave', 'sample', 'catalog', 'brochure', 'newsletter', 'subscribe', 'unsubscribe',
+ 'account', 'register', 'login', 'sign up', 'password',
+];
+
+// Multilingual policy term translations (keyed by normalized term)
+const POLICY_TERMS_I18N = {
+ // German
+ 'rückerstattung': 'refund', 'rückgabe': 'return', 'versand': 'shipping',
+ 'versandkosten': 'shipping', 'rabatt': 'discount', 'mehrwertsteuer': 'vat',
+ 'mitgliedschaft': 'membership', 'stornierung': 'cancellation', 'stornieren': 'cancel',
+ 'rechnung': 'invoice', 'zahlung': 'payment', 'lieferung': 'delivery',
+ 'lieferzeit': 'delivery', 'großhandel': 'wholesale', 'katalog': 'catalog',
+ 'registrieren': 'register', 'konto': 'account',
+ // French
+ 'remboursement': 'refund', 'retour': 'return', 'expédition': 'shipping',
+ 'frais de livraison': 'shipping', 'livraison': 'delivery', 'remise': 'discount',
+ 'réduction': 'discount', 'tva': 'vat', 'adhésion': 'membership',
+ 'annulation': 'cancellation', 'annuler': 'cancel', 'facture': 'invoice',
+ 'paiement': 'payment', 'grossiste': 'wholesale', 'catalogue': 'catalog',
+ 'inscription': 'register', 'compte': 'account',
+ // Dutch
+ 'terugbetaling': 'refund', 'retour': 'return', 'retourneren': 'return',
+ 'verzending': 'shipping', 'verzendkosten': 'shipping', 'korting': 'discount',
+ 'btw': 'vat', 'lidmaatschap': 'membership', 'annulering': 'cancellation',
+ 'annuleren': 'cancel', 'factuur': 'invoice', 'betaling': 'payment',
+ 'groothandel': 'wholesale', 'catalogus': 'catalog', 'registreren': 'register',
+ 'account': 'account',
+ // Italian
+ 'rimborso': 'refund', 'reso': 'return', 'spedizione': 'shipping',
+ 'costi di spedizione': 'shipping', 'sconto': 'discount', 'iva': 'vat',
+ 'iscrizione': 'membership', 'cancellazione': 'cancellation', 'cancellare': 'cancel',
+ 'fattura': 'invoice', 'pagamento': 'payment', 'ingrosso': 'wholesale',
+ 'catalogo': 'catalog', 'registrare': 'register',
+};
+
+// Non-KB intents that should skip hallucination checking
+const NON_KB_INTENTS = new Set(['product_search', 'order_tracking', 'inventory_check', 'sales_lead']);
+
+/**
+ * Extract key entities from text.
+ * Entities: amounts (€/EUR), percentages, time limits (days/weeks/months),
+ * country names, policy terms, number+unit combos.
+ */
+function extractEntities(text) {
+ if (!text || typeof text !== 'string') return [];
+ const lower = text.toLowerCase();
+ const entities = new Set();
+
+ // Amounts: €200, EUR 200, 200€, 200 euros, 200 eur
+ const amountMatches = text.matchAll(/(?:€|EUR\s?|euros?\s?)\s?(\d+(?:[.,]\d+)?)/gi);
+ for (const m of amountMatches) {
+ entities.add(`amount:${m[1].replace(',', '.')}`);
+ }
+ const amountMatches2 = text.matchAll(/(\d+(?:[.,]\d+)?)\s?(?:€|EUR|euros?|eur)/gi);
+ for (const m of amountMatches2) {
+ entities.add(`amount:${m[1].replace(',', '.')}`);
+ }
+
+ // Percentages: 10%, 10 percent
+ const pctMatches = text.matchAll(/(\d+(?:[.,]\d+)?)\s*(?:%|percent)/gi);
+ for (const m of pctMatches) {
+ entities.add(`pct:${m[1].replace(',', '.')}`);
+ }
+
+ // Time limits: 30 days, 2 weeks, 3 months, 14 Tage, 30 jours, 30 dagen, 5-7 werkdagen
+ const timeMatches = text.matchAll(/(\d+(?:\s*[-–]\s*\d+)?)\s*(?:days?|weeks?|months?|werktage?|werkdagen?|jours?|giorni?|dagen?|Tage?|settimane?|mois)/gi);
+ for (const m of timeMatches) {
+ entities.add(`time:${m[0].toLowerCase().replace(/\s+/g, '')}`);
+ }
+
+ // Number + unit combos: 5kg, 3 pieces, 10 items, 2 kg, 5 stück, 3 pièces, 4 stuks
+ const unitMatches = text.matchAll(/(\d+(?:[.,]\d+)?)\s*(?:kg|g|lb|oz|pieces?|items?|units?|stück|pièces?|stuks?|pairs?|sets?|pcs)/gi);
+ for (const m of unitMatches) {
+ entities.add(`unit:${m[0].toLowerCase().replace(/\s+/g, '')}`);
+ }
+
+ // EU country names
+ const words = lower.split(/[\s,.;:!?"'()\-/]+/).filter(Boolean);
+ // Also check bigrams for "united kingdom", "czech republic"
+ for (let i = 0; i < words.length; i++) {
+ if (EU_COUNTRIES.has(words[i])) {
+ entities.add(`country:${words[i]}`);
+ }
+ if (i + 1 < words.length) {
+ const bigram = words[i] + ' ' + words[i + 1];
+ if (EU_COUNTRIES.has(bigram)) {
+ entities.add(`country:${bigram}`);
+ }
+ }
+ }
+
+ // English policy terms (whole word match)
+ for (const term of POLICY_TERMS_EN) {
+ const pattern = new RegExp('\\b' + term.replace(/[- ]/g, '[- ]') + '\\b', 'i');
+ if (pattern.test(text)) {
+ entities.add(`policy:${term.toLowerCase()}`);
+ }
+ }
+
+ // Multilingual policy terms
+ for (const [foreignTerm, englishTerm] of Object.entries(POLICY_TERMS_I18N)) {
+ if (lower.includes(foreignTerm.toLowerCase())) {
+ entities.add(`policy:${englishTerm}`);
+ }
+ }
+
+ return [...entities];
+}
+
+/**
+ * Normalize entities for cross-language comparison.
+ * Maps multilingual entities to their English equivalents.
+ */
+function normalizeEntity(entity) {
+ // Already prefixed with type: e.g., "policy:refund"
+ return entity;
+}
+
+/**
+ * Check if entities from the reply are covered by KB hit texts.
+ * Returns { covered, uncovered } entity arrays.
+ */
+function computeCoverage(replyEntities, kbHitTexts) {
+ if (!replyEntities.length) return { covered: [], uncovered: [] };
+
+ // Collect all entities from KB texts
+ const kbEntities = new Set();
+ for (const hit of kbHitTexts) {
+ const text = typeof hit === 'string' ? hit : [hit.question, hit.answer, hit.category].filter(Boolean).join(' ');
+ const extracted = extractEntities(text);
+ for (const e of extracted) {
+ kbEntities.add(e);
+ }
+ }
+
+ const covered = [];
+ const uncovered = [];
+ for (const entity of replyEntities) {
+ if (kbEntities.has(entity)) {
+ covered.push(entity);
+ } else {
+ uncovered.push(entity);
+ }
+ }
+
+ return { covered, uncovered };
+}
+
+/**
+ * Check for fabricated citations — URLs in the reply that are NOT in the KB sources.
+ * Returns array of fabricated URLs found.
+ */
+function detectFabricatedCitations(replyText, kbSources) {
+ if (!replyText || !kbSources || !kbSources.length) return [];
+
+ // Extract URLs from reply
+ const urlMatches = replyText.matchAll(/https?:\/\/[^\s)\]>"']+/gi);
+ const replyUrls = new Set();
+ for (const m of urlMatches) {
+ replyUrls.add(m[0].toLowerCase().replace(/\/+$/, ''));
+ }
+
+ // Normalize KB source URLs
+ const kbUrls = new Set();
+ for (const url of kbSources) {
+ if (typeof url === 'string') {
+ kbUrls.add(url.toLowerCase().replace(/\/+$/, ''));
+ }
+ }
+
+ // Fabricated = URLs in reply not found in KB sources.
+ // 允许"与 KB 来源同源(根域名)"的链接——AI 附加的站内导航链接(如 dropshipping.yehwang.com)
+ // 不算编造引用;只有引用了一个完全不同的域名/来源才标记。
+ const kbOrigins = new Set();
+ for (const url of kbUrls) {
+ try {
+ kbOrigins.add(new URL(url).origin.toLowerCase());
+ } catch (e) { /* 忽略非法 URL */ }
+ }
+ const fabricated = [];
+ for (const url of replyUrls) {
+ if (kbUrls.has(url)) continue;
+ try {
+ const origin = new URL(url).origin.toLowerCase();
+ if (kbOrigins.has(origin)) continue; // 同源站内链接 → 不算编造
+ } catch (e) { /* 非法 URL 跳过 */ }
+ fabricated.push(url);
+ }
+
+ return fabricated;
+}
+
+/**
+ * Main entry point: check citation consistency of a reply against KB hits.
+ *
+ * @param {string} replyText - Agent's final reply text (stripped of HTML)
+ * @param {Array} kbHitTexts - KB hit texts (strings or objects with question/answer/category)
+ * @param {string} intent - Current intent label
+ * @param {Array} [kbSources] - KB source URLs (for fabricated citation detection)
+ * @returns {{ citation_score: number|null, hallucination_flag: boolean|null, extracted_entities: string[], covered_entities: string[], uncovered_entities: string[], severity: string|null }}
+ */
+function checkCitation(replyText, kbHitTexts, intent, kbSources) {
+ // Non-KB intents: skip hallucination checking
+ if (NON_KB_INTENTS.has(intent)) {
+ return {
+ citation_score: null,
+ hallucination_flag: null,
+ extracted_entities: [],
+ covered_entities: [],
+ uncovered_entities: [],
+ severity: null,
+ };
+ }
+
+ // No reply text: nothing to check
+ if (!replyText || typeof replyText !== 'string') {
+ return {
+ citation_score: null,
+ hallucination_flag: null,
+ extracted_entities: [],
+ covered_entities: [],
+ uncovered_entities: [],
+ severity: null,
+ };
+ }
+
+ // Check for fabricated citations first (most severe)
+ const fabricatedUrls = detectFabricatedCitations(replyText, kbSources || []);
+ if (fabricatedUrls.length > 0) {
+ return {
+ citation_score: 0,
+ hallucination_flag: true,
+ extracted_entities: [],
+ covered_entities: [],
+ uncovered_entities: [],
+ severity: 'fabricated_citation',
+ };
+ }
+
+ // No KB hits but KB-intent (policy_faq/general): if reply has substantive entities, flag
+ const hasKbHits = kbHitTexts && kbHitTexts.length > 0;
+ const extractedEntities = extractEntities(replyText);
+
+ if (!hasKbHits) {
+ // No KB data to check against — if there are policy entities, likely hallucinated
+ const hasPolicyEntities = extractedEntities.some(e => e.startsWith('policy:'));
+ const hasFactualEntities = extractedEntities.some(e => e.startsWith('amount:') || e.startsWith('pct:') || e.startsWith('time:'));
+
+ if (hasPolicyEntities || hasFactualEntities) {
+ return {
+ citation_score: 0,
+ hallucination_flag: true,
+ extracted_entities: extractedEntities,
+ covered_entities: [],
+ uncovered_entities: extractedEntities,
+ severity: 'no_kb_support',
+ };
+ }
+
+ // No entities extracted, no KB hits — can't determine, not flagged
+ return {
+ citation_score: null,
+ hallucination_flag: null,
+ extracted_entities: [],
+ covered_entities: [],
+ uncovered_entities: [],
+ severity: null,
+ };
+ }
+
+ // Normal case: compute coverage
+ const { covered, uncovered } = computeCoverage(extractedEntities, kbHitTexts);
+ const citationScore = extractedEntities.length > 0
+ ? covered.length / extractedEntities.length
+ : 1; // No entities to check = nothing to hallucinate about
+
+ // Hallucination threshold: citation_score < 0.5 for KB intents
+ const hallucinationFlag = citationScore < 0.5;
+
+ return {
+ citation_score: Math.round(citationScore * 1000) / 1000, // 3 decimal places
+ hallucination_flag: hallucinationFlag,
+ extracted_entities: extractedEntities,
+ covered_entities: covered,
+ uncovered_entities: uncovered,
+ severity: hallucinationFlag ? 'low_coverage' : null,
+ };
+}
+
+module.exports = { checkCitation, extractEntities, computeCoverage, detectFabricatedCitations };
diff --git a/bridge/src/escalation.js b/bridge/src/escalation.js
new file mode 100644
index 0000000..04b9b02
--- /dev/null
+++ b/bridge/src/escalation.js
@@ -0,0 +1,65 @@
+'use strict';
+
+// 三级升级策略(旧 bridge escalation/manager 的 LibreDesk 版):
+// Level 0 正常;Level 1 追加“是否转人工”;Level 2 私聊备注 + 分配人工。
+function createEscalation({ client, handoffUserId = 1 }) {
+ function shouldEscalate(state, intent, kbConfidence, consecutiveFails, kbHitCount = 0) {
+ const processed = Number(state && state.messagesProcessed) || 0;
+ if (consecutiveFails >= 3) return { level: 2, reason: 'consecutive_fails' };
+ if (processed > 8 && !(state && state.opportunityId)) return { level: 2, reason: 'long_no_lead' };
+ if (intent === 'policy_faq' && kbConfidence === 'low' && kbHitCount === 0) return { level: 1, reason: 'low_kb_confidence' };
+ if (kbConfidence === 'none' && !['product_search', 'sales_lead', 'order_tracking', 'inventory_check'].includes(intent)) return { level: 1, reason: 'no_kb_match' };
+ if (processed > 5 && consecutiveFails >= 2) return { level: 1, reason: 'repeated_low_confidence' };
+ return { level: 0, reason: null };
+ }
+
+ function appendEscalationOption(reply, query) {
+ const text = String(reply || '').replace(/\s+$/, '');
+ const hints = {
+ zh: '您想转人工客服吗?',
+ de: 'Möchten Sie mit einem Mitarbeiter sprechen?',
+ fr: 'Souhaitez-vous parler à un conseiller ?',
+ it: 'Vuole parlare con un operatore?',
+ nl: 'Wilt u met een medewerker spreken?',
+ };
+ let lang = null;
+ if (/[\u4e00-\u9fff]/.test(query)) lang = 'zh';
+ else if (/[äöüß]/i.test(query)) lang = 'de';
+ else if (/[éèêëàâùûîïôç]/i.test(query)) lang = 'fr';
+ else if (/\b(hoe|voor|wilt|bent|geen|verzending|levering|bestelling)\b/i.test(query)) lang = 'nl';
+ const option = lang ? `\n\n${hints[lang]} / Would you like to speak to a human agent?` : '\n\nWould you like to speak to a human agent?';
+ return text + option;
+ }
+
+ async function escalateToHuman({ convUuid, trigger, history }) {
+ const lines = [`Escalation trigger: ${trigger || 'unknown'}`];
+ if (history && history.length) {
+ lines.push('Recent context:');
+ for (const m of history.slice(-4)) {
+ const role = m.role === 'user' ? 'Customer' : 'AI';
+ lines.push(` [${role}] ${String(m.content || '').slice(0, 150)}`);
+ }
+ }
+ try {
+ await client.sendPrivateNote(convUuid, lines.join('\n'));
+ await client.assignUser(convUuid, handoffUserId);
+ return true;
+ } catch (err) {
+ console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `escalate failed: ${err.message}` }));
+ return false;
+ }
+ }
+
+ function applyEmotionStrategy(emotion) {
+ switch (emotion) {
+ case 'angry': return { skipAI: false, escalate: true, maxToolTurns: 2, tone: 'empathetic' };
+ case 'frustrated': return { skipAI: false, escalate: false, maxToolTurns: 3, tone: 'patient' };
+ case 'urgent': return { skipAI: false, escalate: false, maxToolTurns: 3, tone: 'direct' };
+ default: return { skipAI: false, escalate: false, maxToolTurns: 5, tone: 'normal' };
+ }
+ }
+
+ return { shouldEscalate, appendEscalationOption, escalateToHuman, applyEmotionStrategy };
+}
+
+module.exports = { createEscalation };
diff --git a/bridge/src/experiments.js b/bridge/src/experiments.js
new file mode 100644
index 0000000..81ba573
--- /dev/null
+++ b/bridge/src/experiments.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+
+// A/B 实验:按 conversationId 稳定分桶(旧 bridge config/experiments.js 精简版)。
+function createExperimentManager(cfg = {}) {
+ const file = cfg.experimentsPath || path.resolve(__dirname, '..', 'config', 'experiments.json');
+ let experiments = [];
+ try {
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
+ experiments = Array.isArray(data.experiments) ? data.experiments : [];
+ } catch {
+ experiments = [];
+ }
+
+ function getVariant(convId, experimentId) {
+ const exp = experiments.find((e) => e.active && (!experimentId || e.id === experimentId));
+ if (!exp) return { experiment_id: null, variant: null, overriddenParams: null };
+ const hash = crypto.createHash('md5').update(`${convId}:${exp.id}`).digest('hex');
+ const bucket = parseInt(hash.slice(0, 8), 16) % 100;
+ let cumulative = 0;
+ for (const v of exp.variants || []) {
+ cumulative += v.weight || 50;
+ if (bucket < cumulative) return { experiment_id: exp.id, variant: v.name, overriddenParams: v.overriddenParams || null };
+ }
+ return { experiment_id: exp.id, variant: (exp.variants && exp.variants[0] && exp.variants[0].name) || null, overriddenParams: null };
+ }
+
+ function getActiveExperiments() {
+ return experiments.filter((e) => e.active);
+ }
+
+ return { getVariant, getActiveExperiments };
+}
+
+module.exports = { createExperimentManager };
diff --git a/bridge/src/faq-refiner.js b/bridge/src/faq-refiner.js
new file mode 100644
index 0000000..cbf693c
--- /dev/null
+++ b/bridge/src/faq-refiner.js
@@ -0,0 +1,26 @@
+'use strict';
+
+// 信息型页面 LLM 提炼成 FAQ(旧 bridge faq/llmRefiner.js 的移植)。
+function createFaqRefiner(llm) {
+ const SYSTEM = `You turn website page content into a clean FAQ knowledge base entry.
+Rules: output 3-8 "### Q: ..." / answer pairs based ONLY on the page content; never invent facts, numbers, or policies; answers 1-4 sentences; English; output only the FAQ body, no code fences.`;
+
+ async function refineToFaq(rawMarkdown, category) {
+ const text = String(rawMarkdown || '').slice(0, 6000);
+ if (!text.trim()) return null;
+ const res = await llm.chat({
+ messages: [
+ { role: 'system', content: SYSTEM },
+ { role: 'user', content: `Page category: ${category || 'general'}\n\nRaw page content:\n${text}` },
+ ],
+ temperature: 0.2,
+ maxTokens: 800,
+ });
+ const faq = String(res.content || '').trim();
+ return faq.includes('### Q:') ? faq : null;
+ }
+
+ return { refineToFaq };
+}
+
+module.exports = { createFaqRefiner };
diff --git a/bridge/src/flywheel.js b/bridge/src/flywheel.js
new file mode 100644
index 0000000..6ebdb89
--- /dev/null
+++ b/bridge/src/flywheel.js
@@ -0,0 +1,142 @@
+// middleware/flywheel_writer.js — 数据飞轮: 线上失败样本捕获 + LLM 预标注
+// 捕获幻觉/低分/升级/注入等可疑样本(query+reply+检索KB), LLM 预标注(疑似原因+
+// 建议修正), 供人工每周确认后回流修正 KB / golden_dataset(防复发)。
+// 设计: 捕获失败静默(不影响主链路); 预标注失败不影响样本入库(人工仍可看)。
+function createFlywheelWriter({ pool, callLLM }) {
+ /** 捕获一个可疑样本(单行 INSERT, 失败静默)。返回 sampleId 或 null。 */
+ let ready = null;
+ function ensureTable() {
+ if (!pool) return Promise.resolve();
+ if (!ready) {
+ ready = pool.query(`
+ CREATE TABLE IF NOT EXISTS flywheel_samples (
+ id BIGSERIAL PRIMARY KEY,
+ conv_id TEXT,
+ query TEXT,
+ reply TEXT,
+ intent TEXT,
+ kb_hits JSONB,
+ signal TEXT DEFAULT 'hallucination',
+ citation_score REAL,
+ llm_review JSONB,
+ status TEXT DEFAULT 'pending',
+ note TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+ )
+ `).catch((e) => console.error('[flywheel] table init error:', e.message.slice(0, 120)));
+ }
+ return ready;
+ }
+
+ async function captureSample({ convId, query, reply, intent, kbHits, signal, citationScore }) {
+ if (!pool) return null;
+ try {
+ await ensureTable();
+ const { rows } = await pool.query(
+ `INSERT INTO flywheel_samples (conv_id, query, reply, intent, kb_hits, signal, citation_score)
+ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
+ [
+ convId !== undefined ? convId : null,
+ (query || '').slice(0, 2000),
+ (reply || '').slice(0, 4000),
+ intent || null,
+ kbHits && kbHits.length ? JSON.stringify(kbHits.slice(0, 10)) : null,
+ signal || 'hallucination',
+ citationScore !== undefined ? citationScore : null,
+ ]
+ );
+ return rows[0] ? rows[0].id : null;
+ } catch (e) {
+ console.error('[flywheel] capture error:', e.message.slice(0, 120));
+ return null;
+ }
+ }
+
+ /**
+ * LLM 预标注: 判断回复是否与检索到的 KB 内容矛盾/编造数字, 给出建议修正。
+ * 失败返回 null(样本仍入库, 人工看)。
+ */
+ async function preLabel({ query, reply, kbHits }) {
+ if (!callLLM) return null;
+ try {
+ const kbText = (kbHits || []).slice(0, 5).map((h) => `KB#${h.id}: ${h.question}\n${h.answer}`).join('\n---\n');
+ const res = await callLLM(
+ [{ role: 'user', content: `You are reviewing an AI customer-service reply for factual errors.
+Compare the reply against the retrieved knowledge base (KB) entries below.
+If the reply contains numbers/facts (amounts, percentages, time limits, thresholds) NOT present in the KB text, or contradicts the KB, mark suspected=true and give the reason + suggested correct answer (from the KB if available).
+Output JSON only: {"suspected": true/false, "reason": "...", "suggested_fix": "...", "kb_ref": "KB# or null"}
+
+KB:
+${kbText.slice(0, 3000)}
+
+REPLY:
+${(reply || '').slice(0, 1500)}
+
+QUERY:
+${(query || '').slice(0, 500)}
+
+JSON:` }],
+ 'You are a careful fact-checker. Output ONLY valid JSON.', 300, null
+ );
+ const m = (res.text || '').match(/\{[\s\S]*\}/);
+ if (!m) return null;
+ const parsed = JSON.parse(m[0]);
+ return {
+ suspected: parsed.suspected === true,
+ reason: parsed.reason || null,
+ suggested_fix: parsed.suggested_fix || null,
+ kb_ref: parsed.kb_ref || null,
+ };
+ } catch (e) {
+ console.error('[flywheel] preLabel error:', e.message.slice(0, 100));
+ return null;
+ }
+ }
+
+ /** 写预标注结果(LLM 预标注完成后回填)。 */
+ async function updateReview(sampleId, review) {
+ if (!pool || !sampleId || !review) return;
+ try {
+ await pool.query(`UPDATE flywheel_samples SET llm_review = $1 WHERE id = $2`, [JSON.stringify(review), sampleId]);
+ } catch (e) {
+ console.error('[flywheel] updateReview error:', e.message.slice(0, 100));
+ }
+ }
+
+ /** 待标注列表(分页)。返回 { samples, total }。 */
+ async function listSamples({ status = 'pending', limit = 50, offset = 0 } = {}) {
+ if (!pool) return { samples: [], total: 0 };
+ try {
+ const { rows } = await pool.query(
+ `SELECT id, conv_id, query, reply, intent, kb_hits, signal, citation_score, llm_review, status, note, created_at
+ FROM flywheel_samples
+ WHERE status = $1
+ ORDER BY created_at DESC
+ LIMIT $2 OFFSET $3`, [status, limit, offset]
+ );
+ const { rows: [cnt] } = await pool.query(
+ `SELECT COUNT(*)::int AS total FROM flywheel_samples WHERE status = $1`, [status]
+ );
+ return { samples: rows, total: cnt.total };
+ } catch (e) {
+ console.error('[flywheel] listSamples error:', e.message.slice(0, 100));
+ return { samples: [], total: 0 };
+ }
+ }
+
+ /** 标注样本(人工确认/驳回)。 */
+ async function updateSample(id, { status, note }) {
+ if (!pool || !id) return false;
+ try {
+ await pool.query(`UPDATE flywheel_samples SET status = $1, note = $2 WHERE id = $3`, [status || 'skip', note || null, id]);
+ return true;
+ } catch (e) {
+ console.error('[flywheel] updateSample error:', e.message.slice(0, 100));
+ return false;
+ }
+ }
+
+ return { captureSample, preLabel, updateReview, listSamples, updateSample };
+}
+
+module.exports = { createFlywheelWriter };
diff --git a/bridge/src/kb-version.js b/bridge/src/kb-version.js
new file mode 100644
index 0000000..05b319b
--- /dev/null
+++ b/bridge/src/kb-version.js
@@ -0,0 +1,51 @@
+'use strict';
+
+// KB 版本查看/回滚:备份表命名 kb_docs_v,当前表为 kb_docs。
+function quoteIdent(name) {
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) throw new Error(`invalid identifier: ${name}`);
+ return `"${name}"`;
+}
+
+async function listVersions(pool) {
+ const backups = await pool.query(
+ `SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'kb_docs_v%' ORDER BY tablename DESC`
+ );
+ const runs = await pool.query('SELECT * FROM kb_sync_runs ORDER BY id DESC LIMIT 20').catch(() => ({ rows: [] }));
+ return { backups: backups.rows.map((r) => r.tablename), runs: runs.rows };
+}
+
+async function rollback(pool, target) {
+ if (!/^kb_docs_v[a-zA-Z0-9_]+$/.test(String(target))) throw new Error(`invalid backup table: ${target}`);
+ const exists = await pool.query('SELECT to_regclass($1) IS NOT NULL AS ok', [`public.${target}`]);
+ if (!exists.rows[0].ok) throw new Error(`backup not found: ${target}`);
+ const hasCurrent = await pool.query("SELECT to_regclass('public.kb_docs') IS NOT NULL AS ok");
+ const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
+ const currentBackup = `kb_docs_before_rollback_${stamp}`;
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+ if (hasCurrent.rows[0].ok) {
+ const idx = await client.query("SELECT indexname FROM pg_indexes WHERE schemaname='public' AND tablename='kb_docs'");
+ await client.query(`ALTER TABLE kb_docs RENAME TO ${quoteIdent(currentBackup)}`);
+ for (const row of idx.rows) {
+ const newName = `${currentBackup}_${row.indexname.replace(/^kb_docs_/, '').replace(/_idx$/, '')}_idx`;
+ await client.query(`ALTER INDEX ${quoteIdent(row.indexname)} RENAME TO ${quoteIdent(newName)}`).catch(() => {});
+ }
+ }
+ await client.query(`ALTER TABLE ${quoteIdent(target)} RENAME TO kb_docs`);
+ const targetIdx = await client.query(`SELECT indexname FROM pg_indexes WHERE schemaname='public' AND tablename='kb_docs'`);
+ for (const row of targetIdx.rows) {
+ const suffix = row.indexname.includes('vec') ? 'vec_idx' : row.indexname.includes('tsv') ? 'tsv_idx' : row.indexname.includes('parent') ? 'parent_idx' : null;
+ if (suffix) await client.query(`ALTER INDEX ${quoteIdent(row.indexname)} RENAME TO ${quoteIdent('kb_docs_' + suffix)}`).catch(() => {});
+ }
+ await client.query('COMMIT');
+ return { rolled_back_to: target, previous: hasCurrent.rows[0].ok ? currentBackup : null };
+ } catch (err) {
+ await client.query('ROLLBACK');
+ throw err;
+ } finally {
+ client.release();
+ }
+}
+
+module.exports = { listVersions, rollback };
diff --git a/bridge/src/language.js b/bridge/src/language.js
new file mode 100644
index 0000000..ef55225
--- /dev/null
+++ b/bridge/src/language.js
@@ -0,0 +1,20 @@
+'use strict';
+
+// 语言检测 + 硬性语言约束(最新客户消息决定回复语言)。
+function detectLanguage(text) {
+ const q = String(text || '');
+ if (/[\u3400-\u9fff]/.test(q)) return 'Chinese';
+ if (/\b(und|nicht|für|mit|bitte|haben|preis|menge|lieferung)\b/i.test(q)) return 'German';
+ if (/\b(et|pour|avec|bonjour|prix|quantité|livraison|merci)\b/i.test(q)) return 'French';
+ if (/\b(e|per|con|prezzo|quantità|spedizione|grazie)\b/i.test(q)) return 'Italian';
+ if (/\b(y|para|con|precio|cantidad|envío|gracias)\b/i.test(q)) return 'Spanish';
+ if (/\b(en|voor|met|prijs|aantal|levering|bedankt)\b/i.test(q)) return 'Dutch';
+ return 'English';
+}
+
+function languageMandate(text) {
+ const lang = detectLanguage(text);
+ return `\n\nLANGUAGE MANDATE (hard rule): The customer's latest message is in ${lang}. Reply ENTIRELY in ${lang}, even if earlier messages or tool results are in another language. Do NOT mix languages.`;
+}
+
+module.exports = { detectLanguage, languageMandate };
diff --git a/bridge/src/libredesk-webhook.js b/bridge/src/libredesk-webhook.js
index 4c88795..d7a6563 100644
--- a/bridge/src/libredesk-webhook.js
+++ b/bridge/src/libredesk-webhook.js
@@ -1,6 +1,7 @@
'use strict';
const crypto = require('crypto');
+const prom = require('./prom');
// Phase 0:验签 -> 去重 -> 只处理 contact 的 message.created -> 回一条占位消息。
// Phase 1 会把这里替换成 bridge AI 编排。
@@ -29,6 +30,7 @@ function createLibreDeskWebhook(cfg, deps = {}) {
async function handle(body) {
if (!body || body.event !== 'message.created') return;
const msg = body.payload || {};
+ prom.inc('bridge_webhook_events_total', { event: body.event || 'unknown' });
if (msg.sender_type !== 'contact' || msg.private) return;
if (!msg.uuid || !msg.conversation_uuid) return;
if (!markSeen(msg.uuid)) return;
diff --git a/bridge/src/opportunity.js b/bridge/src/opportunity.js
index 6b11dcf..3b8333a 100644
--- a/bridge/src/opportunity.js
+++ b/bridge/src/opportunity.js
@@ -1,11 +1,12 @@
'use strict';
const { ToolError } = require('./errors');
+const { attachChatToOpportunity } = require('./chat-log');
const STAGES = new Set(['NEW', 'SCREENING', 'MEETING', 'PROPOSAL', 'CUSTOMER']);
// 对齐旧 bridge leadPipeline:指纹去重、跨渠道复用/同步、阶段推进、CRM 故障 fail-closed。
-function createOpportunityService({ cfg, twenty, leadStore }) {
+function createOpportunityService({ cfg, twenty, leadStore, client }) {
function splitName(full) {
const parts = String(full || '').trim().split(/[\s.]+/).filter(Boolean);
if (parts.length >= 2) return { firstName: parts[0], lastName: parts.slice(1).join(' ') };
@@ -140,6 +141,7 @@ function createOpportunityService({ cfg, twenty, leadStore }) {
}
const noteId = await twenty.createNote(summary);
if (noteId) await twenty.linkNoteToOpportunity(noteId, existing.id);
+ setImmediate(() => attachChatToOpportunity({ client, twenty, convUuid: conversationUuid, opportunityId: existing.id }).catch(() => {}));
const stage = await advanceStage({ ctx, state, slots, email: slots.email || email });
return {
created: false,
@@ -187,6 +189,7 @@ function createOpportunityService({ cfg, twenty, leadStore }) {
if (noteId) await twenty.linkNoteToOpportunity(noteId, opp.id);
const dueAt = new Date(Date.now() + 3 * 86400 * 1000).toISOString();
const taskId = await twenty.createTask(`Follow up: ${slots.company}`, summary, dueAt, opp.id);
+ setImmediate(() => attachChatToOpportunity({ client, twenty, convUuid: conversationUuid, opportunityId: opp.id }).catch(() => {}));
const stage = await advanceStage({ ctx, state, slots, email: slots.email || email });
return {
diff --git a/bridge/src/prom.js b/bridge/src/prom.js
new file mode 100644
index 0000000..a20a949
--- /dev/null
+++ b/bridge/src/prom.js
@@ -0,0 +1,34 @@
+'use strict';
+
+// 极简 Prometheus 指标注册表(无外部依赖)。
+const counters = new Map();
+const startedAt = Date.now();
+
+function key(name, labels = {}) {
+ const sorted = Object.keys(labels).sort().map((k) => `${k}="${String(labels[k]).replace(/["\\]/g, '')}"`).join(',');
+ return sorted ? `${name}{${sorted}}` : name;
+}
+
+function inc(name, labels = {}, value = 1) {
+ const k = key(name, labels);
+ counters.set(k, (counters.get(k) || 0) + Number(value || 1));
+}
+
+function render() {
+ const lines = [
+ '# HELP bridge_uptime_seconds Process uptime in seconds',
+ '# TYPE bridge_uptime_seconds gauge',
+ `bridge_uptime_seconds ${((Date.now() - startedAt) / 1000).toFixed(1)}`,
+ '# HELP bridge_process_memory_bytes Process RSS bytes',
+ '# TYPE bridge_process_memory_bytes gauge',
+ `bridge_process_memory_bytes ${process.memoryUsage().rss}`,
+ ];
+ for (const [k, v] of counters) {
+ const name = k.split('{')[0];
+ lines.push(`# TYPE ${name} counter`);
+ lines.push(`${k} ${v}`);
+ }
+ return lines.join('\n') + '\n';
+}
+
+module.exports = { inc, render };
diff --git a/bridge/src/reranker.js b/bridge/src/reranker.js
new file mode 100644
index 0000000..11d1fd3
--- /dev/null
+++ b/bridge/src/reranker.js
@@ -0,0 +1,22 @@
+'use strict';
+
+// LLM rerank:对 RAG 候选做二次排序(旧 bridge rag/reranker.js 的移植)。
+function createReranker(llm) {
+ async function rerank(query, candidates, topK = 3) {
+ if (!candidates || candidates.length <= topK) return candidates || [];
+ const docs = candidates.map((c, i) => `[${i}] (category: ${c.category || ''}) Q: ${c.question}\nA: ${String(c.answer || '').slice(0, 200)}`).join('\n\n');
+ const prompt = `Rank these knowledge base entries by relevance to the query. Output ONLY the indices of the ${topK} most relevant entries, in order, comma-separated. Example: 3,0,7\n\nQuery: "${query}"\n\nEntries:\n${docs}`;
+ try {
+ const res = await llm.chat({ messages: [{ role: 'user', content: prompt }], temperature: 0, maxTokens: 50 });
+ const indices = String(res.content || '').trim().split(',').map((s) => parseInt(s.trim(), 10)).filter((i) => Number.isInteger(i) && i >= 0 && i < candidates.length);
+ if (!indices.length) return candidates.slice(0, topK);
+ return indices.slice(0, topK).map((i) => candidates[i]);
+ } catch (err) {
+ console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `rerank failed: ${err.message}` }));
+ return candidates.slice(0, topK);
+ }
+ }
+ return { rerank };
+}
+
+module.exports = { createReranker };
diff --git a/bridge/src/server.js b/bridge/src/server.js
index 3aacbae..eabcb57 100644
--- a/bridge/src/server.js
+++ b/bridge/src/server.js
@@ -7,6 +7,7 @@ const { createTools, resolveSiteProfile } = require('./tools');
const { createIdentity } = require('./identity');
const { createLibreDeskWebhook } = require('./libredesk-webhook');
const { createAgent } = require('./agent');
+const prom = require('./prom');
const { ToolError } = require('./errors');
const MAX_BODY_BYTES = 256 * 1024;
@@ -113,6 +114,14 @@ function createServer(options = {}) {
});
}
+ // GET /metrics — Prometheus 文本指标(默认仅内网/LAN 暴露)
+ if (req.method === 'GET' && url === '/metrics') {
+ const body = prom.render();
+ res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8', 'content-length': Buffer.byteLength(body) });
+ res.end(body);
+ return;
+ }
+
// POST /libredesk/webhook — 方案 A:LibreDesk 消息事件进入 bridge(HMAC 验签)
if (req.method === 'POST' && url === '/libredesk/webhook') {
const raw = await readRawBody(req);
diff --git a/bridge/src/tools.js b/bridge/src/tools.js
index 63cc101..118c47a 100644
--- a/bridge/src/tools.js
+++ b/bridge/src/tools.js
@@ -325,8 +325,9 @@ function createTools(cfg, deps = {}) {
const leadStore = deps.leadStore || createLeadStore(cfg);
const retriever = deps.retriever || null;
const sitemap = deps.sitemap || createSiteMap(cfg);
- const opportunityService = deps.opportunityService || createOpportunityService({ cfg, twenty, leadStore });
+ const opportunityService = deps.opportunityService || createOpportunityService({ cfg, twenty, leadStore, client: deps.client });
const mailHistory = deps.mailHistory || createMailHistory(cfg);
+ const reranker = deps.reranker || null;
function missingLeadFields(slots) {
const missing = [];
if (!slots.company) missing.push('company');
@@ -376,7 +377,8 @@ function createTools(cfg, deps = {}) {
const query = String(args.query || '').trim();
if (!query) throw new ToolError(400, 'invalid_arguments', 'query is required');
const limit = Math.min(Number(args.limit) || 5, 8);
- const results = await retriever.searchKB(query, limit);
+ let results = await retriever.searchKB(query, Math.max(limit, 8));
+ if (reranker) results = await reranker.rerank(query, results, limit);
return {
results,
count: results.length,
@@ -697,7 +699,7 @@ function createTools(cfg, deps = {}) {
async function sendVerificationCode(args, ctx) {
if (!deps.verification) throw new ToolError(503, 'not_configured', 'Verification is not configured');
if (!ctx.conversationUuid) throw new ToolError(400, 'missing_conversation', 'conversation context is required');
- const out = await deps.verification.sendCode(ctx.conversationUuid, args.email);
+ const out = await deps.verification.sendCode(ctx.conversationUuid, args.email, { language: ctx.language });
return { ...out, note: 'Ask the customer to check their inbox and reply with the 6-digit code.' };
}
diff --git a/bridge/src/verification.js b/bridge/src/verification.js
index 825053e..5223939 100644
--- a/bridge/src/verification.js
+++ b/bridge/src/verification.js
@@ -13,6 +13,17 @@ function normalizeEmail(value) {
return String(value || '').trim().toLowerCase();
}
+function otpEmail(language, code) {
+ const templates = {
+ zh: { subject: 'Yehwang 验证码', text: `您的 Yehwang 验证码是:${code}\n\n10 分钟内有效。如非本人操作请忽略。` },
+ de: { subject: 'Ihr Yehwang Bestätigungscode', text: `Ihr Yehwang Bestätigungscode lautet: ${code}\n\nGültig für 10 Minuten.` },
+ fr: { subject: 'Votre code de vérification Yehwang', text: `Votre code de vérification Yehwang est : ${code}\n\nValable 10 minutes.` },
+ nl: { subject: 'Uw Yehwang verificatiecode', text: `Uw Yehwang verificatiecode is: ${code}\n\n10 minuten geldig.` },
+ en: { subject: 'Your Yehwang verification code', text: `Your Yehwang verification code is: ${code}\n\nIt expires in 10 minutes.` },
+ };
+ return templates[language] || templates.en;
+}
+
function createVerification(cfg, deps = {}) {
const memory = new Map();
const client = deps.redis || (cfg.redisUrl ? new RedisClient(cfg.redisUrl, { timeoutMs: 3000 }) : null);
@@ -40,7 +51,7 @@ function createVerification(cfg, deps = {}) {
}
}
- async function sendCode(conversationUuid, email) {
+ async function sendCode(conversationUuid, email, opts = {}) {
const conv = String(conversationUuid || '');
const address = normalizeEmail(email);
if (!conv) throw new Error('missing conversation');
@@ -55,17 +66,11 @@ function createVerification(cfg, deps = {}) {
const pending = JSON.stringify({ code, email: address, attempts: 0, createdAt: Date.now() });
await setEx(otpKey(conv), pending, OTP_TTL_SEC);
- const text = [
- 'Your Yehwang verification code is: ' + code,
- '',
- 'It expires in 10 minutes. If you did not request it, ignore this email.',
- '',
- 'Yehwang Support',
- ].join('\n');
+ const tpl = otpEmail(opts.language, code);
try {
await sendMail(
{ host: cfg.smtpHost, port: cfg.smtpPort, user: cfg.smtpUser, pass: cfg.smtpPass, from: cfg.smtpFrom },
- { to: address, subject: 'Your Yehwang verification code', text }
+ { to: address, subject: tpl.subject, text: tpl.text }
);
} catch (err) {
await del(otpKey(conv));
@@ -107,4 +112,4 @@ function createVerification(cfg, deps = {}) {
return { sendCode, verifyCode, getVerifiedEmail };
}
-module.exports = { createVerification };
+module.exports = { createVerification, otpEmail };
diff --git a/bridge/test/citation-checker.test.js b/bridge/test/citation-checker.test.js
new file mode 100644
index 0000000..11a3834
--- /dev/null
+++ b/bridge/test/citation-checker.test.js
@@ -0,0 +1,235 @@
+const { describe, it } = require('node:test');
+const assert = require('node:assert');
+const { checkCitation, extractEntities, computeCoverage, detectFabricatedCitations } = require('../src/citation-checker');
+
+describe('citation_checker', () => {
+ describe('extractEntities', () => {
+ it('extracts EUR amounts with € symbol', () => {
+ const entities = extractEntities('Free shipping for orders over €200.');
+ const amountEntities = entities.filter(e => e.startsWith('amount:'));
+ assert.ok(amountEntities.some(e => e === 'amount:200'), `Expected amount:200, got ${amountEntities}`);
+ });
+
+ it('extracts EUR amounts with EUR prefix', () => {
+ const entities = extractEntities('The minimum order is EUR 30.');
+ const amountEntities = entities.filter(e => e.startsWith('amount:'));
+ assert.ok(amountEntities.some(e => e === 'amount:30'), `Expected amount:30, got ${amountEntities}`);
+ });
+
+ it('extracts EUR amounts with suffix', () => {
+ const entities = extractEntities('Total cost: 150€');
+ const amountEntities = entities.filter(e => e.startsWith('amount:'));
+ assert.ok(amountEntities.some(e => e === 'amount:150'), `Expected amount:150, got ${amountEntities}`);
+ });
+
+ it('extracts EUR amounts with "euros" suffix', () => {
+ const entities = extractEntities('Shipping costs 5 euros.');
+ const amountEntities = entities.filter(e => e.startsWith('amount:'));
+ assert.ok(amountEntities.some(e => e === 'amount:5'), `Expected amount:5, got ${amountEntities}`);
+ });
+
+ it('extracts percentages', () => {
+ const entities = extractEntities('We offer a 10% discount for members.');
+ const pctEntities = entities.filter(e => e.startsWith('pct:'));
+ assert.ok(pctEntities.some(e => e === 'pct:10'), `Expected pct:10, got ${pctEntities}`);
+ });
+
+ it('extracts time limits in days', () => {
+ const entities = extractEntities('You have 30 days to return items.');
+ const timeEntities = entities.filter(e => e.startsWith('time:'));
+ assert.ok(timeEntities.length > 0, `Expected time entities, got ${entities}`);
+ });
+
+ it('extracts time limits in months', () => {
+ const entities = extractEntities('Warranty lasts 6 months.');
+ const timeEntities = entities.filter(e => e.startsWith('time:'));
+ assert.ok(timeEntities.length > 0, `Expected time entities, got ${entities}`);
+ });
+
+ it('extracts EU country names', () => {
+ const entities = extractEntities('We ship to all EU countries including Netherlands and Germany.');
+ const countryEntities = entities.filter(e => e.startsWith('country:'));
+ assert.ok(countryEntities.some(e => e === 'country:netherlands'), `Expected country:netherlands, got ${countryEntities}`);
+ assert.ok(countryEntities.some(e => e === 'country:germany'), `Expected country:germany, got ${countryEntities}`);
+ });
+
+ it('extracts English policy terms', () => {
+ const entities = extractEntities('Our refund and return policy covers shipping.');
+ const policyEntities = entities.filter(e => e.startsWith('policy:'));
+ assert.ok(policyEntities.some(e => e === 'policy:refund'), `Expected policy:refund, got ${policyEntities}`);
+ assert.ok(policyEntities.some(e => e === 'policy:return'), `Expected policy:return, got ${policyEntities}`);
+ assert.ok(policyEntities.some(e => e === 'policy:shipping'), `Expected policy:shipping, got ${policyEntities}`);
+ });
+
+ it('extracts number+unit combos', () => {
+ const entities = extractEntities('Minimum order is 5 pieces.');
+ const unitEntities = entities.filter(e => e.startsWith('unit:'));
+ assert.ok(unitEntities.length > 0, `Expected unit entities, got ${entities}`);
+ });
+
+ it('returns empty array for null/undefined text', () => {
+ assert.deepStrictEqual(extractEntities(null), []);
+ assert.deepStrictEqual(extractEntities(undefined), []);
+ assert.deepStrictEqual(extractEntities(''), []);
+ });
+
+ it('handles decimal amounts', () => {
+ const entities = extractEntities('The fee is €4.95.');
+ const amountEntities = entities.filter(e => e.startsWith('amount:'));
+ assert.ok(amountEntities.some(e => e === 'amount:4.95'), `Expected amount:4.95, got ${amountEntities}`);
+ });
+ });
+
+ describe('computeCoverage', () => {
+ it('finds covered entities in KB texts', () => {
+ const replyEntities = ['amount:200', 'policy:shipping'];
+ const kbHitTexts = ['Free shipping for orders over €200 within Netherlands.'];
+ const { covered, uncovered } = computeCoverage(replyEntities, kbHitTexts);
+ assert.ok(covered.includes('amount:200'), `Expected amount:200 in covered, got ${covered}`);
+ assert.ok(covered.includes('policy:shipping'), `Expected policy:shipping in covered, got ${covered}`);
+ assert.strictEqual(uncovered.length, 0);
+ });
+
+ it('identifies uncovered entities', () => {
+ const replyEntities = ['amount:500', 'policy:shipping', 'country:spain'];
+ const kbHitTexts = ['Free shipping for orders over €200 within Netherlands.'];
+ const { covered, uncovered } = computeCoverage(replyEntities, kbHitTexts);
+ assert.ok(covered.includes('policy:shipping'), `Expected policy:shipping in covered`);
+ assert.ok(uncovered.includes('amount:500'), `Expected amount:500 in uncovered`);
+ assert.ok(uncovered.includes('country:spain'), `Expected country:spain in uncovered`);
+ });
+
+ it('handles KB hit objects with question/answer', () => {
+ const replyEntities = ['amount:200', 'policy:shipping'];
+ const kbHitTexts = [{ question: 'Shipping policy?', answer: 'Free shipping for orders over €200.', category: 'shipping' }];
+ const { covered } = computeCoverage(replyEntities, kbHitTexts);
+ assert.ok(covered.includes('amount:200'));
+ });
+
+ it('returns empty for empty reply entities', () => {
+ const { covered, uncovered } = computeCoverage([], ['Some KB text']);
+ assert.deepStrictEqual(covered, []);
+ assert.deepStrictEqual(uncovered, []);
+ });
+ });
+
+ describe('checkCitation', () => {
+ it('returns null for non-KB intent (product_search)', () => {
+ const result = checkCitation('Some product reply', ['KB text'], 'product_search');
+ assert.strictEqual(result.citation_score, null);
+ assert.strictEqual(result.hallucination_flag, null);
+ });
+
+ it('returns null for non-KB intent (order_tracking)', () => {
+ const result = checkCitation('Your order is shipped', [], 'order_tracking');
+ assert.strictEqual(result.citation_score, null);
+ assert.strictEqual(result.hallucination_flag, null);
+ });
+
+ it('returns null for non-KB intent (inventory_check)', () => {
+ const result = checkCitation('5 items in stock', [], 'inventory_check');
+ assert.strictEqual(result.citation_score, null);
+ assert.strictEqual(result.hallucination_flag, null);
+ });
+
+ it('computes high citation_score for well-supported reply', () => {
+ const reply = 'Free shipping for orders over €200 within the Netherlands. We offer a 30-day return policy.';
+ const kbHits = [{ question: 'Shipping?', answer: 'Free shipping for orders over €200 within Netherlands.', category: 'shipping' }];
+ const result = checkCitation(reply, kbHits, 'policy_faq');
+ assert.ok(result.citation_score >= 0.5, `Expected score >= 0.5, got ${result.citation_score}`);
+ assert.strictEqual(result.hallucination_flag, false);
+ });
+
+ it('flags hallucination for low coverage reply', () => {
+ const reply = 'Free shipping for orders over €200 to ALL EU countries including Spain and Italy. 50% discount on membership.';
+ const kbHits = ['Free shipping for orders over €200 within Netherlands only. No special discounts.'];
+ const result = checkCitation(reply, kbHits, 'policy_faq');
+ assert.strictEqual(result.hallucination_flag, true);
+ assert.ok(result.citation_score < 0.5, `Expected score < 0.5, got ${result.citation_score}`);
+ assert.strictEqual(result.severity, 'low_coverage');
+ });
+
+ it('returns citation_score=0 and hallucination_flag=true for no KB hits with policy entities', () => {
+ const reply = 'Our refund policy allows returns within 30 days. Shipping costs €5.99.';
+ const result = checkCitation(reply, [], 'policy_faq');
+ assert.strictEqual(result.citation_score, 0);
+ assert.strictEqual(result.hallucination_flag, true);
+ assert.strictEqual(result.severity, 'no_kb_support');
+ });
+
+ it('returns null for no KB hits and no factual entities', () => {
+ const reply = 'Hello! How can I help you today?';
+ const result = checkCitation(reply, [], 'general');
+ assert.strictEqual(result.citation_score, null);
+ assert.strictEqual(result.hallucination_flag, null);
+ });
+
+ it('returns null for null/empty reply text', () => {
+ const result = checkCitation(null, ['KB text'], 'policy_faq');
+ assert.strictEqual(result.citation_score, null);
+ assert.strictEqual(result.hallucination_flag, null);
+ });
+
+ it('returns citation_score=1 when no entities are extracted from reply', () => {
+ const reply = 'Yes, that is correct.';
+ const kbHits = ['Some KB answer about something.'];
+ const result = checkCitation(reply, kbHits, 'policy_faq');
+ // No entities = nothing to hallucinate about
+ assert.strictEqual(result.citation_score, 1);
+ assert.strictEqual(result.hallucination_flag, false);
+ });
+
+ it('handles general intent with KB hits', () => {
+ const reply = 'You can get a refund within 30 days.';
+ const kbHits = ['Our refund policy: 30-day return window.'];
+ const result = checkCitation(reply, kbHits, 'general');
+ assert.ok(result.citation_score >= 0.5);
+ });
+ });
+
+ describe('detectFabricatedCitations', () => {
+ it('detects cross-domain URLs not in KB sources', () => {
+ const reply = 'See our policy at https://evil-competitor.com/fake-policy for details.';
+ const kbSources = ['https://yehwang.com/real-policy'];
+ const fabricated = detectFabricatedCitations(reply, kbSources);
+ assert.strictEqual(fabricated.length, 1);
+ assert.ok(fabricated[0].includes('evil-competitor.com'));
+ });
+
+ it('allows same-origin navigation links not in KB sources (站内导航不算编造)', () => {
+ const reply = 'Browse items on https://dropshipping.yehwang.com';
+ const kbSources = ['https://dropshipping.yehwang.com/faq/returns'];
+ const fabricated = detectFabricatedCitations(reply, kbSources);
+ assert.strictEqual(fabricated.length, 0);
+ });
+
+ it('returns empty when all reply URLs are in KB sources', () => {
+ const reply = 'See https://yehwang.com/policy for details.';
+ const kbSources = ['https://yehwang.com/policy'];
+ const fabricated = detectFabricatedCitations(reply, kbSources);
+ assert.strictEqual(fabricated.length, 0);
+ });
+
+ it('returns empty for reply with no URLs', () => {
+ const reply = 'Free shipping for orders over €200.';
+ const fabricated = detectFabricatedCitations(reply, ['https://yehwang.com/policy']);
+ assert.strictEqual(fabricated.length, 0);
+ });
+
+ it('returns empty for null/undefined inputs', () => {
+ assert.deepStrictEqual(detectFabricatedCitations(null, []), []);
+ assert.deepStrictEqual(detectFabricatedCitations('', null), []);
+ assert.deepStrictEqual(detectFabricatedCitations('', []), []);
+ });
+
+ it('flags fabricated cross-domain citation as severe hallucination in checkCitation', () => {
+ const reply = 'See our policy at https://other-site.com/fake-page for details about €200 free shipping.';
+ const kbHits = ['Free shipping over €200.'];
+ const kbSources = ['https://yehwang.com/real-shipping-policy'];
+ const result = checkCitation(reply, kbHits, 'policy_faq', kbSources);
+ assert.strictEqual(result.hallucination_flag, true);
+ assert.strictEqual(result.severity, 'fabricated_citation');
+ assert.strictEqual(result.citation_score, 0);
+ });
+ });
+});
diff --git a/bridge/test/escalation.test.js b/bridge/test/escalation.test.js
new file mode 100644
index 0000000..46c4696
--- /dev/null
+++ b/bridge/test/escalation.test.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createEscalation } = require('../src/escalation');
+
+const esc = createEscalation({ client: { sendPrivateNote: async () => {}, assignUser: async () => {} } });
+
+test('escalation: three consecutive fails -> level 2', () => {
+ assert.equal(esc.shouldEscalate({ messagesProcessed: 3 }, 'policy_faq', 'high', 3, 2).level, 2);
+});
+
+test('escalation: low KB with no hits -> level 1', () => {
+ assert.equal(esc.shouldEscalate({ messagesProcessed: 1 }, 'policy_faq', 'low', 0, 0).level, 1);
+});
+
+test('escalation: healthy -> level 0', () => {
+ assert.equal(esc.shouldEscalate({ messagesProcessed: 1 }, 'policy_faq', 'high', 0, 3).level, 0);
+});
+
+test('escalation: appends localized human option', () => {
+ const out = esc.appendEscalationOption('您好', '您好,订单怎么查');
+ assert.match(out, /转人工/);
+});
diff --git a/bridge/test/experiments.test.js b/bridge/test/experiments.test.js
new file mode 100644
index 0000000..7095e8c
--- /dev/null
+++ b/bridge/test/experiments.test.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createExperimentManager } = require('../src/experiments');
+
+test('experiments: no config returns no variant', () => {
+ const m = createExperimentManager({ experimentsPath: '/nonexistent.json' });
+ assert.equal(m.getVariant('c1').variant, null);
+});
diff --git a/bridge/test/faq-refiner.test.js b/bridge/test/faq-refiner.test.js
new file mode 100644
index 0000000..9c5e9ee
--- /dev/null
+++ b/bridge/test/faq-refiner.test.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createFaqRefiner } = require('../src/faq-refiner');
+
+test('faq refiner: returns only Q blocks', async () => {
+ const llm = { async chat() { return { content: '### Q: What?\nAnswer.' }; } };
+ const out = await createFaqRefiner(llm).refineToFaq('page content', 'test');
+ assert.match(out, /^### Q:/);
+});
diff --git a/bridge/test/language.test.js b/bridge/test/language.test.js
new file mode 100644
index 0000000..9bbfddb
--- /dev/null
+++ b/bridge/test/language.test.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { detectLanguage, languageMandate } = require('../src/language');
+
+test('language: detects common languages', () => {
+ assert.equal(detectLanguage('你好,订单怎么查'), 'Chinese');
+ assert.equal(detectLanguage('Wie ist der Preis?'), 'German');
+ assert.equal(detectLanguage('What is the minimum order?'), 'English');
+});
+
+test('language: mandate names latest language', () => {
+ assert.match(languageMandate('你好'), /Chinese/);
+});
diff --git a/bridge/test/reranker.test.js b/bridge/test/reranker.test.js
new file mode 100644
index 0000000..516dd9c
--- /dev/null
+++ b/bridge/test/reranker.test.js
@@ -0,0 +1,18 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createReranker } = require('../src/reranker');
+
+test('reranker: returns LLM-selected candidates', async () => {
+ const llm = { async chat() { return { content: '2,0' }; } };
+ const candidates = [{ question: 'a' }, { question: 'b' }, { question: 'c' }];
+ const out = await createReranker(llm).rerank('q', candidates, 2);
+ assert.deepEqual(out.map((x) => x.question), ['c', 'a']);
+});
+
+test('reranker: short list passes through', async () => {
+ const candidates = [{ question: 'a' }];
+ const out = await createReranker({ chat: async () => ({ content: '' }) }).rerank('q', candidates, 2);
+ assert.deepEqual(out, candidates);
+});
diff --git a/bridge/test/verification-i18n.test.js b/bridge/test/verification-i18n.test.js
new file mode 100644
index 0000000..72fdfe0
--- /dev/null
+++ b/bridge/test/verification-i18n.test.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { otpEmail } = require('../src/verification');
+
+test('otp email is localized', () => {
+ assert.match(otpEmail('zh', '123456').text, /验证码/);
+ assert.match(otpEmail('de', '123456').text, /Bestätigungscode/);
+ assert.match(otpEmail('en', '123456').text, /verification code/);
+});
diff --git a/deploy/seeds/kb_docs.sql.gz b/deploy/seeds/kb_docs.sql.gz
index a4d78ab..36da77a 100644
Binary files a/deploy/seeds/kb_docs.sql.gz and b/deploy/seeds/kb_docs.sql.gz differ
diff --git a/docs/go-live.md b/docs/go-live.md
index 6f519fe..d7a6555 100644
--- a/docs/go-live.md
+++ b/docs/go-live.md
@@ -191,7 +191,7 @@ docker exec crmnew-postgres psql -U crmnew -d bridge -tAc \
预期:
```
-367 | 296 | 71
+594 | 523 | 71
```
然后把测试环境 `.env` 的 `RAG_PG_URL` 指向该库,例如:
@@ -233,12 +233,19 @@ node scripts/sync-kb-online.js --local-only --force
- 需要 `RAG_PG_URL` 指向目标库
- 需要 `EMB_*`(DashScope embedding)
- 旧的 `kb_docs` 会备份为 `kb_docs_v`
-- `bridge/kb/11-site-map.md` 目前是静态类目地图,不随官网自动更新
+- `bridge/kb/11-site-map.md` 可用 `node scripts/build-site-kb.js` 从官网 sitemap 重新生成,再重建索引
-#### 全量商品页站点地图
+#### 全量商品页站点地图 / KB 类目页
```bash
+# 商品页 URL 落库
node scripts/build-site-map.js www us
+
+# 生成 kb/11-site-map.md(类目 Q/A)
+node scripts/build-site-kb.js
+
+# 重建索引(本地模式)
+node scripts/sync-kb-online.js --local-only --force
```
### 6.4 RPC 隧道(仅本地开发)
@@ -359,6 +366,31 @@ node scripts/quality-alert.js 1 --dry-run
---
+### 7.7 质量/观测
+
+```bash
+# Prometheus 指标
+curl -s http://127.0.0.1:4100/metrics | head
+
+# RAG 检索评测
+RAG_PG_URL=postgres://...@127.0.0.1:5440/bridge node scripts/eval-retrieval.js
+
+# KB 备份列表 / 回滚
+RAG_PG_URL=postgres://...@127.0.0.1:5440/bridge node scripts/kb-version.js list
+RAG_PG_URL=postgres://...@127.0.0.1:5440/bridge node scripts/kb-version.js rollback kb_docs_v...
+```
+
+质量相关已补齐:
+
+- Citation Checker + hallucination flag
+- `flywheel_samples` 失败样本(LLM 预标注)
+- RAG reranker
+- 三级升级 + 连续失败计数
+- 语言硬约束
+- 完整会话 Note 挂商机
+- A/B 实验分流
+- Prometheus `/metrics`
+
## 8. 监控与告警
### 质量告警 cron
diff --git a/docs/plan-a.md b/docs/plan-a.md
index 50ecbf2..6ea7e45 100644
--- a/docs/plan-a.md
+++ b/docs/plan-a.md
@@ -51,10 +51,13 @@ LIBREDESK_WEBHOOK_ID=1
- [x] 上下文摘要/压缩(>24 条,保留最近 16 条)
- [x] Prompt 注入防护(消息检测 + 工具结果信任边界)
- [x] 全量商品页站点地图(www 32,691 / us 22,625,商品页 32,051/22,305)
-- [x] Hybrid RAG:text_tsv/parent_id/chunk_type,296 parent + 71 child,向量+全文+RRF+父块扩展
+- [x] Hybrid RAG:text_tsv/parent_id/chunk_type,523 parent + 71 child,向量+全文+RRF+父块扩展
- [x] 购买意向 post-LLM 追问兜底(产品+数量触发,缺公司/联系方式时多语言硬追问)
- [x] 商机补齐:内容指纹去重、跨渠道复用/同步、阶段推进、CRM 故障 fail-closed
- [x] 在线 FAQ/KB 同步(不依赖旧 bridge,`sync-kb-online.js`)
+- [x] Citation Checker / 幻觉标记 / 飞轮样本
+- [x] RAG reranker / 三级升级 / 语言硬约束 / 完整会话 Note
+- [x] KB 版本列表/回滚、sitemap 类目 KB、A/B 实验、Prometheus /metrics
- [x] `search_email_history`:已验证客户邮件历史,强制按 verified email scope
- [x] 发送前输出护栏:句子级自动重写 + URL 白名单 + 价格/库存数字必须有工具证据
- [x] Email/WhatsApp 人工接管:Wholesale Support 团队 + assign_team 规则(AI 不再分配)
diff --git a/docs/runbook.md b/docs/runbook.md
index 81a85c2..cf23619 100644
--- a/docs/runbook.md
+++ b/docs/runbook.md
@@ -61,6 +61,13 @@ bash scripts/rebuild-kb-index.sh
bash scripts/import-kb-snapshot.sh
# 或指定目标库:TARGET_PG_DB=products bash scripts/import-kb-snapshot.sh
+# 从 sitemap 生成类目 KB
+node scripts/build-site-kb.js
+
+# KB 备份列表 / 回滚
+RAG_PG_URL=postgres://crmnew:@127.0.0.1:5440/bridge node scripts/kb-version.js list
+RAG_PG_URL=postgres://crmnew:@127.0.0.1:5440/bridge node scripts/kb-version.js rollback kb_docs_v...
+
# 在线 FAQ 同步 + 重建 KB(不依赖旧 bridge)
# 宿主机执行时 RAG_PG_URL 用 127.0.0.1:5440/bridge
RAG_PG_URL=postgres://crmnew:@127.0.0.1:5440/bridge node scripts/sync-kb-online.js
@@ -73,6 +80,9 @@ bash scripts/export-kb-snapshot.sh
# 全量商品页站点地图(官网 sitemap → bridge 库)
node scripts/build-site-map.js www us
+# Prometheus 指标
+curl -s http://127.0.0.1:4100/metrics
+
# AI 质量日报(最近 24 小时;可传小时数)
node scripts/quality-report.js 24
@@ -84,6 +94,9 @@ node scripts/quality-alert.js 1 --dry-run
# 离线回归评测(确定性逻辑,不调 LLM)
node scripts/eval-offline.js
+# RAG 检索评测
+RAG_PG_URL=postgres://... node scripts/eval-retrieval.js
+
# 真实模型离线评测(fake LibreDesk,不写真实会话;会调 LLM/RPC)
node scripts/eval-live.js
diff --git a/scripts/build-site-kb.js b/scripts/build-site-kb.js
new file mode 100755
index 0000000..ab9e798
--- /dev/null
+++ b/scripts/build-site-kb.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+'use strict';
+
+// 从官网 sitemap 生成 KB 类目地图 bridge/kb/11-site-map.md(旧 bridge site-map.md 的移植)。
+// 只生成导航/类目页 Q/A,不包含商品详情页;生成后需重建 KB 索引。
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+const OUT = path.join(ROOT, 'bridge', 'kb', '11-site-map.md');
+const SITEMAP = process.env.SITE_MAP_URL || 'https://www.yehwang.com/sitemap.xml';
+const BASE = process.env.SITE_BASE_URL || 'https://www.yehwang.com';
+const WHITELIST = new Set(['jewelry', 'clothes', 'accessories', 'packing-and-display', 'bestsellers', 'new', 'top-deal', 'campaigns', 'brands', 'wholesale-service', 'factory-direct-wholesale-en', 'dropshipping', 'restock-request']);
+
+function titleCase(segment) {
+ return String(segment || '')
+ .replace(/-/g, ' ')
+ .replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+function buildEntries(urls) {
+ const out = [];
+ const seen = new Set();
+ for (const url of urls) {
+ if (!url.startsWith(BASE)) continue;
+ const pathPart = url.slice(BASE.length).split('?')[0];
+ if (!pathPart || pathPart === '/' || /\.html$/i.test(pathPart)) continue;
+ const segments = pathPart.split('/').filter(Boolean);
+ if (!segments.length || !WHITELIST.has(segments[0])) continue;
+ if (segments.length > 4) continue;
+ if (seen.has(pathPart)) continue;
+ seen.add(pathPart);
+ out.push({ path: pathPart, url: BASE + pathPart, name: titleCase(segments[segments.length - 1]) });
+ }
+ out.sort((a, b) => a.path.localeCompare(b.path));
+ return out;
+}
+
+function render(entries) {
+ const head = [
+ '---',
+ 'title: Yehwang Website Category Map (Site Map)',
+ 'category: site-map',
+ `source: ${SITEMAP}`,
+ 'generated: true',
+ 'source_url:',
+ ` - ${SITEMAP}`,
+ `last_updated: ${new Date().toISOString().slice(0, 10)}`,
+ '---',
+ '',
+ 'Every Yehwang category page has a direct URL. Use these links to send customers straight to the right page instead of a search query.',
+ '',
+ ];
+ const body = entries.map((e) => `### Q: Where can I find ${e.name}?\nBrowse our ${e.name} category: ${e.url}\n`);
+ return head.join('\n') + '\n' + body.join('\n');
+}
+
+(async () => {
+ const res = await fetch(SITEMAP, {
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36' },
+ signal: AbortSignal.timeout(90000),
+ });
+ if (!res.ok) throw new Error(`sitemap HTTP ${res.status}`);
+ const xml = await res.text();
+ const urls = [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1].trim());
+ const entries = buildEntries(urls);
+ if (!entries.length) throw new Error('generated 0 site-map entries');
+ fs.writeFileSync(OUT, render(entries), 'utf8');
+ console.log(JSON.stringify({ ts: new Date().toISOString(), site_map_kb: 'done', entries: entries.length, out: OUT }));
+})().catch((err) => { console.error('[build-site-kb]', err.message); process.exit(1); });
diff --git a/scripts/eval-retrieval.js b/scripts/eval-retrieval.js
new file mode 100755
index 0000000..3cf9971
--- /dev/null
+++ b/scripts/eval-retrieval.js
@@ -0,0 +1,44 @@
+#!/usr/bin/env node
+'use strict';
+
+// RAG 检索评测:不调 LLM,只验证 search_kb 的 top 命中。
+const fs = require('fs');
+const path = require('path');
+const { loadConfig } = require('../bridge/src/config');
+const { createRetriever } = require('../bridge/src/rag');
+
+const ROOT = path.resolve(__dirname, '..');
+const env = {};
+for (const line of fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n')) {
+ if (!line.includes('=') || line.trim().startsWith('#')) continue;
+ const i = line.indexOf('=');
+ env[line.slice(0, i).trim()] = line.slice(i + 1).trim();
+}
+const cfg = loadConfig({ ...env, ...process.env });
+if (!cfg.ragPgUrl) throw new Error('RAG_PG_URL is not configured');
+
+const CASES = [
+ ['minimum order value', 'minimum order'],
+ ['how to request a refund', 'refund'],
+ ['shipping costs', 'shipping cost'],
+ ['pre order', 'pre-order'],
+ ['factory direct MOQ', 'minimum order quantity'],
+ ['membership discount', 'membership'],
+ ['VAT refund', 'vat'],
+ ['jewelry discolor', 'discolor'],
+ ['最低起订量', 'minimum order'],
+];
+
+(async () => {
+ const retriever = createRetriever(cfg);
+ let failed = 0;
+ for (const [query, expected] of CASES) {
+ const hits = await retriever.searchKB(query, 5);
+ const ok = hits.some((h) => `${h.question} ${h.answer}`.toLowerCase().includes(expected.toLowerCase()));
+ if (!ok) failed += 1;
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${query} -> ${expected} ${ok ? '' : JSON.stringify(hits.map((h) => h.question).slice(0, 3))}`);
+ }
+ console.log(`\n${CASES.length - failed}/${CASES.length} passed`);
+ await retriever.pool.end().catch(() => {});
+ process.exit(failed ? 1 : 0);
+})().catch((err) => { console.error('[eval-retrieval]', err.message); process.exit(1); });
diff --git a/scripts/kb-version.js b/scripts/kb-version.js
new file mode 100755
index 0000000..0e92e36
--- /dev/null
+++ b/scripts/kb-version.js
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+'use strict';
+
+// KB 版本管理:查看备份 / 回滚到 kb_docs_v。
+// 用法:
+// node scripts/kb-version.js list
+// node scripts/kb-version.js rollback kb_docs_v20261023032922
+// 需要 RAG_PG_URL 指向 KB 所在库(宿主机通常是 127.0.0.1:5440/)。
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('../bridge/node_modules/pg');
+const { listVersions, rollback } = require('../bridge/src/kb-version');
+
+const ROOT = path.resolve(__dirname, '..');
+const env = {};
+for (const line of fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n')) {
+ if (!line.includes('=') || line.trim().startsWith('#')) continue;
+ const i = line.indexOf('=');
+ env[line.slice(0, i).trim()] = line.slice(i + 1).trim();
+}
+const url = process.env.RAG_PG_URL || env.RAG_PG_URL;
+if (!url) throw new Error('RAG_PG_URL is required');
+
+(async () => {
+ const pool = new Pool({ connectionString: url, max: 1 });
+ try {
+ const cmd = process.argv[2];
+ if (cmd === 'list') {
+ const out = await listVersions(pool);
+ console.log(JSON.stringify(out, null, 2));
+ } else if (cmd === 'rollback') {
+ const target = process.argv[3];
+ if (!target) throw new Error('usage: node scripts/kb-version.js rollback ');
+ console.log(JSON.stringify(await rollback(pool, target), null, 2));
+ } else {
+ console.log('usage: node scripts/kb-version.js list | rollback ');
+ }
+ } finally {
+ await pool.end();
+ }
+})().catch((err) => { console.error('[kb-version]', err.message); process.exit(1); });