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); });