From 39986f10f2fe23712c69dab5dea882588ca89e62 Mon Sep 17 00:00:00 2001 From: wangliang Date: Thu, 29 Oct 2026 13:24:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20LibreDesk=20?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=AE=9E=E4=BE=8B=E9=83=A8=E7=BD=B2=E4=BB=93?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker compose 部署:app / db / redis / tls(nginx) / bridge-tools,宿主端口错开避免与本机服务冲突 - bridge-tools 内联进仓库(compose build 路径改为 ./bridge-tools),仓库自包含可部署 - 部署说明 README、crm-new 对照与 bridge-tools 方案文档、备份与隧道脚本 - .gitignore 排除 .env、config.toml、uploads/、tls 私钥/证书、backups/、logs/ --- .gitignore | 11 + README.md | 184 ++ bridge-tools/.dockerignore | 4 + bridge-tools/.env.example | 19 + bridge-tools/Dockerfile | 21 + bridge-tools/README.md | 39 + bridge-tools/config/site-map-us.json | 2463 ++++++++++++++++++++++++ bridge-tools/config/site-map.json | 1589 +++++++++++++++ bridge-tools/config/sites.json | 45 + bridge-tools/package.json | 15 + bridge-tools/src/auth.js | 23 + bridge-tools/src/config.js | 65 + bridge-tools/src/errors.js | 17 + bridge-tools/src/product-query.js | 42 + bridge-tools/src/rpc.js | 136 ++ bridge-tools/src/server.js | 180 ++ bridge-tools/src/tools.js | 542 ++++++ bridge-tools/test/bridge-tools.test.js | 195 ++ config.sample.toml | 194 ++ docker-compose.yml | 120 ++ docs/COMPARE-crm-new.md | 133 ++ docs/E2E-livechat.md | 40 + docs/PLAN-bridge-tools-only.md | 147 ++ scripts/backup.sh | 52 + scripts/ensure-tunnel.sh | 25 + tls/nginx.conf | 44 + 26 files changed, 6345 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bridge-tools/.dockerignore create mode 100644 bridge-tools/.env.example create mode 100644 bridge-tools/Dockerfile create mode 100644 bridge-tools/README.md create mode 100644 bridge-tools/config/site-map-us.json create mode 100644 bridge-tools/config/site-map.json create mode 100644 bridge-tools/config/sites.json create mode 100644 bridge-tools/package.json create mode 100644 bridge-tools/src/auth.js create mode 100644 bridge-tools/src/config.js create mode 100644 bridge-tools/src/errors.js create mode 100644 bridge-tools/src/product-query.js create mode 100644 bridge-tools/src/rpc.js create mode 100644 bridge-tools/src/server.js create mode 100644 bridge-tools/src/tools.js create mode 100644 bridge-tools/test/bridge-tools.test.js create mode 100644 config.sample.toml create mode 100644 docker-compose.yml create mode 100644 docs/COMPARE-crm-new.md create mode 100644 docs/E2E-livechat.md create mode 100644 docs/PLAN-bridge-tools-only.md create mode 100755 scripts/backup.sh create mode 100755 scripts/ensure-tunnel.sh create mode 100644 tls/nginx.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8eba69c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +config.toml +uploads/ +tls/privkey.pem +tls/cert.pem + +# 运行数据与备份(含明文密钥副本、数据库 dump,不入库) +backups/ +logs/ +*.dump +*.tar.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d4c16a --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# LibreDesk 本地实例(yehwang-ai) + +按官方文档 [docs.libredesk.io/getting-started/installation](https://docs.libredesk.io/getting-started/installation) 的 Docker 方式部署的独立实例, +与本机 `crm-new` 项目里那套 LibreDesk 完全隔离(独立容器、独立数据卷、独立端口)。 + +## 访问 + +| 项 | 值 | +| --- | --- | +| 后台地址 | **https://192.168.31.61:9443**(自签证书,首次需点“继续前往”;见下方「为什么会一直 Connecting」) | +| API / 直连 | http://localhost:9001(后台界面走这个地址会一直 Connecting,仅用于 API/调试) | +| 登录账号 | `System` | +| 登录密码 | `Ld@42d0ef50ac5d0efa`(也存在 `.env` 的 `LIBREDESK_SYSTEM_USER_PASSWORD`,登录后建议改掉) | + +## 组成 + +| 服务 | 镜像 | 容器名 | 宿主端口 | +| --- | --- | --- | --- | +| app | libredesk/libredesk:**v2.9.1**(当前稳定版) | libredesk_app | **127.0.0.1**:9001 → 9000(仅本机/隧道) | +| db | pgvector/pgvector:pg18(本机已缓存;官方文档默认 postgres:17-alpine,本机拉取该镜像一直失败,见下方「已知偏差」) | libredesk_db | 127.0.0.1:5433 → 5432 | +| redis | redis:7-alpine | libredesk_redis | 127.0.0.1:6380 → 6379 | +| tls | nginx:alpine | libredesk_tls | 9443 → 443 | +| bridge-tools | 本仓库 `./bridge-tools`(Node 24,零依赖) | libredesk_bridge_tools | 127.0.0.1:4101 → 4100(LibreDesk 内部走 `bridge-tools:4100`) | + +数据:`postgres-data`、`redis-data` 两个 Docker 卷;上传文件在 `./uploads`。 +配置:`config.toml`(含加密密钥与 DB 密码,不入库)、`.env`(含容器密码,不入库)。 + +## 为什么会一直 “Connecting...” + +现象:用 `http://192.168.31.61:9001` 打开后台,右上角一直显示 `Connecting...`,app 日志反复报: + +``` +error upgrading tcp connection ... error="websocket: request origin not allowed by FastHTTPUpgrader.CheckOrigin" +``` + +原因:libredesk v2.9.x 的 Agent 实时通道(`/ws`)在 `cmd/websocket.go` 里做了 Origin 校验, +**只接受 `https` 或 `localhost` 的 Origin**,`http://<局域网IP>:9001` 一定被 403 拒绝,前端就无限重连。 + +解决:compose 里加了 `tls`(nginx)容器做 HTTPS 终结,后台走 **https://192.168.31.61:9443**。 +证书是自签的(SAN 含 `192.168.31.61`、`127.0.0.1`、`localhost`),首次访问浏览器会告警,点“高级 → 继续前往”即可; +想彻底不告警,把 [tls/cert.pem](tls/cert.pem) 导入你电脑的“受信任的根证书颁发机构”。 + +另一个等效办法(不动证书):`ssh -L 9001:127.0.0.1:9001 dev@192.168.31.61`,然后开 `http://localhost:9001` +(Origin 是 localhost,校验放行)。 + +## 常用操作 + +```bash +cd /home/dev/code/yehwang-ai/libredesk + +docker compose ps # 状态 +docker compose logs -f app # 日志 +docker compose restart app # 重启 +docker compose down # 停止(保留数据) +docker compose down -v # 停止并删除数据卷(会丢数据) + +# 重设 System 用户密码(交互式) +docker exec -it libredesk_app ./libredesk --set-system-user-password --config /libredesk/config.toml + +# 升级/切换版本:改 .env 的 LIBREDESK_IMAGE 后 recreate,启动命令会自动跑 --upgrade +docker compose pull && docker compose up -d +``` + +## 版本与升级 + +当前版本:**v2.9.1**(2026-10-28 发布的稳定版,替代 v2.9.0)。 + +版本演进:`v2.9.0`(初装)→ `v2.9.1-rc.1`(2026-10-28 15:06)→ `v2.9.1` 正式版(2026-10-28 15:42)。 +每一次升级都只改 `.env` 的 `LIBREDESK_IMAGE` + `docker compose up -d app`,启动命令自动执行 `--install --idempotent-install` 与 `--upgrade`。 + +| 升级 | 迁移 | 说明 | +| --- | --- | --- | +| v2.9.0 → v2.9.1-rc.1 | 无(`no upgrades to run`) | 48 commits / 14 PR,config.sample.toml 逐字节一致 | +| v2.9.1-rc.1 → v2.9.1 | 无(`no upgrades to run`) | 仅 2 commits / 1 个前端文件(automation 帮助文案对齐),无 schema 变化 | + +升级前的备份(都在 [backups/](backups)): + +| 时间 | DB 备份 | 当时版本 | +| --- | --- | --- | +| 2026-10-28 15:02 | [libredesk-v2.9.0-20261028-1502.dump](backups/libredesk-v2.9.0-20261028-1502.dump)(633 TOC 条目) | v2.9.0 | +| 2026-10-28 15:40 | [libredesk-v2.9.1-rc.1-20261028-1540.dump](backups/libredesk-v2.9.1-rc.1-20261028-1540.dump) | v2.9.1-rc.1 | + +**回滚**(到目前为止从没跑过迁移,回滚只是切镜像,不动数据):把 `.env` 的 `LIBREDESK_IMAGE` 改成目标版本(如 `libredesk/libredesk:v2.9.0`)再 `docker compose up -d app`。 +只有当某次升级真的执行了迁移时才需要恢复 dump: + +```bash +docker compose stop app +docker compose exec -T db psql -U libredesk -d libredesk -c 'drop schema public cascade; create schema public;' +docker compose exec -T db pg_restore -U libredesk -d libredesk --no-owner < backups/<对应版本>.dump +docker compose up -d app +``` + +v2.9.1 的主要变化:邮件主题/发件人 MIME 解码、IMAP 启动即扫描、`/health` 数据库不可用时返回非 200(已用于 compose 的 app healthcheck)、10 月安全审计的 5 个修复([PR #733](https://github.com/abhinavxd/libredesk/pull/733))、依赖漏洞升级([PR #742](https://github.com/abhinavxd/libredesk/pull/742))、AI 回复期间新消息继续接答、自动分配跳过 away 的 agent、日期筛选月份跳转、自定义属性可点击、automation 模板变量帮助等。详见 [release notes](https://github.com/abhinavxd/libredesk/releases/tag/v2.9.1)。 + +## 当前业务配置(2026-10-28) + +| 项 | 状态 | +| --- | --- | +| AI completion | `qwen-max` @ DashScope(openai 兼容),temperature 0.3、max_tokens 800 | +| AI embedding | `text-embedding-v4`,1024 维;108 篇帮助文章已嵌入(启动日志 `loaded embeddings into memory count=108`) | +| 通知邮件 / OTP | 163 SMTP(smtp.163.com:465, TLS),发件人 `Yehwang Support ` | +| 团队 | 「Wholesale Support」,Europe/Berlin,Round robin | +| 帮助中心 | `yehwang-help`(已启用,9 个分类 / 108 篇文章,已挂 livechat inbox 1) | +| AI Assistant | 「Yehwang Assistant」,friendly/balanced,max_turns 8,handoff + 引用开启,fallback team = Wholesale Support | +| AI Tools | `search_products` / `search_orders` / `search_inventory` / `get_customer_info`(后两个 requires_verification) | +| 渠道 inbox | **Website(livechat)** 已建:id=1,uuid `81b679b0-33fb-4796-9f83-dcc0f43231cc`;email / WhatsApp 待做 | +| 自动化 | 规则「Assign new conversations to AI Assistant」:新会话 → 分配给 Yehwang Assistant | +| 站点名 | `Yehwang Support` | + +> 换 AI 模型时注意:`qwen-plus-latest` 在这套长 prompt 下不调用工具(会凭自身知识乱答并编造引用),已验证 `qwen-max` 正常。 + +## Live Chat 接入(widget) + +```html + + +``` + +匿名访客即可发起会话;已登录客户可用站点后端签 JWT(`POST /api/v1/widget/chat/auth/exchange`)做身份锚定。 + +## 运维:备份 / 访问控制 / 安全头 + +**备份(已定时)** + +- 脚本:[scripts/backup.sh](scripts/backup.sh),产物在 [backups/](backups):`db-.dump`(pg_dump -Fc)、`uploads-.tar.gz`、`config.toml.`、`env.`; +- 校验:每个 dump 会用 `pg_restore -l` 读出 TOC(当前 629 条),失败则脚本非 0 退出; +- 保留 **14 天**(`find -mtime +14 -delete`); +- cron(dev 用户,已安装):`0 3 * * * cd .../libredesk && ./scripts/backup.sh >> logs/backup.log 2>&1`; +- 恢复演练(已验证过一次:57 张表 / 108 篇文章): + +```bash +docker cp backups/db-.dump libredesk_db:/tmp/restore.dump +docker compose exec -T db createdb -U libredesk libredesk_restore_test +docker compose exec -T db pg_restore -U libredesk -d libredesk_restore_test --no-owner /tmp/restore.dump +docker compose exec -T db psql -U libredesk -d libredesk_restore_test -tAc "select count(*) from help_articles" +docker compose exec -T db dropdb -U libredesk libredesk_restore_test +``` + +**访问控制** + +- `9001`(明文 app)只绑 `127.0.0.1`,局域网只能走 `https://192.168.31.61:9443`;本机/SSH 隧道调试不受影响; +- nginx 已加 `TLSv1.2/1.3`、`X-Content-Type-Options: nosniff`、`Referrer-Policy`(X-Frame-Options 由应用自己发,避免重复)。 + +### ⚠️ bridge-tools 依赖本机 DB 隧道 + +bridge-tools 的 4 个工具都要访问 yehwang 业务 RPC(`host.docker.internal:9632/9612/9509/9602`), +而这些服务依赖本机到 QA1 的 SSH 隧道(`172.18.0.1:3318` MySQL 等)。**隧道没起时**: + +- `search_products`:`/product/list` 报 Internal Server Error → 工具回退 autocomplete,**只有商品名、没有价格/库存**; +- `search_orders` / `get_customer_info`:RPC 返回 `SQLSTATE[HY000] [2002] Connection refused`。 + +启动顺序:先 `yw-db-tunnel start`(`yw-db-tunnel status` 确认“已转发”),再 `docker compose up -d`。 +隧道断线后工具会“半残”,排查时先看 `docker compose logs bridge-tools` 与 `testdb "select 1"`。 + +**隧道已配置自启 / 自愈**(dev 用户 crontab): + +``` +@reboot sleep 60; /home/dev/code/yehwang-ai/libredesk/scripts/ensure-tunnel.sh +*/5 * * * * /home/dev/code/yehwang-ai/libredesk/scripts/ensure-tunnel.sh +``` + +- [scripts/ensure-tunnel.sh](scripts/ensure-tunnel.sh) 幂等:隧道完整时静默退出,异常时 `yw-db-tunnel start` 并写 `logs/tunnel.log`; +- 依赖 cron 开机自启(已确认 enabled)和 `yw-devbox` 容器(restart=unless-stopped,已确认); +- 如需手动确认:`yw-db-tunnel status` + `testdb "select 1"`。 + +## 已知偏差(相对官方 quickstart) + +- **DB 镜像**:官方 compose 用 `postgres:17-alpine`,本机从 docker.io / 各镜像源拉取该 tag 均失败(docker.io 长时间无进展、1panel 403、imgdb 层校验失败), + 因此改用本机已缓存的 `pgvector/pgvector:pg18`(官方 `postgres` 镜像的 PG18 基础映像,`crm-new` 项目的 LibreDesk 已在用,兼容)。 + PG18 的数据目录是 `/var/lib/postgresql`,compose 已按此挂载;如以后想换回 `postgres:17-alpine`,**不能直接复用数据卷**(18→17 需 dump/restore)。 +- **System 用户密码强度**:libredesk 要求 10-72 位且含大小写+数字+特殊字符,纯随机 hex 会被拒绝(启动日志 `system user password is not strong`)。 + +## 注意事项 + +- `app.encryption_key` 首次启动后**不要改**,否则已保存的密钥(如收件箱密码)无法解密。 +- `config.toml` 里 `disable_secure_cookies = true` 是给 http://localhost:9001 直连用的;如果只走 https,可以改回 `false` 更安全。 +- 反代已按官方要求设置 `X-Client-IP $remote_addr`(限流/审计需要),见 [tls/nginx.conf](tls/nginx.conf)。 +- 初始化时 compose 的 command 会执行 `--install --idempotent-install`,这是幂等的;**不要**对已有数据库手工跑 `--install`(官方文档警告它会删除已有数据)。 +- 如需公网/域名访问,建议按官方文档加 Nginx 反向代理,并在 Cloudflare 场景下转发 `CF-Connecting-IP`。 diff --git a/bridge-tools/.dockerignore b/bridge-tools/.dockerignore new file mode 100644 index 0000000..af6f0a8 --- /dev/null +++ b/bridge-tools/.dockerignore @@ -0,0 +1,4 @@ +.env +node_modules +test +*.log diff --git a/bridge-tools/.env.example b/bridge-tools/.env.example new file mode 100644 index 0000000..3d8cd65 --- /dev/null +++ b/bridge-tools/.env.example @@ -0,0 +1,19 @@ +# bridge-tools 环境变量(由 libredesk/docker-compose.yml 注入) +TOOLS_PORT=4100 +TOOLS_PUBLIC_BASE_URL=http://bridge-tools:4100 +# 与 LibreDesk 工具 Headers 里的 X-Tools-Key 保持一致 +TOOLS_SHARED_KEY=change-me +# 仅本地联调可设 true(跳过 X-Libredesk-Contact-Verified 校验),生产必须 false +TOOLS_DEV_BYPASS=false + +# yehwang 业务 RPC(容器内经 host.docker.internal 访问宿主机已发布端口) +RECOMMEND_URL=jsonrpc://host.docker.internal:9632 +ORDER_API_URL=jsonrpc://host.docker.internal:9612 +INVENTORY_API_URL=jsonrpc://host.docker.internal:9509 +MEMBER_RPC_URL=jsonrpc://host.docker.internal:9602 +RPC_TIMEOUT_MS=8000 +PRODUCT_TIMEOUT_MS=30000 + +SITE_SEARCH_URL=https://www.yehwang.com/search.html?q= +DEFAULT_TENANT_ID=2 +DEFAULT_LANGUAGE_ID=1 diff --git a/bridge-tools/Dockerfile b/bridge-tools/Dockerfile new file mode 100644 index 0000000..3bf3fca --- /dev/null +++ b/bridge-tools/Dockerfile @@ -0,0 +1,21 @@ +FROM node:24-alpine + +WORKDIR /app + +# 无第三方依赖(只用 Node 标准库),直接拷贝源码 +COPY package.json ./ +COPY src ./src +COPY config ./config + +# 宿主机生成的文件可能是 600,复制后统一放开只读权限 +RUN chmod -R a+rX /app + +ENV NODE_ENV=production +USER node + +EXPOSE 4100 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:4100/healthz || exit 1 + +CMD ["node", "src/server.js"] diff --git a/bridge-tools/README.md b/bridge-tools/README.md new file mode 100644 index 0000000..e6b6711 --- /dev/null +++ b/bridge-tools/README.md @@ -0,0 +1,39 @@ +# bridge-tools + +LibreDesk 的**业务工具服务**(从 `crm-new/bridge` 裁剪):只保留 AI 助手需要的 4 个 HTTP 工具, +其余能力(AI 编排、RAG/知识库、OTP 验证、转人工、身份签发、质量飞轮)全部由 LibreDesk v2.9.1 内置实现。 + +## 工具 + +| 工具 | 需要验证 | 作用 | +| --- | --- | --- | +| `search_products` | 否 | 商品搜索(价格/库存/图片/product_id/URL),catalog 失败时退回 autocomplete 和站点地图类目链接 | +| `search_orders` | **是** | 订单列表/详情(状态、商品、金额、地址、物流);邮箱只取 LibreDesk 身份头 | +| `search_inventory` | 否 | 实时库存 + 预购 ETA | +| `get_customer_info` | **是** | 会员资料(等级、积分、余额、注册时间等) | + +## 接口 + +- `GET /healthz` — 健康检查(公开) +- `GET /tools/manifest` — 工具清单(`X-Tools-Key`),可用来在 LibreDesk 批量注册 +- `POST /tools/` — 执行工具(`X-Tools-Key` + LibreDesk 注入的 `X-Libredesk-*` 身份头) + +## 与 LibreDesk 的约定 + +- LibreDesk 调用自定义工具时会注入身份头: + `X-Libredesk-Contact-Id` / `-External-Id` / `-Type` / `-Email` / `-Verified`、`X-Libredesk-Conversation-UUID`、`X-Libredesk-Inbox-Id`; +- 邮箱、验证状态一律以这些头为准(不信任模型参数); +- `search_orders` / `get_customer_info` 在 LibreDesk 侧应设置 `requires_verification=true`,客户验证(内置 OTP)后才会被放行; +- 工具鉴权:在 LibreDesk 的工具 Headers 里配置 `X-Tools-Key`,值等于本服务的 `TOOLS_SHARED_KEY`。 + +## 本地运行 / 测试 + +```bash +node --test "test/*.test.js" # 单测(mock RPC,不需要外部服务) +node src/server.js # 本机启动(需环境变量,见 .env.example) +``` + +## 部署 + +作为服务并入 `../libredesk/docker-compose.yml`(服务名 `bridge-tools`), +与 LibreDesk 同一 Docker 网络,LibreDesk 侧工具 URL 用 `http://bridge-tools:4100/tools/`。 diff --git a/bridge-tools/config/site-map-us.json b/bridge-tools/config/site-map-us.json new file mode 100644 index 0000000..d2962bf --- /dev/null +++ b/bridge-tools/config/site-map-us.json @@ -0,0 +1,2463 @@ +{ + "site": "us", + "generated_at": "2026-08-14T10:46:11.381Z", + "total": 20353, + "navCount": 311, + "entries": [ + { + "category": "accessories-5608/apparel-accessories", + "title": "Apparel Accessories", + "url": "https://us.yehwang.com/accessories-5608/apparel-accessories", + "parent": "accessories-5608", + "level": 2, + "aliases": [] + }, + { + "category": "accessories-5608/apparel-accessories/bag-charms", + "title": "Bag Charms", + "url": "https://us.yehwang.com/accessories-5608/apparel-accessories/bag-charms", + "parent": "accessories-5608/apparel-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/apparel-accessories/belts", + "title": "Belts", + "url": "https://us.yehwang.com/accessories-5608/apparel-accessories/belts", + "parent": "accessories-5608/apparel-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/apparel-accessories/brooches", + "title": "Brooches", + "url": "https://us.yehwang.com/accessories-5608/apparel-accessories/brooches", + "parent": "accessories-5608/apparel-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/apparel-accessories/socks", + "title": "Socks", + "url": "https://us.yehwang.com/accessories-5608/apparel-accessories/socks", + "parent": "accessories-5608/apparel-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/makeup-tools-and-bags", + "title": "Makeup Tools And Bags", + "url": "https://us.yehwang.com/accessories-5608/makeup-tools-and-bags", + "parent": "accessories-5608", + "level": 2, + "aliases": [] + }, + { + "category": "accessories-5608/makeup-tools-and-bags/combs", + "title": "Combs", + "url": "https://us.yehwang.com/accessories-5608/makeup-tools-and-bags/combs", + "parent": "accessories-5608/makeup-tools-and-bags", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/makeup-tools-and-bags/makeup-bags", + "title": "Makeup Bags", + "url": "https://us.yehwang.com/accessories-5608/makeup-tools-and-bags/makeup-bags", + "parent": "accessories-5608/makeup-tools-and-bags", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/makeup-tools-and-bags/makeup-brushes", + "title": "Makeup Brushes", + "url": "https://us.yehwang.com/accessories-5608/makeup-tools-and-bags/makeup-brushes", + "parent": "accessories-5608/makeup-tools-and-bags", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/makeup-tools-and-bags/makeup-puffs", + "title": "Makeup Puffs", + "url": "https://us.yehwang.com/accessories-5608/makeup-tools-and-bags/makeup-puffs", + "parent": "accessories-5608/makeup-tools-and-bags", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/phone-accessories", + "title": "Phone Accessories", + "url": "https://us.yehwang.com/accessories-5608/phone-accessories", + "parent": "accessories-5608", + "level": 2, + "aliases": [] + }, + { + "category": "accessories-5608/phone-accessories/earphone-case", + "title": "Earphone Case", + "url": "https://us.yehwang.com/accessories-5608/phone-accessories/earphone-case", + "parent": "accessories-5608/phone-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/phone-accessories/phone-accessories", + "title": "Phone Accessories", + "url": "https://us.yehwang.com/accessories-5608/phone-accessories/phone-accessories", + "parent": "accessories-5608/phone-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/phone-accessories/phone-bags", + "title": "Phone Bags", + "url": "https://us.yehwang.com/accessories-5608/phone-accessories/phone-bags", + "parent": "accessories-5608/phone-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories-5608/phone-accessories/phone-chain", + "title": "Phone Chain", + "url": "https://us.yehwang.com/accessories-5608/phone-accessories/phone-chain", + "parent": "accessories-5608/phone-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "apparel-accessories", + "title": "Apparel Accessories", + "url": "https://us.yehwang.com/apparel-accessories", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "apparel-accessories/bags", + "title": "Bags", + "url": "https://us.yehwang.com/apparel-accessories/bags", + "parent": "apparel-accessories", + "level": 2, + "aliases": [ + "包", + "sacs", + "taschen", + "tassen", + "bags" + ] + }, + { + "category": "apparel-accessories/gloves", + "title": "Gloves", + "url": "https://us.yehwang.com/apparel-accessories/gloves", + "parent": "apparel-accessories", + "level": 2, + "aliases": [ + "手套", + "gloves" + ] + }, + { + "category": "apparel-accessories/hats", + "title": "Hats", + "url": "https://us.yehwang.com/apparel-accessories/hats", + "parent": "apparel-accessories", + "level": 2, + "aliases": [ + "帽子", + "hats", + "hoeden" + ] + }, + { + "category": "apparel-accessories/scarves", + "title": "Scarves", + "url": "https://us.yehwang.com/apparel-accessories/scarves", + "parent": "apparel-accessories", + "level": 2, + "aliases": [ + "围巾", + "scarves", + "sjaals" + ] + }, + { + "category": "apparel-accessories/socks", + "title": "Socks", + "url": "https://us.yehwang.com/apparel-accessories/socks", + "parent": "apparel-accessories", + "level": 2, + "aliases": [] + }, + { + "category": "back-to-school", + "title": "Back To School", + "url": "https://us.yehwang.com/back-to-school", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "bags-5807/beach-bags", + "title": "Beach Bags", + "url": "https://us.yehwang.com/bags-5807/beach-bags", + "parent": "bags-5807", + "level": 2, + "aliases": [] + }, + { + "category": "bags-5807/clutches", + "title": "Clutches", + "url": "https://us.yehwang.com/bags-5807/clutches", + "parent": "bags-5807", + "level": 2, + "aliases": [ + "手拿包", + "clutches" + ] + }, + { + "category": "bags-5807/crossbody-bags", + "title": "Crossbody Bags", + "url": "https://us.yehwang.com/bags-5807/crossbody-bags", + "parent": "bags-5807", + "level": 2, + "aliases": [] + }, + { + "category": "bags-5807/handbags", + "title": "Handbags", + "url": "https://us.yehwang.com/bags-5807/handbags", + "parent": "bags-5807", + "level": 2, + "aliases": [] + }, + { + "category": "bags-5807/laptop-bag", + "title": "Laptop Bag", + "url": "https://us.yehwang.com/bags-5807/laptop-bag", + "parent": "bags-5807", + "level": 2, + "aliases": [] + }, + { + "category": "bags-5807/shoulder-bags", + "title": "Shoulder Bags", + "url": "https://us.yehwang.com/bags-5807/shoulder-bags", + "parent": "bags-5807", + "level": 2, + "aliases": [ + "单肩包", + "shoulder bags" + ] + }, + { + "category": "bags-5807/tote-bags", + "title": "Tote Bags", + "url": "https://us.yehwang.com/bags-5807/tote-bags", + "parent": "bags-5807", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms", + "title": "Bags And Charms", + "url": "https://us.yehwang.com/bags-and-charms", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "bags-and-charms/bag-straps", + "title": "Bag Straps", + "url": "https://us.yehwang.com/bags-and-charms/bag-straps", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/beach-bags", + "title": "Beach Bags", + "url": "https://us.yehwang.com/bags-and-charms/beach-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/clutches", + "title": "Clutches", + "url": "https://us.yehwang.com/bags-and-charms/clutches", + "parent": "bags-and-charms", + "level": 2, + "aliases": [ + "手拿包", + "clutches" + ] + }, + { + "category": "bags-and-charms/crossbody-bags", + "title": "Crossbody Bags", + "url": "https://us.yehwang.com/bags-and-charms/crossbody-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/handbags", + "title": "Handbags", + "url": "https://us.yehwang.com/bags-and-charms/handbags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain", + "title": "Keychain", + "url": "https://us.yehwang.com/bags-and-charms/keychain", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain/beaded-keychains", + "title": "Beaded Keychains", + "url": "https://us.yehwang.com/bags-and-charms/keychain/beaded-keychains", + "parent": "bags-and-charms/keychain", + "level": 3, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain/leather-keychains", + "title": "Leather Keychains", + "url": "https://us.yehwang.com/bags-and-charms/keychain/leather-keychains", + "parent": "bags-and-charms/keychain", + "level": 3, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain/metal-keychains", + "title": "Metal Keychains", + "url": "https://us.yehwang.com/bags-and-charms/keychain/metal-keychains", + "parent": "bags-and-charms/keychain", + "level": 3, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain/plastic-and-resin-keychains", + "title": "Plastic And Resin Keychains", + "url": "https://us.yehwang.com/bags-and-charms/keychain/plastic-and-resin-keychains", + "parent": "bags-and-charms/keychain", + "level": 3, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain/plush-and-crochet-keychains", + "title": "Plush And Crochet Keychains", + "url": "https://us.yehwang.com/bags-and-charms/keychain/plush-and-crochet-keychains", + "parent": "bags-and-charms/keychain", + "level": 3, + "aliases": [] + }, + { + "category": "bags-and-charms/keychain/rhinestone-keychains", + "title": "Rhinestone Keychains", + "url": "https://us.yehwang.com/bags-and-charms/keychain/rhinestone-keychains", + "parent": "bags-and-charms/keychain", + "level": 3, + "aliases": [] + }, + { + "category": "bags-and-charms/laptop-bags", + "title": "Laptop Bags", + "url": "https://us.yehwang.com/bags-and-charms/laptop-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/makeup-bags", + "title": "Makeup Bags", + "url": "https://us.yehwang.com/bags-and-charms/makeup-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/shopper-bags", + "title": "Shopper Bags", + "url": "https://us.yehwang.com/bags-and-charms/shopper-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [ + "购物袋", + "shopper" + ] + }, + { + "category": "bags-and-charms/shoulder-bags", + "title": "Shoulder Bags", + "url": "https://us.yehwang.com/bags-and-charms/shoulder-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [ + "单肩包", + "shoulder bags" + ] + }, + { + "category": "bags-and-charms/sports-bags-and-casual-backpacks", + "title": "Sports Bags And Casual Backpacks", + "url": "https://us.yehwang.com/bags-and-charms/sports-bags-and-casual-backpacks", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/stripe-bags", + "title": "Stripe Bags", + "url": "https://us.yehwang.com/bags-and-charms/stripe-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/suede-and-studded-bags", + "title": "Suede And Studded Bags", + "url": "https://us.yehwang.com/bags-and-charms/suede-and-studded-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/tote-bags", + "title": "Tote Bags", + "url": "https://us.yehwang.com/bags-and-charms/tote-bags", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "bags-and-charms/wallets-and-card-holders", + "title": "Wallets And Card Holders", + "url": "https://us.yehwang.com/bags-and-charms/wallets-and-card-holders", + "parent": "bags-and-charms", + "level": 2, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers", + "title": "Bestsellers", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers", + "parent": "factory-direct-5580", + "level": 2, + "aliases": [ + "畅销", + "畅销榜", + "最畅销", + "bestseller", + "best seller", + "trending", + "popular", + "热门", + "流行", + "bestsellers" + ] + }, + { + "category": "factory-direct-5580/bestsellers/accessories", + "title": "Accessories", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/accessories", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/bag-charms", + "title": "Bag Charms", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/bag-charms", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/bags", + "title": "Bags", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/bags", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "包", + "sacs", + "taschen", + "tassen", + "bags" + ] + }, + { + "category": "factory-direct-5580/bestsellers/beads", + "title": "Beads", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/beads", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/body-jewelry", + "title": "Body Jewelry", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/body-jewelry", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/bracelets", + "title": "Bracelets", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/bracelets", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "手链", + "bracelets", + "armbänder", + "armbanden", + "bracelets" + ] + }, + { + "category": "factory-direct-5580/bestsellers/clothes", + "title": "Clothes", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/clothes", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/earrings", + "title": "Earrings", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/earrings", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "耳环", + "boucles", + "ohrringe", + "oorbellen", + "earrings" + ] + }, + { + "category": "factory-direct-5580/bestsellers/glasses", + "title": "Glasses", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/glasses", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/hair-accessories", + "title": "Hair Accessories", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/hair-accessories", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "发饰", + "hair accessories", + "haaraccessoires" + ] + }, + { + "category": "factory-direct-5580/bestsellers/hats", + "title": "Hats", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/hats", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "帽子", + "hats", + "hoeden" + ] + }, + { + "category": "factory-direct-5580/bestsellers/imitation-pearl-jewelry", + "title": "Imitation Pearl Jewelry", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/imitation-pearl-jewelry", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/jewelry", + "title": "Jewelry", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/jewelry", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/jewelry-making", + "title": "Jewelry Making", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/jewelry-making", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/jewelry-sets", + "title": "Jewelry Sets", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/jewelry-sets", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/keychain", + "title": "Keychain", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/keychain", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/making-supplies", + "title": "Making Supplies", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/making-supplies", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/natural-stone-jewelry", + "title": "Natural Stone Jewelry", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/natural-stone-jewelry", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/necklaces", + "title": "Necklaces", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/necklaces", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "项链", + "colliers", + "halsketten", + "kettingen", + "cadenas", + "necklaces" + ] + }, + { + "category": "factory-direct-5580/bestsellers/packing-and-display", + "title": "Packing And Display", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/packing-and-display", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/pendants", + "title": "Pendants", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/pendants", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/phone-accessories", + "title": "Phone Accessories", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/phone-accessories", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/piercing-jewelry", + "title": "Piercing Jewelry", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/piercing-jewelry", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/rings", + "title": "Rings", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/rings", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "戒指", + "bagues", + "ringe", + "ringen", + "rings" + ] + }, + { + "category": "factory-direct-5580/bestsellers/stainless-steel-anklets", + "title": "Stainless Steel Anklets", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/stainless-steel-anklets", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/stainless-steel-bracelets", + "title": "Stainless Steel Bracelets", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/stainless-steel-bracelets", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/stainless-steel-earrings", + "title": "Stainless Steel Earrings", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/stainless-steel-earrings", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/stainless-steel-necklaces", + "title": "Stainless Steel Necklaces", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/stainless-steel-necklaces", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/stainless-steel-rings", + "title": "Stainless Steel Rings", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/stainless-steel-rings", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/bestsellers/summer-scarves", + "title": "Summer Scarves", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/summer-scarves", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "夏季围巾", + "summer scarves" + ] + }, + { + "category": "factory-direct-5580/bestsellers/winter-scarves", + "title": "Winter Scarves", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/winter-scarves", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [ + "冬季围巾", + "winter scarves" + ] + }, + { + "category": "factory-direct-5580/bestsellers/zircon-jewelry", + "title": "Zircon Jewelry", + "url": "https://us.yehwang.com/factory-direct-5580/bestsellers/zircon-jewelry", + "parent": "factory-direct-5580/bestsellers", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/clothes", + "title": "Clothes", + "url": "https://us.yehwang.com/factory-direct-5580/clothes", + "parent": "factory-direct-5580", + "level": 2, + "aliases": [] + }, + { + "category": "factory-direct-5580/clothes/activewear", + "title": "Activewear", + "url": "https://us.yehwang.com/factory-direct-5580/clothes/activewear", + "parent": "factory-direct-5580/clothes", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/clothes/bottoms", + "title": "Bottoms", + "url": "https://us.yehwang.com/factory-direct-5580/clothes/bottoms", + "parent": "factory-direct-5580/clothes", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/clothes/dresses", + "title": "Dresses", + "url": "https://us.yehwang.com/factory-direct-5580/clothes/dresses", + "parent": "factory-direct-5580/clothes", + "level": 3, + "aliases": [ + "连衣裙", + "robes", + "kleider", + "jurken", + "dresses" + ] + }, + { + "category": "factory-direct-5580/clothes/swimwear", + "title": "Swimwear", + "url": "https://us.yehwang.com/factory-direct-5580/clothes/swimwear", + "parent": "factory-direct-5580/clothes", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/clothes/tops", + "title": "Tops", + "url": "https://us.yehwang.com/factory-direct-5580/clothes/tops", + "parent": "factory-direct-5580/clothes", + "level": 3, + "aliases": [ + "上衣", + "hauts", + "tops", + "blouses" + ] + }, + { + "category": "factory-direct-5580/clothes/women-and-039-s-knitwear", + "title": "Women And 039 S Knitwear", + "url": "https://us.yehwang.com/factory-direct-5580/clothes/women-and-039-s-knitwear", + "parent": "factory-direct-5580/clothes", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/hair-accessories", + "title": "Hair Accessories", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories", + "parent": "factory-direct-5580", + "level": 2, + "aliases": [ + "发饰", + "hair accessories", + "haaraccessoires" + ] + }, + { + "category": "factory-direct-5580/hair-accessories/bridal-headwear", + "title": "Bridal Headwear", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories/bridal-headwear", + "parent": "factory-direct-5580/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/hair-accessories/hair-bands", + "title": "Hair Bands", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories/hair-bands", + "parent": "factory-direct-5580/hair-accessories", + "level": 3, + "aliases": [ + "发带", + "hair bands", + "haarbanden" + ] + }, + { + "category": "factory-direct-5580/hair-accessories/hair-claws", + "title": "Hair Claws", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories/hair-claws", + "parent": "factory-direct-5580/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/hair-accessories/hair-clips", + "title": "Hair Clips", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories/hair-clips", + "parent": "factory-direct-5580/hair-accessories", + "level": 3, + "aliases": [ + "发夹", + "hair clips", + "haarclips" + ] + }, + { + "category": "factory-direct-5580/hair-accessories/hair-ties", + "title": "Hair Ties", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories/hair-ties", + "parent": "factory-direct-5580/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/hair-accessories/scrunchies", + "title": "Scrunchies", + "url": "https://us.yehwang.com/factory-direct-5580/hair-accessories/scrunchies", + "parent": "factory-direct-5580/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "factory-direct-5580/sale", + "title": "Sale", + "url": "https://us.yehwang.com/factory-direct-5580/sale", + "parent": "factory-direct-5580", + "level": 2, + "aliases": [] + }, + { + "category": "glasses", + "title": "Glasses", + "url": "https://us.yehwang.com/glasses", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "glasses/aviator-glasses", + "title": "Aviator Glasses", + "url": "https://us.yehwang.com/glasses/aviator-glasses", + "parent": "glasses", + "level": 2, + "aliases": [] + }, + { + "category": "glasses/optical-glasses", + "title": "Optical Glasses", + "url": "https://us.yehwang.com/glasses/optical-glasses", + "parent": "glasses", + "level": 2, + "aliases": [] + }, + { + "category": "glasses/retro-cat-eye", + "title": "Retro Cat Eye", + "url": "https://us.yehwang.com/glasses/retro-cat-eye", + "parent": "glasses", + "level": 2, + "aliases": [] + }, + { + "category": "glasses/sports-sunglasses", + "title": "Sports Sunglasses", + "url": "https://us.yehwang.com/glasses/sports-sunglasses", + "parent": "glasses", + "level": 2, + "aliases": [] + }, + { + "category": "glasses/sunglasses", + "title": "Sunglasses", + "url": "https://us.yehwang.com/glasses/sunglasses", + "parent": "glasses", + "level": 2, + "aliases": [ + "太阳镜", + "sunglasses", + "zonnebrillen" + ] + }, + { + "category": "glasses/sunglasses-accessories", + "title": "Sunglasses Accessories", + "url": "https://us.yehwang.com/glasses/sunglasses-accessories", + "parent": "glasses", + "level": 2, + "aliases": [] + }, + { + "category": "hats-and-scarves/hats", + "title": "Hats", + "url": "https://us.yehwang.com/hats-and-scarves/hats", + "parent": "hats-and-scarves", + "level": 2, + "aliases": [ + "帽子", + "hats", + "hoeden" + ] + }, + { + "category": "hats-and-scarves/hats/baseball-cap", + "title": "Baseball Cap", + "url": "https://us.yehwang.com/hats-and-scarves/hats/baseball-cap", + "parent": "hats-and-scarves/hats", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/hats/beanie", + "title": "Beanie", + "url": "https://us.yehwang.com/hats-and-scarves/hats/beanie", + "parent": "hats-and-scarves/hats", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/hats/berets", + "title": "Berets", + "url": "https://us.yehwang.com/hats-and-scarves/hats/berets", + "parent": "hats-and-scarves/hats", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/hats/bucket-hats", + "title": "Bucket Hats", + "url": "https://us.yehwang.com/hats-and-scarves/hats/bucket-hats", + "parent": "hats-and-scarves/hats", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/hats/earflap-hat", + "title": "Earflap Hat", + "url": "https://us.yehwang.com/hats-and-scarves/hats/earflap-hat", + "parent": "hats-and-scarves/hats", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/hats/straw-hats", + "title": "Straw Hats", + "url": "https://us.yehwang.com/hats-and-scarves/hats/straw-hats", + "parent": "hats-and-scarves/hats", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/scarves-and-gloves/bandana-and-scarves", + "title": "Bandana And Scarves", + "url": "https://us.yehwang.com/hats-and-scarves/scarves-and-gloves/bandana-and-scarves", + "parent": "hats-and-scarves/scarves-and-gloves", + "level": 3, + "aliases": [] + }, + { + "category": "hats-and-scarves/scarves-and-gloves/gloves", + "title": "Gloves", + "url": "https://us.yehwang.com/hats-and-scarves/scarves-and-gloves/gloves", + "parent": "hats-and-scarves/scarves-and-gloves", + "level": 3, + "aliases": [ + "手套", + "gloves" + ] + }, + { + "category": "hats-and-scarves/scarves-and-gloves/winter-scarves", + "title": "Winter Scarves", + "url": "https://us.yehwang.com/hats-and-scarves/scarves-and-gloves/winter-scarves", + "parent": "hats-and-scarves/scarves-and-gloves", + "level": 3, + "aliases": [ + "冬季围巾", + "winter scarves" + ] + }, + { + "category": "high-end-jewelry-5908", + "title": "High End Jewelry 5908", + "url": "https://us.yehwang.com/high-end-jewelry-5908", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/925-sterling-silver-jewelry", + "title": "925 Sterling Silver Jewelry", + "url": "https://us.yehwang.com/high-end-jewelry-5908/925-sterling-silver-jewelry", + "parent": "high-end-jewelry-5908", + "level": 2, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-bracelets", + "title": "925 Sterling Silver Bracelets", + "url": "https://us.yehwang.com/high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-bracelets", + "parent": "high-end-jewelry-5908/925-sterling-silver-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-earrings", + "title": "925 Sterling Silver Earrings", + "url": "https://us.yehwang.com/high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-earrings", + "parent": "high-end-jewelry-5908/925-sterling-silver-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-necklaces", + "title": "925 Sterling Silver Necklaces", + "url": "https://us.yehwang.com/high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-necklaces", + "parent": "high-end-jewelry-5908/925-sterling-silver-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-rings", + "title": "925 Sterling Silver Rings", + "url": "https://us.yehwang.com/high-end-jewelry-5908/925-sterling-silver-jewelry/925-sterling-silver-rings", + "parent": "high-end-jewelry-5908/925-sterling-silver-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/freshwater-pearl", + "title": "Freshwater Pearl", + "url": "https://us.yehwang.com/high-end-jewelry-5908/freshwater-pearl", + "parent": "high-end-jewelry-5908", + "level": 2, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/freshwater-pearl/loose-pearl", + "title": "Loose Pearl", + "url": "https://us.yehwang.com/high-end-jewelry-5908/freshwater-pearl/loose-pearl", + "parent": "high-end-jewelry-5908/freshwater-pearl", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/moissanite-jewelry", + "title": "Moissanite Jewelry", + "url": "https://us.yehwang.com/high-end-jewelry-5908/moissanite-jewelry", + "parent": "high-end-jewelry-5908", + "level": 2, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/moissanite-jewelry/moissanite-bracelets", + "title": "Moissanite Bracelets", + "url": "https://us.yehwang.com/high-end-jewelry-5908/moissanite-jewelry/moissanite-bracelets", + "parent": "high-end-jewelry-5908/moissanite-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/moissanite-jewelry/moissanite-earrings", + "title": "Moissanite Earrings", + "url": "https://us.yehwang.com/high-end-jewelry-5908/moissanite-jewelry/moissanite-earrings", + "parent": "high-end-jewelry-5908/moissanite-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/moissanite-jewelry/moissanite-necklaces", + "title": "Moissanite Necklaces", + "url": "https://us.yehwang.com/high-end-jewelry-5908/moissanite-jewelry/moissanite-necklaces", + "parent": "high-end-jewelry-5908/moissanite-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "high-end-jewelry-5908/moissanite-jewelry/moissanite-rings", + "title": "Moissanite Rings", + "url": "https://us.yehwang.com/high-end-jewelry-5908/moissanite-jewelry/moissanite-rings", + "parent": "high-end-jewelry-5908/moissanite-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609", + "title": "Jewelry 5609", + "url": "https://us.yehwang.com/jewelry-5609", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "jewelry-5609/body-jewelry", + "title": "Body Jewelry", + "url": "https://us.yehwang.com/jewelry-5609/body-jewelry", + "parent": "jewelry-5609", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-5609/body-jewelry/anklets", + "title": "Anklets", + "url": "https://us.yehwang.com/jewelry-5609/body-jewelry/anklets", + "parent": "jewelry-5609/body-jewelry", + "level": 3, + "aliases": [ + "脚链", + "anklets", + "enkelbanden" + ] + }, + { + "category": "jewelry-5609/body-jewelry/waist-chains", + "title": "Waist Chains", + "url": "https://us.yehwang.com/jewelry-5609/body-jewelry/waist-chains", + "parent": "jewelry-5609/body-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets", + "title": "Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets", + "parent": "jewelry-5609", + "level": 2, + "aliases": [ + "手链", + "bracelets", + "armbänder", + "armbanden", + "bracelets" + ] + }, + { + "category": "jewelry-5609/bracelets/bangles", + "title": "Bangles", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/bangles", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets/beaded-bracelets", + "title": "Beaded Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/beaded-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [ + "串珠手链", + "beaded", + "kralen armbanden" + ] + }, + { + "category": "jewelry-5609/bracelets/bracelets-sets", + "title": "Bracelets Sets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/bracelets-sets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets/braided-bracelets", + "title": "Braided Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/braided-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets/chain-bracelets", + "title": "Chain Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/chain-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets/charm-bracelets", + "title": "Charm Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/charm-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [ + "吊坠手链", + "charm bracelets", + "bedels armbanden" + ] + }, + { + "category": "jewelry-5609/bracelets/cuff-bracelets", + "title": "Cuff Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/cuff-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets/hand-chain-bracelets", + "title": "Hand Chain Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/hand-chain-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/bracelets/statement-bracelets", + "title": "Statement Bracelets", + "url": "https://us.yehwang.com/jewelry-5609/bracelets/statement-bracelets", + "parent": "jewelry-5609/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings", + "title": "Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings", + "parent": "jewelry-5609", + "level": 2, + "aliases": [ + "耳环", + "boucles", + "ohrringe", + "oorbellen", + "earrings" + ] + }, + { + "category": "jewelry-5609/earrings/beads-earrings", + "title": "Beads Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings/beads-earrings", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings/charm-earrings", + "title": "Charm Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings/charm-earrings", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings/dangle-earrings", + "title": "Dangle Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings/dangle-earrings", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings/ear-cuffs", + "title": "Ear Cuffs", + "url": "https://us.yehwang.com/jewelry-5609/earrings/ear-cuffs", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [ + "耳骨夹", + "ear cuffs", + "oorcuffs" + ] + }, + { + "category": "jewelry-5609/earrings/ear-studs", + "title": "Ear Studs", + "url": "https://us.yehwang.com/jewelry-5609/earrings/ear-studs", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [ + "耳钉", + "studs", + "oorknopjes" + ] + }, + { + "category": "jewelry-5609/earrings/earrings-set", + "title": "Earrings Set", + "url": "https://us.yehwang.com/jewelry-5609/earrings/earrings-set", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings/earrings-with-stone", + "title": "Earrings With Stone", + "url": "https://us.yehwang.com/jewelry-5609/earrings/earrings-with-stone", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings/hoop-earrings", + "title": "Hoop Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings/hoop-earrings", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [ + "圈形耳环", + "hoops", + "creolen", + "hoop oorbellen" + ] + }, + { + "category": "jewelry-5609/earrings/party-earrings", + "title": "Party Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings/party-earrings", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/earrings/statement-earrings", + "title": "Statement Earrings", + "url": "https://us.yehwang.com/jewelry-5609/earrings/statement-earrings", + "parent": "jewelry-5609/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/jewelry-sets", + "title": "Jewelry Sets", + "url": "https://us.yehwang.com/jewelry-5609/jewelry-sets", + "parent": "jewelry-5609", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-5609/jewelry-sets/bracelets-sets", + "title": "Bracelets Sets", + "url": "https://us.yehwang.com/jewelry-5609/jewelry-sets/bracelets-sets", + "parent": "jewelry-5609/jewelry-sets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/jewelry-sets/earring-sets", + "title": "Earring Sets", + "url": "https://us.yehwang.com/jewelry-5609/jewelry-sets/earring-sets", + "parent": "jewelry-5609/jewelry-sets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/jewelry-sets/necklaces-sets", + "title": "Necklaces Sets", + "url": "https://us.yehwang.com/jewelry-5609/jewelry-sets/necklaces-sets", + "parent": "jewelry-5609/jewelry-sets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/jewelry-sets/ring-sets", + "title": "Ring Sets", + "url": "https://us.yehwang.com/jewelry-5609/jewelry-sets/ring-sets", + "parent": "jewelry-5609/jewelry-sets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces", + "title": "Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces", + "parent": "jewelry-5609", + "level": 2, + "aliases": [ + "项链", + "colliers", + "halsketten", + "kettingen", + "cadenas", + "necklaces" + ] + }, + { + "category": "jewelry-5609/necklaces/beaded-necklaces", + "title": "Beaded Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/beaded-necklaces", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/braided-necklaces", + "title": "Braided Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/braided-necklaces", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/chain-necklaces", + "title": "Chain Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/chain-necklaces", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/charm-necklaces", + "title": "Charm Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/charm-necklaces", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/chokers", + "title": "Chokers", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/chokers", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/layered-necklace", + "title": "Layered Necklace", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/layered-necklace", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/long-necklaces", + "title": "Long Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/long-necklaces", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [ + "长项链", + "long colliers", + "lange halsketten", + "langen kettingen" + ] + }, + { + "category": "jewelry-5609/necklaces/necklaces-sets", + "title": "Necklaces Sets", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/necklaces-sets", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/necklaces/short-necklaces", + "title": "Short Necklaces", + "url": "https://us.yehwang.com/jewelry-5609/necklaces/short-necklaces", + "parent": "jewelry-5609/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/piercing-jewelry", + "title": "Piercing Jewelry", + "url": "https://us.yehwang.com/jewelry-5609/piercing-jewelry", + "parent": "jewelry-5609", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-5609/piercing-jewelry/nose-stud", + "title": "Nose Stud", + "url": "https://us.yehwang.com/jewelry-5609/piercing-jewelry/nose-stud", + "parent": "jewelry-5609/piercing-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/piercing-jewelry/piercings-earrings", + "title": "Piercings Earrings", + "url": "https://us.yehwang.com/jewelry-5609/piercing-jewelry/piercings-earrings", + "parent": "jewelry-5609/piercing-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings", + "title": "Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings", + "parent": "jewelry-5609", + "level": 2, + "aliases": [ + "戒指", + "bagues", + "ringe", + "ringen", + "rings" + ] + }, + { + "category": "jewelry-5609/rings/adjustable-rings", + "title": "Adjustable Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/adjustable-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/beaded-rings", + "title": "Beaded Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/beaded-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/gemstone-rings", + "title": "Gemstone Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/gemstone-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/minimalist-rings", + "title": "Minimalist Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/minimalist-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/rhinestone-rings", + "title": "Rhinestone Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/rhinestone-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/ring-sets", + "title": "Ring Sets", + "url": "https://us.yehwang.com/jewelry-5609/rings/ring-sets", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/rings-with-sizes", + "title": "Rings With Sizes", + "url": "https://us.yehwang.com/jewelry-5609/rings/rings-with-sizes", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/signet-rings", + "title": "Signet Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/signet-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-5609/rings/statement-rings", + "title": "Statement Rings", + "url": "https://us.yehwang.com/jewelry-5609/rings/statement-rings", + "parent": "jewelry-5609/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material", + "title": "Jewelry By Material", + "url": "https://us.yehwang.com/jewelry-by-material", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "jewelry-by-material/copper-jewelry", + "title": "Copper Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/copper-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-by-material/copper-jewelry/copper-anklets", + "title": "Copper Anklets", + "url": "https://us.yehwang.com/jewelry-by-material/copper-jewelry/copper-anklets", + "parent": "jewelry-by-material/copper-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/copper-jewelry/copper-bracelets", + "title": "Copper Bracelets", + "url": "https://us.yehwang.com/jewelry-by-material/copper-jewelry/copper-bracelets", + "parent": "jewelry-by-material/copper-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/copper-jewelry/copper-earrings", + "title": "Copper Earrings", + "url": "https://us.yehwang.com/jewelry-by-material/copper-jewelry/copper-earrings", + "parent": "jewelry-by-material/copper-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/copper-jewelry/copper-necklaces", + "title": "Copper Necklaces", + "url": "https://us.yehwang.com/jewelry-by-material/copper-jewelry/copper-necklaces", + "parent": "jewelry-by-material/copper-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/crystal-jewelry", + "title": "Crystal Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/crystal-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-by-material/crystal-jewelry/crystal-bracelets", + "title": "Crystal Bracelets", + "url": "https://us.yehwang.com/jewelry-by-material/crystal-jewelry/crystal-bracelets", + "parent": "jewelry-by-material/crystal-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/crystal-jewelry/crystal-earrings", + "title": "Crystal Earrings", + "url": "https://us.yehwang.com/jewelry-by-material/crystal-jewelry/crystal-earrings", + "parent": "jewelry-by-material/crystal-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/crystal-jewelry/crystal-necklaces", + "title": "Crystal Necklaces", + "url": "https://us.yehwang.com/jewelry-by-material/crystal-jewelry/crystal-necklaces", + "parent": "jewelry-by-material/crystal-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/crystal-jewelry/crystal-rings", + "title": "Crystal Rings", + "url": "https://us.yehwang.com/jewelry-by-material/crystal-jewelry/crystal-rings", + "parent": "jewelry-by-material/crystal-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry", + "title": "Gold Plated Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [ + "镀金珠宝", + "gold plated" + ] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry/gold-plated-anklets", + "title": "Gold Plated Anklets", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry/gold-plated-anklets", + "parent": "jewelry-by-material/gold-plated-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry/gold-plated-bracelets", + "title": "Gold Plated Bracelets", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry/gold-plated-bracelets", + "parent": "jewelry-by-material/gold-plated-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry/gold-plated-earrings", + "title": "Gold Plated Earrings", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry/gold-plated-earrings", + "parent": "jewelry-by-material/gold-plated-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry/gold-plated-jewelry-sets", + "title": "Gold Plated Jewelry Sets", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry/gold-plated-jewelry-sets", + "parent": "jewelry-by-material/gold-plated-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry/gold-plated-necklaces", + "title": "Gold Plated Necklaces", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry/gold-plated-necklaces", + "parent": "jewelry-by-material/gold-plated-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/gold-plated-jewelry/gold-plated-rings", + "title": "Gold Plated Rings", + "url": "https://us.yehwang.com/jewelry-by-material/gold-plated-jewelry/gold-plated-rings", + "parent": "jewelry-by-material/gold-plated-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/natural-stone-jewelry", + "title": "Natural Stone Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/natural-stone-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-by-material/natural-stone-jewelry/natural-stone-bracelets", + "title": "Natural Stone Bracelets", + "url": "https://us.yehwang.com/jewelry-by-material/natural-stone-jewelry/natural-stone-bracelets", + "parent": "jewelry-by-material/natural-stone-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/natural-stone-jewelry/natural-stone-earrings", + "title": "Natural Stone Earrings", + "url": "https://us.yehwang.com/jewelry-by-material/natural-stone-jewelry/natural-stone-earrings", + "parent": "jewelry-by-material/natural-stone-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/natural-stone-jewelry/natural-stone-jewelry-sets", + "title": "Natural Stone Jewelry Sets", + "url": "https://us.yehwang.com/jewelry-by-material/natural-stone-jewelry/natural-stone-jewelry-sets", + "parent": "jewelry-by-material/natural-stone-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/natural-stone-jewelry/natural-stone-necklaces", + "title": "Natural Stone Necklaces", + "url": "https://us.yehwang.com/jewelry-by-material/natural-stone-jewelry/natural-stone-necklaces", + "parent": "jewelry-by-material/natural-stone-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/natural-stone-jewelry/natural-stone-rings", + "title": "Natural Stone Rings", + "url": "https://us.yehwang.com/jewelry-by-material/natural-stone-jewelry/natural-stone-rings", + "parent": "jewelry-by-material/natural-stone-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/pearl-jewelry", + "title": "Pearl Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/pearl-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [ + "珍珠珠宝", + "pearl" + ] + }, + { + "category": "jewelry-by-material/pearl-jewelry/imitation-pearl-bracelets", + "title": "Imitation Pearl Bracelets", + "url": "https://us.yehwang.com/jewelry-by-material/pearl-jewelry/imitation-pearl-bracelets", + "parent": "jewelry-by-material/pearl-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/pearl-jewelry/imitation-pearl-earrings", + "title": "Imitation Pearl Earrings", + "url": "https://us.yehwang.com/jewelry-by-material/pearl-jewelry/imitation-pearl-earrings", + "parent": "jewelry-by-material/pearl-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/pearl-jewelry/imitation-pearl-jewelry-sets", + "title": "Imitation Pearl Jewelry Sets", + "url": "https://us.yehwang.com/jewelry-by-material/pearl-jewelry/imitation-pearl-jewelry-sets", + "parent": "jewelry-by-material/pearl-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/pearl-jewelry/imitation-pearl-necklaces", + "title": "Imitation Pearl Necklaces", + "url": "https://us.yehwang.com/jewelry-by-material/pearl-jewelry/imitation-pearl-necklaces", + "parent": "jewelry-by-material/pearl-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/pearl-jewelry/imitation-pearl-rings", + "title": "Imitation Pearl Rings", + "url": "https://us.yehwang.com/jewelry-by-material/pearl-jewelry/imitation-pearl-rings", + "parent": "jewelry-by-material/pearl-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/piercing-jewelry", + "title": "Piercing Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/piercing-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-by-material/piercing-jewelry/belly-rings", + "title": "Belly Rings", + "url": "https://us.yehwang.com/jewelry-by-material/piercing-jewelry/belly-rings", + "parent": "jewelry-by-material/piercing-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/piercing-jewelry/ear-cartilage-rings-and-studs", + "title": "Ear Cartilage Rings And Studs", + "url": "https://us.yehwang.com/jewelry-by-material/piercing-jewelry/ear-cartilage-rings-and-studs", + "parent": "jewelry-by-material/piercing-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/piercing-jewelry/nose-rings-and-studs", + "title": "Nose Rings And Studs", + "url": "https://us.yehwang.com/jewelry-by-material/piercing-jewelry/nose-rings-and-studs", + "parent": "jewelry-by-material/piercing-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry", + "title": "Stainless Steel Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [ + "不锈钢珠宝", + "stainless steel", + "roestvrij staal" + ] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry/stainless-steel-anklets", + "title": "Stainless Steel Anklets", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry/stainless-steel-anklets", + "parent": "jewelry-by-material/stainless-steel-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry/stainless-steel-bracelets", + "title": "Stainless Steel Bracelets", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry/stainless-steel-bracelets", + "parent": "jewelry-by-material/stainless-steel-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry/stainless-steel-earrings", + "title": "Stainless Steel Earrings", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry/stainless-steel-earrings", + "parent": "jewelry-by-material/stainless-steel-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry/stainless-steel-jewelry-sets", + "title": "Stainless Steel Jewelry Sets", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry/stainless-steel-jewelry-sets", + "parent": "jewelry-by-material/stainless-steel-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry/stainless-steel-necklaces", + "title": "Stainless Steel Necklaces", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry/stainless-steel-necklaces", + "parent": "jewelry-by-material/stainless-steel-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-jewelry/stainless-steel-rings", + "title": "Stainless Steel Rings", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-jewelry/stainless-steel-rings", + "parent": "jewelry-by-material/stainless-steel-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-by-material/stainless-steel-zircon-jewelry", + "title": "Stainless Steel Zircon Jewelry", + "url": "https://us.yehwang.com/jewelry-by-material/stainless-steel-zircon-jewelry", + "parent": "jewelry-by-material", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-making", + "title": "Jewelry Making", + "url": "https://us.yehwang.com/jewelry-making", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "jewelry-making-6109", + "title": "Jewelry Making 6109", + "url": "https://us.yehwang.com/jewelry-making-6109", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "jewelry-making/beads", + "title": "Beads", + "url": "https://us.yehwang.com/jewelry-making/beads", + "parent": "jewelry-making", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-making/beads/acrylic-beads", + "title": "Acrylic Beads", + "url": "https://us.yehwang.com/jewelry-making/beads/acrylic-beads", + "parent": "jewelry-making/beads", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/beads/ceramic-beads", + "title": "Ceramic Beads", + "url": "https://us.yehwang.com/jewelry-making/beads/ceramic-beads", + "parent": "jewelry-making/beads", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/beads/crystal-beads", + "title": "Crystal Beads", + "url": "https://us.yehwang.com/jewelry-making/beads/crystal-beads", + "parent": "jewelry-making/beads", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/beads/gemstone-beads", + "title": "Gemstone Beads", + "url": "https://us.yehwang.com/jewelry-making/beads/gemstone-beads", + "parent": "jewelry-making/beads", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/beads/glass-beads", + "title": "Glass Beads", + "url": "https://us.yehwang.com/jewelry-making/beads/glass-beads", + "parent": "jewelry-making/beads", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/jewelry-findings", + "title": "Jewelry Findings", + "url": "https://us.yehwang.com/jewelry-making/jewelry-findings", + "parent": "jewelry-making", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-making/jewelry-findings/links-and-connectors", + "title": "Links And Connectors", + "url": "https://us.yehwang.com/jewelry-making/jewelry-findings/links-and-connectors", + "parent": "jewelry-making/jewelry-findings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/making-supplies", + "title": "Making Supplies", + "url": "https://us.yehwang.com/jewelry-making/making-supplies", + "parent": "jewelry-making", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-making/making-supplies/spacer-beads", + "title": "Spacer Beads", + "url": "https://us.yehwang.com/jewelry-making/making-supplies/spacer-beads", + "parent": "jewelry-making/making-supplies", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/making-supplies/stringing-materials", + "title": "Stringing Materials", + "url": "https://us.yehwang.com/jewelry-making/making-supplies/stringing-materials", + "parent": "jewelry-making/making-supplies", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/making-supplies/tools", + "title": "Tools", + "url": "https://us.yehwang.com/jewelry-making/making-supplies/tools", + "parent": "jewelry-making/making-supplies", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/pendants-and-charms", + "title": "Pendants And Charms", + "url": "https://us.yehwang.com/jewelry-making/pendants-and-charms", + "parent": "jewelry-making", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry-making/pendants-and-charms/ceramic-pendants", + "title": "Ceramic Pendants", + "url": "https://us.yehwang.com/jewelry-making/pendants-and-charms/ceramic-pendants", + "parent": "jewelry-making/pendants-and-charms", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/pendants-and-charms/fruit-charms-and-beaded", + "title": "Fruit Charms And Beaded", + "url": "https://us.yehwang.com/jewelry-making/pendants-and-charms/fruit-charms-and-beaded", + "parent": "jewelry-making/pendants-and-charms", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/pendants-and-charms/glass-pendants", + "title": "Glass Pendants", + "url": "https://us.yehwang.com/jewelry-making/pendants-and-charms/glass-pendants", + "parent": "jewelry-making/pendants-and-charms", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/pendants-and-charms/heart-charms-and-beaded", + "title": "Heart Charms And Beaded", + "url": "https://us.yehwang.com/jewelry-making/pendants-and-charms/heart-charms-and-beaded", + "parent": "jewelry-making/pendants-and-charms", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry-making/pendants-and-charms/stainless-steel-pendants", + "title": "Stainless Steel Pendants", + "url": "https://us.yehwang.com/jewelry-making/pendants-and-charms/stainless-steel-pendants", + "parent": "jewelry-making/pendants-and-charms", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date", + "title": "New By Date", + "url": "https://us.yehwang.com/new-by-date", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/bag-charms", + "title": "Bag Charms", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/bag-charms", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/bags", + "title": "Bags", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/bags", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [ + "包", + "sacs", + "taschen", + "tassen", + "bags" + ] + }, + { + "category": "new-by-date/new-in-accessories/bandana-and-scarves", + "title": "Bandana And Scarves", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/bandana-and-scarves", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/brooches", + "title": "Brooches", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/brooches", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/glasses", + "title": "Glasses", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/glasses", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/hair-accessories", + "title": "Hair Accessories", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/hair-accessories", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [ + "发饰", + "hair accessories", + "haaraccessoires" + ] + }, + { + "category": "new-by-date/new-in-accessories/hats", + "title": "Hats", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/hats", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [ + "帽子", + "hats", + "hoeden" + ] + }, + { + "category": "new-by-date/new-in-accessories/keychains", + "title": "Keychains", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/keychains", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/phone-accessories", + "title": "Phone Accessories", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/phone-accessories", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/resin-pendants", + "title": "Resin Pendants", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/resin-pendants", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-accessories/socks", + "title": "Socks", + "url": "https://us.yehwang.com/new-by-date/new-in-accessories/socks", + "parent": "new-by-date/new-in-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-jewelry", + "title": "New In Jewelry", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry", + "parent": "new-by-date", + "level": 2, + "aliases": [] + }, + { + "category": "new-by-date/new-in-jewelry/anklets", + "title": "Anklets", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/anklets", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [ + "脚链", + "anklets", + "enkelbanden" + ] + }, + { + "category": "new-by-date/new-in-jewelry/bracelets-and-bangles", + "title": "Bracelets And Bangles", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/bracelets-and-bangles", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-jewelry/earrings", + "title": "Earrings", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/earrings", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [ + "耳环", + "boucles", + "ohrringe", + "oorbellen", + "earrings" + ] + }, + { + "category": "new-by-date/new-in-jewelry/necklaces", + "title": "Necklaces", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/necklaces", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [ + "项链", + "colliers", + "halsketten", + "kettingen", + "cadenas", + "necklaces" + ] + }, + { + "category": "new-by-date/new-in-jewelry/pendants", + "title": "Pendants", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/pendants", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-jewelry/piercing-jewelry", + "title": "Piercing Jewelry", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/piercing-jewelry", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/new-in-jewelry/rings", + "title": "Rings", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/rings", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [ + "戒指", + "bagues", + "ringe", + "ringen", + "rings" + ] + }, + { + "category": "new-by-date/new-in-jewelry/stainless-steel-jewelry", + "title": "Stainless Steel Jewelry", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/stainless-steel-jewelry", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [ + "不锈钢珠宝", + "stainless steel", + "roestvrij staal" + ] + }, + { + "category": "new-by-date/new-in-jewelry/zircon-jewelry", + "title": "Zircon Jewelry", + "url": "https://us.yehwang.com/new-by-date/new-in-jewelry/zircon-jewelry", + "parent": "new-by-date/new-in-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "new-by-date/what-and-039-s-new", + "title": "What And 039 S New", + "url": "https://us.yehwang.com/new-by-date/what-and-039-s-new", + "parent": "new-by-date", + "level": 2, + "aliases": [] + }, + { + "category": "packing-and-display-5607", + "title": "Packing And Display 5607", + "url": "https://us.yehwang.com/packing-and-display-5607", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "packing-and-display-5607/jewelry-boxes", + "title": "Jewelry Boxes", + "url": "https://us.yehwang.com/packing-and-display-5607/jewelry-boxes", + "parent": "packing-and-display-5607", + "level": 2, + "aliases": [] + }, + { + "category": "packing-and-display-5607/jewelry-packaging", + "title": "Jewelry Packaging", + "url": "https://us.yehwang.com/packing-and-display-5607/jewelry-packaging", + "parent": "packing-and-display-5607", + "level": 2, + "aliases": [] + }, + { + "category": "packing-and-display-5607/packaging-cards", + "title": "Packaging Cards", + "url": "https://us.yehwang.com/packing-and-display-5607/packaging-cards", + "parent": "packing-and-display-5607", + "level": 2, + "aliases": [] + }, + { + "category": "stationery-and-office-supplies-6317", + "title": "Stationery And Office Supplies 6317", + "url": "https://us.yehwang.com/stationery-and-office-supplies-6317", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "trends/zircon-jewelry", + "title": "Zircon Jewelry", + "url": "https://us.yehwang.com/trends/zircon-jewelry", + "parent": "trends", + "level": 2, + "aliases": [] + }, + { + "category": "trends/zircon-jewelry/zircon-bracelets", + "title": "Zircon Bracelets", + "url": "https://us.yehwang.com/trends/zircon-jewelry/zircon-bracelets", + "parent": "trends/zircon-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "trends/zircon-jewelry/zircon-earrings", + "title": "Zircon Earrings", + "url": "https://us.yehwang.com/trends/zircon-jewelry/zircon-earrings", + "parent": "trends/zircon-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "trends/zircon-jewelry/zircon-jewelry-sets", + "title": "Zircon Jewelry Sets", + "url": "https://us.yehwang.com/trends/zircon-jewelry/zircon-jewelry-sets", + "parent": "trends/zircon-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "trends/zircon-jewelry/zircon-necklaces", + "title": "Zircon Necklaces", + "url": "https://us.yehwang.com/trends/zircon-jewelry/zircon-necklaces", + "parent": "trends/zircon-jewelry", + "level": 3, + "aliases": [] + }, + { + "category": "trends/zircon-jewelry/zircon-rings", + "title": "Zircon Rings", + "url": "https://us.yehwang.com/trends/zircon-jewelry/zircon-rings", + "parent": "trends/zircon-jewelry", + "level": 3, + "aliases": [] + } + ] +} \ No newline at end of file diff --git a/bridge-tools/config/site-map.json b/bridge-tools/config/site-map.json new file mode 100644 index 0000000..d2a3386 --- /dev/null +++ b/bridge-tools/config/site-map.json @@ -0,0 +1,1589 @@ +{ + "site": "www", + "generated_at": "2026-08-14T10:42:23.214Z", + "total": 30023, + "navCount": 511, + "entries": [ + { + "category": "accessories", + "title": "Accessories", + "url": "https://www.yehwang.com/accessories", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "accessories/bags", + "title": "Bags", + "url": "https://www.yehwang.com/accessories/bags", + "parent": "accessories", + "level": 2, + "aliases": [ + "包", + "sacs", + "taschen", + "tassen", + "bags" + ] + }, + { + "category": "accessories/bags/bag-charms", + "title": "Bag Charms", + "url": "https://www.yehwang.com/accessories/bags/bag-charms", + "parent": "accessories/bags", + "level": 3, + "aliases": [] + }, + { + "category": "accessories/bags/clutches", + "title": "Clutches", + "url": "https://www.yehwang.com/accessories/bags/clutches", + "parent": "accessories/bags", + "level": 3, + "aliases": [ + "手拿包", + "clutches" + ] + }, + { + "category": "accessories/bags/make-up-bags", + "title": "Make Up Bags", + "url": "https://www.yehwang.com/accessories/bags/make-up-bags", + "parent": "accessories/bags", + "level": 3, + "aliases": [ + "化妆包", + "makeup bags", + "make-up" + ] + }, + { + "category": "accessories/bags/shopper-bags", + "title": "Shopper Bags", + "url": "https://www.yehwang.com/accessories/bags/shopper-bags", + "parent": "accessories/bags", + "level": 3, + "aliases": [ + "购物袋", + "shopper" + ] + }, + { + "category": "accessories/bags/shoulder-bags", + "title": "Shoulder Bags", + "url": "https://www.yehwang.com/accessories/bags/shoulder-bags", + "parent": "accessories/bags", + "level": 3, + "aliases": [ + "单肩包", + "shoulder bags" + ] + }, + { + "category": "accessories/bags/wholesale-beach-bags", + "title": "Wholesale Beach Bags", + "url": "https://www.yehwang.com/accessories/bags/wholesale-beach-bags", + "parent": "accessories/bags", + "level": 3, + "aliases": [ + "沙滩包批发", + "beach bags" + ] + }, + { + "category": "accessories/fan", + "title": "Fan", + "url": "https://www.yehwang.com/accessories/fan", + "parent": "accessories", + "level": 2, + "aliases": [] + }, + { + "category": "accessories/gloves", + "title": "Gloves", + "url": "https://www.yehwang.com/accessories/gloves", + "parent": "accessories", + "level": 2, + "aliases": [ + "手套", + "gloves" + ] + }, + { + "category": "accessories/hair-accessories", + "title": "Hair Accessories", + "url": "https://www.yehwang.com/accessories/hair-accessories", + "parent": "accessories", + "level": 2, + "aliases": [ + "发饰", + "hair accessories", + "haaraccessoires" + ] + }, + { + "category": "accessories/hair-accessories/extensions", + "title": "Extensions", + "url": "https://www.yehwang.com/accessories/hair-accessories/extensions", + "parent": "accessories/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories/hair-accessories/hair-bands", + "title": "Hair Bands", + "url": "https://www.yehwang.com/accessories/hair-accessories/hair-bands", + "parent": "accessories/hair-accessories", + "level": 3, + "aliases": [ + "发带", + "hair bands", + "haarbanden" + ] + }, + { + "category": "accessories/hair-accessories/hair-clips", + "title": "Hair Clips", + "url": "https://www.yehwang.com/accessories/hair-accessories/hair-clips", + "parent": "accessories/hair-accessories", + "level": 3, + "aliases": [ + "发夹", + "hair clips", + "haarclips" + ] + }, + { + "category": "accessories/hair-accessories/hair-ties", + "title": "Hair Ties", + "url": "https://www.yehwang.com/accessories/hair-accessories/hair-ties", + "parent": "accessories/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories/hair-accessories/scrunchies", + "title": "Scrunchies", + "url": "https://www.yehwang.com/accessories/hair-accessories/scrunchies", + "parent": "accessories/hair-accessories", + "level": 3, + "aliases": [] + }, + { + "category": "accessories/hats-and-caps", + "title": "Hats And Caps", + "url": "https://www.yehwang.com/accessories/hats-and-caps", + "parent": "accessories", + "level": 2, + "aliases": [ + "帽子和棒球帽", + "hats caps" + ] + }, + { + "category": "accessories/keychain", + "title": "Keychain", + "url": "https://www.yehwang.com/accessories/keychain", + "parent": "accessories", + "level": 2, + "aliases": [] + }, + { + "category": "accessories/phone-accessories-226/phone-cases", + "title": "Phone Cases", + "url": "https://www.yehwang.com/accessories/phone-accessories-226/phone-cases", + "parent": "accessories/phone-accessories-226", + "level": 3, + "aliases": [ + "手机壳", + "phone cases", + "telefoonhoesjes" + ] + }, + { + "category": "accessories/phone-accessories-226/phone-cords", + "title": "Phone Cords", + "url": "https://www.yehwang.com/accessories/phone-accessories-226/phone-cords", + "parent": "accessories/phone-accessories-226", + "level": 3, + "aliases": [ + "手机挂绳", + "phone cords" + ] + }, + { + "category": "accessories/scarves", + "title": "Scarves", + "url": "https://www.yehwang.com/accessories/scarves", + "parent": "accessories", + "level": 2, + "aliases": [ + "围巾", + "scarves", + "sjaals" + ] + }, + { + "category": "accessories/scarves.htmls", + "title": "Scarves.htmls", + "url": "https://www.yehwang.com/accessories/scarves.htmls", + "parent": "accessories", + "level": 2, + "aliases": [] + }, + { + "category": "accessories/scarves/scarf-clips", + "title": "Scarf Clips", + "url": "https://www.yehwang.com/accessories/scarves/scarf-clips", + "parent": "accessories/scarves", + "level": 3, + "aliases": [] + }, + { + "category": "accessories/scarves/summer-scarves", + "title": "Summer Scarves", + "url": "https://www.yehwang.com/accessories/scarves/summer-scarves", + "parent": "accessories/scarves", + "level": 3, + "aliases": [ + "夏季围巾", + "summer scarves" + ] + }, + { + "category": "accessories/scarves/winter-scarves", + "title": "Winter Scarves", + "url": "https://www.yehwang.com/accessories/scarves/winter-scarves", + "parent": "accessories/scarves", + "level": 3, + "aliases": [ + "冬季围巾", + "winter scarves" + ] + }, + { + "category": "accessories/shoe-clips", + "title": "Shoe Clips", + "url": "https://www.yehwang.com/accessories/shoe-clips", + "parent": "accessories", + "level": 2, + "aliases": [] + }, + { + "category": "accessories/socks", + "title": "Socks", + "url": "https://www.yehwang.com/accessories/socks", + "parent": "accessories", + "level": 2, + "aliases": [] + }, + { + "category": "accessories/sunglasses", + "title": "Sunglasses", + "url": "https://www.yehwang.com/accessories/sunglasses", + "parent": "accessories", + "level": 2, + "aliases": [ + "太阳镜", + "sunglasses", + "zonnebrillen" + ] + }, + { + "category": "bestsellers", + "title": "Bestsellers", + "url": "https://www.yehwang.com/bestsellers", + "parent": null, + "level": 1, + "aliases": [ + "畅销", + "畅销榜", + "最畅销", + "bestseller", + "best seller", + "trending", + "popular", + "热门", + "流行", + "bestsellers" + ] + }, + { + "category": "bestsellers/bestsellers-accessories", + "title": "Bestsellers Accessories", + "url": "https://www.yehwang.com/bestsellers/bestsellers-accessories", + "parent": "bestsellers", + "level": 2, + "aliases": [] + }, + { + "category": "bestsellers/bestsellers-clothes", + "title": "Bestsellers Clothes", + "url": "https://www.yehwang.com/bestsellers/bestsellers-clothes", + "parent": "bestsellers", + "level": 2, + "aliases": [] + }, + { + "category": "bestsellers/bestsellers-jewelry", + "title": "Bestsellers Jewelry", + "url": "https://www.yehwang.com/bestsellers/bestsellers-jewelry", + "parent": "bestsellers", + "level": 2, + "aliases": [] + }, + { + "category": "brands", + "title": "Brands", + "url": "https://www.yehwang.com/brands", + "parent": null, + "level": 1, + "aliases": [ + "品牌", + "brands", + "merken" + ] + }, + { + "category": "brands/club-spice", + "title": "Club Spice", + "url": "https://www.yehwang.com/brands/club-spice", + "parent": "brands", + "level": 2, + "aliases": [] + }, + { + "category": "brands/lilli-june", + "title": "Lilli June", + "url": "https://www.yehwang.com/brands/lilli-june", + "parent": "brands", + "level": 2, + "aliases": [] + }, + { + "category": "brands/lume", + "title": "Lume", + "url": "https://www.yehwang.com/brands/lume", + "parent": "brands", + "level": 2, + "aliases": [] + }, + { + "category": "brands/yehwang", + "title": "Yehwang", + "url": "https://www.yehwang.com/brands/yehwang", + "parent": "brands", + "level": 2, + "aliases": [] + }, + { + "category": "clothes", + "title": "Clothes", + "url": "https://www.yehwang.com/clothes", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "clothes/blazers", + "title": "Blazers", + "url": "https://www.yehwang.com/clothes/blazers", + "parent": "clothes", + "level": 2, + "aliases": [ + "西装外套", + "blazers" + ] + }, + { + "category": "clothes/blouses", + "title": "Blouses", + "url": "https://www.yehwang.com/clothes/blouses", + "parent": "clothes", + "level": 2, + "aliases": [ + "衬衫", + "blouses" + ] + }, + { + "category": "clothes/clothing-collections", + "title": "Clothing Collections", + "url": "https://www.yehwang.com/clothes/clothing-collections", + "parent": "clothes", + "level": 2, + "aliases": [ + "服装系列", + "clothing collections" + ] + }, + { + "category": "clothes/clothing-collections/fall-and-winter-clothes", + "title": "Fall And Winter Clothes", + "url": "https://www.yehwang.com/clothes/clothing-collections/fall-and-winter-clothes", + "parent": "clothes/clothing-collections", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/clothing-collections/summer-clothes", + "title": "Summer Clothes", + "url": "https://www.yehwang.com/clothes/clothing-collections/summer-clothes", + "parent": "clothes/clothing-collections", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/denim", + "title": "Denim", + "url": "https://www.yehwang.com/clothes/denim", + "parent": "clothes", + "level": 2, + "aliases": [] + }, + { + "category": "clothes/dresses", + "title": "Dresses", + "url": "https://www.yehwang.com/clothes/dresses", + "parent": "clothes", + "level": 2, + "aliases": [ + "连衣裙", + "robes", + "kleider", + "jurken", + "dresses" + ] + }, + { + "category": "clothes/dresses/midi-dresses", + "title": "Midi Dresses", + "url": "https://www.yehwang.com/clothes/dresses/midi-dresses", + "parent": "clothes/dresses", + "level": 3, + "aliases": [ + "中长裙", + "midi dresses" + ] + }, + { + "category": "clothes/dresses/mini-dresses", + "title": "Mini Dresses", + "url": "https://www.yehwang.com/clothes/dresses/mini-dresses", + "parent": "clothes/dresses", + "level": 3, + "aliases": [ + "短裙", + "mini dresses", + "mini jurken" + ] + }, + { + "category": "clothes/jackets", + "title": "Jackets", + "url": "https://www.yehwang.com/clothes/jackets", + "parent": "clothes", + "level": 2, + "aliases": [ + "夹克", + "jackets", + "jassen" + ] + }, + { + "category": "clothes/jeans", + "title": "Jeans", + "url": "https://www.yehwang.com/clothes/jeans", + "parent": "clothes", + "level": 2, + "aliases": [ + "牛仔裤", + "jeans" + ] + }, + { + "category": "clothes/jeans-3580/ripped-jeans", + "title": "Ripped Jeans", + "url": "https://www.yehwang.com/clothes/jeans-3580/ripped-jeans", + "parent": "clothes/jeans-3580", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/jeans/straight-jeans", + "title": "Straight Jeans", + "url": "https://www.yehwang.com/clothes/jeans/straight-jeans", + "parent": "clothes/jeans", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/jeans/wide-leg-jeans", + "title": "Wide Leg Jeans", + "url": "https://www.yehwang.com/clothes/jeans/wide-leg-jeans", + "parent": "clothes/jeans", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/jumpsuits", + "title": "Jumpsuits", + "url": "https://www.yehwang.com/clothes/jumpsuits", + "parent": "clothes", + "level": 2, + "aliases": [ + "连体裤", + "jumpsuits" + ] + }, + { + "category": "clothes/knitwear-and-sweaters", + "title": "Knitwear And Sweaters", + "url": "https://www.yehwang.com/clothes/knitwear-and-sweaters", + "parent": "clothes", + "level": 2, + "aliases": [ + "毛衣", + "sweaters", + "knitwear", + "truien" + ] + }, + { + "category": "clothes/knitwear-and-sweaters/sweaters", + "title": "Sweaters", + "url": "https://www.yehwang.com/clothes/knitwear-and-sweaters/sweaters", + "parent": "clothes/knitwear-and-sweaters", + "level": 3, + "aliases": [ + "毛衣", + "sweaters", + "truien" + ] + }, + { + "category": "clothes/shorts", + "title": "Shorts", + "url": "https://www.yehwang.com/clothes/shorts", + "parent": "clothes", + "level": 2, + "aliases": [ + "短裤", + "shorts" + ] + }, + { + "category": "clothes/skirts", + "title": "Skirts", + "url": "https://www.yehwang.com/clothes/skirts", + "parent": "clothes", + "level": 2, + "aliases": [ + "半身裙", + "jupes", + "röcke", + "rokken", + "skirts" + ] + }, + { + "category": "clothes/skirts/midi-skirts", + "title": "Midi Skirts", + "url": "https://www.yehwang.com/clothes/skirts/midi-skirts", + "parent": "clothes/skirts", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/skirts/mini-skirts", + "title": "Mini Skirts", + "url": "https://www.yehwang.com/clothes/skirts/mini-skirts", + "parent": "clothes/skirts", + "level": 3, + "aliases": [ + "迷你裙", + "mini skirts" + ] + }, + { + "category": "clothes/tops", + "title": "Tops", + "url": "https://www.yehwang.com/clothes/tops", + "parent": "clothes", + "level": 2, + "aliases": [ + "上衣", + "hauts", + "tops", + "blouses" + ] + }, + { + "category": "clothes/tops/long-sleeve-tops", + "title": "Long Sleeve Tops", + "url": "https://www.yehwang.com/clothes/tops/long-sleeve-tops", + "parent": "clothes/tops", + "level": 3, + "aliases": [ + "长袖上衣", + "long sleeve tops" + ] + }, + { + "category": "clothes/tops/short-sleeve-tops", + "title": "Short Sleeve Tops", + "url": "https://www.yehwang.com/clothes/tops/short-sleeve-tops", + "parent": "clothes/tops", + "level": 3, + "aliases": [ + "短袖上衣", + "short sleeve tops" + ] + }, + { + "category": "clothes/tops/sleeveless-tops", + "title": "Sleeveless Tops", + "url": "https://www.yehwang.com/clothes/tops/sleeveless-tops", + "parent": "clothes/tops", + "level": 3, + "aliases": [ + "无袖上衣", + "sleeveless" + ] + }, + { + "category": "clothes/tops/t-shirts", + "title": "T Shirts", + "url": "https://www.yehwang.com/clothes/tops/t-shirts", + "parent": "clothes/tops", + "level": 3, + "aliases": [ + "T恤", + "t shirts", + "t-shirts" + ] + }, + { + "category": "clothes/tops/vest", + "title": "Vest", + "url": "https://www.yehwang.com/clothes/tops/vest", + "parent": "clothes/tops", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/trousers", + "title": "Trousers", + "url": "https://www.yehwang.com/clothes/trousers", + "parent": "clothes", + "level": 2, + "aliases": [ + "裤子", + "pantalons", + "hosen", + "broeken", + "trousers" + ] + }, + { + "category": "clothes/trousers/flaired-pants", + "title": "Flaired Pants", + "url": "https://www.yehwang.com/clothes/trousers/flaired-pants", + "parent": "clothes/trousers", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/trousers/skinny-pants", + "title": "Skinny Pants", + "url": "https://www.yehwang.com/clothes/trousers/skinny-pants", + "parent": "clothes/trousers", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/trousers/straight-pants", + "title": "Straight Pants", + "url": "https://www.yehwang.com/clothes/trousers/straight-pants", + "parent": "clothes/trousers", + "level": 3, + "aliases": [] + }, + { + "category": "clothes/trousers/wide-leg-pants", + "title": "Wide Leg Pants", + "url": "https://www.yehwang.com/clothes/trousers/wide-leg-pants", + "parent": "clothes/trousers", + "level": 3, + "aliases": [] + }, + { + "category": "dropshipping", + "title": "Dropshipping", + "url": "https://www.yehwang.com/dropshipping", + "parent": null, + "level": 1, + "aliases": [ + "一件代发", + "dropshipping", + "dropship" + ] + }, + { + "category": "factory-direct-wholesale-en", + "title": "Factory Direct Wholesale En", + "url": "https://www.yehwang.com/factory-direct-wholesale-en", + "parent": null, + "level": 1, + "aliases": [ + "工厂直发", + "factory direct", + "china warehouse", + "中国仓", + "direct" + ] + }, + { + "category": "jewelry", + "title": "Jewelry", + "url": "https://www.yehwang.com/jewelry", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "jewelry/Anklets", + "title": "Anklets", + "url": "https://www.yehwang.com/jewelry/Anklets", + "parent": "jewelry", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry/advent-calendars", + "title": "Advent Calendars", + "url": "https://www.yehwang.com/jewelry/advent-calendars", + "parent": "jewelry", + "level": 2, + "aliases": [ + "圣诞日历", + "advent calendars", + "adventkalender" + ] + }, + { + "category": "jewelry/bracelets", + "title": "Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets", + "parent": "jewelry", + "level": 2, + "aliases": [ + "手链", + "bracelets", + "armbänder", + "armbanden", + "bracelets" + ] + }, + { + "category": "jewelry/bracelets/bangles", + "title": "Bangles", + "url": "https://www.yehwang.com/jewelry/bracelets/bangles", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/bracelets/bangles-and-cuffs-bracelet", + "title": "Bangles And Cuffs Bracelet", + "url": "https://www.yehwang.com/jewelry/bracelets/bangles-and-cuffs-bracelet", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/bracelets/beaded-bracelets", + "title": "Beaded Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets/beaded-bracelets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [ + "串珠手链", + "beaded", + "kralen armbanden" + ] + }, + { + "category": "jewelry/bracelets/bracelets-sets", + "title": "Bracelets Sets", + "url": "https://www.yehwang.com/jewelry/bracelets/bracelets-sets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/bracelets/braided-bracelets", + "title": "Braided Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets/braided-bracelets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/bracelets/chain-bracelets", + "title": "Chain Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets/chain-bracelets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/bracelets/charm-bracelets", + "title": "Charm Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets/charm-bracelets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [ + "吊坠手链", + "charm bracelets", + "bedels armbanden" + ] + }, + { + "category": "jewelry/bracelets/chunky-bracelets", + "title": "Chunky Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets/chunky-bracelets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [ + "粗链手链" + ] + }, + { + "category": "jewelry/bracelets/hand-chain-bracelets", + "title": "Hand Chain Bracelets", + "url": "https://www.yehwang.com/jewelry/bracelets/hand-chain-bracelets", + "parent": "jewelry/bracelets", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/brooches", + "title": "Brooches", + "url": "https://www.yehwang.com/jewelry/brooches", + "parent": "jewelry", + "level": 2, + "aliases": [] + }, + { + "category": "jewelry/cufflinks", + "title": "Cufflinks", + "url": "https://www.yehwang.com/jewelry/cufflinks", + "parent": "jewelry", + "level": 2, + "aliases": [ + "袖扣", + "cufflinks" + ] + }, + { + "category": "jewelry/display-set-en", + "title": "Display Set En", + "url": "https://www.yehwang.com/jewelry/display-set-en", + "parent": "jewelry", + "level": 2, + "aliases": [ + "陈列套装", + "display set", + "displays" + ] + }, + { + "category": "jewelry/display-set-en/anklets-display-set", + "title": "Anklets Display Set", + "url": "https://www.yehwang.com/jewelry/display-set-en/anklets-display-set", + "parent": "jewelry/display-set-en", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/display-set-en/bracelets-display-set", + "title": "Bracelets Display Set", + "url": "https://www.yehwang.com/jewelry/display-set-en/bracelets-display-set", + "parent": "jewelry/display-set-en", + "level": 3, + "aliases": [ + "手链展示架" + ] + }, + { + "category": "jewelry/display-set-en/earrings-display-set", + "title": "Earrings Display Set", + "url": "https://www.yehwang.com/jewelry/display-set-en/earrings-display-set", + "parent": "jewelry/display-set-en", + "level": 3, + "aliases": [ + "耳环展示架" + ] + }, + { + "category": "jewelry/display-set-en/necklace-display-set", + "title": "Necklace Display Set", + "url": "https://www.yehwang.com/jewelry/display-set-en/necklace-display-set", + "parent": "jewelry/display-set-en", + "level": 3, + "aliases": [ + "项链展示架" + ] + }, + { + "category": "jewelry/display-set-en/rings-display-set", + "title": "Rings Display Set", + "url": "https://www.yehwang.com/jewelry/display-set-en/rings-display-set", + "parent": "jewelry/display-set-en", + "level": 3, + "aliases": [ + "戒指展示架", + "ring display" + ] + }, + { + "category": "jewelry/earrings", + "title": "Earrings", + "url": "https://www.yehwang.com/jewelry/earrings", + "parent": "jewelry", + "level": 2, + "aliases": [ + "耳环", + "boucles", + "ohrringe", + "oorbellen", + "earrings" + ] + }, + { + "category": "jewelry/earrings/beads-earrings", + "title": "Beads Earrings", + "url": "https://www.yehwang.com/jewelry/earrings/beads-earrings", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/earrings/birthstone-earrings", + "title": "Birthstone Earrings", + "url": "https://www.yehwang.com/jewelry/earrings/birthstone-earrings", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/earrings/drop-earrings", + "title": "Drop Earrings", + "url": "https://www.yehwang.com/jewelry/earrings/drop-earrings", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/earrings/ear-cuffs", + "title": "Ear Cuffs", + "url": "https://www.yehwang.com/jewelry/earrings/ear-cuffs", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [ + "耳骨夹", + "ear cuffs", + "oorcuffs" + ] + }, + { + "category": "jewelry/earrings/ear-studs", + "title": "Ear Studs", + "url": "https://www.yehwang.com/jewelry/earrings/ear-studs", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [ + "耳钉", + "studs", + "oorknopjes" + ] + }, + { + "category": "jewelry/earrings/earring-set", + "title": "Earring Set", + "url": "https://www.yehwang.com/jewelry/earrings/earring-set", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/earrings/earrings-with-stone", + "title": "Earrings With Stone", + "url": "https://www.yehwang.com/jewelry/earrings/earrings-with-stone", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/earrings/hoop-earrings", + "title": "Hoop Earrings", + "url": "https://www.yehwang.com/jewelry/earrings/hoop-earrings", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [ + "圈形耳环", + "hoops", + "creolen", + "hoop oorbellen" + ] + }, + { + "category": "jewelry/earrings/party-earrings", + "title": "Party Earrings", + "url": "https://www.yehwang.com/jewelry/earrings/party-earrings", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/earrings/studs", + "title": "Studs", + "url": "https://www.yehwang.com/jewelry/earrings/studs", + "parent": "jewelry/earrings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/jewelry-collections", + "title": "Jewelry Collections", + "url": "https://www.yehwang.com/jewelry/jewelry-collections", + "parent": "jewelry", + "level": 2, + "aliases": [ + "珠宝系列", + "collections" + ] + }, + { + "category": "jewelry/jewelry-collections/acrylic-and-resin-collection", + "title": "Acrylic And Resin Collection", + "url": "https://www.yehwang.com/jewelry/jewelry-collections/acrylic-and-resin-collection", + "parent": "jewelry/jewelry-collections", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/jewelry-collections/beach-collection", + "title": "Beach Collection", + "url": "https://www.yehwang.com/jewelry/jewelry-collections/beach-collection", + "parent": "jewelry/jewelry-collections", + "level": 3, + "aliases": [ + "沙滩系列", + "beach" + ] + }, + { + "category": "jewelry/jewelry-collections/flowers-collection", + "title": "Flowers Collection", + "url": "https://www.yehwang.com/jewelry/jewelry-collections/flowers-collection", + "parent": "jewelry/jewelry-collections", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/jewelry-collections/fruits-and-plants-collection", + "title": "Fruits And Plants Collection", + "url": "https://www.yehwang.com/jewelry/jewelry-collections/fruits-and-plants-collection", + "parent": "jewelry/jewelry-collections", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/jewelry-collections/hearts-collection", + "title": "Hearts Collection", + "url": "https://www.yehwang.com/jewelry/jewelry-collections/hearts-collection", + "parent": "jewelry/jewelry-collections", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/jewelry-parts", + "title": "Jewelry Parts", + "url": "https://www.yehwang.com/jewelry/jewelry-parts", + "parent": "jewelry", + "level": 2, + "aliases": [ + "珠宝配件", + "jewelry parts", + "onderdelen" + ] + }, + { + "category": "jewelry/jewelry-parts/jewelry-beads", + "title": "Jewelry Beads", + "url": "https://www.yehwang.com/jewelry/jewelry-parts/jewelry-beads", + "parent": "jewelry/jewelry-parts", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/jewelry-parts/jewelry-charms", + "title": "Jewelry Charms", + "url": "https://www.yehwang.com/jewelry/jewelry-parts/jewelry-charms", + "parent": "jewelry/jewelry-parts", + "level": 3, + "aliases": [ + "吊坠", + "charms", + "hangers" + ] + }, + { + "category": "jewelry/jewelry-parts/jewelry-working-supplies", + "title": "Jewelry Working Supplies", + "url": "https://www.yehwang.com/jewelry/jewelry-parts/jewelry-working-supplies", + "parent": "jewelry/jewelry-parts", + "level": 3, + "aliases": [ + "DIY 工具", + "jewelry supplies", + "beads", + "珠饰" + ] + }, + { + "category": "jewelry/men-s-bracelets", + "title": "Men S Bracelets", + "url": "https://www.yehwang.com/jewelry/men-s-bracelets", + "parent": "jewelry", + "level": 2, + "aliases": [ + "男士手链", + "men bracelets" + ] + }, + { + "category": "jewelry/men-s-jewelry", + "title": "Men S Jewelry", + "url": "https://www.yehwang.com/jewelry/men-s-jewelry", + "parent": "jewelry", + "level": 2, + "aliases": [ + "男士珠宝", + "men jewelry", + "heren sieraden" + ] + }, + { + "category": "jewelry/men-s-necklaces", + "title": "Men S Necklaces", + "url": "https://www.yehwang.com/jewelry/men-s-necklaces", + "parent": "jewelry", + "level": 2, + "aliases": [ + "男士项链", + "men necklaces" + ] + }, + { + "category": "jewelry/navel-piercings", + "title": "Navel Piercings", + "url": "https://www.yehwang.com/jewelry/navel-piercings", + "parent": "jewelry", + "level": 2, + "aliases": [ + "肚脐环", + "navel piercing", + "navel piercings" + ] + }, + { + "category": "jewelry/necklaces", + "title": "Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces", + "parent": "jewelry", + "level": 2, + "aliases": [ + "项链", + "colliers", + "halsketten", + "kettingen", + "cadenas", + "necklaces" + ] + }, + { + "category": "jewelry/necklaces/beaded-necklaces", + "title": "Beaded Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces/beaded-necklaces", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/necklaces/braided-necklaces", + "title": "Braided Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces/braided-necklaces", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/necklaces/chain-necklaces", + "title": "Chain Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces/chain-necklaces", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/necklaces/layered-necklace", + "title": "Layered Necklace", + "url": "https://www.yehwang.com/jewelry/necklaces/layered-necklace", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/necklaces/long-necklaces", + "title": "Long Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces/long-necklaces", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [ + "长项链", + "long colliers", + "lange halsketten", + "langen kettingen" + ] + }, + { + "category": "jewelry/necklaces/medium-necklaces", + "title": "Medium Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces/medium-necklaces", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/necklaces/necklaces-sets", + "title": "Necklaces Sets", + "url": "https://www.yehwang.com/jewelry/necklaces/necklaces-sets", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/necklaces/pendant-necklaces", + "title": "Pendant Necklaces", + "url": "https://www.yehwang.com/jewelry/necklaces/pendant-necklaces", + "parent": "jewelry/necklaces", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/rings", + "title": "Rings", + "url": "https://www.yehwang.com/jewelry/rings", + "parent": "jewelry", + "level": 2, + "aliases": [ + "戒指", + "bagues", + "ringe", + "ringen", + "rings" + ] + }, + { + "category": "jewelry/rings/birthstone-rings", + "title": "Birthstone Rings", + "url": "https://www.yehwang.com/jewelry/rings/birthstone-rings", + "parent": "jewelry/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/rings/gemstone-rings", + "title": "Gemstone Rings", + "url": "https://www.yehwang.com/jewelry/rings/gemstone-rings", + "parent": "jewelry/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/rings/minimalist-rings", + "title": "Minimalist Rings", + "url": "https://www.yehwang.com/jewelry/rings/minimalist-rings", + "parent": "jewelry/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/rings/rhinestone-rings", + "title": "Rhinestone Rings", + "url": "https://www.yehwang.com/jewelry/rings/rhinestone-rings", + "parent": "jewelry/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/rings/rings-with-sizes", + "title": "Rings With Sizes", + "url": "https://www.yehwang.com/jewelry/rings/rings-with-sizes", + "parent": "jewelry/rings", + "level": 3, + "aliases": [] + }, + { + "category": "jewelry/stainless-steel-jewelry", + "title": "Stainless Steel Jewelry", + "url": "https://www.yehwang.com/jewelry/stainless-steel-jewelry", + "parent": "jewelry", + "level": 2, + "aliases": [ + "不锈钢珠宝", + "stainless steel", + "roestvrij staal" + ] + }, + { + "category": "new", + "title": "New", + "url": "https://www.yehwang.com/new", + "parent": null, + "level": 1, + "aliases": [ + "新品", + "新品上市", + "new arrivals", + "新到", + "nieuw" + ] + }, + { + "category": "new/pre-order", + "title": "Pre Order", + "url": "https://www.yehwang.com/new/pre-order", + "parent": "new", + "level": 2, + "aliases": [] + }, + { + "category": "packing-and-display", + "title": "Packing And Display", + "url": "https://www.yehwang.com/packing-and-display", + "parent": null, + "level": 1, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-display-materials", + "title": "Wholesale Display Materials", + "url": "https://www.yehwang.com/packing-and-display/wholesale-display-materials", + "parent": "packing-and-display", + "level": 2, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-display-materials/displays-for-bracelets", + "title": "Displays For Bracelets", + "url": "https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-bracelets", + "parent": "packing-and-display/wholesale-display-materials", + "level": 3, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-display-materials/displays-for-earrings", + "title": "Displays For Earrings", + "url": "https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-earrings", + "parent": "packing-and-display/wholesale-display-materials", + "level": 3, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-display-materials/displays-for-necklaces", + "title": "Displays For Necklaces", + "url": "https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-necklaces", + "parent": "packing-and-display/wholesale-display-materials", + "level": 3, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-display-materials/displays-for-rings", + "title": "Displays For Rings", + "url": "https://www.yehwang.com/packing-and-display/wholesale-display-materials/displays-for-rings", + "parent": "packing-and-display/wholesale-display-materials", + "level": 3, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-display-materials/jewelry-trays", + "title": "Jewelry Trays", + "url": "https://www.yehwang.com/packing-and-display/wholesale-display-materials/jewelry-trays", + "parent": "packing-and-display/wholesale-display-materials", + "level": 3, + "aliases": [] + }, + { + "category": "packing-and-display/wholesale-packing-materials", + "title": "Wholesale Packing Materials", + "url": "https://www.yehwang.com/packing-and-display/wholesale-packing-materials", + "parent": "packing-and-display", + "level": 2, + "aliases": [] + }, + { + "category": "restock-request", + "title": "Restock Request", + "url": "https://www.yehwang.com/restock-request", + "parent": null, + "level": 1, + "aliases": [ + "补货申请", + "restock", + "补货" + ] + }, + { + "category": "top-deal", + "title": "Top Deal", + "url": "https://www.yehwang.com/top-deal", + "parent": null, + "level": 1, + "aliases": [ + "促销", + "打折", + "sale", + "deals", + "aanbieding" + ] + }, + { + "category": "top-deal/12-12-sale/0-12-zone", + "title": "0 12 Zone", + "url": "https://www.yehwang.com/top-deal/12-12-sale/0-12-zone", + "parent": "top-deal/12-12-sale", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/daily-deals", + "title": "Daily Deals", + "url": "https://www.yehwang.com/top-deal/daily-deals", + "parent": "top-deal", + "level": 2, + "aliases": [ + "每日特价", + "daily deals" + ] + }, + { + "category": "top-deal/new-in-sale", + "title": "New In Sale", + "url": "https://www.yehwang.com/top-deal/new-in-sale", + "parent": "top-deal", + "level": 2, + "aliases": [ + "新品促销", + "new in sale" + ] + }, + { + "category": "top-deal/new-in-sale/1040045430-50-off", + "title": "1040045430 50 Off", + "url": "https://www.yehwang.com/top-deal/new-in-sale/1040045430-50-off", + "parent": "top-deal/new-in-sale", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/new-in-sale/20-30-off", + "title": "20 30 Off", + "url": "https://www.yehwang.com/top-deal/new-in-sale/20-30-off", + "parent": "top-deal/new-in-sale", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/new-in-sale/2113133623-20-off", + "title": "2113133623 20 Off", + "url": "https://www.yehwang.com/top-deal/new-in-sale/2113133623-20-off", + "parent": "top-deal/new-in-sale", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/new-in-sale/30-40-off", + "title": "30 40 Off", + "url": "https://www.yehwang.com/top-deal/new-in-sale/30-40-off", + "parent": "top-deal/new-in-sale", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/new-in-sale/40-50-off", + "title": "40 50 Off", + "url": "https://www.yehwang.com/top-deal/new-in-sale/40-50-off", + "parent": "top-deal/new-in-sale", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/sale-en", + "title": "Sale En", + "url": "https://www.yehwang.com/top-deal/sale-en", + "parent": "top-deal", + "level": 2, + "aliases": [ + "清仓", + "clearance" + ] + }, + { + "category": "top-deal/sale-en/accessories", + "title": "Accessories", + "url": "https://www.yehwang.com/top-deal/sale-en/accessories", + "parent": "top-deal/sale-en", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/sale-en/jewelry", + "title": "Jewelry", + "url": "https://www.yehwang.com/top-deal/sale-en/jewelry", + "parent": "top-deal/sale-en", + "level": 3, + "aliases": [] + }, + { + "category": "top-deal/sale-en/packing-and-display", + "title": "Packing And Display", + "url": "https://www.yehwang.com/top-deal/sale-en/packing-and-display", + "parent": "top-deal/sale-en", + "level": 3, + "aliases": [] + }, + { + "category": "wholesale-service", + "title": "Wholesale Service", + "url": "https://www.yehwang.com/wholesale-service", + "parent": null, + "level": 1, + "aliases": [ + "批发服务", + "wholesale service" + ] + }, + { + "category": "wholesale-service/private-label", + "title": "Private Label", + "url": "https://www.yehwang.com/wholesale-service/private-label", + "parent": "wholesale-service", + "level": 2, + "aliases": [] + } + ] +} \ No newline at end of file diff --git a/bridge-tools/config/sites.json b/bridge-tools/config/sites.json new file mode 100644 index 0000000..e35e498 --- /dev/null +++ b/bridge-tools/config/sites.json @@ -0,0 +1,45 @@ +{ + "_comment": "LibreDesk inbox → site profile。inboxes 的 key 是 inbox 数字 ID;profiles 可按站点扩展。", + "defaultSite": "www", + "profiles": { + "www": { + "site": "www", + "tenantId": 2, + "currencyCode": "EUR", + "searchUrl": "https://www.yehwang.com/search.html?q=", + "siteMap": "site-map.json", + "language": "en" + }, + "us": { + "site": "us", + "tenantId": 2, + "currencyCode": "USD", + "searchUrl": "https://us.yehwang.com/search.html?q=", + "siteMap": "site-map-us.json", + "language": "en" + }, + "eu": { + "site": "www", + "tenantId": 2, + "currencyCode": "EUR", + "searchUrl": "https://www.yehwang.com/search.html?q=", + "siteMap": "site-map.json", + "language": "en" + } + }, + "inboxes": { + "1": "www", + "2": "www", + "3": "www" + }, + "languageIds": { + "en": 1, + "nl": 2, + "de": 3, + "es": 4, + "fr": 5, + "it": 6, + "tr": 7, + "cn": 10 + } +} \ No newline at end of file diff --git a/bridge-tools/package.json b/bridge-tools/package.json new file mode 100644 index 0000000..d497d71 --- /dev/null +++ b/bridge-tools/package.json @@ -0,0 +1,15 @@ +{ + "name": "bridge-tools", + "version": "0.1.0", + "private": true, + "description": "LibreDesk AI business tools only (products / orders / inventory / customer profile)", + "type": "commonjs", + "engines": { + "node": ">=20" + }, + "scripts": { + "start": "node src/server.js", + "dev": "node --watch src/server.js", + "test": "node --test \"test/*.test.js\"" + } +} diff --git a/bridge-tools/src/auth.js b/bridge-tools/src/auth.js new file mode 100644 index 0000000..54bea9e --- /dev/null +++ b/bridge-tools/src/auth.js @@ -0,0 +1,23 @@ +'use strict'; + +const crypto = require('crypto'); + +/** + * 工具服务入口鉴权:LibreDesk 在工具的 Headers 配置里带 X-Tools-Key。 + * 未配置 key 时 fail-closed(返回 enabled=false,server 会直接 503)。 + */ +function createAuther(expectedKey) { + const key = String(expectedKey || ''); + const expected = Buffer.from(key); + + function verify(value) { + if (!key) return false; + const given = Buffer.from(String(value || '')); + if (given.length !== expected.length) return false; + return crypto.timingSafeEqual(given, expected); + } + + return { enabled: key.length > 0, verify }; +} + +module.exports = { createAuther }; diff --git a/bridge-tools/src/config.js b/bridge-tools/src/config.js new file mode 100644 index 0000000..05f94c3 --- /dev/null +++ b/bridge-tools/src/config.js @@ -0,0 +1,65 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// 多站点配置:inbox 数字 ID -> 站点 profile(tenant / 语言 / 类目映射 / 搜索地址) +function loadSites(env) { + const fallback = { + defaultSite: 'www', + profiles: { + www: { site: 'www', tenantId: null, languageId: null, currencyCode: 'EUR', searchUrl: 'https://www.yehwang.com/search.html?q=', siteMap: 'site-map.json' }, + }, + inboxes: {}, + }; + const raw = env.SITES_JSON; + if (raw) { + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') return parsed; + } catch { + // 配置坏了就用默认值 + } + } + try { + const parsed = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config', 'sites.json'), 'utf8')); + if (parsed && typeof parsed === 'object') return parsed; + } catch { + // 没有配置文件时保持默认 + } + return fallback; +} + +/** + * bridge-tools 配置。全部来自环境变量。 + * 密钥类字段不落默认值;缺配置时对应工具返回 503,而不是静默降级。 + */ +function loadConfig(env = process.env) { + const pick = (k, d) => (env[k] === undefined || env[k] === '' ? d : env[k]); + const num = (k, d) => { + const n = Number(pick(k, '')); + return Number.isFinite(n) && n > 0 ? n : d; + }; + + return { + port: num('TOOLS_PORT', 4100), + toolsSharedKey: String(pick('TOOLS_SHARED_KEY', '')), + toolsPublicBaseUrl: String(pick('TOOLS_PUBLIC_BASE_URL', 'http://bridge-tools:4100')).replace(/\/+$/, ''), + // 仅本地联调用:跳过 LibreDesk 注入的 X-Libredesk-Contact-Verified 校验。 + devBypassVerification: /^(1|true|yes)$/i.test(String(pick('TOOLS_DEV_BYPASS', 'false'))), + siteSearchUrl: String(pick('SITE_SEARCH_URL', 'https://www.yehwang.com/search.html?q=')), + + recommendUrl: String(pick('RECOMMEND_URL', '')), + orderApiUrl: String(pick('ORDER_API_URL', '')), + inventoryApiUrl: String(pick('INVENTORY_API_URL', '')), + memberRpcUrl: String(pick('MEMBER_RPC_URL', '')), + rpcTimeoutMs: num('RPC_TIMEOUT_MS', 8000), + productTimeoutMs: num('PRODUCT_TIMEOUT_MS', 15000), + + sites: loadSites(env), + defaultTenantId: num('DEFAULT_TENANT_ID', 2), + defaultLanguageId: num('DEFAULT_LANGUAGE_ID', 1), + }; +} + +module.exports = { loadConfig }; diff --git a/bridge-tools/src/errors.js b/bridge-tools/src/errors.js new file mode 100644 index 0000000..ae5acbe --- /dev/null +++ b/bridge-tools/src/errors.js @@ -0,0 +1,17 @@ +'use strict'; + +/** + * 工具执行错误。status 会直接映射为 HTTP 状态码: + * - 4xx:LibreDesk 视为“工具失败”,AI 通常会告知客户无法完成/转人工 + * - 2xx:即使没有数据也应当返回 2xx(让模型能继续对话) + */ +class ToolError extends Error { + constructor(status, code, message) { + super(message); + this.name = 'ToolError'; + this.status = status; + this.code = code; + } +} + +module.exports = { ToolError }; diff --git a/bridge-tools/src/product-query.js b/bridge-tools/src/product-query.js new file mode 100644 index 0000000..b98b4cc --- /dev/null +++ b/bridge-tools/src/product-query.js @@ -0,0 +1,42 @@ +'use strict'; + +const CJK = /[\u3400-\u9fff]/; + +function normalize(value) { + return String(value || '') + .toLowerCase() + .replace(/[^\p{L}\p{N}\s-]+/gu, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +// 把中文/别名商品词映射到 site-map 的英文类目,例如 戒指 -> Rings。 +function translateProductQuery(query, entries) { + const original = String(query || '').trim(); + const q = normalize(original); + if (!q || !Array.isArray(entries) || !entries.length) return { query: original, category: null, translated: false }; + if (!CJK.test(q)) return { query: original, category: null, translated: false }; + + let best = null; + for (const entry of entries) { + const fields = [entry.title, String(entry.category || '').split('/').pop(), ...(entry.aliases || [])] + .map(normalize) + .filter((f) => f.length >= 2); + for (const field of fields) { + if (q.includes(field) || field.includes(q)) { + const score = field.length + (field === q ? 100 : 0); + if (!best || score > best.score) best = { entry, score, field }; + } + } + } + if (!best) return { query: original, category: null, translated: false }; + + return { + query: best.entry.title || original, + category: { name: best.entry.title || null, url: best.entry.url || null }, + translated: true, + matched_alias: best.field, + }; +} + +module.exports = { translateProductQuery, normalize }; diff --git a/bridge-tools/src/rpc.js b/bridge-tools/src/rpc.js new file mode 100644 index 0000000..5670c4e --- /dev/null +++ b/bridge-tools/src/rpc.js @@ -0,0 +1,136 @@ +'use strict'; + +// JSONRPC-TCP 客户端(Hyperf JSONRPC 协议) +// 协议:TCP + JSON 请求体 + "\r\n" 帧分隔;服务间内部通道,无鉴权。 +// 支持 jsonrpc://host:port 与 http(s):// 双形态地址(后者降级走 HTTP)。 +const net = require('net'); + +const DEFAULT_TIMEOUT = 8000; + +/** + * 解析地址:jsonrpc://host:port 或 host:port 或 http(s)://host:port + * @returns {{protocol: 'jsonrpc'|'http', host: string, port: number, httpUrl: string|null}} + */ +function parseAddress(url) { + const s = String(url || '').trim(); + if (s.startsWith('jsonrpc://')) { + const m = s.replace('jsonrpc://', '').match(/^([^:/]+):?(\d+)?/); + if (m) return { protocol: 'jsonrpc', host: m[1], port: m[2] ? parseInt(m[2], 10) : 9502, httpUrl: null }; + } + if (/^https?:\/\//.test(s)) { + try { + const u = new URL(s); + return { + protocol: 'http', + host: u.hostname, + port: u.port ? parseInt(u.port, 10) : (u.protocol === 'https:' ? 443 : 80), + httpUrl: s, + }; + } catch { + /* fallthrough */ + } + } + const m = s.match(/^([^:/]+):?(\d+)?/); + if (m) return { protocol: 'jsonrpc', host: m[1], port: m[2] ? parseInt(m[2], 10) : 9502, httpUrl: null }; + return { protocol: 'jsonrpc', host: s, port: 9502, httpUrl: null }; +} + +/** + * JSONRPC 调用(TCP)。 + * @param {string} addr jsonrpc://host:port + * @param {string} method 如 /erp_order/getList + * @param {Array} [params] 位置参数数组 + * @param {Array} [context] 请求上下文(可空) + */ +function jsonrpcCall(addr, method, params = [], context = [], timeout = DEFAULT_TIMEOUT) { + return new Promise((resolve, reject) => { + const { host, port } = parseAddress(addr); + const id = 'tools_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); + const payload = JSON.stringify({ jsonrpc: '2.0', method, params, id, context }) + '\r\n'; + + const sock = net.createConnection({ host, port }); + let buf = ''; + let settled = false; + + const finish = (fn, value) => { + if (settled) return; + settled = true; + clearTimeout(timer); + sock.destroy(); + fn(value); + }; + + const timer = setTimeout(() => finish(reject, new Error(`JSONRPC timeout: ${method} @ ${host}:${port}`)), timeout); + sock.setTimeout(timeout); + sock.on('timeout', () => sock.destroy()); + + sock.on('connect', () => sock.write(payload)); + + sock.on('data', (chunk) => { + buf += chunk.toString('utf8'); + const frames = buf.split('\r\n'); + buf = frames.pop(); // 保留不完整尾帧 + for (const frame of frames) { + if (!frame.trim()) continue; + let parsed; + try { + parsed = JSON.parse(frame); + } catch { + continue; + } + if (parsed && parsed.id === id) { + if (parsed.error) { + finish(reject, new Error(`JSONRPC error: ${method} -> ${parsed.error.message || JSON.stringify(parsed.error)}`)); + } else { + finish(resolve, parsed.result); + } + return; + } + } + }); + + sock.on('error', (e) => finish(reject, new Error(`JSONRPC connect failed: ${host}:${port} (${e.message})`))); + sock.on('close', () => finish(reject, new Error(`JSONRPC connection closed before response: ${method}`))); + }); +} + +/** + * 统一执行入口:按地址协议分发(jsonrpc:// → TCP;http(s):// → fetch)。 + */ +async function call(addr, method, params = [], httpOptions = {}, timeout = DEFAULT_TIMEOUT) { + const parsed = parseAddress(addr); + if (parsed.protocol === 'jsonrpc') return jsonrpcCall(addr, method, params, httpOptions.context || [], timeout); + + const qs = httpOptions.query ? '?' + new URLSearchParams(httpOptions.query).toString() : ''; + const resp = await fetch(parsed.httpUrl + method + qs, { + headers: httpOptions.headers || {}, + signal: AbortSignal.timeout(timeout), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status} for ${method}`); + return resp.json(); +} + +/** 业务 RPC 返回体形态不一,统一摊平成数组。 */ +function asList(data) { + if (Array.isArray(data)) return data; + if (!data || typeof data !== 'object') return []; + for (const key of ['list', 'data', 'items', 'rows', 'result']) { + if (data[key] !== undefined) { + const inner = asList(data[key]); + if (inner.length) return inner; + } + } + return []; +} + +/** 业务 RPC 返回体摊平成对象(取 list/data/result 的第一层对象)。 */ +function asObject(data) { + if (!data || typeof data !== 'object') return null; + if (Array.isArray(data)) return data[0] || null; + for (const key of ['data', 'result', 'item']) { + if (data[key] && typeof data[key] === 'object' && !Array.isArray(data[key])) return data[key]; + } + return data; +} + +module.exports = { parseAddress, jsonrpcCall, call, asList, asObject }; diff --git a/bridge-tools/src/server.js b/bridge-tools/src/server.js new file mode 100644 index 0000000..1366539 --- /dev/null +++ b/bridge-tools/src/server.js @@ -0,0 +1,180 @@ +'use strict'; + +// bridge-tools HTTP server:只做三件事 +// GET /healthz 健康检查 +// GET /tools/manifest 给 LibreDesk 注册工具用的清单 +// POST /tools/ 执行工具(X-Tools-Key 鉴权 + X-Libredesk-* 身份头) + +const http = require('http'); +const { loadConfig } = require('./config'); +const { createAuther } = require('./auth'); +const { createTools, resolveSiteProfile } = require('./tools'); +const { ToolError } = require('./errors'); + +const MAX_BODY_BYTES = 256 * 1024; + +function readBody(req) { + return new Promise((resolve, reject) => { + let size = 0; + const chunks = []; + req.on('data', (chunk) => { + size += chunk.length; + if (size > MAX_BODY_BYTES) { + reject(new ToolError(413, 'payload_too_large', 'request body too large')); + req.destroy(); + return; + } + chunks.push(chunk); + }); + req.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf8'); + if (!raw) return resolve({}); + try { + resolve(JSON.parse(raw)); + } catch { + reject(new ToolError(400, 'invalid_json', 'request body must be valid JSON')); + } + }); + req.on('error', reject); + }); +} + +function sendJson(res, status, body) { + const payload = JSON.stringify(body); + res.writeHead(status, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(payload), + }); + res.end(payload); +} + +/** LibreDesk 在工具请求里注入的身份头;服务端必须以此为准,不能信任模型参数。 */ +function contextFromHeaders(headers = {}) { + const h = (name) => String(headers[name] || '').trim(); + return { + contactId: h('x-libredesk-contact-id'), + email: h('x-libredesk-contact-email').toLowerCase(), + externalId: h('x-libredesk-contact-external-id'), + contactType: h('x-libredesk-contact-type'), + conversationUuid: h('x-libredesk-conversation-uuid'), + inboxId: h('x-libredesk-inbox-id'), + verified: h('x-libredesk-contact-verified').toLowerCase() === 'true', + }; +} + +function createServer(options = {}) { + const cfg = options.config || loadConfig(); + const auther = createAuther(cfg.toolsSharedKey); + const tools = options.tools || createTools(cfg); + const byName = new Map(tools.map((t) => [t.name, t])); + const startedAt = Date.now(); + + const server = http.createServer(async (req, res) => { + const url = (req.url || '').split('?')[0]; + + try { + if (req.method === 'GET' && url === '/healthz') { + return sendJson(res, 200, { + ok: true, + service: 'bridge-tools', + uptime_sec: Math.round((Date.now() - startedAt) / 1000), + tools: [...byName.keys()], + verification_bypass: cfg.devBypassVerification, + sites: Object.keys((cfg.sites && cfg.sites.profiles) || {}), + }); + } + + if (!auther.enabled) { + return sendJson(res, 503, { + ok: false, + error: 'tools_key_not_configured', + message: 'Set TOOLS_SHARED_KEY and use the same value in the LibreDesk tool headers.', + }); + } + if (!auther.verify(req.headers['x-tools-key'])) { + return sendJson(res, 401, { ok: false, error: 'unauthorized', message: 'invalid or missing X-Tools-Key' }); + } + + if (req.method === 'GET' && url === '/tools/manifest') { + return sendJson(res, 200, { + base_url: cfg.toolsPublicBaseUrl, + tools: tools.map((t) => ({ + name: t.name, + description: t.description, + method: 'POST', + url: `${cfg.toolsPublicBaseUrl}/tools/${t.name}`, + parameters: t.parameters, + requires_verification: t.requiresVerification, + auth: { headers: [{ key: 'X-Tools-Key', value: '' }] }, + })), + }); + } + + const match = url.match(/^\/tools\/([a-z0-9_]+)$/); + if (req.method === 'POST' && match) { + const tool = byName.get(match[1]); + if (!tool) { + return sendJson(res, 404, { ok: false, error: 'unknown_tool', message: `unknown tool: ${match[1]}` }); + } + + const body = await readBody(req); + const ctx = contextFromHeaders(req.headers); + ctx.siteProfile = resolveSiteProfile(cfg, ctx); + + // 仅本地联调可跳过验证;默认关闭。 + if (cfg.devBypassVerification && !ctx.verified) { + ctx.verified = true; + console.warn(JSON.stringify({ ts: new Date().toISOString(), warn: 'verification bypassed by TOOLS_DEV_BYPASS', tool: tool.name })); + } + + if (tool.requiresVerification && !ctx.verified) { + return sendJson(res, 403, { + ok: false, + error: 'verification_required', + message: + 'Ask the customer to verify their email with the one-time code, then call this tool again. Do not ask for personal data before verification.', + }); + } + + const started = Date.now(); + const result = await tool.handler(body, ctx); + console.log(JSON.stringify({ + ts: new Date().toISOString(), + tool: tool.name, + ms: Date.now() - started, + verified: ctx.verified, + inbox: ctx.inboxId || null, + site: ctx.siteProfile.site, + ok: true, + })); + return sendJson(res, 200, { ok: true, tool: tool.name, ...result }); + } + + return sendJson(res, 404, { ok: false, error: 'not_found' }); + } catch (err) { + if (err instanceof ToolError) { + console.warn(JSON.stringify({ ts: new Date().toISOString(), error: err.code, status: err.status, message: err.message })); + return sendJson(res, err.status, { ok: false, error: err.code, message: err.message }); + } + console.error(JSON.stringify({ ts: new Date().toISOString(), error: 'internal_error', message: err && err.message })); + return sendJson(res, 500, { ok: false, error: 'internal_error', message: 'Tool execution failed' }); + } + }); + + return server; +} + +if (require.main === module) { + const cfg = loadConfig(); + const server = createServer({ config: cfg }); + server.listen(cfg.port, '0.0.0.0', () => { + console.log(`[bridge-tools] listening on :${cfg.port} — manifest: ${cfg.toolsPublicBaseUrl}/tools/manifest`); + if (!cfg.toolsSharedKey) console.warn('[bridge-tools] TOOLS_SHARED_KEY is not set — all /tools/* calls will return 503'); + if (cfg.devBypassVerification) console.warn('[bridge-tools] TOOLS_DEV_BYPASS=true — verification checks are BYPASSED (local development only)'); + }); + const shutdown = () => server.close(() => process.exit(0)); + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +module.exports = { createServer, contextFromHeaders }; diff --git a/bridge-tools/src/tools.js b/bridge-tools/src/tools.js new file mode 100644 index 0000000..2a53e28 --- /dev/null +++ b/bridge-tools/src/tools.js @@ -0,0 +1,542 @@ +'use strict'; + +// bridge-tools:只保留 LibreDesk AI 需要的业务工具(商品 / 订单 / 库存 / 客户资料)。 +// 其余能力(AI 编排、RAG、OTP、转人工、身份签发、质量飞轮)已由 LibreDesk v2.9.1 内置实现。 +// 来源:从 crm-new/bridge 裁剪,仅保留 RPC 业务工具链路。 + +const fs = require('fs'); +const path = require('path'); +const { ToolError } = require('./errors'); +const { asList, asObject } = require('./rpc'); +const { translateProductQuery } = require('./product-query'); + +// ---------------------------------------------------------------- 站点配置 + +const siteMapCache = new Map(); +function loadSiteMapFile(file) { + const name = file || 'site-map.json'; + if (siteMapCache.has(name)) return siteMapCache.get(name); + let entries = []; + try { + const raw = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config', name), 'utf8')); + entries = Array.isArray(raw.entries) ? raw.entries : []; + } catch { + entries = []; + } + siteMapCache.set(name, entries); + return entries; +} + +// 语言 code -> RPC language_id(业务侧枚举) +const DEFAULT_LANGUAGE_IDS = { en: 1, nl: 2, de: 3, es: 4, fr: 5, it: 6, tr: 7, cn: 10 }; + +// inbox 数字 ID -> 站点 profile(tenant / 语言 / 站点地图 / 搜索地址) +function resolveSiteProfile(cfg, ctx) { + const sites = cfg.sites || {}; + const inboxKey = String((ctx && ctx.inboxId) || ''); + const key = (sites.inboxes && sites.inboxes[inboxKey]) || sites.defaultSite || 'www'; + const profile = (sites.profiles && sites.profiles[key]) || {}; + const languageIds = (sites.languageIds && Object.keys(sites.languageIds).length ? sites.languageIds : DEFAULT_LANGUAGE_IDS); + const languageId = profile.languageId != null + ? profile.languageId + : (profile.language && languageIds[profile.language] != null ? languageIds[profile.language] : (cfg.defaultLanguageId != null ? cfg.defaultLanguageId : 1)); + return { + key, + site: profile.site || key, + tenantId: profile.tenantId != null ? profile.tenantId : (cfg.defaultTenantId != null ? cfg.defaultTenantId : 1), + languageId, + currencyCode: profile.currencyCode || '', + searchUrl: profile.searchUrl || cfg.siteSearchUrl, + homeUrl: profile.homeUrl || (() => { try { return new URL(profile.searchUrl || cfg.siteSearchUrl).origin; } catch { return ''; } })(), + siteMap: profile.siteMap || 'site-map.json', + }; +} + +// Hyperf JSON-RPC 的 DTO 反序列化要求嵌套对象带 _class(URL 编码的 PHP 类名)。 +const REQ_CONTEXT_CLASS = encodeURIComponent('Yw\\Common\\Dto\\ReqContext'); +const DEVICE_INFO_CLASS = encodeURIComponent('Yw\\Member\\Client\\Dto\\DeviceInfoDto'); + +function rpcContextFor(profile) { + return { + _class: REQ_CONTEXT_CLASS, + tenantId: profile.tenantId, + languageId: profile.languageId, + currencyCode: profile.currencyCode || '', + defaultCurrencyCode: profile.currencyCode || '', + siteType: 1, + isoCode: '', + }; +} + +function withRpcContext(params, profile) { + if (!Array.isArray(params) || params.length === 0) return params; + const first = params[0]; + if (!first || typeof first !== 'object' || Array.isArray(first)) return params; + return [{ + ...first, + reqContext: rpcContextFor(profile), + deviceInfo: { + _class: DEVICE_INFO_CLASS, + ip: '', + userAgent: '', + deviceToken: '', + deviceType: 'h5', + appVersion: '', + }, + }, ...params.slice(1)]; +} + +// ---------------------------------------------------------------- 站点地图兜底匹配 + +function normalizeText(value) { + return String(value || '') + .toLowerCase() + .replace(/[^\p{L}\p{N}\s-]+/gu, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +const CJK_RE = /[\u3400-\u9fff]/; +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} +// 中文用子串匹配;英文用词边界,避免 "earrings" 误命中 "rings"。 +function fieldMatches(field, token) { + if (!field || !token) return false; + if (CJK_RE.test(field) || CJK_RE.test(token)) return field.includes(token) || token.includes(field); + if (token.length < 2) return false; + return new RegExp(`(^|[^a-z0-9])${escapeRegExp(token)}([^a-z0-9]|$)`).test(field); +} + +function matchSiteMap(entries, query, limit) { + const q = normalizeText(query); + if (!q) return []; + const tokens = q.split(' ').filter(Boolean); + const scored = []; + for (const entry of entries) { + const fields = [entry.title, String(entry.category || '').split('/').pop(), ...(entry.aliases || [])] + .map(normalizeText) + .filter(Boolean); + let score = 0; + for (const field of fields) { + if (field === q) score = Math.max(score, 100); + else if (fieldMatches(field, q) || (q.length >= 2 && fieldMatches(q, field))) score = Math.max(score, 80); + else if (tokens.every((t) => fieldMatches(field, t))) score = Math.max(score, 60); + } + if (!score && tokens.length) { + const hits = tokens.filter((t) => fields.some((f) => fieldMatches(f, t))).length; + if (hits === tokens.length) score = 50; + } + if (score > 0) scored.push({ entry, score }); + } + scored.sort((a, b) => + b.score - a.score || + (a.entry.level || 9) - (b.entry.level || 9) || + String(a.entry.url || '').length - String(b.entry.url || '').length + ); + return scored.slice(0, limit).map(({ entry }) => ({ + name: entry.title || null, + url: entry.url || null, + sku: null, + price: null, + })); +} + +// ---------------------------------------------------------------- 工具定义 + +/** + * 工具定义(同步给 LibreDesk Admin → AI → Tools)。 + * description / parameters 直接进入模型上下文,用英文写。 + * requiresVerification: LibreDesk 侧同名开关(未验证联系人会被平台拦截);服务端仍二次校验(fail-closed)。 + */ +const TOOL_DEFS = [ + { + name: 'search_products', + description: + 'Search the Yehwang product catalog by keyword. Returns product names, prices, images, stock, product ids and page URLs (falls back to category page links when the catalog index has no exact match). Call this for ANY product-finding request instead of answering from memory; always include returned URLs and prices in the reply.', + parameters: { + type: 'object', + properties: { + query: { type: 'string', description: 'product keywords, e.g. "gold hoop earrings" or "stainless steel necklace"' }, + limit: { type: 'integer', description: 'max results, default 5, max 5' }, + }, + required: ['query'], + }, + requiresVerification: false, + }, + { + name: 'search_orders', + description: + 'Look up order status and details for the CURRENT verified customer only. Requires a verified contact. If the customer gives an order number, pass it as orderNumber to get full detail (items, totals, shipping address, tracking). Omit orderNumber to list the latest 5 orders. Never ask for or accept another person\'s email; the email is taken from platform identity headers. Always include the shipping method in the reply.', + parameters: { + type: 'object', + properties: { + orderNumber: { type: 'string', description: 'order number or order id; omit to list the latest 5 orders' }, + }, + required: [], + }, + requiresVerification: true, + }, + { + name: 'search_inventory', + description: + 'Check real-time stock and pre-order ETA for a specific product. Pass product_id returned by search_products when available; otherwise a SKU/product id whose digits identify the product.', + parameters: { + type: 'object', + properties: { + product_id: { type: 'integer', description: 'product id from search_products (preferred)' }, + sku: { type: 'string', description: 'SKU or product id' }, + productName: { type: 'string', description: 'product name if SKU is unknown' }, + }, + required: [], + }, + requiresVerification: false, + }, + { + name: 'get_customer_info', + description: + 'Look up the CURRENT verified customer\'s own account profile: display name, company, country, member grade, discount group, points, store credit, registration date and phone. Requires a verified contact; the identity always comes from platform headers, never from the customer message. Use it to personalise the reply or confirm account context. Never look up or reveal another person\'s profile.', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + requiresVerification: true, + }, +]; + +function digitsOf(value) { + const m = String(value || '').match(/\d+/); + return m ? parseInt(m[0], 10) : null; +} + +function summarizeOrder(o) { + if (!o || typeof o !== 'object') return null; + return { + order_no: o.orderNo || o.order_no || o.order_id || o.orderId || null, + order_id: o.orderId || o.order_id || null, + status: o.statusText || o.orderStatus || o.status || null, + total: o.total || o.totalPrice || o.total_price || null, + date: o.dateAdded || o.date_added || null, + shipping_method: o.shippingMethod || o.shipping_method || null, + tracking_number: o.trackingNumber || o.tracking_number || null, + }; +} + +// ---------------------------------------------------------------- 工具实现 + +function createTools(cfg, deps = {}) { + const rpc = deps.rpc || require('./rpc'); + const siteMapOverride = deps.siteMap || null; + const siteEntries = (profile) => siteMapOverride || loadSiteMapFile(profile.siteMap); + + async function callRpc(addr, method, params, ctx, timeoutMs) { + if (!addr) throw new ToolError(503, 'not_configured', `RPC endpoint for ${method} is not configured`); + const profile = resolveSiteProfile(cfg, ctx || {}); + return rpc.call(addr, method, withRpcContext(params, profile), {}, timeoutMs || cfg.rpcTimeoutMs); + } + + async function searchProducts(args, ctx) { + 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, 5); + const profile = resolveSiteProfile(cfg, ctx); + // 中文/别名商品词先映射到英文类目,例如 简约不锈钢戒指 -> Rings + const translation = translateProductQuery(query, siteEntries(profile)); + const rpcQuery = translation.query || query; + + const absoluteUrl = (href) => { + if (!href) return null; + if (/^https?:\/\//i.test(href)) return href; + return profile.homeUrl ? profile.homeUrl + (href.startsWith('/') ? '' : '/') + href : href; + }; + + let items = []; + let total = null; + let catalogError = null; + try { + const cmd = { + keyword: rpcQuery, + page: 1, + // 多取候选,下面按库存排序后再截断到 limit,避免缺货商品占满前几条 + pageSize: limit * 4, + languageId: profile.languageId, + tenantId: profile.tenantId, + currencyCode: profile.currencyCode || '', + status: 20, + siteType: 1, + supplierCode: '', + tagIds: [], + tenantBaseCurrency: profile.currencyCode || '', + filterSpuList: [], + productSource: 1, + }; + const raw = await callRpc(cfg.recommendUrl, '/product/list', [cmd], ctx, cfg.productTimeoutMs); + const result = (raw && raw.result) || raw || {}; + const paging = result.data || result; + const list = Array.isArray(paging) ? paging : (paging.data || paging.list || []); + total = result.total != null ? result.total : (paging.total != null ? paging.total : null); + items = (Array.isArray(list) ? list : []) + .map((p) => { + const name = p.spuName || p.name || p.title || null; + const sku = p.sku || p.model || (Array.isArray(p.skus) && p.skus[0] ? p.skus[0].productId : null); + const stock = Array.isArray(p.skus) + ? p.skus.reduce((sum, s) => sum + (Number(s.quantity) || 0), 0) + : null; + const href = p.href || p.url || p.product_url || null; + return { + name, + price: p.minPriceWithCurrency || p.priceWithCurrency || (p.price != null ? p.price : null), + url: absoluteUrl(href) || (name && profile.searchUrl ? profile.searchUrl + encodeURIComponent(name) : null), + sku, + product_id: p.spuId != null ? p.spuId : null, + image: p.masterImage || null, + stock, + mark_price: p.markPriceWithCurrency || null, + }; + }) + .filter((p) => p.name) + // 有库存优先:stock>0 → 0,未知 → 1,缺货 → 2;sort 稳定,保留 RPC 原顺序 + .sort((a, b) => { + const rank = (p) => (p.stock > 0 ? 0 : p.stock === null ? 1 : 2); + return rank(a) - rank(b); + }) + .slice(0, limit); + } catch (err) { + catalogError = err.message; + } + + // 兜底 1:商品列表不可用时退回 autocomplete(只有名字) + if (!items.length) { + try { + const raw = await callRpc(cfg.recommendUrl, '/product_new/autocomplete', [ + { keyword: rpcQuery, page: 1, pageSize: limit }, + ], ctx, cfg.productTimeoutMs); + items = asList(raw) + .slice(0, limit) + .map((p) => { + const name = p.name || p.title || null; + const url = p.url || p.product_url || p.href || + (name && profile.searchUrl ? profile.searchUrl + encodeURIComponent(name) : null); + return { name, price: null, url, sku: null }; + }) + .filter((p) => p.name); + if (items.length) catalogError = null; + } catch (err) { + catalogError = catalogError || err.message; + } + } + + let source = 'catalog'; + const categoryLinks = matchSiteMap(siteEntries(profile), query, 3); + if (!items.length && categoryLinks.length) { + items = categoryLinks; + source = 'site-map'; + } + + return { + products: items, + count: items.length, + ...(total != null ? { total } : {}), + source, + site: profile.site, + ...(translation.translated ? { query_used: rpcQuery, translated_from: query, category: translation.category } : {}), + ...(categoryLinks.length && source === 'catalog' ? { category_links: categoryLinks } : {}), + search_url: profile.searchUrl + encodeURIComponent(query), + ...(source === 'site-map' + ? { note: 'These are Yehwang category pages matched from the site map; include the links in the reply.' } + : {}), + ...(source === 'catalog' && categoryLinks.length + ? { note: 'Product names come from the live catalog; category_links are real category pages you can also share. Prices/SKUs are currently unavailable.' } + : {}), + ...(catalogError ? { catalog_error: catalogError } : {}), + }; + } + + async function searchOrders(args, ctx) { + if (!ctx.verified) { + throw new ToolError(403, 'verification_required', 'Order lookup requires a verified contact. Ask the customer to verify their email with the one-time code first.'); + } + const email = String(ctx.email || '').trim().toLowerCase(); + if (!email) throw new ToolError(400, 'missing_email', 'The verified contact has no email on file.'); + + // 隐私门控:邮箱永远来自平台身份头,忽略模型/参数里传入的任何邮箱。 + if (args.orderNumber) { + const wanted = String(args.orderNumber).trim().toLowerCase(); + const own = asList(await callRpc(cfg.orderApiUrl, '/erp_order/getList', [ + { email, page: 1, pageSize: 50, transfer: 10000 }, + ], ctx)); + const match = own.find((o) => + [o.orderId, o.order_id, o.orderNo, o.order_no] + .filter((v) => v != null && v !== '') + .some((v) => String(v).toLowerCase() === wanted) + ); + if (!match) { + // 0 结果也返回 2xx:让模型告知客户核对单号,而不是当成系统故障。 + return { orders: [], count: 0, message: `No order "${args.orderNumber}" found for the verified customer.` }; + } + return { orders: [await fetchOrderDetail(match, ctx)], count: 1 }; + } + + const orders = asList(await callRpc(cfg.orderApiUrl, '/erp_order/getList', [ + { email, page: 1, pageSize: 5, transfer: 10000 }, + ], ctx)) + .slice(0, 5) + .map(summarizeOrder) + .filter(Boolean); + return { + orders, + count: orders.length, + ...(orders.length ? {} : { message: 'No orders found for the verified customer.' }), + }; + } + + async function fetchOrderDetail(listItem, ctx) { + const orderId = parseInt(listItem.orderId || listItem.order_id, 10) || 0; + const [detailRes, trackRes] = await Promise.allSettled([ + callRpc(cfg.orderApiUrl, '/erp_order/getDetail', [{ orderId }], ctx), + callRpc(cfg.orderApiUrl, '/erp_order/getTrackPreSaleOrder', [{ orderId, page: 1, pageSize: 5 }], ctx), + ]); + const detail = detailRes.status === 'fulfilled' ? asObject(detailRes.value) : null; + const trackingRaw = trackRes.status === 'fulfilled' ? trackRes.value : null; + const d = detail || listItem; + + const parcels = asList(trackingRaw) + .slice(0, 5) + .map((p) => ({ + shipping_method: p.shippingMethod || p.shipping_method || p.shippingCode || p.shipping_code || p.carrier || null, + tracking_number: p.trackingNumber || p.tracking_number || p.tracking_code || p.trackingCode || null, + shipped_at: p.shippingAt || p.shipping_at || p.shipped_at || null, + })) + .filter((p) => p.shipping_method || p.tracking_number); + + const address = [ + [d.shippingFirstname, d.shippingLastname].filter(Boolean).join(' '), + d.shippingCompany, + [d.shippingAddress_1, d.shippingAddress_2].filter(Boolean).join(' '), + [d.shippingPostcode, d.shippingCity].filter(Boolean).join(' '), + d.shippingZone, + d.shippingCountry, + ].filter(Boolean).join(', '); + + const items = asList(d.products).slice(0, 10).map((p) => ({ + name: p.name || null, + model: p.model || null, + quantity: p.quantity != null ? p.quantity : null, + price: p.price || null, + total: p.total || null, + can_return: p.canReturn != null ? Boolean(Number(p.canReturn)) : null, + })).filter((p) => p.name); + + const totals = asList(d.totals).slice(0, 8).map((t) => ({ + title: t.title || null, + value: t.value || null, + code: t.code || null, + })).filter((t) => t.title); + + return { + ...summarizeOrder(d), + order_id: d.orderId || orderId, + status: d.statusText || d.status || null, + payment_method: d.paymentMethod || d.paymentCode || null, + customer: { + name: [d.firstname, d.lastname].filter(Boolean).join(' ') || null, + email: d.email || null, + telephone: d.telephone || null, + }, + shipping_address: address || null, + shipping_method: d.shippingMethod || d.shipping_method || d.tracking || null, + shipping_code: d.shippingCode || null, + tracking_number: d.trackingNumber || d.tracking_number || null, + shipping_cost: d.shippingCost != null ? String(d.shippingCost) : null, + items, + totals, + ...(parcels.length ? { parcels } : {}), + email: d.email || null, + }; + } + + async function searchInventory(args, ctx) { + const productId = Number(args.product_id) || digitsOf(args.sku) || digitsOf(args.productName); + if (!productId) { + return { products: [], message: `Could not extract a product id from "${args.sku || args.productName || ''}". Ask the customer for the SKU.` }; + } + const [stockRes, etaRes] = await Promise.allSettled([ + callRpc(cfg.inventoryApiUrl, '/eta/queryStockSummary', [{ productId, warehouseId: 2, includeDetails: false }], ctx), + callRpc(cfg.inventoryApiUrl, '/eta/queryProductEta', [{ productId, warehouseId: 0, useCache: true, customerCode: '' }], ctx), + ]); + const stock = stockRes.status === 'fulfilled' ? asObject(stockRes.value) : null; + // eta 服务对未知 productId 会返回默认值,只有 stockSummary 才是权威数据。 + if (!stock || typeof stock.instantStock !== 'number') { + return { products: [], product_id: productId, message: `No inventory data found for product id ${productId}. Verify the SKU first.` }; + } + const instant = stock.instantStock; + const reserved = stock.reservedQuantity != null ? stock.reservedQuantity : 0; + const eta = etaRes.status === 'fulfilled' ? asObject(etaRes.value) : null; + return { + product_id: productId, + instant_stock: instant, + available: Math.max(0, instant - reserved), + in_transit: stock.inTransitQuantity || 0, + reserved, + ...(eta && (eta.etaDate || eta.status) + ? { eta_date: eta.etaDate || null, eta_available: eta.availableQuantity != null ? eta.availableQuantity : null, eta_status: eta.status || null } + : {}), + }; + } + + async function getCustomerInfo(args, ctx) { + if (!ctx.verified) { + throw new ToolError(403, 'verification_required', 'Customer profile lookup requires a verified contact. Ask the customer to verify their email with the one-time code first.'); + } + const email = String(ctx.email || '').trim().toLowerCase(); + if (!email) throw new ToolError(400, 'missing_email', 'The verified contact has no email on file.'); + if (!cfg.memberRpcUrl) throw new ToolError(503, 'not_configured', 'MEMBER_RPC_URL is not configured'); + + // 邮箱 → customerId(member-service 的 checkEmailExist:customerId=0 表示没有账号) + const check = asObject(await callRpc(cfg.memberRpcUrl, '/customer/checkEmailExist', [{ email }], ctx, cfg.rpcTimeoutMs)) || {}; + const customerId = parseInt(check.customerId, 10) || 0; + if (!customerId) { + return { found: false, email, message: 'No customer account is registered with this email.' }; + } + + const p = asObject(await callRpc(cfg.memberRpcUrl, '/customer/getByCustomerId', [{ customerId }], ctx, cfg.rpcTimeoutMs)) || {}; + const name = [p.firstname, p.lastname].map((v) => String(v || '').trim()).filter(Boolean).join(' ').trim(); + const dial = String(p.dial || '').trim(); + const phone = String(p.telephone || '').trim(); + + return { + found: true, + customer_id: customerId, + email: String(p.email || email).trim().toLowerCase(), + name: name || String(p.nickname || '').trim() || null, + company: String(p.company || '').trim() || null, + country: String(p.countryName || '').trim() || null, + phone: phone ? `${dial ? '+' + dial : ''}${phone}` : null, + member_grade: String(p.gradeName || '').trim() || null, + discount_group: String(p.groupName || '').trim() || null, + registered_at: p.dateAdded || null, + points: p.point != null ? p.point : null, + points_formatted: p.pointFormat || null, + store_credit: p.storeCredit != null ? p.storeCredit : null, + store_credit_formatted: p.storeCreditFormat || null, + mileage: p.mileage != null ? p.mileage : null, + account_status: p.status === 1 ? 'active' : (p.status === 0 ? 'inactive' : (p.status != null ? p.status : null)), + note: 'Profile data comes from the live customer account. Do not read out the email/phone unless the customer asks for it.', + }; + } + + const handlers = { + search_products: searchProducts, + search_orders: searchOrders, + search_inventory: searchInventory, + get_customer_info: getCustomerInfo, + }; + + return TOOL_DEFS.map((def) => ({ + ...def, + handler: (args, ctx) => handlers[def.name](args || {}, ctx), + })); +} + +module.exports = { createTools, TOOL_DEFS, resolveSiteProfile }; diff --git a/bridge-tools/test/bridge-tools.test.js b/bridge-tools/test/bridge-tools.test.js new file mode 100644 index 0000000..a958637 --- /dev/null +++ b/bridge-tools/test/bridge-tools.test.js @@ -0,0 +1,195 @@ +'use strict'; + +// 单测:mock RPC,覆盖 4 个工具 + HTTP 层(鉴权/健康检查/身份头) +const { test } = require('node:test'); +const assert = require('node:assert'); +const { loadConfig } = require('../src/config'); +const { createTools, resolveSiteProfile } = require('../src/tools'); +const { createServer } = require('../src/server'); + +function baseConfig(env = {}) { + return loadConfig({ + TOOLS_SHARED_KEY: 'test-key', + RECOMMEND_URL: 'jsonrpc://rpc:9632', + ORDER_API_URL: 'jsonrpc://rpc:9612', + INVENTORY_API_URL: 'jsonrpc://rpc:9509', + MEMBER_RPC_URL: 'jsonrpc://rpc:9602', + DEFAULT_TENANT_ID: '2', + DEFAULT_LANGUAGE_ID: '1', + ...env, + }); +} + +function mockRpc(handlers = {}) { + const calls = []; + return { + calls, + async call(addr, method, params, opts, timeout) { + calls.push({ addr, method, params, timeout }); + const h = handlers[method]; + if (h === undefined) throw new Error(`unexpected rpc method: ${method}`); + return typeof h === 'function' ? h(params) : h; + }, + }; +} + +const verifiedCtx = { + verified: true, + email: 'customer@example.com', + inboxId: '1', + conversationUuid: 'conv-1', +}; + +test('站点 profile:inbox 1 映射到 www / tenant 2 / EUR', () => { + const profile = resolveSiteProfile(baseConfig(), { inboxId: '1' }); + assert.equal(profile.site, 'www'); + assert.equal(profile.tenantId, 2); + assert.equal(profile.currencyCode, 'EUR'); + assert.equal(profile.searchUrl, 'https://www.yehwang.com/search.html?q='); +}); + +test('工具清单只暴露 4 个业务工具', () => { + const tools = createTools(baseConfig(), { rpc: mockRpc() }); + assert.deepEqual(tools.map((t) => t.name).sort(), ['get_customer_info', 'search_inventory', 'search_orders', 'search_products']); + assert.equal(tools.find((t) => t.name === 'search_orders').requiresVerification, true); + assert.equal(tools.find((t) => t.name === 'get_customer_info').requiresVerification, true); +}); + +test('search_products:catalog 命中,返回价格/库存/URL,并注入 RPC 上下文', async () => { + const rpc = mockRpc({ + '/product/list': { + result: { + data: [{ spuName: 'Gold Hoop Earrings', href: '/p/gold-hoop.html', spuId: 11, minPriceWithCurrency: '€9.99', skus: [{ quantity: 2 }], masterImage: '/img/1.jpg' }], + total: 1, + }, + }, + }); + const tools = createTools(baseConfig(), { rpc }); + const out = await tools.find((t) => t.name === 'search_products').handler({ query: 'gold hoop earrings' }, verifiedCtx); + assert.equal(out.count, 1); + assert.equal(out.source, 'catalog'); + assert.equal(out.products[0].name, 'Gold Hoop Earrings'); + assert.equal(out.products[0].price, '€9.99'); + assert.equal(out.products[0].stock, 2); + assert.equal(out.products[0].product_id, 11); + assert.equal(out.products[0].url, 'https://www.yehwang.com/p/gold-hoop.html'); + const sent = rpc.calls[0].params[0]; + assert.equal(sent.reqContext.tenantId, 2); + assert.equal(sent.reqContext.languageId, 1); + assert.ok(sent.deviceInfo._class); +}); + +test('search_products:catalog 空时退回 site-map 类目链接', async () => { + const rpc = mockRpc({ + '/product/list': { result: { data: [] } }, + '/product_new/autocomplete': { result: [] }, + }); + const tools = createTools(baseConfig(), { rpc }); + const out = await tools.find((t) => t.name === 'search_products').handler({ query: 'rings' }, verifiedCtx); + assert.equal(out.count > 0, true); + assert.equal(out.source, 'site-map'); + assert.ok(out.products[0].url); +}); + +test('search_orders:未验证直接 403,不触发 RPC', async () => { + const rpc = mockRpc({}); + const tools = createTools(baseConfig(), { rpc }); + await assert.rejects( + () => tools.find((t) => t.name === 'search_orders').handler({}, { ...verifiedCtx, verified: false }), + (err) => err.status === 403 && err.code === 'verification_required' + ); + assert.equal(rpc.calls.length, 0); +}); + +test('search_orders:已验证返回订单摘要(邮箱只取身份头)', async () => { + const rpc = mockRpc({ + '/erp_order/getList': { result: [{ orderId: 5, orderNo: 'YW1001', statusText: 'Shipped', total: '€42.00', dateAdded: '2026-10-01', shippingMethod: 'DHL' }] }, + }); + const tools = createTools(baseConfig(), { rpc }); + const out = await tools.find((t) => t.name === 'search_orders').handler({}, verifiedCtx); + assert.equal(out.count, 1); + assert.equal(out.orders[0].order_no, 'YW1001'); + assert.equal(out.orders[0].status, 'Shipped'); + assert.equal(rpc.calls[0].params[0].email, 'customer@example.com'); +}); + +test('search_inventory:合并库存与 ETA', async () => { + const rpc = mockRpc({ + '/eta/queryStockSummary': { result: { instantStock: 10, reservedQuantity: 2, inTransitQuantity: 3 } }, + '/eta/queryProductEta': { result: { etaDate: '2026-11-01', availableQuantity: 5, status: 'in_transit' } }, + }); + const tools = createTools(baseConfig(), { rpc }); + const out = await tools.find((t) => t.name === 'search_inventory').handler({ product_id: 11 }, verifiedCtx); + assert.equal(out.product_id, 11); + assert.equal(out.instant_stock, 10); + assert.equal(out.available, 8); + assert.equal(out.in_transit, 3); + assert.equal(out.eta_date, '2026-11-01'); +}); + +test('get_customer_info:邮箱查 customerId 后返回资料', async () => { + const rpc = mockRpc({ + '/customer/checkEmailExist': { result: { customerId: 7 } }, + '/customer/getByCustomerId': { + result: { firstname: 'Ada', lastname: 'Lovelace', email: 'customer@example.com', company: 'ACME', countryName: 'Netherlands', dial: '31', telephone: '612345678', gradeName: 'VIP', groupName: 'G1', status: 1, point: 100 }, + }, + }); + const tools = createTools(baseConfig(), { rpc }); + const out = await tools.find((t) => t.name === 'get_customer_info').handler({}, verifiedCtx); + assert.equal(out.found, true); + assert.equal(out.customer_id, 7); + assert.equal(out.name, 'Ada Lovelace'); + assert.equal(out.phone, '+31612345678'); + assert.equal(out.member_grade, 'VIP'); +}); + +test('HTTP:healthz 公开、无 key 401、有 key 可调工具、未验证 403', async () => { + const rpc = mockRpc({ + '/erp_order/getList': { result: [{ orderId: 5, orderNo: 'YW1001', statusText: 'Shipped' }] }, + }); + const cfg = baseConfig(); + const server = createServer({ config: cfg, tools: createTools(cfg, { rpc }) }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const base = `http://127.0.0.1:${server.address().port}`; + + try { + const health = await fetch(`${base}/healthz`); + assert.equal(health.status, 200); + const healthBody = await health.json(); + assert.equal(healthBody.service, 'bridge-tools'); + assert.equal(healthBody.tools.length, 4); + + const noKey = await fetch(`${base}/tools/manifest`); + assert.equal(noKey.status, 401); + + const manifest = await fetch(`${base}/tools/manifest`, { headers: { 'x-tools-key': 'test-key' } }); + assert.equal(manifest.status, 200); + const manifestBody = await manifest.json(); + assert.equal(manifestBody.tools.length, 4); + + const unverified = await fetch(`${base}/tools/search_orders`, { + method: 'POST', + headers: { 'x-tools-key': 'test-key', 'content-type': 'application/json' }, + body: '{}', + }); + assert.equal(unverified.status, 403); + + const verified = await fetch(`${base}/tools/search_orders`, { + method: 'POST', + headers: { + 'x-tools-key': 'test-key', + 'content-type': 'application/json', + 'x-libredesk-contact-verified': 'true', + 'x-libredesk-contact-email': 'customer@example.com', + 'x-libredesk-inbox-id': '1', + }, + body: '{}', + }); + assert.equal(verified.status, 200); + const body = await verified.json(); + assert.equal(body.tool, 'search_orders'); + assert.equal(body.orders[0].order_no, 'YW1001'); + } finally { + await new Promise((resolve) => server.close(resolve)); + } +}); diff --git a/config.sample.toml b/config.sample.toml new file mode 100644 index 0000000..3253b12 --- /dev/null +++ b/config.sample.toml @@ -0,0 +1,194 @@ +[app] +# Log level: info, debug, warn, error, fatal +log_level = "debug" +# Environment: dev, prod. +# Setting to "dev" will enable color logging in terminal. +env = "dev" +# Whether to automatically check for application updates on start up, app updates are shown as a banner in the admin panel. +check_updates = true +# Encryption key. Generate using `openssl rand -hex 16` must be 32 characters long. +encryption_key = "your-32-char-random-string-here!" +# Path to a directory with custom static files and templates to override the defaults. +# The directory structure should mirror the built-in static/ directory. +# Only the files you provide will be replaced; the rest use built-in defaults. +# static_dir = "/path/to/custom/static" + +# HTTP server. +[app.server] +# Address to bind the HTTP server to. +address = "0.0.0.0:9000" +# Unix socket path (leave empty to use TCP address instead) +socket = "" +# Do NOT disable secure cookies in production environment if you don't know exactly what you're doing! +disable_secure_cookies = false +# Session lifetime duration. +session_lifetime = "9h" +# Request read and write timeouts. +read_timeout = "60s" +write_timeout = "60s" +# Maximum request body size in bytes (100MB) +# If you are using proxy, you may need to configure them to allow larger request bodies. +max_body_size = 104857600 +# Size of the read buffer for incoming requests (also limits max header size). +read_buffer_size = 65536 +# Keepalive settings. +keepalive_timeout = "10s" + +[auth] +# Allow users to sign in with their libredesk email and password. +# Disable this only after configuring and enabling at least one OIDC provider. +# libredesk automatically keeps local login available when no OIDC provider is enabled to prevent lockout. +# If an enabled provider is unavailable or misconfigured, set auth.local_login_enabled = true to restore password login. +local_login_enabled = true + +# File upload provider to use, either `fs` or `s3`. +[upload] +provider = "fs" + +# Filesystem provider. +[upload.fs] +# Directory where uploaded files are stored, make sure this directory exists and is writable by the application. +upload_path = 'uploads' +# Signed URL expiry duration (e.g., "30m", "1h"). Defaults to 1h if unset. +expiry = "1h" + +# S3 provider. +[upload.s3] +# S3 endpoint URL (required only for non-AWS S3-compatible providers like MinIO). +# Leave empty to use default AWS endpoints. +url = "" + +# AWS S3 credentials, keep empty to use attached IAM roles. +access_key = "" +secret_key = "" + +# AWS region, e.g., "us-east-1", "eu-west-1", etc. +region = "ap-south-1" +# S3 bucket name where files will be stored. +bucket = "bucket-name" +# Optional prefix path within the S3 bucket where files will be stored. +# Example, if set to "uploads/media", files will be stored under that path. +# Useful for organizing files inside a shared bucket. +bucket_path = "" +# S3 signed URL expiry duration (e.g., "30m", "1h") +expiry = "30m" + +# Postgres. +[db] +# If running locally, use `localhost`. +host = "db" +# Database port, default is 5432. +port = 5432 +# Update the following values with your database credentials. +user = "libredesk" +password = "libredesk" +database = "libredesk" +ssl_mode = "disable" +# Maximum number of open database connections +max_open = 30 +# Maximum number of idle connections in the pool +max_idle = 30 +# Maximum time a connection can be reused before being closed +max_lifetime = "300s" + +# Redis. +[redis] +# url parameter overrides other redis config options +# url = "redis://user:password@host:port/db" +# If running locally, use `localhost:6379`. +address = "redis:6379" +user = "" +password = "" +db = 0 + +[message] +# Number of workers processing outgoing message queue +outgoing_queue_workers = 10 +# Number of workers processing incoming message queue +incoming_queue_workers = 10 +# How often to scan for outgoing messages to process, keep it low to process messages quickly. +message_outgoing_scan_interval = "50ms" +# Maximum number of messages that can be queued for incoming processing +incoming_queue_size = 5000 +# Maximum number of messages that can be queued for outgoing processing +outgoing_queue_size = 5000 + +[notification] +# Number of concurrent notification workers +concurrency = 2 +# Maximum number of notifications that can be queued +queue_size = 2000 + +[automation] +# Number of workers processing automation rules +worker_count = 10 +# Applies to conversation creation dates. Zero or negative values use 90 days. +time_trigger_lookback_days = 90 + +[ai_agent] +# Number of workers running autonomous AI assistant responses +worker_count = 10 +# Maximum number of AI assistant response jobs that can be queued +queue_size = 1000 +# Maximum number of tool calls the assistant can make while working on one reply. (1-20) +max_steps = 6 +# Conversation messages sent to the model as history. (5-100) +max_history_messages = 30 + +[autoassigner] +# How often to run automatic conversation assignment +autoassign_interval = "5m" + +[webhook] +# Number of webhook delivery workers +workers = 5 +# Maximum number of webhook deliveries that can be queued +queue_size = 10000 +# HTTP timeout for webhook requests +timeout = "15s" + +# SSRF guard for admin-configured outbound URLs (webhooks, OIDC discovery, AI provider and tool calls). +[ssrf] +# Block outbound requests to private/loopback/link-local/cloud-metadata IPs. +# Leave false for self-hosted single-tenant deployments, where internal webhooks, +# OIDC providers and model endpoints on private IPs are legitimate. +# Set true for multi-tenant/hosted deployments where those URLs come from untrusted tenants. +enabled = false +# CIDR ranges allowed to bypass the guard when enabled, for reaching known internal hosts. +# Examples: ["10.0.0.0/8", "192.168.1.10/32", "172.16.0.0/12", "fd00::/8"] +allowed_cidrs = [] + +[conversation] +# How often to check for conversations to unsnooze +unsnooze_interval = "5m" +# How long to keep drafts before deleting them from the database. (e.g. "360h", "48h") +draft_retention_duration = "360h" +# How often to check for offline conversations in database to send continuity emails +continuity_scan_interval = "5m" + +[sla] +# How often to evaluate SLA compliance for conversations +evaluation_interval = "5m" + +# Per-IP rate limits (requests per minute) for unauthenticated endpoints. +# Setting enabled = false disables that rule entirely. + +# Livechat widget APIs. +[rate_limit.widget] +enabled = true +requests_per_minute = 100 + +# Login, password reset and other auth endpoints. +[rate_limit.auth] +enabled = true +requests_per_minute = 30 + +# Public pages and APIs: help center, CSAT, robots.txt/sitemaps. +[rate_limit.public] +enabled = true +requests_per_minute = 100 + +# Public media downloads (uploaded files, avatars). +[rate_limit.media] +enabled = true +requests_per_minute = 300 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2a7ea55 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,120 @@ +# LibreDesk 独立实例(yehwang-ai 工作区) +# 基于官方 https://github.com/abhinavxd/libredesk/blob/main/docker-compose.yml +# 改动:镜像固定版本;宿主端口错开(app 9001 / pg 5433 / redis 6380),避免与本机已有服务冲突。 +services: + # Libredesk app + app: + image: ${LIBREDESK_IMAGE:-libredesk/libredesk:v2.9.0} + container_name: libredesk_app + restart: unless-stopped + ports: + - "127.0.0.1:9001:9000" + environment: + # 首次启动时设置 System 用户密码(后续可用 --set-system-user-password 修改) + LIBREDESK_SYSTEM_USER_PASSWORD: ${LIBREDESK_SYSTEM_USER_PASSWORD:-} + # v2.9.1 起 /health 会在数据库不可用时返回非 200 + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:9000/health || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + networks: + - libredesk + depends_on: + - db + - redis + volumes: + - ./uploads:/libredesk/uploads:rw + - ./config.toml:/libredesk/config.toml:ro + command: [sh, -c, "./libredesk --install --idempotent-install --yes --config /libredesk/config.toml && ./libredesk --upgrade --yes --config /libredesk/config.toml && ./libredesk --config /libredesk/config.toml"] + + # HTTPS 终止反代:libredesk 的 Agent WebSocket 只允许 https 或 localhost 的 Origin, + # 用 http://<局域网IP>:9001 访问后台会一直 “Connecting...”,故加这层 TLS。 + tls: + image: nginx:alpine + container_name: libredesk_tls + restart: unless-stopped + ports: + - "9443:443" + volumes: + - ./tls/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./tls/cert.pem:/etc/nginx/certs/cert.pem:ro + - ./tls/privkey.pem:/etc/nginx/certs/privkey.pem:ro + depends_on: + - app + networks: + - libredesk + + # 业务工具服务:从 crm-new/bridge 裁剪,只保留 4 个 AI 工具(商品/订单/库存/客户资料) + bridge-tools: + build: ./bridge-tools + container_name: libredesk_bridge_tools + restart: unless-stopped + environment: + TOOLS_PORT: 4100 + TOOLS_PUBLIC_BASE_URL: http://bridge-tools:4100 + TOOLS_SHARED_KEY: ${BRIDGE_TOOLS_KEY} + TOOLS_DEV_BYPASS: "false" + RECOMMEND_URL: ${RPC_RECOMMEND_URL} + ORDER_API_URL: ${RPC_ORDER_URL} + INVENTORY_API_URL: ${RPC_INVENTORY_URL} + MEMBER_RPC_URL: ${RPC_MEMBER_URL} + RPC_TIMEOUT_MS: 8000 + PRODUCT_TIMEOUT_MS: 30000 + SITE_SEARCH_URL: https://www.yehwang.com/search.html?q= + DEFAULT_TENANT_ID: 2 + DEFAULT_LANGUAGE_ID: 1 + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + # 仅本机调试用,LibreDesk 通过容器名 bridge-tools:4100 访问 + - "127.0.0.1:4101:4100" + depends_on: + - app + networks: + - libredesk + + # PostgreSQL database + db: + # 本机 docker.io 拉取 postgres:17-alpine 持续失败,改用已缓存的 PG18 基础镜像(与 crm-new 同款,libredesk 已验证兼容) + image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg18} + container_name: libredesk_db + restart: unless-stopped + networks: + - libredesk + ports: + # 仅本机可连,避免与本机 5432 冲突 + - "127.0.0.1:5433:5432" + environment: + POSTGRES_USER: ${POSTGRES_USER:-libredesk} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-libredesk} + POSTGRES_DB: ${POSTGRES_DB:-libredesk} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-libredesk} -d ${POSTGRES_DB:-libredesk}"] + interval: 10s + timeout: 5s + retries: 6 + volumes: + # PG18 官方镜像要求挂载 /var/lib/postgresql(数据在子目录) + - postgres-data:/var/lib/postgresql + + # Redis + redis: + image: redis:7-alpine + container_name: libredesk_redis + restart: unless-stopped + networks: + - libredesk + ports: + # 仅本机可连,避免与本机 6379 冲突 + - "127.0.0.1:6380:6379" + volumes: + - redis-data:/data + +networks: + libredesk: + +volumes: + postgres-data: + redis-data: diff --git a/docs/COMPARE-crm-new.md b/docs/COMPARE-crm-new.md new file mode 100644 index 0000000..be936dc --- /dev/null +++ b/docs/COMPARE-crm-new.md @@ -0,0 +1,133 @@ +# crm-new vs 本实例(yehwang-ai/libredesk)差距与补充方案 + +> 生成时间:2026-10-28 对比基准:crm-new `docker-compose.yml` + 运行中的 `crmnew-*` 容器 + `crmnew-postgres/libredesk` 库实际数据 +> 本实例:`/home/dev/code/yehwang-ai/libredesk`(LibreDesk v2.9.1 + PG18 + Redis + nginx TLS) + +## 0. 一句话结论 + +本实例目前只是「**能登录的 LibreDesk 空壳**」(0 渠道、0 AI 助手、0 工具、0 知识库、0 自动化), +crm-new 是一条**完整跑通的 AI 客服业务链**(三渠道 + bridge 工具 + Twenty CRM + 公网入口 + 一大套配置脚本与上线文档)。 +差的不是「几个开关」,而是**整条链的组件、配置、凭据和运维资产**。 + +## 1. 能力矩阵(均为实测,不是看代码猜的) + +| 能力 | crm-new | 本实例 | 缺口 | +| --- | --- | --- | --- | +| 容器编排 | LibreDesk + Twenty(server/worker) + bridge + imap-proxy + public-gateway + cloudflared + PG + Redis(9 个) | LibreDesk + PG + Redis + nginx TLS(4 个) | Twenty、bridge、imap-proxy、public-gateway、cloudflared | +| 渠道 inbox | **3 个**:Website(livechat) / Email 163 / WhatsApp | **0** | 全部 | +| 邮件通知 | `notification.email.*`:smtp.163.com:465 TLS,已启用 | 无 | 全部 | +| Live chat | inbox + widget + 身份锚定方案(SITE_JWT / IDENTITY_*,bridge 侧) | 无 | 全部 | +| WhatsApp | inbox + 6 个模板 + Meta Cloud API 凭据 + 回调 | 无 | 全部 | +| AI provider | 2 个已配:completion=`qwen-plus-latest`(DashScope, openai 兼容)、embedding=`text-embedding-v4` | 2 行**空壳**默认记录,api_key 为空 | 填 key/模型/默认项 | +| AI assistant | 1 个(B2B 助手;max_turns=8;handoff 开;guardrails/expectation/tone) | 0 | 全部 | +| AI tools | **11 个** HTTP 工具:search_products / search_orders / search_inventory / search_kb / search_email_history / collect_info / send_verification_code / verify_email_code / handoff_to_human / create_opportunity / get_customer_info | 0 | 全部 | +| 知识库 | 帮助中心 1 个(`yehwang-help`)+ **108 篇已发布文章**;bridge 侧 pgvector RAG(快照 594 chunks) | 0 | 全部 | +| 自动化规则 | 3 条:AI 分配、邮件→Wholesale、WhatsApp→Wholesale | 0 | 全部 | +| 团队/坐席 | 1 个 team(Wholesale Support,时区 Asia/Shanghai)+ 3 agent + 2 角色;库内 27 用户(含 contact/visitor 历史) | 1 个用户(System)+ 2 个默认角色 | 团队、坐席、角色细分 | +| Webhook | 1 条:Bridge AI → `http://bridge:4100/libredesk/webhook` | 0 | 全部 | +| 公网入口 | public-gateway(只放 WhatsApp webhook + 帮助中心)+ cloudflared quick tunnel;root_url 指向临时 trycloudflare 域名(隧道重启即变,不可当长期入口) | nginx 自签 TLS(仅内网 9443);`root_url=http://localhost:9000` | 域名、证书、白名单反代 | +| 数据库 | PG 3 库(libredesk / twenty / bridge)+ pgvector | 单库 libredesk | 若接 Twenty/bridge 需建库 | +| 网络 | 额外接入外部网络 `spug-net`,bridge 直连 yehwang RPC(9503/9505/9506/9509) | 独立 bridge 网络 | 网络互联 | +| 配置脚本 | bootstrap.sh、render-config.js、configure-ai/email、add-email-inbox、add-whatsapp-inbox、register-tools、import-kb-helpcenter、set-public-url、set-wa-webhook、smoke.sh、e2e-*、eval-*、kb-* 等约 25 个 | 无 | 全部 | +| 文档 | architecture / api-mapping / environments / go-live / runbook / human-routing / libredesk-setup / migration-plan / plan-a / deploy-wangliang | 只有一个 README | 上线、运维、切流文档 | +| 备份 | runbook 有 pg_dump / Twenty 文件存储 / KB 快照命令 | 只有我这次升级时手工做的两次 dump,**无定时任务** | 定时备份 + 恢复演练 | + +**基础配置本身就有错**(两边都有,但本实例更明显): + +- 本实例 `app.root_url = http://localhost:9000`(邮件/帮助中心链接会指向 localhost) +- 本实例 `app.timezone = Africa/Cairo`(荷兰业务差 1 小时) +- crm-new 的 `app.timezone = ""`、`root_url` 是已失效的临时隧道域名 + +## 2. 建议补充方案(分 6 期,按依赖排序) + +### P0 正确性与安全(约 0.5h,先做) + +| 事项 | 说明 | +| --- | --- | +| root_url 改为 `https://192.168.31.61:9443` | 影响邮件通知、帮助中心、widget 链接 | +| timezone 改 `Europe/Berlin`(UI)或 `Europe/Amsterdam`(API) | 两者规则等价,见时区分析 | +| 建个人管理员账号,改掉 System 密码 | 避免共用超级账号 | +| 备份脚本落地(pg_dump + uploads,保留 7/30 天)+ 恢复演练一次 | 目前没有定时备份 | +| (可选)`9001` 只监听 `127.0.0.1` | 减少明文暴露面 | + +验收:设置页不再出现 localhost/Cairo;备份能被 `pg_restore -l` 读出;个人账号可登录。 + +### P1 渠道最小闭环(约 1–2h) + +1. **Live Chat inbox + widget**(Website 渠道),把这套的 widget 代码嵌到目标站点; +2. **Email inbox**:先定用哪个邮箱 —— 163 需要 imap-id-proxy(LibreDesk 不发 IMAP ID);建议换企业邮箱/自建 SMTP 省掉代理; +3. 配置 `notification.email.*`(OTP/通知邮件走同一 SMTP); +4. 建 **Team**(Wholesale Support)+ 分配规则(邮件/WhatsApp 转人工); +5. 建 **Help Center**,导入 crm-new 的 108 篇文章(可参考 `import-kb-helpcenter.js`)。 + +验收:widget 能落会话;邮件收发一轮打通;帮助中心可搜索;新会话按规则进人工团队。 + +### P2 AI 能力(约 2–4h) + +1. 填 AI provider(复用 crm-new 的 DashScope key,或新申请),确认默认 provider; +2. 配 AI assistant(instructions / guardrails / handoff / languages / max_turns); +3. 知识库二选一: + - **A**:用 LibreDesk 内置 KB/snippets + embedding(新栈自包含,但要重新灌数据); + - **B**:沿用 bridge 的 pgvector RAG(数据现成,但依赖 bridge 运行); +4. Live Chat 开启 AI 接管,跑 preview/retrieval 验证与转人工。 + +验收:AI 回答能引用知识库;越界时按 guardrails 转人工;有可复跑的检索评测。 + +### P3 业务工具 + CRM(约 0.5–1d,取决于复用方式) + +三条路线(需拍板): + +- **A. 复用 crm-new 的 bridge**:新实例 webhook 指向同一个 `crmnew-bridge:4100`,工具注册表复制一份;改动最小,但两套环境共享 bridge/数据; +- **B. 新建 bridge 实例**:复制代码 + 独立 `.env`(RPC 地址、DashScope、SMTP、Redis 库分开),隔离好但维护两份; +- **C. 只接 Twenty CRM**:不做商品/订单工具,仅联系人/商机同步,工作量最小。 + +再补:注册 11 个工具(`register-tools.js` 适配新实例 API key)、OTP 邮件、输出护栏与质量指标(bridge 自带,复用即得)。 + +验收:搜商品/查订单/建商机一条 e2e 跑通。 + +### P4 公网渠道(约 0.5–1d,依赖域名/凭据) + +1. 固定域名 + Let's Encrypt(推荐)或 Cloudflare 命名隧道;**quick tunnel 不可持续**(crm-new 的 root_url 就是临时域名,cloudflared 一重启就变); +2. WhatsApp:Meta Cloud API 凭据(WABA/phone number/token/verify token)+ 回调地址 + 模板; +3. 复用 public-gateway 白名单思路(只暴露 WhatsApp webhook + 帮助中心,其余 404); +4. root_url 与帮助中心域名统一。 + +验收:Meta 回调校验通过;公网只可达白名单路径;帮助中心/客服入口 HTTPS 正常。 + +### P5 运维与上线(约 2–4h) + +- smoke.sh / e2e 脚本适配新实例;健康检查(已有 `/health`)+ 日志/告警; +- 定时备份 + 恢复演练纳入 runbook; +- 上线文档(范围、切流、验收、回滚)——可沿用 crm-new 的 go-live/runbook 结构; +- 版本策略:跟随稳定版(当前 v2.9.1),AI/WhatsApp 属较新功能,建议低流量验证。 + +## 3. 需要你拍板的决策点 + +1. **本实例定位**:独立演示/联调环境,还是将来生产?(决定域名、证书、备份等级) +2. **和 crm-new 的关系**:并存做隔离验证,还是最终替代它? +3. **邮件渠道**:继续 163(要 imap-proxy),还是换正式邮箱? +4. **AI key**:复用 crm-new 的 DashScope key,还是新申请?知识库走 LibreDesk 内置还是 bridge RAG? +5. **Twenty / bridge**:复用现有实例(A 路线),还是新建(B 路线),或只接 CRM(C 路线)? +6. **公网**:有没有可用域名?走 Cloudflare 隧道还是自有 nginx + 证书? +7. **多站点**:是否要搬 crm-new 的 `sites.json` 多站点路由(www / us + 商品搜索兜底)? + +## 4. 不建议照搬的部分 + +- 旧栈 `crm/`(chatwoot / twenty:flywheel)不要动; +- crm-new 的 quick tunnel 与临时域名(不可持续); +- 全量商品页 sitemap(www 32,691 / us 22,625 URL)——除非要做商品搜索兜底; +- 27 个历史用户(含 visitor/contact 测试数据)——新实例只建必要坐席即可。 + +## 5. 风险提示 + +- **163 IMAP 限制**:必须靠 imap-id-proxy 才能 SELECT,换邮箱可去掉这层; +- **凭据复制**:把 crm-new 的 key/token 复制到新实例前先确认责任边界,建议新实例用独立凭据; +- **两套并存**:本机已跑 20+ 容器(内存 14G,当前用 6G),再加 Twenty 一套会明显吃资源,需先规划; +- **RC/beta 功能**:WhatsApp、AI 相关功能较新,上线前跑一轮 e2e; +- **公网入口**:没有固定域名则 WhatsApp 回调无法长期用。 + +## 6. 推荐路径 + +先做 **P0 + P1(半天内)** 得到「人工渠道 + 帮助中心」可用闭环; +确认本实例定位后,再决定 P2 AI 是否立刻上、P3/P4 是否复用 crm-new 资源。 +不建议一上来复制整套栈——两套并存的维护成本和数据一致性风险不划算。 diff --git a/docs/E2E-livechat.md b/docs/E2E-livechat.md new file mode 100644 index 0000000..e4adca6 --- /dev/null +++ b/docs/E2E-livechat.md @@ -0,0 +1,40 @@ +# Live Chat 端到端验证(2026-10-28) + +环境:LibreDesk v2.9.1(https://192.168.31.61:9443)+ bridge-tools(4 个 AI 工具) +Inbox:Website(livechat,uuid `81b679b0-33fb-4796-9f83-dcc0f43231cc`) +自动化:新会话 → 分配给 Yehwang Assistant(user_id=2) + +## 验证流程与结果 + +| 步骤 | 结果 | +| --- | --- | +| 1. widget 匿名发起会话(`POST /api/v1/widget/chat/conversations/init`,第一条消息=退货政策) | 200,会话创建 | +| 2. 自动化规则触发 | 会话 assigned_user_id=2(Yehwang Assistant) | +| 3. AI 自动回复 | 约 5 秒返回,内容与知识库一致(非质量问题不退,质量问题 7 天内),带 sources | +| 4. 访客问订单状态 | AI 要求邮箱验证 | +| 5. 访客给邮箱 | AI 调用 `set_contact_email` + `send_email_verification`(OTP 走 163 SMTP) | +| 6. 从 Redis 取码(`ai:otp:pending:`)并回复 | AI 调用 `check_email_verification`,验证通过 | +| 7. 重试 `search_orders` | bridge-tools 日志:`tool=search_orders verified=true ok=true`;回复「没有找到该邮箱的订单」 | +| 8. 会话状态 | Open,contact email 已更新 | + +## 发现的问题(已处理) + +在第 4 步之前,模型曾自行声称「已把验证码发到 example@example.com」——这是一个**编造的邮箱和编造的动作**(当时联系人的邮箱为空)。 +已在 assistant instructions 里加护栏: + +> Never invent or assume the customer's email address: ask for it. Only say a verification code or email was sent after the send_email_verification tool succeeds. + +仍建议上线前用真实客户跑一轮回归,重点看「无邮箱时是否会索要邮箱」以及「是否只在工具成功后声称已发送」。 + +## 复现命令 + +widget 匿名发起: + +```bash +curl -sk -X POST https://localhost:9443/api/v1/widget/chat/conversations/init \ + -H 'Host: 192.168.31.61:9443' -H 'Content-Type: application/json' \ + -H 'X-Libredesk-Inbox-ID: 81b679b0-33fb-4796-9f83-dcc0f43231cc' \ + -d '{"message":"What is your return policy?"}' +``` + +发送后续消息需 `Authorization: Bearer ` + `X-Libredesk-Inbox-ID`。 diff --git a/docs/PLAN-bridge-tools-only.md b/docs/PLAN-bridge-tools-only.md new file mode 100644 index 0000000..ad35477 --- /dev/null +++ b/docs/PLAN-bridge-tools-only.md @@ -0,0 +1,147 @@ +# 方案:bridge 只保留 tools,其余用 LibreDesk 内置能力 + +> 2026-10-28 评估。依据:LibreDesk v2.9.1 源码(`internal/aiagent/`、`internal/ai/tools.go`、`schema.sql`)+ crm-new 运行数据。 +> 结论:**可行**,而且 LibreDesk 内置能力比预期多——bridge 可以从 11 个工具收缩到 4–6 个业务工具。 + +## 0. 已确认的决策(2026-10-28) + +| 决策点 | 结论 | +| --- | --- | +| bridge-tools 部署 | **方案 A**:并入本实例 compose(服务名 `bridge-tools`,同 `libredesk` 网络) | +| `create_opportunity`(Twenty) | **先不留** | +| `search_email_history` / `collect_info` | **先删** | +| 程序化输出护栏 | **先接受降级**(后续用 LibreDesk webhook 外采质量指标) | +| `get_customer_info` | **保留**(业务需要) | +| 保留工具清单 | `search_products` / `search_orders` / `search_inventory` / `get_customer_info` | + +## 1. 关键证据:LibreDesk v2.9.1 已经内置了什么 + +AI agent 每次回复时自动注册的内置工具(`internal/aiagent/worker.go:290-314`): + +| 内置工具 | 作用 | 对应 crm-new bridge 的什么 | +| --- | --- | --- | +| `search_knowledge_base` | 检索 snippets + 帮助中心文章(向量),支持引用计数 | `search_kb`(bridge 的 pgvector RAG) | +| `handoff`(`handoff_enabled`) | 转人工(`fallback_team_id`) | `handoff_to_human` | +| `get_previous_conversations` | 拉该客户近 N 天其他会话 | `search_email_history`(部分覆盖) | +| `send_email_verification` | 发 OTP 到联系人邮箱(走通知邮箱 SMTP;Redis 存码,10 分钟有效,30 分钟验证态) | `send_verification_code` | +| `check_email_verification` | 校验 OTP,3 次尝试上限、重发上限 | `verify_email_code` | +| `set_contact_email` | 访客改邮箱后再发码 | (bridge 无对应,新增能力) | +| `resolve` | 结束时给会话打结果 | 无 | + +其他内置: + +- **工具调用上下文自动注入**(`internal/ai/tools.go:250-270`):`X-Libredesk-Contact-Id` / `-External-Id` / `-Type` / `-Email` / `-Verified` / `X-Libredesk-Conversation-UUID` / `X-Libredesk-Inbox-Id` —— bridge 的多站点路由(sites.json)和身份判断可以直接用这些头,不再需要自己发验证 token。 +- **工具级验证开关** `ai_tools.requires_verification`:不验证的客户调私有数据工具会被拦,并引导模型走内置 OTP 流程(`tools.go:186`)。 +- **工具鉴权** `ai_tools.auth`(自定义 header,支持加密存储):bridge 的 shared key 直接配在工具上。 +- **AI 助手配置**:instructions / guardrails / expectation / tone / response_length / max_turns / fallback_team_id / handoff_enabled / citations_enabled / languages / enabled。 +- **知识库**:`ai_knowledge_base`(snippet 类)+ 帮助中心文章都会作为 embedding 源(`internal/ai/embedsource.go`)。 +- **Widget 身份**:inbox secret 的 JWT 校验(`cmd/chat.go`),站点后端签 JWT 即可,不需要 bridge 的 identity 服务。 +- **通知邮件 / OTP 发信**:`notification.email.*`(crm-new 里就是 163 SMTP)。 + +## 1.5 落地进度 + +| 期 | 状态 | 说明 | +| --- | --- | --- | +| P0 root_url / timezone | ✅ 完成 | `root_url=https://192.168.31.61:9443`、`timezone=Europe/Berlin`(API 已写入并回读) | +| P0 SMTP / embedding key | ✅ 完成 | 复用 crm-new 的 163 SMTP(smtp.163.com:465 TLS)和 DashScope key;completion 实际用 **qwen-max**(见下) | +| P2 瘦身 bridge | ✅ 完成 | `bridge-tools/`(零第三方依赖,4 个工具,9 个单测通过),已并入 compose 并 healthy | +| P3 工具注册 | ✅ 完成 | LibreDesk `ai_tools` 已注册 4 个,`search_orders`/`get_customer_info` 带 `requires_verification` | +| P1 帮助中心/assistant | ✅ 完成 | Team「Wholesale Support」+ 帮助中心(108 篇,已嵌入)+ Assistant「Yehwang Assistant」(4 个工具、fallback team、handoff/citations 开);预览已能基于文章回答并返回 sources | +| P4 渠道 | 🟡 部分完成 | **Website livechat 已建 + 自动化分配到 Assistant + 端到端验证通过**(含 OTP → search_orders);email / WhatsApp 待做 | + +### ⚠️ 模型选择的关键发现 + +`qwen-plus-latest` / `qwen-plus` 在 LibreDesk 这套较长的 system prompt 下**不会调用工具**(实测 tool_calls=0),直接凭模型自身知识作答,还会编造 `[[cite:45]]` 这种不存在的引用(第一次预览回答「30 天退货」,而知识库实际是「非质量问题不退,质量问题 7 天内」)。 +换成 **`qwen-max`** 后工具调用正常(sources 命中 5 篇,score 0.33–0.61),回答与知识库一致。因此 completion 配置为 `qwen-max`;如果以后换模型,务必用「退货政策/预购时效」这类问题验证是否会调用 `search_knowledge_base`。 + +## 2. bridge 收缩后保留什么 + +| crm-new 现有工具 | 处理 | 说明 | +| --- | --- | --- | +| `search_products` | **保留** | 业务 RPC 商品搜索,LibreDesk 无替代 | +| `search_orders` | **保留**(`requires_verification=true`) | 私有数据,用内置 OTP 把关 | +| `search_inventory` | **保留** | 库存/ETA | +| `get_customer_info` | 保留(可选) | 若 Twenty 客户资料要查;也可并进 orders | +| `create_opportunity` | 保留(可选) | Twenty 商机写入,LibreDesk 无替代;不做 CRM 则去掉 | +| `search_kb` | **删除** | 用内置 `search_knowledge_base`;108 篇帮助文章已在 LibreDesk 里 | +| `search_email_history` | 先删 | 内置 `get_previous_conversations` 覆盖「本系统内历史」;旧邮件库不再接入 | +| `collect_info` | 先删(可选保留) | lead 槽位采集改为 assistant 指令 + 联系人自定义属性;要做销售漏斗再保留 | +| `handoff_to_human` | **删除** | 内置 handoff + fallback_team_id | +| `send_verification_code` | **删除** | 内置 OTP 工具 | +| `verify_email_code` | **删除** | 内置 OTP 工具 | + +**结果:bridge = 一个只暴露 3–5 个 HTTP 工具的瘦服务**(+ 多站点配置、RPC 客户端、Twenty 客户端、shared key 鉴权、健康检查)。 + +## 3. 明确会被「降级」的能力(要接受或后补) + +| 现有能力 | 用内置后的状态 | 建议 | +| --- | --- | --- | +| 程序化输出护栏(URL 白名单、价格/库存数字校验、幻觉重写) | **没有等价物**;只有 assistant 的 `guardrails` 提示词约束 | 先接受(低流量验证);需要时用 LibreDesk webhook → 独立质量服务补 | +| 质量飞轮 / 指标 / 日报 / 引用检查 / A-B | 无等价物;LibreDesk 只有基础报表 | 后续用 webhook 采集到外部(bridge 加一个 consumer,不做实时改回复) | +| 旧 bridge Hybrid RAG(parent-child + 向量 + 全文 + RRF + rerank) | 退化为向量 top-k(snippets + 文章) | 用真实问题跑检索评测对比;不满意可把 snippet 切细或补 FAQ | +| 邮件历史检索(旧 `kb_mail_docs`) | 只覆盖 LibreDesk 内会话 | 需要历史邮件时再做离线 ingest | +| lead 槽位(Redis 跨轮) | 没有 | 用 assistant instructions + 自定义属性;或保留 `collect_info` 工具 | + +## 4. 实施分期 + +### P0 前置(0.5h) + +- 配置 `notification.email.*`(OTP + 通知都依赖它,必须有可用 SMTP); +- AI provider:completion + **embedding**(内置 KB 检索依赖 embedding,必须配); +- 修 `root_url`、`timezone`(见时区方案)。 + +### P1 LibreDesk 内置能力配置(2–4h) + +1. 帮助中心 + 108 篇文章(从 crm-new 导出后导入,或按原源重灌); +2. 需要时补 snippets(FAQ 短答案); +3. AI assistant:instructions / guardrails / tone / max_turns / languages / citations / fallback_team_id / handoff_enabled; +4. 团队(Wholesale Support)+ 自动化规则(AI 分配、邮件/WhatsApp 转人工)。 + +验收:AI 能基于帮助中心回答并带引用;越界/复杂问题按 fallback_team 转人工;OTP 流程能发码、验码(先用 livechat 访客验证)。 + +### P2 瘦身 bridge(0.5–1d) + +- **新建目录 `bridge-tools/`(从 `crm-new/bridge` 裁剪,不改 crm-new 本身)**,保留: + `server.js`(只留工具路由 + healthz)、`tools.js`(裁剪)、`rpc.js`、`product-query.js`、`sitemap.js`、`opportunity.js` / `twenty.js`(若保留)、`redis.js`、`auth.js`、`config.js`、`security.js` + 对应测试; +- 删除:agent.js、prompts.js、llm*.js、rag.js、kb-*.js、embedding.js、reranker.js、verification.js、smtp.js、identity.js、jwt.js、libredesk-webhook.js、metrics/quality/citation-checker/output-guard/flywheel/audit/experiments/trace 等; +- 依赖精简:PG 可去掉(若不做 lead/质量库),Redis 保留(RPC 缓存/限流); +- 单测裁剪后必须 `npm test` 通过。 + +验收:`/healthz` 正常;5 个工具逐个用 curl + shared key 调通;容器内存/镜像明显变小。 + +### P3 工具注册到 LibreDesk(2–4h) + +- 适配 `register-tools.js`:URL 指向 `bridge-tools:4100`,`auth` 带 shared key header; +- `search_orders` / `create_opportunity` 设 `requires_verification=true`; +- 绑定 assistant 的 tool allowlist; +- 多站点:`X-Libredesk-Inbox-Id` 映射 sites.json(不需要改协议,LibreDesk 已经带)。 + +验收:livechat 里问「查订单」→ 触发 OTP → 验码后返回订单;问商品 → 返回价格/库存/链接。 + +### P4 渠道(2–4h) + +- Live Chat inbox + widget(站点用 inbox secret 签 JWT 做身份锚定); +- Email inbox(决定用 163 走 imap-proxy,还是换企业邮箱直连); +- WhatsApp(依赖公网域名/隧道,单独排期)。 + +### P5 公网/运维/质量(按需) + +- 固定域名 + 证书 + public-gateway 白名单; +- 备份定时化、监控、smoke/e2e、上线文档; +- 若需要质量护栏:LibreDesk webhook → 轻量质量服务(不改 AI 回复,只记指标)。 + +## 5. 风险与前提 + +- **SMTP 必须可用**:内置 OTP 依赖通知邮箱,livechat 访客验证走它;没有就会卡住「查订单」这类工具。 +- **embedding provider 必须配**:内置 KB 检索是向量检索,缺 key 时 `search_knowledge_base` 不可用。 +- **RPC 网络**:bridge-tools 仍需要访问 yehwang RPC(crm-new 是靠外部网络 `spug-net`),新 compose 要接同一网络或走隧道。 +- **内置 OTP 的边界**:只对「email 渠道会话」或「非 contact 类型联系人(访客)」提供;已存在且非访客的 contact 不会自动有 OTP 工具(`worker.go:308`),要确认业务场景。 +- **检索质量下降**:内置 top-k vs 旧 hybrid;上线前用 20–30 个真实问题对比命中率。 +- **两套并存**:bridge-tools 与 crm-new bridge 可共用 RPC/Redis 配置,但建议独立 redis db 和独立 shared key。 + +## 6. 需要拍板 + +1. `create_opportunity` / Twenty 商机要不要保留?(决定 bridge 留 3 个还是 5 个工具) +2. `search_email_history`、`collect_info` 先删还是保留? +3. 程序化输出护栏:先接受降级,还是一开始就按 webhook 方案补? +4. bridge-tools 部署位置:同一台机新 compose,还是并到本实例 compose? diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..fe53461 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# LibreDesk 定时备份:Postgres dump + uploads + 密钥配置,保留 14 天。 +# 用法:./scripts/backup.sh +# cron:0 3 * * * cd /home/dev/code/yehwang-ai/libredesk && ./scripts/backup.sh >> logs/backup.log 2>&1 +set -euo pipefail + +cd "$(dirname "$0")/.." + +TS="$(date +%Y%m%d-%H%M%S)" +BACKUP_DIR="backups" +LOG_PREFIX="[backup $(date -Is)]" + +mkdir -p "$BACKUP_DIR" logs + +DB_FILE="$BACKUP_DIR/db-$TS.dump" +UPLOADS_FILE="$BACKUP_DIR/uploads-$TS.tar.gz" +CONFIG_FILE="$BACKUP_DIR/config.toml.$TS" +ENV_FILE="$BACKUP_DIR/env.$TS" + +POSTGRES_USER="${POSTGRES_USER:-libredesk}" +POSTGRES_DB="${POSTGRES_DB:-libredesk}" + +echo "$LOG_PREFIX start" + +# 1) 数据库(custom format,可直接 pg_restore) +docker compose exec -T db pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB" > "$DB_FILE" + +# 2) 上传文件 +tar -czf "$UPLOADS_FILE" -C . uploads + +# 3) 配置文件(含加密密钥 / DB 密码,保持 600) +cp -p config.toml "$CONFIG_FILE" +cp -p .env "$ENV_FILE" +chmod 600 "$CONFIG_FILE" "$ENV_FILE" + +# 4) 校验 dump:先看 PGDMP 头,再用 pg_restore 读出 TOC(容器内 bind mount,避免 stdin 不可 seek) +if [ "$(head -c 5 "$DB_FILE")" != "PGDMP" ]; then + echo "$LOG_PREFIX ERROR: dump 文件头不是 PGDMP:$DB_FILE" >&2 + exit 1 +fi +TOC_COUNT="$(docker run --rm -v "$PWD/$BACKUP_DIR:/b:ro" "${VERIFY_IMAGE:-pgvector/pgvector:pg18}" pg_restore -l "/b/$(basename "$DB_FILE")" | grep -cE '^[0-9]+;' || true)" +if [ "${TOC_COUNT:-0}" -lt 100 ]; then + echo "$LOG_PREFIX ERROR: dump 校验失败(TOC 条目=$TOC_COUNT),保留文件供排查:$DB_FILE" >&2 + exit 1 +fi + +# 5) 保留 14 天 +find "$BACKUP_DIR" -maxdepth 1 -type f \ + \( -name 'db-*.dump' -o -name 'uploads-*.tar.gz' -o -name 'config.toml.*' -o -name 'env.*' \) \ + -mtime +14 -delete + +echo "$LOG_PREFIX ok db=$(du -h "$DB_FILE" | cut -f1) uploads=$(du -h "$UPLOADS_FILE" | cut -f1) toc=$TOC_COUNT" diff --git a/scripts/ensure-tunnel.sh b/scripts/ensure-tunnel.sh new file mode 100755 index 0000000..5988af1 --- /dev/null +++ b/scripts/ensure-tunnel.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# 确保 yw-dev DB 隧道在线(bridge-tools 依赖它)。 +# 幂等:隧道已完整转发时直接退出;否则调用 yw-db-tunnel start 并记日志。 +# cron:@reboot sleep 60; .../ensure-tunnel.sh +# */5 * * * * .../ensure-tunnel.sh +set -uo pipefail +export PATH="/home/dev/.local/bin:/usr/local/bin:/usr/bin:/bin" + +TUNNEL="/home/dev/.local/bin/yw-db-tunnel" +LOG="/home/dev/code/yehwang-ai/libredesk/logs/tunnel.log" +mkdir -p "$(dirname "$LOG")" + +status_out="$("$TUNNEL" status 2>/dev/null || true)" +if printf '%s' "$status_out" | grep -q '隧道进程: 运行中' \ + && ! printf '%s' "$status_out" | grep -q '未转发'; then + exit 0 +fi + +echo "[$(date -Is)] tunnel down, starting" >> "$LOG" +if "$TUNNEL" start >> "$LOG" 2>&1; then + echo "[$(date -Is)] tunnel ok" >> "$LOG" + exit 0 +fi +echo "[$(date -Is)] tunnel start FAILED (see log above)" >> "$LOG" +exit 1 diff --git a/tls/nginx.conf b/tls/nginx.conf new file mode 100644 index 0000000..48a41fb --- /dev/null +++ b/tls/nginx.conf @@ -0,0 +1,44 @@ +# LibreDesk 局域网 HTTPS 反代(9443) +# 原因:libredesk v2.9.x 的 Agent WebSocket 只接受 https 或 localhost 的 Origin, +# 用 http://<局域网IP>:9001 打开后台,WS 会被 403 拒绝,界面一直 “Connecting...”。 +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/certs/cert.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + # 基础安全头(自签证书,不启用 HSTS) + ssl_protocols TLSv1.2 TLSv1.3; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + # X-Frame-Options 由应用自身设置(SAMEORIGIN),避免重复 + + client_max_body_size 100M; + + location / { + proxy_pass http://app:9000; + proxy_http_version 1.1; + + # WebSocket + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + # 保留原始 Host(含 9443 端口),libredesk 的 WS Origin 校验依赖它 + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + # libredesk 用 X-Client-IP 做限流/审计,必须设置 + proxy_set_header X-Client-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + + # 长连 WS 不要过早断开 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +}