From 4afd5f91438c1a2b857295b4d27ca05fb894e33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E5=AE=87=E5=AE=81?= Date: Fri, 16 Oct 2026 14:10:10 +0700 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 +- README.md | 28 +- config/development.env | 11 +- config/production.env | 4 +- config/test.env | 4 +- public/static/common/web-sdk-v1.2.0.js | 28 - scripts/browser-smoke.ts | 18 +- scripts/catalog-react-smoke.ts | 4 +- scripts/native-site-smoke.ts | 61 +-- scripts/native-ssr-smoke.ts | 10 - scripts/playback-smoke.ts | 85 +--- scripts/tracker-smoke.ts | 170 ------- src/components/analytics/TrackerProvider.tsx | 178 ------- src/components/analytics/tracker.ts | 479 ------------------ src/components/catalog/Promotions.tsx | 22 +- src/components/layout/SiteShell.tsx | 21 +- src/components/player/VideoPlayer.tsx | 11 +- src/components/player/usePlaybackAnalytics.ts | 123 ----- src/components/providers/SiteDialogs.tsx | 32 -- src/features/catalog/DiscoveryPages.tsx | 25 - src/features/content/CompliancePage.tsx | 2 +- src/features/content/DMCAPage.tsx | 2 +- src/features/playback/DramaWatchPage.tsx | 23 +- src/features/playback/PlaybackStage.tsx | 5 - src/features/playback/SeriesWatchPage.tsx | 2 - src/features/playback/Shared.tsx | 4 - src/features/playback/model.ts | 21 - src/lib/api.ts | 130 +---- src/lib/routes.ts | 2 +- src/lib/tracker.ts | 10 - src/server/config.ts | 6 +- src/server/pages/playback.ts | 9 +- src/server/services/shared.ts | 14 +- src/server/upstream.ts | 156 ------ tests/brand.test.ts | 4 +- tests/catalog-react.test.tsx | 5 +- tests/pages.test.ts | 4 +- tests/playback-react.test.ts | 26 - tests/proxy.test.ts | 14 +- tests/react-rendering.test.tsx | 4 +- tests/routes.test.ts | 10 +- tests/sdk-system.test.ts | 81 --- 42 files changed, 89 insertions(+), 1765 deletions(-) delete mode 100644 public/static/common/web-sdk-v1.2.0.js delete mode 100644 scripts/tracker-smoke.ts delete mode 100644 src/components/analytics/TrackerProvider.tsx delete mode 100644 src/components/analytics/tracker.ts delete mode 100644 src/components/player/usePlaybackAnalytics.ts delete mode 100644 src/lib/tracker.ts delete mode 100644 tests/sdk-system.test.ts diff --git a/AGENTS.md b/AGENTS.md index 5b0c1b9..7698cac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,19 +13,19 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex - 本仓库是花魁短剧原生 Next.js App Router + 严格 TypeScript 项目,原站参考目录为 `/Users/mac/Desktop/work/web/jha-1045-hgdjnet-web`。页面必须使用 React/TSX;不要恢复 Go/HTML 模板解析或 `dangerouslySetInnerHTML` 页面适配器。 - 保持原站功能、PC/H5 界面和完整 SEO 行为:metadata、JSON-LD、canonical、分页、robots、llms、sitemap、站点验证、IndexNow,以及重定向/404/503 的状态和索引规则。 - 保留服务端 API 转发、请求头和 Cookie 透传、游客会话恢复、多条 Set-Cookie 和私有响应禁止缓存。不要把鉴权或 SDK 私密配置泄露到公开数据、日志或图片 CDN。 -- 页面按 `src/features/{catalog,account,content,playback}` 组织;页头/页尾、卡片、图片解密、播放器、鉴权、支付及埋点优先复用 `src/components`、`src/lib`、`src/server` 中的公共实现。 +- 页面按 `src/features/{catalog,account,content,playback}` 组织;页头/页尾、卡片、图片解密、播放器、鉴权及支付优先复用 `src/components`、`src/lib`、`src/server` 中的公共实现。 - 业务 CSS 统一在 `src/styles/{foundations,components,pages,overrides}`,通过 TSX 顶层导入并由 Next 编译为哈希资源;不要恢复 `/static/css` 或手工 stylesheet 链接。业务类名使用 `hk-` 与语义组件名称,xgplayer 第三方类名保留;页面样式使用零特异性 `html[data-hk-page-styles]` 隔离,PWA 最后加载。客户端切页由 `PagePresentation` 同步作用域与页面 body class,不可覆盖滚动锁/PWA/临时状态。 - 2026-10-14 用户明确重新启用语义新路径并同步全部引用,覆盖此前回退要求。代码入口和对外 URL 使用 `/dramas`、`/drama`、`/series/watch`、`/collections`、`/collection`、`/account`、`/navigation` 等;旧有效页面 GET/HEAD 一次 301 到新路径,规则统一在 `src/lib/routes.ts`。页面文件使用 `HomePage`、`BrowsePages`、`DiscoveryPages`、`DramaWatchPage`、`SeriesWatchPage`、`SettingsPage`、`SiteNavigationPage`,服务端播放模块为 `src/server/pages/playback.ts`。站内链接、metadata、分页、JSON-LD、llms、安装默认入口使用新路径,API/媒体签名/外链保持原合同与编码。`npm run routes:export -- --admin ../hkdj-web-admin` 从实际路由生成全部新旧对照及后台路径清单,`routes:check` 核验无漂移;后台导航分类支持选择静态入口或自由输入,参数模板不作为可选链接。后端 `hkdj-web-server` 的 h5path、sitemap 与 SEO 推送源同步新路径,Next 仍原字节服务 XML,不在请求中改写生成结果。 - robots/llms/sitemap 完整遵循参考 Go 合同:robots/llms 读取公共配置并使用原永久域名/代理 Host 规则,robots 保留四个磁盘候选与内嵌文本的优先级及原字符串处理。sitemap 只服务后端生成的文件;开发 `SITEMAP_DIR=./template/pages/sitemaps`,测试/生产 `SITEMAP_DIR=../template/pages/sitemaps`,仅作为部署目录映射。测试 Node 工作目录 `/server/hkdj_net/h5/next`,真实产物在父级 `h5/template/pages/sitemaps`,生产遵循相同目录关系;索引其次读取 `./template/pages/sitemap.xml`。请求不读取远端 sitemap、不生成静态回退或合成索引、不改写 XML;缺失/非法文件走原完整 404 页面及共享数据加载。开发仅可显式 `npm run sitemap:sync` 通过可信 SSH 只读同步索引引用的完整分片,保留原字节/mtime,不修改远端、不删除未知文件;开发 XML 忽略提交,页面 trace 排除;Next 16.4 Node Proxy trace 仍可能带入原始 standalone,必须通过 bundle 脚本在隔离 dist 精确清理 XML,并逐层 lstat 拒绝符号链接父目录,保留 robots/key/未知文件和后端源目录。导航模式与 Go `DHInit` 一致,不注册抓取文件/验证/API/媒体路由。发布保留后端 XML,持续写入的原目录应外置挂载,不能随前端包清空。 - 成功抓取文件通过原生 Route Handler 返回;Proxy 仅选择成功出口或准备并改写完整 TSX 404,不能直接从 Proxy 输出文件,否则 Next 会过滤 Content-Length。保留 Go ServeFile 的 MIME、原字节、Last-Modified、Content-Length、Accept-Ranges、304/412/206/416、If-Range/多段 Range 和 HEAD;Next compress=false 避免框架自动压缩改变文件响应,外层代理可沿用原传输配置。 - 播放器通过 npm 使用 `xgplayer` 与 `xgplayer-hls`;不要恢复 CDN 脚本版 DPlayer/Hls/Swiper 或旧业务脚本。 - `/h5/m3u8/**`、`/enkey`、`/newenkey` 保留服务端媒体转发:签名 query 不重组,UA/IP/Range 保留,上游重定向按原 Go 客户端跟随。签名媒体请求按原合同不附登录 Cookie/Authorization;上游业务 JSON 错误返回 502,不能当播放清单成功响应。 -- `config/development.env`、`config/test.env`、`config/production.env` 分别管理三套环境,本地覆盖使用忽略提交的 `*.local.env`;进程环境优先。开发 API 按用户指定保持 `http://172.104.181.99:30331`,不要擅自切回原站 25319。 +- `config/development.env`、`config/test.env`、`config/production.env` 分别管理三套环境,本地覆盖使用忽略提交的 `*.local.env`;进程环境优先。2026-10-16 起开发 API 按用户指定使用 `https://webhuakuiapi.mfcg.me`(替代原 `http://172.104.181.99:30331`),不要擅自切回原站 25319 或旧地址。 - SSR 请求准备在 `src/proxy.ts` 完成,通过 `src/server/react/handoff.ts` 向 React 交接;保留同请求 Cookie/页面数据一致性。大快照使用私有临时文件,部署多实例时须处理交接存储共享或实例亲和。 - 所有“黄果短剧”统一为“花魁短剧”。占位图品牌 logo 透明度固定为 `0.2`。Figma 文件为 `1vUq4wf5xOs8ZOlmt26Q3O`,读取设计使用本地 `FIGMA_TOKEN` 调用 REST API,不能打印令牌。 - 本地 `npm run dev` 默认使用 Webpack;该项目已确认 Next 16.4 Turbopack 开发 HMR 存在循环刷新,未经验证不要改回默认。Charles 调试使用 `npm run dev:charles`,不改业务接口地址。 - 验证至少覆盖 `npm run check` 和受影响的浏览器链路;图片必须检查真实接口返回的地址、解密后的自然尺寸、背景图及最终可见性(包含 computed opacity),模拟图片测试通过不能证明 CDN 可用。首页 Hero 封面成功/失败均需结束加载状态,保留淡入和失败占位;不能遗漏 CSS 依赖的就绪标记。 -- 图片地址由公共 ImageProvider/解密组件统一解析:优先上游 `imgCdn`、`imgAppUsCdn`,再用环境 `IMAGE_CDN_URL`。当前开发 API 缺 CDN,开发环境单独配置已核验的 CloudFront 来源;不要把 API 地址当作图片 CDN。保留原 XOR 协议并验证图片文件头,错误响应不能缓存为图片。 +- 图片地址由公共 ImageProvider/解密组件统一解析:优先上游 `imgCdn`、`imgAppUsCdn`,再用环境 `IMAGE_CDN_URL`。开发 API 已返回 imgCdn/imgAppUsCdn,测试 API 30331 缺 CDN,环境仍配置已核验的 CloudFront 作为备用;不要把 API 地址当作图片 CDN。保留原 XOR 协议并验证图片文件头,错误响应不能缓存为图片。 - 提交遵循用户全局 AGENTS 规则:无前缀请求使用 `[Ai] : <中文主题>`,正文具体说明文件/模块/资源;仅确有 Claude 共创时添加 Claude trailer。 - PWA 安装配置使用 `src/app/manifest.ts` 与 `src/lib/pwa/config.ts`:fullscreen 优先、standalone 回退、orientation any;全局 viewport-fit cover 与 Apple 独立窗口标签不可只放播放器页。Next 16 的 appleWebApp.capable 仅生成非 Apple 前缀标签,需保留 Metadata.other 中的 apple-mobile-web-app-capable。 - `/manifest.webmanifest` 与 `/sw.js` 必须绕过 SSR Proxy/游客初始化,包括 NAVIGATION_ONLY;Worker 源码为 TypeScript,开发/构建自动生成 public/sw.js,不拦截或缓存鉴权 API、HTML/RSC、签名媒体及密钥。PWA 安全区样式最后加载,普通浏览器不改变原布局;验证 `npm run test:pwa` 与实际 SSR 清单/标签/资源,真实设备安装需 HTTPS。 diff --git a/README.md b/README.md index ad8276e..36f4ee6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 花魁短剧 · Next.js / TypeScript -从 `jha-1045-hgdjnet-web` 迁移的独立 Next.js 16 项目。运行时仅需要 Node.js,不依赖 Go、原项目目录或原 Go 服务。保留内容浏览页面、原 URL、游客会话、播放器和统计交互(登录、账户与充值功能已移除,旧账户地址 301 到首页);PC/H5 界面按花魁短剧 Figma 设计更新。 +从 `jha-1045-hgdjnet-web` 迁移的独立 Next.js 16 项目。运行时仅需要 Node.js,不依赖 Go、原项目目录或原 Go 服务。保留内容浏览页面、原 URL、游客会话和播放器(登录、账户与充值功能已移除,旧账户地址 301 到首页);PC/H5 界面按花魁短剧 Figma 设计更新。 ## 启动 @@ -68,20 +68,18 @@ Node 原生代理说明:[Node.js HTTP 文档](https://nodejs.org/download/rele 配置优先级:进程环境变量 > `config/<环境>.local.env` > `config/<环境>.env`。本地覆盖文件不会提交。`APP_ENV` 选择业务环境;测试环境的优化构建仍使用 Next.js 要求的 `NODE_ENV=production`。 -开发 `UPSTREAM_API_URL` 使用 `http://172.104.181.99:30331`;测试服务与 API 同机,使用 `http://127.0.0.1:30331`,测试应用端口为 `30334`。本地运行测试环境且 API 不在本机时,可在 `config/test.local.env` 覆盖上游地址。正式配置使用同机 API `http://127.0.0.1:26931`、服务端口 `26935`,可由部署环境或 `config/production.local.env` 覆盖。构建不访问上游,启动时检查地址。 +开发 `UPSTREAM_API_URL` 使用 `https://webhuakuiapi.mfcg.me`;测试服务与 API 同机,使用 `http://127.0.0.1:30331`,测试应用端口为 `30334`。本地运行测试环境且 API 不在本机时,可在 `config/test.local.env` 覆盖上游地址。正式配置使用同机 API `http://127.0.0.1:26931`、服务端口 `26935`,可由部署环境或 `config/production.local.env` 覆盖。构建不访问上游,启动时检查地址。 ```dotenv UPSTREAM_API_URL=https://your-api.example UPSTREAM_TIMEOUT_MS=20000 IMAGE_CDN_URL= SITEMAP_DIR=../template/pages/sitemaps -TRACKER_APP_ID=JHA-4001 -TRACKER_ENCRYPTED_CONFIG= ``` -这些变量仅供服务端使用。`config/info.reportSdkConfig` 优先于本地埋点密文配置。官方导航独立进程可以使用 `NAVIGATION_ONLY=true PORT=26813 npm start`,开放 `/`、`/navigation/` 和静态资源,原 `/nav/`、`/dh/` 一次 301 到 `/navigation/`。与原 Go `DHInit` 一致,导航模式不注册 robots、llms、sitemap、验证文件及业务接口,这些地址返回原站 404 页面。 +这些变量仅供服务端使用。官方导航独立进程可以使用 `NAVIGATION_ONLY=true PORT=26813 npm start`,开放 `/`、`/navigation/` 和静态资源,原 `/nav/`、`/dh/` 一次 301 到 `/navigation/`。与原 Go `DHInit` 一致,导航模式不注册 robots、llms、sitemap、验证文件及业务接口,这些地址返回原站 404 页面。 -图片 CDN 与 API 独立配置。上游 `config/info.config.imgCdn`、`imgAppUsCdn` 按顺序提供图片来源,`IMAGE_CDN_URL` 提供环境级备用来源。开发与测试共用 API 30331,该接口返回裸 `v3/image/...` 且 CDN 配置为空,因此两套环境均显式配置原站已验证可用的 CloudFront 图片线路;生产独立配置。修改环境配置后需重启服务。 +图片 CDN 与 API 独立配置。上游 `config/info.config.imgCdn`、`imgAppUsCdn` 按顺序提供图片来源,`IMAGE_CDN_URL` 提供环境级备用来源。开发 API 返回 `imgCdn`/`imgAppUsCdn`;测试 API 30331 返回裸 `v3/image/...` 且 CDN 配置为空,因此两套环境均显式配置原站已验证可用的 CloudFront 图片线路作为备用;生产独立配置。修改环境配置后需重启服务。 公共图片组件统一解析相对路径,覆盖封面、横幅、背景和头像;相对路径不会随页面目录变化。图片保持原首 100 字节 XOR 协议,也兼容未加密的有效图片;校验文件头后才缓存,HTTP、证书、JSON 错误或超时会尝试配置的备用 CDN。完整外域 URL 保持原签名,只有属于已配置 CDN 的资源才允许切换相同对象路径;图片请求不向外域发送本站 Cookie/Authorization。 @@ -91,15 +89,15 @@ TRACKER_ENCRYPTED_CONFIG= | --- | --- | | `src/app/` | 原生 App Router TSX 页面、Metadata 与 BFF/媒体 Route Handlers | | `src/features/` | 短剧目录、播放、账号与内容页面组件 | -| `src/components/` | 页头、页尾、卡片、播放器、图片、弹窗、认证、支付、埋点共用组件 | +| `src/components/` | 页头、页尾、卡片、播放器、图片、弹窗、认证、支付共用组件 | | `src/styles/` | 基础、公共组件、页面和 PWA 覆盖样式,通过 TSX 导入并由 Next 编译 | -| `src/lib/` | 类型化浏览器请求、Cookie、搜索/展示方法与埋点上下文 | +| `src/lib/` | 类型化浏览器请求、Cookie、搜索/展示方法 | | `src/proxy.ts`、`src/server/react/` | SSR 数据、状态、响应 Cookie 和 React 数据交接 | | `src/server/pages/` | 原路由参数、分页、页面数据组合与播放/账号场景 | | `src/server/services/` | 上游 DTO、展示模型、公共配置和请求级复用 | | `src/server/upstream.ts`、`auth.ts` | 上游请求、请求头/Cookie 透传、登录与游客恢复 | | `src/server/seo/` | 原 SEO 策略、TDK、canonical、JSON-LD 和抓取文件 | -| `public/static/` | 本地 Figma 品牌/图标、分享图与私有埋点 SDK | +| `public/static/` | 本地 Figma 品牌/图标、分享图 | 所有业务页面由 TSX 组件服务端渲染,交互使用 React hooks 和 Client Components。App Router 的 `generateMetadata` 输出完整首屏 SEO;JSON-LD 使用安全序列化的独立脚本。没有 HTML 模板运行时或注入整页 HTML 的适配层。 @@ -206,7 +204,7 @@ SSR 在 Proxy 中完成上游数据、游客恢复和 Cookie 写回,再交给 公共配置可短期复用;登录用户内容仅在单次请求内复用,页面响应使用 `private, no-store`。必要内容接口临时不可用返回 503、`Retry-After` 和 noindex;真实缺失的实体及越界分页仍返回 404。 -播放器通过 `package.json` 管理 `xgplayer`、`xgplayer-hls`,按需加载官方模块和样式,支持 MP4/HLS、加密密钥、预告切换、章节播放、全屏、解锁、评论及播放埋点。原 DPlayer、全局 Hls 和浏览器业务 JS 已删除。加密图片、二维码分别封装为 TSX 组件,二维码使用 npm `qrcode`;占位图 Logo 透明度保持 0.2。 +播放器通过 `package.json` 管理 `xgplayer`、`xgplayer-hls`,按需加载官方模块和样式,支持 MP4/HLS、加密密钥、预告切换、章节播放、全屏、解锁及评论。原 DPlayer、全局 Hls 和浏览器业务 JS 已删除。加密图片、二维码分别封装为 TSX 组件,二维码使用 npm `qrcode`;占位图 Logo 透明度保持 0.2。 ## SEO 保留范围 @@ -217,7 +215,7 @@ SSR 在 Proxy 中完成上游数据、游客恢复和 Cookie 写回,再交给 - Organization、WebSite、WebPage、Breadcrumb、ItemList、FAQ、TVSeries/TVEpisode、VideoObject 等原 JSON-LD 条件和真实元数据。 - robots.txt、llms.txt、sitemap 索引/分片、纯渠道首页 302、下载地址 302。 -上游 TDK、导航、公告和 JSON-LD 中的完整旧品牌词「黄果短剧」统一显示为「花魁短剧」。永久域名、canonical、真实联系地址、普通影片名称、验证值和 SDK 配置保留原值。 +上游 TDK、导航、公告和 JSON-LD 中的完整旧品牌词「黄果短剧」统一显示为「花魁短剧」。永久域名、canonical、真实联系地址、普通影片名称、验证值保留原值。 抓取文件遵循原 Go 路由。普通站点的 robots/llms 读取公共配置,保留 60 秒成功缓存;刷新失败沿用最近成功配置,首次失败使用空配置。地址使用原永久域名优先、代理协议/Host 回退规则。robots 按顺序读取 `template/pages/robots.txt`、`templates/pages/robots.txt`、`app/template/pages/robots.txt`、`app/templates/pages/robots.txt`,没有可用文件时使用原内嵌文本,再回退原内置默认文本。保留原爬虫规则、行处理、已有 Host 和大小写语义,按原算法写入 Sitemap 地址。 @@ -263,11 +261,13 @@ APP_ANDROID_APK_URL= APP_DEEP_LINK_URL= ``` -平台地址未配置时,按钮显示“前往下载页”,沿用上游 `AppAddr` 或 `/app/download/` 跳转;未配置 App 深链时隐藏打开 App 入口。链接保留原始跳转地址与导航埋点,不生成设计示例中的安装包、描述文件或 App Store 地址。 +平台地址未配置时,按钮显示“前往下载页”,沿用上游 `AppAddr` 或 `/app/download/` 跳转;未配置 App 深链时隐藏打开 App 入口。链接保留原始跳转地址,不生成设计示例中的安装包、描述文件或 App Store 地址。 + +所有 Telegram 联系入口(页尾、官方导航、回家的路、播放页链接、2257/DMCA、意见反馈,以及 Telegram 形式的「联系我们」客服链接)统一使用 `CONTACT_TELEGRAM_URL`,三套环境默认 `https://t.me/jbd125`,覆盖上游 `tgGroup`、`telegram`、`telegramAdd`。值无效时回退为默认账号。 ## 接口和媒体 -保留原 BFF 路由、别名、参数校验、业务响应壳、上传和 CSV 导出。`Cookie`、cookie token 优先的 `Authorization`、客户端 IP、浏览器 UA、`X-User-Agent`、`X-SDK-Common` 透传;过滤 hop-by-hop 和内部控制头。多个 `Set-Cookie` 独立回传。埋点上下文按原白名单校验并保留 SDK 原字段。 +保留原 BFF 路由、别名、参数校验、业务响应壳、上传和 CSV 导出。`Cookie`、cookie token 优先的 `Authorization`、客户端 IP、浏览器 UA、`X-User-Agent` 透传;过滤 hop-by-hop 和内部控制头。多个 `Set-Cookie` 独立回传。 公共配置隔离过期登录凭证;个人资料刷新与游客恢复写回当前请求,避免首屏重复建立会话。正式账号失效和内容允许降级游客的规则分别保留。 @@ -289,7 +289,7 @@ SMOKE_URL=http://localhost:25131 npm run test:browser SMOKE_URL=http://localhost:25131 npm run test:ui ``` -浏览器检查默认使用 macOS Chrome,可通过 `CHROME_PATH` 指定。`test:catalog`、`test:popups`、`test:playback` 使用隔离上游与真实 React 组件,验证目录交互、弹层焦点/滚动、官方 xgplayer、加密 HLS、评论与埋点,不改动真实账号。`test:ssr` 使用生产构建核验 SEO、HTTP 状态、Cookie、透传和并发隔离,不访问真实业务接口。 +浏览器检查默认使用 macOS Chrome,可通过 `CHROME_PATH` 指定。`test:catalog`、`test:popups`、`test:playback` 使用隔离上游与真实 React 组件,验证目录交互、弹层焦点/滚动、官方 xgplayer、加密 HLS 与评论,不改动真实账号。`test:ssr` 使用生产构建核验 SEO、HTTP 状态、Cookie、透传和并发隔离,不访问真实业务接口。 全站界面截图默认输出 `/tmp/hkdj-figma/implementation`,浏览器基础检查输出 `/tmp/hkdj-browser-smoke`。完整真实内容回归需要原上游剧集、专题与标签数据;空环境可设置 `UI_ALLOW_EMPTY_CONTENT=1`,缺失实体仍必须返回 404 与 noindex。 diff --git a/config/development.env b/config/development.env index 44d1914..986b4d2 100644 --- a/config/development.env +++ b/config/development.env @@ -1,14 +1,11 @@ APP_ENV=development -UPSTREAM_API_URL=http://172.104.181.99:30331 -# UPSTREAM_API_URL=http://172.105.118.202:26931 -# The development API returns relative image keys and has no imgCdn configured. -# Verified image source from the original site's imgAppUsCdn; API stays on 30331. +UPSTREAM_API_URL=https://webhuakuiapi.mfcg.me +# UPSTREAM_API_URL=http://172.104.181.99:30331 +# The API supplies imgCdn/imgAppUsCdn; this CloudFront source is only the fallback. # Primary public domain; overrides the backend foreverUrlAddr. SITE_URL=https://huakuidj.com IMAGE_CDN_URL=https://d3n8qx4rvy4vol.cloudfront.net/ UPSTREAM_TIMEOUT_MS=20000 -TRACKER_APP_ID=JHA-4001 -TRACKER_ENCRYPTED_CONFIG= PORT=25121 # Backend-generated sitemap files; relative to the Node working directory. SITEMAP_DIR=./template/pages/sitemaps @@ -20,6 +17,8 @@ M3U8_KEY_FILE=./config/secrets/m3u8.key SEO_INDEXNOW_KEY=d8e5b742d13809c10865fc3b29de24ee SEO_BAIDU_SITE_VERIFICATION= SEO_BAIDU_HTML_FILE= +# Telegram contact for every TG entry (footer, navigation, return-home, legal pages, customer service). +CONTACT_TELEGRAM_URL=https://t.me/jbd125 APP_IOS_STORE_URL= APP_IOS_PROFILE_URL= APP_ANDROID_APK_URL= diff --git a/config/production.env b/config/production.env index 6be786f..413eafe 100644 --- a/config/production.env +++ b/config/production.env @@ -6,8 +6,6 @@ UPSTREAM_API_URL=http://127.0.0.1:26931 SITE_URL=https://huakuidj.com IMAGE_CDN_URL=https://d3n8qx4rvy4vol.cloudfront.net/ UPSTREAM_TIMEOUT_MS=20000 -TRACKER_APP_ID=JHA-4001 -TRACKER_ENCRYPTED_CONFIG= PORT=26935 # Node runs in h5/next; the backend generates files in h5/template/pages/sitemaps. SITEMAP_DIR=../template/pages/sitemaps @@ -15,6 +13,8 @@ M3U8_KEY_FILE=./config/secrets/m3u8.key SEO_INDEXNOW_KEY=d8e5b742d13809c10865fc3b29de24ee SEO_BAIDU_SITE_VERIFICATION= SEO_BAIDU_HTML_FILE= +# Telegram contact for every TG entry (footer, navigation, return-home, legal pages, customer service). +CONTACT_TELEGRAM_URL=https://t.me/jbd125 APP_IOS_STORE_URL= APP_IOS_PROFILE_URL= APP_ANDROID_APK_URL= diff --git a/config/test.env b/config/test.env index da94c9a..0cd4e99 100644 --- a/config/test.env +++ b/config/test.env @@ -6,8 +6,6 @@ UPSTREAM_API_URL=http://127.0.0.1:30331 SITE_URL=https://huakuidj.com IMAGE_CDN_URL=https://d3n8qx4rvy4vol.cloudfront.net/ UPSTREAM_TIMEOUT_MS=20000 -TRACKER_APP_ID=JHA-4001 -TRACKER_ENCRYPTED_CONFIG= PORT=30334 # Node runs in h5/next; the backend generates files in h5/template/pages/sitemaps. SITEMAP_DIR=../template/pages/sitemaps @@ -15,6 +13,8 @@ M3U8_KEY_FILE=./config/secrets/m3u8.key SEO_INDEXNOW_KEY=d8e5b742d13809c10865fc3b29de24ee SEO_BAIDU_SITE_VERIFICATION= SEO_BAIDU_HTML_FILE= +# Telegram contact for every TG entry (footer, navigation, return-home, legal pages, customer service). +CONTACT_TELEGRAM_URL=https://t.me/jbd125 APP_IOS_STORE_URL= APP_IOS_PROFILE_URL= APP_ANDROID_APK_URL= diff --git a/public/static/common/web-sdk-v1.2.0.js b/public/static/common/web-sdk-v1.2.0.js deleted file mode 100644 index 1114a3b..0000000 --- a/public/static/common/web-sdk-v1.2.0.js +++ /dev/null @@ -1,28 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).WebSDK=t()}(this,function(){"use strict";function e(e){var t,n,r,o,i,a,s,c,u,l,d,_,p,f,h,g,m,y,v,w,k,b,C,S,P=function(e){var t,n,r,o,i,a;for(t=(null==e?"":e)+"",n=[],r=0;t.length>r;r++)128>(o=t.charCodeAt(r))?n.push(o):2048>o?n.push(192|o>>6,128|63&o):55296>o||o>56319||r+1>=t.length||56320>(i=t.charCodeAt(r+1))||i>57343?n.push(224|o>>12,128|o>>6&63,128|63&o):(n.push(240|(a=i-56320+(o-55296<<10)+65536)>>18,128|a>>12&63,128|a>>6&63,128|63&a),r++);return n}((null==e?"":e)+""),E=8*P.length;for(P.push(128);P.length%64!=56;)P.push(0);for(t=0;8>t;t++)P.push(E>>>8*t&255);for(n=function(e,t){return e<>>32-t},r=function(e,t){return((e>>>0)+(t>>>0)&4294967295)>>>0},o=function(e,t,n,o){return r(r(e,t),r(n,o))},i=[7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21],a=Array(64),s=0;64>s;s++)a[s]=Math.floor(4294967296*Math.abs(Math.sin(s+1)))>>>0;for(c=1732584193,u=4023233417,l=2562383102,d=271733878,_=0;P.length>_;_+=64){for(p=Array(16),f=0;16>f;f++)p[f]=(P[h=_+4*f]|P[h+1]<<8|P[h+2]<<16|P[h+3]<<24)>>>0;for(g=c,m=u,y=l,v=d,w=0;64>w;w++)16>w?(k=m&y|~m&v,b=w):32>w?(k=v&m|~v&y,b=(5*w+1)%16):48>w?(k=m^y^v,b=(3*w+5)%16):(k=y^(m|~v),b=7*w%16),C=v,v=y,y=m,m=r(m,n(o(g,k,a[w],p[b]),i[w])),g=C;c=r(c,g),u=r(u,m),l=r(l,y),d=r(d,v)}return S=function(e){for(var t="",n=0;4>n;n++)t+=(256+(e>>>8*n&255)).toString(16).slice(1);return t},S(c)+S(u)+S(l)+S(d)}function t(e,t){return(e>>>t|e<<32-t)>>>0}function n(e,t,n){return(e&t^~e&n)>>>0}function r(e,t,n){return(e&t^e&n^t&n)>>>0}function o(e){return(t(e,2)^t(e,13)^t(e,22))>>>0}function i(e){return(t(e,6)^t(e,11)^t(e,25))>>>0}function a(e){return(t(e,7)^t(e,18)^e>>>3)>>>0}function s(e){return(t(e,17)^t(e,19)^e>>>10)>>>0}function c(e){for(var t=(e>>>0).toString(16);8>t.length;)t="0"+t;return t}function u(e,t){return((255&e[t])<<24|(255&e[t+1])<<16|(255&e[t+2])<<8|255&e[t+3])>>>0}function l(e){for(var t=function(e){var t,c,l,d,_,p,f,h,g,m,y,v,w,k,b,C,S,P,E,T,x,D=e.slice(),M=0,I=8*D.length>>>0;for(D.length>=536870912&&(M=Math.floor(D.length/536870912),0==(I=D.length%536870912*8>>>0)&&D.length>0&&M--),D.push(128);D.length%64!=56;)D.push(0);for(D.push(M>>>24&255,M>>>16&255,M>>>8&255,255&M,I>>>24&255,I>>>16&255,I>>>8&255,255&I),t=1779033703,c=3144134277,l=1013904242,d=2773480762,_=1359893119,p=2600822924,f=528734635,h=1541459225,g=0;D.length>g;g+=64){for(y=Array(64),m=0;16>m;m++)y[m]=u(D,g+4*m);for(m=16;64>m;m++)y[m]=s(y[m-2])+y[m-7]+a(y[m-15])+y[m-16]>>>0;for(v=t,w=c,k=l,b=d,C=_,S=p,P=f,E=h,m=0;64>m;m++)T=E+i(C)+n(C,S,P)+zt[m]+y[m]>>>0,x=o(v)+r(v,w,k)>>>0,E=P,P=S,S=C,C=b+T>>>0,b=k,k=w,w=v,v=T+x>>>0;t=t+v>>>0,c=c+w>>>0,l=l+k>>>0,d=d+b>>>0,_=_+C>>>0,p=p+S>>>0,f=f+P>>>0,h=h+E>>>0}return[t,c,l,d,_,p,f,h]}(e||[]),l="",d=0;t.length>d;d++)l+=c(t[d]);return l}function d(e){return l(function(e){var t,n,r,o,i,a;for(t=(null==e?"":e)+"",n=[],r=0;t.length>r;r++)128>(o=t.charCodeAt(r))?n.push(o):2048>o?n.push(192|o>>6,128|63&o):55296>o||o>56319||r+1>=t.length||56320>(i=t.charCodeAt(r+1))||i>57343?n.push(224|o>>12,128|o>>6&63,128|63&o):(n.push(240|(a=i-56320+(o-55296<<10)+65536)>>18,128|a>>12&63,128|a>>6&63,128|63&a),r++);return n}((null==e?"":e)+""))}function _(e){var t,n,r,o,i,a;for(t=(null==e?"":e)+"",n=[],r=0;t.length>r;r++)128>(o=t.charCodeAt(r))?n.push(o):2048>o?n.push(192|o>>6,128|63&o):55296>o||o>56319||r+1>=t.length||56320>(i=t.charCodeAt(r+1))||i>57343?n.push(224|o>>12,128|o>>6&63,128|63&o):(n.push(240|(a=i-56320+(o-55296<<10)+65536)>>18,128|a>>12&63,128|a>>6&63,128|63&a),r++);return n}function p(e){for(var t=(null==e?"":e)+"",n=[],r=0;t.length>r;r+=2)n.push(parseInt(t.substr(r,2),16));return n}function f(e,t){var n,r=Array(e.length+t.length);for(n=0;e.length>n;n++)r[n]=e[n];for(n=0;t.length>n;n++)r[e.length+n]=t[n];return r}function h(e,t){var n,r;return null===e?"__t:null__":"string"==typeof e?"__t:str__:"+e:"number"==typeof e&&isFinite(e)?"__t:num__:"+e:"boolean"==typeof e?"__t:bool__:"+(e?"1":"0"):Array.isArray(e)?-1!==t.indexOf(e)?"__t:circular__":(t.push(e),n="["+e.map(function(e){return h(e,t)}).join(",")+"]",t.splice(t.indexOf(e),1),n):null!==e&&"object"==typeof e?-1!==t.indexOf(e)?"__t:circular__":(t.push(e),r="{"+Object.keys(e).sort().map(function(n){return n+":"+h(e[n],t)}).join(",")+"}",t.splice(t.indexOf(e),1),r):"__t:any__:"+e}function g(e){return h(e,[])}function m(t){var n,r=t||{},o=Object.keys(r).filter(function(e){return"event_id"!==e}).sort();return o.length?(n=o.map(function(t){return e(g(r[t]))}).join(""),e(n)):e("")}function y(){if("undefined"==typeof navigator)return"PC";var e=navigator.userAgent||"";return/iPhone|iPad|iPod/i.test(e)?"iOS":function(e){return/HarmonyOS|OpenHarmony/i.test((null==e?"":e)+"")}(e)||/Android/i.test(e)?"Android":"PC"}function v(e){var t=((null==e?"":e)+"").trim();return t&&/^\d+\.\d+\.\d+$/.test(t)?t:"1.0.0"}function w(e,t){return null==e||""===e?"":"int"===t?parseInt(e,10)+"":(e+"").replace(/^\s+|\s+$/g,"").toLowerCase()}function k(e,t){try{return e()}catch(e){return t}}function b(t){return e((null==t?"":t)+"").substr(0,32)}function C(e){return b((e||[]).slice().sort().join("|"))}function S(){return k(function(){var e,t,n,r,o,i,a,s="undefined"!=typeof OfflineAudioContext?OfflineAudioContext:"undefined"!=typeof webkitOfflineAudioContext?webkitOfflineAudioContext:null;if(!s)return"";if((t=(e=new s(1,44100,44100)).createOscillator()).type="triangle",t.frequency.value=1e4,n=e.createDynamicsCompressor(),t.connect(n),n.connect(e.destination),t.start(0),(r=e.startRendering())&&"function"==typeof r.then)return"";if(!r||!r.getChannelData)return"";for(o=r.getChannelData(0),i=0,a=4500;5e3>a&&o.length>a;a++)i+=Math.abs(o[a]);return b(i+"")},"")}function P(){return k(function(){function e(e){var t,n=document.createElement("span");return n.style.cssText="position:absolute;left:-9999px;top:-9999px;visibility:hidden;font-size:72px;font-family:"+e+",monospace;",n.textContent="mmmmmmmmmmlli",document.body.appendChild(n),t=n.offsetWidth,document.body.removeChild(n),t}var t,n,r,o;if("undefined"==typeof document||!document.body)return{hash:"",installed:[],count:-1};for(t=e("monospace"),n=[],r=0;$t.length>r;r++)e("'"+(o=$t[r])+"'")!==t&&n.push(o);return n.sort(),{hash:C(n.map(function(e){return e.toLowerCase()})),installed:n,count:n.length}},{hash:"",installed:[],count:-1})}function E(){return k(function(){var e,t,n,r,o,i,a;if("undefined"==typeof document)return{hash:"",supported:[]};for(e=document.createElement("video"),t=document.createElement("audio"),n=[],r=0;Zt.length>r;r++){i="video"===(o=Zt[r]).kind?e:t,a="";try{a=i.canPlayType?(i.canPlayType(o.type)||"")+"":""}catch(e){}a&&"no"!==a&&n.push(o.type+":"+a)}return n.sort(),{hash:C(n.map(function(e){return e.toLowerCase()})),supported:n}},{hash:"",supported:[]})}function T(e){var t;try{return"undefined"==typeof window?0:(t="session"===e?window.sessionStorage:window.localStorage)?(t.setItem("__sdk_probe__","1"),t.removeItem("__sdk_probe__"),1):0}catch(e){return 0}}function x(){try{return"undefined"!=typeof indexedDB?1:0}catch(e){return 0}}function D(){try{return"undefined"!=typeof navigator&&navigator.serviceWorker?1:0}catch(e){return 0}}function M(e){return/Trident|MSIE/i.test(e=(e||"")+"")?"trident":/Edge\/\d/i.test(e)&&!/Edg\//i.test(e)?"edgehtml":/Edg\//i.test(e)||/Chrome\//i.test(e)&&!/Edg\//i.test(e)?"chromium":/Firefox\//i.test(e)?"gecko":/Safari\//i.test(e)&&!/Chrome\//i.test(e)?"webkit":"unknown"}function I(e,t){return t=(t||"")+"",/HarmonyOS|OpenHarmony/i.test(e=(e||"")+"")?"harmonyos":/iPhone|iPad|iPod/i.test(e)?"ios":/Android/i.test(e)?"android":/Mac OS X|Macintosh/i.test(e)||/^Mac/i.test(t)?"macos":/Windows/i.test(e)||/^Win/i.test(t)?"windows":/Linux/i.test(e)||/Linux/i.test(t)?"linux":"unknown"}function A(e){return/iPad|Tablet/i.test(e=(e||"")+"")?"tablet":/Mobile|iPhone|Android/i.test(e)?"phone":"desktop"}function O(e){var t,n,r;for(e=(e||"")+"",t=[[/EdgiOS\/([\d.]+)/i,"Edge"],[/CriOS\/([\d.]+)/i,"Chrome"],[/FxiOS\/([\d.]+)/i,"Firefox"],[/Edg\/([\d.]+)/i,"Edge"],[/OPR\/([\d.]+)/i,"Opera"],[/Chrome\/([\d.]+)/i,"Chrome"],[/Firefox\/([\d.]+)/i,"Firefox"],[/Version\/([\d.]+).*Safari/i,"Safari"]],n=0;t.length>n;n++)if(r=e.match(t[n][0]))return{browser_name:t[n][1],browser_version:r[1]||"unknown"};return{browser_name:"unknown",browser_version:"unknown"}}function L(){var e=k(function(){var e,t="undefined"!=typeof screen?screen:null;if(t&&t.orientation&&t.orientation.type)return t.orientation.type+"";if("undefined"!=typeof window&&"number"==typeof window.orientation){if(0===(e=window.orientation)||180===e)return"portrait-primary";if(90===e||-90===e)return"landscape-primary"}return t&&null!=t.width&&null!=t.height?t.width>t.height?"landscape-primary":"portrait-primary":"unknown"},"unknown");if(-1!==e.indexOf("landscape"))return"landscape";if(-1!==e.indexOf("portrait"))return"portrait";try{if("undefined"!=typeof screen&&null!=screen.width&&null!=screen.height){if(screen.width>screen.height)return"landscape";if(screen.widthu;u++){l=s[u];try{null!=(d=t[l])&&(c[l]=t.getParameter(d)+"")}catch(e){}}for(_=Object.keys(c).sort(),p=[],f=0;_.length>f;f++)p.push((h=_[f])+"="+c[h]);return g=b(p.join("|")),m=b([n,r,a,g].join("|")),{vendor:n,renderer:r,extensions_hash:a,params_hash:g,webgl_fp:m}},{vendor:"",renderer:"",extensions_hash:"",params_hash:"",webgl_fp:""}),H=F.webgl_fp||"",U=S(),X=k(function(){var e,t,n;if("undefined"==typeof navigator||!navigator.plugins)return"";for(e=[],t=0;t0?Math.round(t):-1},-1),J=(R.platform||"")+"",Q=0;try{Q=R.plugins?R.plugins.length:0}catch(R){}e=G.count,t=H?"available":"unavailable",n=!0===R.webdriver?1:0,r=0,o=0;try{K&&K.documentElement&&(r=K.documentElement.clientWidth||B.innerWidth||0,o=K.documentElement.clientHeight||B.innerHeight||0)}catch(R){}i=0,(1===n||"available"!==t&&0===Q&&e>=0&&2>=e&&(0>=r||0>=o))&&(i=1),a="";try{"undefined"!=typeof Intl&&Intl.DateTimeFormat&&(a=Intl.DateTimeFormat().resolvedOptions().timeZone||"")}catch(R){}s=function(){try{return"undefined"==typeof location?"":(location.protocol||"")+"//"+(location.host||"")+(location.pathname||"")}catch(R){return""}}(),c="",u="",l="";try{"undefined"!=typeof location&&(c=location.hostname||"",u=location.pathname||"",location.search&&(l=b(location.search)))}catch(R){}_=function(e){var t,n=((e||"")+"").trim();if(!n)return{referer_domain:"",referer_path:""};try{return{referer_domain:(t=new URL(n)).hostname||"",referer_path:t.pathname||""}}catch(e){return{referer_domain:"",referer_path:""}}}(d=K&&K.referrer?K.referrer+"":""),p=function(e){var t,n,r,o;if(t=(e=e||{}).userAgent,/iPhone|iPad|iPod/i.test(t=(t||"")+"")&&!/CriOS\/|FxiOS\/|EdgiOS\/|OPiOS\//i.test(t)&&/Version\/[\d.]+.*Safari/i.test(t))return O(e.userAgent);try{if((n=e.userAgentData&&e.userAgentData.brands)&&n.length)for(r=0;n.length>r;r++)if((o=(n[r].brand||"")+"")&&"Chromium"!==o&&0!==o.indexOf("Not")&&"Not.A/Brand"!==o)return{browser_name:o,browser_version:(n[r].version||"unknown")+""}}catch(e){}return O(e.userAgent)}(R),f=z.supported?z.supported.length:-1,g=Math.round(96*(h=B.devicePixelRatio||1)),m=-1;try{null!=R.hardwareConcurrency&&R.hardwareConcurrency>0&&(m=R.hardwareConcurrency)}catch(R){}y=m,v=-1;try{null!=R.deviceMemory&&R.deviceMemory>0&&(v=Math.round(10*R.deviceMemory)/10)}catch(R){}return w=j&&j.type?j.type+"":"unknown",C=R.maxTouchPoints||0,{platform:"web",os_family:I(V,J),device_type:A(V),browser_kernel:M(V),browser_name:p.browser_name,browser_version:p.browser_version,browser_platform:J,display_mode:k(function(){return"undefined"==typeof window||"function"!=typeof window.matchMedia?"unknown":window.matchMedia("(display-mode: standalone)").matches?"standalone":window.matchMedia("(display-mode: fullscreen)").matches?"fullscreen":window.matchMedia("(display-mode: minimal-ui)").matches?"minimal-ui":"browser"},"unknown"),visibility_state:k(function(){if("undefined"==typeof document)return"unknown";var e=document.visibilityState;return"visible"===e||"hidden"===e?e:"unknown"},"unknown"),screen_width:q.width||0,screen_height:q.height||0,screen_dpi:g,device_pixel_ratio:Math.round(100*h),orientation:L(),viewport_width:r>0?Math.round(r):-1,viewport_height:o>0?Math.round(o):-1,color_depth:q.colorDepth||0,max_touch_points:C,touch_support:C>0?1:0,hardware_concurrency:m,cpu_cores:y,device_memory_gb:v,is_debug:0,language:R.language||"",timezone:a,cookie_enabled:R.cookieEnabled?1:0,local_storage_enabled:T("local"),session_storage_enabled:T("session"),indexeddb_enabled:x(),service_worker_enabled:D(),network_type:"unknown",connection_type:w,effective_type:j&&j.effectiveType?j.effectiveType+"":"unknown",downlink:j&&null!=j.downlink?Math.round(1e3*j.downlink):-1,rtt:j&&null!=j.rtt?j.rtt:-1,canvas_fp:N,webgl_fp:H,webgl_vendor:F.vendor||"",webgl_renderer:F.renderer||"",webgl_extensions_hash:F.extensions_hash||"",webgl_params_hash:F.params_hash||"",audio_fp:U,audio_sample_rate:W>0?W:44100,fonts:G.installed||[],fonts_hash:G.hash,plugins:Y,plugins_hash:X,mimes:z.supported||[],mimes_hash:z.hash,mime_count:f,plugin_count:Q,font_count:e,webgl_status:t,has_webdriver:n,is_headless:i,headless_reason:i?n?"webdriver":"composite":"",current_url:s,current_domain:c,current_path:u,current_query_hash:l,referer:d,referer_domain:_.referer_domain,referer_path:_.referer_path}}function q(e){return null==e?"":e+""} -// Keep OS fields in the common event envelope so IDs and signatures include them. -function hkSystemFields() { - var nav = typeof navigator === "undefined" ? {} : navigator; - var ua = nav.userAgent || "", platform = nav.platform || "", match; - var isDesktopIPad = /^Mac/i.test(platform) && nav.maxTouchPoints > 1; - if (/iPhone|iPad|iPod/i.test(ua) || isDesktopIPad) { - match = ua.match(/(?:CPU(?: iPhone)? OS|iPhone OS)\s+(\d+(?:[._]\d+)*)/i); - return { system_name: "iOS", system_version: match ? match[1].replace(/_/g, ".") : "" }; - } - if (/Android/i.test(ua)) { - match = ua.match(/Android\s+(\d+(?:[._]\d+)*)/i); - return { system_name: "Android", system_version: match ? match[1].replace(/_/g, ".") : "" }; - } - if (/Windows/i.test(ua) || /^Win/i.test(platform)) { - match = ua.match(/Windows NT\s+(\d+(?:\.\d+)*)/i); - return { system_name: "Windows", system_version: match ? match[1] : "" }; - } - if (/Mac OS X|Macintosh/i.test(ua) || /^Mac/i.test(platform)) { - match = ua.match(/Mac OS X\s+(\d+(?:[._]\d+)*)/i); - return { system_name: "macOS", system_version: match ? match[1].replace(/_/g, ".") : "" }; - } - if (/Linux/i.test(ua) || /Linux/i.test(platform)) { - return { system_name: "Linux", system_version: "" }; - } - return { system_name: "Other", system_version: "" }; -} -function B(e){var t=e||{},s=hkSystemFields(),n={system_name:s.system_name,system_version:s.system_version,app_id:(t.appId||"")+"",uid:null!=t.uid?t.uid:0,channel:(t.channel||"")+"",sid:(t.sid||"")+"",client_ts:Math.floor(Date.now()/1e3),device:(t.deviceType||"PC")+"",device_id:(t.deviceId||"")+"",device_brand:(t.deviceBrand||"")+"",device_model:(t.deviceModel||"")+"",device_fingerprint:(t.deviceFingerprint||"")+"",fp_version:(t.fpVersion||"")+"",user_agent:"undefined"!=typeof navigator&&navigator.userAgent||"",sdk_version:(t.sdkVersion||"")+"",app_version:(t.appVersion||"1.0.0")+""};return null!=t.deviceEnv&&(n.device_env=t.deviceEnv),n}function K(){var e,t,n,r=new Uint8Array(16);if("undefined"!=typeof crypto&&crypto.getRandomValues)crypto.getRandomValues(r);else for(e=0;16>e;e++)r[e]=Math.floor(256*Math.random());for(t="",n=0;r.length>n;n++)t+=("0"+r[n].toString(16)).slice(-2);return t}function V(){try{return!("undefined"==typeof window||!window.sessionStorage||(window.sessionStorage.setItem("__sdk_ss_ping__","1"),window.sessionStorage.removeItem("__sdk_ss_ping__"),0))}catch(e){return!1}}function j(){if(!un){if(un=!0,function(){try{return!("undefined"==typeof window||!window.localStorage||(window.localStorage.setItem("__web_sdk_ping__","1"),window.localStorage.removeItem("__web_sdk_ping__"),0))}catch(e){return!1}}())try{window.localStorage.removeItem(rn)}catch(e){}if("undefined"!=typeof document&&"string"==typeof document.cookie)try{var t="path=/;max-age=0;SameSite=Lax";"undefined"!=typeof location&&"https:"===location.protocol&&(t+=";Secure"),document.cookie=encodeURIComponent(on)+"=;"+t}catch(e){}}}function N(t){j();var n=function(e){return JSON.stringify({sid:e.sid,sessionStartTs:e.sessionStartTs,lastValidBehaviorAt:e.lastValidBehaviorAt})}(t);if(V())try{window.sessionStorage.setItem(tn,t.sid),window.sessionStorage.setItem(nn,n)}catch(e){}cn=t}function F(){if(cn)return cn;j();var t=null;if(V())try{t=window.sessionStorage.getItem(nn)}catch(e){}return cn=t?function(t){try{var n=JSON.parse(t);return n&&n.sid?{sid:n.sid+"",sessionStartTs:"number"==typeof n.sessionStartTs?n.sessionStartTs:Date.now(),lastValidBehaviorAt:"number"==typeof n.lastValidBehaviorAt?n.lastValidBehaviorAt:Date.now()}:null}catch(e){return null}}(t):null,cn}function H(e){return{sid:K(),sessionStartTs:e,lastValidBehaviorAt:e}}function U(e){return"number"==typeof e&&isFinite(e)?1e12>e?1e3*e:e:null}function X(e,t,n){var r=U(e.sessionStartTs)||n,o=U(e.lastValidBehaviorAt)||r,i=n;return"idle"===t?i=Math.min(i=Math.max(r,o+an),n):"forced"===t&&(i=Math.min(r+sn,n)),Math.max(r,i)}function Y(e,t,n){var r,o;return n=n||Date.now(),r=U(e.sessionStartTs)||n,o=X(e,t,n),{sid:e.sid,sessionStartTs:r,lastValidBehaviorAt:U(e.lastValidBehaviorAt)||r,sessionEndTs:o,endReason:t}}function G(e){var t,n,r,o,i;return e=e||Date.now(),(t=F())?(r=e-t.sessionStartTs>sn,e-t.lastValidBehaviorAt>an||r?(o=Y(t,r?"forced":"idle",e),N(i=H(e)),{sid:i.sid,isNew:!0,endedSession:o}):{sid:t.sid,isNew:!1}):(N(n=H(e)),{sid:n.sid,isNew:!0})}function z(){var e=F();return e?e.sid:""}function W(){cn=null}function J(){try{return!("undefined"==typeof window||!window.localStorage||(window.localStorage.setItem(dn,"1"),window.localStorage.removeItem(dn),0))}catch(e){return!1}}function Q(){var t,n;if(!J())return Promise.resolve(_n||"NULL");try{return(t=window.localStorage.getItem(ln))&&/^[0-9a-f]{32}$/.test(t)?Promise.resolve(t):(n=function(){var e,t,n,r=new Uint8Array(16);if("undefined"!=typeof crypto&&crypto.getRandomValues)crypto.getRandomValues(r);else for(e=0;16>e;e++)r[e]=Math.floor(256*Math.random());for(t="",n=0;r.length>n;n++)t+=("0"+r[n].toString(16)).slice(-2);return t}(),window.localStorage.setItem(ln,n),_n=n,Promise.resolve(n))}catch(e){return Promise.resolve("NULL")}}function $(t,n){n&&(t.audio_fp=n),fn=t;var r=function(){if(!J())return"";try{var t=window.localStorage.getItem(pn);if(t&&/^[0-9a-f]{16,64}$/i.test(t))return t.toLowerCase()}catch(e){}return""}();return r?hn=r:(hn=function(e){var t,n,r,o,i,a;for(t=e||{},n={screen_width:1,screen_height:1,screen_dpi:1,device_pixel_ratio:1,color_depth:1,max_touch_points:1,cpu_cores:1,cookie_enabled:1,local_storage_enabled:1,session_storage_enabled:1,indexeddb_enabled:1,service_worker_enabled:1},r={canvas_fp:1,webgl_fp:1,audio_fp:1,fonts_hash:1,plugins_hash:1,mimes_hash:1},o=[],i=0;Qt.length>i;i++)o.push((a=Qt[i])+"="+w(t[a],n[a]?"int":r[a]?"hash":"string"));return d("fp_v3|fingerprint_platform=web|"+o.join("|"))}(t),function(t){if(t&&J())try{window.localStorage.setItem(pn,t)}catch(e){}}(hn)),{snapshot:t,fingerprint:hn}}function Z(){return fn&&hn?Promise.resolve({snapshot:fn,fingerprint:hn}):gn||(gn=new Promise(function(e){k(function(){var t,n,r,o,i="undefined"!=typeof OfflineAudioContext?OfflineAudioContext:"undefined"!=typeof webkitOfflineAudioContext?webkitOfflineAudioContext:null;i?((n=(t=new i(1,44100,44100)).createOscillator()).type="triangle",n.frequency.value=1e4,r=t.createDynamicsCompressor(),n.connect(r),r.connect(t.destination),n.start(0),(o=t.startRendering())&&"function"==typeof o.then?o.then(function(t){for(var n=t.getChannelData(0),r=0,o=4500;5e3>o&&n.length>o;o++)r+=Math.abs(n[o]);e(b(r+""))}).catch(function(){e("")}):e(S())):e("")},e(""))}).then(function(e){return $(R(),e)}).catch(function(){return $(R(),"")}),gn)}function ee(){return hn}function te(){return Wt}function ne(){return mn.digest()}function re(){try{if("undefined"!=typeof screen&&screen.orientation&&screen.orientation.type){var t=screen.orientation.type+"";if(-1!==t.indexOf("landscape"))return"landscape";if(-1!==t.indexOf("portrait"))return"portrait"}if("undefined"!=typeof screen&&null!=screen.width&&null!=screen.height){if(screen.width>screen.height)return"landscape";if(screen.width0?Math.round(n):-1}catch(e){return-1}}function ie(e,t){var n=t.accumulated_ms||0;return null!=t.resume_at_ms&&(n+=Math.max(0,e-t.resume_at_ms)),n}function ae(){return xn.pageSeqCounter++,xn.currentPageSeq=xn.pageSeqCounter,xn.currentPageSeq}function se(){try{return!("undefined"==typeof window||!window.sessionStorage||(window.sessionStorage.setItem("__sdk_metrics_ping__","1"),window.sessionStorage.removeItem("__sdk_metrics_ping__"),0))}catch(e){return!1}}function ce(e){var t,n=e||xn;return{eventCountTotal:n.eventCountTotal||0,businessEventCount:n.businessEventCount||0,pageViewCount:n.pageViewCount||0,pageClickCount:n.pageClickCount||0,networkChangeCount:n.networkChangeCount||0,backgroundCount:n.backgroundCount||0,pageKeys:le({},n.pageKeys||{}),pageStayList:(n.pageStayList||[]).slice(),pageSeqCounter:n.pageSeqCounter||0,currentPageSeq:n.currentPageSeq||0,lastEventName:n.lastEventName||"",scrollTotalCount:n.scrollTotalCount||0,scrollTotalDistance:n.scrollTotalDistance||0,scrollUpCount:n.scrollUpCount||0,scrollDownCount:n.scrollDownCount||0,scrollDirectionChangeCount:n.scrollDirectionChangeCount||0,lastScrollDirection:n.lastScrollDirection||0,firstScrollDelayMs:null!=n.firstScrollDelayMs?n.firstScrollDelayMs:-1,hasScrolled:!!n.hasScrolled,maxScrollDepthPercent:n.maxScrollDepthPercent||0,isReachBottom:n.isReachBottom||0,keydownTotalCount:n.keydownTotalCount||0,inputCount:n.inputCount||0,focusTotalCount:n.focusTotalCount||0,firstInteractionDelayMs:null!=n.firstInteractionDelayMs?n.firstInteractionDelayMs:-1,firstInteractionRecorded:!!n.firstInteractionRecorded,lastOrientation:n.lastOrientation||"unknown",touchTotalCount:n.touchTotalCount||0,swipeTotalCount:n.swipeTotalCount||0,longPressTotalCount:n.longPressTotalCount||0,pinchTotalCount:n.pinchTotalCount||0,currentPage:(t=n.currentPage,t&&"object"==typeof t?{page_key:(t.page_key||"")+"",page_name:(t.page_name||"")+"",enter_time:null!=t.enter_time?t.enter_time:Date.now(),enter_time_ms:null!=t.enter_time_ms?t.enter_time_ms:t.enter_time,page_seq_in_sid:t.page_seq_in_sid||0,has_interaction:!!t.has_interaction,accumulated_ms:t.accumulated_ms||0,resume_at_ms:null!=t.resume_at_ms?t.resume_at_ms:null,hidden_paused:!!t.hidden_paused,stay_duration_ms:t.stay_duration_ms}:null)}}function ue(e,t){return null==e||0>e||t>0&&e>t?-1:e}function le(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function de(e){return kn+":"+(e||"")}function _e(t){var n,r;if(!t)return null;if(n=null,se())try{n=window.sessionStorage.getItem(de(t))}catch(e){}if(!n&&ye())try{n=window.localStorage.getItem(de(t))}catch(e){}if(!n)return null;try{return(r=JSON.parse(n))&&r.sid===t?r:null}catch(e){return null}}function pe(t,n){if(t){var r=JSON.stringify(n||{});if(se())try{window.sessionStorage.setItem(de(t),r)}catch(e){}if(ye())try{window.localStorage.setItem(de(t),r)}catch(e){}}}function fe(e){if(!e)return null;var t=_e(e);return t&&t.metrics?t.metrics:null}function he(){var e,t=z();t&&(Cn?Sn||(Sn=t):(Cn=!0,(e=_e(t))&&e.metrics?(xn=ce(e.metrics),e.sessionStartedAt&&(Dn=e.sessionStartedAt),Sn=t):Sn=t))}function ge(e){var t=_e(e);return t&&t.behaviorHash?t.behaviorHash:""}function me(){var e=z();e&&(Sn&&Sn!==e||(Sn||(Sn=e),function(e){e&&pe(e,{sid:e,savedAt:Date.now(),sessionStartedAt:Dn,behaviorHash:ne(),metrics:ce(xn)})}(e)))}function ye(){try{return!("undefined"==typeof window||!window.localStorage||(window.localStorage.setItem("__sdk_metrics_ping__","1"),window.localStorage.removeItem("__sdk_metrics_ping__"),0))}catch(e){return!1}}function ve(){var t,n;if(!ye())return[];try{return(t=window.localStorage.getItem(wn))?(n=JSON.parse(t),Array.isArray(n)?n:[]):[]}catch(e){return[]}}function we(t){if(ye())try{window.localStorage.setItem(wn,JSON.stringify(t||[]))}catch(e){}}function ke(e){we(ve().filter(function(t){return t&&t.sid!==e}))}function be(e){for(var t,n=e.slice(0,yn);n.length>0;){if(t=JSON.stringify(n),vn>=t.length)return n;n.pop()}return n}function Ce(e){var t,n=xn.currentPage;n&&(n.stay_duration_ms=ie(e,n),t={page_key:n.page_key,page_name:n.page_name||"",stay_duration_ms:n.stay_duration_ms,enter_time:Math.floor(n.enter_time/1e3),has_interaction:n.has_interaction?1:0},n.page_seq_in_sid&&(t.page_seq_in_sid=n.page_seq_in_sid),xn.pageStayList.push(t),xn.pageStayList=be(xn.pageStayList),xn.currentPage=null)}function Se(){xn.firstInteractionRecorded||(xn.firstInteractionRecorded=!0,xn.firstInteractionDelayMs=Math.max(0,Date.now()-Dn))}function Pe(){xn.lastOrientation=re()}function Ee(e){e&&xn.currentPage&&xn.currentPage.page_key===e&&(xn.currentPage.has_interaction=!0)}function Te(e,t,n,r){var o=Date.now();Ce(o),xn.currentPage={page_key:(e||"")+"",page_name:(t||"")+"",enter_time:null!=n?1e3*n:o,enter_time_ms:o,page_seq_in_sid:r||xn.currentPageSeq||0,has_interaction:!1,accumulated_ms:0,resume_at_ms:o,hidden_paused:!1}}function xe(){Ce(Date.now())}function De(t){var n,r=t;if(null==r)return null;if("string"==typeof r)try{r=JSON.parse(r)}catch(e){return null}return r&&"object"==typeof r?(n={page_key:(r.page_key||"")+"",page_name:(r.page_name||"")+"",stay_duration_ms:null!=r.stay_duration_ms?r.stay_duration_ms:0,enter_time:null!=r.enter_time?r.enter_time:0,has_interaction:r.has_interaction?1:0},null!=r.page_seq_in_sid&&(n.page_seq_in_sid=r.page_seq_in_sid),n):null}function Me(e,t,n){var r,o,i,a,s,c,u,l,d,_,p,f=e||{},h=f.sid||z(),g=h&&h===z();for(n?r=ce(n):g?(xe(),me(),r=ce(xn)):r=function(e){var t,n;return he(),!(t=e||z())||t!==z()||Sn&&Sn!==t?(n=fe(t))?ce(n):Sn&&Sn!==t?{eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null}:ce(xn):ce(xn)}(h),o=function(e,t){var n=Date.now(),r=e||{},o=U(r.sessionStartTs)||n,i=null!=r.sessionEndTs?U(r.sessionEndTs):X(r,t||"idle",n);return{startTs:o,endTs:i=Math.max(o,i||n)}}(f,t),s=Math.max(0,(a=o.endTs)-(i=o.startTs)),c=le({},r.pageKeys||{}),u=function(e){var t,n,r=[];for(t=0;(e||[]).length>t;t++)(n=De(e[t]))&&r.push(n);return r}(function(e){var t,n,r,o={},i=[];for(t=0;e.length>t;t++)null!=(r=(n=e[t])&&n.page_seq_in_sid)&&o[r]||(null!=r&&(o[r]=!0),i.push(n));return i}((r.pageStayList||[]).slice()).sort(function(e,t){return(e.page_seq_in_sid||0)-(t.page_seq_in_sid||0)})),u=function(e,t,n){var r,o,i,a=Math.floor((t||0)/1e3),s=Math.floor((n||Date.now())/1e3),c=Math.max(0,a-5),u=[];for(r=0;(e||[]).length>r;r++)(o=e[r])&&((i=null!=o.enter_time?+o.enter_time:0)>0&&c>i||i>s+5||u.push(o));return u}(u,i,a),l=0;u.length>l;l++)(d=u[l]&&u[l].page_key?u[l].page_key+"":"")&&(c[d]=1);return _=Object.keys(c),p=n?ge(h)||ne():g?ne():ge(h)||ne(),{session_start_ts:Math.floor(i/1e3),session_end_ts:Math.floor(a/1e3),session_duration_ms:s,event_count_total:r.eventCountTotal||0,business_event_count:r.businessEventCount||0,page_count:_.length,page_view_count:r.pageViewCount||0,page_click_count:r.pageClickCount||0,session_end_reason:(t||"idle")+"",last_event_name:r.lastEventName||"",page_stay_list:u,behavior_sequence_hash:p,network_change_count:r.networkChangeCount||0,scroll_total_count:r.scrollTotalCount||0,scroll_total_distance_px:Math.round(r.scrollTotalDistance||0),scroll_up_count:r.scrollUpCount||0,scroll_down_count:r.scrollDownCount||0,scroll_direction_change_count:r.scrollDirectionChangeCount||0,first_scroll_delay_ms:ue(null!=r.firstScrollDelayMs?r.firstScrollDelayMs:-1,s),max_scroll_depth_percent:r.maxScrollDepthPercent||0,is_reach_bottom:r.isReachBottom||0,content_height:oe(),keydown_total_count:r.keydownTotalCount||0,input_count:r.inputCount||0,focus_total_count:r.focusTotalCount||0,first_interaction_delay_ms:ue(null!=r.firstInteractionDelayMs?r.firstInteractionDelayMs:-1,s),orientation:r.lastOrientation||re(),touch_total_count:r.touchTotalCount||0,swipe_total_count:r.swipeTotalCount||0,long_press_total_count:r.longPressTotalCount||0,pinch_total_count:r.pinchTotalCount||0}}function Ie(){On=!0,setTimeout(function(){On=!1},2e3)}function Ae(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function Oe(t){return e((null==t?"":t)+"").substr(0,32)}function Le(e){return null!=e&&""!==(e+"").trim()}function Re(){try{return"undefined"==typeof location?"":(location.protocol||"")+"//"+(location.host||"")+(location.pathname||"")}catch(e){return""}}function qe(e){var t=(null==e?"":e)+"";return(t=(t=(t=t.replace(/\.html?$/i,"")).replace(/[^a-zA-Z0-9_]+/g,"_")).replace(/_+/g,"_").replace(/^_|_$/g,""))||"index"}function Be(){try{return qe(("undefined"!=typeof location&&location.pathname?location.pathname:"/").replace(/^\/+|\/+$/g,"").replace(/\//g,"_")||"index")}catch(e){return"index"}}function Ke(){try{return document.scrollingElement||document.documentElement||document.body||null}catch(e){return null}}function Ve(){try{var t=Ke();return t?{scroll_top:Math.max(0,Math.round(t.scrollTop||0)),scroll_left:Math.max(0,Math.round(t.scrollLeft||0))}:{scroll_top:-1,scroll_left:-1}}catch(e){return{scroll_top:-1,scroll_left:-1}}}function je(e,t){if(null==e||!isFinite(e))return-1;var n=Math.round(+e);return 0>n&&(n=0),null!=t&&t>0&&n>t&&(n=t),n}function Ne(){var t,n,r;try{return(t=document.documentElement||document.body)?(r=t.scrollHeight||0,(n=t.scrollWidth||0)>0&&r>0?{page_width:Math.round(n),page_height:Math.round(r)}:{page_width:-1,page_height:-1}):{page_width:-1,page_height:-1}}catch(e){return{page_width:-1,page_height:-1}}}function Fe(){var t,n;try{return"undefined"==typeof window?{viewport_width:-1,viewport_height:-1}:(t=window.innerWidth||0,n=window.innerHeight||0,t>0&&n>0?{viewport_width:Math.round(t),viewport_height:Math.round(n)}:{viewport_width:-1,viewport_height:-1})}catch(e){return{viewport_width:-1,viewport_height:-1}}}function He(){var t,n;try{return"undefined"==typeof screen?{screen_width:-1,screen_height:-1}:(t=screen.width||0,n=screen.height||0,t>0&&n>0?{screen_width:Math.round(t),screen_height:Math.round(n)}:{screen_width:-1,screen_height:-1})}catch(e){return{screen_width:-1,screen_height:-1}}}function Ue(){var t,n,r;try{if("undefined"!=typeof performance&&performance.timing&&(t=performance.timing).loadEventEnd&&t.navigationStart&&t.loadEventEnd>0)return Math.max(0,t.loadEventEnd-t.navigationStart);if("undefined"!=typeof performance&&"function"==typeof performance.getEntriesByType&&(n=performance.getEntriesByType("navigation"))&&n.length>0&&(r=n[0]).loadEventEnd>0&&r.fetchStart>=0)return Math.max(0,Math.round(r.loadEventEnd-r.fetchStart))}catch(e){}return 0}function Xe(e,t){return t>0?Math.min(100,Math.max(0,Math.round(e/t*100))):0}function Ye(e){return e?{clientX:null!=e.clientX?e.clientX:null,clientY:null!=e.clientY?e.clientY:null,pageX:null!=e.pageX?e.pageX:null,pageY:null!=e.pageY?e.pageY:null}:{}}function Ge(e){var t,n,r,o,i,a,s,c,u=e||{},l=Fe(),d=Ne(),_=Ve(),p=function(e){return!!e&&(null!=e.clientX||null!=e.clientY||null!=e.pageX||null!=e.pageY)}(u),f=d.page_width>0&&d.page_height>0,h=He(),g=f&&_.scroll_top>=0?_.scroll_top:0,m=f&&_.scroll_left>=0?_.scroll_left:0,y=l.viewport_width>0?l.viewport_width:-1,v=l.viewport_height>0?l.viewport_height:-1;return p?(n=null!=u.clientY?+u.clientY:null,r=null!=u.pageX?+u.pageX:null,o=null!=u.pageY?+u.pageY:null,null!=(t=null!=u.clientX?+u.clientX:null)||null!=n?(i=null!=t&&isFinite(t)?t:0,a=null!=n&&isFinite(n)?n:0,s=f?null!=r&&isFinite(r)?r:i+m:-1,c=f?null!=o&&isFinite(o)?o:a+g:-1):(s=f&&null!=r&&isFinite(r)?r:-1,c=f&&null!=o&&isFinite(o)?o:-1,i=null!=r&&isFinite(r)?r-m:0,a=null!=o&&isFinite(o)?o-g:0),i=je(i,y),a=je(a,v),f&&(s=je(s,d.page_width),c=je(c,d.page_height)),Ae({click_viewport_x:i,click_viewport_y:a,click_document_x:f?s:-1,click_document_y:f?c:-1,click_page_x:i,click_page_y:a,click_x_percent:y>0?Xe(i,y):-1,click_y_percent:v>0?Xe(a,v):-1,screen_width:h.screen_width,screen_height:h.screen_height},l,f?d:{page_width:-1,page_height:-1},f?_:{scroll_top:-1,scroll_left:-1})):{click_viewport_x:-1,click_viewport_y:-1,click_document_x:-1,click_document_y:-1,click_page_x:-1,click_page_y:-1,click_x_percent:-1,click_y_percent:-1,screen_width:h.screen_width,screen_height:h.screen_height,viewport_width:l.viewport_width,viewport_height:l.viewport_height,page_width:f?d.page_width:-1,page_height:f?d.page_height:-1,scroll_top:f?_.scroll_top:-1,scroll_left:f?_.scroll_left:-1}}function ze(t){var n=t||{},r=qe(n.pageKey||Be()),o=n.pageName||"undefined"!=typeof document&&document.title||r,i=function(){try{return"undefined"==typeof location?{}:{page_url:Re(),page_domain:(location.hostname||"")+"",page_path:(location.pathname||"")+"",page_query_hash:location.search?Oe(location.search):""}}catch(e){return{}}}(),a=Fe();return{user_type:"",page_key:r,page_name:o,referrer_page_key:(n.previousPageKey||"")+"",referrer_page_name:(n.previousPageName||"")+"",current_page_key:r,current_page_name:o,recommend_trace_id:"",page_url:i.page_url,viewport_width:a.viewport_width,viewport_height:a.viewport_height,is_sdk_auto_collected:1}}function We(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function Je(e,t){var n,r;return"app_session_summary"===t?We({},e.summaryFields||{},e.payload||{}):(n=function(e){var t=We({},e||{});return delete t._metricsRecorded,delete t._pageSeqPrealloc,delete t._clickSnap,t}(e.payload||{}),r={event:1,payload:1,event_id:1,summaryFields:1,_pageSeqInSid:1,_metricsRecorded:1,_sdkAutoClick:1,_pageSeqPrealloc:1},Object.keys(e).forEach(function(t){r[t]||(n[t]=e[t])}),n)}function Qe(e,t,n,r,o){var i,a,s,c,u,d,h,g=e||{},y=((n.event||"")+"").trim(),v=n.lifecycle_event_key||n.payload&&n.payload.lifecycle_event_key||"";return function(e,t){var n,r=(e||"")+"";return"app_page_view"===r||"page_click"===r||("app_lifecycle"===r?"cold_start"==(n=(t||"")+"")||"web_load"===n||"warm_start"===n||"foreground"===n:"sdk_init"!==r&&"network_state_change"!==r&&"app_session_summary"!==r&&"background"!==r)}(y,v)&&(c=Date.now(),(u=F())?(u.lastValidBehaviorAt=c,N(u)):N(H(c))),i="",n.payload&&null!=n.payload.page_key?i=n.payload.page_key+"":null!=n.page_key&&(i=n.page_key+""),function(e,t,n){"app_lifecycle"===e&&t&&en[t]?mn.append(t,n):("app_page_view"===e||"page_click"===e||e&&!Tn[e])&&mn.append(e,n)}(y,v,i),a=function(e,t){return B({appId:e.appId,channel:e.channel,uid:e.uid,sid:t,deviceId:(n=e.deviceId,((null==n?"":n)+"").trim()||"NULL"),deviceType:e.deviceType||"PC",deviceBrand:e.deviceBrand||"",deviceModel:e.deviceModel||"",deviceFingerprint:ee(),fpVersion:te(),sdkVersion:e.sdkVersion,appVersion:e.appVersion});var n}(g,t),function(e,t){var n,r;if(he(),(n=(e||"")+"")&&"app_session_summary"!==n){if("app_page_view"===n&&t&&t._metricsRecorded)return xn.lastEventName=n,void me();xn.eventCountTotal++,xn.lastEventName=n,Tn[n]||xn.businessEventCount++,r=t&&null!=t.page_key?t.page_key+"":"","app_page_view"===n?(xn.pageViewCount++,r&&(xn.pageKeys[r]=1)):"page_click"===n?(xn.pageClickCount++,Ee(r)):"network_state_change"===n?xn.networkChangeCount++:r&&Ee(r),me()}}(y,n.payload||{}),s=function(e,t){return function(e,t){for(var n=t||{},r={},o=Object.keys(e||{}),i=0;o.length>i;i++)r[o[i]]=e[o[i]];return r.event_sig_version=(n.eventSigVersion||Jt)+"",r.event_sig=n.hmacKey?function(e,t){return t?function(e,t){var n,r,o,i,a=_((null==t?"":t)+"");for(a.length>64&&(a=p(l(a)));64>a.length;)a.push(0);for(n=[],r=[],o=0;64>o;o++)n.push(54^a[o]),r.push(92^a[o]);return i=p(l(f(n,_((null==e?"":e)+"")))),l(f(r,i))}(function(e){var t=e||{};return[q(t.event),q(t.event_id),q(t.app_id),q(t.client_ts),q(t.device_fingerprint)].join("|")}(e),t+""):""}(e,n.hmacKey):"",r}(e,{hmacKey:t&&t.hmacKey?t.hmacKey:"",eventSigVersion:Jt})}(function(e,t,n){return function(e,t){var n=function(){var e,t,n,r,o;for(e={},t=0;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)e[r[o]]=n[r[o]];return e}(e,t);return n.event_id||(n.event_id=m(n)),n}(e,{event:n,payload:Je(t,n)})}(a,n,y),r),"app_page_view"===y&&n.payload&&n.payload.page_key&&(n.payload._metricsRecorded||n._metricsRecorded||Te(n.payload.page_key,n.payload.page_name,n.payload.page_enter_ts,null!=n._pageSeqInSid?n._pageSeqInSid:n.payload.page_seq_in_sid)),"page_click"===y&&n.payload&&!n._sdkAutoClick&&An.push({x:null!=(d=null!=n.payload.click_viewport_x?n.payload.click_viewport_x:n.payload.click_page_x)?d:0,y:null!=(h=null!=n.payload.click_viewport_y?n.payload.click_viewport_y:n.payload.click_page_y)?h:0,t:Date.now()}),s._sessionInfo=o||null,s}function $e(t,n,r){var o=We({},n||{}),i=((o.event||"")+"").trim();return Bn[i]&&o.payload&&1!==o.payload.is_sdk_auto_collected&&(o.payload=function(e,t,n){var r,o=e||{},i=1===o.is_sdk_auto_collected,a=((n||"")+"").trim(),s="app_page_view"===a||null!=o.page_key&&(null!=o.current_page_key||null!=o.user_type),c="page_click"===a,u={}||{},l=qe(o.page_key||Be());return r=s?ze(Ae(u,{pageKey:l,pageName:o.page_name})):c?Ae({page_key:l,page_name:o.page_name||"undefined"!=typeof document&&document.title||l,recommend_trace_id:"",referrer_page_key:u.previousPageKey||""},function(e){var t=e||{},n=Fe(),r=He(),o=Ne(),i=Ve(),a=o.page_width>0&&o.page_height>0,s=a&&i.scroll_top>=0?i.scroll_top:0,c=a&&i.scroll_left>=0?i.scroll_left:0,u=n.viewport_width>0?n.viewport_width:-1,l=n.viewport_height>0?n.viewport_height:-1,d=null!=t.click_page_x?+t.click_page_x:null,_=null!=t.click_page_y?+t.click_page_y:null,p=-1,f=-1,h=-1,g=-1;return null!=d&&isFinite(d)&&null!=_&&isFinite(_)?(p=d,f=_,a&&(h=d+c,g=_+s)):null!=t.click_x_percent&&null!=t.click_y_percent&&u>0&&l>0&&(p=+t.click_x_percent/100*u,f=+t.click_y_percent/100*l,a&&(h=p+c,g=f+s)),-1!==p&&(p=je(p,u)),-1!==f&&(f=je(f,l)),a&&-1!==h&&(h=je(h,o.page_width)),a&&-1!==g&&(g=je(g,o.page_height)),{click_viewport_x:p,click_viewport_y:f,click_document_x:a?h:-1,click_document_y:a?g:-1,screen_width:r.screen_width,screen_height:r.screen_height,viewport_width:n.viewport_width,viewport_height:n.viewport_height,page_width:a?o.page_width:-1,page_height:a?o.page_height:-1,scroll_top:a?i.scroll_top:-1,scroll_left:a?i.scroll_left:-1}}(o)):{page_key:l,page_name:o.page_name||"undefined"!=typeof document&&document.title||l,recommend_trace_id:"",referrer_page_key:u.previousPageKey||""},i||s?i&&(r.is_sdk_auto_collected=1):delete r.is_sdk_auto_collected,function(e,t){var n,r,o=Ae({},e||{}),i=Object.keys(t||{});for(n=0;i.length>n;n++)Le(o[r=i[n]])||(o[r]=t[r]);return o}(o,r)}(o.payload,0,i)),Z().then(function(){var n,a=G();if(a.endedSession&&qn)try{qn(a.endedSession)}catch(e){}return n=a.sid||z(),Bn[i]&&o.payload&&function(t,n){var r,o,i,a=((t.event||"")+"").trim();Bn[a]&&t.payload&&("app_page_view"===a?1===t.payload.is_sdk_auto_collected?(r=t.payload._pageSeqPrealloc||ae(),o=t.payload._metricsRecorded,null!=t.payload._pageSeqPrealloc&&delete t.payload._pageSeqPrealloc,t.payload=function(e,t,n){var r=e||{},o=Fe(),i=Math.floor(Date.now()/1e3),a=Oe((t||"")+"|"+(n||0)+"|"+i),s=Ue(),c=qe(r.page_key||""),u=(r.page_name||"")+"";return{user_type:(null!=r.user_type?r.user_type:"")+"",page_key:c,page_name:u,referrer_page_key:(null!=r.referrer_page_key?r.referrer_page_key:"")+"",referrer_page_name:(null!=r.referrer_page_name?r.referrer_page_name:"")+"",current_page_key:(null!=r.current_page_key?r.current_page_key:c)+"",current_page_name:(null!=r.current_page_name?r.current_page_name:u)+"",recommend_trace_id:(null!=r.recommend_trace_id?r.recommend_trace_id:"")+"",page_id:a,page_enter_ts:i,page_url:r.page_url||Re(),page_load_time:null!=s?s:0,viewport_width:o.viewport_width,viewport_height:o.viewport_height}}(t.payload,n,r),t._pageSeqInSid=r,o&&(t._metricsRecorded=1)):(i=function(t,n){var r,o=t||{},i=null!=o.page_enter_ts?o.page_enter_ts:Math.floor(Date.now()/1e3),a=o.page_seq_in_sid||ae();return{page_seq_in_sid:a,page_id:o.page_id||e((null==(r=(n||"")+"|"+a+"|"+i)?"":r)+"").substr(0,32),page_enter_ts:i}}(t.payload,n),t.payload=We(t.payload,i)):"page_click"===a&&1===t.payload.is_sdk_auto_collected&&(t.payload=function(e){var t=e||{},n=Ge(t._clickSnap||t);return{page_key:qe(t.page_key||""),page_name:(t.page_name||"")+"",recommend_trace_id:(null!=t.recommend_trace_id?t.recommend_trace_id:"")+"",referrer_page_key:(null!=t.referrer_page_key?t.referrer_page_key:"")+"",click_viewport_x:n.click_viewport_x,click_viewport_y:n.click_viewport_y,click_document_x:n.click_document_x,click_document_y:n.click_document_y,click_page_x:n.click_page_x,click_page_y:n.click_page_y,click_x_percent:n.click_x_percent,click_y_percent:n.click_y_percent,screen_width:n.screen_width,screen_height:n.screen_height,viewport_width:n.viewport_width,viewport_height:n.viewport_height,page_width:n.page_width,page_height:n.page_height,scroll_top:n.scroll_top,scroll_left:n.scroll_left}}(t.payload),t._sdkAutoClick=1,delete t.payload.is_sdk_auto_collected,delete t.payload._clickSnap))}(o,n),Qe(t,n,o,r,a)})}function Ze(){try{return"undefined"==typeof location?"":(location.protocol||"")+"//"+(location.host||"")+(location.pathname||"")}catch(e){return""}}function et(){var t,n;try{return(t=function(){try{return document.scrollingElement||document.documentElement||document.body||null}catch(e){return null}}())&&(n=Math.max(0,(t.scrollHeight||0)-(t.clientHeight||0)))>0?Math.min(100,Math.max(0,Math.round((t.scrollTop||0)/n*100))):0}catch(e){return 0}}function tt(){if(On)return"navigation";try{if("hidden"===document.visibilityState)return"visibility_hidden"}catch(e){}return"unknown"}function nt(){return function(){if(xn.currentPage&&xn.currentPage.page_key)return xn.currentPage.page_key;var e=xn.pageStayList||[];return e.length>0&&e[e.length-1].page_key||""}()||Be()}function rt(){var t,n="unknown";try{n="visible"===document.visibilityState||"hidden"===document.visibilityState?document.visibilityState:"unknown"}catch(e){}return{page_url:Ze(),unload_reason:tt(),scroll_depth_percent_at_unload:et(),web_visibility_state:n,page_stay_duration_ms:(t=xn.currentPage,t?ie(Date.now(),t):0),last_active_page_key:nt()}}function ot(e,t,n,r){var o,i,a=n||{};return a.lifecycle_event_key=t,t===Kn&&(a=at((o=Fe(),{page_url:Ze(),page_load_time_ms:Ue(),viewport_width:o.viewport_width,viewport_height:o.viewport_height}),a)),t===Nn&&(a=at(a,rt())),t===jn&&((i=xn.currentPage)&&!i.hidden_paused&&(i.accumulated_ms=ie(Date.now(),i),i.resume_at_ms=null,i.hidden_paused=!0),a.last_active_page_key||(a.last_active_page_key=nt())),t===Vn&&function(){var e=xn.currentPage;e&&e.hidden_paused&&(e.resume_at_ms=Date.now(),e.hidden_paused=!1)}(),$e(e,{event:"app_lifecycle",payload:a},r)}function it(e,t,n,r){return function(e,t,n,r){var o=We({},n||{});return Z().then(function(){return Qe(e,t,o,r,null)})}(e,r||z(),{event:"app_session_summary",summaryFields:t||{}},n)}function at(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function st(e){var t,n;return!e||2>e.length?0:Math.sqrt((t=e[0].clientX-e[1].clientX)*t+(n=e[0].clientY-e[1].clientY)*n)}function ct(){Gn&&(clearTimeout(Gn),Gn=null)}function ut(t,n,r){var o,i,a,s,c,u;if(r=r||{},!t)return Promise.resolve({sent:!1,ok:!1,status:0,reason:"no_endpoint"});if(!Array.isArray(n)||0===n.length)return Promise.resolve({sent:!0,ok:!0,status:200,reason:"empty_batch"});if(o=JSON.stringify(n),i=!0===r.useBeacon,a=!1!==r.requireHttps,c=isFinite(s=+r.timeoutMs)&&s>0?Math.floor(s):Qn,!function(e,t){if(!t)return!0;var n=((e||"")+"").match(/^(https?):\/\/([^/:?#]+)/i);return!(!n||"https"!==n[1].toLowerCase()&&("http"!==n[1].toLowerCase()||!function(e){var t=((e||"")+"").toLowerCase();return"localhost"===t||"127.0.0.1"===t||"::1"===t}(n[2])))}(t,a))return void 0!==console&&console.warn&&console.warn("[sdk][web] 已拒绝非 HTTPS 上报地址(localhost HTTP 除外)"),Promise.resolve({sent:!1,ok:!1,status:0,reason:"insecure_endpoint"});if(i&&"undefined"!=typeof navigator&&"function"==typeof navigator.sendBeacon)try{if(u=new Blob([o],{type:"application/json"}),navigator.sendBeacon(t,u))return Promise.resolve({sent:!0,ok:!0,status:200,transport:"beacon",uncertain:!0})}catch(e){}return function(e,t,n){var r,o;return"function"!=typeof fetch?Promise.resolve(null):(r="undefined"!=typeof AbortController?new AbortController:null,o=r&&n>0?setTimeout(function(){r.abort()},n):null,fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:t,keepalive:!0,signal:r?r.signal:void 0}).then(function(e){return o&&clearTimeout(o),{sent:!0,ok:e.ok,status:+(e.status||0),transport:"fetch"}}).catch(function(e){return o&&clearTimeout(o),e&&"object"==typeof e&&"AbortError"===e.name?{sent:!1,ok:!1,status:0,reason:"timeout",transport:"fetch"}:null}))}(t,o,c).then(function(n){return null!==n?n:function(t,n,r){return new Promise(function(o){try{if("undefined"==typeof XMLHttpRequest)return o({sent:!1,ok:!1,status:0,reason:"no_xhr"});var i=new XMLHttpRequest;i.open("POST",t,!0),i.timeout=r,i.setRequestHeader("Content-Type","application/json"),i.onreadystatechange=function(){4===i.readyState&&o({sent:!0,ok:i.status>=200&&300>i.status,status:i.status,transport:"xhr"})},i.onerror=function(){o({sent:!1,ok:!1,status:0,reason:"xhr_error"})},i.ontimeout=function(){o({sent:!1,ok:!1,status:0,reason:"timeout",transport:"xhr"})},i.send(n)}catch(e){o({sent:!1,ok:!1,status:0,reason:"xhr_exception"})}})}(t,o,c)})}function lt(e){e=e||{},this.selectedBatchEndpoint="",this.remoteEndpoints=[],this.remoteProbePromise=null,this.remoteProbeKey="",this.shouldPersistOnFail=!1,this.hasBatchProbeStarted=!1,this.initialized=!1,this.initPromise=null,this.endpointReadyListeners=[],this.timeoutMs=+e.pingTimeoutMs||1500}function dt(e){return 255&(e<<1^(128&e?27:0))}function _t(e){return(Mt[e>>>24&255]<<24|Mt[e>>>16&255]<<16|Mt[e>>>8&255]<<8|Mt[255&e])>>>0}function pt(e,t){var n,r,o,i,a,s,c,u,l,d,_,p,f=e.slice(0);for(o=[],i=0;16>i;i++)f[i]=255&(f[i]^t[i>>2]>>>24-8*(3&i));for(a=1;14>=a;a++){for(n=[],i=0;16>i;i++)n[i]=Mt[f[i]];for(r=[],c=0;4>c;c++)for(u=0;4>u;u++)r[c+4*u]=n[c+4*(u+c&3)];if(14>a){for(o=[],s=0;4>s;s++)d=r[4*s+1],_=r[4*s+2],p=r[4*s+3],o[4*s]=255&(dt(l=r[4*s])^dt(d)^d^_^p),o[4*s+1]=255&(l^dt(d)^dt(_)^_^p),o[4*s+2]=255&(l^d^dt(_)^dt(p)^p),o[4*s+3]=255&(dt(l)^l^d^_^dt(p));for(f=o,o=[],i=0;16>i;i++)f[i]=255&(f[i]^t[4*a+(i>>2)]>>>24-8*(3&i))}else for(f=r,i=0;16>i;i++)f[i]=255&(f[i]^t[4*a+(i>>2)]>>>24-8*(3&i))}return f}function ft(e,t){var n,r,o,i=[],a=t.slice(0);for(n=0;16>n;n++)i[n]=0;for(n=0;128>n;n++){if(e[n>>3]&128>>(7&n))for(r=0;16>r;r++)i[r]^=a[r];for(o=1&a[15],r=15;r>0;r--)a[r]=255&(a[r]>>>1|a[r-1]<<7);a[0]=a[0]>>>1&255,o&&(a[0]^=225)}return i}function ht(e){var t,n=e.slice(0);for(t=15;t>=12&&(n[t]=n[t]+1&255,!n[t]);t--);return n}function gt(e){this.encryptedConfigText=(e=e||{}).encryptedConfig||e.configCipher||"",this.cachedConfigPromise=null,this._secondGcmKeyBase64="en1BNo0VBrN/zi+mI2LO7E9W40ehCBYwC+frBn8s3rQ="}function mt(e,t){var n=((e&&e.event_id||"")+"").trim();return n?n+"|||"+(t||""):null}function yt(e){var t,n={},r=Object.keys(e);for(t=0;r.length>t;t++)n[r[t]]=e[r[t]];return n}function vt(){this.maxSize=tr}function wt(e){var t,n,r=e||{},o=r.im_behavior_key,i=qt[o];if(!i)throw(t=Error("[sdk][web] 未知 im_behavior_key:"+o)).name="EventValidationError",t.eventMethod="imEvent",t.typeErrorFields=["im_behavior_key(unknown)"],t;return n={},Object.keys(r).forEach(function(e){"im_behavior_name"!==e&&(n[e]=r[e])}),n.im_behavior_name=i,n}function kt(e,t){var n,r,o,i,a,s=Lt[e];if(!s)throw Error("[sdk][web] 未知事件方法:"+e);if(n=t||{},r={},o=[],i=[],Object.keys(s).forEach(function(e){var t,a=s[e],c=1&a,u=a>>1&3,l=!!(8&a),d=n[e];l&&null===d&&(d=""),!c||l||null!=d&&""!==d?c&&l&&void 0===d?o.push(e):(c||null!=d)&&(t=1===u?function(e){return"number"==typeof e&&isFinite(e)&&e===Math.floor(e)}(d):2===u?function(e){return"number"==typeof e&&isFinite(e)}(d):function(e){return"string"==typeof e}(d),t?r[e]=d:i.push(e+"(expected "+(1===u?"int":2===u?"number":"string")+")")):o.push(e)}),o.length||i.length)throw(a=Error("[sdk][web] 事件字段校验失败:"+e)).name="EventValidationError",a.eventMethod=e,a.missingFields=o.slice(),a.typeErrorFields=i.slice(),a;return r}function bt(){var e,t,n,r,o={};for(e=0;arguments.length>e;e++)if(t=arguments[e])for(n=Object.keys(t),r=0;n.length>r;r++)o[n[r]]=t[n[r]];return o}function Ct(e){var t,n,r={};if(!e||!Array.isArray(e))return r;for(t=0;e.length>t;t++)(n=((null==e[t]?"":e[t])+"").trim())&&(Vt[n]?r[n]=1:Kt('[sdk][web] disableAutoEvents 含未知事件 "'+n+'",已忽略'));return r}function St(e){var t,n=[],r=Object.keys(e||{});for(t=0;r.length>t;t++)n.push(r[t]);return n}function Pt(e,t){this.opts=e,this.resolver=t,this.queue=[],this.timer=null,this.flushTail=Promise.resolve(),this.replaying=!1,this.replayTimer=null,this.hasPersistentItems=!0,this.persistentQueue=new vt,this._resolveEndpoint=null,this._getEventCodes=null,this.startReplayTimer()}function Et(e){var t,n;if("undefined"==typeof window)return Kt("[web-sdk] 非浏览器环境(SSR/Node.js),SDK 仅支持浏览器,初始化已跳过。"),this._ssrSkipped=!0,this.options=e,this.configMgr=null,this.resolver=null,void(this.batcher=null);if(!Object.prototype.hasOwnProperty.call(e,"channel"))throw Error('channel 为必传字段(可传空字符串 "")');this._ssrSkipped=!1,(t=bt(e)).channel=null==e.channel?"":e.channel+"",this._disableAutoEvents=Ct(t.disableAutoEvents),t.disableAutoEvents=St(this._disableAutoEvents),this.options=t,this.configMgr=new gt(e),this.resolver=new lt({pingTimeoutMs:nr}),this.batcher=new Pt(this.options,this.resolver),n=this,this.batcher._resolveEndpoint=function(e){return n.resolver.resolveEndpoint(function(){return n.configMgr.getBatchEndpoints()},e)},this.batcher._getEventCodes=function(){return n.configMgr.getEventCodes()},this.batcher._getSigningConfig=function(){return n.configMgr.getSigningConfig()},this._unsubReady=null,this._refreshTimer=null,this._autoLifecycleTeardown=null,this._webLoadTeardown=null,this._autoNetworkTeardown=null,this._pageTrackerTeardown=null,this._webInteractionTeardown=null,this._sessionPollTimer=null,this._autoEventsStarted=!1,this._init({}),this._startAutoEvents()}function Tt(e){if(!Ht)throw Error("请先调用 init() 初始化(参数须包含 channel)");return Ht}function xt(e,t,n){var r,o,i;if(n>=e.length)return Promise.resolve();if(!Ht||Ht!==t){for(o=(r=e.slice(n)).length-1;o>=0;o--)Ut.unshift(r[o]);return Promise.resolve()}return t.track((i=e[n]).event,i.endpoint).catch(function(){}).then(function(){return xt(e,t,n+1)})}function Dt(e,t){var n;return Ht?(n=Ht,(Xt||Promise.resolve()).then(function(){return n.track(e,t)})):(Ut.push({event:e,endpoint:t}),Ut.length>500&&Ut.shift(),Promise.resolve({sent:!1,ok:!0,status:0,reason:"pre_init_cached"}))}var Mt,It,At,Ot,Lt,Rt,qt,Bt,Kt,Vt,jt,Nt,Ft,Ht,Ut,Xt,Yt,Gt,zt=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],Wt="3.0.1",Jt="sig_v2",Qt=["os_family","browser_kernel","browser_platform","screen_width","screen_height","screen_dpi","device_pixel_ratio","color_depth","max_touch_points","cpu_cores","cookie_enabled","local_storage_enabled","session_storage_enabled","indexeddb_enabled","service_worker_enabled","canvas_fp","webgl_fp","audio_fp","fonts_hash","plugins_hash","mimes_hash"],$t=["Arial","Arial Black","Comic Sans MS","Courier New","Georgia","Impact","Times New Roman","Trebuchet MS","Verdana","Microsoft YaHei","SimHei","SimSun","PingFang SC","Helvetica","Roboto","Monaco","Menlo","Consolas","Palatino Linotype","Tahoma","Lucida Console","Candara","Futura","Geneva"],Zt=[{kind:"video",type:'video/mp4; codecs="avc1.42E01E"'},{kind:"video",type:'video/webm; codecs="vp8, vorbis"'},{kind:"video",type:'video/ogg; codecs="theora"'},{kind:"video",type:"video/mp4"},{kind:"video",type:"video/webm"},{kind:"audio",type:"audio/mpeg"},{kind:"audio",type:'audio/ogg; codecs="vorbis"'},{kind:"audio",type:'audio/wav; codecs="1"'},{kind:"audio",type:"audio/mp4"},{kind:"audio",type:"audio/aac"}],en={cold_start:1,web_load:1,warm_start:1,foreground:1,background:1,web_unload:1,terminate:1,unknown:1},tn="__web_sdk_sid__",nn="__web_sdk_sid_meta__",rn="__web_sdk_sid__",on="__web_sdk_sid_v1",an=18e5,sn=216e5,cn=null,un=!1,ln="__web_sdk_device_id__",dn="__web_sdk_ping__",_n=null,pn="__web_sdk_device_fp__",fn=null,hn="",gn=null,mn=function(){var e="";return{append:function(t,n){var r=((t||"")+"").trim();r&&(e+=r+"|"+(null!=n?n+"":"")+";")},digest:function(){return d(e)},reset:function(){e=""}}}(),yn=50,vn=16384,wn="__web_sdk_pending_session_summaries__",kn="__web_sdk_session_metrics_v1__",bn="__web_sdk_summary_claimed__:",Cn=!1,Sn="",Pn={},En={},Tn={sdk_init:1,app_lifecycle:1,app_session_summary:1,network_state_change:1},xn={eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null},Dn=Date.now(),Mn=0,In=0,An=[],On=!1,Ln="",Rn="",qn=null,Bn={app_page_view:1,page_click:1},Kn="web_load",Vn="foreground",jn="background",Nn="web_unload",Fn=null,Hn=0,Un=0,Xn=0,Yn=0,Gn=null,zn=!1,Wn=null,Jn=!1,Qn=1e4,$n="1.2.0",Zn=5e3,er=3e4,tr=2e3,nr=1500,rr=!1;if(lt.prototype.normalizeBatchEndpoints=function(e){return(Array.isArray(e)?e:[]).map(function(e){return((e||"")+"").trim()}).filter(Boolean).filter(function(e){return/^https:\/\//i.test(e)}).map(function(e){var t=e.match(/^(https?:\/\/[^/?#]+)/i);return(t?t[1]:e.replace(/\/+$/,""))+"/api/eventTracking/batchReport.json"})},lt.prototype._withTimeout=function(e,t){return new Promise(function(n,r){var o=setTimeout(function(){r(Error("timeout"))},t);e.then(function(e){clearTimeout(o),n(e)}).catch(function(e){clearTimeout(o),r(e)})})},lt.prototype._xhrRace=function(t,n){return t.length?new Promise(function(r){function o(){for(var t=0;c.length>t;t++)try{c[t].abort()}catch(e){}}var i,a=t.length,s=!1,c=[];for(i=0;t.length>i;i++)(function(t){var i,u,l;try{function d(e){l||(l=!0,u&&(clearTimeout(u),u=null),e&&!s?(s=!0,o(),r(t)):e||(a--,s||0!==a||r("")))}i=new XMLHttpRequest,u=null,l=!1,c.push(i),u=setTimeout(function(){d(!1)},n),i.open("POST",t,!0),i.setRequestHeader("Content-Type","application/json"),i.onreadystatechange=function(){4===i.readyState&&d(i.status>0)},i.onerror=function(){d(!1)},i.send("[]")}catch(e){a--,s||0!==a||r("")}})(t[i])}):Promise.resolve("")},lt.prototype._pickFastest=function(t,n){function r(){if(o)for(var t=0;i.length>t;t++)try{i[t].abort()}catch(e){}}var o,i,a=this;return"undefined"==typeof fetch?a._xhrRace(t,n):t.length?(o="undefined"!=typeof AbortController,i=[],new Promise(function(e){var s,c=t.length,u=!1;for(s=0;t.length>s;s++)(function(t){var s,l=o?new AbortController:null;l&&i.push(l),s={method:"POST",mode:"cors",headers:{"Content-Type":"application/json"},body:"[]",cache:"no-store"},l&&(s.signal=l.signal),a._withTimeout(fetch(t,s),n).then(function(n){n&&n.status>0?u||(u=!0,r(),e(t)):(c--,u||0!==c||e(""))}).catch(function(){c--,u||0!==c||e("")})})(t[s])})):Promise.resolve("")},lt.prototype._notifyReady=function(){var t=this.selectedBatchEndpoint;this.endpointReadyListeners.forEach(function(n){try{n(t)}catch(e){}})},lt.prototype._setSelectedEndpoint=function(e){var t=(e||"")+"";t&&t!==this.selectedBatchEndpoint&&(this.selectedBatchEndpoint=t,this._notifyReady())},lt.prototype.startRemoteProbe=function(e){var t,n,r=e.join("|");return e.length?this.remoteProbePromise&&this.remoteProbeKey===r?this.remoteProbePromise:(this.remoteProbeKey=r,this.shouldPersistOnFail=!1,t=this,n=this._pickFastest(e,this.timeoutMs).then(function(r){if(t.remoteProbePromise!==n)return t.selectedBatchEndpoint;if(t.remoteProbePromise=null,r)t._setSelectedEndpoint(r);else if(!t.selectedBatchEndpoint&&e.length){t.shouldPersistOnFail=!0;var o=e[Math.floor(Math.random()*e.length)];t._setSelectedEndpoint(o)}return t.selectedBatchEndpoint}).catch(function(){return t.remoteProbePromise===n&&(t.remoteProbePromise=null),t.selectedBatchEndpoint}),this.remoteProbePromise=n,n):Promise.resolve("")},lt.prototype.refreshDomainConfig=function(e){var t=this;return e().then(function(e){var n=t.normalizeBatchEndpoints(e);return t.remoteEndpoints=n,n.length?(t.hasBatchProbeStarted=!0,t.startRemoteProbe(n)):t.selectedBatchEndpoint})},lt.prototype.init=function(e){return this.initialized?Promise.resolve():(this.initPromise||(this.initialized=!0,this.initPromise=this.refreshDomainConfig(e).catch(function(){})),this.initPromise)},lt.prototype.resolveEndpoint=function(e,t){if(t)return Promise.resolve(t);var n=this;return this.init(e).then(function(){return n.selectedBatchEndpoint||""})},lt.prototype.onEndpointReady=function(e){if("function"!=typeof e)return function(){};this.endpointReadyListeners.push(e);var t=this;return function(){var n=t.endpointReadyListeners.indexOf(e);0>n||t.endpointReadyListeners.splice(n,1)}},lt.prototype.consumeProbeFailurePersistSignal=function(){return!!this.shouldPersistOnFail&&(this.shouldPersistOnFail=!1,!0)},lt.prototype.getEndpointState=function(){return this.selectedBatchEndpoint?"ready":this.remoteEndpoints.length>0?"probing":"unavailable"},lt.prototype.getSelectedBatchEndpoint=function(){return this.selectedBatchEndpoint},Mt=function(){var e,t=[];for(e=0;1272>e;e+=2)t.push(parseInt("637c777bf26b6fc53001672bed2a28a248888da76f3248a3043813899432c6b326fb8d0d37e84760dad3574759e0b764532a380602b1fc71a9e7f97d3b6415c21d02b5612440a4500bf9c043d33e085f45c20e0b65124603dfa8cd7a76b65c11b829e69a4e7f7872c6c7150b6a0f0801d163ae27d4d6cd5d19e77a9b1b48aaf2428594d3752f1d67250bab67676203b9b40df3d7cdf7eebf4b1bd5b6e16416be39a4c4a56e644726087d013b8ea78a6750d8b0339b871ee1b6a9ab66c8032cc1ac2462c695459dc034deae0678ae1779d9e7abce45d167e719b7776c73222f3c7e3cc11ae6da5cd659b79ce60ca82ec97f6059f674c029a68db0853dc22bb6518048ed4f66256d430b74de2f1a95f0589c341befce783e7c6d9dc7dd8e21cb643ee710b3c11329260195e0c0a956f14f970a9e61e594c663f68266a11045baf84a11e051ea5b6a42067253b2d37392c3516e1fc226a5cd4ab8990c475651a4698910219b94e2eb7d2b7129f38c94e3b1246a3298fef32dcae306137c39e1f0e32fe97d11b986c41a55bd5351319f7f078010fa81eb86b8a0556f880000e3630c6a0b136307027bf70e1b5722632937a93b53bf2d240e3949fa5a6e4e6a2a6f4d5b43a39d051df60bc98f25ff3130c31fd85b6e4167b276aed3276b70c3e527f2f2963b60554543e32e1f0c1754766b610c20b0ffe75d3fb88f6fa49605d6a3d6ca3022ffc62f5c8d297b65c778da96c8941ac9c55845e094f1c4a020bff5bb01a0cb04f5a8ac42b1cb41c38d27c1e1358256f3e99e3295a6e3b8c9c0481ed49af7d43f580a8571a0b13162171ebe8666ee5365e9ce6bef0a682080268b3bb67a1076a5776a996c26c2c8c12c7490c356348d10351b5279fa09851ebf".substr(e,2),16));return t}(),It=[1,2,4,8,16,32,64,128,27,54],At="__web_sdk_remote_config_cache__",gt.prototype.setEncryptedConfig=function(e){this.encryptedConfigText!==e&&(this.encryptedConfigText=e,this.cachedConfigPromise=null)},gt.prototype._tryParse=function(t){try{return JSON.parse(t)}catch(e){return t}},gt.prototype._unwrap=function(e,t){var n,r,o;for(t=t||3,n=e,r=0;t>r&&"string"==typeof n&&(o=this._tryParse(n))!==n;r++)n=o;return n},gt.prototype._toStringArray=function(e){var t,n=this._unwrap(e);return Array.isArray(n)?n.map(function(e){return((e||"")+"").trim()}).filter(Boolean):"string"==typeof n&&n.trim()?(t=this._tryParse(n),Array.isArray(t)?t.map(function(e){return((e||"")+"").trim()}).filter(Boolean):[n.trim()]):[]},gt.prototype._getStorage=function(){try{return"undefined"!=typeof window&&window.localStorage?window.localStorage:null}catch(e){return null}},gt.prototype._readPersisted=function(){var t,n,r;try{return(t=this._getStorage())&&(n=t.getItem(At))?(r=JSON.parse(n),Date.now()-(r.updatedAt||0)>24e5?{}:this._mapPayload(r)):{}}catch(e){return{}}},gt.prototype._mapPayload=function(e){if(!e||"object"!=typeof e)return{};var t=e.data&&"object"==typeof e.data?e.data:e;return{batchUrls:this._toStringArray(null!=t.batchUrls?t.batchUrls:t.domainList),eventCodes:this._toStringArray(null!=t.eventCodes?t.eventCodes:t.eventList),signKey:(t.signKey||t.hmacKey||t.eventSignKey||"")+"",securityKeyVersion:(t.securityKeyVersion||t.signKeyVersion||"")+""}},gt.prototype._persist=function(t){try{var n=this._getStorage();if(!n)return;n.setItem(At,JSON.stringify({batchUrls:this._toStringArray(t.batchUrls),eventCodes:this._toStringArray(t.eventCodes),signKey:(t.signKey||"")+"",securityKeyVersion:(t.securityKeyVersion||"")+"",updatedAt:Date.now()}))}catch(e){}},gt.prototype._base64ToBytes=function(e){var t,n,r,o,i=((e||"")+"").trim().replace(/-/g,"+").replace(/_/g,"/"),a=i.length%4?4-i.length%4:0;for(t=0;a>t;t++)i+="=";if("undefined"!=typeof atob&&"undefined"!=typeof Uint8Array){for(n=atob(i),r=new Uint8Array(n.length),o=0;n.length>o;o++)r[o]=n.charCodeAt(o);return r}return new Uint8Array(0)},gt.prototype._decryptAes256Gcm=function(t,n){var r,o,i,a,s,c,u;try{if(28>(r=this._base64ToBytes(t)).length)return Promise.reject(Error("密文过短"));if(o=this._base64ToBytes(n),i=r.slice(0,12),a=r.slice(12),"undefined"!=typeof crypto&&crypto.subtle)return crypto.subtle.importKey("raw",o,{name:"AES-GCM"},!1,["decrypt"]).then(function(e){return crypto.subtle.decrypt({name:"AES-GCM",iv:i},e,a)}).then(function(e){return new Uint8Array(e)});if(!(s=function(e,t,n){var r,o,i,a,s,c,u,l,d,_,p,f,h,g,m=[],y=[],v=[],w=[];for(r=0;32>r;r++)m[r]=e[r]||0;for(r=0;12>r;r++)y[r]=t[r]||0;if(0>(i=n.length-16))return null;for(r=0;i>r;r++)v[r]=n[r];for(r=0;16>r;r++)w[r]=n[i+r];for(a=function(e){var t,n,r=[];for(t=0;8>t;t++)r[t]=(e[4*t]<<24|e[4*t+1]<<16|e[4*t+2]<<8|e[4*t+3])>>>0;for(t=8;60>t;t++)n=r[t-1],t%8?t%8==4&&(n=_t(n)):n=(_t((n<<8|n>>>24)>>>0)^It[t/8-1]<<24)>>>0,r[t]=(r[t-8]^n)>>>0;return r}(m),s=[],r=0;16>r;r++)s[r]=0;for(c=pt(s,a),u=y.slice(0),r=12;15>r;r++)u[r]=0;for(u[15]=1,l=pt(u,a),d=ht(u),_=[],r=0;i>r;r+=16)for(p=pt(d,a),d=ht(d),o=0;16>o&&i>r+o;o++)_[r+o]=v[r+o]^p[o];for(f=v.slice(0);15&f.length;)f.push(0);for(r=0;8>r;r++)f.push(0);for(h=8*i,f.push(0,0,0,0),f.push(h>>>24&255,h>>>16&255,h>>>8&255,255&h),g=function(e,t){var n,r,o=[];for(n=0;16>n;n++)o[n]=0;for(n=0;t.length>n;n+=16){for(r=0;16>r;r++)o[r]^=t[n+r]||0;o=ft(o,e)}return o}(c,f),r=0;16>r;r++)if((l[r]^g[r])!==w[r])return null;return _}(o,i,a)))return Promise.reject(Error("AES-GCM 认证标签校验失败"));if("undefined"!=typeof Uint8Array)for(c=new Uint8Array(s.length),u=0;s.length>u;u++)c[u]=s[u];else c=s;return Promise.resolve(c)}catch(e){return Promise.reject(e)}},gt.prototype.fetchAndDecryptConfig=function(){var e,t,n;return this.cachedConfigPromise?this.cachedConfigPromise:(e=this,t=this._readPersisted(),this.encryptedConfigText?(n=this._decryptAes256Gcm(this.encryptedConfigText,this._secondGcmKeyBase64).then(function(t){var n,r,o,i;if("undefined"!=typeof TextDecoder)n=(new TextDecoder).decode(t);else{for(r=[],o=0;t.length>o;o++)r[o]=t[o];n=String.fromCharCode.apply(null,r)}return i=e._mapPayload(JSON.parse(n)),e._persist(i),i}).catch(function(e){return void 0!==console&&console.warn&&console.warn("[sdk][config] 解密失败:",e),t}),this.cachedConfigPromise=n,n):(this.cachedConfigPromise=Promise.resolve(t),this.cachedConfigPromise))},gt.prototype.getBatchEndpoints=function(){return this.fetchAndDecryptConfig().then(function(e){return e.batchUrls||[]})},gt.prototype.getEventCodes=function(){return this.fetchAndDecryptConfig().then(function(e){return Array.isArray(e.eventCodes)&&e.eventCodes.length>0?e.eventCodes:[]})},gt.prototype.getSigningConfig=function(){return this.fetchAndDecryptConfig().then(function(e){return{hmacKey:(e.signKey||"")+"",securityKeyVersion:(e.securityKeyVersion||"")+""}})},gt.prototype.prewarm=function(){this.fetchAndDecryptConfig().catch(function(){})},gt.prototype.forceRefresh=function(){this.cachedConfigPromise=null},Ot="events",vt.prototype.isAvailable=function(){return"undefined"!=typeof indexedDB&&"undefined"!=typeof window&&null!=window.indexedDB},vt.prototype.openDb=function(){var t=this;return new Promise(function(n,r){if(!t.isAvailable())return n(null);var o=indexedDB.open("web_sdk_event_queue",3);o.onerror=function(){r(o.error||Error("open indexedDB failed"))},o.onupgradeneeded=function(t){var n,r,i,a,s=o.result,c=t.oldVersion,u=t.target.transaction;if(!s.objectStoreNames.contains(Ot))return(n=s.createObjectStore(Ot,{keyPath:"id",autoIncrement:!0})).createIndex("createdAt","createdAt",{unique:!1}),void n.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!0});2>c&&u&&((r=u.objectStore(Ot)).indexNames.contains("pendingDedupeKey")||r.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!1})),3>c&&u&&((i=u.objectStore(Ot)).indexNames.contains("pendingDedupeKey")&&i.deleteIndex("pendingDedupeKey"),(a=i.getAll()).onsuccess=function(){var t,n,r,o,s,c,u,l=a.result||[],d={};for(t=0;l.length>t;t++)(r=(n=l[t]).pendingDedupeKey||mt(n.record,n.endpointOverride))&&(d[r]||(d[r]=[]),d[r].push(n.id));for(o=Object.keys(d),s=0;o.length>s;s++)if((c=d[o[s]]).length>=2)for(c.sort(function(e,t){return e-t}),u=1;c.length>u;u++)i.delete(c[u]);try{i.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!0})}catch(e){}},a.onerror=function(){try{i.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!0})}catch(e){}})},o.onsuccess=function(){n(o.result)}})},vt.prototype._txDone=function(e){return new Promise(function(t,n){e.oncomplete=function(){t()},e.onerror=function(){n(e.error||Error("tx failed"))},e.onabort=function(){n(e.error||Error("tx aborted"))}})},vt.prototype._trimOverflow=function(e){var t=this,n=Ot,r=e.transaction(n,"readonly");return new Promise(function(e,t){var o=r.objectStore(n).count();o.onsuccess=function(){e(+(o.result||0))},o.onerror=function(){t(o.error)}}).then(function(e){return t._txDone(r).then(function(){return e})}).then(function(r){var o,i,a,s;if(r>t.maxSize)return o=r-t.maxSize,i=e.transaction(n,"readwrite"),a=i.objectStore(n).index("createdAt"),s=0,new Promise(function(e,t){var r=a.openCursor();r.onerror=function(){t(r.error)},r.onsuccess=function(){var t=r.result;if(!t||s>=o)return e();i.objectStore(n).delete(t.primaryKey),s++,t.continue()}}).then(function(){return t._txDone(i)})})},vt.prototype.enqueueMany=function(e){var t=this;return e&&e.length?this.openDb().then(function(n){var r,o;if(n)return r=Date.now(),o=e.map(function(e,t){var n=e&&"object"==typeof e&&"record"in e?e:{record:e},o=mt(n.record,n.endpointOverride||""),i={createdAt:r+t,record:n.record,endpointOverride:n.endpointOverride||"",retryCount:0,nextRetryAt:0};return o&&(i.pendingDedupeKey=o),i}),new Promise(function(e,t){var r,i=n.transaction(Ot,"readwrite"),a=i.objectStore(Ot);i.oncomplete=function(){e()},i.onerror=function(){t(i.error)},i.onabort=function(){t(i.error)},r=0,function e(){var n,i;o.length>r&&(n=o[r++],(i=a.add(n)).onsuccess=function(){e()},i.onerror=function(n){var r=i.error;if(r&&"ConstraintError"===r.name)return n.preventDefault(),void e();t(r||Error("add failed"))})}()}).then(function(){return t._trimOverflow(n)}).then(function(){n.close()}).catch(function(e){throw n.close(),e})}):Promise.resolve()},vt.prototype.readOldest=function(e){return e=e||20,this.openDb().then(function(t){var n,r;return t?(n=Date.now(),r=[],new Promise(function(o,i){var a=t.transaction(Ot,"readonly").objectStore(Ot).index("createdAt").openCursor();a.onerror=function(){i(a.error)},a.onsuccess=function(){var t,i,s,c,u,l=a.result;if(!l||r.length>=e)return o();(s=isFinite(i=+(t=l.value).nextRetryAt)?Math.floor(i):0)>n||((c=yt(t)).endpointOverride=t.endpointOverride||"",c.retryCount=isFinite(u=+t.retryCount)?Math.floor(u):0,c.nextRetryAt=s,r.push(c)),l.continue()}}).then(function(){return t.close(),r}).catch(function(e){throw t.close(),e})):[]})},vt.prototype.removeByIds=function(e){var t=this;return e&&e.length?this.openDb().then(function(n){var r,o,i;if(n){for(o=(r=n.transaction(Ot,"readwrite")).objectStore(Ot),i=0;e.length>i;i++)o.delete(e[i]);return t._txDone(r).then(function(){n.close()}).catch(function(e){throw n.close(),e})}}):Promise.resolve()},vt.prototype.updateRetryMeta=function(e,t,n){var r=this;return e&&e.length?this.openDb().then(function(o){var i,a,s,c,u,l,d,_,p,f,h,g,m,y,v;if(!o)return{dropped:0,kept:0};for(i=Date.now(),s=isFinite(a=+t)?Math.max(0,Math.floor(a)):0,u=isFinite(c=+n)?Math.max(0,Math.floor(c)):0,l=0,d=0,p=(_=o.transaction(Ot,"readwrite")).objectStore(Ot),f=0;e.length>f;f++)u<(m=(isFinite(g=+(h=e[f]).retryCount)?Math.floor(g):0)+1)?(l++,p.delete(h.id)):(d++,y=h.pendingDedupeKey||mt(h.record,h.endpointOverride||""),v=yt(h),y&&(v.pendingDedupeKey=y),v.retryCount=m,v.nextRetryAt=i+s,p.put(v));return r._txDone(_).then(function(){return o.close(),{dropped:l,kept:d}}).catch(function(e){throw o.close(),e})}):Promise.resolve({dropped:0,kept:0})},vt.prototype.count=function(){var e=this;return this.openDb().then(function(t){if(!t)return 0;var n=t.transaction(Ot,"readonly");return new Promise(function(e,t){var r=n.objectStore(Ot).count();r.onsuccess=function(){e(+(r.result||0))},r.onerror=function(){t(r.error)}}).then(function(r){return e._txDone(n).then(function(){return t.close(),r})}).catch(function(e){throw t.close(),e})})},Lt={navigation:{navigation_key:1,navigation_name:1},appPageView:{user_type:1,page_key:1,page_name:1,referrer_page_key:9,referrer_page_name:9,current_page_key:1,current_page_name:1,page_load_time:3,recommend_trace_id:0},appInstall:{trace_id:1,attribution_flag:2},pageClick:{page_key:1,page_name:1,click_page_x:3,click_page_y:3,click_x_percent:3,click_y_percent:3,screen_width:3,screen_height:3,recommend_trace_id:0},recommendListView:{page_key:1,page_name:1,recommend_content_type:1,recommend_trace_id:0,recommend_id:1,recommend_trace_info:0,client_version:0},recommendListClick:{page_key:1,page_name:1,recommend_content_type:1,recommend_trace_id:0,recommend_id:1,recommend_trace_info:0,client_version:0},advertising:{event_type:1,advertising_key:1,advertising_name:1,advertising_id:1},videoEvent:{media_id:0,video_id:1,video_title:1,video_type_id:1,video_type_name:1,video_content_type:9,recommend_trace_id:0,video_tag_key:9,video_tag_name:9,video_duration:3,play_duration:3,play_progress:3,play_speed:3,video_behavior_key:1,video_behavior_name:1,post_id:0},novelEvent:{media_id:0,novel_id:1,novel_title:1,chapter_id:0,chapter_name:0,novel_type_id:1,novel_type_name:1,recommend_trace_id:0,novel_tag_key:9,novel_tag_name:9,read_progress:3,page_no:3,novel_behavior_key:1,novel_behavior_name:1},comicEvent:{media_id:0,comic_id:1,comic_title:1,chapter_id:0,chapter_name:0,comic_type_id:1,comic_type_name:1,recommend_trace_id:0,comic_tag_key:9,comic_tag_name:9,read_progress:3,page_no:3,comic_behavior_key:1,comic_behavior_name:1},keywordSearch:{keyword:1,search_result_count:3,search_content_type:0,search_trace_id:0,search_id:0},keywordClick:{keyword:1,click_item_id:1,click_item_type_key:1,click_item_type_name:1,click_position:3,search_trace_id:0},adImpression:{page_key:1,page_name:1,ad_slot_key:1,ad_slot_name:1,ad_id:1,creative_id:0,ad_type:0},adClick:{page_key:1,page_name:1,ad_slot_key:1,ad_slot_name:1,ad_id:1,creative_id:0,ad_type:0},appPageitemExposure:{item_id:1,item_type:1,item_name:1,parent_item_id:1,page_key:1,page_name:1,referrer_page_key:9,referrer_page_name:9,recommend_trace_id:9,recommend_id:9,recommend_trace_info:9},appPageitemClick:{item_id:1,item_type:1,item_name:1,parent_item_id:1,position:3,page_key:1,page_name:1,recommend_trace_id:9,recommend_id:9,recommend_trace_info:9},adRequest:{request_id:1,ad_slot_key:1},adFill:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,fill_count:3,is_skippable:3,is_closable:3},adNoFill:{request_id:1,ad_slot_key:1,ad_type:1,reason:1},adLoadSuccess:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,load_duration:3},adImpressionSdk:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,material_size:1,is_skippable:3,is_closable:3},adClickSdk:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1},adClose:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,close_type:1},adError:{trace_id:1,request_id:0,ad_slot_key:1,ad_type:1,ad_id:9,error_code:1,error_msg:1,error_stage:1},imEvent:{conversation_id:1,conversation_type:1,current_uid:1,target_uid:1,target_role_key:1,target_role_name:1,message_id:9,message_type:9,im_behavior_key:1,im_behavior_name:1},imConversationView:{current_uid:1,conversation_count:3,total_unread_count:3,normal_conversation_count:3,archived_count:3,pinned_count:3},imConnectionSuccess:{current_uid:9,im_sdk_version:1},imMessageContent:{conversation_id:1,conversation_type:1,current_uid:1,target_uid:1,sender_uid:1,message_id:1,message_type:1,message:9,message_content_length:3,content_hash:9},postEvent:{media_id:0,post_id:1,post_title:1,post_create_time:3,post_user_id:1,post_user_nick:1,post_type_id:1,post_type_name:1,post_content_type:1,recommend_trace_id:9,post_tag_key:9,post_tag_name:9,post_behavior_key:1,post_behavior_name:1},dramaEvent:{media_id:0,drama_id:1,drama_title:1,episode:3,total_episode:3,video_id:1,drama_type_id:1,drama_type_name:1,drama_content_type:1,recommend_trace_id:9,drama_tag_key:9,drama_tag_name:9,drama_duration:3,play_duration:3,play_progress:3,play_speed:3,play_behavior_key:1,play_behavior_name:1},dramaStatusChange:{media_id:0,drama_id:1,drama_title:1,episode:3,video_id:1,pre_episode:3,drama_behavior_key:1,drama_behavior_name:1}},qt={message_send:"发送消息",message_receive:"收到消息",message_read:"消息已读",message_recall:"消息撤回",conversation_pin:"置顶会话",conversation_unpin:"取消置顶",conversation_archive:"归档会话",conversation_unarchive:"取消归档"},Bt=Object.keys(Rt={navigation:"navigation",appPageView:"app_page_view",appInstall:"app_install",pageClick:"page_click",recommendListView:"recommend_list_view",recommendListClick:"recommend_list_click",advertising:"advertising",videoEvent:"video_event",novelEvent:"novel_event",comicEvent:"comic_event",keywordSearch:"keyword_search",keywordClick:"keyword_click",adImpression:"ad_impression",adClick:"ad_click",appPageitemExposure:"app_pageitem_exposure",appPageitemClick:"app_pageitem_click",adRequest:"ad_request",adFill:"ad_fill",adNoFill:"ad_no_fill",adLoadSuccess:"ad_load_success",adImpressionSdk:"ad_impression_sdk",adClickSdk:"ad_click_sdk",adClose:"ad_close",adError:"ad_error",imEvent:"im_event",imConversationView:"im_conversation_view",imConnectionSuccess:"im_connection_success",imMessageContent:"im_message_content",postEvent:"post_event",dramaEvent:"drama_event",dramaStatusChange:"drama_status_change"}).reduce(function(e,t){var n=Rt[t];return e[t]="imEvent"===t?function(e){return{event:n,payload:kt(t,wt(e))}}:function(e){return{event:n,payload:kt(t,e||{})}},e},{}),Kt=void 0!==console&&console.warn?function(e){console.warn(e)}:function(){},Vt={sdk_init:1,app_lifecycle:1,network_state_change:1,app_session_summary:1,app_page_view:1,page_click:1},jt=6e4,Nt="__web_sdk_recent_ok_event_ids__",Ft=function(){function t(e){var t,n=Object.keys(r);for(t=0;n.length>t;t++)e-r[n[t]]>jt&&delete r[n[t]]}function n(){try{"undefined"!=typeof window&&window.localStorage&&window.localStorage.setItem(Nt,JSON.stringify(r))}catch(e){}}var r={};if(function(){var t,n,o,i,a;try{if("undefined"!=typeof window&&window.localStorage&&(t=window.localStorage.getItem(Nt)))for(n=JSON.parse(t),o=Date.now(),i=Object.keys(n),a=0;i.length>a;a++)/^[0-9a-f]{32}$/.test(i[a])&&(o-n[i[a]]>jt||(r[i[a]]=n[i[a]]))}catch(e){}}(),"undefined"!=typeof window)try{window.addEventListener("storage",function(e){var t,n,o,i,a;try{if(e.key!==Nt||!e.newValue)return;for(t=JSON.parse(e.newValue),n=Date.now(),o=Object.keys(t),i=0;o.length>i;i++)a=t[o[i]],/^[0-9a-f]{32}$/.test(o[i])&&(n-a>jt||(r[o[i]]=a))}catch(s){}})}catch(e){}return{has:function(e){var n,o;return!!e&&(t(n=Date.now()),void 0!==(o=r[e])&&jt>=n-o)},set:function(e){var o,i,a,s,c,u=Date.now();for(t(u),o=0;e.length>o;o++)e[o]&&(r[e[o]]=u);if((i=Object.keys(r)).length>500){for(a="",s=1/0,c=0;i.length>c;c++)s>r[i[c]]&&(s=r[i[c]],a=i[c]);a&&delete r[a]}n()},reset:function(){r={},n()}}}(),Pt.prototype.startReplayTimer=function(){var e=this;this.replayTimer=setInterval(function(){e.replayPersistent()},Zn)},Pt.prototype._stopTimer=function(){this.timer&&(clearTimeout(this.timer),this.timer=null)},Pt.prototype._settleItem=function(e,t){e.settled||(e.settled=!0,e.timeoutTimer&&(clearTimeout(e.timeoutTimer),e.timeoutTimer=null),e.resolve(t))},Pt.prototype._purgeSettled=function(){this.queue.length&&(this.queue=this.queue.filter(function(e){return!e.settled}))},Pt.prototype._toPersistentEntries=function(e){return e.map(function(e){return{record:e.record,endpointOverride:e.endpointOverride||"",retryCount:0,nextRetryAt:0}})},Pt.prototype._getRetryDelay=function(e){return Math.min(6e4,Math.max(Zn,Math.floor(Zn*Math.pow(2,Math.min(10,Math.max(0,e))))))},Pt.prototype.schedule=function(){var e=this;this._purgeSettled(),!this.timer&&this.queue.length&&(this.timer=setTimeout(function(){e.timer=null,e.flush().catch(function(){})},5e3))},Pt.prototype.flush=function(){var e=this,t=this.flushTail.then(function(){return e.flushOnce()});return this.flushTail=t.catch(function(){}),t},Pt.prototype._processFlushGroup=function(e,t,n){var r,o,i,a,s,c,u,l,d,_,p,f=this,h="__default__"===e?"":e,g=[],m=[];for(r=0;t.length>r;r++)((i=(((o=t[r]).record&&o.record.event_id||"")+"").trim())&&Ft.has(i)?m:g).push(o);for(a=0;m.length>a;a++)f._settleItem(m[a],{sent:!1,ok:!1,status:0,reason:"cross_batch_recent_success_deduped"});if(!g.length)return Promise.resolve();if(!f._resolveEndpoint){for(Kt("[sdk][web] _resolveEndpoint 未注入,事件无法发送"),s=0;g.length>s;s++)f._settleItem(g[s],{sent:!1,ok:!1,status:0,reason:"no_endpoint_resolver"});return Promise.resolve()}for(c={},u=[],l={},d=0;g.length>d;d++)c[p=(((_=g[d]).record&&_.record.event_id||"")+"").trim()||"__empty__:"+d]||(c[p]=!0,u.push(_.record),l[d]=!0);return f.resolver.consumeProbeFailurePersistSignal()?f.persistentQueue.enqueueMany(f._toPersistentEntries(g)).then(function(){f.hasPersistentItems=!0;for(var e=0;g.length>e;e++)f._settleItem(g[e],{sent:!1,ok:!1,status:0,reason:"probe_all_failed"})}).catch(function(){}):f._resolveEndpoint(h).then(function(e){var t,r,o,i,a,s;if(e){for(r=[],o=0;u.length>o;o++)if((i=((u[o]&&u[o].event_id||"")+"").trim())&&Ft.has(i)){for(a=0;g.length>a;a++)if(l[a]&&((g[a].record&&g[a].record.event_id||"")+"").trim()===i){f._settleItem(g[a],{sent:!1,ok:!1,status:0,reason:"cross_batch_recent_success_deduped"}),l[a]=!1;break}}else r.push(u[o]);if(!r.length){for(s=0;g.length>s;s++)l[s]||f._settleItem(g[s],{sent:!1,ok:!1,status:0,reason:"batch_deduped_duplicate"});return Promise.resolve()}return ut(e,r,{useBeacon:rr}).then(function(e){var t,n,o,i,a,s=Promise.resolve();if(e&&e.ok){for(t=[],n=0;r.length>n;n++)(o=((r[n]&&r[n].event_id||"")+"").trim())&&t.push(o);Ft.set(t)}else{for(i=[],a=0;g.length>a;a++)l[a]&&i.push(g[a]);s=f.persistentQueue.enqueueMany(f._toPersistentEntries(i)).then(function(){f.hasPersistentItems=!0}).catch(function(){})}return s.then(function(){for(var t=0;g.length>t;t++)f._settleItem(g[t],l[t]?e:{sent:!1,ok:!1,status:0,reason:"batch_deduped_duplicate"})})})}if("unavailable"===f.resolver.getEndpointState())return f.persistentQueue.enqueueMany(f._toPersistentEntries(g)).then(function(){f.hasPersistentItems=!0;for(var e=0;g.length>e;e++)f._settleItem(g[e],{sent:!1,ok:!1,status:0,reason:"endpoint_unavailable"})}).catch(function(){});for(t=0;g.length>t;t++)n.push(g[t])})},Pt.prototype.flushOnce=function(){var e,t,n,r,o,i,a,s,c,u=this;if(this._purgeSettled(),!this.queue.length)return Promise.resolve();for(this._stopTimer(),e=this.queue.splice(0,10),t={},n=0;e.length>n;n++)(r=e[n]).settled||(t[o=r.endpointOverride||"__default__"]||(t[o]=[]),t[o].push(r));for(i=[],a=Object.keys(t),s=Promise.resolve(),c=0;a.length>c;c++)(function(e){s=s.then(function(){return u._processFlushGroup(e,t[e],i)})})(a[c]);return s.then(function(){var e,t,n,r,o,a,s;if(i.length){for(e=Date.now(),t=[],n=[],r=0;i.length>r;r++)(i[r].timeoutAt>e?n:t).push(i[r]);for(o=0;n.length>o;o++)u.queue.push(n[o]);for(a=Promise.resolve(),s=0;t.length>s;s++)(function(e){a=a.then(function(){return u.persistentQueue.enqueueMany(u._toPersistentEntries([e])).catch(function(){}).then(function(){u.hasPersistentItems=!0,u._settleItem(e,{sent:!1,ok:!1,status:0,reason:"track_timeout",transport:"queue"})})})})(t[s]);return a}}).then(function(){u.queue.length&&u.schedule()},function(e){throw u.queue.length&&u.schedule(),e})},Pt.prototype._processReplayGroup=function(e,t){var n=this;return n._resolveEndpoint?n._resolveEndpoint("__default__"===e?"":e).then(function(e){var r,o,i,a,s,c,u,l,d;if(e){for(r={},o=[],i=[],a=[],s=0;t.length>s;s++)u=(((c=t[s]).record&&c.record.event_id||"")+"").trim()||"__empty__:"+c.id,(l=((c.record&&c.record.event_id||"")+"").trim())&&Ft.has(l)||r[u]?a.push(c.id):(r[u]=!0,o.push(c.record),i.push(c.id));return d=Promise.resolve(),a.length&&(d=d.then(function(){return n.persistentQueue.removeByIds(a)})),o.length?d.then(function(){return ut(e,o,{useBeacon:rr})}).then(function(e){var r,s,c,u,l,d,_,p,f,h;if(e&&e.ok){for(r=[],s=0;o.length>s;s++)(c=((o[s]&&o[s].event_id||"")+"").trim())&&r.push(c);for(Ft.set(r),u=[],l=0;i.length>l;l++)u.push(i[l]);for(d=0;a.length>d;d++)u.push(a[d]);return n.persistentQueue.removeByIds(u)}for(_=0,p=0;t.length>p;p++)(f=t[p].retryCount||0)>_&&(_=f);return h=n._getRetryDelay(_),n.persistentQueue.updateRetryMeta(t,h,5)}):d}}):(Kt("[sdk][web] _resolveEndpoint 未注入,持久队列重试跳过"),Promise.resolve())},Pt.prototype.replayPersistent=function(){var e=this;return!this.hasPersistentItems||this.replaying?Promise.resolve():(this.replaying=!0,this.persistentQueue.readOldest(10).then(function(t){var n,r,o,i,a,s,c;if(!t.length)return e.persistentQueue.count().then(function(t){0===t&&(e.hasPersistentItems=!1)});for(n={},r=0;t.length>r;r++)n[i=(o=t[r]).endpointOverride||"__default__"]||(n[i]=[]),n[i].push(o);for(a=Object.keys(n),s=Promise.resolve(),c=0;a.length>c;c++)(function(t){s=s.then(function(){return e._processReplayGroup(t,n[t])})})(a[c]);return s}).catch(function(){}).then(function(){e.replaying=!1},function(){e.replaying=!1}))},Pt.prototype.enqueueAssembled=function(e,t){var n=this;return(n._getEventCodes?n._getEventCodes():Promise.resolve([])).then(function(r){return r&&0===r.length||r&&-1===r.indexOf(e.event)?{sent:!1,ok:!1,status:0,reason:"event_code_not_allowed",event:e.event}:new Promise(function(r){var o={record:e,endpointOverride:t||"",resolve:r,reject:r,settled:!1,timeoutAt:Date.now()+er,timeoutTimer:null};o.timeoutTimer=setTimeout(function(){o.settled||n.persistentQueue.enqueueMany(n._toPersistentEntries([o])).catch(function(){}).then(function(){n.hasPersistentItems=!0,n._settleItem(o,{sent:!1,ok:!1,status:0,reason:"track_timeout",transport:"queue"})})},er),n.queue.push(o),10>n.queue.length?n.schedule():n.flush().catch(function(){})})})},Pt.prototype.track=function(e,t){var n=this;return Q().then(function(r){var o={appId:n.opts.appId,channel:n.opts.channel,uid:n.opts.uid||"",deviceId:n.opts.deviceId||r,deviceType:y(),sdkVersion:$n,appVersion:v(n.opts.appVersion)};return(n._getSigningConfig?n._getSigningConfig():Promise.resolve({hmacKey:"",securityKeyVersion:""})).then(function(t){return $e(o,e,t)}).then(function(e){return delete e._sessionInfo,(n._getEventCodes?n._getEventCodes():Promise.resolve([])).then(function(r){return r&&0===r.length?(Kt("[sdk][web] 当前没有事件允许上报,请检查服务端配置(eventCodes 为空)"),{sent:!1,ok:!1,status:0,reason:"event_code_not_allowed",event:e.event}):r&&-1===r.indexOf(e.event)?(Kt('[sdk][web] 事件 "'+e.event+'" 不在服务端允许上报的列表中,已忽略'),{sent:!1,ok:!1,status:0,reason:"event_code_not_allowed",event:e.event}):new Promise(function(r,o){var i={record:e,endpointOverride:t||"",resolve:r,reject:o,settled:!1,timeoutAt:Date.now()+er,timeoutTimer:null};i.timeoutTimer=setTimeout(function(){i.settled||n.persistentQueue.enqueueMany(n._toPersistentEntries([i])).catch(function(){}).then(function(){n.hasPersistentItems=!0,n._settleItem(i,{sent:!1,ok:!1,status:0,reason:"track_timeout",transport:"queue"})})},er),n.queue.push(i),10>n.queue.length?n.schedule():n.flush().catch(function(){})})})})})},Pt.prototype.stop=function(){var e,t;this._stopTimer(),this.replayTimer&&(clearInterval(this.replayTimer),this.replayTimer=null),e={sent:!1,ok:!1,status:0,reason:"stopped"},t=this,this.queue.splice(0).forEach(function(n){t._settleItem(n,e)})},Pt.prototype.getConfig=function(){return{batchMaxSize:10,batchIntervalMs:5e3,replayBatchSize:10,replayIntervalMs:Zn}},Et.prototype._sdkOpts=function(){return{appId:this.options.appId,channel:this.options.channel,uid:this.options.uid||"",deviceId:this.options.deviceId||"",deviceType:y(),sdkVersion:$n,appVersion:v(this.options.appVersion)}},Et.prototype._isAutoDisabled=function(e){return!(!this._disableAutoEvents||!this._disableAutoEvents[e])},Et.prototype._handleEndedSession=function(t){var n,r,o,i,a,s,c;return t&&t.sid&&!this._ssrSkipped?this._isAutoDisabled("app_session_summary")?Promise.resolve():function(t){if(!t)return!1;if(Pn[t]||En[t])return!1;if(se())try{if(window.sessionStorage.getItem(bn+(t+"")))return Pn[t]=1,!1}catch(e){}return En[t]=1,!0}(t.sid)?(n=this,o=t.sid,a=(null!=t.sessionEndTs?1e12>t.sessionEndTs?1e3*t.sessionEndTs:t.sessionEndTs:Date.now())||Date.now(),s=fe(o),c=!Sn||Sn===o||!s&&(xn.eventCountTotal>0||xn.currentPage||(xn.pageStayList||[]).length>0),s&&Sn&&Sn!==o&&(c=!1),s&&c&&(s.eventCountTotal||0)>(xn.eventCountTotal||0)&&(c=!1),c?(xe(),i=ce(xn)):i=s?function(e,t){var n,r,o,i=e&&e.currentPage;return i?(n=t||Date.now(),r=i.accumulated_ms||0,null!=i.resume_at_ms&&(r+=Math.max(0,n-i.resume_at_ms)),o={page_key:i.page_key,page_name:i.page_name||"",stay_duration_ms:r,enter_time:Math.floor((i.enter_time||n)/1e3),has_interaction:i.has_interaction?1:0},i.page_seq_in_sid&&(o.page_seq_in_sid=i.page_seq_in_sid),e.pageStayList=be((e.pageStayList||[]).concat([o])),e.currentPage=null,e):e}(ce(s),a):{eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null},pe(o,{sid:o,savedAt:Date.now(),sessionStartedAt:Dn,behaviorHash:ne(),metrics:i}),r=i,Q().then(function(e){var o=bt(n._sdkOpts(),{deviceId:e});return n.configMgr.getSigningConfig().then(function(e){var n=Me(t,t.endReason,r);return it(o,n,e,t.sid)})}).then(function(e){return e}).then(function(e){return delete e._sessionInfo,n.batcher.enqueueAssembled(e,"")}).then(function(){var n,r;!function(t){if(t&&(delete En[t],Pn[t]=1,se()))try{window.sessionStorage.setItem(bn+(t+""),"1")}catch(e){}}(t.sid),ke(t.sid),n={pageKey:Ln||"",pageName:Rn||""},r=function(e){if(!e||null==e.sessionStartTs)return Date.now();var t=e.sessionStartTs;return 1e12>t?1e3*t:t}(F()),function(t,n){var r,o;t&&function(t){if(t){if(se())try{window.sessionStorage.removeItem(de(t))}catch(e){}if(ye())try{window.localStorage.removeItem(de(t))}catch(e){}}}(t),xn={eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null},Dn=r=n&&"number"==typeof n.sessionStartMs&&isFinite(n.sessionStartMs)?n.sessionStartMs:Date.now(),xn.lastOrientation=re(),mn.reset(),Cn=!0,Sn=z()||"",(o=n&&n.carryOver)&&o.pageKey?function(t,n,r){var o,i,a,s,c=((t||"")+"").trim();if(!c)return!1;o="number"==typeof r&&isFinite(r)?r:Date.now(),i=Date.now(),a=!1;try{a="undefined"!=typeof document&&"hidden"===document.visibilityState}catch(e){}xn.pageKeys[c]=1,s=ae(),xn.currentPage={page_key:c,page_name:(n||"")+"",enter_time:o,enter_time_ms:o,page_seq_in_sid:s,has_interaction:!1,accumulated_ms:0,resume_at_ms:a?null:i,hidden_paused:a},me()}(o.pageKey,o.pageName,r):me()}(t.sid,{sessionStartMs:r,carryOver:n.pageKey?{pageKey:n.pageKey,pageName:n.pageName}:null})}).catch(function(){var e;(e=t.sid)&&delete En[e],function(e){if(e&&e.sid){var t=ve().filter(function(t){return t&&t.sid!==e.sid});t.push(e),we(t)}}({sid:t.sid,sessionStartTs:t.sessionStartTs,sessionEndTs:t.sessionEndTs,endReason:t.endReason,fields:Me(t,t.endReason,r)})})):Promise.resolve():Promise.resolve()},Et.prototype._replayPendingSummaries=function(e,t,n){var r,o,i=ve();if(!i.length)return Promise.resolve();for(r=Promise.resolve(),o=0;i.length>o;o++)(function(o){r=r.then(function(){return n(it(e,o.fields,t,o.sid)).then(function(){return ke(o.sid),null}).catch(function(){})})})(i[o]);return r},Et.prototype._startAutoEvents=function(){if(!this._ssrSkipped&&!this._autoEventsStarted){var t=this;this._autoEventsStarted=!0,qn=function(e){t._handleEndedSession(e).catch(function(){})},Z().then(function(){return Q()}).then(function(n){var r=bt(t._sdkOpts(),{deviceId:n});return t.configMgr.getSigningConfig().then(function(n){return t.configMgr.fetchAndDecryptConfig().then(function(o){function i(e){return!s||-1!==s.indexOf(e)}function a(e){return i(e)&&!t._isAutoDisabled(e)}var s=o.eventCodes&&o.eventCodes.length>0?o.eventCodes:null,c=function(e){return e.then(function(e){return delete e._sessionInfo,t.batcher.enqueueAssembled(e,"")})};return t._replayPendingSummaries(r,n,c).then(function(){function o(e){if(!s&&(s=!0,a("app_session_summary"))){var n=function(e){var t,n,r=Date.now()||Date.now(),o=F();return o?(t=Y(o,e||"web_visibility_hidden",r),"web_visibility_hidden"!==e&&"app_exit"!==e||(t.sessionEndTs=r),N(n=H(r)),{endedSession:t,sid:n.sid}):null}(e);n&&n.endedSession&&t._handleEndedSession(n.endedSession).catch(function(){})}}var s,u,l,d,_,p,f,h,g,m,y,v,w,k,b;a("sdk_init")&&c(function(e,t){var n,r=fn||{},o={},i=Object.keys(r);for(n=0;i.length>n;n++)o[i[n]]=r[i[n]];return $e(e,{event:"sdk_init",payload:o},t)}(r,n)).catch(function(){}),a("app_lifecycle")&&(t._webLoadTeardown=function(t,n,r){function o(){a||(a=!0,s&&(window.removeEventListener("load",s,!1),s=null),t(ot(n,Kn,{},r)).catch(function(){}))}function i(){(function(){var t,n,r;try{if("undefined"!=typeof performance&&performance.timing&&(t=performance.timing).loadEventEnd&&t.navigationStart&&t.loadEventEnd>0)return!0;if("undefined"!=typeof performance&&"function"==typeof performance.getEntriesByType&&(n=performance.getEntriesByType("navigation"))&&n.length>0&&(r=n[0]).loadEventEnd>0&&r.fetchStart>=0)return!0}catch(e){}return!1})()&&o()}var a,s;return"undefined"==typeof window?(t(ot(n,Kn,{},r)).catch(function(){}),function(){}):(a=!1,s=null,"complete"===document.readyState?(i(),a||o(),function(){}):(s=function(){i(),a||o()},window.addEventListener("load",s,!1),setTimeout(function(){a||o()},1e4),function(){a=!0,s&&(window.removeEventListener("load",s,!1),s=null)}))}(c,r,n)),s=!1,t._autoLifecycleTeardown=function(e,t){var n,r,o,i,a;return"undefined"==typeof window||"undefined"==typeof document?function(){}:(r=!0===(n=t||{}).webMode,o=function(){"visible"===document.visibilityState?r?n.onVisible&&n.onVisible():e(Vn,{}):"hidden"===document.visibilityState&&(r?n.onVisibleHidden&&n.onVisibleHidden():e(jn,{}),n.onHidden&&n.onHidden())},i=function(){e(Nn,rt()),n.onPageUnload&&n.onPageUnload()},a=function(e){e&&e.persisted||n.onPageHide&&n.onPageHide()},window.addEventListener("visibilitychange",o,!1),window.addEventListener("beforeunload",i,!1),window.addEventListener("pagehide",a,!1),function(){window.removeEventListener("visibilitychange",o,!1),window.removeEventListener("beforeunload",i,!1),window.removeEventListener("pagehide",a,!1)})}(function(e,t){return a("app_lifecycle")?c(ot(r,e,t||{},n)):Promise.resolve()},{onHidden:function(){t.batcher.flush().catch(function(){})},onPageUnload:function(){t.batcher.flush().catch(function(){}),o("app_exit")},onPageHide:function(){t.batcher.flush().catch(function(){}),o("web_visibility_hidden")}}),t._autoNetworkTeardown=(l=function(e){return a("network_state_change")?c(function(e,t,n){return $e(e,{event:"network_state_change",payload:t||{}},n)}(r,e,n)):Promise.resolve()},"undefined"==typeof navigator?function(){}:(d=navigator.connection||navigator.mozConnection||navigator.webkitConnection)?(_="unknown",p=d.type?d.type+"":"",f="boolean"==typeof navigator.onLine?navigator.onLine:null,d.onchange=function(){var e,t,n,r,o,i,a=d.type?d.type+"":"",s="boolean"==typeof navigator.onLine?navigator.onLine:null;(a!==p||s!==f)&&(p=a,f=s,r="boolean"==typeof(t=s)?t?1:0:-1,"none"==(n=(a||"")+"")&&1===r?n="unknown":n&&"none"!==n&&"unknown"!==n&&0===r&&(r=1),l(at({network_type:_,from_network_type:_,to_network_type:_,is_online:(e={connection_type:n,is_online:r}).is_online,change_count_in_sid:(xn.networkChangeCount||0)+1},(i={},(o=e.connection_type)?i.connection_type=o+"":d.type&&(i.connection_type=d.type+""),d.effectiveType&&(i.effective_type=d.effectiveType+""),null!=d.downlink&&(i.downlink=Math.round(1e3*d.downlink)),null!=d.rtt&&(i.rtt=d.rtt),i))))},function(){d.onchange=null}):function(){}),t._webInteractionTeardown="undefined"==typeof window||"undefined"==typeof document?function(){}:(h=function(e){!e||e.repeat||e.isComposing||(Se(),he(),xn.keydownTotalCount++,me())},g=function(e){var t,n=e&&e.target;(function(t){if(!t||1!==t.nodeType)return!1;var n=((t.tagName||"")+"").toLowerCase();if("input"===n||"textarea"===n)return!0;try{if(t.isContentEditable)return!0}catch(e){}return!1})(n)&&(t=Date.now(),Fn===n&&50>t-Hn||(Fn=n,Hn=t,Se(),he(),xn.inputCount++,me()))},m=function(e){(function(t){if(!t||1!==t.nodeType)return!1;var n=((t.tagName||"")+"").toLowerCase();if("input"===n||"textarea"===n||"select"===n)return!0;try{if(t.isContentEditable)return!0}catch(e){}return!1})(e&&e.target)&&(Se(),he(),xn.focusTotalCount++,me())},y=function(){Pe()},v=function(e){e&&e.changedTouches&&e.changedTouches.length&&(Se(),he(),xn.touchTotalCount++,me(),e.touches&&1===e.touches.length?(Un=e.touches[0].clientX,Xn=e.touches[0].clientY,Yn=Date.now(),ct(),zn=!1,Gn=setTimeout(function(){zn=!0,he(),xn.longPressTotalCount++,me()},500)):(Yn=0,ct()),e.touches&&2===e.touches.length&&(Wn=st(e.touches),Jn=!1))},w=function(e){var t,n,r;e&&e.touches&&(1===e.touches.length&&Yn>0&&Math.sqrt((t=e.touches[0].clientX-Un)*t+(n=e.touches[0].clientY-Xn)*n)>8&&ct(),2===e.touches.length&&(r=st(e.touches),null==Wn||Jn||12>Math.abs(r-Wn)||(he(),xn.pinchTotalCount++,me(),Jn=!0),Wn=r))},k=function(e){var t,n;ct(),Yn>0&&e.changedTouches&&e.changedTouches.length&&!zn&&(30>Math.sqrt((t=e.changedTouches[0].clientX-Un)*t+(n=e.changedTouches[0].clientY-Xn)*n)||(he(),xn.swipeTotalCount++,me())),Yn=0,zn=!1,e.touches&&e.touches.length>=2||(Wn=null,Jn=!1)},b=function(){ct(),Yn=0,zn=!1,Wn=null,Jn=!1},document.addEventListener("keydown",h,!1),document.addEventListener("input",g,!1),document.addEventListener("focusin",m,!1),window.addEventListener("orientationchange",y,!1),document.addEventListener("touchstart",v,!1),document.addEventListener("touchmove",w,!1),document.addEventListener("touchend",k,!1),document.addEventListener("touchcancel",b,!1),Pe(),function(){document.removeEventListener("keydown",h,!1),document.removeEventListener("input",g,!1),document.removeEventListener("focusin",m,!1),window.removeEventListener("orientationchange",y,!1),document.removeEventListener("touchstart",v,!1),document.removeEventListener("touchmove",w,!1),document.removeEventListener("touchend",k,!1),document.removeEventListener("touchcancel",b,!1),ct()}),(i("app_page_view")||i("page_click"))&&(u=a("app_page_view"),t._pageTrackerTeardown=function(t){function n(){if(u.resolvePageKey)try{var t=u.resolvePageKey();if(t)return qe(t+"")}catch(e){}return Be()}function r(){var e,t=n(),r=ze({previousPageKey:d,previousPageName:_,pageKey:t});d=t,_=r.page_name,Ln=t,Rn=r.page_name||"",l&&(e=ae(),function(e,t,n,r){he(),xn.eventCountTotal++,xn.lastEventName="app_page_view",xn.pageViewCount++;var o=(e||"")+"";o&&(xn.pageKeys[o]=1),Te(o,t,n,r),me()}(t,r.page_name,Math.floor(Date.now()/1e3),e),r._pageSeqPrealloc=e,r._metricsRecorded=1,u.onPageView&&u.onPageView(r))}function o(){var t,n,r,o,i=Date.now();200>i-Mn||(Mn=i,(t=Ke())&&(r=(n=t.scrollTop||0)-In,In=n,o=function(){var t,n,r;try{return(t=Ke())?(n=Math.max(0,(t.scrollHeight||0)-(t.clientHeight||0)))>0?{depthPercent:Math.min(100,Math.max(0,Math.round((r=t.scrollTop||0)/n*100))),atBottom:r>=n-8}:{depthPercent:0,atBottom:!0}:{depthPercent:0,atBottom:!1}}catch(e){return{depthPercent:0,atBottom:!1}}}(),u.onScroll&&u.onScroll(r,o.depthPercent,o.atBottom)))}function i(e){var t,r,o,i;e&&0>=e.button&&((t=e.target)&&"function"==typeof t.closest&&t.closest("[data-sdk-no-auto-click]")||(r=null!=e.clientX?e.clientX:0,o=null!=e.clientY?e.clientY:0,i=Ye(e),setTimeout(function(){if(!function(e,t){var n,r;for(function(e){for(var t=An.length-1;t>=0;t--)e-An[t].t>300&&An.splice(t,1)}(Date.now()),n=0;An.length>n;n++)if(10>=Math.abs((r=An[n]).x-e)&&10>=Math.abs(r.y-t))return!0;return!1}(r,o)){Se();var e=function(e,t){var n=e||{},r=qe(n.pageKey||Be()),o=Ye(t);return Ae({page_key:r,page_name:n.pageName||"undefined"!=typeof document&&document.title||r,recommend_trace_id:"",referrer_page_key:n.previousPageKey||"",is_sdk_auto_collected:1,_clickSnap:o},Ge(o))}({pageKey:d||n(),pageName:_,previousPageKey:d},i);u.onPageClick&&u.onPageClick(e)}},50)))}function a(){Ie(),r()}function s(e){e&&e.persisted&&r()}function c(){var e,t,n=Ke();In=n&&n.scrollTop||0,r(),!p&&window.history&&(p=!0,e=history.pushState,t=history.replaceState,history.pushState=function(){var t=e.apply(history,arguments);return Ie(),setTimeout(r,0),t},history.replaceState=function(){var e=t.apply(history,arguments);return Ie(),setTimeout(r,0),e},window.addEventListener("popstate",a,!1))}var u,l,d,_,p;return"undefined"==typeof window||"undefined"==typeof document?function(){}:(l=!1!==(u=t||{}).enableAutoPageView,d="",_="",p=!1,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c,!1):c(),window.addEventListener("pageshow",s,!1),window.addEventListener("scroll",o,!1),document.addEventListener("click",i,!0),function(){document.removeEventListener("click",i,!0),window.removeEventListener("scroll",o,!1),window.removeEventListener("pageshow",s,!1),window.removeEventListener("popstate",a,!1),document.removeEventListener("DOMContentLoaded",c,!1)})}({resolvePageKey:t.options.pageKeyResolver,enableAutoPageView:u,onPageView:function(e){u&&t.batcher.track({event:"app_page_view",payload:e},"").catch(function(){})},onPageClick:function(e){a("page_click")&&t.batcher.track({event:"page_click",payload:e},"").catch(function(){})},onScroll:function(e,t,n){!function(e,t,n){var r,o;he(),8>(r=Math.abs(e))||(xn.hasScrolled||(xn.hasScrolled=!0,xn.firstScrollDelayMs=Math.max(0,Date.now()-Dn)),xn.scrollTotalCount++,xn.scrollTotalDistance+=r,o=e>0?1:-1,0!==xn.lastScrollDirection&&o!==xn.lastScrollDirection&&xn.scrollDirectionChangeCount++,xn.lastScrollDirection=o,0>e?xn.scrollUpCount++:e>0&&xn.scrollDownCount++,null!=t&&(xn.maxScrollDepthPercent=Math.max(xn.maxScrollDepthPercent,Math.min(100,Math.max(0,Math.round(t))))),n&&(xn.isReachBottom=1),me())}(e,t,n)}})),t._sessionPollTimer&&clearInterval(t._sessionPollTimer),t._sessionPollTimer=setInterval(function(){var e=G();e.endedSession&&t._handleEndedSession(e.endedSession).catch(function(){})},6e4)})})})}).catch(function(){})}},Et.prototype._init=function(e){var t,n,r;if(this._ssrSkipped)return this._buildInitReturn();for(t=Object.keys(e||{}),n=0;t.length>n;n++)this.options[t[n]]=e[t[n]];return null==this.options.channel&&(this.options.channel=""),this._disableAutoEvents=Ct(this.options.disableAutoEvents),this.options.disableAutoEvents=St(this._disableAutoEvents),this.configMgr.setEncryptedConfig(this.options.encryptedConfig||this.options.configCipher||""),this.configMgr.prewarm(),r=this,this.resolver.init(function(){return r.configMgr.getBatchEndpoints()}),this._refreshTimer&&clearInterval(this._refreshTimer),this._refreshTimer=setInterval(function(){r.refreshConfig()},12e5),this._unsubReady&&this._unsubReady(),this._unsubReady=this.resolver.onEndpointReady(function(){r.batcher.flush().catch(function(){})}),this._buildInitReturn()},Et.prototype.refreshConfig=function(){var e=this;return this.configMgr.forceRefresh(),this.resolver.refreshDomainConfig(function(){return e.configMgr.getBatchEndpoints()}).catch(function(){})},Et.prototype.track=function(e,t){return this._ssrSkipped?Promise.resolve({sent:!1,ok:!0,status:0,reason:"ssr_skipped"}):this.batcher.track(e,t)},Et.prototype.setContext=function(e){if(this._ssrSkipped)return this.getConfig();var t=e&&"object"==typeof e?e:{};return Object.prototype.hasOwnProperty.call(t,"uid")&&(this.options.uid=null==t.uid?"":t.uid+""),Object.prototype.hasOwnProperty.call(t,"channel")&&(this.options.channel=null==t.channel?"":t.channel+""),this.batcher.opts=bt(this.options),this.getConfig()},Et.prototype.setUid=function(e){return this.setContext({uid:e})},Et.prototype.getSelectedHost=function(){var e,t;return this._ssrSkipped||!this.resolver?"":(e=this.resolver.getSelectedBatchEndpoint())&&(t=/^(https?:\/\/[^/]+)/.exec(e))?t[1]:""},Et.prototype._buildInitReturn=function(){return this._ssrSkipped||!this.batcher?{selectedHost:"",version:$n}:bt(this.batcher.getConfig(),{selectedHost:this.getSelectedHost(),version:$n})},Et.prototype.getConfig=function(){if(this._ssrSkipped||!this.batcher)return bt(this.options,{selectedHost:"",selectedBatchEndpoint:"",version:$n});var e=bt(this.options,this.batcher.getConfig(),{selectedHost:this.getSelectedHost(),selectedBatchEndpoint:this.resolver.getSelectedBatchEndpoint(),version:$n});return delete e.encryptedConfig,delete e.configCipher,e},Et.prototype.flush=function(){return this._ssrSkipped?Promise.resolve():this.batcher.flush()},Et.prototype.refreshDomainConfig=function(){if(this._ssrSkipped)return Promise.resolve({selectedBatchEndpoint:"",selectedHost:"",version:$n});var e=this;return this.resolver.refreshDomainConfig(function(){return e.configMgr.getBatchEndpoints()}).then(function(t){return{selectedBatchEndpoint:t,selectedHost:e.getSelectedHost(),version:$n}})},Et.prototype.stop=function(){this._ssrSkipped||(this._sessionPollTimer&&(clearInterval(this._sessionPollTimer),this._sessionPollTimer=null),this._pageTrackerTeardown&&(this._pageTrackerTeardown(),this._pageTrackerTeardown=null),this._webInteractionTeardown&&(this._webInteractionTeardown(),this._webInteractionTeardown=null),this._webLoadTeardown&&(this._webLoadTeardown(),this._webLoadTeardown=null),this._autoLifecycleTeardown&&(this._autoLifecycleTeardown(),this._autoLifecycleTeardown=null),this._autoNetworkTeardown&&(this._autoNetworkTeardown(),this._autoNetworkTeardown=null),this._unsubReady&&(this._unsubReady(),this._unsubReady=null),this._refreshTimer&&(clearInterval(this._refreshTimer),this._refreshTimer=null),this.batcher.stop())},Ht=null,Ut=[],Xt=null,(Yt=Dt).setContext=function(e){return Tt().setContext(e)},Yt.setUid=function(e){return Tt().setUid(e)},Yt.setChannel=function(e){return Tt().setContext({channel:e})},Yt.notifyPageLoad=function(){!function(){if(cn=null,V())try{window.sessionStorage.removeItem(tn),window.sessionStorage.removeItem(nn)}catch(e){}}(),G(Date.now())},Yt.debugInvalidateSessionCache=function(){W()},Yt.debugForceEnsureSession=function(){var e,t;return W(),e=G(),t=Tt(),e.endedSession&&t._handleEndedSession(e.endedSession).catch(function(){}),e},Yt.getConfig=function(){return Tt().getConfig()},Yt.flush=function(){return Tt().flush()},Yt.refreshDomainConfig=function(){return Tt().refreshDomainConfig()},Yt.stop=function(){Ht?(Ht.stop(),Ht=null):Ut.length=0},Yt.version=$n,Yt.getCommonFields=function(e){var t=Ht?Ht.getConfig():{};return Z().then(function(){return Q().then(function(n){var r,o,i,a,s=B({appId:t.appId,channel:t.channel,uid:t.uid,deviceId:t.deviceId||n,sid:(a=z(),a||G().sid),deviceType:y(),deviceBrand:"",deviceModel:"",deviceFingerprint:ee(),fpVersion:te(),sdkVersion:$n,appVersion:v(t.appVersion)});if(!Array.isArray(e)||!e.length)return s;for(r={},o=0;e.length>o;o++)(i=((e[o]||"")+"").trim())&&(r[i]=Object.prototype.hasOwnProperty.call(s,i)?s[i]:"");return r})})},Yt.getCommonField=function(e){var t=((e||"")+"").trim();return t?Yt.getCommonFields([t]).then(function(e){return e[t]}):Promise.resolve("")},Yt.init=function(e){var t,n,r,o;return Ht&&(t=Ht,Ht=null,t.flush().catch(function(){}).then(function(){t.stop()})),n=new Et(e),Ht=n,o=xt(Ut.splice(0),n,0).then(function(){Xt===r&&(Xt=null)},function(){Xt===r&&(Xt=null)}),Xt=r=o,bt(n._buildInitReturn(),{track:Yt.track,setContext:Yt.setContext,setUid:Yt.setUid,setChannel:Yt.setChannel,notifyPageLoad:Yt.notifyPageLoad,getCommonFields:Yt.getCommonFields,getCommonField:Yt.getCommonField})},Gt=function(){return Kt("[sdk][web] 非法上报:禁止直接调用 WebSDK.track({...}),请使用 WebSDK.track.videoEvent({...}) 等具名方法"),Promise.resolve({sent:!1,ok:!1,status:0,reason:"direct_track_forbidden"})},Object.keys(Bt).forEach(function(t){var n=Bt[t];Gt[t]=function(r,o){try{return Dt(n(r),o)}catch(e){if(e&&"EventValidationError"===e.name)return Promise.resolve({sent:!1,ok:!1,status:0,reason:"event_validation_failed",event:e.eventMethod||t,missingFields:e.missingFields||[],typeErrorFields:e.typeErrorFields||[]});throw e}}}),Yt.track="undefined"!=typeof Proxy?new Proxy(Gt,{get:function(e,t){return"string"!=typeof t||t in e||!/^[a-z]/.test(t)?e[t]:function(){return Kt('[sdk][web] 未知上报事件 "'+t+'",该方法不存在,事件未上报'),Promise.resolve({sent:!1,ok:!1,status:0,reason:"unknown_event_method",method:t})}}}):Gt,"undefined"!=typeof globalThis)try{globalThis.WebSDK=Yt}catch(e){}else"undefined"!=typeof window&&(window.WebSDK=Yt);return Yt}); diff --git a/scripts/browser-smoke.ts b/scripts/browser-smoke.ts index 3045b2b..a75cdfe 100644 --- a/scripts/browser-smoke.ts +++ b/scripts/browser-smoke.ts @@ -26,7 +26,7 @@ try { await context.route("**/*", async (route) => { const request = route.request(); const local = new URL(request.url()).origin === origin; - // Exercise UI without recording synthetic analytics, purchases or user actions. + // Exercise UI without recording synthetic purchases or user actions. if ( !local && (request.method() !== "GET" || request.resourceType() === "script") @@ -55,12 +55,6 @@ try { timeout: 40000, }); await page.waitForTimeout(500); - if (path !== "/discover/search/") - await page.waitForFunction( - () => typeof (window as unknown as Record).tracker === "object", - null, - { timeout: 15000 }, - ); for (let index = 0; index < 12; index++) { const close = page.locator( "[data-dialog-ad].hk-is-open [data-dialog-ad-item]:not([hidden]) [data-dialog-ad-close]", @@ -75,9 +69,6 @@ try { legacyScripts: Array.from(document.scripts).some((script) => /\/static\/js\//.test(script.src), ), - tracker: - typeof (window as unknown as Record).tracker === - "object", })); results.push({ device: name, @@ -91,11 +82,8 @@ try { throw new Error( `${name} ${path} overflows horizontally: ${layout.document}`, ); - if ( - layout.legacyScripts || - (path !== "/discover/search/" && !layout.tracker) - ) - throw new Error(`${name} ${path} missing shared client runtime`); + if (layout.legacyScripts) + throw new Error(`${name} ${path} loaded legacy business scripts`); if (path === "/") { const accountEntries = await page.locator( "[data-login-open], [data-register-open], .hk-user-menu, a[href^='/account']", diff --git a/scripts/catalog-react-smoke.ts b/scripts/catalog-react-smoke.ts index c5c840e..f71a517 100644 --- a/scripts/catalog-react-smoke.ts +++ b/scripts/catalog-react-smoke.ts @@ -146,10 +146,8 @@ try { page.on("pageerror", (error) => errors.push(error.message)); await context.addInitScript({ content: ` - window.__copies=[];window.__events=[];window.__navigation=''; + window.__copies=[];window.__navigation=''; Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async function(value){window.__copies.push(value)}}}); - window.tracker={getSDKCommonHeader:function(){return 'mock-common'},getCommonFields:function(){return {device_id:'mock-device',device:'OTHER',sid:'mock-sid'}}}; - window.WebSDK={track:new Proxy({},{get:function(_,method){return function(payload){window.__events.push({method:String(method),payload:payload})}}})}; if(!localStorage.getItem('hgdj_search_history'))localStorage.setItem('hgdj_search_history',JSON.stringify(['历史词'])); `, }); diff --git a/scripts/native-site-smoke.ts b/scripts/native-site-smoke.ts index 0184b99..d42e3d3 100644 --- a/scripts/native-site-smoke.ts +++ b/scripts/native-site-smoke.ts @@ -12,7 +12,7 @@ import { createStyleFixture } from "./style-fixtures"; // Bundle the actual React provider, dialogs, header/footer and encrypted media // hooks. The ephemeral server and browser request policy isolate all writes, -// destinations and analytics; no original business JS or template runtime runs. +// and destinations; no original business JS or template runtime runs. const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const temporary = await mkdtemp(join(tmpdir(), "huakui-native-site-")), bundle = join(temporary, "site.js"); @@ -23,7 +23,6 @@ await build({ contents: ` import {createRoot} from 'react-dom/client'; import {SiteProvider,useSite} from '@/components/providers/SiteProvider'; -import {TrackerProvider} from '@/components/analytics/TrackerProvider'; import {SiteHeader} from '@/components/layout/SiteHeader'; import {SiteFooter} from '@/components/layout/SiteFooter'; import {SiteShell} from '@/components/layout/SiteShell'; @@ -32,7 +31,7 @@ import {AppDownloadLink} from '@/components/catalog/Interactions'; import {setAuthSession} from '@/lib/api'; window.__setAuthSession=setAuthSession; const app=createRoot(document.getElementById('root'));function UserProbe(){const site=useSite();return
} -function render(data){app.render(data.requestPath==='/navigation/'?:

真实 React 共享组件隔离检查

点击下载
);}window.__rerender=render;render(window.__fixture); +function render(data){app.render(data.requestPath==='/navigation/'?:

真实 React 共享组件隔离检查

点击下载
);}window.__rerender=render;render(window.__fixture); `, }, bundle: true, @@ -72,13 +71,6 @@ const server = createServer(async (request, response) => { response.end(await readFile(bundle)); return; } - if (path === "/static/common/web-sdk-v1.2.0.js") { - response.setHeader("Content-Type", "application/javascript"); - response.end( - "/* SDK mocked by the browser fixture before React mounts. */", - ); - return; - } if (path.startsWith("/static/")) { const file = resolve(root, "public", "." + path); if (!file.startsWith(join(root, "public") + "/")) { @@ -151,7 +143,6 @@ const server = createServer(async (request, response) => { requestPath: path, pageKey: "home", pageName: "首页", - trackerAppId: "isolated", isLoggedIn: user, isRegistered: false, userName: "隔离游客", @@ -203,9 +194,7 @@ const image = Buffer.from( key = Buffer.from("2019ysapp7527"); for (let i = 0; i < Math.min(100, image.length); i++) image[i] ^= key[i % key.length]; -type EventRecord = { method: string; payload: Record }; -const events: EventRecord[] = [], - errors: string[] = [], +const errors: string[] = [], missing: string[] = [], unexpected: string[] = [], destinations: string[] = []; @@ -214,14 +203,8 @@ let releaseImage: (() => void) | undefined, delayImage = false, imageRequested = 0; const configure = async (context: BrowserContext, ageAccepted = false) => { - await context.exposeBinding( - "__recordEvent", - (_source, event: EventRecord) => { - events.push(event); - }, - ); await context.addInitScript({ - content: `window.__ageClosed=0;document.addEventListener('eighteen:closed',function(){window.__ageClosed++});window.WebSDK={init:function(){return Promise.resolve()},getCommonFields:function(){return Promise.resolve({device_id:'isolated-device',device:'OTHER',sid:'isolated-sid'})},flush:function(){},track:new Proxy({},{get:function(_,method){return function(payload){return window.__recordEvent({method:String(method),payload:payload})}}})};try{${ageAccepted ? "localStorage.setItem('hg_age_ok','1');" : ""}if(!localStorage.getItem('__fixture_history_seeded')){localStorage.setItem('hgdj_search_history',JSON.stringify(['历史词']));localStorage.setItem('__fixture_history_seeded','1');}}catch{}`, + content: `window.__ageClosed=0;document.addEventListener('eighteen:closed',function(){window.__ageClosed++});try{${ageAccepted ? "localStorage.setItem('hg_age_ok','1');" : ""}if(!localStorage.getItem('__fixture_history_seeded')){localStorage.setItem('hgdj_search_history',JSON.stringify(['历史词']));localStorage.setItem('__fixture_history_seeded','1');}}catch{}`, }); await context.route("**/*", async (route) => { const request = route.request(), @@ -302,7 +285,6 @@ const configure = async (context: BrowserContext, ageAccepted = false) => { }; const ready = async (page: Page) => { await page.waitForSelector(".hk-site-header"); - await page.waitForFunction(() => !!window.__trackerCommonFields); }; const footerQR = async (page: Page, value: string, previous = "") => { await page.waitForFunction(({ value, previous }) => { @@ -413,17 +395,8 @@ try { await stack.keyboard.press("Escape"); await stack.waitForSelector("[data-dialog-ad]", { state: "detached" }); await expectOverflow(stack, ""); - await stack.waitForTimeout(50); - assert.equal( - events.filter( - (event) => - event.method === "adImpression" && event.payload.ad_id === "stack-1", - ).length, - 1, - ); await stacked.close(); imageRequested = 0; - events.length = 0; // Age gate cannot be bypassed by Escape. Ads wait for actual XOR decode + image // load, resume their queue on another page, and stay dismissed in this session. const context = await browser.newContext({ @@ -459,7 +432,6 @@ try { await page.waitForTimeout(150); assert.equal(imageRequested, 1); assert.equal(await page.locator("[data-dialog-ad]").isVisible(), false); - assert.equal(events.filter((e) => e.method === "adImpression").length, 0); assert.equal( await page.evaluate(() => localStorage.getItem("hg_age_ok")), "1", @@ -515,16 +487,6 @@ try { await page.evaluate(() => sessionStorage.getItem("dialogAdDismissed")), "2|1|2", ); - assert.equal( - events.filter((e) => e.method === "adImpression" && e.payload.ad_id === "1") - .length, - 1, - ); - assert.equal( - events.filter((e) => e.method === "adClick" && e.payload.ad_id === "1") - .length, - 1, - ); await page.goto(origin + "/?ads=1&bad=1"); await ready(page); await page.locator("[data-dialog-ad].hk-is-open").waitFor(); @@ -532,12 +494,6 @@ try { await page.locator("[data-dialog-ad-id]").getAttribute("data-dialog-ad-id"), "2", ); - assert.equal( - events.some( - (e) => e.method === "adImpression" && e.payload.ad_id === "bad", - ), - false, - ); await page.keyboard.press("Escape"); // Actual HeaderSearch hooks, keyboard navigation and history; no account entry remains. await page.goto(origin + "/?user=1"); @@ -1053,15 +1009,8 @@ try { assert.ok(destinations.includes("/mock/android.apk")); assert.ok(destinations.includes("/mock/fallback")); assert.ok(destinations.includes("/mock/launch")); - assert.ok( - events.some( - (event) => - event.method === "navigation" && - event.payload.navigation_key === "header_download", - ), - ); console.log( - "Native React site smoke passed: age gate/focus lock, encrypted advertisement readiness/failure/queue/cross-page session, footer QR pixels/size/URL/official fallback/empty configuration/restoration, HeaderSearch/history/keyboard/encoded URLs, configured desktop hover/focus/toggle/Escape/outside-click and mobile expandable navigation/canonical/external/download links, no login/user-menu/account entries, PC download and configured/fallback iOS/Android tabs/actions/deep links, drawer/footer/main entries, scroll/focus restoration and analytics. All APIs and outbound destinations isolated.", + "Native React site smoke passed: age gate/focus lock, encrypted advertisement readiness/failure/queue/cross-page session, footer QR pixels/size/URL/official fallback/empty configuration/restoration, HeaderSearch/history/keyboard/encoded URLs, configured desktop hover/focus/toggle/Escape/outside-click and mobile expandable navigation/canonical/external/download links, no login/user-menu/account entries, PC download and configured/fallback iOS/Android tabs/actions/deep links, drawer/footer/main entries, scroll/focus restoration. All APIs and outbound destinations isolated.", ); } finally { releaseImage?.(); diff --git a/scripts/native-ssr-smoke.ts b/scripts/native-ssr-smoke.ts index 0b11759..aff53d1 100644 --- a/scripts/native-ssr-smoke.ts +++ b/scripts/native-ssr-smoke.ts @@ -39,13 +39,6 @@ async function expectPageStyles(page: Page, required: string[], absent: string[] const canonical = "https://canonical.drama.test"; const userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) NativeSSRSmoke/1.0"; -const sdkCommon = JSON.stringify({ - sid: "ssr-sdk-session", - device_id: "ssr-sdk-device", - device: "iOS", - device_fingerprint: "ssr-fingerprint", - fp_version: "2", -}); const indexNow = "native_ssr_verification_20261010"; const verifyFilename = `native-ssr-${randomBytes(6).toString("hex")}.html`; const verifyPath = resolve("public", verifyFilename); @@ -189,7 +182,6 @@ const mock = createServer(async (req, res) => { seoNavInfo: {}, navPageList: [], advitise: {}, - reportSdkConfig: "", }); return; } @@ -381,7 +373,6 @@ function headers(caseName: string, token = ""): Headers { "user-agent": userAgent, "x-forwarded-for": "203.0.113.42, 198.51.100.7", "x-user-agent": "DevType=SSR smoke;SysType=h5_ios;sid=original", - "x-sdk-common": sdkCommon, ...(token ? { cookie: `token=${token}; nickName=STALE_COOKIE; userId=1; isRegister=${token.startsWith("guest-") ? "0" : "1"}; channel=smoke-channel; client_cookie=must_forward`, @@ -540,7 +531,6 @@ try { call.headers["x-user-agent"], "DevType=SSR smoke;SysType=h5_ios;sid=original", ); - assert.equal(call.headers["x-sdk-common"], sdkCommon); assert.equal( Object.keys(call.headers).some((key) => key.startsWith("x-hk-")), false, diff --git a/scripts/playback-smoke.ts b/scripts/playback-smoke.ts index 166a0fb..b29d3eb 100644 --- a/scripts/playback-smoke.ts +++ b/scripts/playback-smoke.ts @@ -86,7 +86,7 @@ const encrypted = decryptImageBytes( const entry = join(temp, "fixture.tsx"); await writeFile( entry, - `import {useState} from 'react';import {createRoot} from 'react-dom/client';import {SiteProvider} from '${cwd}/src/components/providers/SiteProvider';import {TrackerProvider} from '${cwd}/src/components/analytics/TrackerProvider';import {DramaWatchPage} from '${cwd}/src/features/playback/DramaWatchPage';import {SeriesWatchPage} from '${cwd}/src/features/playback/SeriesWatchPage';import {EncryptedImage} from '${cwd}/src/components/media/EncryptedImage';function Fixture(){const [current,setCurrent]=useState(window.fixtureData);window.changePlaybackFixture=setCurrent;return {current.series?:}window.imageResolved=(window.imageResolved||0)+1} onResolveError={()=>window.imageErrors=(window.imageErrors||0)+1}/>window.failedImageResolved=true} onResolveError={()=>window.failedImageError=true}/>};createRoot(document.getElementById('root')).render();`, + `import {useState} from 'react';import {createRoot} from 'react-dom/client';import {SiteProvider} from '${cwd}/src/components/providers/SiteProvider';import {DramaWatchPage} from '${cwd}/src/features/playback/DramaWatchPage';import {SeriesWatchPage} from '${cwd}/src/features/playback/SeriesWatchPage';import {EncryptedImage} from '${cwd}/src/components/media/EncryptedImage';function Fixture(){const [current,setCurrent]=useState(window.fixtureData);window.changePlaybackFixture=setCurrent;return {current.series?:}window.imageResolved=(window.imageResolved||0)+1} onResolveError={()=>window.imageErrors=(window.imageErrors||0)+1}/>window.failedImageResolved=true} onResolveError={()=>window.failedImageError=true}/>};createRoot(document.getElementById('root')).render();`, ); const navStub = join(temp, "navigation.ts"); await writeFile( @@ -159,18 +159,6 @@ const baseData = () => ({ { id: 102, currentEpisode: 2, payType: 1, needUnlock: true }, ], }, - trackerDrama: { - DramaID: 10, - DramaTitle: "播放测试", - VideoID: 101, - Episode: 1, - TotalEpisode: 2, - DramaDuration: 12, - DramaTypeID: "2", - DramaTypeName: "都市", - DramaTagKey: "3", - DramaTagName: "剧情", - }, hotList: [], guessLike: [ { id: 30, slug: "guess", playName: "猜你喜欢测试", cover: "/static/brand/placeholder.svg", totalEpisode: 3, updateStatus: 2 }, @@ -194,13 +182,6 @@ const server = createServer(async (req, res) => { res.end(playbackStyles.css); return; } - if (url.pathname === "/static/common/web-sdk-v1.2.0.js") { - res.setHeader("content-type", "application/javascript"); - res.end( - 'window.sdkEvents=[];window.WebSDK={init:options=>{window.sdkInit=options},track:new Proxy({},{get:(_,method)=>payload=>{window.sdkEvents.push({method,payload});return Promise.resolve({ok:true})}}),getCommonFields:()=>Promise.resolve({sid:"mock-session",device_id:"mock-device",device:"PC",device_fingerprint:"mock-fp",fp_version:"1"}),flush:()=>Promise.resolve(),setUid:()=>{}};', - ); - return; - } if (url.pathname.startsWith("/api/")) { res.setHeader("content-type", "application/json"); let data: unknown = {}; @@ -378,35 +359,6 @@ try { return video.play(); }); await page.waitForFunction(() => document.querySelector("video")!.ended); - const events = await page.evaluate( - () => - ( - window as unknown as { - sdkEvents: { method: string; payload: Record }[]; - } - ).sdkEvents, - ); - for (const behavior of [ - "ep_view", - "ep_play", - "ep_pause", - "ep_forward", - "ep_complete", - ]) - assert( - events.some( - (event) => - event.method === "dramaEvent" && - event.payload.play_behavior_key === behavior, - ), - behavior, - ); - assert( - events.some( - (event) => - event.method === "dramaEvent" && event.payload.play_speed === 150, - ), - ); assert.equal( await page .locator("#failed-image-test") @@ -440,30 +392,11 @@ try { ), true, ); - const beforePreview = events.filter( - (event) => - event.method === "dramaEvent" && - event.payload.play_behavior_key === "ep_play", - ).length; await page.getByRole("button", { name: "预告片", exact: true }).click(); await page.waitForFunction(() => { const video = document.querySelector("video"); return !!video && !video.paused && video.currentTime > 0.1; }); - await page.waitForTimeout(300); - const afterPreview = await page.evaluate( - () => - ( - window as unknown as { - sdkEvents: { method: string; payload: Record }[]; - } - ).sdkEvents.filter( - (event) => - event.method === "dramaEvent" && - event.payload.play_behavior_key === "ep_play", - ).length, - ); - assert.equal(afterPreview, beforePreview); assert.equal( await page.locator(".hk-playback-player").getAttribute("data-play-mode"), "preview", @@ -493,18 +426,8 @@ try { const watchReq = requests.find( (req) => req.url === "/api/playlet/chapter/watch/report", ); - assert.equal( - watchReq?.headers["x-sdk-common"], - Buffer.from( - JSON.stringify({ - sid: "mock-session", - device_id: "mock-device", - device: "PC", - device_fingerprint: "mock-fp", - fp_version: "1", - }), - ).toString("base64"), - ); + assert(watchReq, "watch report request"); + assert.equal(watchReq.headers["x-sdk-common"], undefined); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: join(output, device + "-player.png") }); const paid = { @@ -667,7 +590,7 @@ try { fullscreen: true, errorRetry: true, realMp4: true, - recommendationsAndTracking: true, + recommendations: true, encryptedImageAndFallback: true, lockedAppDownloadOnly: true, }); diff --git a/scripts/tracker-smoke.ts b/scripts/tracker-smoke.ts deleted file mode 100644 index f5fbeab..0000000 --- a/scripts/tracker-smoke.ts +++ /dev/null @@ -1,170 +0,0 @@ -import assert from "node:assert/strict"; -import { createCipheriv, randomBytes } from "node:crypto"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import { createServer } from "node:http"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { build } from "esbuild"; -import { chromium } from "@playwright/test"; - -// Exercise the shipped SDK and React provider. Every report terminates in a -// browser route; synthetic configuration never reaches an analytics service. -const root = process.cwd(); -const temp = await mkdtemp(join(tmpdir(), "huakui-tracker-")); -const sdk = await readFile(join(root, "public/static/common/web-sdk-v1.2.0.js"), "utf8"); -const configKey = sdk.match(/_secondGcmKeyBase64="([^"\n]+)"/)?.[1]; -assert.ok(configKey, "The SDK configuration encryption contract is available"); -const iv = randomBytes(12); -const cipher = createCipheriv("aes-256-gcm", Buffer.from(configKey, "base64"), iv); -const encryptedConfig = Buffer.concat([ - iv, - cipher.update(JSON.stringify({ - batchUrls: ["https://analytics.fixture.invalid"], - eventCodes: ["app_page_view", "ad_impression", "page_click", "ad_click"], - signKey: "synthetic-tracker-fixture-key", - securityKeyVersion: "fixture", - })), - cipher.final(), - cipher.getAuthTag(), -]).toString("base64"); -const appFile = join(temp, "app.js"); -let browser: Awaited> | undefined; -let server: ReturnType | undefined; - -try { - await build({ - stdin: { - contents: `import {createRoot} from 'react-dom/client'; - import {TrackerProvider} from './src/components/analytics/TrackerProvider'; - createRoot(document.getElementById('root')).render( - - 广告验证 - );`, - loader: "tsx", - resolveDir: root, - }, - outfile: appFile, - bundle: true, - platform: "browser", - format: "iife", - tsconfig: join(root, "tsconfig.json"), - define: { "process.env.NODE_ENV": '"production"' }, - }); - const app = await readFile(appFile); - server = createServer((request, response) => { - const path = new URL(request.url || "/", "http://fixture").pathname; - if (path === "/app.js") { - response.setHeader("Content-Type", "application/javascript").end(app); - } else if (path === "/static/common/web-sdk-v1.2.0.js") { - response.setHeader("Content-Type", "application/javascript").end(sdk); - } else if (path === "/") { - response.setHeader("Content-Type", "text/html; charset=utf-8").end( - 'Tracker fixture
', - ); - } else response.writeHead(404).end(); - }); - await new Promise((done) => server!.listen(0, "127.0.0.1", done)); - const address = server.address(); - assert.ok(address && typeof address !== "string"); - const origin = `http://127.0.0.1:${address.port}`; - browser = await chromium.launch({ - executablePath: process.env.CHROME_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - headless: true, - }); - const commonKeys = ["sid", "device_id", "device", "device_fingerprint", "fp_version"].sort(); - const results: Record[] = []; - for (const fixture of [ - { - device: "iOS", system: "iOS", version: "18.0", - ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1", - }, - { - device: "Android", system: "Android", version: "14", - ua: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/130.0.0.0 Mobile Safari/537.36", - }, - { - device: "PC", system: "Windows", version: "10.0", - ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130.0.0.0 Safari/537.36", - }, - { - device: "PC", system: "macOS", version: "14.6", - ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15", - }, - ]) { - const context = await browser.newContext({ userAgent: fixture.ua }); - const reports: Record[] = []; - const errors: string[] = []; - await context.route("**/*", async (route) => { - const request = route.request(); - const url = new URL(request.url()); - if (url.origin === origin) return route.continue(); - if (url.hostname === "analytics.fixture.invalid") { - if (request.method() === "POST") { - const body: unknown = JSON.parse(request.postData() || "[]"); - assert.ok(Array.isArray(body), "SDK reports use a top-level batch array"); - for (const record of body) { - assert.ok(record && typeof record === "object"); - reports.push(record as Record); - } - } - return route.fulfill({ status: 200, contentType: "application/json", body: "{}" }); - } - return route.abort(); - }); - const page = await context.newPage(); - page.on("pageerror", (error) => errors.push(error.message)); - await page.goto(origin, { waitUntil: "load" }); - await page.waitForFunction(() => !!window.__trackerCommonFields, null, { timeout: 15000 }); - await page.waitForFunction(() => !!window.tracker, null, { timeout: 15000 }); - const common = await page.evaluate(async () => { - const fields = await window.WebSDK!.getCommonFields!(["device", "system_name", "system_version"]); - const header = await window.tracker!.getSDKCommonHeader(); - return { fields, headerKeys: Object.keys(JSON.parse(atob(header))).sort() }; - }); - assert.equal(common.fields.device, fixture.device); - assert.equal(common.fields.system_name, fixture.system); - assert.equal(common.fields.system_version, fixture.version); - assert.deepEqual(common.headerKeys, commonKeys, "X-SDK-Common retains its five-field contract"); - await page.waitForFunction(async () => { - await window.tracker!.flush(); - return true; - }); - await assertEventually(() => reports.some((r) => r.event === "app_page_view") && - reports.some((r) => r.event === "ad_impression")); - for (const event of ["app_page_view", "ad_impression"]) { - const report = reports.find((record) => record.event === event); - assert.ok(report, `${fixture.system}: ${event} was sent through the real SDK`); - assert.equal(report.device, fixture.device); - assert.equal(report.system_name, fixture.system); - assert.equal(report.system_version, fixture.version); - assert.equal(report.app_id, "JHA-4001"); - assert.ok(typeof report.event_sig === "string" && report.event_sig.length > 0, "Real SDK signs the report"); - } - assert.deepEqual(errors, []); - results.push({ system: fixture.system, version: fixture.version, events: ["app_page_view", "ad_impression"], headerKeys: common.headerKeys.length }); - await context.close(); - } - console.log(JSON.stringify({ passed: true, cases: results }, null, 2)); -} finally { - await browser?.close(); - if (server) await new Promise((done) => server!.close(() => done())); - await rm(temp, { recursive: true, force: true }); -} - -async function assertEventually(predicate: () => boolean) { - const deadline = Date.now() + 15000; - while (!predicate() && Date.now() < deadline) - await new Promise((done) => setTimeout(done, 50)); - assert.ok(predicate(), "The SDK sent both the page view and visible ad impression"); -} diff --git a/src/components/analytics/TrackerProvider.tsx b/src/components/analytics/TrackerProvider.tsx deleted file mode 100644 index cf698e2..0000000 --- a/src/components/analytics/TrackerProvider.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; -import { useEffect, type ReactNode } from "react"; -import type { Data } from "@/server/services/model"; -import { - initializeTracker, - navigation, - pageClick, - pageView, - externalAd, - tracker, - track, - type TrackerBoot, -} from "./tracker"; - -function loadScript(src: string): Promise { - return new Promise((resolve) => { - const existing = document.querySelector( - `script[src="${src}"]`, - ); - if (existing) { - resolve(); - return; - } - const script = document.createElement("script"); - script.src = src; - script.async = true; - script.onload = () => resolve(); - script.onerror = () => resolve(); - document.head.append(script); - }); -} -export function TrackerProvider({ - data, - children, - boot: explicitBoot, -}: { - data?: Data; - boot?: TrackerBoot; - children?: ReactNode; -}) { - useEffect(() => { - let disposed = false; - const boot: TrackerBoot = explicitBoot || { - appId: String(data?.trackerAppId || ""), - encryptedConfig: - data?.reportSdkConfig ?? - data?.appConfig?.reportSdkConfig ?? - data?.appConfig?.ReportSDKConfig ?? - data?.appConfig?.reportSDKConfig, - appVersion: "1.0.0", - pageKey: data?.pageKey, - pageName: data?.pageName, - userType: data?.isVip ? "vip" : "normal", - uid: String(data?.userID || data?.userId || ""), - channel: data?.channel, - }; - void loadScript("/static/common/web-sdk-v1.2.0.js?v=huakui-20261013-system") - .then(() => initializeTracker(boot)) - .then(() => { - if (!disposed) pageView(); - }); - const google = "G-J58C18CTS0"; - const yandex = - data?.isNavPage || data?.requestPath === "/navigation/" ? 112952789 : 112951899; - window.dataLayer ||= []; - window.gtag ||= (...args) => { - window.dataLayer!.push(args); - }; - window.gtag("js", new Date()); - window.gtag("config", google); - void loadScript(`https://www.googletagmanager.com/gtag/js?id=${google}`); - window.ym ||= Object.assign( - (...args: unknown[]) => { - window.ym!.a!.push(args); - }, - { a: [] as unknown[][], l: Date.now() }, - ); - window.ym(yandex, "init", { - ssr: true, - webvisor: true, - clickmap: true, - ecommerce: "dataLayer", - referrer: document.referrer, - url: location.href, - accurateTrackBounce: true, - trackLinks: true, - }); - void loadScript(`https://mc.yandex.ru/metrika/tag.js?id=${yandex}`); - const click = (event: MouseEvent) => { - if (!(event.target instanceof Element)) return; - const target = event.target; - if ( - !target.closest( - "[data-ad-close],[data-dialog-ad-close],[data-download-close],[data-login-close],[data-register-close],[data-recharge-close],[data-play-coin-close],[data-setting-password-close],[data-drawer-close]", - ) - ) - pageClick(event); - const anchor = target.closest( - "a[data-navigation-key],a[data-nav-key]", - ); - if (anchor) navigation(anchor); - const ad = target.closest( - '[data-track-ad="external"][data-ad-id]', - ); - if (ad) externalAd(ad, "click"); - }; - document.addEventListener("click", click, true); - const observer = - typeof IntersectionObserver === "undefined" - ? null - : new IntersectionObserver( - (entries) => { - for (const entry of entries) - if (entry.isIntersecting) { - externalAd(entry.target as HTMLElement, "show"); - observer?.unobserve(entry.target); - } - }, - { threshold: 0.1 }, - ); - const observed = new WeakSet(); - const observe = () => - document - .querySelectorAll( - '[data-track-ad="external"][data-ad-id]:not([data-ad-clone])', - ) - .forEach((node) => { - if (!observed.has(node)) { - observed.add(node); - if (observer) observer.observe(node); - else externalAd(node as HTMLElement, "show"); - } - }); - observe(); - const mutations = new MutationObserver(observe); - mutations.observe(document.body, { childList: true, subtree: true }); - const external = (event: Event) => { - const detail = ( - event as CustomEvent<{ element: HTMLElement; eventType: string }> - ).detail; - if (detail?.element) externalAd(detail.element, detail.eventType); - }; - const advertising = (event: Event) => { - const detail = ( - event as CustomEvent<{ element: HTMLElement; eventType: string }> - ).detail; - const node = detail?.element; - if (node) - void track("advertising", { - event_type: detail.eventType, - advertising_key: node.dataset.advertisingKey || "dialog_ad", - advertising_name: node.dataset.advertisingName || "弹窗广告", - advertising_id: - node.dataset.advertisingId || node.dataset.dialogAdId || "", - }); - }; - document.addEventListener("external-ad:track", external); - document.addEventListener("dialog-ad:track", advertising); - const flush = () => { - void tracker.flush(); - }; - window.addEventListener("pagehide", flush); - return () => { - disposed = true; - observer?.disconnect(); - mutations.disconnect(); - document.removeEventListener("click", click, true); - document.removeEventListener("external-ad:track", external); - document.removeEventListener("dialog-ad:track", advertising); - window.removeEventListener("pagehide", flush); - }; - }, [data, explicitBoot]); - return children; -} -export function useTracker() { - return tracker; -} -export default TrackerProvider; diff --git a/src/components/analytics/tracker.ts b/src/components/analytics/tracker.ts deleted file mode 100644 index afcc231..0000000 --- a/src/components/analytics/tracker.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { toLegacyPath } from "@/lib/routes"; -export type Fields = Record; -export interface TrackerBoot { - appId?: string; - encryptedConfig?: unknown; - appVersion?: string; - uid?: string; - channel?: string; - pageKey?: string; - pageName?: string; - userType?: string; -} -interface WebSDK { - init(options: Fields): unknown; - track: Record unknown>; - getCommonFields?(keys: string[]): Promise; - flush?(): unknown; - setUid?(uid: string): void; - setChannel?(channel: string): void; - setContext?(fields: Fields): void; -} -declare global { - interface Window { - WebSDK?: WebSDK; - __TRACKER_BOOT__?: TrackerBoot; - __TRACKER__?: typeof tracker; - tracker?: typeof tracker; - __trackerCommonFields?: Record; - __CHANNEL_CODE__?: string; - dataLayer?: unknown[]; - gtag?: (...args: unknown[]) => void; - ym?: ((...args: unknown[]) => void) & { a?: unknown[][]; l?: number }; - } -} -const str = (value: unknown, fallback = "") => - String(value ?? "").trim() || fallback; -const number = (value: unknown, fallback = 0) => - Number.isFinite(Number(value)) ? Number(value) : fallback; -const commonKeys = [ - "sid", - "device_id", - "device", - "device_fingerprint", - "fp_version", -]; -const serverKeys = [ - "app_id", - "uid", - "channel", - "sid", - "client_ts", - "device", - "device_id", - "device_brand", - "device_model", - "device_fingerprint", - "fp_version", - "user_agent", - "sdk_version", - "app_version", -]; -let activeBoot: TrackerBoot = {}; -let finishReady: () => void; -const ready = new Promise((resolve) => { - finishReady = resolve; -}); -let initializing = false; -let common: Record = Object.fromEntries( - commonKeys.map((key) => [key, ""]), -); -let commonReady: Promise = Promise.resolve(); -let page = { key: "internal_other", name: "站内其他页面" }; -let userType = "normal"; -let drama: Fields | null = null; -const impressions = new Set(); -export function normalizeUID(value: unknown): string { - const uid = str(value); - return uid.length >= 4 && - uid.length <= 32 && - /^[A-Za-z0-9_]+$/.test(uid) && - !["u_1001", "stress_user", "u_lab"].includes(uid.toLowerCase()) - ? uid - : ""; -} -export function normalizeChannel(value: unknown): string { - const channel = str(value); - return channel.length <= 50 && - /^[A-Za-z0-9_.-]+$/.test(channel) && - !["0", "self", "natural", "official", "channel", "unknown", "-"].includes( - channel.toLowerCase(), - ) - ? channel - : "organic"; -} -export function inferPage(path: string) { - path = toLegacyPath(path); - if (path === "/") return { key: "home", name: "首页" }; - const rules = [ - ["series-play", "series_play", "AI成人剧集播放页"], - ["series", "series", "AI成人剧集"], - ["play", "play", "短剧播放页"], - ["detail", "detail", "短剧详情"], - ["list", "list", "短剧列表"], - ["rank", "rank", "排行榜"], - ["topic-detail", "topic_detail", "专题详情"], - ["topic", "topic", "专题列表"], - ["tags", "tags", "标签详情"], - ["search-mobile", "search", "搜索结果页"], - ["search", "search", "搜索结果页"], - ["history", "history", "观看历史"], - ["favorites", "favorites", "我的收藏"], - ["profile", "profile", "个人中心"], - ["home-road", "home_road", "回家的路"], - ]; - const rule = rules.find(([prefix]) => - new RegExp(`^/${prefix}(?:/|$)`).test(path), - ); - return rule - ? { key: rule[1], name: rule[2] } - : { key: "internal_other", name: "站内其他页面" }; -} -export function initializeTracker(boot: TrackerBoot): Promise { - const inferred = inferPage(location.pathname); - page = { - key: str(boot.pageKey, inferred.key), - name: str(boot.pageName, inferred.name), - }; - userType = boot.userType === "vip" ? "vip" : "normal"; - activeBoot = boot; - window.__TRACKER_BOOT__ = boot; - window.__TRACKER__ = tracker; - window.tracker = tracker; - if (!initializing) { - initializing = true; - const resolve = () => finishReady(); - let attempts = 0; - const timer = setInterval(() => { - if (!window.WebSDK?.track && ++attempts < 100) return; - clearInterval(timer); - if (!window.WebSDK) { - resolve(); - return; - } - Promise.resolve() - .then(() => - window.WebSDK?.init({ - appId: str(boot.appId), - uid: normalizeUID(boot.uid), - channel: normalizeChannel(boot.channel || window.__CHANNEL_CODE__), - encryptedConfig: str(boot.encryptedConfig), - appVersion: str(boot.appVersion, "1.0.0"), - disableAutoEvents: ["app_page_view", "page_click"], - }), - ) - .catch(() => undefined) - .then(() => { - // Core events need only SDK initialization; device fingerprint loading must not stall them. - commonReady = (async () => { - try { - const fields = await window.WebSDK?.getCommonFields?.( - commonKeys.slice(), - ); - common = Object.fromEntries( - commonKeys.map((key) => [key, str(fields?.[key])]), - ); - } catch { - /* Empty common fields preserve the original degraded SDK contract. */ - } - window.__trackerCommonFields = common; - window.dispatchEvent( - new CustomEvent("tracker:common-fields", { detail: common }), - ); - })(); - window.dispatchEvent( - new CustomEvent("tracker:ready", { detail: page }), - ); - resolve(); - }); - }, 50); - } - return ready; -} -export async function track(method: string, payload: Fields): Promise { - await ready; - try { - return await window.WebSDK?.track[method]?.(payload); - } catch { - return { sent: false, ok: false, reason: "exception" }; - } -} -export function pageView(extra: Fields = {}) { - let referrer = { key: "direct", name: "直接访问" }; - try { - const saved = sessionStorage.getItem("__tracker_last_page"); - if (saved) referrer = JSON.parse(saved); - else if (document.referrer) { - const url = new URL(document.referrer); - referrer = - url.host === location.host - ? inferPage(url.pathname) - : { key: "external", name: url.host }; - } - } catch { - /* Storage is optional. */ - } - const navigation = performance.getEntriesByType("navigation")[0] as - | PerformanceNavigationTiming - | undefined; - void track("appPageView", { - user_type: userType, - page_key: page.key, - page_name: page.name, - referrer_page_key: referrer.key, - referrer_page_name: referrer.name, - current_page_key: page.key, - current_page_name: page.name, - page_load_time: Math.max( - 0, - Math.round( - navigation?.loadEventEnd || - navigation?.domContentLoadedEventEnd || - performance.now(), - ), - ), - recommend_trace_id: "", - ...extra, - }); - try { - sessionStorage.setItem("__tracker_last_page", JSON.stringify(page)); - } catch { - /* Storage is optional. */ - } -} -export function navigation(anchor: HTMLAnchorElement) { - const href = anchor.getAttribute("href"); - if (!href || /^(#|javascript:)/i.test(href)) return; - let key = str(anchor.dataset.navigationKey || anchor.dataset.navKey); - if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key)) { - const url = new URL(href, location.href); - const target = inferPage(url.pathname); - key = - url.origin !== location.origin - ? "nav_external" - : target.key !== "internal_other" - ? "nav_" + target.key - : "nav_" + - (url.pathname.split("/").filter(Boolean)[0] || "home") - .replace(/[^a-z0-9]+/gi, "_") - .slice(0, 48); - } - void track("navigation", { - navigation_key: key.toLowerCase(), - navigation_name: str( - anchor.dataset.navigationName || anchor.dataset.navName, - anchor.getAttribute("aria-label") || - anchor.textContent?.replace(/\s+/g, " ") || - key, - ).slice(0, 255), - }); -} -export function pageClick(event: MouseEvent) { - void track("pageClick", { - page_key: page.key, - page_name: page.name, - click_page_x: Math.trunc(event.clientX), - click_page_y: Math.trunc(event.clientY), - click_x_percent: innerWidth - ? Math.round( - Math.max(0, Math.min(100, (event.clientX / innerWidth) * 100)), - ) - : 0, - click_y_percent: innerHeight - ? Math.round( - Math.max(0, Math.min(100, (event.clientY / innerHeight) * 100)), - ) - : 0, - screen_width: screen.width, - screen_height: screen.height, - recommend_trace_id: "", - }); -} -export function externalAd(node: HTMLElement, type: string) { - if (!["show", "click"].includes(type) || !node.dataset.adId) return; - const slots = Array.from( - document.querySelectorAll( - '[data-track-ad="external"][data-ad-id]:not([data-ad-clone])', - ), - ); - const slot = node.hasAttribute("data-ad-clone") - ? slots.find( - (slot) => slot.getAttribute("data-ad-id") === node.dataset.adId, - ) - : node; - const position = Math.max(1, slots.indexOf(slot as Element) + 1); - const interstitial = node.dataset.adType === "interstitial"; - const payload = { - page_key: page.key, - page_name: page.name, - ad_slot_key: str( - node.dataset.adSlotKey, - `${page.key}_${interstitial ? "popup" : "content_banner"}_${position}`, - ), - ad_slot_name: str( - node.dataset.adSlotName, - page.name + (interstitial ? "弹窗广告位" : `内容横幅第${position}位`), - ), - ad_id: node.dataset.adId, - creative_id: str(node.dataset.creativeId), - ad_type: str(node.dataset.adType, "banner"), - }; - const key = payload.ad_slot_key + "|" + payload.ad_id; - if (!impressions.has(key)) { - impressions.add(key); - void track("adImpression", payload); - } - if (type === "click" && node.dataset.adClickable === "1") - void track("adClick", payload); -} -export function setDramaContext(value: Fields | null) { - drama = value; -} -const behaviorNames: Record = { - ep_view: "展示", - ep_play: "播放", - ep_pause: "暂停", - ep_forward: "快进", - ep_rewind: "快退", - ep_complete: "播放完成", - ep_share: "分享", -}; -export function dramaEvent( - behavior: string, - video?: HTMLVideoElement | null, - options: { played?: number; forceProgress?: number } = {}, -) { - if (!drama) return Promise.resolve(); - const duration = - video && Number.isFinite(video.duration) - ? Math.round(video.duration) - : number(drama.drama_duration); - const current = Math.round(video?.currentTime || 0); - return track("dramaEvent", { - ...drama, - drama_duration: Math.max(0, duration), - play_duration: Math.max(0, Math.round(options.played || 0)), - play_progress: - options.forceProgress ?? - (behavior === "ep_complete" - ? 100 - : duration > 0 - ? Math.round(Math.min(100, (current / duration) * 100)) - : 0), - play_speed: Math.max(1, Math.round((video?.playbackRate || 1) * 100)), - play_behavior_key: behavior, - play_behavior_name: behaviorNames[behavior] || behavior, - }); -} -export function dramaStatusChange( - episode: number, - videoID: string | number, - override?: string, -) { - if (!drama) return Promise.resolve(); - const previous = number(drama.episode, 1); - const behavior = - override || - (episode === previous + 1 - ? "ep_next" - : episode === previous - 1 - ? "ep_previous" - : "ep_switch"); - if (!override && episode === previous && str(videoID) === str(drama.video_id)) - return Promise.resolve(); - const names: Record = { - ep_auto_play: "自动播放", - ep_previous: "上一集", - ep_next: "下一集", - ep_switch: "切换剧集", - ep_unlock: "解锁", - }; - return track("dramaStatusChange", { - media_id: drama.media_id, - drama_id: drama.drama_id, - drama_title: drama.drama_title, - episode: Math.max(1, episode), - video_id: str(videoID, str(drama.video_id)), - pre_episode: previous, - drama_behavior_key: behavior, - drama_behavior_name: names[behavior] || behavior, - }); -} -/** Full per-request SDK snapshot for server-side drama events; header compatibility remains five fields. */ -export async function serverContext(keys = serverKeys): Promise { - await ready; - let fields: Fields = {}; - try { - fields = (await window.WebSDK?.getCommonFields?.(serverKeys.slice())) || {}; - } catch { - /* Cached device identifiers remain usable. */ - } - const defaults: Fields = { - ...common, - app_id: str(activeBoot.appId), - uid: normalizeUID(activeBoot.uid), - channel: normalizeChannel(activeBoot.channel || window.__CHANNEL_CODE__), - client_ts: Math.floor(Date.now() / 1000), - device_brand: "", - device_model: "", - user_agent: navigator.userAgent, - sdk_version: "1.2.0", - app_version: str(activeBoot.appVersion, "1.0.0"), - }; - return Object.fromEntries( - keys.map((key) => [ - key, - key === "client_ts" - ? Math.floor(number(fields[key], number(defaults[key]))) - : str(fields[key] ?? defaults[key]), - ]), - ); -} -export const tracker = { - init: initializeTracker, - pageView, - dramaEvent, - dramaStatusChange, - dramaUnlock: () => - dramaStatusChange( - number(drama?.episode, 1), - str(drama?.video_id), - "ep_unlock", - ), - externalAd, - advertising: (payload: Fields) => track("advertising", payload), - keywordSearch: (payload: Fields) => track("keywordSearch", payload), - keywordClick: (payload: Fields) => track("keywordClick", payload), - getDramaContext: () => ({ ...drama }), - getCommonFieldsSync: () => common, - getCommonFields: serverContext, - serverContext, - getCommonField: async (key: string) => { - await ready; - await commonReady; - return common[key] || ""; - }, - encodeSDKCommonHeader: (fields = common) => - btoa( - unescape( - encodeURIComponent( - JSON.stringify( - Object.fromEntries( - commonKeys.map((key) => [key, str(fields[key])]), - ), - ), - ), - ), - ), - getSDKCommonHeader: async () => { - await ready; - await commonReady; - return tracker.encodeSDKCommonHeader(); - }, - flush: async () => { - try { - await window.WebSDK?.flush?.(); - } catch { - /* Analytics must never interrupt playback. */ - } - }, - setUid: (uid: unknown) => { - activeBoot.uid = normalizeUID(uid); - window.WebSDK?.setUid?.(activeBoot.uid); - }, - setChannel: (channel: unknown) => { - activeBoot.channel = normalizeChannel(channel); - window.WebSDK?.setChannel?.(activeBoot.channel); - }, - setContext: (context: Fields) => window.WebSDK?.setContext?.(context), -}; diff --git a/src/components/catalog/Promotions.tsx b/src/components/catalog/Promotions.tsx index 2435fca..e4a4b31 100644 --- a/src/components/catalog/Promotions.tsx +++ b/src/components/catalog/Promotions.tsx @@ -8,23 +8,10 @@ import { str, type CatalogRecord, } from "@/features/catalog/model"; -function adProps(item: CatalogRecord, type: "banner" | "icon") { - const location = str(item, "Location"), - link = str(item, "Link"); +function adProps(item: CatalogRecord) { + const link = str(item, "Link"); return { href: safeHref(link), - "data-track-ad": "external", - "data-ad-clickable": link ? "1" : undefined, - "data-ad-id": first(get(item, "AdvertiseCode"), get(item, "ID")), - "data-creative-id": "", - "data-ad-location": location, - "data-ad-slot-key": first(get(item, "AdvertiseLocationCode"), location), - "data-ad-slot-name": first( - get(item, "AdvertiseLocationName"), - location ? `广告位${location}` : "", - ), - "data-ad-title": first(get(item, "Title"), "推荐"), - "data-ad-type": type, ...(link ? { target: "_blank", rel: "sponsored nofollow noopener" } : {}), }; } @@ -37,7 +24,7 @@ export function AdBanners({ items }: { items: unknown }) { - - {presentation.header && } {children} - {presentation.footer && }{" "} - {presentation.header && } - - + {presentation.header && } {children} + {presentation.footer && }{" "} + {presentation.header && } ); } diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index c61399f..26ddf5c 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -13,7 +13,6 @@ import { resolvePlayURL, SHELL_BROWSER, } from "@/lib/media/playback"; -import { bindPlaybackAnalytics } from "./usePlaybackAnalytics"; import "xgplayer/dist/index.min.css"; import "@/styles/components/video-player.css"; export interface PlayerHandle { @@ -23,14 +22,13 @@ export interface PlayerHandle { } export interface VideoPlayerProps { url: string; - trackDrama?: boolean; onPlaying?: () => void; onEnded?: () => void; autoPlay?: boolean; } export const VideoPlayer = forwardRef( function VideoPlayer( - { url, trackDrama = false, onPlaying, onEnded, autoPlay = false }, + { url, onPlaying, onEnded, autoPlay = false }, ref, ) { const host = useRef(null), @@ -58,8 +56,7 @@ export const VideoPlayer = forwardRef( ); useEffect(() => { let disposed = false, - instance: Player | null = null, - unbind = () => undefined as void; + instance: Player | null = null; setError(""); void Promise.all([import("xgplayer"), import("xgplayer-hls")]) .then(([{ default: XGPlayer }, { default: HlsPlugin }]) => { @@ -112,7 +109,6 @@ export const VideoPlayer = forwardRef( void instance.play()?.catch(() => undefined); } const video = instance.video as HTMLVideoElement; - unbind = bindPlaybackAnalytics(video, trackDrama); video.addEventListener("playing", () => { setError(""); callbacks.current.onPlaying?.(); @@ -125,11 +121,10 @@ export const VideoPlayer = forwardRef( }); return () => { disposed = true; - unbind(); instance?.destroy(); if (player.current === instance) player.current = null; }; - }, [url, trackDrama, autoPlay, retry]); + }, [url, autoPlay, retry]); return ( <>
{ - setDramaContext(context); - const key = context - ? String(context.drama_id) + ":" + String(context.video_id) - : ""; - if (context && viewed.current !== key) { - viewed.current = key; - void dramaEvent("ep_view"); - } - return () => setDramaContext(null); - }, [context]); -} -const reporters = new WeakMap void>(); -export function reportPlaybackAction( - behavior: string, - video?: HTMLVideoElement | null, -) { - const report = video && reporters.get(video); - if (report) report(behavior); - else void dramaEvent(behavior, video); -} -/** Count wall-clock playing time, excluding buffering and seeking, as the original SDK expects. */ -export function bindPlaybackAnalytics( - video: HTMLVideoElement, - enabled: boolean, -): () => void { - if (!enabled) return () => undefined; - let started = 0, - elapsed = 0, - position = 0, - seekFrom: number | null = null; - const settle = () => { - if (started) elapsed += Math.max(0, Date.now() - started); - started = 0; - }; - const begin = () => { - if (!started) started = Date.now(); - }; - const emit = (behavior: string, keep = false, forceProgress?: number) => { - const played = - (elapsed + (started ? Math.max(0, Date.now() - started) : 0)) / 1000; - void dramaEvent(behavior, video, { played, forceProgress }); - if (!keep) { - elapsed = 0; - if (started) started = Date.now(); - } - }; - reporters.set(video, emit); - const events: Record void> = { - play: () => emit("ep_play"), - playing: begin, - waiting: settle, - pause: () => { - const pending = started || elapsed > 0; - settle(); - if (!video.ended && !video.seeking && pending) emit("ep_pause"); - }, - timeupdate: () => { - if (!video.seeking) position = video.currentTime; - }, - seeking: () => { - if (seekFrom == null) { - settle(); - seekFrom = position; - } - }, - seeked: () => { - if (seekFrom == null) return; - const from = seekFrom; - seekFrom = null; - position = video.currentTime; - if (Math.abs(position - from) >= 0.5) - emit(position > from ? "ep_forward" : "ep_rewind"); - if (!video.paused && !video.ended) begin(); - }, - ended: () => { - settle(); - emit("ep_complete", true); - emit("ep_play", false, 100); - }, - }; - Object.entries(events).forEach(([name, handler]) => - video.addEventListener(name, handler), - ); - const report = () => { - if (elapsed + (started ? Date.now() - started : 0) >= 500) { - settle(); - emit("ep_play"); - void tracker.flush(); - setTimeout(() => void tracker.flush(), 50); - setTimeout(() => void tracker.flush(), 200); - } - }; - const visibility = () => { - if (document.visibilityState === "hidden") report(); - if (!video.paused && !video.ended) begin(); - }; - document.addEventListener("visibilitychange", visibility); - window.addEventListener("pagehide", report); - window.addEventListener("beforeunload", report); - return () => { - reporters.delete(video); - const pending = started || elapsed; - settle(); - if (pending && !video.ended) emit("ep_pause"); - Object.entries(events).forEach(([name, handler]) => - video.removeEventListener(name, handler), - ); - document.removeEventListener("visibilitychange", visibility); - window.removeEventListener("pagehide", report); - window.removeEventListener("beforeunload", report); - }; -} diff --git a/src/components/providers/SiteDialogs.tsx b/src/components/providers/SiteDialogs.tsx index e07c360..7399b02 100644 --- a/src/components/providers/SiteDialogs.tsx +++ b/src/components/providers/SiteDialogs.tsx @@ -439,17 +439,6 @@ function AdItem({ ad, onClose }: { ad: Ad; onClose: () => void }) { const timer = setTimeout(onClose, 12000); return () => clearTimeout(timer); }, [onClose]); - useEffect(() => { - if (ready && ref.current) - document.dispatchEvent( - new CustomEvent("external-ad:track", { - detail: { - element: ref.current.querySelector("[data-track-ad]"), - eventType: "show", - }, - }), - ); - }, [ready, ref]); const image = ( void }) { onError={onClose} /> ); - const click = () => - document.dispatchEvent( - new CustomEvent("external-ad:track", { - detail: { - element: ref.current?.querySelector("[data-track-ad]"), - eventType: "click", - }, - }), - ); return (
void }) { className="hk-advertisement-dialog-panel" data-dialog-ad-item="" data-dialog-ad-id={id} - data-track-ad="external-dialog" - data-ad-id={id} - data-creative-id="" - data-ad-location={ad.location} - data-ad-slot-key={ad.advertiseLocationCode || ad.location || ""} - data-ad-slot-name={ - ad.advertiseLocationName || "弹窗广告位" + (ad.location || "") - } - data-ad-type="interstitial" - data-ad-clickable={ad.link ? "1" : undefined} role="dialog" aria-modal="true" aria-label={ad.title || "广告"} @@ -511,7 +481,6 @@ function AdItem({ ad, onClose }: { ad: Ad; onClose: () => void }) { target="_blank" rel="sponsored nofollow noopener" data-dialog-ad-link="" - onClick={click} > {image} @@ -541,7 +510,6 @@ function AdItem({ ad, onClose }: { ad: Ad; onClose: () => void }) { target="_blank" rel="sponsored nofollow noopener" data-dialog-ad-link="" - onClick={click} > setQuery(keyword), [keyword]); - useEffect(() => { - if (keyword && num(data, "pageNum") <= 1) - void track("keywordSearch", { - keyword, - search_result_count: num(data, "resultCount"), - search_content_type: "", - search_trace_id: "", - search_id: "", - }); - }, [keyword, data.pageNum, data.resultCount]); return ( <> { - void track("keywordClick", { - keyword, - click_item_id: str(item, "ID"), - click_item_type_key: "short_drama", - click_item_type_name: "短剧", - click_position: - (Math.max(1, num(data, "pageNum")) - 1) * - Math.max(1, num(data, "pageSize")) + - index + - 1, - search_trace_id: "", - }); - }} /> ))}
diff --git a/src/features/content/CompliancePage.tsx b/src/features/content/CompliancePage.tsx index 15e88bd..b55fdfc 100644 --- a/src/features/content/CompliancePage.tsx +++ b/src/features/content/CompliancePage.tsx @@ -9,7 +9,7 @@ export function CompliancePage({ data }: PageProps) { value(config, "TGGroup") || value(config, "Telegram") || value(config, "TelegramAdd") || - "https://t.me/huangguosan"; + "https://t.me/jbd125"; const ref = useLegalNavigation(); return (
diff --git a/src/features/content/DMCAPage.tsx b/src/features/content/DMCAPage.tsx index 94e8b98..f7308cc 100644 --- a/src/features/content/DMCAPage.tsx +++ b/src/features/content/DMCAPage.tsx @@ -9,7 +9,7 @@ export function DMCAPage({ data }: PageProps) { value(config, "TGGroup") || value(config, "Telegram") || value(config, "TelegramAdd") || - "https://t.me/huangguosan"; + "https://t.me/jbd125"; const ref = useLegalNavigation(); return (
diff --git a/src/features/playback/DramaWatchPage.tsx b/src/features/playback/DramaWatchPage.tsx index e5b5056..97df4c1 100644 --- a/src/features/playback/DramaWatchPage.tsx +++ b/src/features/playback/DramaWatchPage.tsx @@ -1,5 +1,5 @@ "use client"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { apiRequest } from "@/lib/api"; import { useSite } from "@/components/providers/SiteProvider"; import { Icon } from "@/components/ui/Icon"; @@ -7,7 +7,6 @@ import { BackLink } from "@/components/catalog/Interactions"; import { AdBanners } from "@/components/catalog/Promotions"; import { PlayLinks } from "@/components/catalog/PlayLinks"; import type { PlayerHandle } from "@/components/player/VideoPlayer"; -import { useDramaContext } from "@/components/player/usePlaybackAnalytics"; import { array, chapterPlayURL, @@ -23,8 +22,7 @@ import { externalProps, type Data, } from "@/features/catalog/model"; -import { dramaStatusChange } from "@/lib/tracker"; -import { trackingContext, coinGate } from "./model"; +import { coinGate } from "./model"; import { PlaybackStage } from "./PlaybackStage"; import { AppOnlyDialog } from "./AppOnlyDialog"; import { GuessLike, HotList, PlaybackShare, Related, Synopsis } from "./Shared"; @@ -47,12 +45,7 @@ export function DramaWatchPage({ data }: { data: Data }) { watchSent = useRef(false); const site = useSite(), loggedIn = !!site.user, - name = displayName(p), - context = useMemo( - () => trackingContext(data.trackerDrama), - [data.trackerDrama], - ); - useDramaContext(context); + name = displayName(p); const themes = namedThemeTags( get(p, "ThemeIDs"), get(p, "ThemeNames"), @@ -103,11 +96,7 @@ export function DramaWatchPage({ data }: { data: Data }) {
{name}
- - mode === "main" ? player.current?.video() || null : null - } - /> +
@@ -208,9 +197,6 @@ export function DramaWatchPage({ data }: { data: Data }) { - mode === "main" ? player.current?.video() || null : null - } /> void dramaStatusChange(ep, chid)} > {ep} diff --git a/src/features/playback/PlaybackStage.tsx b/src/features/playback/PlaybackStage.tsx index e78f66a..6316a8e 100644 --- a/src/features/playback/PlaybackStage.tsx +++ b/src/features/playback/PlaybackStage.tsx @@ -18,7 +18,6 @@ import { type Data, } from "@/features/catalog/model"; import { mediaTime } from "./model"; -import { dramaStatusChange } from "@/lib/tracker"; export interface PlaybackStageProps { data: Data; series?: boolean; @@ -84,7 +83,6 @@ export function PlaybackStage({ @@ -183,9 +181,6 @@ export function PlaybackStage({ className="hk-playback-player__icon" href={nextURL} aria-label="下一集" - onClick={() => - void dramaStatusChange(ep + 1, Number(data.nextChapterID)) - } > diff --git a/src/features/playback/SeriesWatchPage.tsx b/src/features/playback/SeriesWatchPage.tsx index dafbb66..ef5d1e8 100644 --- a/src/features/playback/SeriesWatchPage.tsx +++ b/src/features/playback/SeriesWatchPage.tsx @@ -5,7 +5,6 @@ import { BackLink } from "@/components/catalog/Interactions"; import { AdBanners } from "@/components/catalog/Promotions"; import { PlayLinks } from "@/components/catalog/PlayLinks"; import type { PlayerHandle } from "@/components/player/VideoPlayer"; -import { useDramaContext } from "@/components/player/usePlaybackAnalytics"; import { array, first, @@ -24,7 +23,6 @@ export function SeriesWatchPage({ data }: { data: Data }) { title = str(video, "Title") || "视频", player = useRef(null), [playing, setPlaying] = useState(false); - useDramaContext(null); const play = () => player.current?.play(), actions = "btn btn--ghost play-actions__btn play-actions__btn--icon"; return ( diff --git a/src/features/playback/Shared.tsx b/src/features/playback/Shared.tsx index 6c73626..daae03c 100644 --- a/src/features/playback/Shared.tsx +++ b/src/features/playback/Shared.tsx @@ -20,15 +20,12 @@ import { type Data, type NamedTag, } from "@/features/catalog/model"; -import { reportPlaybackAction } from "@/components/player/usePlaybackAnalytics"; export function PlaybackShare({ className = "page-subheader__btn", label = false, - getVideo, }: { className?: string; label?: boolean; - getVideo?: () => HTMLVideoElement | null; }) { const site = useSite(); return ( @@ -37,7 +34,6 @@ export function PlaybackShare({ type="button" aria-label="分享" onClick={async () => { - reportPlaybackAction("ep_share", getVideo?.()); try { if (navigator.share) await navigator.share({ diff --git a/src/features/playback/model.ts b/src/features/playback/model.ts index f3e361f..a328841 100644 --- a/src/features/playback/model.ts +++ b/src/features/playback/model.ts @@ -1,26 +1,5 @@ import { get, number, type CatalogRecord } from "@/features/catalog/model"; -import type { Fields } from "@/components/analytics/tracker"; export const integer = (value: unknown) => Math.trunc(number(value)); -export function trackingContext(input: unknown): Fields | null { - if (!input || !number(get(input, "DramaID"))) return null; - const string = (key: string, fallback = "") => - String(get(input, key) ?? "").trim() || fallback; - return { - media_id: string("MediaID"), - drama_id: string("DramaID"), - drama_title: string("DramaTitle", "未命名短剧"), - episode: Math.max(1, integer(get(input, "Episode"))), - total_episode: Math.max(1, integer(get(input, "TotalEpisode"))), - video_id: string("VideoID"), - drama_type_id: string("DramaTypeID", "default"), - drama_type_name: string("DramaTypeName", "默认分类"), - drama_content_type: "short_drama", - recommend_trace_id: string("RecommendTraceID"), - drama_tag_key: string("DramaTagKey", "default"), - drama_tag_name: string("DramaTagName", "默认标签"), - drama_duration: Math.max(0, number(get(input, "DramaDuration"))), - }; -} export function coinGate(chapter: CatalogRecord): boolean { return ( !chapter.canWatch && diff --git a/src/lib/api.ts b/src/lib/api.ts index 21dee64..f99c499 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -10,35 +10,14 @@ export class ApiError extends Error { this.name = "ApiError"; } } -export interface DramaContext { - media_id?: string | number; - drama_id?: string | number; - episode?: number; - video_id?: string | number; - recommend_trace_id?: string; -} export interface RequestOptions { method?: string; params?: Record; headers?: Record; timeout?: number; - trackerContext?: DramaContext; - trackerEvent?: string | false; signal?: AbortSignal; skipRecovery?: boolean; } -type Tracker = { - getSDKCommonHeader?: () => string | Promise; - getCommonFields?: () => - | Record - | Promise>; - getDramaContext?: () => DramaContext; - setUid?: (uid: string) => void; -}; -const tracker = () => - typeof window === "undefined" - ? undefined - : (window as unknown as { tracker?: Tracker }).tracker; export function getCookie(name: string): string { if (typeof document === "undefined") return ""; const value = @@ -103,7 +82,6 @@ export function setAuthSession(user: Record) { } catch { /* storage disabled */ } - if (user.userId != null) tracker()?.setUid?.(String(user.userId)); document.dispatchEvent(new CustomEvent("auth:login", { detail: { user } })); } export function getDeviceID(): string { @@ -149,111 +127,11 @@ export function getChannel(): string { setCookie("channel", value, 365); return value; } -const events: Record = { - "/api/playlet/chapter/like/set": "drama_like", - "/api/playlet/like/set": "drama_like", - "/api/playlet/chapter/collect/set": "drama_collect", - "/api/playlet/collect/set": "drama_collect", - "/api/comment/send": "drama_comment", - "/api/playlet/chapter/unlock": "drama_purchase", -}; -const base64 = (value: unknown) => - btoa( - Array.from(new TextEncoder().encode(JSON.stringify(value)), (n) => - String.fromCharCode(n), - ).join(""), - ) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); -const clean = (value: unknown) => - String(value ?? "") - .replace(/[^\x20-\x7e]|[;=]/g, " ") - .trim(); -async function deadline( - promise: Promise, - fallback: T, - ms = 1500, -): Promise { - let timer: ReturnType | undefined; - try { - return await Promise.race([ - promise.catch(() => fallback), - new Promise((resolve) => { - timer = setTimeout(() => resolve(fallback), ms); - }), - ]); - } finally { - clearTimeout(timer); - } -} -async function requestHeaders( - path: string, - method: string, - options: RequestOptions, -): Promise { - const headers = new Headers({ +function requestHeaders(options: RequestOptions): Headers { + return new Headers({ Accept: "application/json", ...options.headers, }); - if (new URL(path, location.href).origin !== location.origin) return headers; - const sdk = tracker(); - const commonHeader = deadline( - Promise.resolve().then(() => sdk?.getSDKCommonHeader?.() || ""), - "", - ); - const event = - options.trackerEvent === false - ? "" - : options.trackerEvent || events[new URL(path, location.href).pathname]; - const context = options.trackerContext || sdk?.getDramaContext?.(); - const common = - event && !["GET", "HEAD"].includes(method) && context?.drama_id - ? await deadline( - Promise.resolve().then(() => sdk?.getCommonFields?.() || {}), - {}, - ) - : null; - if (common && context) { - const requestID = Array.from( - crypto.getRandomValues(new Uint8Array(16)), - (n) => n.toString(16).padStart(2, "0"), - ).join(""); - const drama = { - media_id: String(context.media_id ?? "").trim(), - drama_id: String(context.drama_id ?? "").trim(), - episode: Math.max(1, context.episode || 1), - video_id: String(context.video_id ?? "").trim(), - recommend_trace_id: String(context.recommend_trace_id ?? "").trim(), - }; - headers.set("X-Tracker-Request-Id", requestID); - headers.set("X-Tracker-Event-Name", event); - headers.set( - "X-DC-Request-Context", - base64({ v: 1, request_id: requestID, event_name: event, common, drama }), - ); - headers.set( - "X-User-Agent", - [ - "DevID=" + clean(common.device_id), - "DevType=" + clean(common.device || "OTHER"), - "SysType=" + - (common.device === "Android" - ? "h5_android" - : common.device === "iOS" - ? "h5_ios" - : "h5_pc"), - "Ver=" + clean(common.app_version || "1.0.0"), - "BuildID=web", - "device_brand=" + clean(common.device_brand), - "device_model=" + clean(common.device_model), - "sid=" + clean(common.sid), - ].join(";"), - ); - } - const value = await commonHeader; - if (value && !headers.has("X-SDK-Common")) headers.set("X-SDK-Common", value); - return headers; } let guestPromise: Promise | null = null; export async function ensureGuestSession(): Promise { @@ -294,7 +172,7 @@ async function recoverSession(): Promise { }); return recovery; } -/** Same-origin BFF request; prepared tracking headers and request_id survive one guest retry. */ +/** Same-origin BFF request; retries once after guest session recovery. */ export async function apiRequest>( path: string, body?: unknown, @@ -304,7 +182,7 @@ export async function apiRequest>( const url = new URL(path, location.href); for (const [key, value] of Object.entries(options.params || {})) if (value != null) url.searchParams.set(key, String(value)); - const headers = await requestHeaders(url.href, method, options); + const headers = requestHeaders(options); const payload = body == null ? undefined diff --git a/src/lib/routes.ts b/src/lib/routes.ts index a357aad..7ff9a72 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -79,7 +79,7 @@ const navigationFields = new Set([ // Raw API/SDK/payment/media objects must retain their request and signed source addresses. const opaqueFields = new Set([ "appConfig", "config", "advitise", "advertise", "chapter", "rawMedia", "videoInfo", "video", "media", "images", - "reportSdkConfig", "ReportSDKConfig", "installGuide", "deposit", "payment", "payMethods", + "installGuide", "deposit", "payment", "payMethods", "jsonLD", "seoInfo", "canonicalURL", "paginationPrevURL", "paginationNextURL", ]); diff --git a/src/lib/tracker.ts b/src/lib/tracker.ts deleted file mode 100644 index dcd34c7..0000000 --- a/src/lib/tracker.ts +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; -export { - track, - tracker, - dramaEvent, - dramaStatusChange, - setDramaContext, - externalAd, -} from "@/components/analytics/tracker"; -export type { Fields, TrackerBoot } from "@/components/analytics/tracker"; diff --git a/src/server/config.ts b/src/server/config.ts index c2eabaf..bcb7c59 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -15,7 +15,7 @@ export function installationURL(value: string | undefined, deepLink = false): st export function serverConfig() { const environment = (process.env.APP_ENV || 'development') as AppEnvironment; if (!['development', 'test', 'production'].includes(environment)) throw new Error('Invalid APP_ENV'); - const upstreamURL = (process.env.UPSTREAM_API_URL || (environment === 'development' ? 'http://172.104.181.99:30331' : '')).trim().replace(/\/+$/, ''); + const upstreamURL = (process.env.UPSTREAM_API_URL || (environment === 'development' ? 'https://webhuakuiapi.mfcg.me' : '')).trim().replace(/\/+$/, ''); if (upstreamURL && !/^https?:\/\//.test(upstreamURL)) throw new Error('UPSTREAM_API_URL must be an HTTP(S) URL'); return { environment, upstreamURL, @@ -23,10 +23,10 @@ export function serverConfig() { siteURL: (process.env.SITE_URL || '').trim().replace(/\/+$/, ''), imageCDNURL: (process.env.IMAGE_CDN_URL || '').trim(), timeout: Math.max(100, Number(process.env.UPSTREAM_TIMEOUT_MS) || 20000), - trackerAppID: process.env.TRACKER_APP_ID || 'JHA-4001', - trackerEncryptedConfig: process.env.TRACKER_ENCRYPTED_CONFIG || '', m3u8KeyFile: process.env.M3U8_KEY_FILE || './config/secrets/m3u8.key', navigationOnly: process.env.NAVIGATION_ONLY === 'true', + /** Every Telegram contact entry points here, overriding the upstream tgGroup/telegram/telegramAdd. */ + contactTelegramURL: installationURL(process.env.CONTACT_TELEGRAM_URL) || 'https://t.me/jbd125', installation: { iosStoreURL: installationURL(process.env.APP_IOS_STORE_URL), iosProfileURL: installationURL(process.env.APP_IOS_PROFILE_URL), diff --git a/src/server/pages/playback.ts b/src/server/pages/playback.ts index bf0b098..48a6a9f 100644 --- a/src/server/pages/playback.ts +++ b/src/server/pages/playback.ts @@ -12,13 +12,6 @@ async function guessData(ctx:RequestContext,id:number,page:number):Promise if(!arr(raw.list).length&&page>1)raw=await apiData(ctx,'/api/app/playlet/guess-like/list',{playletId:id,pageNum:1,pageSize:6},{public:true}); raw.pageNum=num(raw.pageNum)||page;raw.pageSize=num(raw.pageSize)||6;return raw; } -export function dramaTracking(playlet:Data,chapter:Data,chapterID:number,episode:number):Data{ - const data:Data={MediaID:'',DramaID:num(playlet.id),DramaTitle:first(playlet.playName,playlet.title),Episode:Math.max(1,episode),TotalEpisode:num(playlet.totalEpisode)>0?num(playlet.totalEpisode):arr(playlet.playletChapterInfo).length||1,VideoID:num(chapter.id)>0?num(chapter.id):chapterID,DramaTypeID:'default',DramaTypeName:'默认分类',DramaTagKey:'default',DramaTagName:'默认标签',DramaDuration:Math.max(0,num(chapter.mediaTime)),RecommendTraceID:''}; - const ids=Array.isArray(playlet.themeIds)?playlet.themeIds:[],names=Array.isArray(playlet.themeNames)?playlet.themeNames:[]; - for(let i=0;i0&&text(names[i])){data.DramaTypeID=String(ids[i]);data.DramaTypeName=text(names[i]);break;} - const tags=Array.isArray(playlet.tagIds)?playlet.tagIds:[],tagNames=Array.isArray(playlet.tagNames)?playlet.tagNames:[],good=tags.map((id:number,i:number)=>({id,name:text(tagNames[i])})).filter((tag:Data)=>num(tag.id)>0&&tag.name); - if(good.length){data.DramaTagKey=good.map((tag:Data)=>tag.id).join(',');data.DramaTagName=good.map((tag:Data)=>tag.name).join(',');}return data; -} export async function play(ctx:RequestContext,p:Params):Promise{ if(p.ep&&integer(p.ep)<=0)return null; await ensureGuestOnEnter(ctx); @@ -32,7 +25,7 @@ export async function play(ctx:RequestContext,p:Params):Promise{ let unlockCost=num(chapter.costCoins)>0?num(chapter.costCoins):Math.max(0,num(chapter.singleCoins));if(unlockCost<=0){const paid=chapters.find(item=>!item.AllowWatch&&(item.needUnlock||num(item.payType)===1));unlockCost=num(paid?.costCoins)>0?num(paid?.costCoins):num(paid?.singleCoins);} const [hot,guess]=await Promise.all([hotListData(ctx),guessData(ctx,num(playlet.id),positive(p.guessPage))]); const seo=resolvePlayPageSEO(chapter,playlet,episode); - const hotList=arr(hot.list),data:Data={title:seo.title,navActive:'list',seoInfo:seo,playlet,chapter,chapterID,episode,canWatch,mainURL,previewURL,hasPreview:!!previewURL,unlockCost,balance:num(chapter.balance),balanceText:count(chapter.balance),nextChapterID:num(chapters[index+1]?.id),advitise:playlet.advitise,related:arr(playlet.relatedRecommendData),guessLike:arr(guess.list),guessPage:num(guess.pageNum),guessPageSize:num(guess.pageSize),guessTotal:num(guess.totalCount),hotList,hotMoreURL:'/list/all/0/2/',trackerDrama:dramaTracking(playlet,chapter,chapterID,episode),canonicalPath:playURL(playlet.slug,playlet.id)}; + const hotList=arr(hot.list),data:Data={title:seo.title,navActive:'list',seoInfo:seo,playlet,chapter,chapterID,episode,canWatch,mainURL,previewURL,hasPreview:!!previewURL,unlockCost,balance:num(chapter.balance),balanceText:count(chapter.balance),nextChapterID:num(chapters[index+1]?.id),advitise:playlet.advitise,related:arr(playlet.relatedRecommendData),guessLike:arr(guess.list),guessPage:num(guess.pageNum),guessPageSize:num(guess.pageSize),guessTotal:num(guess.totalCount),hotList,hotMoreURL:'/list/all/0/2/',canonicalPath:playURL(playlet.slug,playlet.id)}; data.playResponse={playlet,chapter,chapterID,canWatch,mainURL,previewURL,hasPreview:!!previewURL,guessLike:guess,hotList}; return model(data); } diff --git a/src/server/services/shared.ts b/src/server/services/shared.ts index 440ba19..f28f9c2 100644 --- a/src/server/services/shared.ts +++ b/src/server/services/shared.ts @@ -14,12 +14,16 @@ export function searchTrends(items: unknown): Data[] { } export async function appConfigData(ctx:RequestContext):Promise{ const raw=await apiData(ctx,'/api/app/config/info',{}, {public:true,ttl:60000,stale:true,fallback:{config:{},newAddressInfo:{},advitise:{},seoInfo:{},seoNavInfo:{},navPageList:[]}}); - const clean=(v:unknown)=>text(v).replace(/\\+$/,'').trim(), cfg:Data={...raw.config},addr=raw.newAddressInfo||{}; + const clean=(v:unknown)=>text(v).replace(/\\+$/,'').trim(), cfg:Data={...raw.config},addr:Data={...raw.newAddressInfo}; for(const [key,v] of Object.entries(cfg))if(typeof v==='string')cfg[key]=clean(v); const email=(v:unknown)=>{let value=text(v);while(/^(?:mailto:|https?:\/\/)/i.test(value))value=value.replace(/^(?:mailto:|https?:\/\/)/i,'').trim();return value;}; cfg.qq=text(raw.config?.qq);cfg.weiChart=text(raw.config?.weiChart); cfg.gmailAddr=email(cfg.gmailAddr); - const vm:Data={config:cfg,seoInfo:normalizeSEO(raw.seoInfo),seoNavInfo:raw.seoNavInfo||{},navPageList:arr(raw.navPageList).map(item=>({line:num(item.line),url:clean(item.url)})).filter(item=>item.url),reportSdkConfig:first(raw.reportSdkConfig,process.env.TRACKER_ENCRYPTED_CONFIG),advitise:raw.advitise||{}}; + // All Telegram contact entries, including a Telegram customer-service link, use one configured account. + const telegram=serverConfig().contactTelegramURL,isTelegram=(v:unknown)=>/^https?:\/\/(?:www\.)?(?:t|telegram)\.me\//i.test(clean(v)); + for(const key of ['tgGroup','telegram','telegramAdd'])cfg[key]=addr[key]=telegram; + if(isTelegram(cfg.customerServiceHost)||isTelegram(addr.customerServiceHost))cfg.customerServiceHost=addr.customerServiceHost=telegram; + const vm:Data={config:cfg,seoInfo:normalizeSEO(raw.seoInfo),seoNavInfo:raw.seoNavInfo||{},navPageList:arr(raw.navPageList).map(item=>({line:num(item.line),url:clean(item.url)})).filter(item=>item.url),advitise:raw.advitise||{}}; for(const key of ['foreverUrlAddr','newUrlAddr','newUrlAddr1','newUrlAddr2','gmailAddr','qq','tgGroup','telegram','potaoGroup','customerServiceHost','appAddr','officialAppAddr','pdfAddr','twitterAddr'])vm[key]=first(cfg[key],key==='gmailAddr'?email(addr[key]):clean(addr[key])); const siteURL=serverConfig().siteURL; if(siteURL)cfg.foreverUrlAddr=vm.foreverUrlAddr=siteURL; @@ -47,7 +51,7 @@ export async function loadSharedData(ctx:RequestContext,data:Data={}):Promise({...item}))); data.themeList??=themes;data.homeConfigPlaylets??=activeConfigs(home).filter(config=>num(config.type)===1); data.searchTrends??=model(searchTrends(hot.hotSearch).slice(0,8)); - data.requestPath??=ctx.pathname;data.trackerAppId=serverConfig().trackerAppID; + data.requestPath??=ctx.pathname; const user=ctx.user as Data|null; data.isLoggedIn=!!ctx.token; data.isRegistered=isRegistered(ctx); @@ -64,10 +68,10 @@ export async function loadSharedData(ctx:RequestContext,data:Data={}):Promisepath==='/'+prefix||path.startsWith('/'+prefix+'/'));return rule?[rule[1],rule[2]]:['internal_other','站内其他页面']; diff --git a/src/server/upstream.ts b/src/server/upstream.ts index 9f3b54f..0411bc9 100644 --- a/src/server/upstream.ts +++ b/src/server/upstream.ts @@ -31,160 +31,6 @@ const excluded = new Set([ "x-tracker-request-id", "x-tracker-event-name", ]); -const trackerEvents: Record = { - "/api/playlet/chapter/like/set": "drama_like", - "/api/playlet/like/set": "drama_like", - "/api/playlet/chapter/collect/set": "drama_collect", - "/api/playlet/collect/set": "drama_collect", - "/api/comment/send": "drama_comment", - "/api/playlet/chapter/unlock": "drama_purchase", -}; -const commonKeys = [ - "app_id", - "uid", - "channel", - "sid", - "client_ts", - "device", - "device_id", - "device_brand", - "device_model", - "device_fingerprint", - "fp_version", - "user_agent", - "sdk_version", - "app_version", -]; -const dramaKeys = [ - "media_id", - "drama_id", - "episode", - "video_id", - "recommend_trace_id", -]; -function isRecord(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value); -} -function exactKeys( - value: unknown, - keys: string[], -): value is Record { - return ( - isRecord(value) && - Object.keys(value).length === keys.length && - keys.every((k) => Object.hasOwn(value, k)) - ); -} -export function canonicalTrackerContext( - ctx: RequestContext, -): Record { - const id = ctx.headers.get("x-tracker-request-id") || ""; - const event = ctx.headers.get("x-tracker-event-name") || ""; - const packed = ctx.headers.get("x-dc-request-context") || ""; - if ( - ctx.request.method !== "POST" || - trackerEvents[ctx.pathname] !== event || - !/^[a-f\d]{32}$/i.test(id) || - !/^[A-Za-z\d_-]{1,8192}$/.test(packed) - ) - return {}; - try { - const raw = Buffer.from(packed, "base64url"); - if (raw.length > 6144) return {}; - const data: unknown = JSON.parse(raw.toString()); - if ( - !isRecord(data) || - data.v !== 1 || - data.request_id !== id || - data.event_name !== event || - !exactKeys(data.common, commonKeys) || - !exactKeys(data.drama, dramaKeys) - ) - return {}; - const c = data.common; - const d = data.drama; - const stringField = ( - o: Record, - k: string, - max: number, - required = false, - ) => - typeof o[k] === "string" && - [...(o[k] as string)].length <= max && - (!required || o[k] !== ""); - if ( - !stringField(c, "app_id", 50, true) || - !/^[A-Z]{1,6}-\d{1,6}$/.test(String(c.app_id)) || - c.app_id !== serverConfig().trackerAppID - ) - return {}; - if ( - !stringField(c, "uid", 32) || - (c.uid && - (!/^[A-Za-z\d_]{4,32}$/.test(String(c.uid)) || - ["u_1001", "stress_user", "u_lab"].includes(String(c.uid)))) - ) - return {}; - if ( - !stringField(c, "channel", 50) || - (c.channel && - (!/^[A-Za-z\d_.-]+$/.test(String(c.channel)) || - ["{}", "0", "-", "unknown"].includes( - String(c.channel).toLowerCase(), - ))) - ) - return {}; - if ( - !Number.isInteger(c.client_ts) || - Number(c.client_ts) < 1767196800 || - Number(c.client_ts) > 9999999999 - ) - return {}; - if ( - !stringField(c, "device", 30) || - (c.device && - !["Android", "iOS", "PC", "OTHER"].includes(String(c.device))) - ) - return {}; - for (const [key, max, required] of [ - ["sid", 50, false], - ["device_id", 50, true], - ["device_brand", 100, false], - ["device_model", 100, false], - ["device_fingerprint", 128, false], - ["fp_version", 128, false], - ["user_agent", 4096, false], - ["sdk_version", 128, false], - ["app_version", 128, false], - ] as const) - if (!stringField(c, key, max, required)) return {}; - for (const [key, max, required] of [ - ["media_id", 512, false], - ["drama_id", 512, true], - ["video_id", 512, false], - ["recommend_trace_id", 2048, false], - ] as const) - if (!stringField(d, key, max, required)) return {}; - if (!Number.isSafeInteger(d.episode) || Number(d.episode) <= 0) return {}; - const canonical = Buffer.from( - JSON.stringify({ - v: 1, - request_id: id, - event_name: event, - common: c, - drama: d, - }), - ); - if (canonical.length > 6144) return {}; - return { - "X-DC-Request-Context": canonical.toString("base64url"), - "X-Tracker-Request-Id": id, - "X-Tracker-Event-Name": event, - }; - } catch { - return {}; - } -} export function buildXUserAgent(ctx: RequestContext): string { const supplied = ctx.headers.get("x-user-agent")?.trim(); if (supplied) return supplied; @@ -289,8 +135,6 @@ export function buildUpstreamHeaders( result.delete("Cookie"); } if (options.forwardCookies === false) result.delete("Cookie"); - for (const [name, value] of Object.entries(canonicalTrackerContext(ctx))) - result.set(name, value); return result; } export async function upstreamFetch( diff --git a/tests/brand.test.ts b/tests/brand.test.ts index 2bd8fef..31925e3 100644 --- a/tests/brand.test.ts +++ b/tests/brand.test.ts @@ -37,14 +37,14 @@ test('page injection and cached upstream configuration cannot restore the former process.env.UPSTREAM_API_URL='http://brand-contract.test';const calls=new Map(); globalThis.fetch=async(input)=>{ const path=new URL(String(input)).pathname;calls.set(path,(calls.get(path)||0)+1);let data:unknown={list:[],totalCount:0}; - if(path==='/api/app/config/info')data={config:{foreverUrlAddr:'https://canonical.drama.test',gmailAddr:'support@huangguo.com',brandName:'黄果短剧'},seoInfo:{title:'黄果短剧首页',description:'黄果短剧介绍',webSiteName:'黄果短剧',keywords:['黄果短剧']},seoNavInfo:{dhTitle:'黄果短剧导航',dhDescription:'黄果短剧最新线路',dhSiteName:'黄果短剧'},reportSdkConfig:'黄果短剧-signed-sdk'}; + if(path==='/api/app/config/info')data={config:{foreverUrlAddr:'https://canonical.drama.test',gmailAddr:'support@huangguo.com',brandName:'黄果短剧'},seoInfo:{title:'黄果短剧首页',description:'黄果短剧介绍',webSiteName:'黄果短剧',keywords:['黄果短剧']},seoNavInfo:{dhTitle:'黄果短剧导航',dhDescription:'黄果短剧最新线路',dhSiteName:'黄果短剧'}}; if(path==='/api/app/translation/category/list')data={categoryList:[{name:'黄果短剧首页',urlValue:'/',categoryType:1,seoInfo:{title:'黄果短剧首页',description:'黄果短剧介绍'}}]}; if(path==='/api/app/playlet/home')data={seoInfo:{title:'黄果短剧首页'},announcements:[{content:'欢迎来到黄果短剧'}],homeConfigData:[{name:'黄果短剧精选',type:1,status:true}],homeBannerData:[{id:1,playName:'黄果恋人'}]}; return Response.json({code:200,data}); }; try{ const page=await buildPage(context('/'));const config=page.data.appConfig; - assert.equal(page.data.title,'花魁短剧首页');assert.equal(config.config.brandName,BRAND_NAME);assert.equal(config.seoInfo.webSiteName,BRAND_NAME);assert.equal(config.seoNavInfo.dhTitle,'花魁短剧导航');assert.equal(page.data.categoryList[0].name,'花魁短剧首页');assert.equal(page.data.announcements[0].content,'欢迎来到花魁短剧');assert.equal(page.data.homeConfigPlaylets[0].name,'花魁短剧精选');assert.equal(page.data.homeBannerData[0].playName,'黄果恋人');assert.equal(config.gmailAddr,'support@huangguo.com');assert.equal(config.reportSdkConfig,'黄果短剧-signed-sdk'); + assert.equal(page.data.title,'花魁短剧首页');assert.equal(config.config.brandName,BRAND_NAME);assert.equal(config.seoInfo.webSiteName,BRAND_NAME);assert.equal(config.seoNavInfo.dhTitle,'花魁短剧导航');assert.equal(page.data.categoryList[0].name,'花魁短剧首页');assert.equal(page.data.announcements[0].content,'欢迎来到花魁短剧');assert.equal(page.data.homeConfigPlaylets[0].name,'花魁短剧精选');assert.equal(page.data.homeBannerData[0].playName,'黄果恋人');assert.equal(config.gmailAddr,'support@huangguo.com'); const cached=await apiData(context('/'),'/api/app/config/info',{}, {public:true,ttl:60000});assert.equal(cached.seoInfo.webSiteName,BRAND_NAME);assert.equal(cached.config.brandName,BRAND_NAME);assert.equal(calls.get('/api/app/config/info'),1); const rendered=enrichSEO(context('/'),page.data);assert.equal(rendered.siteName,BRAND_NAME);assert.equal(rendered.canonicalURL,'https://canonical.drama.test/');assert.equal(rendered.metaTitle,'花魁短剧首页'); const help=await buildPage(context('/help/'));assert.equal(help.data.faqItems[0].Question,'花魁短剧网页版可以看全集吗?'); diff --git a/tests/catalog-react.test.tsx b/tests/catalog-react.test.tsx index 768b551..9498f17 100644 --- a/tests/catalog-react.test.tsx +++ b/tests/catalog-react.test.tsx @@ -112,9 +112,8 @@ test("React list advertising keeps independent desktop and mobile row positions 3, ); const promo = renderToStaticMarkup(); - assert.equal((promo.match(/data-track-ad="external"/g) || []).length, 2); - assert.equal((promo.match(/data-ad-clone="1"/g) || []).length, 1); - assert.match(promo, /rel="sponsored nofollow noopener"/); + assert.equal((promo.match(/rel="sponsored nofollow noopener"/g) || []).length, 2); + assert.equal((promo.match(/tabindex="-1"/g) || []).length, 1); }); test("React filter and pagination links preserve the source path-only canonical route contracts", () => { diff --git a/tests/pages.test.ts b/tests/pages.test.ts index 40aab31..2015152 100644 --- a/tests/pages.test.ts +++ b/tests/pages.test.ts @@ -63,7 +63,7 @@ test('missing upstream fields retain legacy Go zero values rather than blank att assert.equal(detail.CountLike,0);assert.equal(detail.IsLike,0);assert.equal(detail.Preview,'');assert.equal(detail.ThemeNames,null);assert.equal(detail.PlayletChapterInfo[0].MediaTime,0);assert.equal(detail.PlayletChapterInfo[0].NeedUnlock,false);assert.equal(detail.PlayletChapterInfo[0].AllowWatch,true); }); -test('list and playback page loaders preserve filters, canonical paths, locked media and tracking',async()=>{ +test('list and playback page loaders preserve filters, canonical paths and locked media',async()=>{ const originalFetch=globalThis.fetch,originalUpstream=process.env.UPSTREAM_API_URL;process.env.UPSTREAM_API_URL='http://page-contract-test';const calls:{url:URL;headers:Headers}[]=[]; globalThis.fetch=async(input,init)=>{ const url=new URL(String(input));calls.push({url,headers:new Headers(init?.headers)});let data:any={list:[],totalCount:0}; @@ -78,7 +78,7 @@ test('list and playback page loaders preserve filters, canonical paths, locked m const listed=await list(ctx('/list/12/2/3/page/2/'),{theme:'12',updateStatus:'2',sortType:'3',page:'2'}); assert.equal(listed.themeId,12);assert.equal(listed.themeSlug,'romance');assert.equal(listed.canonicalPath,'/list/romance/2/3/page/2/');assert.equal(listed.pagination.NextURL,'/list/romance/2/3/page/3/');assert.equal(listed.pageHeading,'爱情短剧'); const catalogue=calls.find(call=>call.url.pathname==='/api/app/playlet/all/list')!;assert.equal(catalogue.url.searchParams.get('themeSlug'),'romance');assert.equal(catalogue.headers.has('authorization'),false); - const played=await play(ctx('/play/drama/102/'),{slug:'drama',ep:'102'});assert.ok(played);assert.equal(played.chapterID,102);assert.equal(played.episode,2);assert.equal(played.canWatch,false);assert.equal(played.mainURL,'');assert.equal(played.previewURL,'preview.m3u8');assert.equal(played.balanceText,'12,345');assert.equal(played.seoInfo.title,'剧名 - 第二集');assert.equal(played.canonicalPath,'/drama/drama/');assert.equal(played.trackerDrama.VideoID,102);assert.equal(played.trackerDrama.DramaTypeID,'12');assert.equal(played.comments,undefined);assert.ok(!calls.some(call=>call.url.pathname==='/api/app/comment/list')); + const played=await play(ctx('/play/drama/102/'),{slug:'drama',ep:'102'});assert.ok(played);assert.equal(played.chapterID,102);assert.equal(played.episode,2);assert.equal(played.canWatch,false);assert.equal(played.mainURL,'');assert.equal(played.previewURL,'preview.m3u8');assert.equal(played.balanceText,'12,345');assert.equal(played.seoInfo.title,'剧名 - 第二集');assert.equal(played.canonicalPath,'/drama/drama/');assert.equal(played.comments,undefined);assert.ok(!calls.some(call=>call.url.pathname==='/api/app/comment/list')); const chapter=calls.find(call=>call.url.pathname==='/api/app/playlet/chapter/detail')!;assert.equal(chapter.url.searchParams.get('chapterId'),'102');assert.equal(chapter.headers.get('authorization'),'test-session'); assert.equal(await play(ctx('/play/drama/999/'),{slug:'drama',ep:'999'}),null); for(const path of ['/search/?q=legacy','/search/?page=2','/play/drama/?guessPage=2','/favorites/?filter=all','/favorites/invalid/','/history/page/10001/','/settings//'])assert.equal((await buildPage(ctx(path))).status,404,path); diff --git a/tests/playback-react.test.ts b/tests/playback-react.test.ts index 85a39dd..d40e5ca 100644 --- a/tests/playback-react.test.ts +++ b/tests/playback-react.test.ts @@ -6,11 +6,6 @@ import { shouldDecryptImage, } from "../src/lib/media/images"; import { isHLSURL, nativeHLS, resolvePlayURL } from "../src/lib/media/playback"; -import { trackingContext } from "../src/features/playback/model"; -import { - normalizeChannel, - normalizeUID, -} from "../src/components/analytics/tracker"; test("legacy image XOR is immutable, symmetric and changes only the first 100 bytes", () => { const plain = Uint8Array.from({ length: 150 }, (_, index) => index), @@ -75,24 +70,3 @@ test("native HLS only uses Safari and shell browsers keep the inline MSE player" ]) assert(!nativeHLS(video, "Mozilla iPhone Safari/604.1 " + browser)); }); -test("drama tracking and timestamps preserve upstream field meaning", () => { - assert.equal(trackingContext({ DramaID: 0 }), null); - const context = trackingContext({ - DramaID: 42, - VideoID: 101, - Episode: 2, - TotalEpisode: 8, - DramaTypeID: 3, - DramaTypeName: "都市", - DramaTagKey: "1,2", - DramaTagName: "剧情,原创", - }); - assert.equal(context?.drama_id, "42"); - assert.equal(context?.episode, 2); - assert.equal(context?.drama_content_type, "short_drama"); - assert.equal(context?.drama_tag_key, "1,2"); - assert.equal(normalizeChannel("official"), "organic"); - assert.equal(normalizeChannel("channel_A-2"), "channel_A-2"); - assert.equal(normalizeUID("stress_user"), ""); - assert.equal(normalizeUID("real_1001"), "real_1001"); -}); diff --git a/tests/proxy.test.ts b/tests/proxy.test.ts index 3fa5277..07b6947 100644 --- a/tests/proxy.test.ts +++ b/tests/proxy.test.ts @@ -44,13 +44,13 @@ after(async () => { await new Promise(resolve => server.close(() => resolv const request = (path: string, init: RequestInit = {}) => new Request('http://local.example' + path, init); const post = (path: string, body: unknown, headers: HeadersInit = {}) => request(path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...Object.fromEntries(new Headers(headers)) }, body: JSON.stringify(body) }); -test('SSR/upstream preserve client cookies, cookie-token priority, IP, custom UA and SDK headers', async () => { +test('SSR/upstream preserve client cookies, cookie-token priority, IP, and custom UA headers', async () => { scenario = ''; received.length = 0; - const ctx = createContext(request('/list/', { headers: { Cookie: 'token=cookie-token; extra=keep%20me', Authorization: 'header-token', 'User-Agent': 'browser-agent', 'X-User-Agent': 'client-UA', 'X-Real-IP': '203.0.113.12', 'X-SDK-Common': 'sdk-snapshot' } })); + const ctx = createContext(request('/list/', { headers: { Cookie: 'token=cookie-token; extra=keep%20me', Authorization: 'header-token', 'User-Agent': 'browser-agent', 'X-User-Agent': 'client-UA', 'X-Real-IP': '203.0.113.12'} })); await upstreamGet(ctx, '/api/app/config/info'); const headers = received[0].headers; assert.equal(headers.authorization, 'cookie-token'); assert.equal(headers.cookie, 'token=cookie-token; extra=keep%20me'); - assert.equal(headers['user-agent'], 'browser-agent'); assert.equal(headers['x-user-agent'], 'client-UA'); assert.equal(headers['x-real-ip'], '203.0.113.12'); assert.equal(headers['x-sdk-common'], 'sdk-snapshot'); + assert.equal(headers['user-agent'], 'browser-agent'); assert.equal(headers['x-user-agent'], 'client-UA'); assert.equal(headers['x-real-ip'], '203.0.113.12'); }); test('public data requests isolate both stale Authorization and auth cookies', async () => { scenario = ''; received.length = 0; @@ -67,14 +67,6 @@ test('hop-by-hop, Connection-nominated and internal headers never cross the BFF' const headers = buildUpstreamHeaders(ctx); for (const name of ['connection', 'x-internal', 'x-origin-head', 'x-tracker-request-id', 'x-dc-request-context']) assert.equal(headers.has(name), false); }); -test('canonical attribution snapshot forwards only on the original allowed action', () => { - const id = 'a'.repeat(32); - const envelope = { v: 1, request_id: id, event_name: 'drama_like', common: { app_id: 'JHA-4001', uid: '12345', channel: 'campaign', sid: 'session', client_ts: 1791550000, device: 'PC', device_id: 'device', device_brand: '', device_model: '', device_fingerprint: '', fp_version: '', user_agent: 'browser', sdk_version: '1.2.0', app_version: '1.0.0' }, drama: { media_id: '', drama_id: '123', episode: 1, video_id: '', recommend_trace_id: '' } }; - const headers = { 'X-Tracker-Request-Id': id, 'X-Tracker-Event-Name': 'drama_like', 'X-DC-Request-Context': Buffer.from(JSON.stringify(envelope)).toString('base64url') }; - const allowed = buildUpstreamHeaders(createContext(post('/api/playlet/like/set', { id: 123, isLike: true }, headers))); - assert.deepEqual(JSON.parse(Buffer.from(allowed.get('X-DC-Request-Context')!, 'base64url').toString()), envelope); - assert.equal(buildUpstreamHeaders(createContext(post('/api/video/like/set', {}, headers))).has('X-DC-Request-Context'), false); -}); test('Set-Cookie values remain separate and updated same-name cookies are written once', async () => { scenario = 'cookies'; received.length = 0; const ctx = createContext(request('/api/auth/login')); diff --git a/tests/react-rendering.test.tsx b/tests/react-rendering.test.tsx index b1e26a7..be38684 100644 --- a/tests/react-rendering.test.tsx +++ b/tests/react-rendering.test.tsx @@ -107,8 +107,8 @@ test("React tags and list pages preserve the original Go catalog content and nav name, ); assert.equal( - (actual.match(/data-track-ad="external"/g) || []).length, - (expected.match(/data-track-ad="external"/g) || []).length, + (actual.match(/rel="sponsored nofollow noopener"/g) || []).length, + (expected.match(/rel="sponsored nofollow noopener"/g) || []).length, name + " ad placement", ); assert.match(actual, /花魁 & 第一部/); diff --git a/tests/routes.test.ts b/tests/routes.test.ts index 5ec712d..978abee 100644 --- a/tests/routes.test.ts +++ b/tests/routes.test.ts @@ -6,7 +6,7 @@ import { canonicalRedirect } from "../src/server/redirects"; import { matchPageRoute } from "../src/server/pages"; import { pageRoutes, publicPageRoutes } from "../src/server/pages/routes"; import { preparePage } from "../src/server/react/page-data"; -import { trackerPage } from "../src/server/services/shared"; +import { pageIdentity } from "../src/server/services/shared"; test("Huakui route prefixes preserve encoded slugs, raw queries and fragments", () => { const suffix = "/%E7%9F%AD%E5%89%A7%2F%E9%9B%86%252F1/?x=a+b&x=%2b&token=a%2FB#episode%202"; @@ -262,7 +262,7 @@ test("navigation-only home road links use a configured main-site origin, then th }); -test("semantic page paths retain their original analytics classification", () => { +test("semantic page paths retain their original page classification", () => { const paths: [string, string, string][] = [ ["/list/romance/", "/dramas/romance/", "list"], ["/play/a%2Fb/123/", "/drama/a%2Fb/123/", "play"], @@ -279,8 +279,8 @@ test("semantic page paths retain their original analytics classification", () => ["/home-road/", "/return-home/", "home_road"], ]; for (const [legacy, current, key] of paths) { - assert.deepEqual(trackerPage(current), trackerPage(legacy), current); - assert.equal(trackerPage(current)[0], key, current); + assert.deepEqual(pageIdentity(current), pageIdentity(legacy), current); + assert.equal(pageIdentity(current)[0], key, current); } - assert.equal(trackerPage("/unknown/")[0], "internal_other"); + assert.equal(pageIdentity("/unknown/")[0], "internal_other"); }); diff --git a/tests/sdk-system.test.ts b/tests/sdk-system.test.ts deleted file mode 100644 index c0004c2..0000000 --- a/tests/sdk-system.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { runInNewContext } from "node:vm"; - -const sdkSource = readFileSync(new URL("../public/static/common/web-sdk-v1.2.0.js", import.meta.url), "utf8"); - -interface SDKContext { - navigator?: { userAgent: string; platform: string; maxTouchPoints: number }; - WebSDK?: { getCommonFields(fields?: string[]): Promise> }; -} - -function loadSDK(userAgent?: string, platform = "", maxTouchPoints = 0) { - const context: SDKContext = {}; - if (userAgent !== undefined) context.navigator = { userAgent, platform, maxTouchPoints }; - runInNewContext(sdkSource, context, { filename: "web-sdk-v1.2.0.js" }); - assert.ok(context.WebSDK); - return context.WebSDK; -} - -const userAgents = [ - { - name: "iPhone", - ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1", - platform: "iPhone", touch: 5, system: "iOS", version: "18.0", - }, - { - name: "iPad mobile mode", - ua: "Mozilla/5.0 (iPad; CPU OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 Version/17.6 Mobile/15E148 Safari/604.1", - platform: "iPad", touch: 5, system: "iOS", version: "17.6.1", - }, - { - name: "Android", - ua: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/129.0.0.0 Mobile Safari/537.36", - platform: "Linux armv8l", touch: 5, system: "Android", version: "14", - }, - { - name: "Windows NT", - ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/129.0.0.0 Safari/537.36", - platform: "Win32", touch: 0, system: "Windows", version: "10.0", - }, - { - name: "macOS", - ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_1) AppleWebKit/605.1.15 Version/17.6 Safari/605.1.15", - platform: "MacIntel", touch: 0, system: "macOS", version: "14.6.1", - }, - { - name: "iPad desktop mode without an exposed iOS version", - ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15", - platform: "MacIntel", touch: 5, system: "iOS", version: "", - }, - { - name: "Linux without an exposed kernel version", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/129.0.0.0 Safari/537.36", - platform: "Linux x86_64", touch: 0, system: "Linux", version: "", - }, - { - name: "unknown operating system", - ua: "ExampleBrowser/1.0", platform: "", touch: 0, system: "Other", version: "", - }, -]; - -for (const device of userAgents) { - test(`bundled SDK common fields report the operating system for ${device.name}`, async () => { - const sdk = loadSDK(device.ua, device.platform, device.touch); - const common = await sdk.getCommonFields(); - assert.equal(common.system_name, device.system); - assert.equal(common.system_version, device.version); - assert.equal(common.user_agent, device.ua); - const selected = await sdk.getCommonFields(["system_name", "system_version"]); - assert.deepEqual(JSON.parse(JSON.stringify(selected)), { - system_name: device.system, system_version: device.version, - }); - }); -} - -test("bundled SDK OS fields tolerate a missing navigator without inventing a version", async () => { - const common = await loadSDK().getCommonFields(); - assert.equal(common.system_name, "Other"); - assert.equal(common.system_version, ""); -});