diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/.gitignore b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/.gitignore new file mode 100644 index 000000000..804bd4a4a --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +*.local diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/.npmrc b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/.npmrc new file mode 100644 index 000000000..0ea8b5f01 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/.npmrc @@ -0,0 +1,6 @@ +# 独立项目:不加入 admin-web 的 pnpm workspace(见上级 pnpm-workspace.yaml 的排除项), +# 依赖和锁文件都在本目录。安装:pnpm install --ignore-workspace +registry=https://registry.npmmirror.com +fetch-timeout=600000 +fetch-retries=6 +network-concurrency=6 diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/README.md b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/README.md new file mode 100644 index 000000000..74f0426f1 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/README.md @@ -0,0 +1,54 @@ +# 企微客服助手 · 手机远程控制台(mobile-h5) + +给客服本人用的手机 / 平板 H5:用 ZYT 账号登录,查看自己电脑上客服软件的运行状态,远程启动 / +停止自动回复,管理会话黑名单。有 `wecom_rpa.remote/all` 权限的主管可以切换查看其他客服。 + +- 技术栈:Vue 3 + Vite + Vant 4,hash 路由,按需引入 Vant 样式。 +- 布局:手机单栏 + 底部标签栏;宽度 ≥ 768px(平板、电脑浏览器)切成左列表 / 右详情两栏。 +- 适配:iOS 安全区(`viewport-fit=cover`)、深色模式(跟随系统)、可"添加到主屏幕"(manifest + 不缓存的 Service Worker)。 +- 托管:构建产物由管理端 `wechat_rpa/admin_api.py` 挂在 `/m/`,接口是同源的 `/api/v2/mobile/*`。 + +## 独立项目 + +这个目录**不属于** admin-web 的 pnpm workspace(`pnpm-workspace.yaml` 里有 `!apps/mobile-h5`), +依赖和锁文件都在本目录,不会影响 vben 管理端的安装和构建。 + +```bash +cd admin-web/apps/mobile-h5 +pnpm install --ignore-workspace # 走本目录 .npmrc 里的 npmmirror +pnpm dev # http://127.0.0.1:5190/m/ ,/api 代理到 127.0.0.1:8766(MOBILE_API_TARGET 可改) +pnpm test # vitest:时间格式、设备 / 指令状态文案 +pnpm build # vue-tsc 类型检查 + vite build → dist/ +``` + +发布时把 `dist/` 放到服务器 `/opt/im-admin/admin-web/apps/mobile-h5/dist/`(见 +`deploy/mobile-remote-20260928/`),`deploy/im-admin/Dockerfile` 会把它一起打进镜像。 + +## 页面 + +| 路由 | 内容 | +|---|---| +| `#/login` | ZYT 账号密码登录(后台代登 ZYT 终端 8,密码不落地);公网明文 HTTP 下提示改用 HTTPS | +| `#/desktops` | 电脑卡片:在线 / 离线、监听中 / 已停止、企业微信连接、回复通道、待处理数;启动 / 停止自动回复(需确认),下发后每 3 秒查询一次指令结果 | +| `#/conversations` | 会话列表(来自聊天归档,只有名称、类型、最近时间,不含聊天正文)+ 搜索 + 黑名单页签;详情里加入 / 移出黑名单 | +| `#/me` | 当前账号、五项手机端权限是否开通、登录信息、添加到主屏幕说明、退出登录 | + +电脑离线(客服软件没开)时只显示"电脑离线",无法远程拉起;电脑上是旧版客服软件(没有命令通道)时显示 +"版本过低",按钮不可用。 + +## 接口(`/api/v2/mobile`,Bearer 令牌) + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/auth/login` | `{account, password, client_name}` → `{access_token, expires_at, me}` | +| POST | `/auth/logout` | 同时注销 ZYT 终端 8 会话 | +| GET | `/me` | 账号、权限(control / control_all / blacklist_add / blacklist_remove / root)、会话有效期 | +| GET | `/accounts?q=` | 全部客服电脑概况,仅 `wecom_rpa.remote/all` | +| GET | `/desktops?account_id=` | 名下电脑与实时状态;`account_id` 仅 remote/all 可指定别人 | +| POST | `/desktops/{device_id}/commands?account_id=` | `{type: start_listening \| stop_listening}`,离线 409 `device_offline`,旧版 409 `client_outdated` | +| GET | `/commands/{id}` | 指令状态:pending → delivered → succeeded / failed / expired / superseded | +| GET | `/conversations?q=&scope=all\|blocked&cursor=&limit=` | 会话与黑名单状态 | +| PUT | `/blacklist/{account}/{conversation}?account_id=` | `{name, account_name, reason}`,需 `wecom_rpa.blacklist/add` | +| DELETE | `/blacklist/{account}/{conversation}?account_id=` | 需 `wecom_rpa.blacklist/remove` | + +错误体统一是 `{"detail": {"code": "...", "message": "给人看的中文"}}`,401 时前端清掉令牌回登录页。 diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/index.html b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/index.html new file mode 100644 index 000000000..2187497e7 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + +
+ + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/package.json b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/package.json new file mode 100644 index 000000000..5e3e2686d --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/package.json @@ -0,0 +1,27 @@ +{ + "name": "@wecom/mobile-h5", + "version": "1.0.0", + "private": true, + "description": "企微客服助手 · 手机远程控制台(托管在管理端 /m/)", + "type": "module", + "packageManager": "pnpm@11.16.0", + "scripts": { + "dev": "vite --mode development", + "build": "vue-tsc --noEmit && vite build --mode production", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "vant": "4.10.2", + "vue": "3.5.41", + "vue-router": "5.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "6.0.8", + "typescript": "6.0.3", + "vite": "8.2.1", + "vitest": "4.1.10", + "vue-tsc": "3.3.9" + } +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/pnpm-lock.yaml b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/pnpm-lock.yaml new file mode 100644 index 000000000..cc6594c99 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/pnpm-lock.yaml @@ -0,0 +1,1439 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + vant: + specifier: 4.10.2 + version: 4.10.2(vue@3.5.41(typescript@6.0.3)) + vue: + specifier: 3.5.41 + version: 3.5.41(typescript@6.0.3) + vue-router: + specifier: 5.2.0 + version: 5.2.0(@vue/compiler-sfc@3.5.43)(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: 6.0.8 + version: 6.0.8(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3)) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: 8.2.1 + version: 8.2.1(yaml@2.9.1) + vitest: + specifier: 4.1.10 + version: 4.1.10(vite@8.2.1(yaml@2.9.1)) + vue-tsc: + specifier: 3.3.9 + version: 3.3.9(typescript@6.0.3) + +packages: + + '@babel/generator@8.0.6': + resolution: {integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@8.0.6': + resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.6': + resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.6': + resolution: {integrity: sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.6': + resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/gen-mapping@0.4.0-beta.0': + resolution: {integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.151.0': + resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + + '@rolldown/binding-android-arm-eabi@1.2.11': + resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.11': + resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.11': + resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.11': + resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.11': + resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.11': + resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.11': + resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.11': + resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.11': + resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.11': + resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@vant/popperjs@1.3.0': + resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==} + + '@vant/use@1.6.0': + resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==} + peerDependencies: + vue: ^3.0.0 + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue-macros/common@3.1.4': + resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} + + '@vue/compiler-core@3.5.43': + resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} + + '@vue/compiler-dom@3.5.43': + resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} + + '@vue/compiler-sfc@3.5.43': + resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + + '@vue/compiler-ssr@3.5.43': + resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==} + + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} + + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} + + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} + + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + + '@vue/shared@3.5.43': + resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@2.1.2: + resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==} + engines: {node: '>=20.18.0'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.9.0: + resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} + engines: {node: '>=20.19.0'} + + birpc@2.6.1: + resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} + engines: {node: '>=14'} + + magic-string-ast@1.0.2: + resolution: {integrity: sha512-8ngQgLhcT0t3YBdn9CGkZqCYlvwW9pm7aWJwd7AxseVWf1RU8ZHCQvG1mt3N5vvUme+pXTcHB8G/7fE666U8Vw==} + engines: {node: '>=20.18.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nostics@1.3.0: + resolution: {integrity: sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg==} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + rolldown@1.2.11: + resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + unplugin-utils@0.3.2: + resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} + engines: {node: '>=20.19.0'} + + unplugin@3.4.0: + resolution: {integrity: sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rsbuild/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rsbuild/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + + vant@4.10.2: + resolution: {integrity: sha512-7lm1KtFub11ozleFqQ6rL8oa3Z8i33G3IUEuJGWBXlQwPTRfu8BYWGGZ/GhA5QdWhERp5mtBq6TjpYEmLcwwow==} + peerDependencies: + vue: ^3.0.0 + + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + vue-router@5.2.0: + resolution: {integrity: sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.34 || ^4.0.0 + pinia: ^3.0.4 || ^4.0.2 + vite: ^7.3.0 || ^8.0.0 + vue: ^3.5.34 || ^4.0.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + vite: + optional: true + + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@babel/generator@8.0.6': + dependencies: + '@babel/parser': 8.0.6 + '@babel/types': 8.0.6 + '@jridgewell/gen-mapping': 0.4.0-beta.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-string-parser@8.0.6': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-identifier@8.0.6': {} + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/parser@8.0.6': + dependencies: + '@babel/types': 8.0.6 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@babel/types@8.0.6': + dependencies: + '@babel/helper-string-parser': 8.0.6 + '@babel/helper-validator-identifier': 8.0.6 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.4.0-beta.0': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.151.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.11': + optional: true + + '@rolldown/binding-android-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-x64@1.2.11': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.11': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.11': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.11': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.11': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.11': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/jsesc@2.5.1': {} + + '@vant/popperjs@1.3.0': {} + + '@vant/use@1.6.0(vue@3.5.41(typescript@6.0.3))': + dependencies: + vue: 3.5.41(typescript@6.0.3) + + '@vitejs/plugin-vue@6.0.8(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.1(yaml@2.9.1) + vue: 3.5.41(typescript@6.0.3) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.1(yaml@2.9.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.1(yaml@2.9.1) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.2.0 + + '@vue-macros/common@3.1.4(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@vue/compiler-sfc': 3.5.43 + ast-kit: 2.1.2 + local-pkg: 1.2.1 + magic-string-ast: 1.0.2 + unplugin-utils: 0.3.2 + optionalDependencies: + vue: 3.5.41(typescript@6.0.3) + + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.43 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-dom@3.5.43': + dependencies: + '@vue/compiler-core': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-sfc@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.43 + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-ssr': 3.5.43 + '@vue/shared': 3.5.43 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-ssr@3.5.43': + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/devtools-api@8.2.1': + dependencies: + '@vue/devtools-kit': 8.2.1 + + '@vue/devtools-kit@8.2.1': + dependencies: + '@vue/devtools-shared': 8.2.1 + birpc: 2.6.1 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.2.1': {} + + '@vue/language-core@3.3.9': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.7 + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + + '@vue/shared@3.5.43': {} + + acorn@8.18.0: {} + + alien-signals@3.2.1: {} + + assertion-error@2.0.1: {} + + ast-kit@2.1.2: + dependencies: + '@babel/parser': 7.29.9 + pathe: 2.0.3 + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.9 + pathe: 2.0.3 + + ast-walker-scope@0.9.0: + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + ast-kit: 2.2.0 + + birpc@2.6.1: {} + + chai@6.2.2: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + confbox@0.1.8: {} + + confbox@0.3.1: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + entities@7.0.1: {} + + es-module-lexer@2.3.2: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + exsolve@1.1.1: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + hookable@5.5.3: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + local-pkg@1.2.1: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.3 + quansync: 0.2.11 + + magic-string-ast@1.0.2: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + muggle-string@0.4.1: {} + + nanoid@3.3.19: {} + + nostics@1.3.0: {} + + obug@2.2.1: {} + + path-browserify@1.0.1: {} + + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.3: + dependencies: + confbox: 0.3.1 + exsolve: 1.1.1 + pathe: 2.0.3 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + quansync@0.2.11: {} + + readdirp@5.1.1: {} + + rolldown@1.2.11: + dependencies: + '@oxc-project/types': 0.151.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.11 + '@rolldown/binding-android-arm64': 1.2.11 + '@rolldown/binding-darwin-arm64': 1.2.11 + '@rolldown/binding-darwin-x64': 1.2.11 + '@rolldown/binding-freebsd-x64': 1.2.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 + '@rolldown/binding-linux-arm64-gnu': 1.2.11 + '@rolldown/binding-linux-arm64-musl': 1.2.11 + '@rolldown/binding-linux-ppc64-gnu': 1.2.11 + '@rolldown/binding-linux-s390x-gnu': 1.2.11 + '@rolldown/binding-linux-x64-gnu': 1.2.11 + '@rolldown/binding-linux-x64-musl': 1.2.11 + '@rolldown/binding-openharmony-arm64': 1.2.11 + '@rolldown/binding-win32-arm64-msvc': 1.2.11 + '@rolldown/binding-win32-x64-msvc': 1.2.11 + + scule@1.3.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + unplugin-utils@0.3.2: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.7 + + unplugin@3.4.0(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + rolldown: 1.2.11 + vite: 8.2.1(yaml@2.9.1) + + vant@4.10.2(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vant/popperjs': 1.3.0 + '@vant/use': 1.6.0(vue@3.5.41(typescript@6.0.3)) + '@vue/shared': 3.5.43 + vue: 3.5.41(typescript@6.0.3) + + vite@8.2.1(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.11 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + yaml: 2.9.1 + + vitest@4.1.10(vite@8.2.1(yaml@2.9.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.1(yaml@2.9.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.1(yaml@2.9.1) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - msw + + vscode-uri@3.2.0: {} + + vue-router@5.2.0(@vue/compiler-sfc@3.5.43)(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@babel/generator': 8.0.6 + '@vue-macros/common': 3.1.4(vue@3.5.41(typescript@6.0.3)) + '@vue/devtools-api': 8.2.1 + ast-walker-scope: 0.9.0 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.2.1 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + nostics: 1.3.0 + pathe: 2.0.3 + picomatch: 4.0.7 + scule: 1.3.0 + tinyglobby: 0.2.17 + unplugin: 3.4.0(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1)) + unplugin-utils: 0.3.2 + vue: 3.5.41(typescript@6.0.3) + yaml: 2.9.1 + optionalDependencies: + '@vue/compiler-sfc': 3.5.43 + vite: 8.2.1(yaml@2.9.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - webpack + + vue-tsc@3.3.9(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + typescript: 6.0.3 + + vue@3.5.41(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + optionalDependencies: + typescript: 6.0.3 + + webpack-virtual-modules@0.6.2: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + yaml@2.9.1: {} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/apple-touch-icon.png b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/apple-touch-icon.png differ diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/favicon-64.png b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/favicon-64.png differ diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-192.png b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-192.png differ diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-512.png b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-512.png differ diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-maskable-512.png b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/icons/icon-maskable-512.png differ diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/manifest.webmanifest b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/manifest.webmanifest new file mode 100644 index 000000000..bc53828cc --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话忽略。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/sw.js b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/public/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/App.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/App.vue new file mode 100644 index 000000000..6bc117b42 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/App.vue @@ -0,0 +1,47 @@ + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/api.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/api.ts new file mode 100644 index 000000000..602943255 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/api.ts @@ -0,0 +1,320 @@ +// 手机控制台只调用 /api/v2/mobile/*。令牌放 localStorage,登出或失效即清除; +// 不保存密码。 +const API_BASE = '/api/v2/mobile'; +const TOKEN_KEY = 'wecom-mobile-token'; + +export interface Permissions { + control: boolean; + control_all: boolean; + blacklist_add: boolean; + blacklist_remove: boolean; + root: boolean; + known: boolean; +} + +export interface Me { + account: { id: number; nickname: string; sn: string; avatar: string }; + permissions: Permissions; + permission_codes: Record, string>; + session: { created_at: string; expires_at: number }; +} + +export interface DeviceStatus { + listening: boolean; + state: string; + state_label: string; + transport: string; + transport_label: string; + wecom_connected: boolean; + pending_count: number; + reported_at: string; + reported_seconds: number | null; + fresh: boolean; +} + +export type CommandType = 'start_listening' | 'stop_listening'; +export type CommandStatus = + | 'pending' + | 'delivered' + | 'succeeded' + | 'failed' + | 'expired' + | 'superseded'; + +export interface Command { + id: string; + type: CommandType; + type_label: string; + device_id: string; + status: CommandStatus; + status_label: string; + final: boolean; + created_at: string; + created_by: string; + delivered_at: string; + acked_at: string; + expires_at: number; + result: { message?: string; listening?: boolean; [key: string]: unknown }; + error: string; +} + +export interface Device { + device_id: string; + device_name: string; + app_version: string; + online: boolean; + session_active: boolean; + last_seen_at: string; + last_seen_seconds: number | null; + remote_capable: boolean; + blacklist_sync: boolean; + status: DeviceStatus | null; + last_command: Command | null; +} + +export interface DesktopsResponse { + account: { id: number; nickname: string; sn: string }; + items: Device[]; + server_time: number; +} + +export interface AccountSummary { + id: number; + nickname: string; + sn: string; + device_count: number; + online: boolean; + listening: boolean; + last_online_at: string; +} + +export interface BlacklistEntry { + account: string; + account_name: string; + conversation: string; + name: string; + reason: string; + source: string; + created_by: string; + created_at: string; +} + +export interface Conversation { + account: string; + account_name: string; + conversation: string; + name: string; + type: string; + type_label: string; + last_message_at: string; + blocked: boolean; + blacklist: BlacklistEntry | null; +} + +export interface ConversationPage { + items: Conversation[]; + next_cursor: string; + has_more: boolean; + blocked_total: number; +} + +export class ApiError extends Error { + readonly status: number; + readonly code: string; + readonly retryAfter: number; + + constructor(status: number, code: string, message: string, retryAfter = 0) { + super(message); + this.status = status; + this.code = code; + this.retryAfter = retryAfter; + } +} + +let unauthorizedHandler: (() => void) | null = null; + +/** 任意接口返回 401(令牌失效 / 被管理端下线)时回调,由路由层跳回登录页。 */ +export function onUnauthorized(handler: () => void): void { + unauthorizedHandler = handler; +} + +export function readToken(): string { + try { + return window.localStorage.getItem(TOKEN_KEY) || ''; + } catch { + return ''; + } +} + +export function writeToken(token: string): void { + try { + if (token) window.localStorage.setItem(TOKEN_KEY, token); + else window.localStorage.removeItem(TOKEN_KEY); + } catch { + // 隐私模式下 localStorage 可能不可写:本次打开期间仍可用,刷新后需要重新登录。 + } +} + +let memoryToken = ''; + +export function currentToken(): string { + return memoryToken || readToken(); +} + +export function setToken(token: string): void { + memoryToken = token; + writeToken(token); +} + +/** 把后端各种形状的错误体(字符串、{code,message}、422 校验数组)归一成一句话。 */ +export function errorFromBody(status: number, body: unknown): ApiError { + const detail = (body && typeof body === 'object' && 'detail' in body + ? (body as { detail: unknown }).detail + : body) as unknown; + if (detail && typeof detail === 'object' && !Array.isArray(detail)) { + const value = detail as { code?: unknown; message?: unknown; retry_after?: unknown }; + return new ApiError( + status, + String(value.code || `http_${status}`), + String(value.message || defaultMessage(status)), + Number(value.retry_after || 0), + ); + } + if (Array.isArray(detail)) { + return new ApiError(status, 'validation', '填写的内容不符合要求,请检查后重试'); + } + if (typeof detail === 'string' && detail.trim()) { + return new ApiError(status, `http_${status}`, detail.trim()); + } + return new ApiError(status, `http_${status}`, defaultMessage(status)); +} + +function defaultMessage(status: number): string { + if (status === 401) return '登录已失效,请重新登录'; + if (status === 403) return '没有权限执行这个操作'; + if (status === 404) return '内容不存在或已被删除'; + if (status === 429) return '操作太频繁,请稍后再试'; + if (status >= 500) return '服务暂时不可用,请稍后重试'; + return `请求失败(HTTP ${status})`; +} + +interface RequestOptions { + body?: unknown; + query?: Record; + auth?: boolean; + signal?: AbortSignal; +} + +async function request(method: string, path: string, options: RequestOptions = {}): Promise { + const { body, query, auth = true, signal } = options; + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query || {})) { + if (value !== undefined && value !== null && value !== '') params.set(key, String(value)); + } + const url = `${API_BASE}${path}${params.toString() ? `?${params}` : ''}`; + const headers: Record = { Accept: 'application/json' }; + if (body !== undefined) headers['Content-Type'] = 'application/json'; + const token = currentToken(); + if (auth && token) headers.Authorization = `Bearer ${token}`; + let response: Response; + try { + response = await fetch(url, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + signal, + cache: 'no-store', + credentials: 'omit', + }); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') throw error; + throw new ApiError(0, 'network', '网络连接失败,请检查网络后重试'); + } + let data: unknown = null; + const text = await response.text(); + if (text) { + try { + data = JSON.parse(text); + } catch { + data = null; + } + } + if (!response.ok) { + const error = errorFromBody(response.status, data); + if (response.status === 401 && auth) unauthorizedHandler?.(); + throw error; + } + return data as T; +} + +const segment = (value: string) => encodeURIComponent(value); + +export const api = { + login(account: string, password: string, clientName: string) { + return request<{ access_token: string; expires_at: number; me: Me }>('POST', '/auth/login', { + auth: false, + body: { account, password, client_name: clientName }, + }); + }, + logout() { + return request<{ ok: boolean }>('POST', '/auth/logout'); + }, + me() { + return request('GET', '/me'); + }, + accounts(q = '') { + return request<{ items: AccountSummary[] }>('GET', '/accounts', { query: { q } }); + }, + desktops(accountId?: number) { + return request('GET', '/desktops', { query: { account_id: accountId } }); + }, + sendCommand(deviceId: string, type: CommandType, accountId?: number) { + return request<{ command: Command }>('POST', `/desktops/${segment(deviceId)}/commands`, { + body: { type }, + query: { account_id: accountId }, + }); + }, + command(commandId: string) { + return request<{ command: Command }>('GET', `/commands/${segment(commandId)}`); + }, + conversations(options: { + accountId?: number; + q?: string; + scope?: 'all' | 'blocked'; + cursor?: string; + limit?: number; + signal?: AbortSignal; + }) { + return request('GET', '/conversations', { + query: { + account_id: options.accountId, + q: options.q, + scope: options.scope, + cursor: options.cursor, + limit: options.limit, + }, + signal: options.signal, + }); + }, + block( + target: { account: string; conversation: string; name?: string; account_name?: string }, + reason: string, + accountId?: number, + ) { + return request<{ entry: BlacklistEntry; version: number }>( + 'PUT', + `/blacklist/${segment(target.account)}/${segment(target.conversation)}`, + { + body: { name: target.name || '', account_name: target.account_name || '', reason }, + query: { account_id: accountId }, + }, + ); + }, + unblock(account: string, conversation: string, accountId?: number) { + return request<{ ok: boolean; version: number }>( + 'DELETE', + `/blacklist/${segment(account)}/${segment(conversation)}`, + { query: { account_id: accountId } }, + ); + }, +}; diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/brand-mark.png b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/brand-mark.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/brand-mark.png differ diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/AccountSwitcher.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/AccountSwitcher.vue new file mode 100644 index 000000000..9a1c2acca --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/AccountSwitcher.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/ConversationDetail.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/ConversationDetail.vue new file mode 100644 index 000000000..e9f52050c --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/ConversationDetail.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/ConversationRow.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/ConversationRow.vue new file mode 100644 index 000000000..9ac12c709 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/ConversationRow.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/DevicePanel.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/DevicePanel.vue new file mode 100644 index 000000000..f00103240 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/components/DevicePanel.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/composables.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/composables.ts new file mode 100644 index 000000000..1deace3e5 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/composables.ts @@ -0,0 +1,74 @@ +import { onActivated, onBeforeUnmount, onDeactivated, onMounted, ref } from 'vue'; + +/** ≥768px(平板横竖屏、桌面浏览器)切成左右两栏。 */ +export function useWide(query = '(min-width: 768px)') { + const media = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia(query) : null; + const wide = ref(media ? media.matches : false); + const update = (event: MediaQueryListEvent) => { + wide.value = event.matches; + }; + onMounted(() => media?.addEventListener?.('change', update)); + onBeforeUnmount(() => media?.removeEventListener?.('change', update)); + return wide; +} + +export function usePrefersDark() { + const media = + typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; + const dark = ref(media ? media.matches : false); + const update = (event: MediaQueryListEvent) => { + dark.value = event.matches; + }; + onMounted(() => media?.addEventListener?.('change', update)); + onBeforeUnmount(() => media?.removeEventListener?.('change', update)); + return dark; +} + +/** + * 页面可见时按间隔刷新;切到后台就停,回到前台立刻补一次。 + * 手机锁屏后还在轮询只会白白耗电和流量。被 KeepAlive 缓存的页面切走时也停。 + */ +export function useVisibleInterval(task: () => unknown, intervalMs: number) { + let timer: number | undefined; + let active = false; + const stop = () => { + if (timer !== undefined) window.clearInterval(timer); + timer = undefined; + }; + const start = () => { + stop(); + timer = window.setInterval(() => { + if (document.visibilityState === 'visible') void task(); + }, intervalMs); + }; + const onVisibility = () => { + if (!active) return; + if (document.visibilityState === 'visible') { + void task(); + start(); + } else { + stop(); + } + }; + onMounted(() => { + active = true; + start(); + document.addEventListener('visibilitychange', onVisibility); + }); + onActivated(() => { + // 首次挂载也会触发 activated;那一次 onMounted 已经启动过了。 + if (active) return; + active = true; + void task(); + start(); + }); + onDeactivated(() => { + active = false; + stop(); + }); + onBeforeUnmount(() => { + active = false; + stop(); + document.removeEventListener('visibilitychange', onVisibility); + }); +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/format.test.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/format.test.ts new file mode 100644 index 000000000..877a348c4 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/format.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; + +import type { Command, Device } from './api'; +import { describeCommand, describeDevice, durationText, initial, relativeTime } from './format'; + +const now = new Date(2026, 8, 28, 15, 30, 0); + +function device(overrides: Partial = {}): Device { + return { + device_id: 'dev-1', + device_name: 'PC', + app_version: '1.5.0', + online: true, + session_active: true, + last_seen_at: '', + last_seen_seconds: 3, + remote_capable: true, + blacklist_sync: true, + status: { + listening: true, + state: 'running', + state_label: '监听中', + transport: 'visual', + transport_label: '视觉通道', + wecom_connected: true, + pending_count: 0, + reported_at: '', + reported_seconds: 4, + fresh: true, + }, + last_command: null, + ...overrides, + }; +} + +describe('relativeTime', () => { + it('uses friendly buckets', () => { + expect(relativeTime(new Date(2026, 8, 28, 15, 29, 40).toISOString(), now)).toBe('刚刚'); + expect(relativeTime(new Date(2026, 8, 28, 15, 10, 0).toISOString(), now)).toBe('20 分钟前'); + expect(relativeTime(new Date(2026, 8, 28, 9, 5, 0).toISOString(), now)).toBe('今天 09:05'); + expect(relativeTime(new Date(2026, 8, 27, 21, 0, 0).toISOString(), now)).toBe('昨天 21:00'); + expect(relativeTime(new Date(2026, 1, 3, 8, 0, 0).toISOString(), now)).toBe('02-03 08:00'); + expect(relativeTime(new Date(2025, 11, 31, 8, 0, 0).toISOString(), now)).toBe('2025-12-31'); + }); + + it('accepts backend offsets and tolerates junk', () => { + expect(relativeTime('2026-09-28T15:29:30+08:00', new Date('2026-09-28T15:30:00+08:00'))).toBe('刚刚'); + expect(relativeTime('', now)).toBe('—'); + expect(relativeTime('not a date', now)).toBe('—'); + }); +}); + +describe('durationText', () => { + it('rounds to the largest sensible unit', () => { + expect(durationText(12)).toBe('12 秒'); + expect(durationText(150)).toBe('3 分钟'); + expect(durationText(7200)).toBe('2 小时'); + expect(durationText(3 * 86400)).toBe('3 天'); + expect(durationText(null)).toBe('—'); + }); +}); + +describe('describeDevice', () => { + it('prioritises offline, then outdated, then status', () => { + expect(describeDevice(device({ online: false, last_seen_seconds: 600 })).label).toBe('离线'); + expect(describeDevice(device({ remote_capable: false })).label).toBe('版本过低'); + expect(describeDevice(device()).tone).toBe('success'); + const stopped = device(); + stopped.status = { ...stopped.status!, listening: false, state: 'stopped', state_label: '已停止' }; + expect(describeDevice(stopped)).toMatchObject({ tone: 'muted', label: '已停止' }); + const failed = device(); + failed.status = { ...failed.status!, state: 'error', state_label: '连接失败' }; + expect(describeDevice(failed).tone).toBe('danger'); + }); +}); + +describe('describeCommand', () => { + const base: Command = { + id: 'c1', + type: 'stop_listening', + type_label: '停止监听', + device_id: 'dev-1', + status: 'pending', + status_label: '', + final: false, + created_at: '', + created_by: '', + delivered_at: '', + acked_at: '', + expires_at: 0, + result: {}, + error: '', + }; + + it('describes progress and outcomes', () => { + expect(describeCommand(base)).toMatchObject({ busy: true, tone: 'primary' }); + expect(describeCommand({ ...base, status: 'succeeded', result: { message: '监听已停止' } }).text).toBe('监听已停止'); + expect(describeCommand({ ...base, status: 'failed', error: '企业微信未登录' }).text).toContain('企业微信未登录'); + expect(describeCommand({ ...base, status: 'expired' }).tone).toBe('warning'); + }); +}); + +describe('initial', () => { + it('takes the first visible character', () => { + expect(initial('张三')).toBe('张'); + expect(initial('alice')).toBe('A'); + expect(initial(' ')).toBe('?'); + expect(initial('😀客户')).toBe('😀'); + }); +}); diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/format.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/format.ts new file mode 100644 index 000000000..ee4ad3b50 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/format.ts @@ -0,0 +1,107 @@ +import type { Command, Device } from './api'; + +const pad = (value: number) => String(value).padStart(2, '0'); + +/** 解析后端的 ISO 时间(本地时区偏移或 UTC 都有),失败返回 null。 */ +export function parseTime(value: string | null | undefined): Date | null { + const text = String(value || '').trim(); + if (!text) return null; + const date = new Date(text); + return Number.isNaN(date.getTime()) ? null : date; +} + +/** 刚刚 / N 分钟前 / 今天 09:30 / 昨天 21:05 / 09-18 14:00 / 2025-12-31 */ +export function relativeTime(value: string | null | undefined, now: Date = new Date()): string { + const date = parseTime(value); + if (!date) return '—'; + const seconds = Math.round((now.getTime() - date.getTime()) / 1000); + if (seconds < 45 && seconds > -60) return '刚刚'; + if (seconds >= 45 && seconds < 3600) return `${Math.max(1, Math.round(seconds / 60))} 分钟前`; + const hm = `${pad(date.getHours())}:${pad(date.getMinutes())}`; + const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); + const day = 24 * 3600 * 1000; + if (date.getTime() >= startOfToday && date.getTime() < startOfToday + day) return `今天 ${hm}`; + if (date.getTime() >= startOfToday - day && date.getTime() < startOfToday) return `昨天 ${hm}`; + if (date.getFullYear() === now.getFullYear()) { + return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${hm}`; + } + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; +} + +/** 秒数 → "3 秒 / 2 分钟 / 5 小时 / 3 天" */ +export function durationText(seconds: number | null | undefined): string { + if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) return '—'; + const value = Math.max(0, Math.round(seconds)); + if (value < 60) return `${value} 秒`; + if (value < 3600) return `${Math.round(value / 60)} 分钟`; + if (value < 86400) return `${Math.round(value / 3600)} 小时`; + return `${Math.round(value / 86400)} 天`; +} + +export type Tone = 'success' | 'warning' | 'danger' | 'muted' | 'primary'; + +export interface DeviceSummary { + tone: Tone; + label: string; + hint: string; +} + +/** 设备卡片最上面那一行:一眼看清在不在线、有没有在自动回复。 */ +export function describeDevice(device: Device): DeviceSummary { + if (!device.online) { + const since = device.last_seen_seconds; + return { + tone: 'muted', + label: '离线', + hint: since === null ? '客服软件没有运行' : `客服软件没有运行,${durationText(since)}前在线`, + }; + } + if (!device.remote_capable) { + return { tone: 'warning', label: '版本过低', hint: '电脑上的客服软件需要升级后才能远程控制' }; + } + const status = device.status; + if (!status) return { tone: 'muted', label: '状态未知', hint: '等待电脑上报状态' }; + if (status.state === 'error') return { tone: 'danger', label: status.state_label, hint: '请到电脑上查看运行日志' }; + if (status.listening) { + if (status.state === 'running') { + return { + tone: 'success', + label: '监听中', + hint: status.wecom_connected ? '正在自动处理新消息' : '监听已开启,企业微信尚未连接', + }; + } + return { tone: 'warning', label: status.state_label, hint: '监听已开启,正在准备' }; + } + if (status.state === 'stopping') return { tone: 'warning', label: '正在停止', hint: '等待后台任务退出' }; + return { tone: 'muted', label: '已停止', hint: '不会自动回复新消息' }; +} + +/** 下发指令后的进度文字。 */ +export function describeCommand(command: Command): { tone: Tone; text: string; busy: boolean } { + const verb = command.type_label || '指令'; + switch (command.status) { + case 'pending': + return { tone: 'primary', text: `「${verb}」已下发,等待电脑执行…`, busy: true }; + case 'delivered': + return { tone: 'primary', text: `电脑已收到「${verb}」,正在执行…`, busy: true }; + case 'succeeded': { + const message = typeof command.result?.message === 'string' ? command.result.message : ''; + return { tone: 'success', text: message || `「${verb}」已执行`, busy: false }; + } + case 'failed': + return { tone: 'danger', text: `「${verb}」执行失败:${command.error || '电脑没有给出原因'}`, busy: false }; + case 'expired': + return { tone: 'warning', text: `「${verb}」没有在 10 分钟内得到电脑确认,已作废`, busy: false }; + case 'superseded': + return { tone: 'muted', text: `「${verb}」已被新的指令取代`, busy: false }; + default: + return { tone: 'muted', text: command.status_label || verb, busy: false }; + } +} + +/** 头像圆里显示的那个字:会话名的第一个字符。 */ +export function initial(name: string): string { + const text = String(name || '').trim(); + if (!text) return '?'; + return Array.from(text)[0]!.toUpperCase(); +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/main.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/main.ts new file mode 100644 index 000000000..001e5b9e4 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/main.ts @@ -0,0 +1,17 @@ +import { createApp } from 'vue'; + +import App from './App.vue'; +import { router } from './router'; +import './vant'; +import './styles.css'; + +createApp(App).use(router).mount('#app'); + +// 只为"添加到主屏幕"注册,不做离线缓存;明文 HTTP 下浏览器不允许注册,直接跳过。 +if (import.meta.env.PROD && 'serviceWorker' in navigator && window.isSecureContext) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register(`${import.meta.env.BASE_URL}sw.js`, { scope: import.meta.env.BASE_URL }) + .catch(() => undefined); + }); +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/router.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/router.ts new file mode 100644 index 000000000..654e41e17 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/router.ts @@ -0,0 +1,83 @@ +import { createRouter, createWebHashHistory } from 'vue-router'; + +import { ApiError, onUnauthorized } from './api'; +import { clearSession, loadMe, session } from './session'; + +// hash 路由:刷新任何页面都落在 /m/ 这一个入口上,后台不用为前端路由做重写。 +export const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/login', + name: 'login', + component: () => import('./views/LoginView.vue'), + meta: { public: true }, + }, + { + path: '/', + component: () => import('./views/ShellView.vue'), + children: [ + { path: '', redirect: { name: 'desktops' } }, + { path: 'desktops', name: 'desktops', component: () => import('./views/DevicesView.vue') }, + { + path: 'conversations', + name: 'conversations', + component: () => import('./views/ConversationsView.vue'), + }, + { path: 'me', name: 'me', component: () => import('./views/MeView.vue') }, + ], + }, + { path: '/:pathMatch(.*)*', redirect: { name: 'desktops' } }, + ], +}); + +router.beforeEach(async (to) => { + if (to.meta.public) return true; + if (!session.token) return { name: 'login', query: { redirect: to.fullPath } }; + if (!session.me) { + try { + await loadMe(); + } catch (error) { + if (error instanceof ApiError && error.status === 401) { + clearSession(); + return { name: 'login', query: { redirect: to.fullPath } }; + } + // 网络问题:放行,由页面自己展示错误并提供重试。 + } + } + return true; +}); + +onUnauthorized(() => { + clearSession(); + const current = router.currentRoute.value; + // 首次导航还没完成(路由守卫里 loadMe 得到 401):由守卫带着目标地址跳登录, + // 这里再跳一次会把 redirect 覆盖成起始的 "/"。 + if (!current.matched.length) return; + if (current.name !== 'login') { + void router.replace({ name: 'login', query: { redirect: current.fullPath, expired: '1' } }); + } +}); + +// 发版后,还开着的旧页面按旧文件名去拉页面代码会 404。整页刷新一次就能拿到新版本; +// 10 秒内只刷新一次,服务端真出问题时不会无限刷新。 +const CHUNK_RELOAD_KEY = 'wecom-mobile-chunk-reload'; +router.onError((error, to) => { + const message = String((error as Error)?.message || error); + const staleChunk = + /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test( + message, + ); + if (!staleChunk) return; + let last = 0; + try { + last = Number(window.sessionStorage.getItem(CHUNK_RELOAD_KEY) || 0); + window.sessionStorage.setItem(CHUNK_RELOAD_KEY, String(Date.now())); + } catch { + // sessionStorage 不可用时照样刷新一次 + } + if (Date.now() - last > 10_000) { + window.location.assign(`${import.meta.env.BASE_URL}#${to.fullPath}`); + window.location.reload(); + } +}); diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/session.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/session.ts new file mode 100644 index 000000000..cbdcc3015 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/session.ts @@ -0,0 +1,85 @@ +import { computed, reactive } from 'vue'; + +import { type AccountSummary, api, currentToken, type Me, setToken } from './api'; + +const VIEW_ACCOUNT_KEY = 'wecom-mobile-view-account'; + +interface ViewedAccount { + id: number; + nickname: string; + sn: string; +} + +function readViewedAccount(): ViewedAccount | null { + try { + const raw = window.localStorage.getItem(VIEW_ACCOUNT_KEY); + const value = raw ? (JSON.parse(raw) as ViewedAccount) : null; + return value && typeof value.id === 'number' ? value : null; + } catch { + return null; + } +} + +export const session = reactive({ + token: currentToken(), + me: null as Me | null, + // 有 remote/all 权限的人可以切到别的客服;其他人永远看自己。 + viewed: readViewedAccount() as ViewedAccount | null, +}); + +export const permissions = computed(() => session.me?.permissions); + +/** 当前查看的是不是别人的电脑。是的话,接口要带 account_id。 */ +export const viewedAccountId = computed(() => { + const me = session.me; + const viewed = session.viewed; + if (!me || !viewed || !me.permissions.control_all || viewed.id === me.account.id) return undefined; + return viewed.id; +}); + +export const viewedName = computed(() => { + if (viewedAccountId.value !== undefined && session.viewed) { + return session.viewed.nickname || session.viewed.sn || `客服 #${session.viewed.id}`; + } + return session.me?.account.nickname || session.me?.account.sn || ''; +}); + +export function viewAccount(account: AccountSummary | ViewedAccount | null): void { + session.viewed = account ? { id: account.id, nickname: account.nickname, sn: account.sn } : null; + try { + if (session.viewed) window.localStorage.setItem(VIEW_ACCOUNT_KEY, JSON.stringify(session.viewed)); + else window.localStorage.removeItem(VIEW_ACCOUNT_KEY); + } catch { + // 忽略:只是记住上次看的是谁 + } +} + +export function signIn(token: string, me: Me): void { + setToken(token); + session.token = token; + session.me = me; + if (!me.permissions.control_all) viewAccount(null); +} + +export function clearSession(): void { + setToken(''); + session.token = ''; + session.me = null; + viewAccount(null); +} + +export async function loadMe(): Promise { + const me = await api.me(); + session.me = me; + if (!me.permissions.control_all) viewAccount(null); + return me; +} + +export async function signOut(): Promise { + try { + await api.logout(); + } catch { + // 令牌已经失效也照样清掉本地状态 + } + clearSession(); +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/styles.css b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/styles.css new file mode 100644 index 000000000..f6bb20956 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/styles.css @@ -0,0 +1,123 @@ +/* 配色沿用桌面端控制台(zhenyang-ai-console)的蓝白体系。 */ +:root { + --app-bg: #f4f7fd; + --app-surface: #ffffff; + --app-surface-2: #f6f8fe; + --app-border: rgba(110, 137, 201, 0.18); + --app-text: #1d315f; + --app-text-2: #3f527d; + --app-muted: #7b89ab; + --app-primary: #2f6fff; + --app-primary-soft: rgba(47, 111, 255, 0.1); + --app-success: #0f9e69; + --app-success-soft: rgba(19, 183, 122, 0.12); + --app-warning: #c46f00; + --app-warning-soft: rgba(255, 152, 29, 0.14); + --app-danger: #e5383e; + --app-danger-soft: rgba(255, 83, 88, 0.12); + --app-muted-soft: rgba(123, 137, 171, 0.14); + --app-shadow: 0 10px 28px rgba(78, 99, 162, 0.08); + --app-radius: 16px; + --app-font: 'PingFang SC', 'HarmonyOS Sans SC', 'MiSans', 'Microsoft YaHei', system-ui, + -apple-system, 'Segoe UI', sans-serif; + --app-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace; + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root { + --app-bg: #0b1222; + --app-surface: #131c31; + --app-surface-2: #18223b; + --app-border: rgba(140, 160, 210, 0.16); + --app-text: #e6ecfb; + --app-text-2: #bcc6e0; + --app-muted: #8b97b6; + --app-primary: #6f9bff; + --app-primary-soft: rgba(91, 140, 255, 0.16); + --app-success: #2fd19a; + --app-success-soft: rgba(47, 209, 154, 0.14); + --app-warning: #ffb45a; + --app-warning-soft: rgba(255, 174, 74, 0.14); + --app-danger: #ff7a7f; + --app-danger-soft: rgba(255, 107, 112, 0.15); + --app-muted-soft: rgba(131, 144, 176, 0.16); + --app-shadow: 0 10px 28px rgba(0, 0, 0, 0.28); + color-scheme: dark; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--app-bg); + color: var(--app-text); + font-family: var(--app-font); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + +body, +#app { + min-height: 100vh; + min-height: 100dvh; +} + +button, +input, +textarea { + font-family: inherit; +} + +.app-card { + background: var(--app-surface); + border: 1px solid var(--app-border); + border-radius: var(--app-radius); + box-shadow: var(--app-shadow); +} + +.app-muted { + color: var(--app-muted); +} + +.app-mono { + font-family: var(--app-mono); + font-size: 12px; + word-break: break-all; +} + +.tone-success { + color: var(--app-success); +} +.tone-warning { + color: var(--app-warning); +} +.tone-danger { + color: var(--app-danger); +} +.tone-muted { + color: var(--app-muted); +} +.tone-primary { + color: var(--app-primary); +} + +.van-nav-bar .van-nav-bar__title { + font-weight: 600; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/vant.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/vant.ts new file mode 100644 index 000000000..f93a2f8c8 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/vant.ts @@ -0,0 +1,25 @@ +// 按需引入 Vant 组件样式(组件本身在各 .vue 里具名导入),不打包用不到的 CSS。 +import 'vant/es/style/base.css'; +import 'vant/es/config-provider/style/index'; +import 'vant/es/nav-bar/style/index'; +import 'vant/es/tabbar/style/index'; +import 'vant/es/tabbar-item/style/index'; +import 'vant/es/form/style/index'; +import 'vant/es/field/style/index'; +import 'vant/es/cell/style/index'; +import 'vant/es/cell-group/style/index'; +import 'vant/es/button/style/index'; +import 'vant/es/search/style/index'; +import 'vant/es/list/style/index'; +import 'vant/es/pull-refresh/style/index'; +import 'vant/es/tag/style/index'; +import 'vant/es/tabs/style/index'; +import 'vant/es/tab/style/index'; +import 'vant/es/popup/style/index'; +import 'vant/es/empty/style/index'; +import 'vant/es/loading/style/index'; +import 'vant/es/notice-bar/style/index'; +import 'vant/es/icon/style/index'; +import 'vant/es/skeleton/style/index'; +import 'vant/es/dialog/style/index'; +import 'vant/es/toast/style/index'; diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/ConversationsView.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/ConversationsView.vue new file mode 100644 index 000000000..60cf4ee07 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/ConversationsView.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/DevicesView.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/DevicesView.vue new file mode 100644 index 000000000..6bfa07363 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/DevicesView.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/LoginView.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/LoginView.vue new file mode 100644 index 000000000..36203f868 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/LoginView.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/MeView.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/MeView.vue new file mode 100644 index 000000000..6f0e51d08 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/MeView.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/ShellView.vue b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/ShellView.vue new file mode 100644 index 000000000..3ec07548f --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/src/views/ShellView.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/tsconfig.json b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/tsconfig.json new file mode 100644 index 000000000..21d43a866 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "strict": true, + "jsx": "preserve", + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/vite.config.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/vite.config.ts new file mode 100644 index 000000000..ff8621bc6 --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/vite.config.ts @@ -0,0 +1,46 @@ +import { fileURLToPath, URL } from 'node:url'; + +import vue from '@vitejs/plugin-vue'; +import { defineConfig, type Plugin } from 'vite'; + +// Vant 的图标字体同时写了内嵌的 woff2 和一个阿里 CDN 的 woff 兜底。内嵌那份总能用; +// 外链只会在内网环境里失败,并被页面的 CSP(font-src 'self' data:)拦下报错。 +// 在打包前改写源 CSS,文件名里的内容哈希才会跟着变,浏览器缓存不会拿到旧内容。 +function dropRemoteIconFont(): Plugin { + const remote = /,\s*url\(["']?\/\/at\.alicdn\.com[^)]*\)\s*format\(["']?woff["']?\)/g; + return { + name: 'drop-remote-icon-font', + enforce: 'pre', + transform(code, id) { + if (!id.split('?')[0].endsWith('.css') || !code.includes('at.alicdn.com')) return null; + return { code: code.replace(remote, ''), map: null }; + }, + }; +} + +// 构建产物由管理端 admin_api.py 托管在 /m/,与 /api/v2/mobile/* 同源。 +// 开发时把 /api 代理到本机后台(默认 8766,可用 MOBILE_API_TARGET 覆盖)。 +export default defineConfig({ + base: '/m/', + plugins: [vue(), dropRemoteIconFont()], + resolve: { + alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, + }, + build: { + outDir: 'dist', + assetsDir: 'assets', + // 企业微信 / 微信内置浏览器和老一点的 iOS Safari 也要能打开。 + target: ['es2019', 'safari13', 'chrome80'], + sourcemap: false, + }, + server: { + host: '127.0.0.1', + port: 5190, + proxy: { + '/api': { + target: process.env.MOBILE_API_TARGET || 'http://127.0.0.1:8766', + changeOrigin: false, + }, + }, + }, +}); diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/vitest.config.ts b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/vitest.config.ts new file mode 100644 index 000000000..7eeb3f85d --- /dev/null +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/apps/mobile-h5/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/pnpm-workspace.yaml b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/pnpm-workspace.yaml index fe3ab032c..0ee3273d9 100644 --- a/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/pnpm-workspace.yaml +++ b/.pnpm-store/v11/projects/a46d6327308ec23b0167cf73344b78d4/pnpm-workspace.yaml @@ -9,6 +9,9 @@ packages: - packages/effects/* - packages/business/* - apps/* + # 手机远程控制台是独立的轻量项目(Vue 3 + Vant),自带依赖和锁文件, + # 不跟 vben 管理端共用 workspace:cd apps/mobile-h5 && pnpm install --ignore-workspace + - '!apps/mobile-h5' - scripts/* publicHoistPattern: diff --git a/admin-web/apps/mobile-h5/.gitignore b/admin-web/apps/mobile-h5/.gitignore new file mode 100644 index 000000000..804bd4a4a --- /dev/null +++ b/admin-web/apps/mobile-h5/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +*.local diff --git a/admin-web/apps/mobile-h5/.npmrc b/admin-web/apps/mobile-h5/.npmrc new file mode 100644 index 000000000..0ea8b5f01 --- /dev/null +++ b/admin-web/apps/mobile-h5/.npmrc @@ -0,0 +1,6 @@ +# 独立项目:不加入 admin-web 的 pnpm workspace(见上级 pnpm-workspace.yaml 的排除项), +# 依赖和锁文件都在本目录。安装:pnpm install --ignore-workspace +registry=https://registry.npmmirror.com +fetch-timeout=600000 +fetch-retries=6 +network-concurrency=6 diff --git a/admin-web/apps/mobile-h5/README.md b/admin-web/apps/mobile-h5/README.md new file mode 100644 index 000000000..74f0426f1 --- /dev/null +++ b/admin-web/apps/mobile-h5/README.md @@ -0,0 +1,54 @@ +# 企微客服助手 · 手机远程控制台(mobile-h5) + +给客服本人用的手机 / 平板 H5:用 ZYT 账号登录,查看自己电脑上客服软件的运行状态,远程启动 / +停止自动回复,管理会话黑名单。有 `wecom_rpa.remote/all` 权限的主管可以切换查看其他客服。 + +- 技术栈:Vue 3 + Vite + Vant 4,hash 路由,按需引入 Vant 样式。 +- 布局:手机单栏 + 底部标签栏;宽度 ≥ 768px(平板、电脑浏览器)切成左列表 / 右详情两栏。 +- 适配:iOS 安全区(`viewport-fit=cover`)、深色模式(跟随系统)、可"添加到主屏幕"(manifest + 不缓存的 Service Worker)。 +- 托管:构建产物由管理端 `wechat_rpa/admin_api.py` 挂在 `/m/`,接口是同源的 `/api/v2/mobile/*`。 + +## 独立项目 + +这个目录**不属于** admin-web 的 pnpm workspace(`pnpm-workspace.yaml` 里有 `!apps/mobile-h5`), +依赖和锁文件都在本目录,不会影响 vben 管理端的安装和构建。 + +```bash +cd admin-web/apps/mobile-h5 +pnpm install --ignore-workspace # 走本目录 .npmrc 里的 npmmirror +pnpm dev # http://127.0.0.1:5190/m/ ,/api 代理到 127.0.0.1:8766(MOBILE_API_TARGET 可改) +pnpm test # vitest:时间格式、设备 / 指令状态文案 +pnpm build # vue-tsc 类型检查 + vite build → dist/ +``` + +发布时把 `dist/` 放到服务器 `/opt/im-admin/admin-web/apps/mobile-h5/dist/`(见 +`deploy/mobile-remote-20260928/`),`deploy/im-admin/Dockerfile` 会把它一起打进镜像。 + +## 页面 + +| 路由 | 内容 | +|---|---| +| `#/login` | ZYT 账号密码登录(后台代登 ZYT 终端 8,密码不落地);公网明文 HTTP 下提示改用 HTTPS | +| `#/desktops` | 电脑卡片:在线 / 离线、监听中 / 已停止、企业微信连接、回复通道、待处理数;启动 / 停止自动回复(需确认),下发后每 3 秒查询一次指令结果 | +| `#/conversations` | 会话列表(来自聊天归档,只有名称、类型、最近时间,不含聊天正文)+ 搜索 + 黑名单页签;详情里加入 / 移出黑名单 | +| `#/me` | 当前账号、五项手机端权限是否开通、登录信息、添加到主屏幕说明、退出登录 | + +电脑离线(客服软件没开)时只显示"电脑离线",无法远程拉起;电脑上是旧版客服软件(没有命令通道)时显示 +"版本过低",按钮不可用。 + +## 接口(`/api/v2/mobile`,Bearer 令牌) + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/auth/login` | `{account, password, client_name}` → `{access_token, expires_at, me}` | +| POST | `/auth/logout` | 同时注销 ZYT 终端 8 会话 | +| GET | `/me` | 账号、权限(control / control_all / blacklist_add / blacklist_remove / root)、会话有效期 | +| GET | `/accounts?q=` | 全部客服电脑概况,仅 `wecom_rpa.remote/all` | +| GET | `/desktops?account_id=` | 名下电脑与实时状态;`account_id` 仅 remote/all 可指定别人 | +| POST | `/desktops/{device_id}/commands?account_id=` | `{type: start_listening \| stop_listening}`,离线 409 `device_offline`,旧版 409 `client_outdated` | +| GET | `/commands/{id}` | 指令状态:pending → delivered → succeeded / failed / expired / superseded | +| GET | `/conversations?q=&scope=all\|blocked&cursor=&limit=` | 会话与黑名单状态 | +| PUT | `/blacklist/{account}/{conversation}?account_id=` | `{name, account_name, reason}`,需 `wecom_rpa.blacklist/add` | +| DELETE | `/blacklist/{account}/{conversation}?account_id=` | 需 `wecom_rpa.blacklist/remove` | + +错误体统一是 `{"detail": {"code": "...", "message": "给人看的中文"}}`,401 时前端清掉令牌回登录页。 diff --git a/admin-web/apps/mobile-h5/index.html b/admin-web/apps/mobile-h5/index.html new file mode 100644 index 000000000..2187497e7 --- /dev/null +++ b/admin-web/apps/mobile-h5/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + +
+ + + + diff --git a/admin-web/apps/mobile-h5/package.json b/admin-web/apps/mobile-h5/package.json new file mode 100644 index 000000000..5e3e2686d --- /dev/null +++ b/admin-web/apps/mobile-h5/package.json @@ -0,0 +1,27 @@ +{ + "name": "@wecom/mobile-h5", + "version": "1.0.0", + "private": true, + "description": "企微客服助手 · 手机远程控制台(托管在管理端 /m/)", + "type": "module", + "packageManager": "pnpm@11.16.0", + "scripts": { + "dev": "vite --mode development", + "build": "vue-tsc --noEmit && vite build --mode production", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "vant": "4.10.2", + "vue": "3.5.41", + "vue-router": "5.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "6.0.8", + "typescript": "6.0.3", + "vite": "8.2.1", + "vitest": "4.1.10", + "vue-tsc": "3.3.9" + } +} diff --git a/admin-web/apps/mobile-h5/pnpm-lock.yaml b/admin-web/apps/mobile-h5/pnpm-lock.yaml new file mode 100644 index 000000000..cc6594c99 --- /dev/null +++ b/admin-web/apps/mobile-h5/pnpm-lock.yaml @@ -0,0 +1,1439 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + vant: + specifier: 4.10.2 + version: 4.10.2(vue@3.5.41(typescript@6.0.3)) + vue: + specifier: 3.5.41 + version: 3.5.41(typescript@6.0.3) + vue-router: + specifier: 5.2.0 + version: 5.2.0(@vue/compiler-sfc@3.5.43)(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: 6.0.8 + version: 6.0.8(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3)) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: 8.2.1 + version: 8.2.1(yaml@2.9.1) + vitest: + specifier: 4.1.10 + version: 4.1.10(vite@8.2.1(yaml@2.9.1)) + vue-tsc: + specifier: 3.3.9 + version: 3.3.9(typescript@6.0.3) + +packages: + + '@babel/generator@8.0.6': + resolution: {integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@8.0.6': + resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.6': + resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.6': + resolution: {integrity: sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.6': + resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/gen-mapping@0.4.0-beta.0': + resolution: {integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.151.0': + resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + + '@rolldown/binding-android-arm-eabi@1.2.11': + resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.11': + resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.11': + resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.11': + resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.11': + resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.11': + resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.11': + resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.11': + resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.11': + resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.11': + resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@vant/popperjs@1.3.0': + resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==} + + '@vant/use@1.6.0': + resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==} + peerDependencies: + vue: ^3.0.0 + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue-macros/common@3.1.4': + resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} + + '@vue/compiler-core@3.5.43': + resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} + + '@vue/compiler-dom@3.5.43': + resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} + + '@vue/compiler-sfc@3.5.43': + resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + + '@vue/compiler-ssr@3.5.43': + resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==} + + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} + + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} + + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} + + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + + '@vue/shared@3.5.43': + resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@2.1.2: + resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==} + engines: {node: '>=20.18.0'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.9.0: + resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} + engines: {node: '>=20.19.0'} + + birpc@2.6.1: + resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} + engines: {node: '>=14'} + + magic-string-ast@1.0.2: + resolution: {integrity: sha512-8ngQgLhcT0t3YBdn9CGkZqCYlvwW9pm7aWJwd7AxseVWf1RU8ZHCQvG1mt3N5vvUme+pXTcHB8G/7fE666U8Vw==} + engines: {node: '>=20.18.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nostics@1.3.0: + resolution: {integrity: sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg==} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + rolldown@1.2.11: + resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + unplugin-utils@0.3.2: + resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} + engines: {node: '>=20.19.0'} + + unplugin@3.4.0: + resolution: {integrity: sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rsbuild/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rsbuild/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + + vant@4.10.2: + resolution: {integrity: sha512-7lm1KtFub11ozleFqQ6rL8oa3Z8i33G3IUEuJGWBXlQwPTRfu8BYWGGZ/GhA5QdWhERp5mtBq6TjpYEmLcwwow==} + peerDependencies: + vue: ^3.0.0 + + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + vue-router@5.2.0: + resolution: {integrity: sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.34 || ^4.0.0 + pinia: ^3.0.4 || ^4.0.2 + vite: ^7.3.0 || ^8.0.0 + vue: ^3.5.34 || ^4.0.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + vite: + optional: true + + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@babel/generator@8.0.6': + dependencies: + '@babel/parser': 8.0.6 + '@babel/types': 8.0.6 + '@jridgewell/gen-mapping': 0.4.0-beta.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-string-parser@8.0.6': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-identifier@8.0.6': {} + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/parser@8.0.6': + dependencies: + '@babel/types': 8.0.6 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@babel/types@8.0.6': + dependencies: + '@babel/helper-string-parser': 8.0.6 + '@babel/helper-validator-identifier': 8.0.6 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.4.0-beta.0': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.151.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.11': + optional: true + + '@rolldown/binding-android-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-x64@1.2.11': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.11': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.11': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.11': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.11': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.11': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/jsesc@2.5.1': {} + + '@vant/popperjs@1.3.0': {} + + '@vant/use@1.6.0(vue@3.5.41(typescript@6.0.3))': + dependencies: + vue: 3.5.41(typescript@6.0.3) + + '@vitejs/plugin-vue@6.0.8(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.1(yaml@2.9.1) + vue: 3.5.41(typescript@6.0.3) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.1(yaml@2.9.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.1(yaml@2.9.1) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.2.0 + + '@vue-macros/common@3.1.4(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@vue/compiler-sfc': 3.5.43 + ast-kit: 2.1.2 + local-pkg: 1.2.1 + magic-string-ast: 1.0.2 + unplugin-utils: 0.3.2 + optionalDependencies: + vue: 3.5.41(typescript@6.0.3) + + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.43 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-dom@3.5.43': + dependencies: + '@vue/compiler-core': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-sfc@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.43 + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-ssr': 3.5.43 + '@vue/shared': 3.5.43 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-ssr@3.5.43': + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/devtools-api@8.2.1': + dependencies: + '@vue/devtools-kit': 8.2.1 + + '@vue/devtools-kit@8.2.1': + dependencies: + '@vue/devtools-shared': 8.2.1 + birpc: 2.6.1 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.2.1': {} + + '@vue/language-core@3.3.9': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.7 + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + + '@vue/shared@3.5.43': {} + + acorn@8.18.0: {} + + alien-signals@3.2.1: {} + + assertion-error@2.0.1: {} + + ast-kit@2.1.2: + dependencies: + '@babel/parser': 7.29.9 + pathe: 2.0.3 + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.9 + pathe: 2.0.3 + + ast-walker-scope@0.9.0: + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + ast-kit: 2.2.0 + + birpc@2.6.1: {} + + chai@6.2.2: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + confbox@0.1.8: {} + + confbox@0.3.1: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + entities@7.0.1: {} + + es-module-lexer@2.3.2: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + exsolve@1.1.1: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + hookable@5.5.3: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + local-pkg@1.2.1: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.3 + quansync: 0.2.11 + + magic-string-ast@1.0.2: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + muggle-string@0.4.1: {} + + nanoid@3.3.19: {} + + nostics@1.3.0: {} + + obug@2.2.1: {} + + path-browserify@1.0.1: {} + + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.3: + dependencies: + confbox: 0.3.1 + exsolve: 1.1.1 + pathe: 2.0.3 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + quansync@0.2.11: {} + + readdirp@5.1.1: {} + + rolldown@1.2.11: + dependencies: + '@oxc-project/types': 0.151.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.11 + '@rolldown/binding-android-arm64': 1.2.11 + '@rolldown/binding-darwin-arm64': 1.2.11 + '@rolldown/binding-darwin-x64': 1.2.11 + '@rolldown/binding-freebsd-x64': 1.2.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 + '@rolldown/binding-linux-arm64-gnu': 1.2.11 + '@rolldown/binding-linux-arm64-musl': 1.2.11 + '@rolldown/binding-linux-ppc64-gnu': 1.2.11 + '@rolldown/binding-linux-s390x-gnu': 1.2.11 + '@rolldown/binding-linux-x64-gnu': 1.2.11 + '@rolldown/binding-linux-x64-musl': 1.2.11 + '@rolldown/binding-openharmony-arm64': 1.2.11 + '@rolldown/binding-win32-arm64-msvc': 1.2.11 + '@rolldown/binding-win32-x64-msvc': 1.2.11 + + scule@1.3.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + unplugin-utils@0.3.2: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.7 + + unplugin@3.4.0(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + rolldown: 1.2.11 + vite: 8.2.1(yaml@2.9.1) + + vant@4.10.2(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vant/popperjs': 1.3.0 + '@vant/use': 1.6.0(vue@3.5.41(typescript@6.0.3)) + '@vue/shared': 3.5.43 + vue: 3.5.41(typescript@6.0.3) + + vite@8.2.1(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.11 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + yaml: 2.9.1 + + vitest@4.1.10(vite@8.2.1(yaml@2.9.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.1(yaml@2.9.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.1(yaml@2.9.1) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - msw + + vscode-uri@3.2.0: {} + + vue-router@5.2.0(@vue/compiler-sfc@3.5.43)(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1))(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@babel/generator': 8.0.6 + '@vue-macros/common': 3.1.4(vue@3.5.41(typescript@6.0.3)) + '@vue/devtools-api': 8.2.1 + ast-walker-scope: 0.9.0 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.2.1 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + nostics: 1.3.0 + pathe: 2.0.3 + picomatch: 4.0.7 + scule: 1.3.0 + tinyglobby: 0.2.17 + unplugin: 3.4.0(rolldown@1.2.11)(vite@8.2.1(yaml@2.9.1)) + unplugin-utils: 0.3.2 + vue: 3.5.41(typescript@6.0.3) + yaml: 2.9.1 + optionalDependencies: + '@vue/compiler-sfc': 3.5.43 + vite: 8.2.1(yaml@2.9.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - webpack + + vue-tsc@3.3.9(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + typescript: 6.0.3 + + vue@3.5.41(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + optionalDependencies: + typescript: 6.0.3 + + webpack-virtual-modules@0.6.2: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + yaml@2.9.1: {} diff --git a/admin-web/apps/mobile-h5/public/icons/apple-touch-icon.png b/admin-web/apps/mobile-h5/public/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/admin-web/apps/mobile-h5/public/icons/apple-touch-icon.png differ diff --git a/admin-web/apps/mobile-h5/public/icons/favicon-64.png b/admin-web/apps/mobile-h5/public/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/admin-web/apps/mobile-h5/public/icons/favicon-64.png differ diff --git a/admin-web/apps/mobile-h5/public/icons/icon-192.png b/admin-web/apps/mobile-h5/public/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/admin-web/apps/mobile-h5/public/icons/icon-192.png differ diff --git a/admin-web/apps/mobile-h5/public/icons/icon-512.png b/admin-web/apps/mobile-h5/public/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/admin-web/apps/mobile-h5/public/icons/icon-512.png differ diff --git a/admin-web/apps/mobile-h5/public/icons/icon-maskable-512.png b/admin-web/apps/mobile-h5/public/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/admin-web/apps/mobile-h5/public/icons/icon-maskable-512.png differ diff --git a/admin-web/apps/mobile-h5/public/manifest.webmanifest b/admin-web/apps/mobile-h5/public/manifest.webmanifest new file mode 100644 index 000000000..bc53828cc --- /dev/null +++ b/admin-web/apps/mobile-h5/public/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话忽略。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/admin-web/apps/mobile-h5/public/sw.js b/admin-web/apps/mobile-h5/public/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/admin-web/apps/mobile-h5/public/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/admin-web/apps/mobile-h5/src/App.vue b/admin-web/apps/mobile-h5/src/App.vue new file mode 100644 index 000000000..6bc117b42 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/App.vue @@ -0,0 +1,47 @@ + + + diff --git a/admin-web/apps/mobile-h5/src/api.ts b/admin-web/apps/mobile-h5/src/api.ts new file mode 100644 index 000000000..602943255 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/api.ts @@ -0,0 +1,320 @@ +// 手机控制台只调用 /api/v2/mobile/*。令牌放 localStorage,登出或失效即清除; +// 不保存密码。 +const API_BASE = '/api/v2/mobile'; +const TOKEN_KEY = 'wecom-mobile-token'; + +export interface Permissions { + control: boolean; + control_all: boolean; + blacklist_add: boolean; + blacklist_remove: boolean; + root: boolean; + known: boolean; +} + +export interface Me { + account: { id: number; nickname: string; sn: string; avatar: string }; + permissions: Permissions; + permission_codes: Record, string>; + session: { created_at: string; expires_at: number }; +} + +export interface DeviceStatus { + listening: boolean; + state: string; + state_label: string; + transport: string; + transport_label: string; + wecom_connected: boolean; + pending_count: number; + reported_at: string; + reported_seconds: number | null; + fresh: boolean; +} + +export type CommandType = 'start_listening' | 'stop_listening'; +export type CommandStatus = + | 'pending' + | 'delivered' + | 'succeeded' + | 'failed' + | 'expired' + | 'superseded'; + +export interface Command { + id: string; + type: CommandType; + type_label: string; + device_id: string; + status: CommandStatus; + status_label: string; + final: boolean; + created_at: string; + created_by: string; + delivered_at: string; + acked_at: string; + expires_at: number; + result: { message?: string; listening?: boolean; [key: string]: unknown }; + error: string; +} + +export interface Device { + device_id: string; + device_name: string; + app_version: string; + online: boolean; + session_active: boolean; + last_seen_at: string; + last_seen_seconds: number | null; + remote_capable: boolean; + blacklist_sync: boolean; + status: DeviceStatus | null; + last_command: Command | null; +} + +export interface DesktopsResponse { + account: { id: number; nickname: string; sn: string }; + items: Device[]; + server_time: number; +} + +export interface AccountSummary { + id: number; + nickname: string; + sn: string; + device_count: number; + online: boolean; + listening: boolean; + last_online_at: string; +} + +export interface BlacklistEntry { + account: string; + account_name: string; + conversation: string; + name: string; + reason: string; + source: string; + created_by: string; + created_at: string; +} + +export interface Conversation { + account: string; + account_name: string; + conversation: string; + name: string; + type: string; + type_label: string; + last_message_at: string; + blocked: boolean; + blacklist: BlacklistEntry | null; +} + +export interface ConversationPage { + items: Conversation[]; + next_cursor: string; + has_more: boolean; + blocked_total: number; +} + +export class ApiError extends Error { + readonly status: number; + readonly code: string; + readonly retryAfter: number; + + constructor(status: number, code: string, message: string, retryAfter = 0) { + super(message); + this.status = status; + this.code = code; + this.retryAfter = retryAfter; + } +} + +let unauthorizedHandler: (() => void) | null = null; + +/** 任意接口返回 401(令牌失效 / 被管理端下线)时回调,由路由层跳回登录页。 */ +export function onUnauthorized(handler: () => void): void { + unauthorizedHandler = handler; +} + +export function readToken(): string { + try { + return window.localStorage.getItem(TOKEN_KEY) || ''; + } catch { + return ''; + } +} + +export function writeToken(token: string): void { + try { + if (token) window.localStorage.setItem(TOKEN_KEY, token); + else window.localStorage.removeItem(TOKEN_KEY); + } catch { + // 隐私模式下 localStorage 可能不可写:本次打开期间仍可用,刷新后需要重新登录。 + } +} + +let memoryToken = ''; + +export function currentToken(): string { + return memoryToken || readToken(); +} + +export function setToken(token: string): void { + memoryToken = token; + writeToken(token); +} + +/** 把后端各种形状的错误体(字符串、{code,message}、422 校验数组)归一成一句话。 */ +export function errorFromBody(status: number, body: unknown): ApiError { + const detail = (body && typeof body === 'object' && 'detail' in body + ? (body as { detail: unknown }).detail + : body) as unknown; + if (detail && typeof detail === 'object' && !Array.isArray(detail)) { + const value = detail as { code?: unknown; message?: unknown; retry_after?: unknown }; + return new ApiError( + status, + String(value.code || `http_${status}`), + String(value.message || defaultMessage(status)), + Number(value.retry_after || 0), + ); + } + if (Array.isArray(detail)) { + return new ApiError(status, 'validation', '填写的内容不符合要求,请检查后重试'); + } + if (typeof detail === 'string' && detail.trim()) { + return new ApiError(status, `http_${status}`, detail.trim()); + } + return new ApiError(status, `http_${status}`, defaultMessage(status)); +} + +function defaultMessage(status: number): string { + if (status === 401) return '登录已失效,请重新登录'; + if (status === 403) return '没有权限执行这个操作'; + if (status === 404) return '内容不存在或已被删除'; + if (status === 429) return '操作太频繁,请稍后再试'; + if (status >= 500) return '服务暂时不可用,请稍后重试'; + return `请求失败(HTTP ${status})`; +} + +interface RequestOptions { + body?: unknown; + query?: Record; + auth?: boolean; + signal?: AbortSignal; +} + +async function request(method: string, path: string, options: RequestOptions = {}): Promise { + const { body, query, auth = true, signal } = options; + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query || {})) { + if (value !== undefined && value !== null && value !== '') params.set(key, String(value)); + } + const url = `${API_BASE}${path}${params.toString() ? `?${params}` : ''}`; + const headers: Record = { Accept: 'application/json' }; + if (body !== undefined) headers['Content-Type'] = 'application/json'; + const token = currentToken(); + if (auth && token) headers.Authorization = `Bearer ${token}`; + let response: Response; + try { + response = await fetch(url, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + signal, + cache: 'no-store', + credentials: 'omit', + }); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') throw error; + throw new ApiError(0, 'network', '网络连接失败,请检查网络后重试'); + } + let data: unknown = null; + const text = await response.text(); + if (text) { + try { + data = JSON.parse(text); + } catch { + data = null; + } + } + if (!response.ok) { + const error = errorFromBody(response.status, data); + if (response.status === 401 && auth) unauthorizedHandler?.(); + throw error; + } + return data as T; +} + +const segment = (value: string) => encodeURIComponent(value); + +export const api = { + login(account: string, password: string, clientName: string) { + return request<{ access_token: string; expires_at: number; me: Me }>('POST', '/auth/login', { + auth: false, + body: { account, password, client_name: clientName }, + }); + }, + logout() { + return request<{ ok: boolean }>('POST', '/auth/logout'); + }, + me() { + return request('GET', '/me'); + }, + accounts(q = '') { + return request<{ items: AccountSummary[] }>('GET', '/accounts', { query: { q } }); + }, + desktops(accountId?: number) { + return request('GET', '/desktops', { query: { account_id: accountId } }); + }, + sendCommand(deviceId: string, type: CommandType, accountId?: number) { + return request<{ command: Command }>('POST', `/desktops/${segment(deviceId)}/commands`, { + body: { type }, + query: { account_id: accountId }, + }); + }, + command(commandId: string) { + return request<{ command: Command }>('GET', `/commands/${segment(commandId)}`); + }, + conversations(options: { + accountId?: number; + q?: string; + scope?: 'all' | 'blocked'; + cursor?: string; + limit?: number; + signal?: AbortSignal; + }) { + return request('GET', '/conversations', { + query: { + account_id: options.accountId, + q: options.q, + scope: options.scope, + cursor: options.cursor, + limit: options.limit, + }, + signal: options.signal, + }); + }, + block( + target: { account: string; conversation: string; name?: string; account_name?: string }, + reason: string, + accountId?: number, + ) { + return request<{ entry: BlacklistEntry; version: number }>( + 'PUT', + `/blacklist/${segment(target.account)}/${segment(target.conversation)}`, + { + body: { name: target.name || '', account_name: target.account_name || '', reason }, + query: { account_id: accountId }, + }, + ); + }, + unblock(account: string, conversation: string, accountId?: number) { + return request<{ ok: boolean; version: number }>( + 'DELETE', + `/blacklist/${segment(account)}/${segment(conversation)}`, + { query: { account_id: accountId } }, + ); + }, +}; diff --git a/admin-web/apps/mobile-h5/src/brand-mark.png b/admin-web/apps/mobile-h5/src/brand-mark.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/admin-web/apps/mobile-h5/src/brand-mark.png differ diff --git a/admin-web/apps/mobile-h5/src/components/AccountSwitcher.vue b/admin-web/apps/mobile-h5/src/components/AccountSwitcher.vue new file mode 100644 index 000000000..9a1c2acca --- /dev/null +++ b/admin-web/apps/mobile-h5/src/components/AccountSwitcher.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/components/ConversationDetail.vue b/admin-web/apps/mobile-h5/src/components/ConversationDetail.vue new file mode 100644 index 000000000..e9f52050c --- /dev/null +++ b/admin-web/apps/mobile-h5/src/components/ConversationDetail.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/components/ConversationRow.vue b/admin-web/apps/mobile-h5/src/components/ConversationRow.vue new file mode 100644 index 000000000..9ac12c709 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/components/ConversationRow.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/components/DevicePanel.vue b/admin-web/apps/mobile-h5/src/components/DevicePanel.vue new file mode 100644 index 000000000..f00103240 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/components/DevicePanel.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/composables.ts b/admin-web/apps/mobile-h5/src/composables.ts new file mode 100644 index 000000000..1deace3e5 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/composables.ts @@ -0,0 +1,74 @@ +import { onActivated, onBeforeUnmount, onDeactivated, onMounted, ref } from 'vue'; + +/** ≥768px(平板横竖屏、桌面浏览器)切成左右两栏。 */ +export function useWide(query = '(min-width: 768px)') { + const media = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia(query) : null; + const wide = ref(media ? media.matches : false); + const update = (event: MediaQueryListEvent) => { + wide.value = event.matches; + }; + onMounted(() => media?.addEventListener?.('change', update)); + onBeforeUnmount(() => media?.removeEventListener?.('change', update)); + return wide; +} + +export function usePrefersDark() { + const media = + typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; + const dark = ref(media ? media.matches : false); + const update = (event: MediaQueryListEvent) => { + dark.value = event.matches; + }; + onMounted(() => media?.addEventListener?.('change', update)); + onBeforeUnmount(() => media?.removeEventListener?.('change', update)); + return dark; +} + +/** + * 页面可见时按间隔刷新;切到后台就停,回到前台立刻补一次。 + * 手机锁屏后还在轮询只会白白耗电和流量。被 KeepAlive 缓存的页面切走时也停。 + */ +export function useVisibleInterval(task: () => unknown, intervalMs: number) { + let timer: number | undefined; + let active = false; + const stop = () => { + if (timer !== undefined) window.clearInterval(timer); + timer = undefined; + }; + const start = () => { + stop(); + timer = window.setInterval(() => { + if (document.visibilityState === 'visible') void task(); + }, intervalMs); + }; + const onVisibility = () => { + if (!active) return; + if (document.visibilityState === 'visible') { + void task(); + start(); + } else { + stop(); + } + }; + onMounted(() => { + active = true; + start(); + document.addEventListener('visibilitychange', onVisibility); + }); + onActivated(() => { + // 首次挂载也会触发 activated;那一次 onMounted 已经启动过了。 + if (active) return; + active = true; + void task(); + start(); + }); + onDeactivated(() => { + active = false; + stop(); + }); + onBeforeUnmount(() => { + active = false; + stop(); + document.removeEventListener('visibilitychange', onVisibility); + }); +} diff --git a/admin-web/apps/mobile-h5/src/format.test.ts b/admin-web/apps/mobile-h5/src/format.test.ts new file mode 100644 index 000000000..877a348c4 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/format.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; + +import type { Command, Device } from './api'; +import { describeCommand, describeDevice, durationText, initial, relativeTime } from './format'; + +const now = new Date(2026, 8, 28, 15, 30, 0); + +function device(overrides: Partial = {}): Device { + return { + device_id: 'dev-1', + device_name: 'PC', + app_version: '1.5.0', + online: true, + session_active: true, + last_seen_at: '', + last_seen_seconds: 3, + remote_capable: true, + blacklist_sync: true, + status: { + listening: true, + state: 'running', + state_label: '监听中', + transport: 'visual', + transport_label: '视觉通道', + wecom_connected: true, + pending_count: 0, + reported_at: '', + reported_seconds: 4, + fresh: true, + }, + last_command: null, + ...overrides, + }; +} + +describe('relativeTime', () => { + it('uses friendly buckets', () => { + expect(relativeTime(new Date(2026, 8, 28, 15, 29, 40).toISOString(), now)).toBe('刚刚'); + expect(relativeTime(new Date(2026, 8, 28, 15, 10, 0).toISOString(), now)).toBe('20 分钟前'); + expect(relativeTime(new Date(2026, 8, 28, 9, 5, 0).toISOString(), now)).toBe('今天 09:05'); + expect(relativeTime(new Date(2026, 8, 27, 21, 0, 0).toISOString(), now)).toBe('昨天 21:00'); + expect(relativeTime(new Date(2026, 1, 3, 8, 0, 0).toISOString(), now)).toBe('02-03 08:00'); + expect(relativeTime(new Date(2025, 11, 31, 8, 0, 0).toISOString(), now)).toBe('2025-12-31'); + }); + + it('accepts backend offsets and tolerates junk', () => { + expect(relativeTime('2026-09-28T15:29:30+08:00', new Date('2026-09-28T15:30:00+08:00'))).toBe('刚刚'); + expect(relativeTime('', now)).toBe('—'); + expect(relativeTime('not a date', now)).toBe('—'); + }); +}); + +describe('durationText', () => { + it('rounds to the largest sensible unit', () => { + expect(durationText(12)).toBe('12 秒'); + expect(durationText(150)).toBe('3 分钟'); + expect(durationText(7200)).toBe('2 小时'); + expect(durationText(3 * 86400)).toBe('3 天'); + expect(durationText(null)).toBe('—'); + }); +}); + +describe('describeDevice', () => { + it('prioritises offline, then outdated, then status', () => { + expect(describeDevice(device({ online: false, last_seen_seconds: 600 })).label).toBe('离线'); + expect(describeDevice(device({ remote_capable: false })).label).toBe('版本过低'); + expect(describeDevice(device()).tone).toBe('success'); + const stopped = device(); + stopped.status = { ...stopped.status!, listening: false, state: 'stopped', state_label: '已停止' }; + expect(describeDevice(stopped)).toMatchObject({ tone: 'muted', label: '已停止' }); + const failed = device(); + failed.status = { ...failed.status!, state: 'error', state_label: '连接失败' }; + expect(describeDevice(failed).tone).toBe('danger'); + }); +}); + +describe('describeCommand', () => { + const base: Command = { + id: 'c1', + type: 'stop_listening', + type_label: '停止监听', + device_id: 'dev-1', + status: 'pending', + status_label: '', + final: false, + created_at: '', + created_by: '', + delivered_at: '', + acked_at: '', + expires_at: 0, + result: {}, + error: '', + }; + + it('describes progress and outcomes', () => { + expect(describeCommand(base)).toMatchObject({ busy: true, tone: 'primary' }); + expect(describeCommand({ ...base, status: 'succeeded', result: { message: '监听已停止' } }).text).toBe('监听已停止'); + expect(describeCommand({ ...base, status: 'failed', error: '企业微信未登录' }).text).toContain('企业微信未登录'); + expect(describeCommand({ ...base, status: 'expired' }).tone).toBe('warning'); + }); +}); + +describe('initial', () => { + it('takes the first visible character', () => { + expect(initial('张三')).toBe('张'); + expect(initial('alice')).toBe('A'); + expect(initial(' ')).toBe('?'); + expect(initial('😀客户')).toBe('😀'); + }); +}); diff --git a/admin-web/apps/mobile-h5/src/format.ts b/admin-web/apps/mobile-h5/src/format.ts new file mode 100644 index 000000000..ee4ad3b50 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/format.ts @@ -0,0 +1,107 @@ +import type { Command, Device } from './api'; + +const pad = (value: number) => String(value).padStart(2, '0'); + +/** 解析后端的 ISO 时间(本地时区偏移或 UTC 都有),失败返回 null。 */ +export function parseTime(value: string | null | undefined): Date | null { + const text = String(value || '').trim(); + if (!text) return null; + const date = new Date(text); + return Number.isNaN(date.getTime()) ? null : date; +} + +/** 刚刚 / N 分钟前 / 今天 09:30 / 昨天 21:05 / 09-18 14:00 / 2025-12-31 */ +export function relativeTime(value: string | null | undefined, now: Date = new Date()): string { + const date = parseTime(value); + if (!date) return '—'; + const seconds = Math.round((now.getTime() - date.getTime()) / 1000); + if (seconds < 45 && seconds > -60) return '刚刚'; + if (seconds >= 45 && seconds < 3600) return `${Math.max(1, Math.round(seconds / 60))} 分钟前`; + const hm = `${pad(date.getHours())}:${pad(date.getMinutes())}`; + const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); + const day = 24 * 3600 * 1000; + if (date.getTime() >= startOfToday && date.getTime() < startOfToday + day) return `今天 ${hm}`; + if (date.getTime() >= startOfToday - day && date.getTime() < startOfToday) return `昨天 ${hm}`; + if (date.getFullYear() === now.getFullYear()) { + return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${hm}`; + } + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; +} + +/** 秒数 → "3 秒 / 2 分钟 / 5 小时 / 3 天" */ +export function durationText(seconds: number | null | undefined): string { + if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) return '—'; + const value = Math.max(0, Math.round(seconds)); + if (value < 60) return `${value} 秒`; + if (value < 3600) return `${Math.round(value / 60)} 分钟`; + if (value < 86400) return `${Math.round(value / 3600)} 小时`; + return `${Math.round(value / 86400)} 天`; +} + +export type Tone = 'success' | 'warning' | 'danger' | 'muted' | 'primary'; + +export interface DeviceSummary { + tone: Tone; + label: string; + hint: string; +} + +/** 设备卡片最上面那一行:一眼看清在不在线、有没有在自动回复。 */ +export function describeDevice(device: Device): DeviceSummary { + if (!device.online) { + const since = device.last_seen_seconds; + return { + tone: 'muted', + label: '离线', + hint: since === null ? '客服软件没有运行' : `客服软件没有运行,${durationText(since)}前在线`, + }; + } + if (!device.remote_capable) { + return { tone: 'warning', label: '版本过低', hint: '电脑上的客服软件需要升级后才能远程控制' }; + } + const status = device.status; + if (!status) return { tone: 'muted', label: '状态未知', hint: '等待电脑上报状态' }; + if (status.state === 'error') return { tone: 'danger', label: status.state_label, hint: '请到电脑上查看运行日志' }; + if (status.listening) { + if (status.state === 'running') { + return { + tone: 'success', + label: '监听中', + hint: status.wecom_connected ? '正在自动处理新消息' : '监听已开启,企业微信尚未连接', + }; + } + return { tone: 'warning', label: status.state_label, hint: '监听已开启,正在准备' }; + } + if (status.state === 'stopping') return { tone: 'warning', label: '正在停止', hint: '等待后台任务退出' }; + return { tone: 'muted', label: '已停止', hint: '不会自动回复新消息' }; +} + +/** 下发指令后的进度文字。 */ +export function describeCommand(command: Command): { tone: Tone; text: string; busy: boolean } { + const verb = command.type_label || '指令'; + switch (command.status) { + case 'pending': + return { tone: 'primary', text: `「${verb}」已下发,等待电脑执行…`, busy: true }; + case 'delivered': + return { tone: 'primary', text: `电脑已收到「${verb}」,正在执行…`, busy: true }; + case 'succeeded': { + const message = typeof command.result?.message === 'string' ? command.result.message : ''; + return { tone: 'success', text: message || `「${verb}」已执行`, busy: false }; + } + case 'failed': + return { tone: 'danger', text: `「${verb}」执行失败:${command.error || '电脑没有给出原因'}`, busy: false }; + case 'expired': + return { tone: 'warning', text: `「${verb}」没有在 10 分钟内得到电脑确认,已作废`, busy: false }; + case 'superseded': + return { tone: 'muted', text: `「${verb}」已被新的指令取代`, busy: false }; + default: + return { tone: 'muted', text: command.status_label || verb, busy: false }; + } +} + +/** 头像圆里显示的那个字:会话名的第一个字符。 */ +export function initial(name: string): string { + const text = String(name || '').trim(); + if (!text) return '?'; + return Array.from(text)[0]!.toUpperCase(); +} diff --git a/admin-web/apps/mobile-h5/src/main.ts b/admin-web/apps/mobile-h5/src/main.ts new file mode 100644 index 000000000..001e5b9e4 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/main.ts @@ -0,0 +1,17 @@ +import { createApp } from 'vue'; + +import App from './App.vue'; +import { router } from './router'; +import './vant'; +import './styles.css'; + +createApp(App).use(router).mount('#app'); + +// 只为"添加到主屏幕"注册,不做离线缓存;明文 HTTP 下浏览器不允许注册,直接跳过。 +if (import.meta.env.PROD && 'serviceWorker' in navigator && window.isSecureContext) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register(`${import.meta.env.BASE_URL}sw.js`, { scope: import.meta.env.BASE_URL }) + .catch(() => undefined); + }); +} diff --git a/admin-web/apps/mobile-h5/src/router.ts b/admin-web/apps/mobile-h5/src/router.ts new file mode 100644 index 000000000..654e41e17 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/router.ts @@ -0,0 +1,83 @@ +import { createRouter, createWebHashHistory } from 'vue-router'; + +import { ApiError, onUnauthorized } from './api'; +import { clearSession, loadMe, session } from './session'; + +// hash 路由:刷新任何页面都落在 /m/ 这一个入口上,后台不用为前端路由做重写。 +export const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/login', + name: 'login', + component: () => import('./views/LoginView.vue'), + meta: { public: true }, + }, + { + path: '/', + component: () => import('./views/ShellView.vue'), + children: [ + { path: '', redirect: { name: 'desktops' } }, + { path: 'desktops', name: 'desktops', component: () => import('./views/DevicesView.vue') }, + { + path: 'conversations', + name: 'conversations', + component: () => import('./views/ConversationsView.vue'), + }, + { path: 'me', name: 'me', component: () => import('./views/MeView.vue') }, + ], + }, + { path: '/:pathMatch(.*)*', redirect: { name: 'desktops' } }, + ], +}); + +router.beforeEach(async (to) => { + if (to.meta.public) return true; + if (!session.token) return { name: 'login', query: { redirect: to.fullPath } }; + if (!session.me) { + try { + await loadMe(); + } catch (error) { + if (error instanceof ApiError && error.status === 401) { + clearSession(); + return { name: 'login', query: { redirect: to.fullPath } }; + } + // 网络问题:放行,由页面自己展示错误并提供重试。 + } + } + return true; +}); + +onUnauthorized(() => { + clearSession(); + const current = router.currentRoute.value; + // 首次导航还没完成(路由守卫里 loadMe 得到 401):由守卫带着目标地址跳登录, + // 这里再跳一次会把 redirect 覆盖成起始的 "/"。 + if (!current.matched.length) return; + if (current.name !== 'login') { + void router.replace({ name: 'login', query: { redirect: current.fullPath, expired: '1' } }); + } +}); + +// 发版后,还开着的旧页面按旧文件名去拉页面代码会 404。整页刷新一次就能拿到新版本; +// 10 秒内只刷新一次,服务端真出问题时不会无限刷新。 +const CHUNK_RELOAD_KEY = 'wecom-mobile-chunk-reload'; +router.onError((error, to) => { + const message = String((error as Error)?.message || error); + const staleChunk = + /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test( + message, + ); + if (!staleChunk) return; + let last = 0; + try { + last = Number(window.sessionStorage.getItem(CHUNK_RELOAD_KEY) || 0); + window.sessionStorage.setItem(CHUNK_RELOAD_KEY, String(Date.now())); + } catch { + // sessionStorage 不可用时照样刷新一次 + } + if (Date.now() - last > 10_000) { + window.location.assign(`${import.meta.env.BASE_URL}#${to.fullPath}`); + window.location.reload(); + } +}); diff --git a/admin-web/apps/mobile-h5/src/session.ts b/admin-web/apps/mobile-h5/src/session.ts new file mode 100644 index 000000000..cbdcc3015 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/session.ts @@ -0,0 +1,85 @@ +import { computed, reactive } from 'vue'; + +import { type AccountSummary, api, currentToken, type Me, setToken } from './api'; + +const VIEW_ACCOUNT_KEY = 'wecom-mobile-view-account'; + +interface ViewedAccount { + id: number; + nickname: string; + sn: string; +} + +function readViewedAccount(): ViewedAccount | null { + try { + const raw = window.localStorage.getItem(VIEW_ACCOUNT_KEY); + const value = raw ? (JSON.parse(raw) as ViewedAccount) : null; + return value && typeof value.id === 'number' ? value : null; + } catch { + return null; + } +} + +export const session = reactive({ + token: currentToken(), + me: null as Me | null, + // 有 remote/all 权限的人可以切到别的客服;其他人永远看自己。 + viewed: readViewedAccount() as ViewedAccount | null, +}); + +export const permissions = computed(() => session.me?.permissions); + +/** 当前查看的是不是别人的电脑。是的话,接口要带 account_id。 */ +export const viewedAccountId = computed(() => { + const me = session.me; + const viewed = session.viewed; + if (!me || !viewed || !me.permissions.control_all || viewed.id === me.account.id) return undefined; + return viewed.id; +}); + +export const viewedName = computed(() => { + if (viewedAccountId.value !== undefined && session.viewed) { + return session.viewed.nickname || session.viewed.sn || `客服 #${session.viewed.id}`; + } + return session.me?.account.nickname || session.me?.account.sn || ''; +}); + +export function viewAccount(account: AccountSummary | ViewedAccount | null): void { + session.viewed = account ? { id: account.id, nickname: account.nickname, sn: account.sn } : null; + try { + if (session.viewed) window.localStorage.setItem(VIEW_ACCOUNT_KEY, JSON.stringify(session.viewed)); + else window.localStorage.removeItem(VIEW_ACCOUNT_KEY); + } catch { + // 忽略:只是记住上次看的是谁 + } +} + +export function signIn(token: string, me: Me): void { + setToken(token); + session.token = token; + session.me = me; + if (!me.permissions.control_all) viewAccount(null); +} + +export function clearSession(): void { + setToken(''); + session.token = ''; + session.me = null; + viewAccount(null); +} + +export async function loadMe(): Promise { + const me = await api.me(); + session.me = me; + if (!me.permissions.control_all) viewAccount(null); + return me; +} + +export async function signOut(): Promise { + try { + await api.logout(); + } catch { + // 令牌已经失效也照样清掉本地状态 + } + clearSession(); +} diff --git a/admin-web/apps/mobile-h5/src/styles.css b/admin-web/apps/mobile-h5/src/styles.css new file mode 100644 index 000000000..f6bb20956 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/styles.css @@ -0,0 +1,123 @@ +/* 配色沿用桌面端控制台(zhenyang-ai-console)的蓝白体系。 */ +:root { + --app-bg: #f4f7fd; + --app-surface: #ffffff; + --app-surface-2: #f6f8fe; + --app-border: rgba(110, 137, 201, 0.18); + --app-text: #1d315f; + --app-text-2: #3f527d; + --app-muted: #7b89ab; + --app-primary: #2f6fff; + --app-primary-soft: rgba(47, 111, 255, 0.1); + --app-success: #0f9e69; + --app-success-soft: rgba(19, 183, 122, 0.12); + --app-warning: #c46f00; + --app-warning-soft: rgba(255, 152, 29, 0.14); + --app-danger: #e5383e; + --app-danger-soft: rgba(255, 83, 88, 0.12); + --app-muted-soft: rgba(123, 137, 171, 0.14); + --app-shadow: 0 10px 28px rgba(78, 99, 162, 0.08); + --app-radius: 16px; + --app-font: 'PingFang SC', 'HarmonyOS Sans SC', 'MiSans', 'Microsoft YaHei', system-ui, + -apple-system, 'Segoe UI', sans-serif; + --app-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace; + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root { + --app-bg: #0b1222; + --app-surface: #131c31; + --app-surface-2: #18223b; + --app-border: rgba(140, 160, 210, 0.16); + --app-text: #e6ecfb; + --app-text-2: #bcc6e0; + --app-muted: #8b97b6; + --app-primary: #6f9bff; + --app-primary-soft: rgba(91, 140, 255, 0.16); + --app-success: #2fd19a; + --app-success-soft: rgba(47, 209, 154, 0.14); + --app-warning: #ffb45a; + --app-warning-soft: rgba(255, 174, 74, 0.14); + --app-danger: #ff7a7f; + --app-danger-soft: rgba(255, 107, 112, 0.15); + --app-muted-soft: rgba(131, 144, 176, 0.16); + --app-shadow: 0 10px 28px rgba(0, 0, 0, 0.28); + color-scheme: dark; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--app-bg); + color: var(--app-text); + font-family: var(--app-font); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + +body, +#app { + min-height: 100vh; + min-height: 100dvh; +} + +button, +input, +textarea { + font-family: inherit; +} + +.app-card { + background: var(--app-surface); + border: 1px solid var(--app-border); + border-radius: var(--app-radius); + box-shadow: var(--app-shadow); +} + +.app-muted { + color: var(--app-muted); +} + +.app-mono { + font-family: var(--app-mono); + font-size: 12px; + word-break: break-all; +} + +.tone-success { + color: var(--app-success); +} +.tone-warning { + color: var(--app-warning); +} +.tone-danger { + color: var(--app-danger); +} +.tone-muted { + color: var(--app-muted); +} +.tone-primary { + color: var(--app-primary); +} + +.van-nav-bar .van-nav-bar__title { + font-weight: 600; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/admin-web/apps/mobile-h5/src/vant.ts b/admin-web/apps/mobile-h5/src/vant.ts new file mode 100644 index 000000000..f93a2f8c8 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/vant.ts @@ -0,0 +1,25 @@ +// 按需引入 Vant 组件样式(组件本身在各 .vue 里具名导入),不打包用不到的 CSS。 +import 'vant/es/style/base.css'; +import 'vant/es/config-provider/style/index'; +import 'vant/es/nav-bar/style/index'; +import 'vant/es/tabbar/style/index'; +import 'vant/es/tabbar-item/style/index'; +import 'vant/es/form/style/index'; +import 'vant/es/field/style/index'; +import 'vant/es/cell/style/index'; +import 'vant/es/cell-group/style/index'; +import 'vant/es/button/style/index'; +import 'vant/es/search/style/index'; +import 'vant/es/list/style/index'; +import 'vant/es/pull-refresh/style/index'; +import 'vant/es/tag/style/index'; +import 'vant/es/tabs/style/index'; +import 'vant/es/tab/style/index'; +import 'vant/es/popup/style/index'; +import 'vant/es/empty/style/index'; +import 'vant/es/loading/style/index'; +import 'vant/es/notice-bar/style/index'; +import 'vant/es/icon/style/index'; +import 'vant/es/skeleton/style/index'; +import 'vant/es/dialog/style/index'; +import 'vant/es/toast/style/index'; diff --git a/admin-web/apps/mobile-h5/src/views/ConversationsView.vue b/admin-web/apps/mobile-h5/src/views/ConversationsView.vue new file mode 100644 index 000000000..60cf4ee07 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/views/ConversationsView.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/views/DevicesView.vue b/admin-web/apps/mobile-h5/src/views/DevicesView.vue new file mode 100644 index 000000000..6bfa07363 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/views/DevicesView.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/views/LoginView.vue b/admin-web/apps/mobile-h5/src/views/LoginView.vue new file mode 100644 index 000000000..36203f868 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/views/LoginView.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/views/MeView.vue b/admin-web/apps/mobile-h5/src/views/MeView.vue new file mode 100644 index 000000000..6f0e51d08 --- /dev/null +++ b/admin-web/apps/mobile-h5/src/views/MeView.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/src/views/ShellView.vue b/admin-web/apps/mobile-h5/src/views/ShellView.vue new file mode 100644 index 000000000..3ec07548f --- /dev/null +++ b/admin-web/apps/mobile-h5/src/views/ShellView.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/admin-web/apps/mobile-h5/tsconfig.json b/admin-web/apps/mobile-h5/tsconfig.json new file mode 100644 index 000000000..21d43a866 --- /dev/null +++ b/admin-web/apps/mobile-h5/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "strict": true, + "jsx": "preserve", + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/admin-web/apps/mobile-h5/vite.config.ts b/admin-web/apps/mobile-h5/vite.config.ts new file mode 100644 index 000000000..ff8621bc6 --- /dev/null +++ b/admin-web/apps/mobile-h5/vite.config.ts @@ -0,0 +1,46 @@ +import { fileURLToPath, URL } from 'node:url'; + +import vue from '@vitejs/plugin-vue'; +import { defineConfig, type Plugin } from 'vite'; + +// Vant 的图标字体同时写了内嵌的 woff2 和一个阿里 CDN 的 woff 兜底。内嵌那份总能用; +// 外链只会在内网环境里失败,并被页面的 CSP(font-src 'self' data:)拦下报错。 +// 在打包前改写源 CSS,文件名里的内容哈希才会跟着变,浏览器缓存不会拿到旧内容。 +function dropRemoteIconFont(): Plugin { + const remote = /,\s*url\(["']?\/\/at\.alicdn\.com[^)]*\)\s*format\(["']?woff["']?\)/g; + return { + name: 'drop-remote-icon-font', + enforce: 'pre', + transform(code, id) { + if (!id.split('?')[0].endsWith('.css') || !code.includes('at.alicdn.com')) return null; + return { code: code.replace(remote, ''), map: null }; + }, + }; +} + +// 构建产物由管理端 admin_api.py 托管在 /m/,与 /api/v2/mobile/* 同源。 +// 开发时把 /api 代理到本机后台(默认 8766,可用 MOBILE_API_TARGET 覆盖)。 +export default defineConfig({ + base: '/m/', + plugins: [vue(), dropRemoteIconFont()], + resolve: { + alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, + }, + build: { + outDir: 'dist', + assetsDir: 'assets', + // 企业微信 / 微信内置浏览器和老一点的 iOS Safari 也要能打开。 + target: ['es2019', 'safari13', 'chrome80'], + sourcemap: false, + }, + server: { + host: '127.0.0.1', + port: 5190, + proxy: { + '/api': { + target: process.env.MOBILE_API_TARGET || 'http://127.0.0.1:8766', + changeOrigin: false, + }, + }, + }, +}); diff --git a/admin-web/apps/mobile-h5/vitest.config.ts b/admin-web/apps/mobile-h5/vitest.config.ts new file mode 100644 index 000000000..7eeb3f85d --- /dev/null +++ b/admin-web/apps/mobile-h5/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/admin-web/pnpm-workspace.yaml b/admin-web/pnpm-workspace.yaml index fe3ab032c..0ee3273d9 100644 --- a/admin-web/pnpm-workspace.yaml +++ b/admin-web/pnpm-workspace.yaml @@ -9,6 +9,9 @@ packages: - packages/effects/* - packages/business/* - apps/* + # 手机远程控制台是独立的轻量项目(Vue 3 + Vant),自带依赖和锁文件, + # 不跟 vben 管理端共用 workspace:cd apps/mobile-h5 && pnpm install --ignore-workspace + - '!apps/mobile-h5' - scripts/* publicHoistPattern: diff --git a/deploy/im-admin/Dockerfile b/deploy/im-admin/Dockerfile index 8c3423b23..36761e36f 100644 --- a/deploy/im-admin/Dockerfile +++ b/deploy/im-admin/Dockerfile @@ -12,6 +12,8 @@ RUN pip install --disable-pip-version-check -r /tmp/requirements.txt \ COPY --chown=app:app wechat_rpa/*.py /app/wechat_rpa/ COPY --chown=app:app admin-web/apps/web-antd/dist/ /app/admin-web/apps/web-antd/dist/ +# 手机远程控制台(admin-web/apps/mobile-h5,独立项目),由 admin_api.py 托管在 /m/ +COPY --chown=app:app admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ USER app WORKDIR /app/wechat_rpa diff --git a/deploy/im-admin/im.zhenyangtang.cn.https.conf.example b/deploy/im-admin/im.zhenyangtang.cn.https.conf.example new file mode 100644 index 000000000..aa7804292 --- /dev/null +++ b/deploy/im-admin/im.zhenyangtang.cn.https.conf.example @@ -0,0 +1,85 @@ +# 手机远程控制台(/m/)的 HTTPS 入口。 +# +# 自 2026-09-28(mobile-http-20260928)起按用户要求,内外网都允许明文 HTTP 登录手机端 +# (MOBILE_REQUIRE_HTTPS 默认 never)。HTTP 下 ZYT 密码和手机会话令牌在公网上可被截获; +# 配好本 HTTPS 入口后,建议给 api 服务加环境变量 MOBILE_REQUIRE_HTTPS=auto(公网必须 HTTPS,内网仍可 HTTP)。 +# +# 启用步骤(宝塔面板或手工均可): +# 1. 证书:宝塔「网站 → im.zhenyangtang.cn → SSL」申请 Let's Encrypt,或 acme.sh。 +# 公网 443 没有转发到本机时,HTTP-01 验证会失败,改用 DNS-01(DNS API)签发。 +# 2. 端口:路由器把公网 TCP 443(或选定的 HTTPS 端口,例如 8443)转发到 192.168.110.111。 +# 用非 443 端口时把下面的 listen 改成对应端口,手机访问 https://im.zhenyangtang.cn:8443/m/。 +# 3. 把本文件复制成 im.zhenyangtang.cn.https.conf 放到与现有 conf 相同的目录,修改证书路径, +# `nginx -t` 通过后 `nginx -s reload`。现有的 80 端口配置保持不动,桌面端继续可用。 +# 4. 验证:curl -I https://im.zhenyangtang.cn/m/ 返回 200;手机浏览器打开后登录页不再提示明文连接。 +# +# 注意 X-Real-IP 必须由 Nginx 用 $remote_addr 覆盖写入:后台用它做登录限流和"是否内网"的判断。 + +server { + listen 443 ssl http2; + server_name im.zhenyangtang.cn; + + ssl_certificate /www/server/panel/vhost/cert/im.zhenyangtang.cn/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/im.zhenyangtang.cn/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL_IM:10m; + ssl_session_timeout 1d; + add_header Strict-Transport-Security "max-age=31536000" always; + + client_max_body_size 100m; + + location ^~ /api/v2/ { + proxy_pass http://127.0.0.1:18766; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 15s; + proxy_send_timeout 300s; + # 桌面端命令通道长轮询最长挂起 25 秒,远小于这里的读超时。 + proxy_read_timeout 300s; + } + + location ^~ /api/ { + proxy_pass http://127.0.0.1:18766/api/v2/; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 15s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + location ^~ /gateway/ { + proxy_pass http://127.0.0.1:18770/; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 15s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + location / { + proxy_pass http://127.0.0.1:18766; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_connect_timeout 15s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + access_log /www/wwwlogs/im.zhenyangtang.cn.https.log; + error_log /www/wwwlogs/im.zhenyangtang.cn.https.error.log; +} diff --git a/deploy/mobile-http-20260928/01-preflight.sh b/deploy/mobile-http-20260928/01-preflight.sh new file mode 100644 index 000000000..2ddb9643b --- /dev/null +++ b/deploy/mobile-http-20260928/01-preflight.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# 基线核对、备份将被替换的源文件和 /m/ 目录、给当前镜像打回滚标签。只读线上 + 写本目录。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +test ! -e baseline-check.json +python3 remote_server_guard.py baseline > baseline-check.json.tmp +mv baseline-check.json.tmp baseline-check.json +cat baseline-check.json +test ! -e source-before +test ! -e preflight-complete +if docker image inspect zyt/wecom-admin:before-mobile-http-20260928 >/dev/null 2>&1; then + echo 'Refuse to replace existing rollback tag' >&2; exit 1 +fi +mkdir -m 700 source-before +python3 - <<'PY' +from pathlib import Path +import json, shutil +r = Path('/home/ps/mobile-http-20260928'); project = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + target = r / 'source-before' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(project / name, target) +shutil.copytree(project / plan['frontendDir'], r / 'source-before' / plan['frontendDir']) +PY +docker tag zyt/wecom-admin:current zyt/wecom-admin:before-mobile-http-20260928 +docker image inspect zyt/wecom-admin:current --format '{{.Id}}' > image-before.txt +printf '%s\n' ready > preflight-complete +echo "preflight complete; rollback image tagged before-mobile-http-20260928 ($(cat image-before.txt))" diff --git a/deploy/mobile-http-20260928/02-build.sh b/deploy/mobile-http-20260928/02-build.sh new file mode 100644 index 000000000..4662eea14 --- /dev/null +++ b/deploy/mobile-http-20260928/02-build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# 构建候选镜像 + 离线跑测试 + 核对候选镜像文件。线上服务不受影响。 +# 本次不改数据库(只改登录的 HTTPS 默认策略和 /m/ 静态文件),不做整库备份;回滚靠旧镜像和 source-before。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +test -f preflight-complete +test ! -e candidate-passed +python3 remote_server_guard.py baseline +test "$(docker image inspect zyt/wecom-admin:before-mobile-http-20260928 --format '{{.Id}}')" = "$(cat image-before.txt)" +docker build --network=none --pull=false -f Dockerfile.overlay -t zyt/wecom-admin:mobile-http-20260928 . > candidate-build.log 2>&1 +# serverTestModules 随包放在 tests/;imageTestModules 用镜像自带的测试文件(PYTHONPATH 后一项)。 +# -P:不把工作目录 /app/wechat_rpa 放到 sys.path 最前面,否则镜像里同名的旧测试会盖住随包的新测试。 +mapfile -t modules < <(python3 -c 'import json;p=json.load(open("release-plan.json"));print("\n".join(p["serverTestModules"]+p.get("imageTestModules",[])))') +test "${#modules[@]}" -gt 0 +docker run --rm --network=none --read-only --tmpfs /tmp:size=512m,mode=1777 --tmpfs /data:size=256m,mode=1777 \ + -v "$release/tests:/release-tests:ro" -e PYTHONPATH=/release-tests:/app/wechat_rpa \ + zyt/wecom-admin:mobile-http-20260928 python -X utf8 -B -P -m unittest "${modules[@]}" -q > candidate-tests.log 2>&1 +tail -n 3 candidate-tests.log +python3 remote_server_guard.py candidate +printf '%s\n' passed > candidate-passed diff --git a/deploy/mobile-http-20260928/03-deploy.sh b/deploy/mobile-http-20260928/03-deploy.sh new file mode 100644 index 000000000..fbb06a557 --- /dev/null +++ b/deploy/mobile-http-20260928/03-deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# 替换 remote_api.py 和整个 /m/ 目录;切换镜像,只重建 api 容器。任何一步失败自动调用 rollback.sh。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +test -f candidate-passed +python3 remote_server_guard.py baseline +cd /opt/im-admin/deploy/im-admin +docker compose config --quiet +rollback_on_error() { rc=$?; trap - ERR; bash "$release/rollback.sh"; exit "$rc"; } +trap rollback_on_error ERR +python3 - <<'PY' +from pathlib import Path +import json, hashlib, os, shutil +r = Path('/home/ps/mobile-http-20260928'); target = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +rows = [row for row in json.loads((r / 'manifest.json').read_text()) if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +# 容器从镜像里读 /m/,宿主上的目录只给以后整包重建用;仍然整体换名,避免留下半套文件。 +staging = target / (frontend + '.mobile-http.tmp'); assert not staging.exists() +old = target / (frontend + '.mobile-http.old'); assert not old.exists() +for row in rows: + rel = row['path'][len('payload/'):] + data = (r / row['path']).read_bytes(); assert hashlib.sha256(data).hexdigest() == row['sha256'], rel + if rel.startswith(frontend + '/'): + dest = staging / rel[len(frontend) + 1:] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data); os.chmod(dest, 0o644) + continue + dest = (target / rel).resolve(); assert dest.is_relative_to(target) and dest.exists() + tmp = dest.with_name(dest.name + '.mobile-http.tmp'); assert not tmp.exists() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, dest.stat().st_mode & 0o777); os.replace(tmp, dest) +for directory in [staging] + [p for p in staging.rglob('*') if p.is_dir()]: + os.chmod(directory, 0o755) +final = target / frontend +os.replace(final, old) +os.replace(staging, final) +shutil.rmtree(old) # 原目录已在 source-before 里 +PY +docker tag zyt/wecom-admin:mobile-http-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +bash "$release/04-verify.sh" +trap - ERR +printf '%s\n' 'Plain-HTTP phone login enabled, deployed and verified.' diff --git a/deploy/mobile-http-20260928/04-verify.sh b/deploy/mobile-http-20260928/04-verify.sh new file mode 100644 index 000000000..117df1ba0 --- /dev/null +++ b/deploy/mobile-http-20260928/04-verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +python3 remote_server_guard.py deployed | tee deployed-verification.json diff --git a/deploy/mobile-http-20260928/Dockerfile.overlay b/deploy/mobile-http-20260928/Dockerfile.overlay new file mode 100644 index 000000000..769afeb35 --- /dev/null +++ b/deploy/mobile-http-20260928/Dockerfile.overlay @@ -0,0 +1,9 @@ +ARG BASE_IMAGE=zyt/wecom-admin:before-mobile-http-20260928 +FROM ${BASE_IMAGE} +# 先删掉旧版 /m/ 目录再放新版,镜像里不残留旧的(带内网横幅的)页面文件。 +USER root +RUN rm -rf /app/admin-web/apps/mobile-h5/dist +USER app +COPY --chown=app:app payload/wechat_rpa/remote_api.py /app/wechat_rpa/remote_api.py +COPY --chown=app:app payload/admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ +WORKDIR /app/wechat_rpa diff --git a/deploy/mobile-http-20260928/manifest.json b/deploy/mobile-http-20260928/manifest.json new file mode 100644 index 000000000..ada9d72f1 --- /dev/null +++ b/deploy/mobile-http-20260928/manifest.json @@ -0,0 +1,162 @@ +[ + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js", + "sha256": "8d6186020c3d8562f581ea579742dd497649203a880b9c13cc904462b134bc60", + "bytes": 2308 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css", + "sha256": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "bytes": 359 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js", + "sha256": "226d4cde03c5434f20aa96c84cfb33538af084d79805348a089e343c5056281f", + "bytes": 14528 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png", + "sha256": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "bytes": 16743 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js", + "sha256": "b0d4b420bc5197a5df6d918e740e2b90b108c6e06ba930c3fd8e97fabe010556", + "bytes": 4490 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js", + "sha256": "75cd3809eb0d7db6c994302dd296674eadd366a53a4feb97a759173d70546e80", + "bytes": 28526 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css", + "sha256": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "bytes": 4220 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css", + "sha256": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "bytes": 3983 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js", + "sha256": "237de4f76004cbf16e5928060cf009fbbb3beec826006a876ddbede0ee9512f7", + "bytes": 10460 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js", + "sha256": "da55fbacb606cdb51f6eb09fb8bf4963fe249e869d653f0fb71d3c0da9ed7b4c", + "bytes": 8422 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js", + "sha256": "a98c816d31f9a937c5418dd48d25bc5b7b96fb3084d58571565d0810f420ced9", + "bytes": 14363 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-C_mEe-tZ.js", + "sha256": "6c6620136850dbdfa6ea22a9fa6c320f3f5d33150b83f1d1b9597e0894f5d0de", + "bytes": 117757 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css", + "sha256": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "bytes": 97051 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js", + "sha256": "29bdcbe8030ef2e169b2ea453cef160f2b3c86b9a83718971023a9862e10bd45", + "bytes": 6336 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css", + "sha256": "15ecdd5cb294bb51eef05e4843ac3a65f6e1718cbf16206547a876c52c5e8a2d", + "bytes": 1107 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css", + "sha256": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "bytes": 927 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js", + "sha256": "9e5ff0c332dfae54d9f8e1c70b0a896e48aef80e8b45101092c08a59dfddf521", + "bytes": 3465 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css", + "sha256": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "bytes": 130 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js", + "sha256": "13f70e99d3eccfa014f14dc313a195b4f24200929bf60142f6fd190ef33f2f3d", + "bytes": 2939 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js", + "sha256": "530e97b69f045bf3c2798b79c1f49bd3ee69f53a657bad39825cb69d68552c9c", + "bytes": 607 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png", + "sha256": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "bytes": 24681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png", + "sha256": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "bytes": 5439 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png", + "sha256": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "bytes": 26433 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png", + "sha256": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "bytes": 144310 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png", + "sha256": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "bytes": 95961 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/index.html", + "sha256": "6f335e7804fe9a1ce7bb5df388d2af81c7d919ddebb3beaee9533553a36adf04", + "bytes": 1357 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest", + "sha256": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "bytes": 684 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/sw.js", + "sha256": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "bytes": 339 + }, + { + "path": "payload/wechat_rpa/remote_api.py", + "sha256": "e8a51c1701a05d54713f45325e03481ad22609bc721455aa5ddef0d3ed5ae29a", + "bytes": 37707 + }, + { + "path": "tests/test_backend.py", + "sha256": "7473ec5583c97941e132ade7a1f66abcf54fa6108ec6bb2a182f0152964fff61", + "bytes": 66279 + }, + { + "path": "tests/test_remote_api.py", + "sha256": "479cb9b042dc14707af718734c33688013203d4154f36199b7b0ced656653c39", + "bytes": 38523 + }, + { + "path": "tests/test_zyt_auth.py", + "sha256": "40ab2d37f51780af26497e22948ef665c92b7f3707f6ba30a6d711bf5b15dfa6", + "bytes": 4907 + } +] \ No newline at end of file diff --git a/deploy/mobile-http-20260928/mobile-http-server-bundle.tar.gz b/deploy/mobile-http-20260928/mobile-http-server-bundle.tar.gz new file mode 100644 index 000000000..73d889f6b Binary files /dev/null and b/deploy/mobile-http-20260928/mobile-http-server-bundle.tar.gz differ diff --git a/deploy/mobile-http-20260928/payload-diff.txt b/deploy/mobile-http-20260928/payload-diff.txt new file mode 100644 index 000000000..4ad90e89b --- /dev/null +++ b/deploy/mobile-http-20260928/payload-diff.txt @@ -0,0 +1,27 @@ +--- production/wechat_rpa/remote_api.py ++++ release/wechat_rpa/remote_api.py +@@ -79,15 +79,17 @@ + + + def https_required(request: Request) -> bool: +- """手机端要在公网上提交 ZYT 密码,明文 HTTP 只允许在内网试用。 +- +- MOBILE_REQUIRE_HTTPS:auto(默认,公网地址必须 HTTPS)/ always / never。 +- +- 没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker 网桥 +- (172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 ++ """手机端登录是否必须走 HTTPS。 ++ ++ MOBILE_REQUIRE_HTTPS:never(默认,内外网都允许明文 HTTP 登录,按用户要求)/ ++ auto(公网地址必须 HTTPS,内网可用 HTTP)/ always。配好 HTTPS 入口后建议改成 ++ auto:明文 HTTP 下 ZYT 密码和手机会话令牌在公网上可被截获。 ++ ++ auto 模式下没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker ++ 网桥(172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 + (开发时直接访问 127.0.0.1)才在没有这个头时被信任。 + """ +- policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "auto").strip().lower() ++ policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "never").strip().lower() + if policy == "never" or request_scheme(request) == "https": + return False + if policy == "always": diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css new file mode 100644 index 000000000..03b9e2d0d --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css @@ -0,0 +1 @@ +.switcher[data-v-91cf283c]{max-height:80vh}.switcher-body[data-v-91cf283c]{padding:16px 0 12px}.switcher-body h3[data-v-91cf283c]{margin:0 16px 8px;font-size:17px}.switcher-state[data-v-91cf283c]{place-items:center;padding:32px 0;display:grid}.switcher-list[data-v-91cf283c]{margin-top:12px}.current[data-v-91cf283c]{color:var(--app-primary);margin-left:6px} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js new file mode 100644 index 000000000..0537ea489 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{c as n,d as r,l as i,u as a}from"./format-2xch3ZbY.js";import{n as o,o as s,s as c,t as l}from"./cell-group-CEKvg8Wf.js";import{n as u,r as d,t as f}from"./field-Bs9V8Rb1.js";import{$ as p,Bt as m,Ct as h,Dt as g,E as _,Et as v,Ft as y,G as b,Gt as x,Jt as S,K as C,Kt as w,Lt as T,Mt as E,Nt as ee,O as D,Q as O,St as k,Ut as A,V as te,Vt as j,X as M,Y as N,Yt as ne,Z as re,_ as P,_t as F,a as ie,c as ae,et as oe,ft as se,g as I,gt as L,ht as ce,l as le,n as ue,o as R,ot as z,q as B,qt as V,rt as de,vt as fe,wt as pe,x as me,yt as H}from"./index-C_mEe-tZ.js";var U=0;function he(e){e?(U||document.body.classList.add(`van-toast--unclickable`),U++):U&&(U--,U||document.body.classList.remove(`van-toast--unclickable`))}var[ge,W]=P(`toast`),_e=[`show`,`overlay`,`teleport`,`transition`,`overlayClass`,`overlayStyle`,`closeOnClickOverlay`,`zIndex`],ve={icon:String,show:Boolean,type:N(`text`),overlay:Boolean,message:M,iconSize:M,duration:C(2e3),position:N(`middle`),teleport:[String,Object],wordBreak:String,className:null,iconPrefix:String,transition:N(`van-fade`),loadingType:String,forbidClick:Boolean,overlayClass:null,overlayStyle:Object,closeOnClick:Boolean,closeOnClickOverlay:Boolean,zIndex:M},ye=h({name:ge,props:ve,emits:[`update:show`],setup(t,{emit:n,slots:r}){let i,o=!1,c=()=>{let e=t.show&&t.forbidClick;o!==e&&(o=e,he(o))},l=e=>n(`update:show`,e),u=()=>{t.closeOnClick&&l(!1)},d=()=>clearTimeout(i),f=()=>{let{icon:n,type:r,iconSize:i,iconPrefix:a,loadingType:o}=t;if(n||r===`success`||r===`fail`)return k(e,{name:n||r,size:i,class:W(`icon`),classPrefix:a},null);if(r===`loading`)return k(s,{class:W(`loading`),size:i,type:o},null)},p=()=>{let{type:e,message:n}=t;if(r.message)return k(`div`,{class:W(`text`)},[r.message()]);if(oe(n)&&n!==``)return e===`html`?k(`div`,{key:0,class:W(`text`),innerHTML:String(n)},null):k(`div`,{class:W(`text`)},[n])};return j(()=>[t.show,t.forbidClick],c),j(()=>[t.show,t.type,t.message,t.duration],()=>{d(),t.show&&t.duration>0&&(i=setTimeout(()=>{l(!1)},t.duration))}),E(c),ee(c),()=>k(a,v({class:[W([t.position,t.wordBreak===`normal`?`break-normal`:t.wordBreak,{[t.type]:!t.icon}]),t.className],lockScroll:!1,onClick:u,onClosed:d,"onUpdate:show":l},z(t,_e)),{default:()=>[f(),p()]})}}),be={icon:``,type:`text`,message:``,className:``,overlay:!1,onClose:void 0,onOpened:void 0,duration:2e3,teleport:`body`,iconSize:void 0,iconPrefix:void 0,position:`middle`,transition:`van-fade`,forbidClick:!1,loadingType:void 0,overlayClass:``,overlayStyle:void 0,closeOnClick:!1,closeOnClickOverlay:!1},G=[],K=O({},be),xe=new Map;function Se(e){return de(e)?e:{message:e}}function Ce(){let{instance:e,unmount:t}=n({setup(){let e=w(``),{open:t,state:n,close:r,toggle:a}=i(),o=()=>{},s=()=>k(ye,v(n,{onClosed:o,"onUpdate:show":a}),null);return j(e,e=>{n.message=e}),pe().render=s,{open:t,close:r,message:e}}});return e}function we(){if(!G.length){let e=Ce();G.push(e)}return G[G.length-1]}function Te(e={}){if(!p)return{};let t=we(),n=Se(e);return t.open(O({},K,xe.get(n.type||K.type),n)),t}var[Ee,q]=P(`empty`),De={image:N(`default`),imageSize:[Number,String,Array],description:String},Oe=h({name:Ee,props:De,setup(e,{slots:t}){let n=()=>{let n=t.description?t.description():e.description;if(n)return k(`p`,{class:q(`description`)},[n])},r=()=>{if(t.default)return k(`div`,{class:q(`bottom`)},[t.default()])},i=d(),a=e=>`${i}-${e}`,o=e=>`url(#${a(e)})`,s=(e,t,n)=>k(`stop`,{"stop-color":e,offset:`${t}%`,"stop-opacity":n},null),c=(e,t)=>[s(e,0),s(t,100)],l=e=>[k(`defs`,null,[k(`radialGradient`,{id:a(e),cx:`50%`,cy:`54%`,fx:`50%`,fy:`54%`,r:`297%`,gradientTransform:`matrix(-.16 0 0 -.33 .58 .72)`,"data-allow-mismatch":`attribute`},[s(`#EBEDF0`,0),s(`#F2F3F5`,100,.3)])]),k(`ellipse`,{fill:o(e),opacity:`.8`,cx:`80`,cy:`140`,rx:`46`,ry:`8`,"data-allow-mismatch":`attribute`},null)],u=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`a`),x1:`64%`,y1:`100%`,x2:`64%`,"data-allow-mismatch":`attribute`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M36 131V53H16v20H2v58h34z`,fill:o(`a`)},null),k(`path`,{d:`M123 15h22v14h9v77h-31V15z`,fill:o(`a`)},null)])],f=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`b`),x1:`64%`,y1:`97%`,x2:`64%`,y2:`0%`,"data-allow-mismatch":`attribute`},[s(`#F2F3F5`,0,.3),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M87 6c3 0 7 3 8 6a8 8 0 1 1-1 16H80a7 7 0 0 1-8-6c0-4 3-7 6-7 0-5 4-9 9-9Z`,fill:o(`b`)},null),k(`path`,{d:`M19 23c2 0 3 1 4 3 2 0 4 2 4 4a4 4 0 0 1-4 3v1h-7v-1l-1 1c-2 0-3-2-3-4 0-1 1-3 3-3 0-2 2-4 4-4Z`,fill:o(`b`)},null)])],p=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{id:a(1),x1:`64%`,y1:`100%`,x2:`64%`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)]),k(`linearGradient`,{id:a(2),x1:`50%`,x2:`50%`,y2:`84%`},[s(`#EBEDF0`,0),s(`#DCDEE0`,100,0)]),k(`linearGradient`,{id:a(3),x1:`100%`,x2:`100%`,y2:`100%`},[c(`#EAEDF0`,`#DCDEE0`)]),k(`radialGradient`,{id:a(4),cx:`50%`,cy:`0%`,fx:`50%`,fy:`0%`,r:`100%`,gradientTransform:`matrix(0 1 -.54 0 .5 -.5)`},[s(`#EBEDF0`,0),s(`#FFF`,100,0)])]),k(`g`,{fill:`none`},[u(),k(`path`,{fill:o(4),d:`M0 139h160v21H0z`,"data-allow-mismatch":`attribute`},null),k(`path`,{d:`M80 54a7 7 0 0 1 3 13v27l-2 2h-2a2 2 0 0 1-2-2V67a7 7 0 0 1 3-13z`,fill:o(2),"data-allow-mismatch":`attribute`},null),k(`g`,{opacity:`.6`,"stroke-linecap":`round`,"stroke-width":`7`,"data-allow-mismatch":`children`},[k(`path`,{d:`M64 47a19 19 0 0 0-5 13c0 5 2 10 5 13`,stroke:o(3)},null),k(`path`,{d:`M53 36a34 34 0 0 0 0 48`,stroke:o(3)},null),k(`path`,{d:`M95 73a19 19 0 0 0 6-13c0-5-2-9-6-13`,stroke:o(3)},null),k(`path`,{d:`M106 84a34 34 0 0 0 0-48`,stroke:o(3)},null)]),k(`g`,{transform:`translate(31 105)`},[k(`rect`,{fill:`#EBEDF0`,width:`98`,height:`34`,rx:`2`},null),k(`rect`,{fill:`#FFF`,x:`9`,y:`8`,width:`80`,height:`18`,rx:`1.1`},null),k(`rect`,{fill:`#EBEDF0`,x:`15`,y:`12`,width:`18`,height:`6`,rx:`1.1`},null)])])]),m=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(5)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`95%`,y1:`48%`,x2:`5.5%`,y2:`51%`,id:a(6)},[c(`#EAEDF1`,`#DCDEE0`)]),k(`linearGradient`,{y1:`45%`,x2:`100%`,y2:`54%`,id:a(7)},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),k(`g`,{transform:`translate(36 50)`,fill:`none`},[k(`g`,{transform:`translate(8)`},[k(`rect`,{fill:`#EBEDF0`,opacity:`.6`,x:`38`,y:`13`,width:`36`,height:`53`,rx:`2`},null),k(`rect`,{fill:o(5),width:`64`,height:`66`,rx:`2`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#FFF`,x:`6`,y:`6`,width:`52`,height:`55`,rx:`1`},null),k(`g`,{transform:`translate(15 17)`,fill:o(6),"data-allow-mismatch":`attribute`},[k(`rect`,{width:`34`,height:`6`,rx:`1`},null),k(`path`,{d:`M0 14h34v6H0z`},null),k(`rect`,{y:`28`,width:`34`,height:`6`,rx:`1`},null)])]),k(`rect`,{fill:o(7),y:`61`,width:`88`,height:`28`,rx:`1`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#F7F8FA`,x:`29`,y:`72`,width:`30`,height:`6`,rx:`1`},null)])]),h=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,null,[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(8),"data-allow-mismatch":`attribute`},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),l(`c`),k(`path`,{d:`m59 60 21 21 21-21h3l9 9v3L92 93l21 21v3l-9 9h-3l-21-21-21 21h-3l-9-9v-3l21-21-21-21v-3l9-9h3Z`,fill:o(8),"data-allow-mismatch":`attribute`},null)]),g=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,y1:`100%`,x2:`50%`,id:a(9)},[c(`#EEE`,`#D8D8D8`)]),k(`linearGradient`,{x1:`100%`,y1:`50%`,y2:`50%`,id:a(10)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(11)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(12)},[c(`#FFF`,`#F7F8FA`)])]),u(),f(),l(`d`),k(`g`,{transform:`rotate(-45 113 -4)`,fill:`none`,"data-allow-mismatch":`children`},[k(`rect`,{fill:o(9),x:`24`,y:`52.8`,width:`5.8`,height:`19`,rx:`1`},null),k(`rect`,{fill:o(10),x:`22.1`,y:`67.3`,width:`9.9`,height:`28`,rx:`1`},null),k(`circle`,{stroke:o(11),"stroke-width":`8`,cx:`27`,cy:`27`,r:`27`},null),k(`circle`,{fill:o(12),cx:`27`,cy:`27`,r:`16`},null),k(`path`,{d:`M37 7c-8 0-15 5-16 12`,stroke:o(11),"stroke-width":`3`,opacity:`.5`,"stroke-linecap":`round`,transform:`rotate(45 29 13)`},null)])]),_=()=>{var n;if(t.image)return t.image();let r={error:h,search:g,network:p,default:m};return((n=r[e.image])==null?void 0:n.call(r))||k(`img`,{src:e.image},null)};return()=>k(`div`,{class:q()},[k(`div`,{class:q(`image`),style:me(e.imageSize)},[_()]),n(),r()])}}),J=I(Oe),[ke,Y,Ae]=P(`pull-refresh`),X=50,je=[`pulling`,`loosing`,`success`],Me={disabled:Boolean,modelValue:Boolean,headHeight:B(X),successText:String,pullingText:String,loosingText:String,loadingText:String,pullDistance:M,successDuration:B(500),animationDuration:B(300)},Ne=h({name:ke,props:Me,emits:[`change`,`refresh`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i,a=w(),o=w(),c=b(a),l=x({status:`normal`,distance:0,duration:0}),u=r(),d=()=>{if(e.headHeight!==X)return{height:`${e.headHeight}px`}},f=()=>l.status!==`loading`&&l.status!==`success`&&!e.disabled,p=t=>{let n=+(e.pullDistance||e.headHeight);return t>n&&(t=t{let i=+(e.pullDistance||e.headHeight);l.distance=n,r?l.status=`loading`:n===0?l.status=`normal`:n{let{status:t}=l;return t===`normal`?``:e[`${t}Text`]||Ae(t)},v=()=>{let{status:e,distance:t}=l;if(n[e])return n[e]({distance:t});let r=[];return je.includes(e)&&r.push(k(`div`,{class:Y(`text`)},[h()])),e===`loading`&&r.push(k(s,{class:Y(`loading`)},{default:h})),r},y=()=>{l.status=`success`,setTimeout(()=>{m(0)},+e.successDuration)},S=e=>{i=_(c.value)===0,i&&(l.duration=0,u.start(e))},C=e=>{f()&&S(e)},T=e=>{if(f()){i||S(e);let{deltaY:t}=u;u.move(e),i&&t.value>=0&&u.isVertical()&&(D(e),m(p(t.value)))}},E=()=>{i&&u.deltaY.value&&f()&&(l.duration=+e.animationDuration,l.status===`loosing`?(m(+e.headHeight,!0),t(`update:modelValue`,!0),g(()=>t(`refresh`))):m(0))};return j(()=>e.modelValue,t=>{l.duration=+e.animationDuration,t?m(+e.headHeight,!0):n.success||e.successText?y():m(0,!1)}),te(`touchmove`,T,{target:o}),()=>{var e;let t={transitionDuration:`${l.duration}ms`,transform:l.distance?`translate3d(0,${l.distance}px, 0)`:``};return k(`div`,{ref:a,class:Y()},[k(`div`,{ref:o,class:Y(`track`),style:t,onTouchstartPassive:C,onTouchend:E,onTouchcancel:E},[k(`div`,{class:Y(`head`),style:d()},[v()]),(e=n.default)==null?void 0:e.call(n)])])}}}),Pe=I(Ne),[Fe,Z,Q]=P(`search`),Ie=O({},u,{label:String,shape:N(`square`),leftIcon:N(`search`),clearable:re,actionText:String,background:String,showAction:Boolean}),Le=h({name:Fe,props:Ie,emits:[`blur`,`focus`,`clear`,`search`,`cancel`,`clickInput`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(e,{emit:t,slots:n,attrs:r}){let i=d(),a=w(),o=()=>{n.action||(t(`update:modelValue`,``),t(`cancel`))},s=n=>{n.keyCode===13&&(D(n),t(`search`,e.modelValue))},l=()=>e.id||`${i}-input`,p=()=>{if(n.label||e.label)return k(`label`,{class:Z(`label`),for:l(),"data-allow-mismatch":`attribute`},[n.label?n.label():e.label])},m=()=>{if(e.showAction){let t=e.actionText||Q(`cancel`);return k(`div`,{class:Z(`action`),role:`button`,tabindex:0,onClick:o},[n.action?n.action():t])}},h=()=>{var e;return(e=a.value)==null?void 0:e.blur()},g=()=>{var e;return(e=a.value)==null?void 0:e.focus()},_=e=>t(`blur`,e),y=e=>t(`focus`,e),b=e=>t(`clear`,e),x=e=>t(`clickInput`,e),S=e=>t(`clickLeftIcon`,e),C=e=>t(`clickRightIcon`,e),T=Object.keys(u),E=()=>{let i=O({},r,z(e,T),{id:l()});return k(f,v({ref:a,type:`search`,class:Z(`field`,{"with-message":i.errorMessage}),border:!1,labelAlign:`left`,onBlur:_,onFocus:y,onClear:b,onKeypress:s,onClickInput:x,onClickLeftIcon:S,onClickRightIcon:C,"onUpdate:modelValue":e=>t(`update:modelValue`,e)},i),z(n,[`left-icon`,`right-icon`]))};return c({focus:g,blur:h}),()=>{var t;return k(`div`,{class:Z({"show-action":e.showAction}),style:{background:e.background}},[(t=n.left)==null?void 0:t.call(n),k(`div`,{class:Z(`content`,e.shape)},[p(),E()]),m()])}}}),$=I(Le),Re={class:`switcher-body`},ze={key:0,class:`switcher-state`},Be={key:0,class:`current`},Ve=t(h({__name:`AccountSwitcher`,props:{show:{type:Boolean,default:!1},showModifiers:{}},emits:[`update:show`],setup(e){let t=m(e,`show`),n=w(``),r=w([]),i=w(!1),c=w(``),u=0,d,f=ce(()=>{var e;return(e=ue.me)==null?void 0:e.account});async function p(){let e=++u;i.value=!0,c.value=``;try{let{items:t}=await le.accounts(n.value.trim());e===u&&(r.value=t.filter(e=>{var t;return e.id!==((t=f.value)==null?void 0:t.id)}))}catch(t){e===u&&(c.value=t instanceof ae?t.message:`加载失败`)}finally{e===u&&(i.value=!1)}}j(t,e=>{e&&p()}),j(n,()=>{window.clearTimeout(d),d=window.setTimeout(p,300)});function h(e){ie(e),t.value=!1}function g(e){return e.device_count?e.online?e.listening?`监听中`:`已停止`:`离线`:`没有电脑`}function _(e){return e.online?e.listening?`tone-success`:`tone-warning`:`tone-muted`}return(e,u)=>(y(),F(V(a),{show:t.value,"onUpdate:show":u[2]||(u[2]=e=>t.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`switcher`},{default:A(()=>[L(`div`,Re,[u[3]||(u[3]=L(`h3`,null,`切换客服`,-1)),k(V($),{modelValue:n.value,"onUpdate:modelValue":u[0]||(u[0]=e=>n.value=e),shape:`round`,placeholder:`搜索客服姓名或账号`},null,8,[`modelValue`]),k(V(l),{inset:``},{default:A(()=>[k(V(o),{title:`我自己`,label:f.value?`${f.value.nickname||f.value.sn}`:``,icon:`user-o`,clickable:``,value:V(R)===void 0?`当前`:``,onClick:u[1]||(u[1]=e=>h(null))},null,8,[`label`,`value`])]),_:1}),i.value&&!r.value.length?(y(),H(`div`,ze,[k(V(s),{size:`20`})])):c.value?(y(),F(V(J),{key:1,image:`network`,description:c.value},null,8,[`description`])):r.value.length?(y(),F(V(l),{key:3,inset:``,class:`switcher-list`},{default:A(()=>[(y(!0),H(se,null,T(r.value,e=>(y(),F(V(o),{key:e.id,title:e.nickname||e.sn||`客服 #${e.id}`,label:e.sn,clickable:``,onClick:t=>h(e)},{value:A(()=>[L(`span`,{class:S(_(e))},ne(g(e)),3),V(R)===e.id?(y(),H(`span`,Be,`当前`)):fe(``,!0)]),_:2},1032,[`title`,`label`,`onClick`]))),128))]),_:1})):(y(),F(V(J),{key:2,image:`search`,description:`没有找到其他客服`}))])]),_:1},8,[`show`]))}}),[[`__scopeId`,`data-v-91cf283c`]]);export{Te as a,J as i,$ as n,Pe as r,Ve as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js new file mode 100644 index 000000000..441f01f3e --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,n as r,p as i,r as a,t as o}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n as s}from"./use-placeholder-D9ZemjXL.js";import{a as c,d as l,i as u,o as d,s as f,u as p}from"./format-2xch3ZbY.js";import{a as m,n as h,o as g,s as _,t as v}from"./cell-group-CEKvg8Wf.js";import{r as y,t as b}from"./field-Bs9V8Rb1.js";import{a as x,i as S,n as ee,r as C,t as w}from"./AccountSwitcher-DHX0QDeT.js";import{$ as te,A as ne,At as T,B as E,C as D,Ct as O,D as k,Dt as A,E as j,Et as M,F as N,Ft as P,G as re,Gt as ie,H as ae,Ht as F,I,It as oe,J as se,Jt as ce,Kt as L,Lt as le,M as ue,Mt as de,N as fe,O as pe,Ot as me,Pt as he,Q as ge,R as _e,S as ve,St as R,Tt as ye,U as be,Ut as z,V as xe,Vt as B,W as V,Wt as Se,X as H,Y as Ce,Yt as U,Z as W,_ as G,_t as K,bt as we,c as Te,et as Ee,ft as De,g as Oe,gt as q,ht as J,j as ke,jt as Ae,kt as je,l as Me,n as Ne,o as Pe,ot as Fe,p as Ie,q as Y,qt as X,s as Le,t as Re,ut as ze,v as Be,vt as Z,w as Ve,wt as He,xt as Q,y as Ue,yt as $,z as We}from"./index-C_mEe-tZ.js";var Ge=Array.isArray,Ke=e=>typeof e==`string`,qe=e=>typeof e==`object`&&!!e,Je=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ye=/\B([A-Z])/g,Xe=Je(e=>e.replace(Ye,`-$1`).toLowerCase());function Ze(e){if(Ge(e)){let t={};for(let n=0;ne.startsWith(`/*`)?``:e).split(Qe).forEach(e=>{if(e){let n=e.split($e);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function nt(e){if(!e)return``;if(Ke(e))return e;let t=``;for(let n in e){let r=e[n];if(Ke(r)||typeof r==`number`){let e=n.startsWith(`--`)?n:Xe(n);t+=`${e}:${r};`}}return t}function rt(e){let t=``;if(Ke(e))t=e;else if(Ge(e))for(let n=0;nt||!o&&at?i=We(u):r&&(i=We(r))}return u(),l}function ot(){let e=L([]),t=[];return T(()=>{e.value=[]}),[e,n=>(t[n]||(t[n]=t=>{e.value[n]=t}),t[n])]}function st(e,t){if(!te||!window.IntersectionObserver)return;let n=new IntersectionObserver(e=>{t(e[0].intersectionRatio>0)},{root:document.body}),r=()=>{e.value&&n.observe(e.value)},i=()=>{e.value&&n.unobserve(e.value)};Ae(i),je(i),_e(r)}var[ct,lt]=G(`sticky`),ut={zIndex:H,position:Ce(`top`),container:Object,offsetTop:Y(0),offsetBottom:Y(0)},dt=O({name:ct,props:ut,emits:[`scroll`,`change`],setup(e,{emit:t,slots:n}){let r=L(),i=re(r),a=ie({fixed:!1,width:0,height:0,transform:0}),o=L(!1),s=J(()=>D(e.position===`top`?e.offsetTop:e.offsetBottom)),c=J(()=>{if(o.value)return;let{fixed:e,height:t,width:n}=a;if(e)return{width:`${n}px`,height:`${t}px`}}),l=J(()=>{if(!a.fixed||o.value)return;let t=ge(ve(e.zIndex),{width:`${a.width}px`,height:`${a.height}px`,[e.position]:`${s.value}px`});return a.transform&&(t.transform=`translate3d(0, ${a.transform}px, 0)`),t}),u=e=>t(`scroll`,{scrollTop:e,isFixed:a.fixed}),d=()=>{if(!r.value||k(r))return;let{container:t,position:n}=e,i=V(r),o=j(window);if(a.width=i.width,a.height=i.height,n===`top`){if(t){let e=V(t),n=e.bottom-s.value-a.height;a.fixed=s.value>i.top&&e.bottom>0,a.transform=n<0?n:0}else a.fixed=s.value>i.top}else{let{clientHeight:e}=document.documentElement;if(t){let n=V(t),r=e-n.top-s.value-a.height;a.fixed=e-s.valuen.top,a.transform=r<0?-r:0}else a.fixed=e-s.valuea.fixed,e=>t(`change`,e)),xe(`scroll`,d,{target:i,passive:!0}),st(r,d),B([fe,ue],()=>{r.value&&!k(r)&&a.fixed&&(o.value=!0,A(()=>{let e=V(r);a.width=e.width,a.height=e.height,o.value=!1}))}),()=>{var e;return R(`div`,{ref:r,style:c.value},[R(`div`,{class:lt({fixed:a.fixed&&!o.value}),style:l.value},[(e=n.default)==null?void 0:e.call(n)])])}}}),ft=Oe(dt),[pt,mt]=G(`swipe`),ht={loop:W,width:H,height:H,vertical:Boolean,autoplay:Y(0),duration:Y(500),touchable:W,lazyRender:Boolean,initialSwipe:Y(0),indicatorColor:String,showIndicators:W,stopPropagation:W},gt=Symbol(pt),_t=O({name:pt,props:ht,emits:[`change`,`dragStart`,`dragEnd`],setup(e,{emit:n,slots:r}){let i=L(),a=L(),o=ie({rect:null,width:0,height:0,offset:0,active:0,swiping:!1}),s=!1,c=l(),{children:u,linkChildren:d}=E(gt),f=J(()=>u.length),p=J(()=>o[e.vertical?`height`:`width`]),m=J(()=>e.vertical?c.deltaY.value:c.deltaX.value),h=J(()=>o.rect?(e.vertical?o.rect.height:o.rect.width)-p.value*f.value:0),g=J(()=>p.value?Math.ceil(Math.abs(h.value)/p.value):f.value),v=J(()=>f.value*p.value),y=J(()=>(o.active+f.value)%f.value),b=J(()=>{let t=e.vertical?`vertical`:`horizontal`;return c.direction.value===t}),x=J(()=>{let t={transitionDuration:`${o.swiping?0:e.duration}ms`,transform:`translate${e.vertical?`Y`:`X`}(${+o.offset.toFixed(2)}px)`};if(p.value){let n=e.vertical?`height`:`width`,r=e.vertical?`width`:`height`;t[n]=`${v.value}px`,t[r]=e[r]?`${e[r]}px`:``}return t}),S=t=>{let{active:n}=o;return t?e.loop?Ue(n+t,-1,f.value):Ue(n+t,0,g.value):n},ee=(t,n=0)=>{let r=t*p.value;e.loop||(r=Math.min(r,-h.value));let i=n-r;return e.loop||(i=Ue(i,h.value,0)),i},C=({pace:t=0,offset:r=0,emitChange:i})=>{if(f.value<=1)return;let{active:a}=o,s=S(t),c=ee(s,r);if(e.loop){if(u[0]&&c!==h.value){let e=c0;u[f.value-1].setOffset(e?-v.value:0)}}o.active=s,o.offset=c,i&&s!==a&&n(`change`,y.value)},w=()=>{o.swiping=!0,o.active<=-1?C({pace:f.value}):o.active>=f.value&&C({pace:-f.value})},te=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:-1,emitChange:!0})})},ne=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:1,emitChange:!0})})},T,D=()=>clearTimeout(T),O=()=>{D(),+e.autoplay>0&&f.value>1&&(T=setTimeout(()=>{ne(),O()},+e.autoplay))},j=(t=+e.initialSwipe)=>{if(!i.value)return;let n=()=>{var n,r;if(!k(i)){let t={width:i.value.offsetWidth,height:i.value.offsetHeight};o.rect=t,o.width=+((n=e.width)==null?t.width:n),o.height=+((r=e.height)==null?t.height:r)}f.value&&(t=Math.min(f.value-1,t),t===-1&&(t=f.value-1)),o.active=t,o.swiping=!0,o.offset=ee(t),u.forEach(e=>{e.setOffset(0)}),O()};k(i)?A().then(n):n()},M=()=>j(o.active),N,P=t=>{!e.touchable||t.touches.length>1||(c.start(t),s=!1,N=Date.now(),D(),w())},re=t=>{e.touchable&&o.swiping&&(c.move(t),b.value&&(!e.loop&&(o.active===0&&m.value>0||o.active===f.value-1&&m.value<0)||(pe(t,e.stopPropagation),C({offset:m.value}),s||(n(`dragStart`,{index:y.value}),s=!0))))},F=()=>{if(!e.touchable||!o.swiping)return;let t=Date.now()-N,r=m.value/t;if((Math.abs(r)>.25||Math.abs(m.value)>p.value/2)&&b.value){let t=e.vertical?c.offsetY.value:c.offsetX.value,n=0;n=e.loop?t>0?m.value>0?-1:1:0:-Math[m.value>0?`ceil`:`floor`](m.value/p.value),C({pace:n,emitChange:!0})}else m.value&&C({pace:0});s=!1,o.swiping=!1,n(`dragEnd`,{index:y.value}),O()},oe=(t,n={})=>{w(),c.reset(),I(()=>{let r;r=e.loop&&t===f.value?o.active===0?0:t:t%f.value,n.immediate?I(()=>{o.swiping=!1}):o.swiping=!1,C({pace:r-o.active,emitChange:!0})})},se=(t,n)=>{let r=n===y.value,i=r?{backgroundColor:e.indicatorColor}:void 0;return R(`i`,{style:i,class:mt(`indicator`,{active:r})},null)},ce=()=>{if(r.indicator)return r.indicator({active:y.value,total:f.value});if(e.showIndicators&&f.value>1)return R(`div`,{class:mt(`indicators`,{vertical:e.vertical})},[Array(f.value).fill(``).map(se)])};return _({prev:te,next:ne,state:o,resize:M,swipeTo:oe}),d({size:p,props:e,count:f,activeIndicator:y}),B(()=>e.initialSwipe,e=>j(+e)),B(f,()=>j(o.active)),B(()=>e.autoplay,O),B([fe,ue,()=>e.width,()=>e.height],M),B(ae(),e=>{e===`visible`?O():D()}),de(j),me(()=>j(o.active)),t(()=>j(o.active)),Ae(D),je(D),xe(`touchmove`,re,{target:a}),()=>{var t;return R(`div`,{ref:i,class:mt()},[R(`div`,{ref:a,style:x.value,class:mt(`track`,{vertical:e.vertical}),onTouchstartPassive:P,onTouchend:F,onTouchcancel:F},[(t=r.default)==null?void 0:t.call(r)]),ce()])}}}),vt=Oe(_t),[yt,bt]=G(`tabs`),xt=O({name:yt,props:{count:se(Number),inited:Boolean,animated:Boolean,duration:se(H),swipeable:Boolean,lazyRender:Boolean,currentIndex:se(Number)},emits:[`change`],setup(e,{emit:t,slots:n}){let r=L(),i=e=>t(`change`,e),a=()=>{var t;let a=(t=n.default)==null?void 0:t.call(n);return e.animated||e.swipeable?R(vt,{ref:r,loop:!1,class:bt(`track`),duration:e.duration*1e3,touchable:e.swipeable,lazyRender:e.lazyRender,showIndicators:!1,onChange:i},{default:()=>[a]}):a},o=t=>{let n=r.value;n&&n.state.active!==t&&n.swipeTo(t,{immediate:!e.inited})};return B(()=>e.currentIndex,o),de(()=>{o(e.currentIndex)}),_({swipeRef:r}),()=>R(`div`,{class:bt(`content`,{animated:e.animated||e.swipeable})},[a()])}}),[St,Ct]=G(`tabs`),wt={type:Ce(`line`),color:String,border:Boolean,sticky:Boolean,shrink:Boolean,active:Y(0),duration:Y(.3),animated:Boolean,ellipsis:W,swipeable:Boolean,scrollspy:Boolean,offsetTop:Y(0),background:String,lazyRender:W,showHeader:W,lineWidth:H,lineHeight:H,beforeChange:Function,swipeThreshold:Y(5),titleActiveColor:String,titleInactiveColor:String},Tt=Symbol(St),Et=O({name:St,props:wt,emits:[`change`,`scroll`,`rendered`,`clickTab`,`update:active`],setup(e,{emit:r,slots:a}){let o,c,l,u,d,f=L(),p=L(),m=L(),h=L(),g=y(),v=re(f),[b,x]=ot(),{children:S,linkChildren:ee}=E(Tt),C=ie({inited:!1,position:``,lineStyle:{},currentIndex:-1}),w=J(()=>S.length>+e.swipeThreshold||!e.ellipsis||e.shrink),te=J(()=>({borderColor:e.color,background:e.background})),T=(e,t)=>{var n;return(n=e.name)==null?t:n},O=J(()=>{let e=S[C.currentIndex];if(e)return T(e,C.currentIndex)}),j=J(()=>D(e.offsetTop)),M=J(()=>e.sticky?j.value+o:0),N=t=>{let n=p.value,r=b.value;if(!w.value||!n||!r||!r[C.currentIndex])return;let i=r[C.currentIndex].$el,a=i.offsetLeft-(n.offsetWidth-i.offsetWidth)/2;u&&u(),u=it(n,a,t?0:+e.duration)},P=()=>{let t=C.inited;A(()=>{let n=b.value;if(!n||!n[C.currentIndex]||e.type!==`line`||k(f.value))return;let r=n[C.currentIndex].$el,{lineWidth:i,lineHeight:a}=e,o=r.offsetLeft+r.offsetWidth/2,s={width:Be(i),backgroundColor:e.color,transform:`translateX(${o}px) translateX(-50%)`};if(t&&(s.transitionDuration=`${e.duration}s`),Ee(a)){let e=Be(a);s.height=e,s.borderRadius=e}C.lineStyle=s})},ae=e=>{let t=e=0&&e{let i=ae(t);if(!Ee(i))return;let a=S[i],o=T(a,i),s=C.currentIndex!==null;C.currentIndex!==i&&(C.currentIndex=i,n||N(),P()),o!==e.active&&(r(`update:active`,o),s&&r(`change`,o,a.title)),l&&!e.scrollspy&&ne(Math.ceil(Ve(f.value)-j.value))},I=(e,t)=>{let n=S.findIndex((t,n)=>T(t,n)===e);F(n===-1?0:n,t)},oe=(t=!1)=>{if(e.scrollspy){let n=S[C.currentIndex].$el;if(n&&v.value){let r=Ve(n,v.value)-M.value;c=!0,d&&d(),d=at(v.value,r,t?0:+e.duration,()=>{c=!1})}}},se=(t,i,a)=>{let{title:o,disabled:c}=S[i],l=T(S[i],i);c||(s(e.beforeChange,{args:[l],done:()=>{F(i),oe()}}),n(t)),r(`clickTab`,{name:l,title:o,event:a,disabled:c})},ce=e=>{l=e.isFixed,r(`scroll`,e)},le=e=>{A(()=>{I(e),oe(!0)})},ue=()=>{for(let e=0;eM.value)return e===0?0:e-1}return S.length-1},de=()=>{if(e.scrollspy&&!c){let e=ue();F(e)}},pe=()=>{if(e.type===`line`&&S.length)return R(`div`,{class:Ct(`line`),style:C.lineStyle},null)},he=()=>{var t,n,r;let{type:o,border:s,sticky:c}=e,l=[R(`div`,{ref:c?void 0:m,class:[Ct(`wrap`),{[i]:o===`line`&&s}]},[R(`div`,{ref:p,role:`tablist`,class:Ct(`nav`,[o,{shrink:e.shrink,complete:w.value}]),style:te.value,"aria-orientation":`horizontal`},[(t=a[`nav-left`])==null?void 0:t.call(a),S.map(e=>e.renderTitle(se)),pe(),(n=a[`nav-right`])==null?void 0:n.call(a)])]),(r=a[`nav-bottom`])==null?void 0:r.call(a)];return c?R(`div`,{ref:m},[l]):l},ge=()=>{P(),A(()=>{var e,t;N(!0),(t=(e=h.value)==null?void 0:e.swipeRef.value)==null||t.resize()})};return B(()=>[e.color,e.duration,e.lineWidth,e.lineHeight],P),B(fe,ge),B(()=>e.active,e=>{e!==O.value&&I(e)}),B(()=>S.length,()=>{C.inited&&(I(e.active),P(),A(()=>{N(!0)}))}),_({resize:ge,scrollTo:le}),me(P),t(P),_e(()=>{I(e.active,!0),A(()=>{C.inited=!0,m.value&&(o=V(m.value).height),N(!0)})}),st(f,P),xe(`scroll`,de,{target:v,passive:!0}),ee({id:g,props:e,setLine:P,scrollable:w,onRendered:(e,t)=>r(`rendered`,e,t),currentName:O,setTitleRefs:x,scrollIntoView:N}),()=>R(`div`,{ref:f,class:Ct([e.type])},[e.showHeader?e.sticky?R(ft,{container:f.value,offsetTop:j.value,onScroll:ce},{default:()=>[he()]}):he():null,R(xt,{ref:h,count:S.length,inited:C.inited,animated:e.animated,duration:e.duration,swipeable:e.swipeable,lazyRender:e.lazyRender,currentIndex:C.currentIndex,onChange:F},{default:()=>{var e;return[(e=a.default)==null?void 0:e.call(a)]}})])}}),Dt=Symbol(),Ot=Symbol(),kt=()=>ye(Ot,null),At=e=>{let t=kt();oe(Dt,e),oe(Ot,J(()=>(t==null||t.value)&&e.value))},[jt,Mt]=G(`tab`),Nt=O({name:jt,props:{id:String,dot:Boolean,type:String,color:String,title:String,badge:H,shrink:Boolean,isActive:Boolean,disabled:Boolean,controls:String,scrollable:Boolean,activeColor:String,inactiveColor:String,showZeroBadge:W},setup(e,{slots:t}){let n=J(()=>{let t={},{type:n,color:r,disabled:i,isActive:a,activeColor:o,inactiveColor:s}=e;r&&n===`card`&&(t.borderColor=r,i||(a?t.backgroundColor=r:t.color=r));let c=a?o:s;return c&&(t.color=c),t}),r=()=>{let n=R(`span`,{class:Mt(`text`,{ellipsis:!e.scrollable})},[t.title?t.title():e.title]);return e.dot||Ee(e.badge)&&e.badge!==``?R(a,{dot:e.dot,content:e.badge,showZero:e.showZeroBadge},{default:()=>[n]}):n};return()=>R(`div`,{id:e.id,role:`tab`,class:[Mt([e.type,{grow:e.scrollable&&!e.shrink,shrink:e.shrink,active:e.isActive,disabled:e.disabled}])],style:n.value,tabindex:e.disabled?void 0:e.isActive?0:-1,"aria-selected":e.isActive,"aria-disabled":e.disabled||void 0,"aria-controls":e.controls,"data-allow-mismatch":`attribute`},[r()])}}),[Pt,Ft]=G(`swipe-item`),It=O({name:Pt,setup(e,{slots:t}){let n,r=ie({offset:0,inited:!1,mounted:!1}),{parent:i,index:a}=be(gt);if(!i)return;let o=J(()=>{let e={},{vertical:t}=i.props;return i.size.value&&(e[t?`height`:`width`]=`${i.size.value}px`),r.offset&&(e.transform=`translate${t?`Y`:`X`}(${r.offset}px)`),e}),s=J(()=>{let{loop:e,lazyRender:t}=i.props;if(!t||n)return!0;if(!r.mounted)return!1;let o=i.activeIndicator.value,s=i.count.value-1,c=o===0&&e?s:o-1,l=o===s&&e?0:o+1;return n=a.value===o||a.value===c||a.value===l,n});return de(()=>{A(()=>{r.mounted=!0})}),_({setOffset:e=>{r.offset=e}}),()=>{var e;return R(`div`,{class:Ft(),style:o.value},[s.value?(e=t.default)==null?void 0:e.call(t):null])}}}),Lt=Oe(It),[Rt,zt]=G(`tab`),Bt=ge({},e,{dot:Boolean,name:H,badge:H,title:String,disabled:Boolean,titleClass:null,titleStyle:[String,Object],showZeroBadge:W}),Vt=O({name:Rt,props:Bt,setup(e,{slots:t}){let n=y(),r=L(!1),i=He(),{parent:a,index:o}=be(Tt);if(!a)return;let s=()=>{var t;return(t=e.name)==null?o.value:t},c=()=>{r.value=!0,a.props.lazyRender&&A(()=>{a.onRendered(s(),e.title)})},l=J(()=>{let e=s()===a.currentName.value;return e&&!r.value&&c(),e}),u=L(``),d=L(``);F(()=>{let{titleClass:t,titleStyle:n}=e;u.value=t?rt(t):``,d.value=n&&typeof n!=`string`?nt(Ze(n)):n});let f=r=>R(Nt,M({key:n,id:`${a.id}-${o.value}`,ref:a.setTitleRefs(o.value),style:d.value,class:u.value,isActive:l.value,controls:n,scrollable:a.scrollable.value,activeColor:a.props.titleActiveColor,inactiveColor:a.props.titleInactiveColor,onClick:e=>r(i.proxy,o.value,e)},Fe(a.props,[`type`,`color`,`shrink`]),Fe(e,[`dot`,`badge`,`title`,`disabled`,`showZeroBadge`])),{title:t.title}),p=L(!l.value);return B(l,e=>{e?p.value=!1:I(()=>{p.value=!0})}),B(()=>e.title,()=>{a.setLine(),a.scrollIntoView()}),At(l),_({id:n,renderTitle:f}),()=>{var e;let i=`${a.id}-${o.value}`,{animated:s,swipeable:c,scrollspy:u,lazyRender:d}=a.props;if(!t.default&&!s)return;let f=u||l.value;if(s||c)return R(Lt,{id:n,role:`tabpanel`,class:zt(`panel-wrapper`,{inactive:p.value}),tabindex:l.value?0:-1,"aria-hidden":!l.value,"aria-labelledby":i,"data-allow-mismatch":`attribute`},{default:()=>{var e;return[R(`div`,{class:zt(`panel`)},[(e=t.default)==null?void 0:e.call(t)])]}});let m=r.value||u||!d?(e=t.default)==null?void 0:e.call(t):null;return Se(R(`div`,{id:n,role:`tabpanel`,class:zt(`panel`),tabindex:f?0:-1,"aria-labelledby":i,"data-allow-mismatch":`attribute`},[m]),[[ze,f]])}}}),Ht=Oe(Vt),Ut=Oe(Et),[Wt,Gt,Kt]=G(`list`),qt={error:Boolean,offset:Y(300),loading:Boolean,disabled:Boolean,finished:Boolean,scroller:Object,errorText:String,direction:Ce(`down`),loadingText:{type:String,default:``},finishedText:String,immediateCheck:W},Jt=O({name:Wt,props:qt,emits:[`load`,`update:error`,`update:loading`],setup(e,{emit:t,slots:n}){let r=L(e.loading),i=L(),a=L(),o=kt(),s=re(i),c=J(()=>e.scroller||s.value),l=()=>{A(()=>{if(r.value||e.finished||e.disabled||e.error||(o==null?void 0:o.value)===!1)return;let{direction:n}=e,s=+e.offset,l=V(c);if(!l.height||k(i))return;let u=!1,d=V(a);u=n===`up`?l.top-d.top<=s:d.bottom-l.bottom<=s,u&&(r.value=!0,t(`update:loading`,!0),t(`load`))})},u=()=>{if(e.finished){let t=n.finished?n.finished():e.finishedText;if(t)return R(`div`,{class:Gt(`finished-text`)},[t])}},d=()=>{t(`update:error`,!1),l()},f=()=>{if(e.error){let t=n.error?n.error():e.errorText;if(t)return R(`div`,{role:`button`,class:Gt(`error-text`),tabindex:0,onClick:d},[t])}},p=()=>{if(r.value&&!e.finished&&!e.disabled)return R(`div`,{class:Gt(`loading`)},[n.loading?n.loading():e.loadingText!=null&&R(g,{class:Gt(`loading-icon`)},{default:()=>[e.loadingText||Kt(`loading`)]})])};return B(()=>[e.loading,e.finished,e.error],l),o&&B(o,e=>{e&&l()}),he(()=>{r.value=e.loading}),de(()=>{e.immediateCheck&&l()}),_({check:l}),xe(`scroll`,l,{target:c,passive:!0}),()=>{var t;let o=(t=n.default)==null?void 0:t.call(n),s=R(`div`,{ref:a,class:Gt(`placeholder`)},null);return R(`div`,{ref:i,role:`feed`,class:Gt(),"aria-busy":r.value},[e.direction===`down`?o:s,p(),u(),f(),e.direction===`up`?o:s])}}}),Yt=Oe(Jt),Xt={class:`detail`},Zt={class:`detail-head`},Qt={class:`avatar`,"aria-hidden":`true`},$t={class:`detail-title`},en={class:`app-muted`},tn={key:0,class:`badge`},nn={class:`app-mono`},rn={key:0,class:`state state--blocked`},an={key:1,class:`note`},on={key:1,class:`state`},sn={key:1,class:`note`},cn=o(O({__name:`ConversationDetail`,props:{conversation:{},accountId:{},canAdd:{type:Boolean},canRemove:{type:Boolean},addCode:{},removeCode:{}},emits:[`updated`],setup(e,{emit:t}){let n=e,i=t,a=L(``),o=L(!1);B(()=>`${n.conversation.account}|${n.conversation.conversation}`,()=>{a.value=``});let s=J(()=>n.conversation.blacklist),l=J(()=>{var e;let t=(e=s.value)==null?void 0:e.source;return t===`mobile`?`手机控制台`:t===`desktop`?`客服电脑`:t||`—`});async function d(){if(!o.value){try{await f({title:`加入黑名单?`,message:`加入后,系统不再向「${n.conversation.name}」自动发送任何消息,只能由客服手动处理。`,confirmButtonText:`加入黑名单`,confirmButtonColor:`var(--app-danger)`})}catch{return}o.value=!0;try{let{entry:e}=await Me.block(n.conversation,a.value.trim(),n.accountId);i(`updated`,{...n.conversation,blocked:!0,blacklist:e}),x({message:`已加入黑名单,电脑同步后立即生效`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}async function p(){if(!o.value){try{await f({title:`移出黑名单?`,message:`移出后,系统会恢复向「${n.conversation.name}」自动回复。`,confirmButtonText:`移出`})}catch{return}o.value=!0;try{await Me.unblock(n.conversation.account,n.conversation.conversation,n.accountId),i(`updated`,{...n.conversation,blocked:!1,blacklist:null}),x({message:`已移出黑名单`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}return(t,n)=>(P(),$(`section`,Xt,[q(`header`,Zt,[q(`span`,Qt,U(X(u)(e.conversation.name)),1),q(`div`,$t,[q(`h2`,null,U(e.conversation.name),1),q(`p`,en,U(e.conversation.type_label),1)]),e.conversation.blocked?(P(),$(`span`,tn,[R(X(r),{name:`shield-o`}),n[1]||(n[1]=Q(` 黑名单`,-1))])):Z(``,!0)]),R(X(v),{inset:``,class:`group`},{default:z(()=>[R(X(h),{title:`企业微信账号`,value:e.conversation.account_name||`—`,label:e.conversation.account},null,8,[`value`,`label`]),R(X(h),{title:`最近消息`,value:e.conversation.last_message_at?X(c)(e.conversation.last_message_at):`—`},null,8,[`value`]),R(X(h),{title:`会话标识`},{label:z(()=>[q(`span`,nn,U(e.conversation.conversation),1)]),_:1})]),_:1}),e.conversation.blocked&&s.value?(P(),$(`div`,rn,[q(`h3`,null,[R(X(r),{name:`shield-o`}),n[2]||(n[2]=Q(` 已加入黑名单`,-1))]),n[8]||(n[8]=q(`p`,null,`系统不会向这个会话自动发送消息;电脑离线时,下次联网自动同步。`,-1)),q(`dl`,null,[q(`div`,null,[n[3]||(n[3]=q(`dt`,null,`原因`,-1)),q(`dd`,null,U(s.value.reason||`未填写`),1)]),q(`div`,null,[n[4]||(n[4]=q(`dt`,null,`操作人`,-1)),q(`dd`,null,U(s.value.created_by||`—`),1)]),q(`div`,null,[n[5]||(n[5]=q(`dt`,null,`时间`,-1)),q(`dd`,null,U(X(c)(s.value.created_at)),1)]),q(`div`,null,[n[6]||(n[6]=q(`dt`,null,`来源`,-1)),q(`dd`,null,U(l.value),1)])]),e.canRemove?(P(),K(X(m),{key:0,block:``,round:``,plain:``,type:`primary`,icon:`revoke`,loading:o.value,onClick:p},{default:z(()=>[...n[7]||(n[7]=[Q(` 移出黑名单 `,-1)])]),_:1},8,[`loading`])):(P(),$(`p`,an,`移出黑名单需要主管权限(`+U(e.removeCode)+`),请联系主管处理。`,1))])):(P(),$(`div`,on,[q(`h3`,null,[R(X(r),{name:`info-o`}),n[9]||(n[9]=Q(` 正常接收自动回复`,-1))]),n[11]||(n[11]=q(`p`,null,`客户投诉、需要人工跟进或不适合机器人回复的会话,可以加入黑名单。`,-1)),e.canAdd?(P(),$(De,{key:0},[R(X(v),{inset:``,class:`reason`},{default:z(()=>[R(X(b),{modelValue:a.value,"onUpdate:modelValue":n[0]||(n[0]=e=>a.value=e),label:`原因`,placeholder:`选填,例如:客户要求人工服务`,maxlength:`200`,"show-word-limit":``,rows:`2`,autosize:``,type:`textarea`},null,8,[`modelValue`])]),_:1}),R(X(m),{block:``,round:``,type:`danger`,icon:`shield-o`,loading:o.value,onClick:d},{default:z(()=>[...n[10]||(n[10]=[Q(` 加入黑名单 `,-1)])]),_:1},8,[`loading`])],64)):(P(),$(`p`,sn,`加入黑名单需要 ZYT 权限(`+U(e.addCode)+`),请联系管理员开通。`,1))]))]))}}),[[`__scopeId`,`data-v-70465908`]]),ln={class:`body`},un={class:`line`},dn={class:`name`},fn={class:`time`},pn={class:`line sub`},mn={class:`meta`},hn={key:0,class:`blocked`},gn=o(O({__name:`ConversationRow`,props:{item:{},active:{type:Boolean}},emits:[`select`],setup(e){let t=e,n=J(()=>t.item.type===`group`?`group`:t.item.type===`direct_wechat`?`wechat`:`wecom`);return(t,r)=>(P(),$(`button`,{type:`button`,class:ce([`row`,{active:e.active}]),onClick:r[0]||(r[0]=n=>t.$emit(`select`,e.item))},[q(`span`,{class:ce([`avatar`,`avatar--${n.value}`]),"aria-hidden":`true`},U(X(u)(e.item.name)),3),q(`span`,ln,[q(`span`,un,[q(`strong`,dn,U(e.item.name),1),q(`span`,fn,U(e.item.last_message_at?X(c)(e.item.last_message_at):``),1)]),q(`span`,pn,[q(`span`,mn,[Q(U(e.item.type_label),1),e.item.account_name?(P(),$(De,{key:0},[Q(` · `+U(e.item.account_name),1)],64)):Z(``,!0)]),e.item.blocked?(P(),$(`span`,hn,`已拉黑`)):Z(``,!0)])])],2))}}),[[`__scopeId`,`data-v-645c253b`]]),_n={class:`page`},vn={class:`list-pane`},yn={class:`toolbar`},bn={key:0,class:`detail-pane app-card`},xn=o(O({name:`ConversationsView`,__name:`ConversationsView`,setup(e){let t=Ie(),n=L(``),i=L(`all`),a=L([]),o=L(``),s=L(!1),c=L(!1),l=L(!1),u=L(``),f=L(!1),h=L(0),g=L(null),_=L(!1),v=L(!1),y=0,b=null,x,te=J(()=>{var e;return(e=Ne.me)==null?void 0:e.permission_codes}),ne=J(()=>Pe.value===void 0?`会话`:`${Le.value} 的会话`),T=e=>`${e.account}|${e.conversation}`;function E(){y+=1,b==null||b.abort(),b=null,a.value=[],o.value=``,s.value=!1,l.value=!1,u.value=``,c.value=!1,D()}async function D(){if(s.value)return;let e=y;b=new AbortController,c.value=!0;try{let r=await Me.conversations({accountId:Pe.value,q:n.value.trim(),scope:i.value,cursor:o.value,limit:30,signal:b.signal});if(e!==y)return;let c=new Set(a.value.map(T));a.value=a.value.concat(r.items.filter(e=>!c.has(T(e)))),o.value=r.next_cursor,s.value=!r.has_more,h.value=r.blocked_total,l.value=!1,t.value&&!g.value&&a.value.length&&(g.value=a.value[0])}catch(t){if(e!==y||t instanceof DOMException&&t.name===`AbortError`)return;l.value=!0,u.value=t instanceof Te?t.message:`加载失败`}finally{e===y&&(c.value=!1,f.value=!1)}}function O(){f.value=!0,E()}function k(e){g.value=e,t.value||(_.value=!0)}function A(e){var t;let n=a.value.findIndex(t=>T(t)===T(e)),r=n>=0?a.value[n].blocked:(t=g.value)==null?void 0:t.blocked;n>=0&&(i.value===`blocked`&&!e.blocked?a.value.splice(n,1):a.value.splice(n,1,e)),r!==e.blocked&&(h.value+=e.blocked?1:-1),g.value=e}return B(n,()=>{window.clearTimeout(x),x=window.setTimeout(E,350)}),B(i,E),B(Pe,()=>{g.value=null,_.value=!1,E()}),E(),(e,o)=>{var y,b,x,E,j,M,N,re;return P(),$(`div`,_n,[R(X(d),{title:ne.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},we({_:2},[(y=X(Re))!=null&&y.control_all?{name:`right`,fn:z(()=>[q(`button`,{type:`button`,class:`nav-action`,onClick:o[0]||(o[0]=e=>v.value=!0)},[R(X(r),{name:`friends-o`}),o[9]||(o[9]=Q(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),q(`div`,{class:ce(X(t)?`split`:`single`)},[q(`section`,vn,[q(`div`,yn,[R(X(ee),{modelValue:n.value,"onUpdate:modelValue":o[1]||(o[1]=e=>n.value=e),shape:`round`,placeholder:`搜索会话名称`,clearable:!0},null,8,[`modelValue`]),R(X(Ut),{active:i.value,"onUpdate:active":o[2]||(o[2]=e=>i.value=e),shrink:``,border:!1,class:`tabs`},{default:z(()=>[R(X(Ht),{name:`all`,title:`全部会话`}),R(X(Ht),{name:`blocked`,title:h.value?`黑名单 ${h.value}`:`黑名单`},null,8,[`title`])]),_:1},8,[`active`])]),R(X(C),{modelValue:f.value,"onUpdate:modelValue":o[6]||(o[6]=e=>f.value=e),class:`list-body`,onRefresh:O},{default:z(()=>[R(X(Yt),{loading:c.value,"onUpdate:loading":o[3]||(o[3]=e=>c.value=e),error:l.value,"onUpdate:error":o[4]||(o[4]=e=>l.value=e),finished:s.value,"error-text":`${u.value},点击重试`,"finished-text":a.value.length?`没有更多了`:``,"immediate-check":!1,onLoad:D},{default:z(()=>[(P(!0),$(De,null,le(a.value,e=>(P(),K(gn,{key:T(e),item:e,active:X(t)&&g.value!==null&&T(g.value)===T(e),onSelect:k},null,8,[`item`,`active`]))),128))]),_:1},8,[`loading`,`error`,`finished`,`error-text`,`finished-text`]),s.value&&!a.value.length&&!l.value?(P(),K(X(S),{key:0,image:n.value?`search`:`default`,description:n.value?`没有找到匹配的会话`:i.value===`blocked`?`黑名单是空的`:`还没有归档的会话:电脑开启聊天备份后会自动出现`},{default:z(()=>[n.value?(P(),K(X(m),{key:0,round:``,size:`small`,onClick:o[5]||(o[5]=e=>n.value=``)},{default:z(()=>[...o[10]||(o[10]=[Q(`清除搜索`,-1)])]),_:1})):Z(``,!0)]),_:1},8,[`image`,`description`])):Z(``,!0)]),_:1},8,[`modelValue`])]),X(t)?(P(),$(`section`,bn,[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((b=X(Re))!=null&&b.blacklist_add),"can-remove":!!((x=X(Re))!=null&&x.blacklist_remove),"add-code":(E=(j=te.value)==null?void 0:j.blacklist_add)==null?``:E,"remove-code":(M=(N=te.value)==null?void 0:N.blacklist_remove)==null?``:M,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):(P(),K(X(S),{key:1,image:`default`,description:`选择左侧的会话查看详情`}))])):Z(``,!0)],2),X(t)?Z(``,!0):(P(),K(X(p),{key:0,show:_.value,"onUpdate:show":o[7]||(o[7]=e=>_.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`sheet`},{default:z(()=>{var e,t,n,r,i,a;return[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((e=X(Re))!=null&&e.blacklist_add),"can-remove":!!((t=X(Re))!=null&&t.blacklist_remove),"add-code":(n=(r=te.value)==null?void 0:r.blacklist_add)==null?``:n,"remove-code":(i=(a=te.value)==null?void 0:a.blacklist_remove)==null?``:i,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):Z(``,!0)]}),_:1},8,[`show`])),(re=X(Re))!=null&&re.control_all?(P(),K(w,{key:1,show:v.value,"onUpdate:show":o[8]||(o[8]=e=>v.value=e)},null,8,[`show`])):Z(``,!0)])}}}),[[`__scopeId`,`data-v-cc7f80e6`]]);export{xn as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css new file mode 100644 index 000000000..464684d56 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css @@ -0,0 +1 @@ +.detail[data-v-70465908]{padding-bottom:16px}.detail-head[data-v-70465908]{align-items:center;gap:12px;padding:16px;display:flex}.avatar[data-v-70465908]{width:52px;height:52px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:14px;flex:none;place-items:center;font-size:22px;font-weight:600;display:grid}.detail-title[data-v-70465908]{flex:1;min-width:0}.detail-title h2[data-v-70465908]{text-overflow:ellipsis;white-space:nowrap;margin:0 0 4px;font-size:18px;overflow:hidden}.detail-title p[data-v-70465908]{margin:0;font-size:13px}.badge[data-v-70465908]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.group[data-v-70465908]{margin-bottom:12px}.state[data-v-70465908]{background:var(--app-surface);border:1px solid var(--app-border);border-radius:14px;margin:0 16px;padding:16px}.state--blocked[data-v-70465908]{border-color:var(--app-danger-soft);background:linear-gradient(0deg, var(--app-danger-soft), var(--app-danger-soft)), var(--app-surface)}.state h3[data-v-70465908]{align-items:center;gap:6px;margin:0 0 6px;font-size:15px;display:flex}.state--blocked h3[data-v-70465908]{color:var(--app-danger)}.state>p[data-v-70465908]{color:var(--app-text-2);margin:0 0 12px;font-size:13px;line-height:1.6}.state dl[data-v-70465908]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin:0 0 14px;display:grid}.state dt[data-v-70465908]{color:var(--app-muted);font-size:12px}.state dd[data-v-70465908]{word-break:break-all;margin:2px 0 0;font-size:14px}.reason[data-v-70465908]{margin:0 0 12px}.reason[data-v-70465908] .van-cell-group--inset{margin:0}.state[data-v-70465908] .van-cell-group--inset{margin:0 0 12px}.note[data-v-70465908]{color:var(--app-muted);margin:0;font-size:13px;line-height:1.6}.row[data-v-645c253b]{border:0;border-bottom:1px solid var(--app-border);background:var(--app-surface);width:100%;min-height:64px;color:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;padding:10px 16px;display:flex}.row[data-v-645c253b]:active,.row.active[data-v-645c253b]{background:var(--app-primary-soft)}.avatar[data-v-645c253b]{border-radius:12px;flex:none;place-items:center;width:42px;height:42px;font-size:17px;font-weight:600;display:grid}.avatar--wecom[data-v-645c253b]{color:#2f6fff;background:#2f6fff1f}.avatar--wechat[data-v-645c253b]{color:#0f9e69;background:#13b77a24}.avatar--group[data-v-645c253b]{color:#7a5cff;background:#8a6bff24}.body[data-v-645c253b]{flex:1;gap:4px;min-width:0;display:grid}.line[data-v-645c253b]{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.name[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.time[data-v-645c253b]{color:var(--app-muted);flex:none;font-size:12px}.sub[data-v-645c253b]{color:var(--app-muted);font-size:12px}.meta[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.blocked[data-v-645c253b]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;padding:1px 8px;font-weight:600}.page[data-v-cc7f80e6]{min-height:100%}.nav-action[data-v-cc7f80e6]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.toolbar[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top));z-index:2;background:var(--app-bg);position:-webkit-sticky;position:sticky}.toolbar[data-v-cc7f80e6] .van-search{background:0 0;padding-bottom:4px}.tabs[data-v-cc7f80e6]{--van-tabs-nav-background:transparent;padding:0 4px}.list-body[data-v-cc7f80e6]{min-height:60vh}.single .list-pane[data-v-cc7f80e6]{max-width:720px;margin:0 auto}.split[data-v-cc7f80e6]{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;max-width:1180px;margin:0 auto;padding:0 24px 24px;display:grid}.split .list-body[data-v-cc7f80e6]{border-radius:16px;overflow:clip}.detail-pane[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top) + 12px);min-height:320px;margin-top:12px;position:-webkit-sticky;position:sticky}.sheet[data-v-cc7f80e6]{max-height:88vh;padding-top:8px} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css new file mode 100644 index 000000000..617ba43f7 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css @@ -0,0 +1 @@ +.panel[data-v-bcd7f731]{padding:16px}.panel-head[data-v-bcd7f731]{align-items:flex-start;gap:12px;display:flex}.panel-icon[data-v-bcd7f731]{width:44px;height:44px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:12px;flex:none;place-items:center;font-size:22px;display:grid}.panel-title[data-v-bcd7f731]{flex:1;min-width:0}.panel-title h2[data-v-bcd7f731]{text-overflow:ellipsis;white-space:nowrap;margin:2px 0 4px;font-size:17px;font-weight:600;overflow:hidden}.panel-title p[data-v-bcd7f731]{margin:0;font-size:13px;line-height:1.45}.pill[data-v-bcd7f731]{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.pill .dot[data-v-bcd7f731]{background:currentColor;border-radius:50%;width:7px;height:7px}.pill--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.pill--success .dot[data-v-bcd7f731]{box-shadow:0 0 0 3px var(--app-success-soft);animation:2.2s ease-in-out infinite breathe-bcd7f731}.pill--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.pill--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.pill--muted[data-v-bcd7f731],.pill--primary[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}@keyframes breathe-bcd7f731{50%{opacity:.45}}.stats[data-v-bcd7f731]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0 0;display:grid}.stat[data-v-bcd7f731]{background:var(--app-surface-2);border:1px solid var(--app-border);border-radius:12px;padding:10px 12px}.stat dt[data-v-bcd7f731]{color:var(--app-muted);font-size:12px}.stat dd[data-v-bcd7f731]{margin:4px 0 0;font-size:15px;font-weight:600}.meta[data-v-bcd7f731]{margin:12px 0 0;font-size:12px}.progress[data-v-bcd7f731]{border-radius:12px;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45;display:flex}.progress--primary[data-v-bcd7f731]{color:var(--app-primary);background:var(--app-primary-soft)}.progress--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.progress--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.progress--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.progress--muted[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}.actions[data-v-bcd7f731]{margin-top:16px}.blocked[data-v-bcd7f731]{margin:10px 4px 0;font-size:13px;line-height:1.5}.page[data-v-8ead1f63]{min-height:100%}.page-body[data-v-8ead1f63]{min-height:calc(100vh - 96px);min-height:calc(100dvh - 96px)}.content[data-v-8ead1f63]{max-width:720px;margin:0 auto;padding:12px 16px 24px}.stack[data-v-8ead1f63]{gap:12px;display:grid}.skeleton-card[data-v-8ead1f63]{padding:16px 0}.empty-hint[data-v-8ead1f63]{max-width:280px;color:var(--app-muted);text-align:center;margin:0 auto;font-size:13px;line-height:1.6}.nav-action[data-v-8ead1f63]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.stale[data-v-8ead1f63]{margin:0 4px;font-size:12px}.split[data-v-8ead1f63]{grid-template-columns:minmax(240px,320px) minmax(0,1fr);align-items:start;gap:16px;max-width:1080px;margin:0 auto;padding:16px 24px 24px;display:grid}.split-list[data-v-8ead1f63]{gap:10px;display:grid}.device-row[data-v-8ead1f63]{text-align:left;width:100%;color:inherit;cursor:pointer;align-items:center;gap:12px;padding:12px 14px;display:flex}.device-row.active[data-v-8ead1f63]{border-color:var(--app-primary);box-shadow:0 0 0 2px var(--app-primary-soft)}.device-row-icon[data-v-8ead1f63]{color:var(--app-primary);font-size:20px}.device-row-text[data-v-8ead1f63]{gap:2px;min-width:0;display:grid}.device-row-text strong[data-v-8ead1f63]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;overflow:hidden}.device-row-text small[data-v-8ead1f63]{font-size:12px} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js new file mode 100644 index 000000000..ae83f281b --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n,o as r,r as i,s as a,t as o}from"./format-2xch3ZbY.js";import{a as s,o as c}from"./cell-group-CEKvg8Wf.js";import{a as l,i as u,r as d,t as f}from"./AccountSwitcher-DHX0QDeT.js";import{Ct as p,Et as m,Ft as h,Jt as g,Kt as _,Lt as v,St as y,Ut as b,Vt as x,X as S,Y as C,Yt as w,Z as T,_ as E,_t as D,bt as O,c as k,f as A,ft as j,g as M,gt as N,ht as P,kt as F,l as I,n as L,o as R,p as z,q as B,qt as V,s as ee,t as H,v as U,vt as W,x as te,xt as G,yt as K}from"./index-C_mEe-tZ.js";var[ne,re]=E(`skeleton-title`),ie=p({name:ne,props:{round:Boolean,titleWidth:S},setup(e){return()=>y(`h3`,{class:re([{round:e.round}]),style:{width:U(e.titleWidth)}},null)}}),q=M(ie),[J,Y]=E(`skeleton-avatar`),ae={avatarSize:S,avatarShape:C(`round`)},oe=p({name:J,props:ae,setup(e){return()=>y(`div`,{class:Y([e.avatarShape]),style:te(e.avatarSize)},null)}}),se=M(oe),X=`100%`,ce={round:Boolean,rowWidth:{type:S,default:X}},[le,ue]=E(`skeleton-paragraph`),de=p({name:le,props:ce,setup(e){return()=>y(`div`,{class:ue([{round:e.round}]),style:{width:e.rowWidth}},null)}}),fe=M(de),[pe,Z]=E(`skeleton`),me=`60%`,he={row:B(0),round:Boolean,title:Boolean,titleWidth:S,avatar:Boolean,avatarSize:S,avatarShape:C(`round`),loading:T,animate:T,rowWidth:{type:[Number,String,Array],default:X}},ge=p({name:pe,inheritAttrs:!1,props:he,setup(e,{slots:t,attrs:n}){let r=()=>{if(e.avatar)return y(se,{avatarShape:e.avatarShape,avatarSize:e.avatarSize},null)},i=()=>{if(e.title)return y(q,{round:e.round,titleWidth:e.titleWidth},null)},a=t=>{let{rowWidth:n}=e;return n===`100%`&&t===e.row-1?me:Array.isArray(n)?n[t]:n},o=()=>Array(+e.row).fill(``).map((t,n)=>y(fe,{key:n,round:e.round,rowWidth:U(a(n))},null)),s=()=>t.template?t.template():y(j,null,[r(),y(`div`,{class:Z(`content`)},[i(),o()])]);return()=>{var r;return e.loading?y(`div`,m({class:Z({animate:e.animate,round:e.round})},n),[s()]):(r=t.default)==null?void 0:r.call(t)}}}),_e=M(ge),ve={class:`panel-head`},ye={class:`panel-icon`,"aria-hidden":`true`},be={class:`panel-title`},xe={class:`app-muted`},Se={class:`stats`},Ce={class:`stat`},we={class:`stat`},Te={class:`stat`},Ee={class:`stat`},De={class:`meta app-muted`},Oe={key:0},ke={key:1},Ae={class:`actions`},je={key:0,class:`blocked app-muted`},Q=t(p({__name:`DevicePanel`,props:{device:{},accountId:{},canControl:{type:Boolean},controlCode:{}},emits:[`changed`],setup(t,{emit:r}){let u=t,d=r,f=_(!1),p=_(null),m,v=0,S=P(()=>n(u.device)),C=P(()=>u.device.status),T=P(()=>{var e;return!!((e=C.value)!=null&&e.listening)}),E=P(()=>T.value?`stop_listening`:`start_listening`),O=P(()=>p.value?o(p.value):null),A=P(()=>{var e;return f.value||!!((e=O.value)!=null&&e.busy)}),j=P(()=>u.device.online?u.device.remote_capable?u.canControl?``:`你的 ZYT 账号没有远程启停权限(${u.controlCode}),请联系管理员开通。`:`电脑上的客服软件版本过低,升级到最新版后才能远程控制。`:`电脑离线:客服软件没有运行,无法远程拉起。请到电脑上打开软件。`);function M(){m!==void 0&&window.clearTimeout(m),m=void 0}async function L(){let e=p.value;if(e&&!e.final){try{let{command:t}=await I.command(e.id);if(p.value=t,t.final){d(`changed`);let e=o(t).text;l({message:e,type:t.status===`succeeded`?`success`:`fail`});return}}catch(e){if(e instanceof k&&e.status===404)return}Date.now()u.device.last_command,e=>{if(!e)return;if(p.value&&p.value.id===e.id){!p.value.final&&e.final&&(p.value=e);return}let t=e.expires_at*1e3>Date.now();!e.final&&t?R(e):!p.value&&e.final&&t&&(p.value=e)},{immediate:!0}),F(M);async function z(){if(A.value||j.value)return;let e=E.value===`stop_listening`;try{await a({title:e?`停止自动回复?`:`启动自动回复?`,message:e?`「${u.device.device_name||`这台电脑`}」将停止监听,新消息不再自动回复。`:`「${u.device.device_name||`这台电脑`}」将开始监听,并按电脑上的设置自动回复新消息。`,confirmButtonText:e?`停止`:`启动`,confirmButtonColor:e?`var(--app-danger)`:`var(--app-primary)`})}catch{return}f.value=!0;try{let{command:e}=await I.sendCommand(u.device.device_id,E.value,u.accountId);R(e)}catch(e){l({message:e instanceof k?e.message:`指令发送失败,请重试`,type:`fail`}),d(`changed`)}finally{f.value=!1}}return(n,r)=>{var a,o,l,u,d;return h(),K(`article`,{class:g([`panel app-card`,`panel--${S.value.tone}`])},[N(`header`,ve,[N(`div`,ye,[y(V(e),{name:`desktop-o`})]),N(`div`,be,[N(`h2`,null,w(t.device.device_name||`未命名电脑`),1),N(`p`,xe,w(S.value.hint),1)]),N(`span`,{class:g([`pill`,`pill--${S.value.tone}`])},[r[0]||(r[0]=N(`span`,{class:`dot`,"aria-hidden":`true`},null,-1)),G(` `+w(S.value.label),1)],2)]),N(`dl`,Se,[N(`div`,Ce,[r[1]||(r[1]=N(`dt`,null,`自动回复`,-1)),N(`dd`,{class:g(T.value?`tone-success`:`tone-muted`)},w(C.value?T.value?`监听中`:`已停止`:`—`),3)]),N(`div`,we,[r[2]||(r[2]=N(`dt`,null,`企业微信`,-1)),N(`dd`,{class:g((a=C.value)!=null&&a.wecom_connected?`tone-success`:`tone-muted`)},w(C.value?C.value.wecom_connected?`已连接`:`未连接`:`—`),3)]),N(`div`,Te,[r[3]||(r[3]=N(`dt`,null,`回复通道`,-1)),N(`dd`,null,w(((o=C.value)==null?void 0:o.transport_label)||`—`),1)]),N(`div`,Ee,[r[4]||(r[4]=N(`dt`,null,`待处理`,-1)),N(`dd`,{class:g((((l=C.value)==null?void 0:l.pending_count)||0)>0?`tone-warning`:``)},w(C.value?`${C.value.pending_count} 条`:`—`),3)])]),N(`p`,De,[N(`span`,null,`版本 `+w(t.device.app_version||`未知`),1),t.device.online&&((u=C.value)==null?void 0:u.reported_seconds)!==null&&((d=C.value)==null?void 0:d.reported_seconds)!==void 0?(h(),K(`span`,Oe,` · 状态 `+w(V(i)(C.value.reported_seconds))+`前上报 `,1)):!t.device.online&&t.device.last_seen_seconds!==null?(h(),K(`span`,ke,` · `+w(V(i)(t.device.last_seen_seconds))+`前在线 `,1)):W(``,!0)]),O.value?(h(),K(`div`,{key:0,class:g([`progress`,`progress--${O.value.tone}`]),role:`status`,"aria-live":`polite`},[O.value.busy?(h(),D(V(c),{key:0,size:`16`})):(h(),D(V(e),{key:1,name:O.value.tone===`success`?`passed`:`info-o`},null,8,[`name`])),N(`span`,null,w(O.value.text),1)],2)):W(``,!0),N(`div`,Ae,[y(V(s),{block:``,round:``,type:T.value?`danger`:`primary`,plain:T.value,icon:T.value?`pause-circle-o`:`play-circle-o`,disabled:!!j.value,loading:A.value,"loading-text":f.value?`正在下发…`:`等待电脑执行…`,onClick:z},{default:b(()=>[G(w(T.value?`停止自动回复`:`启动自动回复`),1)]),_:1},8,[`type`,`plain`,`icon`,`disabled`,`loading`,`loading-text`]),j.value?(h(),K(`p`,je,w(j.value),1)):W(``,!0)])],2)}}}),[[`__scopeId`,`data-v-bcd7f731`]]),Me={class:`page`},Ne={key:0,class:`content`},Pe={class:`app-card skeleton-card`},Fe={key:1,class:`content`},Ie={key:2,class:`content`},Le={class:`empty-hint`},$={key:3,class:`split`},Re={class:`split-list`,"aria-label":`电脑列表`},ze=[`onClick`],Be={class:`device-row-text`},Ve={key:0,class:`stale app-muted`},He={class:`split-detail`},Ue={key:4,class:`content stack`},We={key:0,class:`stale app-muted`},Ge=t(p({name:`DevicesView`,__name:`DevicesView`,setup(t){let i=z(),a=_(null),o=_(!1),c=_(!1),l=_(``),p=_(``),m=_(!1),S=0,C=P(()=>{var e,t;return(e=(t=a.value)==null?void 0:t.items)==null?[]:e}),T=P(()=>{var e,t;return(e=(t=C.value.find(e=>e.device_id===p.value))==null?C.value[0]:t)==null?null:e}),E=P(()=>{var e;return!!((e=H.value)!=null&&e.control)}),M=P(()=>{var e,t;return(e=(t=L.me)==null?void 0:t.permission_codes.control)==null?`wecom_rpa.remote/control`:e}),F=P(()=>R.value===void 0?`我的电脑`:`${ee.value} 的电脑`);async function B(){let e=++S;a.value||(o.value=!0);try{let t=await I.desktops(R.value);if(e!==S)return;a.value=t,l.value=``}catch(t){if(e!==S)return;l.value=t instanceof k?t.message:`加载失败,请重试`}finally{e===S&&(o.value=!1,c.value=!1)}}async function U(){c.value=!0,await B()}return x(R,()=>{a.value=null,p.value=``,B()},{immediate:!0}),A(B,5e3),(t,_)=>{var x,S;return h(),K(`div`,Me,[y(V(r),{title:F.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},O({_:2},[(x=V(H))!=null&&x.control_all?{name:`right`,fn:b(()=>[N(`button`,{type:`button`,class:`nav-action`,onClick:_[0]||(_[0]=e=>m.value=!0)},[y(V(e),{name:`friends-o`}),_[3]||(_[3]=G(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),y(V(d),{modelValue:c.value,"onUpdate:modelValue":_[1]||(_[1]=e=>c.value=e),class:`page-body`,onRefresh:U},{default:b(()=>[o.value&&!a.value?(h(),K(`div`,Ne,[N(`div`,Pe,[y(V(_e),{title:``,row:4})])])):l.value&&!a.value?(h(),K(`div`,Fe,[y(V(u),{image:`network`,description:l.value},{default:b(()=>[y(V(s),{round:``,type:`primary`,size:`small`,onClick:B},{default:b(()=>[..._[4]||(_[4]=[G(`重新加载`,-1)])]),_:1})]),_:1},8,[`description`])])):C.value.length?V(i)?(h(),K(`div`,$,[N(`aside`,Re,[(h(!0),K(j,null,v(C.value,t=>{var r;return h(),K(`button`,{key:t.device_id,type:`button`,class:g([`device-row app-card`,{active:((r=T.value)==null?void 0:r.device_id)===t.device_id}]),onClick:e=>p.value=t.device_id},[y(V(e),{name:`desktop-o`,class:`device-row-icon`}),N(`span`,Be,[N(`strong`,null,w(t.device_name||`未命名电脑`),1),N(`small`,{class:g(`tone-${V(n)(t).tone}`)},w(V(n)(t).label),3)])],10,ze)}),128)),l.value?(h(),K(`p`,Ve,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0)]),N(`section`,He,[T.value?(h(),D(Q,{key:T.value.device_id,device:T.value,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`])):W(``,!0)])])):(h(),K(`div`,Ue,[l.value?(h(),K(`p`,We,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0),(h(!0),K(j,null,v(C.value,e=>(h(),D(Q,{key:e.device_id,device:e,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`]))),128))])):(h(),K(`div`,Ie,[y(V(u),{image:`default`,description:`还没有登录过企微客服助手的电脑`},{default:b(()=>[N(`p`,Le,` 在电脑上打开企微客服助手,用`+w(V(R)===void 0?`同一个`:`该客服的`)+` ZYT 账号登录后,这里会显示电脑状态。 `,1)]),_:1})]))]),_:1},8,[`modelValue`]),(S=V(H))!=null&&S.control_all?(h(),D(f,{key:0,show:m.value,"onUpdate:show":_[2]||(_[2]=e=>m.value=e)},null,8,[`show`])):W(``,!0)])}}}),[[`__scopeId`,`data-v-8ead1f63`]]);export{Ge as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js new file mode 100644 index 000000000..63630d4bd --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js @@ -0,0 +1 @@ +import{c as e,m as t,n,t as r}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{a as i,s as a,t as o}from"./cell-group-CEKvg8Wf.js";import{t as s}from"./field-Bs9V8Rb1.js";import{B as c,Ct as l,Ft as u,Gt as d,I as f,Kt as p,O as m,R as h,St as g,Ut as _,V as v,Vt as y,W as b,Wt as x,X as S,Yt as C,Z as w,_ as T,_t as E,c as D,d as O,et as k,g as A,gt as j,ht as M,l as N,q as P,qt as F,r as I,u as L,ut as R,vt as z,xt as B,yt as V,z as H}from"./index-C_mEe-tZ.js";var[U,W]=T(`form`),G=l({name:U,props:{colon:Boolean,disabled:Boolean,readonly:Boolean,required:[Boolean,String],showError:Boolean,labelWidth:S,labelAlign:String,inputAlign:String,scrollToError:Boolean,scrollToErrorPosition:String,validateFirst:Boolean,submitOnEnter:w,showErrorMessage:w,errorMessageAlign:String,validateTrigger:{type:[String,Array],default:`onBlur`}},emits:[`submit`,`failed`],setup(e,{emit:n,slots:r}){let{children:i,linkChildren:o}=c(t),s=e=>e?i.filter(t=>e.includes(t.name)):i,l=e=>new Promise((t,n)=>{let r=[];s(e).reduce((e,t)=>e.then(()=>{if(!r.length)return t.validate().then(e=>{e&&r.push(e)})}),Promise.resolve()).then(()=>{r.length?n(r):t()})}),u=e=>new Promise((t,n)=>{let r=s(e);Promise.all(r.map(e=>e.validate())).then(e=>{e=e.filter(Boolean),e.length?n(e):t()})}),d=e=>{let t=i.find(t=>t.name===e);return t?new Promise((e,n)=>{t.validate().then(t=>{t?n(t):e()})}):Promise.reject()},f=t=>typeof t==`string`?d(t):e.validateFirst?l(t):u(t),p=e=>{typeof e==`string`&&(e=[e]),s(e).forEach(e=>{e.resetValidation()})},h=()=>i.reduce((e,t)=>(e[t.name]=t.getValidationStatus(),e),{}),_=(e,t)=>{i.some(n=>n.name===e&&(n.$el.scrollIntoView(t),!0))},v=()=>i.reduce((e,t)=>(t.name!==void 0&&(e[t.name]=t.formValue.value),e),{}),y=()=>{let t=v();f().then(()=>n(`submit`,t)).catch(r=>{n(`failed`,{values:t,errors:r});let{scrollToError:i,scrollToErrorPosition:a}=e;i&&r[0].name&&_(r[0].name,a?{block:a}:void 0)})},b=e=>{m(e),y()};return o({props:e}),a({submit:y,validate:f,getValues:v,scrollToField:_,resetValidation:p,getValidationStatus:h}),()=>{var e;return g(`form`,{class:W(),onSubmit:b},[(e=r.default)==null?void 0:e.call(r)])}}}),K=A(G),[q,J]=T(`notice-bar`),Y={text:String,mode:String,color:String,delay:P(1),speed:P(60),leftIcon:String,wrapable:Boolean,background:String,scrollable:{type:Boolean,default:null}},X=l({name:q,props:Y,emits:[`close`,`replay`],setup(t,{emit:r,slots:i}){let o=0,s=0,c,l=p(),u=p(),m=d({show:!0,offset:0,duration:0}),_=()=>{if(i[`left-icon`])return i[`left-icon`]();if(t.leftIcon)return g(n,{class:J(`left-icon`),name:t.leftIcon},null)},S=()=>{if(t.mode===`closeable`)return`cross`;if(t.mode===`link`)return`arrow`},C=e=>{t.mode===`closeable`&&(m.show=!1,r(`close`,e))},w=()=>{if(i[`right-icon`])return i[`right-icon`]();let e=S();if(e)return g(n,{name:e,class:J(`right-icon`),onClick:C},null)},T=()=>{m.offset=o,m.duration=0,H(()=>{f(()=>{m.offset=-s,m.duration=(s+o)/+t.speed,r(`replay`)})})},E=()=>{let e=t.scrollable===!1&&!t.wrapable,n={transform:m.offset?`translateX(${m.offset}px)`:``,transitionDuration:`${m.duration}s`};return g(`div`,{ref:l,role:`marquee`,class:J(`wrap`)},[g(`div`,{ref:u,style:n,class:[J(`content`),{"van-ellipsis":e}],onTransitionend:T},[i.default?i.default():t.text])])},D=()=>{let{delay:e,speed:n,scrollable:r}=t,i=k(e)?e*1e3:0;o=0,s=0,m.offset=0,m.duration=0,clearTimeout(c),c=setTimeout(()=>{if(!l.value||!u.value||r===!1)return;let e=b(l).width,t=b(u).width;(r||t>e)&&f(()=>{o=e,s=t,m.offset=-s,m.duration=s/+n})},i)};return e(D),h(D),v(`pageshow`,D),a({reset:D}),y(()=>[t.text,t.scrollable],D),()=>{let{color:e,wrapable:n,background:r}=t;return x(g(`div`,{role:`alert`,class:J({wrapable:n}),style:{color:e,background:r}},[_(),E(),w()]),[[R,m.show]])}}}),Z=A(X),Q=`/m/assets/brand-mark-CPUQS9Am.png`,$={class:`login`},ee={class:`login-card`},te={class:`login-head`},ne=[`src`],re={key:0,class:`login-error`,role:`alert`},ie={class:`login-actions`},ae=r(l({__name:`LoginView`,setup(e){let t=L(),n=O(),r=p(``),a=p(``),c=p(!1),l=p(``),d=M(()=>t.query.expired===`1`);function f(){let e=navigator.userAgent,t=/iPad/.test(e)?`iPad`:/iPhone/.test(e)?`iPhone`:/HarmonyOS/i.test(e)?`鸿蒙`:/Android/.test(e)?`Android`:/Windows/.test(e)?`Windows`:/Mac OS/.test(e)?`Mac`:`浏览器`,n=/wxwork/i.test(e)?`企业微信`:/MicroMessenger/i.test(e)?`微信`:``;return n?`${t} · ${n}`:t}function m(){let e=t.query.redirect;return typeof e==`string`&&e.startsWith(`/`)&&!e.startsWith(`//`)&&!e.startsWith(`/login`)?e:`/desktops`}async function h(){if(!c.value){l.value=``,c.value=!0;try{let e=await N.login(r.value.trim(),a.value,f());a.value=``,I(e.access_token,e.me),await n.replace(m())}catch(e){e instanceof D?(l.value=e.message,e.code===`invalid_credentials`&&(a.value=``)):l.value=`登录失败,请稍后重试`}finally{c.value=!1}}}return(e,t)=>(u(),V(`main`,$,[j(`section`,ee,[j(`header`,te,[j(`img`,{class:`login-mark`,src:F(Q),alt:`甄养堂`,width:`72`,height:`72`},null,8,ne),t[2]||(t[2]=j(`h1`,null,`企微客服助手`,-1)),t[3]||(t[3]=j(`p`,null,`手机控制台 · 使用 ZYT 账号登录`,-1))]),d.value?(u(),E(F(Z),{key:0,class:`login-notice`,"left-icon":`info-o`,scrollable:!1,wrapable:``,text:`登录已失效或已在管理端被下线,请重新登录。`})):z(``,!0),g(F(K),{class:`login-form`,onSubmit:h},{default:_(()=>[g(F(o),{inset:``},{default:_(()=>[g(F(s),{modelValue:r.value,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),name:`account`,label:`账号`,placeholder:`ZYT 登录账号`,autocomplete:`username`,autocapitalize:`off`,spellcheck:!1,enterkeyhint:`next`,clearable:``,rules:[{required:!0,message:`请输入账号`}]},null,8,[`modelValue`]),g(F(s),{modelValue:a.value,"onUpdate:modelValue":t[1]||(t[1]=e=>a.value=e),type:`password`,name:`password`,label:`密码`,placeholder:`ZYT 登录密码`,autocomplete:`current-password`,enterkeyhint:`go`,rules:[{required:!0,message:`请输入密码`}]},null,8,[`modelValue`])]),_:1}),l.value?(u(),V(`p`,re,C(l.value),1)):z(``,!0),j(`div`,ie,[g(F(i),{round:``,block:``,type:`primary`,"native-type":`submit`,loading:c.value,"loading-text":`正在登录…`},{default:_(()=>[...t[4]||(t[4]=[B(` 登录 `,-1)])]),_:1},8,[`loading`])])]),_:1}),t[5]||(t[5]=j(`p`,{class:`login-foot`},`密码只用于向 ZYT 验证身份,不会保存在手机上。`,-1))])]))}}),[[`__scopeId`,`data-v-004201f6`]]);export{ae as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css new file mode 100644 index 000000000..54f80a155 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css @@ -0,0 +1 @@ +.login[data-v-004201f6]{min-height:100vh;min-height:100dvh;padding:calc(env(safe-area-inset-top) + 24px) 16px calc(env(safe-area-inset-bottom) + 24px);background:radial-gradient(120% 60% at 50% 0%, var(--app-primary-soft), transparent 70%), var(--app-bg);justify-content:center;align-items:center;display:flex}.login-card[data-v-004201f6]{width:100%;max-width:420px}.login-head[data-v-004201f6]{text-align:center;margin-bottom:24px}.login-mark[data-v-004201f6]{box-shadow:var(--app-shadow);border-radius:20px}.login-head h1[data-v-004201f6]{letter-spacing:.5px;margin:16px 0 6px;font-size:24px;font-weight:700}.login-head p[data-v-004201f6]{color:var(--app-muted);margin:0;font-size:14px}.login-notice[data-v-004201f6]{border-radius:12px;margin:0 16px 12px}.login-form[data-v-004201f6] .van-cell-group--inset,.login-form[data-v-004201f6]{margin:0}.login-error[data-v-004201f6]{color:var(--app-danger);margin:12px 16px 0;font-size:14px;line-height:1.5}.login-actions[data-v-004201f6]{margin:20px 16px 0}.login-foot[data-v-004201f6]{text-align:center;color:var(--app-muted);margin:16px 16px 0;font-size:12px} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css new file mode 100644 index 000000000..5045f6a8e --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css @@ -0,0 +1 @@ +.content[data-v-1710faf3]{max-width:720px;margin:0 auto;padding:12px 0 32px}.profile[data-v-1710faf3]{align-items:center;gap:14px;margin:0 16px;padding:16px;display:flex}.avatar[data-v-1710faf3]{color:#fff;background:linear-gradient(135deg,#2f6fff,#5a65ff);border-radius:50%;flex:none;place-items:center;width:54px;height:54px;font-size:22px;font-weight:700;display:grid}.profile-text[data-v-1710faf3]{flex:1;min-width:0}.profile-text h2[data-v-1710faf3]{margin:0 0 4px;font-size:18px}.profile-text p[data-v-1710faf3]{margin:0;font-size:13px}.root[data-v-1710faf3]{color:var(--app-warning);background:var(--app-warning-soft);border-radius:999px;flex:none;padding:4px 10px;font-size:12px;font-weight:600}.section-title[data-v-1710faf3]{color:var(--app-muted);margin:24px 32px 8px;font-size:13px;font-weight:600}.hint[data-v-1710faf3]{margin:8px 32px 0;font-size:12px;line-height:1.6}.logout[data-v-1710faf3]{margin:32px 16px 0} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js new file mode 100644 index 000000000..ee886d01c --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{a as n,i as r,o as i,s as a}from"./format-2xch3ZbY.js";import{a as o,n as s,t as c}from"./cell-group-CEKvg8Wf.js";import{Ct as l,Ft as u,Jt as d,Kt as f,Lt as p,St as m,Ut as h,Yt as g,_t as _,d as v,ft as y,gt as b,ht as x,i as S,n as C,qt as w,vt as T,xt as E,yt as D}from"./index-C_mEe-tZ.js";var O={class:`page`},k={key:0,class:`content`},A={class:`profile app-card`},j={class:`avatar`,"aria-hidden":`true`},M={class:`profile-text`},N={class:`app-muted`},P={key:0,class:`root`},F={class:`hint app-muted`},I={class:`logout`},L=t(l({__name:`MeView`,setup(t){let l=v(),L=f(!1),R=x(()=>C.me),z=x(()=>{let e=R.value;if(!e)return[];let{permissions:t,permission_codes:n}=e;return[{title:`查看本人电脑与会话`,code:`登录即可`,granted:!0},{title:`远程启停本人电脑`,code:n.control,granted:t.control},{title:`查看并控制所有客服电脑`,code:n.control_all,granted:t.control_all},{title:`会话加入黑名单`,code:n.blacklist_add,granted:t.blacklist_add},{title:`会话移出黑名单`,code:n.blacklist_remove,granted:t.blacklist_remove}]}),B=x(()=>{var e;let t=(e=R.value)==null?void 0:e.session.expires_at;return t?new Date(t*1e3).toISOString():``}),V=/iPhone|iPad|iPod/.test(navigator.userAgent);async function H(){try{await a({title:`退出登录?`,message:`退出后需要重新输入 ZYT 账号密码。`,confirmButtonText:`退出`})}catch{return}L.value=!0,await S(),await l.replace({name:`login`})}return(t,a)=>(u(),D(`div`,O,[m(w(i),{title:`我的`,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1}),R.value?(u(),D(`div`,k,[b(`section`,A,[b(`span`,j,g(w(r)(R.value.account.nickname||R.value.account.sn)),1),b(`div`,M,[b(`h2`,null,g(R.value.account.nickname||R.value.account.sn),1),b(`p`,N,`ZYT 账号 `+g(R.value.account.sn||`—`),1)]),R.value.permissions.root?(u(),D(`span`,P,`超级管理员`)):T(``,!0)]),a[1]||(a[1]=b(`h3`,{class:`section-title`},`手机端权限`,-1)),m(w(c),{inset:``},{default:h(()=>[(u(!0),D(y,null,p(z.value,t=>(u(),_(w(s),{key:t.title,title:t.title,label:t.code,center:``},{value:h(()=>[b(`span`,{class:d(t.granted?`tone-success`:`tone-muted`)},[m(w(e),{name:t.granted?`passed`:`close`},null,8,[`name`]),E(` `+g(t.granted?`已开通`:`未开通`),1)],2)]),_:2},1032,[`title`,`label`]))),128))]),_:1}),a[2]||(a[2]=b(`p`,{class:`hint app-muted`},` 权限在 ZYT「权限管理 → 角色管理」里勾选「企微客服助手」下的项目,调整后 10 分钟内自动生效,或退出重新登录。 `,-1)),a[3]||(a[3]=b(`h3`,{class:`section-title`},`登录信息`,-1)),m(w(c),{inset:``},{default:h(()=>[m(w(s),{title:`登录时间`,value:w(n)(R.value.session.created_at)},null,8,[`value`]),m(w(s),{title:`最长有效至`,value:B.value?w(n)(B.value):`—`,label:`ZYT 登录失效后会提前退出`},null,8,[`value`])]),_:1}),a[4]||(a[4]=b(`h3`,{class:`section-title`},`添加到主屏幕`,-1)),b(`p`,F,[w(V)?(u(),D(y,{key:0},[E(`在 Safari 中点底部「分享」按钮,选择「添加到主屏幕」,之后像 App 一样打开。`)],64)):(u(),D(y,{key:1},[E(`在浏览器菜单中选择「添加到主屏幕」或「安装应用」,之后像 App 一样打开。`)],64))]),b(`div`,I,[m(w(o),{block:``,round:``,type:`danger`,plain:``,loading:L.value,onClick:H},{default:h(()=>[...a[0]||(a[0]=[E(`退出登录`,-1)])]),_:1},8,[`loading`])])])):T(``,!0)]))}}),[[`__scopeId`,`data-v-1710faf3`]]);export{L as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css new file mode 100644 index 000000000..a8ec81749 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css @@ -0,0 +1 @@ +.shell[data-v-5972669c]{min-height:100vh;min-height:100dvh}.shell-tabbar[data-v-5972669c]{border-top:1px solid var(--app-border)} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js new file mode 100644 index 000000000..2aaa7cd8d --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js @@ -0,0 +1 @@ +import{a as e,n as t,o as n,p as r,r as i,t as a}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n as o,t as s}from"./use-placeholder-D9ZemjXL.js";import{B as c,Ct as l,Et as u,Ft as d,Kt as f,Q as p,Rt as m,S as h,St as g,U as _,Ut as v,X as y,Z as b,_ as x,_t as S,g as C,ht as w,pt as T,q as E,qt as D,rt as O,wt as k,xt as A,yt as j,zt as M}from"./index-C_mEe-tZ.js";var[N,P]=x(`tabbar`),F={route:Boolean,fixed:b,border:b,zIndex:y,placeholder:Boolean,activeColor:String,beforeChange:Function,inactiveColor:String,modelValue:E(0),safeAreaInsetBottom:{type:Boolean,default:null}},I=Symbol(N),L=l({name:N,props:F,emits:[`change`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i=f(),{linkChildren:a}=c(I),l=s(i,P),u=()=>{var t;return(t=e.safeAreaInsetBottom)==null?e.fixed:t},d=()=>{var t;let{fixed:a,zIndex:o,border:s}=e;return g(`div`,{ref:i,role:`tablist`,style:h(o),class:[P({fixed:a}),{[r]:s,"van-safe-area-bottom":u()}]},[(t=n.default)==null?void 0:t.call(n)])};return a({props:e,setActive:(n,r)=>{o(e.beforeChange,{args:[n],done(){t(`update:modelValue`,n),t(`change`,n),r()}})}}),()=>e.fixed&&e.placeholder?l(d):d()}}),R=C(L),[z,B]=x(`tabbar-item`),V=p({},e,{dot:Boolean,icon:String,name:y,badge:y,badgeProps:Object,iconPrefix:String}),H=l({name:z,props:V,emits:[`click`],setup(e,{emit:r,slots:a}){let o=n(),s=k().proxy,{parent:c,index:l}=_(I);if(!c)return;let d=w(()=>{var t;let{route:n,modelValue:r}=c.props;if(n&&`$route`in s){let{$route:t}=s,{to:n}=e,r=O(n)?n:{path:n};return t.matched.some(e=>{let t=`path`in r&&r.path===e.path,n=`name`in r&&r.name===e.name;return t||n})}return((t=e.name)==null?l.value:t)===r}),f=t=>{var n;d.value||c.setActive((n=e.name)==null?l.value:n,o),r(`click`,t)},p=()=>{if(a.icon)return a.icon({active:d.value});if(e.icon)return g(t,{name:e.icon,classPrefix:e.iconPrefix},null)};return()=>{var t;let{dot:n,badge:r}=e,{activeColor:o,inactiveColor:s}=c.props,l=d.value?o:s;return g(`div`,{role:`tab`,class:B({active:d.value}),style:{color:l},tabindex:0,"aria-selected":d.value,onClick:f},[g(i,u({dot:n,class:B(`icon`),content:r},e.badgeProps),{default:p}),g(`div`,{class:B(`text`)},[(t=a.default)==null?void 0:t.call(a,{active:d.value})])])}}}),U=C(H),W={class:`shell`},G=a(l({__name:`ShellView`,setup(e){return(e,t)=>{let n=m(`RouterView`);return d(),j(`div`,W,[g(n,null,{default:v(({Component:e})=>[(d(),S(T,{include:[`DevicesView`,`ConversationsView`]},[(d(),S(M(e)))],1024))]),_:1}),g(D(R),{route:``,placeholder:``,"safe-area-inset-bottom":``,class:`shell-tabbar`},{default:v(()=>[g(D(U),{replace:``,to:{name:`desktops`},icon:`desktop-o`},{default:v(()=>[...t[0]||(t[0]=[A(`电脑`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`conversations`},icon:`chat-o`},{default:v(()=>[...t[1]||(t[1]=[A(`会话`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`me`},icon:`user-o`},{default:v(()=>[...t[2]||(t[2]=[A(`我的`,-1)])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-5972669c`]]);export{G as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js new file mode 100644 index 000000000..68f1479be --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js @@ -0,0 +1 @@ +import{Ct as e,Et as t,St as n,Tt as r,Vt as i,X as a,Y as o,Z as s,_ as c,et as l,g as u,ht as d,m as f,nt as p,v as m,wt as h}from"./index-C_mEe-tZ.js";var g=`van-hairline`,_=`${g}--top`,v=`${g}--left`;`${g}`;var y=`${g}--bottom`,b=`${g}--surround`,x=`${g}--top-bottom`;`${g}`;var S=`van-haptics-feedback`,C=Symbol(`van-form`),w=Symbol();function T(e){let t=r(w,null);t&&i(t,t=>{t&&e()})}var E={to:[String,Object],url:String,replace:Boolean};function D({to:e,url:t,replace:n,$router:r}){e&&r?r[n?`replace`:`push`](e):t&&(n?location.replace(t):location.href=t)}function O(){let e=h().proxy;return()=>D(e)}var[k,A]=c(`badge`),j={dot:Boolean,max:a,tag:o(`div`),color:String,offset:Array,content:a,showZero:s,position:o(`top-right`)},M=e({name:k,props:j,setup(e,{slots:t}){let r=()=>{if(t.content)return!0;let{content:n,showZero:r}=e;return l(n)&&n!==``&&(r||n!==0&&n!==`0`)},i=()=>{let{dot:n,max:i,content:a}=e;if(!n&&r())return t.content?t.content():l(i)&&p(a)&&+a>+i?`${i}+`:a},a=e=>e.startsWith(`-`)?e.replace(`-`,``):`-${e}`,o=d(()=>{let n={background:e.color};if(e.offset){let[r,i]=e.offset,{position:o}=e,[s,c]=o.split(`-`);t.default?(n[s]=typeof i==`number`?m(s===`top`?i:-i):s===`top`?m(i):a(i),n[c]=typeof r==`number`?m(c===`left`?r:-r):c===`left`?m(r):a(r)):(n.marginTop=m(i),n.marginLeft=m(r))}return n}),s=()=>{if(r()||e.dot)return n(`div`,{class:A([e.position,{dot:e.dot,fixed:!!t.default}]),style:o.value},[i()])};return()=>{if(t.default){let{tag:r}=e;return n(r,{class:A(`wrapper`)},{default:()=>[t.default(),s()]})}return s()}}}),N=u(M),[P,F]=c(`icon`),I=e=>e==null?void 0:e.includes(`/`),L={dot:Boolean,tag:o(`i`),name:String,size:a,badge:a,color:String,badgeProps:Object,classPrefix:String},R=e({name:P,props:L,setup(e,{slots:i}){let a=r(f,null),o=d(()=>e.classPrefix||(a==null?void 0:a.iconPrefix)||F());return()=>{let{tag:r,dot:a,name:s,size:c,badge:l,color:u}=e,d=I(s);return n(N,t({dot:a,tag:r,class:[o.value,d?``:`${o.value}-${s}`],style:{color:u,fontSize:m(c)},content:l},e.badgeProps),{default:()=>{var e;return[(e=i.default)==null?void 0:e.call(i),d&&n(`img`,{class:F(`image`),src:s},null)]}})}}}),z=u(R),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{E as a,T as c,b as d,_ as f,S as h,D as i,y as l,C as m,z as n,O as o,x as p,N as r,w as s,B as t,v as u}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png differ diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js new file mode 100644 index 000000000..6642657c6 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js @@ -0,0 +1 @@ +import{a as e,d as t,n,o as r,p as i}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{Ct as a,Et as o,O as s,Q as c,St as l,X as u,Y as d,Z as f,_ as p,et as m,ft as h,g,ht as _,v,wt as y,x as b}from"./index-C_mEe-tZ.js";function x(e){let t=y();t&&c(t.proxy,e)}var[S,C]=p(`loading`),w=Array(12).fill(null).map((e,t)=>l(`i`,{class:C(`line`,String(t+1))},null)),T=l(`svg`,{class:C(`circular`),viewBox:`25 25 50 50`},[l(`circle`,{cx:`50`,cy:`50`,r:`20`,fill:`none`},null)]),E={size:u,type:d(`circular`),color:String,vertical:Boolean,textSize:u,textColor:String},D=a({name:S,props:E,setup(e,{slots:t}){let n=_(()=>c({color:e.color},b(e.size))),r=()=>{let r=e.type===`spinner`?w:T;return l(`span`,{class:C(`spinner`,e.type),style:n.value},[t.icon?t.icon():r])},i=()=>{var n;if(t.default)return l(`span`,{class:C(`text`),style:{fontSize:v(e.textSize),color:(n=e.textColor)==null?e.color:n}},[t.default()])};return()=>{let{type:t,vertical:n}=e;return l(`div`,{class:C([t,{vertical:n}]),"aria-live":`polite`,"aria-busy":!0},[r(),i()])}}}),O=g(D),[k,A]=p(`button`),j=c({},e,{tag:d(`button`),text:String,icon:String,type:d(`default`),size:d(`normal`),color:String,block:Boolean,plain:Boolean,round:Boolean,square:Boolean,loading:Boolean,hairline:Boolean,disabled:Boolean,iconPrefix:String,nativeType:d(`button`),loadingSize:u,loadingText:String,loadingType:String,iconPosition:d(`left`)}),M=a({name:k,props:j,emits:[`click`],setup(e,{emit:i,slots:a}){let o=r(),c=()=>a.loading?a.loading():l(O,{size:e.loadingSize,type:e.loadingType,class:A(`loading`)},null),u=()=>{if(e.loading)return c();if(a.icon)return l(`div`,{class:A(`icon`)},[a.icon()]);if(e.icon)return l(n,{name:e.icon,class:A(`icon`),classPrefix:e.iconPrefix},null)},d=()=>{let t;if(t=e.loading?e.loadingText:a.default?a.default():e.text,t)return l(`span`,{class:A(`text`)},[t])},f=()=>{let{color:t,plain:n}=e;if(t){let e={color:n?t:`white`};return n||(e.background=t),t.includes(`gradient`)?e.border=0:e.borderColor=t,e}},p=t=>{e.loading?s(t):e.disabled||(i(`click`,t),o())};return()=>{let{tag:n,type:r,size:i,block:a,round:o,plain:s,square:c,loading:m,disabled:h,hairline:g,nativeType:_,iconPosition:v}=e,y=[A([r,i,{plain:s,block:a,round:o,square:c,loading:m,disabled:h,hairline:g}]),{[t]:g}];return l(n,{type:_,class:y,style:f(),disabled:h,onClick:p},{default:()=>[l(`div`,{class:A(`content`)},[v===`left`&&u(),d(),v===`right`&&u()])]})}}}),N=g(M),P=()=>{var e;let{scopeId:t}=((e=y())==null?void 0:e.vnode)||{};return t?{[t]:``}:null},[F,I]=p(`cell`),L={tag:d(`div`),icon:String,size:String,title:u,value:u,label:u,center:Boolean,isLink:Boolean,border:f,iconPrefix:String,valueClass:null,labelClass:null,titleClass:null,titleStyle:null,arrowDirection:String,required:{type:[Boolean,String],default:null},clickable:{type:Boolean,default:null}},R=c({},L,e),z=a({name:F,props:R,setup(e,{slots:t}){let i=r(),a=()=>{if(t.label||m(e.label))return l(`div`,{class:[I(`label`),e.labelClass]},[t.label?t.label():e.label])},o=()=>{var n;if(t.title||m(e.title)){let r=(n=t.title)==null?void 0:n.call(t);return Array.isArray(r)&&r.length===0?void 0:l(`div`,{class:[I(`title`),e.titleClass],style:e.titleStyle},[r||l(`span`,null,[e.title]),a()])}},s=()=>{let n=t.value||t.default;if(n||m(e.value))return l(`div`,{class:[I(`value`),e.valueClass]},[n?n():l(`span`,null,[e.value])])},c=()=>{if(t.icon)return t.icon();if(e.icon)return l(n,{name:e.icon,class:I(`left-icon`),classPrefix:e.iconPrefix},null)},u=()=>{if(t[`right-icon`])return t[`right-icon`]();if(e.isLink){let t=e.arrowDirection&&e.arrowDirection!==`right`?`arrow-${e.arrowDirection}`:`arrow`;return l(n,{name:t,class:I(`right-icon`)},null)}};return()=>{var n,r,a;let{tag:d,size:f,center:p,border:m,isLink:h,required:g}=e,_=!!((a=(r=(n=e.clickable)==null?e.to:n)==null?e.url:r)==null?h:a),v={center:p,required:!!g,clickable:_,borderless:!m};return f&&(v[f]=!!f),l(d,{class:I(v),role:_?`button`:void 0,tabindex:_?0:void 0,onClick:_?i:void 0},{default:()=>{var e;return[c(),o(),s(),u(),(e=t.extra)==null?void 0:e.call(t)]}})}}}),B=g(z),[V,H]=p(`cell-group`),U=a({name:V,inheritAttrs:!1,props:{title:String,inset:Boolean,border:f},setup(e,{slots:t,attrs:n}){let r=()=>{var r;return l(`div`,o({class:[H({inset:e.inset}),{[i]:e.border&&!e.inset}]},n,P()),[(r=t.default)==null?void 0:r.call(t)])},a=()=>l(`div`,{class:H(`title`,{inset:e.inset})},[t.title?t.title():e.title]);return()=>e.title||t.title?l(h,null,[a(),r()]):r()}}),W=g(U);export{N as a,P as i,B as n,O as o,L as r,x as s,W as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js new file mode 100644 index 000000000..2e2483612 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js @@ -0,0 +1 @@ +import{m as e,n as t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n,r,s as i}from"./cell-group-CEKvg8Wf.js";import{A as a,Ct as o,Dt as s,Et as c,Gt as ee,It as l,Kt as u,Mt as d,O as f,P as te,Q as p,St as m,T as h,U as g,V as _,Vt as v,X as y,Y as b,_ as x,b as S,et as C,g as w,ht as T,it as E,k as ne,q as D,rt as O,st as k,tt as A,v as j,wt as M,xt as re,y as ie}from"./index-C_mEe-tZ.js";var N=0;function P(){let e=M(),{name:t=`unknown`}=(e==null?void 0:e.type)||{};return`${t}-${++N}`}function F(e){return Array.isArray(e)?!e.length:e!==0&&!e}function ae(e,t){if(F(e)){if(t.required)return!1;if(t.validateEmpty===!1)return!0}return!(t.pattern&&!t.pattern.test(String(e)))}function oe(e,t){return new Promise(n=>{let r=t.validator(e,t);if(E(r)){r.then(n);return}n(r)})}function I(e,t){let{message:n}=t;return A(n)?n(e,t):n||``}function se({target:e}){e.composing=!0}function L({target:e}){e.composing&&(e.composing=!1,e.dispatchEvent(new Event(`input`)))}function ce(e,t){let n=h();e.style.height=`auto`;let r=e.scrollHeight;if(O(t)){let{maxHeight:e,minHeight:n}=t;e!==void 0&&(r=Math.min(r,e)),n!==void 0&&(r=Math.max(r,n))}r&&(e.style.height=`${r}px`,a(n))}function le(e,t){return e===`number`&&(e=`text`,t!=null||(t=`decimal`)),e===`digit`&&(e=`tel`,t!=null||(t=`numeric`)),{type:e,inputmode:t}}function R(e){return[...e].length}function z(e,t){return[...e].slice(0,t).join(``)}var[B,V]=x(`field`),H={id:String,name:String,leftIcon:String,rightIcon:String,autofocus:Boolean,clearable:Boolean,maxlength:y,max:Number,min:Number,formatter:Function,clearIcon:b(`clear`),modelValue:D(``),inputAlign:String,placeholder:String,autocomplete:String,autocapitalize:String,autocorrect:String,errorMessage:String,enterkeyhint:String,clearTrigger:b(`focus`),formatTrigger:b(`onChange`),spellcheck:{type:Boolean,default:null},error:{type:Boolean,default:null},disabled:{type:Boolean,default:null},readonly:{type:Boolean,default:null},inputmode:String},U=p({},r,H,{rows:y,type:b(`text`),rules:Array,autosize:[Boolean,Object],labelWidth:y,labelClass:null,labelAlign:String,showWordLimit:Boolean,errorMessageAlign:String,colon:{type:Boolean,default:null}}),W=o({name:B,props:U,emits:[`blur`,`focus`,`clear`,`keypress`,`clickInput`,`endValidate`,`startValidate`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(r,{emit:a,slots:o}){let p=P(),h=ee({status:`unvalidated`,focused:!1,validateMessage:``}),y=u(),b=u(),x=u(),{parent:w}=g(e),E=()=>{var e;return String((e=r.modelValue)==null?``:e)},D=e=>{if(C(r[e]))return r[e];if(w&&C(w.props[e]))return w.props[e]},O=T(()=>{let e=D(`readonly`);if(r.clearable&&!e){let e=E()!==``,t=r.clearTrigger===`always`||r.clearTrigger===`focus`&&h.focused;return e&&t}return!1}),A=T(()=>x.value&&o.input?x.value():r.modelValue),M=T(()=>{var e;let t=D(`required`);return t===`auto`?(e=r.rules)==null?void 0:e.some(e=>e.required):t}),N=e=>e.reduce((e,t)=>e.then(()=>{if(h.status===`failed`)return;let{value:e}=A;if(t.formatter&&(e=t.formatter(e,t)),!ae(e,t)){h.status=`failed`,h.validateMessage=I(e,t);return}if(t.validator)return F(e)&&t.validateEmpty===!1?void 0:oe(e,t).then(n=>{n&&typeof n==`string`?(h.status=`failed`,h.validateMessage=n):n===!1&&(h.status=`failed`,h.validateMessage=I(e,t))})}),Promise.resolve()),B=()=>{h.status=`unvalidated`,h.validateMessage=``},H=()=>a(`endValidate`,{status:h.status,message:h.validateMessage}),U=(e=r.rules)=>new Promise(t=>{B(),e?(a(`startValidate`),N(e).then(()=>{h.status===`failed`?(t({name:r.name,message:h.validateMessage}),H()):(h.status=`passed`,t(),H())})):t()}),W=e=>{if(w&&r.rules){let{validateTrigger:t}=w.props,n=k(t).includes(e),i=r.rules.filter(t=>t.trigger?k(t.trigger).includes(e):n);i.length&&U(i)}},G=e=>{var t;let{maxlength:n}=r;if(C(n)&&R(e)>+n){let r=E();if(r&&R(r)===+n)return r;let i=(t=y.value)==null?void 0:t.selectionEnd;if(h.focused&&i){let t=[...e],r=t.length-+n;return i=R(e.slice(0,i)),t.splice(i-r,r),t.join(``)}return z(e,+n)}return e},K=(e,t=`onChange`)=>{var n,i;let o=e;e=G(e);let s=o.length-e.length;if(r.type===`number`||r.type===`digit`){let a=r.type===`number`;if(e=S(e,a,a),t===`onBlur`&&e!==``&&(r.min!==void 0||r.max!==void 0)){let t=ie(+e,(n=r.min)==null?-1/0:n,(i=r.max)==null?1/0:i);+e!==t&&(e=t.toString())}}let c=0;if(r.formatter&&t===r.formatTrigger){let{formatter:t,maxlength:n}=r;if(e=t(e),C(n)&&R(e)>+n&&(e=z(e,+n)),y.value&&h.focused){let{selectionEnd:e}=y.value,n=z(o,e);c=t(n).length-n.length}}if(y.value&&y.value.value!==e){if(h.focused){let{selectionStart:t,selectionEnd:n}=y.value;if(y.value.value=e,C(t)&&C(n)){let r=e.length;s?(t-=s,n-=s):c&&(t+=c,n+=c),y.value.setSelectionRange(Math.min(t,r),Math.min(n,r))}}else y.value.value=e}e!==r.modelValue&&a(`update:modelValue`,e)},ue=e=>{e.target.composing||K(e.target.value)},q=()=>{var e;return(e=y.value)==null?void 0:e.blur()},J=()=>{var e;return(e=y.value)==null?void 0:e.focus()},Y=()=>{let e=y.value;r.type===`textarea`&&r.autosize&&e&&ce(e,r.autosize)},de=e=>{h.focused=!0,a(`focus`,e),s(Y),D(`readonly`)&&q()},fe=e=>{h.focused=!1,K(E(),`onBlur`),a(`blur`,e),!D(`readonly`)&&(W(`onBlur`),s(Y),ne())},X=e=>a(`clickInput`,e),pe=e=>a(`clickLeftIcon`,e),me=e=>a(`clickRightIcon`,e),he=e=>{f(e),a(`update:modelValue`,``),a(`clear`,e)},Z=T(()=>{if(typeof r.error==`boolean`)return r.error;if(w&&w.props.showError&&h.status===`failed`)return!0}),ge=T(()=>{let e=D(`labelWidth`),t=D(`labelAlign`);if(e&&t!==`top`)return{width:j(e)}}),Q=e=>{e.keyCode===13&&(!(w&&w.props.submitOnEnter)&&r.type!==`textarea`&&f(e),r.type===`search`&&q()),a(`keypress`,e)},$=()=>r.id||`${p}-input`,_e=()=>h.status,ve=()=>{let e=V(`control`,[D(`inputAlign`),{error:Z.value,custom:!!o.input,"min-height":r.type===`textarea`&&!r.autosize}]);if(o.input)return m(`div`,{class:e,onClick:X},[o.input()]);let t={id:$(),ref:y,name:r.name,rows:r.rows===void 0?void 0:+r.rows,class:e,disabled:D(`disabled`),readonly:D(`readonly`),autofocus:r.autofocus,placeholder:r.placeholder,autocomplete:r.autocomplete,autocapitalize:r.autocapitalize,autocorrect:r.autocorrect,enterkeyhint:r.enterkeyhint,spellcheck:r.spellcheck,"aria-labelledby":r.label?`${p}-label`:void 0,"data-allow-mismatch":`attribute`,onBlur:fe,onFocus:de,onInput:ue,onClick:X,onChange:L,onKeypress:Q,onCompositionend:L,onCompositionstart:se};return r.type===`textarea`?m(`textarea`,c(t,{inputmode:r.inputmode}),null):m(`input`,c(le(r.type,r.inputmode),t),null)},ye=()=>{let e=o[`left-icon`];if(r.leftIcon||e)return m(`div`,{class:V(`left-icon`),onClick:pe},[e?e():m(t,{name:r.leftIcon,classPrefix:r.iconPrefix},null)])},be=()=>{let e=o[`right-icon`];if(r.rightIcon||e)return m(`div`,{class:V(`right-icon`),onClick:me},[e?e():m(t,{name:r.rightIcon,classPrefix:r.iconPrefix},null)])},xe=()=>{if(r.showWordLimit&&r.maxlength){let e=R(E());return m(`div`,{class:V(`word-limit`)},[m(`span`,{class:V(`word-num`)},[e]),re(`/`),r.maxlength])}},Se=()=>{if(w&&w.props.showErrorMessage===!1)return;let e=r.errorMessage||h.validateMessage;if(e){let t=o[`error-message`],n=D(`errorMessageAlign`);return m(`div`,{class:V(`error-message`,n)},[t?t({message:e}):e])}},Ce=()=>{let e=D(`labelWidth`),t=D(`labelAlign`),n=D(`colon`)?`:`:``;if(o.label)return[o.label(),n];if(r.label)return m(`label`,{id:`${p}-label`,for:o.input?void 0:$(),"data-allow-mismatch":`attribute`,onClick:e=>{f(e),J()},style:t===`top`&&e?{width:j(e)}:void 0},[r.label+n])},we=()=>[m(`div`,{class:V(`body`)},[ve(),O.value&&m(t,{ref:b,name:r.clearIcon,class:V(`clear`)},null),be(),o.button&&m(`div`,{class:V(`button`)},[o.button()])]),xe(),Se()];return i({blur:q,focus:J,validate:U,formValue:A,resetValidation:B,getValidationStatus:_e,adjustTextareaSize:Y}),l(te,{customValue:x,resetValidation:B,validateWithTrigger:W}),v(()=>r.modelValue,()=>{K(E()),B(),W(`onChange`),s(Y)}),d(()=>{K(E(),r.formatTrigger),s(Y)}),_(`touchstart`,he,{target:T(()=>{var e;return(e=b.value)==null?void 0:e.$el})}),()=>{let e=D(`disabled`),t=D(`labelAlign`),i=ye(),a=()=>{let e=Ce();return t===`top`?[i,e].filter(Boolean):e||[]};return m(n,{size:r.size,class:V({error:Z.value,disabled:e,[`label-${t}`]:t}),center:r.center,border:r.border,isLink:!e&&r.isLink,clickable:!e&&r.clickable,onClick:e?e=>e.stopImmediatePropagation():void 0,titleStyle:ge.value,valueClass:V(`value`),titleClass:[V(`label`,[t,{required:M.value}]),r.labelClass],arrowDirection:r.arrowDirection},{icon:i&&t!==`top`?()=>i:null,title:a,value:we,extra:o.extra})}}}),G=w(W);export{H as n,P as r,G as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js new file mode 100644 index 000000000..c9c7f9e94 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js @@ -0,0 +1 @@ +import{a as e,f as t,h as n,l as r,n as i,o as a,s as o,u as s}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n as c,t as l}from"./use-placeholder-D9ZemjXL.js";import{a as u,i as d,s as f}from"./cell-group-CEKvg8Wf.js";import{$ as p,B as m,Ct as h,Dt as ee,Et as g,Gt as _,It as te,Kt as v,L as y,Mt as b,O as x,Ot as ne,Q as S,R as C,S as w,St as T,U as E,V as re,Vt as D,Wt as O,X as k,Y as A,Z as j,_ as M,at as N,ct as ie,dt as P,et as ae,ft as oe,g as F,h as se,ht as I,jt as ce,kt as le,lt as ue,mt as de,ot as fe,tt as pe,ut as L,v as me}from"./index-C_mEe-tZ.js";var[R,z]=M(`action-bar`),B=Symbol(R),he=h({name:R,props:{placeholder:Boolean,safeAreaInsetBottom:j},setup(e,{slots:t}){let n=v(),r=l(n,z),{linkChildren:i}=m(B);i();let a=()=>{var r;return T(`div`,{ref:n,class:[z(),{"van-safe-area-bottom":e.safeAreaInsetBottom}]},[(r=t.default)==null?void 0:r.call(t)])};return()=>e.placeholder?r(a):a()}}),ge=F(he),[_e,ve]=M(`action-bar-button`),ye=S({},e,{type:String,text:String,icon:String,color:String,loading:Boolean,disabled:Boolean}),be=h({name:_e,props:ye,setup(e,{slots:t}){let n=a(),{parent:r,index:i}=E(B),o=I(()=>{if(r){let e=r.children[i.value-1];return!(e&&`isButton`in e)}}),s=I(()=>{if(r){let e=r.children[i.value+1];return!(e&&`isButton`in e)}});return f({isButton:!0}),()=>{let{type:r,icon:i,text:a,color:c,loading:l,disabled:d}=e;return T(u,{class:ve([r,{last:s.value,first:o.value}]),size:`large`,type:r,icon:i,color:c,loading:l,disabled:d,onClick:n},{default:()=>[t.default?t.default():a]})}}}),V=F(be),H={show:Boolean,zIndex:k,overlay:j,duration:k,teleport:[String,Object],lockScroll:j,lazyRender:j,beforeClose:Function,overlayProps:Object,overlayStyle:Object,overlayClass:null,transitionAppear:Boolean,closeOnClickOverlay:j},xe=Object.keys(H);function Se(e,t){return e>t?`horizontal`:t>e?`vertical`:``}function U(){let e=v(0),t=v(0),n=v(0),r=v(0),i=v(0),a=v(0),o=v(``),s=v(!0),c=()=>o.value===`vertical`,l=()=>o.value===`horizontal`,u=()=>{n.value=0,r.value=0,i.value=0,a.value=0,o.value=``,s.value=!0};return{move:(c=>{let l=c.touches[0];n.value=(l.clientX<0?0:l.clientX)-e.value,r.value=l.clientY-t.value,i.value=Math.abs(n.value),a.value=Math.abs(r.value),(!o.value||i.value<10&&a.value<10)&&(o.value=Se(i.value,a.value)),s.value&&(i.value>5||a.value>5)&&(s.value=!1)}),start:(n=>{u(),e.value=n.touches[0].clientX,t.value=n.touches[0].clientY}),reset:u,startX:e,startY:t,deltaX:n,deltaY:r,offsetX:i,offsetY:a,direction:o,isVertical:c,isHorizontal:l,isTap:s}}var W=0,G=`van-overflow-hidden`;function Ce(e,t){let n=U(),r=t=>{n.move(t);let r=n.deltaY.value>0?`10`:`01`,i=y(t.target,e.value);for(;i.scrollHeight<=i.offsetHeight&&i!==e.value&&i.parentElement;)i=y(i.parentElement,e.value);let{scrollHeight:a,offsetHeight:o,scrollTop:s}=i,c=`11`;s===0?c=o>=a?`00`:`01`:s+o>=a&&(c=`10`),c!==`11`&&n.isVertical()&&!(parseInt(c,2)&parseInt(r,2))&&x(t,!0)},i=()=>{document.addEventListener(`touchstart`,n.start),document.addEventListener(`touchmove`,r,{passive:!1}),W||document.body.classList.add(G),W++},a=()=>{W&&(document.removeEventListener(`touchstart`,n.start),document.removeEventListener(`touchmove`,r),W--,W||document.body.classList.remove(G))},o=()=>t()&&i(),s=()=>t()&&a();C(o),ce(s),le(s),D(t,e=>{e?i():a()})}function K(e){let t=v(!1);return D(e,e=>{e&&(t.value=e)},{immediate:!0}),e=>()=>t.value?e():null}var[we,Te]=M(`overlay`),Ee=h({name:we,inheritAttrs:!1,props:{show:Boolean,zIndex:k,duration:k,className:null,lockScroll:j,lazyRender:j,customStyle:Object,teleport:[String,Object]},setup(e,{attrs:t,slots:n}){let r=v(),i=K(()=>e.show||!e.lazyRender),a=t=>{e.lockScroll&&x(t,!0)},o=i(()=>{var i;let a=S(w(e.zIndex),e.customStyle);return ae(e.duration)&&(a.animationDuration=`${e.duration}s`),O(T(`div`,g({ref:r,style:a,class:[Te(),e.className]},t),[(i=n.default)==null?void 0:i.call(n)]),[[L,e.show]])});return re(`touchmove`,a,{target:r}),()=>{let t=T(ie,{name:`van-fade`,appear:!0},{default:o});return e.teleport?T(de,{to:e.teleport},{default:()=>[t]}):t}}}),De=F(Ee),Oe=S({},H,{round:Boolean,position:A(`center`),closeIcon:A(`cross`),closeable:Boolean,transition:String,iconPrefix:String,closeOnPopstate:Boolean,closeIconPosition:A(`top-right`),destroyOnClose:Boolean,safeAreaInsetTop:Boolean,safeAreaInsetBottom:Boolean}),[ke,q]=M(`popup`),Ae=h({name:ke,inheritAttrs:!1,props:Oe,emits:[`open`,`close`,`opened`,`closed`,`keydown`,`update:show`,`clickOverlay`,`clickCloseIcon`],setup(e,{emit:t,attrs:r,slots:a}){let s,l,u=v(),p=v(),m=K(()=>e.show||!e.lazyRender),h=I(()=>{let t={zIndex:u.value};if(ae(e.duration)){let n=e.position===`center`?`animationDuration`:`transitionDuration`;t[n]=`${e.duration}s`}return t}),_=()=>{s||(s=!0,u.value=e.zIndex===void 0?se():+e.zIndex,t(`open`))},y=()=>{s&&c(e.beforeClose,{done(){s=!1,t(`close`),t(`update:show`,!1)}})},x=n=>{t(`clickOverlay`,n),e.closeOnClickOverlay&&y()},C=()=>{if(e.overlay){let t=S({show:e.show,class:e.overlayClass,zIndex:u.value,duration:e.duration,customStyle:e.overlayStyle,role:e.closeOnClickOverlay?`button`:void 0,tabindex:e.closeOnClickOverlay?0:void 0},e.overlayProps);return T(De,g(t,d(),{onClick:x}),{default:a[`overlay-content`]})}},w=e=>{t(`clickCloseIcon`,e),y()},E=()=>{if(e.closeable)return T(i,{role:`button`,tabindex:0,name:e.closeIcon,class:[q(`close-icon`,e.closeIconPosition),n],classPrefix:e.iconPrefix,onClick:w},null)},k,A=()=>{k&&clearTimeout(k),k=setTimeout(()=>{t(`opened`)})},j=()=>t(`closed`),M=e=>t(`keydown`,e),N=m(()=>{var t;let{destroyOnClose:n,round:i,position:o,safeAreaInsetTop:s,safeAreaInsetBottom:c,show:l}=e;if(l||!n)return O(T(`div`,g({ref:p,style:h.value,role:`dialog`,tabindex:0,class:[q({round:i,[o]:o}),{"van-safe-area-top":s,"van-safe-area-bottom":c}],onKeydown:M},r,d()),[(t=a.default)==null?void 0:t.call(a),E()]),[[L,l]])}),P=()=>{let{position:t,transition:n,transitionAppear:r}=e,i=t===`center`?`van-fade`:`van-popup-slide-${t}`;return T(ie,{name:n||i,appear:r,onAfterEnter:A,onAfterLeave:j},{default:N})};return D(()=>e.show,e=>{e&&!s&&(_(),r.tabindex===0&&ee(()=>{var e;(e=p.value)==null||e.focus()})),!e&&s&&(s=!1,t(`close`))}),f({popupRef:p}),Ce(p,()=>e.show&&e.lockScroll),re(`popstate`,()=>{e.closeOnPopstate&&(y(),l=!1)}),b(()=>{e.show&&_()}),ne(()=>{l&&(t(`update:show`,!0),l=!1)}),ce(()=>{e.show&&e.teleport&&(y(),l=!0)}),te(o,()=>e.show),()=>e.teleport?T(de,{to:e.teleport},{default:()=>[C(),P()]}):T(oe,null,[C(),P()])}}),J=F(Ae);function je(){let e=_({show:!1}),t=t=>{e.show=t},n=n=>{S(e,n,{transitionAppear:!0}),t(!0)},r=()=>t(!1);return f({open:n,close:r,toggle:t}),{open:n,close:r,state:e,toggle:t}}function Me(e){let t=ue(e),n=document.createElement(`div`);return document.body.appendChild(n),{instance:t.mount(n),unmount(){t.unmount(),document.body.removeChild(n)}}}var[Ne,Y,X]=M(`dialog`),Pe=S({},H,{title:String,theme:String,width:k,message:[String,Function],callback:Function,allowHtml:Boolean,className:null,transition:A(`van-dialog-bounce`),messageAlign:String,closeOnPopstate:j,showCancelButton:Boolean,cancelButtonText:String,cancelButtonColor:String,cancelButtonDisabled:Boolean,confirmButtonText:String,confirmButtonColor:String,confirmButtonDisabled:Boolean,showConfirmButton:j,closeOnClickOverlay:Boolean,keyboardEnabled:j,destroyOnClose:Boolean}),Fe=[...xe,`transition`,`closeOnPopstate`,`destroyOnClose`],Ie=h({name:Ne,props:Pe,emits:[`confirm`,`cancel`,`keydown`,`update:show`],setup(e,{emit:n,slots:r}){let i=v(),a=_({confirm:!1,cancel:!1}),o=e=>n(`update:show`,e),l=t=>{var n;o(!1),(n=e.callback)==null||n.call(e,t)},d=t=>()=>{e.show&&(n(t),e.beforeClose?(a[t]=!0,c(e.beforeClose,{args:[t],done(){l(t),a[t]=!1},canceled(){a[t]=!1}})):l(t))},f=d(`cancel`),p=d(`confirm`),m=P(t=>{var r,a;e.keyboardEnabled&&t.target===((a=(r=i.value)==null?void 0:r.popupRef)==null?void 0:a.value)&&({Enter:e.showConfirmButton?p:N,Escape:e.showCancelButton?f:N}[t.key](),n(`keydown`,t))},[`enter`,`esc`]),h=()=>{let t=r.title?r.title():e.title;if(t)return T(`div`,{class:Y(`header`,{isolated:!e.message&&!r.default})},[t])},ee=t=>{let{message:n,allowHtml:r,messageAlign:i}=e,a=Y(`message`,{"has-title":t,[i]:i}),o=pe(n)?n():n;return r&&typeof o==`string`?T(`div`,{class:a,innerHTML:o},null):T(`div`,{class:a},[o])},te=()=>{if(r.default)return T(`div`,{class:Y(`content`)},[r.default()]);let{title:t,message:n,allowHtml:i}=e;if(n){let e=!!(t||r.title);return T(`div`,{key:+!!i,class:Y(`content`,{isolated:!e})},[ee(e)])}},y=()=>T(`div`,{class:[t,Y(`footer`)]},[e.showCancelButton&&T(u,{size:`large`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),style:{color:e.cancelButtonColor},loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(u,{size:`large`,text:e.confirmButtonText||X(`confirm`),class:[Y(`confirm`),{[s]:e.showCancelButton}],style:{color:e.confirmButtonColor},loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]),b=()=>T(ge,{class:Y(`footer`)},{default:()=>[e.showCancelButton&&T(V,{type:`warning`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),color:e.cancelButtonColor,loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(V,{type:`danger`,text:e.confirmButtonText||X(`confirm`),class:Y(`confirm`),color:e.confirmButtonColor,loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]}),x=()=>r.footer?r.footer():e.theme===`round-button`?b():y();return()=>{let{width:t,title:n,theme:r,message:a,className:s}=e;return T(J,g({ref:i,role:`dialog`,class:[Y([r]),s],style:{width:me(t)},tabindex:0,"aria-labelledby":n||a,onKeydown:m,"onUpdate:show":o},fe(e,Fe)),{default:()=>[h(),te(),x()]})}}}),Z,Le=S({},{title:``,width:``,theme:null,message:``,overlay:!0,callback:null,teleport:`body`,className:``,allowHtml:!1,lockScroll:!0,transition:void 0,beforeClose:null,overlayClass:``,overlayStyle:void 0,messageAlign:``,cancelButtonText:``,cancelButtonColor:null,cancelButtonDisabled:!1,confirmButtonText:``,confirmButtonColor:null,confirmButtonDisabled:!1,showConfirmButton:!0,showCancelButton:!1,closeOnPopstate:!0,closeOnClickOverlay:!1,destroyOnClose:!1});function Re(){let e={setup(){let{state:e,toggle:t}=je();return()=>T(Ie,g(e,{"onUpdate:show":t}),null)}};({instance:Z}=Me(e))}function ze(e){return p?new Promise((t,n)=>{Z||Re(),Z.open(S({},Le,e,{callback:e=>{(e===`confirm`?t:n)(e)}}))}):Promise.resolve(void 0)}var Be=e=>ze(S({showCancelButton:!0},e)),[Ve,Q]=M(`nav-bar`),He=h({name:Ve,props:{title:String,fixed:Boolean,zIndex:k,border:j,leftText:String,rightText:String,leftDisabled:Boolean,rightDisabled:Boolean,leftArrow:Boolean,placeholder:Boolean,safeAreaInsetTop:Boolean,clickable:j},emits:[`clickLeft`,`clickRight`],setup(e,{emit:t,slots:n}){let a=v(),o=l(a,Q),s=n=>{e.leftDisabled||t(`clickLeft`,n)},c=n=>{e.rightDisabled||t(`clickRight`,n)},u=()=>n.left?n.left():[e.leftArrow&&T(i,{class:Q(`arrow`),name:`arrow-left`},null),e.leftText&&T(`span`,{class:Q(`text`)},[e.leftText])],d=()=>n.right?n.right():T(`span`,{class:Q(`text`)},[e.rightText]),f=()=>{let{title:t,fixed:i,border:o,zIndex:l}=e,f=w(l),p=e.leftArrow||e.leftText||n.left,m=e.rightText||n.right;return T(`div`,{ref:a,style:f,class:[Q({fixed:i}),{[r]:o,"van-safe-area-top":e.safeAreaInsetTop}]},[T(`div`,{class:Q(`content`)},[p&&T(`div`,{class:[Q(`left`,{disabled:e.leftDisabled}),e.clickable&&!e.leftDisabled?`van-haptics-feedback`:``],onClick:s},[u()]),T(`div`,{class:[Q(`title`),`van-ellipsis`]},[n.title?n.title():t]),m&&T(`div`,{class:[Q(`right`,{disabled:e.rightDisabled}),e.clickable&&!e.rightDisabled?`van-haptics-feedback`:``],onClick:c},[d()])])])};return()=>e.fixed&&e.placeholder?o(f):f()}}),Ue=F(He),$=e=>String(e).padStart(2,`0`);function We(e){let t=String(e||``).trim();if(!t)return null;let n=new Date(t);return Number.isNaN(n.getTime())?null:n}function Ge(e,t=new Date){let n=We(e);if(!n)return`—`;let r=Math.round((t.getTime()-n.getTime())/1e3);if(r<45&&r>-60)return`刚刚`;if(r>=45&&r<3600)return`${Math.max(1,Math.round(r/60))} 分钟前`;let i=`${$(n.getHours())}:${$(n.getMinutes())}`,a=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),o=864e5;return n.getTime()>=a&&n.getTime()=a-o&&n.getTime()i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>S(e)===`[object Map]`,p=e=>S(e)===`[object Set]`,m=e=>S(e)===`[object Date]`,h=e=>S(e)===`[object RegExp]`,g=e=>typeof e==`function`,_=e=>typeof e==`string`,v=e=>typeof e==`symbol`,y=e=>typeof e==`object`&&!!e,b=e=>(y(e)||g(e))&&g(e.then)&&g(e.catch),x=Object.prototype.toString,S=e=>x.call(e),C=e=>S(e).slice(8,-1),w=e=>S(e)===`[object Object]`,T=e=>_(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,ee=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),E=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},te=/-\w/g,D=E(e=>e.replace(te,e=>e.slice(1).toUpperCase())),ne=/\B([A-Z])/g,O=E(e=>e.replace(ne,`-$1`).toLowerCase()),re=E(e=>e.charAt(0).toUpperCase()+e.slice(1)),ie=E(e=>e?`on${re(e)}`:``),k=(e,t)=>!Object.is(e,t),A=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},oe=e=>{let t=parseFloat(e);return isNaN(t)?e:t},se=e=>{let t=_(e)?Number(e):NaN;return isNaN(t)?e:t},ce,le=()=>ce||(ce=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{});function ue(e){if(d(e)){let t={};for(let n=0;n{if(e){let n=e.split(fe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function he(e){let t=``;if(_(e))t=e;else if(d(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Se=e=>_(e)?e:e==null?``:d(e)||y(e)&&(e.toString===x||!g(e.toString))?xe(e)?Se(e.value):JSON.stringify(e,Ce,2):String(e),Ce=(e,t)=>xe(t)?Ce(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[we(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>we(e))}:v(t)?we(t):y(t)&&!d(t)&&!w(t)?String(t):t,we=(e,t=``)=>{var n;return v(e)?`Symbol(${(n=e.description)==null?t:n})`:e},j,Te=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&j&&(j.active?(this.parent=j,this.index=(j.scopes||(j.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes){let n=this.scopes.slice();for(e=0,t=n.length;e0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(je){let e=je;for(je=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ae;){let t=Ae;for(Ae=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}function Fe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ie(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),ze(r),Be(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Re(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Re(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ke)||(e.globalVersion=Ke,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Le(e))))return;e.flags|=2;let t=e.dep,n=M,r=Ve;M=e,Ve=!0;try{Fe(e);let n=e.fn(e._value);(t.version===0||k(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{M=n,Ve=r,Ie(e),e.flags&=-3}}function ze(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ze(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Be(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var Ve=!0,He=[];function Ue(){He.push(Ve),Ve=!1}function We(){let e=He.pop();Ve=e===void 0||e}function Ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;try{t()}finally{M=e}}}var Ke=0,qe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Je=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!M||!Ve||M===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==M)t=this.activeLink=new qe(M,this),M.deps?(t.prevDep=M.depsTail,M.depsTail.nextDep=t,M.depsTail=t):M.deps=M.depsTail=t,Ye(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=M.depsTail,t.nextDep=void 0,M.depsTail.nextDep=t,M.depsTail=t,M.deps===t&&(M.deps=e)}return t}trigger(e){this.version++,Ke++,this.notify(e)}notify(e){Ne();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pe()}}};function Ye(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Ye(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var Xe=new WeakMap,Ze=Symbol(``),Qe=Symbol(``),$e=Symbol(``);function N(e,t,n){if(Ve&&M){let t=Xe.get(e);t||Xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Je),r.map=t,r.key=n),r.track()}}function et(e,t,n,r,i,a){let o=Xe.get(e);if(!o){Ke++;return}let s=e=>{e&&e.trigger()};if(Ne(),t===`clear`)o.forEach(s);else{let i=d(e),a=i&&T(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===$e||!v(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get($e)),t){case`add`:i?a&&s(o.get(`length`)):(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`delete`:i||(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`set`:f(e)&&s(o.get(Ze))}}Pe()}function tt(e){let t=F(e);return t===e?t:(N(t,`iterate`,$e),P(e)?t:t.map(Ht))}function nt(e){return N(e=F(e),`iterate`,$e),e}function rt(e,t){return zt(e)?Ut(Rt(e)?Ht(t):t):Ht(t)}var it={__proto__:null,[Symbol.iterator](){return at(this,Symbol.iterator,e=>rt(this,e))},concat(...e){return tt(this).concat(...e.map(e=>d(e)?tt(e):e))},entries(){return at(this,`entries`,e=>(e[1]=rt(this,e[1]),e))},every(e,t){return st(this,`every`,e,t,void 0,arguments)},filter(e,t){return st(this,`filter`,e,t,e=>e.map(e=>rt(this,e)),arguments)},find(e,t){return st(this,`find`,e,t,e=>rt(this,e),arguments)},findIndex(e,t){return st(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return st(this,`findLast`,e,t,e=>rt(this,e),arguments)},findLastIndex(e,t){return st(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return st(this,`forEach`,e,t,void 0,arguments)},includes(...e){return lt(this,`includes`,e)},indexOf(...e){return lt(this,`indexOf`,e)},join(e){return tt(this).join(e)},lastIndexOf(...e){return lt(this,`lastIndexOf`,e)},map(e,t){return st(this,`map`,e,t,void 0,arguments)},pop(){return ut(this,`pop`)},push(...e){return ut(this,`push`,e)},reduce(e,...t){return ct(this,`reduce`,e,t)},reduceRight(e,...t){return ct(this,`reduceRight`,e,t)},shift(){return ut(this,`shift`)},some(e,t){return st(this,`some`,e,t,void 0,arguments)},splice(...e){return ut(this,`splice`,e)},toReversed(){return tt(this).toReversed()},toSorted(e){return tt(this).toSorted(e)},toSpliced(...e){return tt(this).toSpliced(...e)},unshift(...e){return ut(this,`unshift`,e)},values(){return at(this,`values`,e=>rt(this,e))}};function at(e,t,n){let r=nt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var ot=Array.prototype;function st(e,t,n,r,i,a){let o=nt(e),s=o!==e&&!P(e),c=o[t];if(c!==ot[t]){let t=c.apply(e,a);return s?Ht(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,rt(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function ct(e,t,n,r){let i=nt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=rt(e,t)),n.call(this,t,rt(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?rt(e,c):c}function lt(e,t,n){let r=F(e);N(r,`iterate`,$e);let i=r[t](...n);return(i===-1||i===!1)&&Bt(n[0])?(n[0]=F(n[0]),r[t](...n)):i}function ut(e,t,n=[]){Ue(),Ne();let r=F(e)[t].apply(e,n);return Pe(),We(),r}var dt=e(`__proto__,__v_isRef,__isVue`),ft=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(v));function pt(e){v(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.hasOwnProperty(e)}var mt=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?Mt:jt:i?At:kt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=it[t]))return e;if(t===`hasOwnProperty`)return pt}let o=Reflect.get(e,t,I(e)?e:n);if((v(t)?ft.has(t):dt(t))||(r||N(e,`get`,t),i))return o;if(I(o)){let e=a&&T(t)?o:o.value;return r&&y(e)?It(e):e}return y(o)?r?It(o):Pt(o):o}},ht=class extends mt{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&T(t);if(!this._isShallow){let e=zt(i);if(!P(n)&&!zt(n)&&(i=F(i),n=F(n)),!a&&I(i)&&!I(n))return e||(i.value=n),!0}let o=a?Number(t)e,xt=e=>Reflect.getPrototypeOf(e);function St(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a),c=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?bt:t?Ut:Ht;return!t&&N(a,`iterate`,l?Qe:Ze),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Ct(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function wt(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(k(n,a)&&N(i,`get`,n),N(i,`get`,a));let{has:o}=xt(i),s=t?bt:e?Ut:Ht;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(F(t),`iterate`,Ze),t.size},has(t){let n=this.__v_raw,r=F(n),i=F(t);return e||(k(t,i)&&N(r,`has`,t),N(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=F(a),s=t?bt:e?Ut:Ht;return!e&&N(o,`iterate`,Ze),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Ct(`add`),set:Ct(`set`),delete:Ct(`delete`),clear:Ct(`clear`)}:{add(e){let n=F(this),r=xt(n),i=F(e),a=!t&&!P(e)&&!zt(e)?i:e;return r.has.call(n,a)||k(e,a)&&r.has.call(n,e)||k(i,a)&&r.has.call(n,i)||(n.add(a),et(n,`add`,a,a)),this},set(e,n){!t&&!P(n)&&!zt(n)&&(n=F(n));let r=F(this),{has:i,get:a}=xt(r),o=i.call(r,e);o||(e=F(e),o=i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?k(n,s)&&et(r,`set`,e,n,s):et(r,`add`,e,n),this},delete(e){let t=F(this),{has:n,get:r}=xt(t),i=n.call(t,e);i||(e=F(e),i=n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&et(t,`delete`,e,void 0,a),o},clear(){let e=F(this),t=e.size!==0,n=e.clear();return t&&et(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=St(r,e,t)}),n}function Tt(e,t){let n=wt(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var Et={get:Tt(!1,!1)},Dt={get:Tt(!1,!0)},Ot={get:Tt(!0,!1)},kt=new WeakMap,At=new WeakMap,jt=new WeakMap,Mt=new WeakMap;function Nt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function Pt(e){return zt(e)?e:Lt(e,!1,_t,Et,kt)}function Ft(e){return Lt(e,!1,yt,Dt,At)}function It(e){return Lt(e,!0,vt,Ot,jt)}function Lt(e,t,n,r,i){if(!y(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let a=i.get(e);if(a)return a;let o=Nt(C(e));if(o===0)return e;let s=new Proxy(e,o===2?r:n);return i.set(e,s),s}function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function P(e){return!!(e&&e.__v_isShallow)}function Bt(e){return e?!!e.__v_raw:!1}function F(e){let t=e&&e.__v_raw;return t?F(t):e}function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&ae(e,`__v_skip`,!0),e}var Ht=e=>y(e)?Pt(e):e,Ut=e=>y(e)?It(e):e;function I(e){return e?e.__v_isRef===!0:!1}function Wt(e){return Kt(e,!1)}function Gt(e){return Kt(e,!0)}function Kt(e,t){return I(e)?e:new qt(e,t)}var qt=class{constructor(e,t){this.dep=new Je,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:F(e),this._value=t?e:Ht(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?e:F(e),k(e,t)&&(this._rawValue=e,this._value=n?e:Ht(e),this.dep.trigger())}};function L(e){return I(e)?e.value:e}var Jt={get:(e,t,n)=>t===`__v_raw`?e:L(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return I(i)&&!I(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return Rt(e)?e:new Proxy(e,Jt)}var Xt=class{constructor(e){this.__v_isRef=!0,this._value=void 0;let t=this.dep=new Je,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}};function Zt(e){return new Xt(e)}var Qt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Je(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ke-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return Me(this,!0),!0}get value(){let e=this.dep.track();return Re(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function $t(e,t,n=!1){let r,i;return g(e)?r=e:(r=e.get,i=e.set),new Qt(r,i,n)}var en={},tn=new WeakMap,nn=void 0;function rn(e,t=!1,n=nn){if(n){let t=tn.get(n);t||tn.set(n,t=[]),t.push(e)}}function an(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:P(e)||o===!1||o===0?on(e,1):on(e),m,h,_,v,y=!1,b=!1;if(I(e)?(h=()=>e.value,y=P(e)):Rt(e)?(h=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Rt(e)||P(e)),h=()=>e.map(e=>{if(I(e))return e.value;if(Rt(e))return p(e);if(g(e))return f?f(e,2):e()})):h=g(e)?n?f?()=>f(e,2):e:()=>{if(_){Ue();try{_()}finally{We()}}let t=nn;nn=m;try{return f?f(e,3,[v]):e(v)}finally{nn=t}}:r,n&&o){let e=h,t=o===!0?1/0:o;h=()=>on(e(),t)}let x=Ee(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{let n=e(...t);return S(),n}}let C=b?Array(e.length).fill(en):en,w=e=>{if(m.flags&1&&(m.dirty||e)){if(n){let t=m.run();if(e||o||y||(b?t.some((e,t)=>k(e,C[t])):k(t,C))){_&&_();let e=nn;nn=m;try{let e=[t,C===en?void 0:b&&C[0]===en?[]:C,v];C=t,f?f(n,3,e):n(...e)}finally{nn=e}}}else m.run()}};return u&&u(w),m=new Oe(h),m.scheduler=l?()=>l(w,!1):w,v=e=>rn(e,!1,m),_=m.onStop=()=>{let e=tn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();tn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function on(e,t=1/0,n){if(t<=0||!y(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,I(e))on(e.value,t,n);else if(d(e))for(let r=0;r{on(e,t,n)});else if(w(e)){for(let r in e)on(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&on(e[r],t,n)}return e}function sn(e,t,n,r){try{return r?e(...r):e()}catch(e){cn(e,t,n)}}function R(e,t,n,r){if(g(e)){let i=sn(e,t,n,r);return i&&b(i)&&i.catch(e=>{cn(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a>>1,i=z[r],a=Cn(i);a=Cn(n)?z.push(e):z.splice(_n(t),0,e),e.flags|=1,yn()}}function yn(){hn||(hn=mn.then(wn))}function bn(e){if(!d(e))fn&&e.id===-1?fn.splice(pn+1,0,e):e.flags&1||(dn.push(e),e.flags|=1);else for(let t=0;tCn(e)-Cn(t));if(dn.length=0,fn){for(let t=0;te.id==null?e.flags&2?-1:1/0:e.id;function wn(e){try{for(un=0;un{r._d&&pa(-1);let i=En(t),a=la.length,o;try{o=e(...n)}finally{for(let e=la.length;e>a;e--)da();En(i),r._d&&pa(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function On(e,n){if(B===null)return e;let r=qa(B),i=e.dirs||(e.dirs=[]);for(let e=0;e1)return n&&g(t)?t.call(r&&r.proxy):t}}var jn=Symbol.for(`v-scx`),Mn=()=>V(jn);function Nn(e,t){return In(e,null,t)}function Pn(e,t){return In(e,null,{flush:`sync`})}function Fn(e,t,n){return In(e,t,n)}function In(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ba){if(c===`sync`){let e=Mn();f=e.__watcherHandles||(e.__watcherHandles=[])}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=X;u.call=(e,t,n)=>R(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{G(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():vn(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=an(e,n,u);return Ba&&(f?f.push(h):d&&h()),h}function Ln(e,t,n){let r=this.proxy,i=_(e)?e.includes(`.`)?Rn(r,e):()=>r[e]:e.bind(r,r),a;g(t)?a=t:(a=t.handler,n=t);let o=La(this),s=In(i,a.bind(r),n);return o(),s}function Rn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;ee.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===``),Un=e=>e&&(e.defer||e.defer===``),Wn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Gn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Kn=(e,t)=>{let n=e&&e.to;return _(n)?t?t(n):null:n},qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g,parentNode:_}}=l,v=Hn(t.props),{dynamicChildren:y}=t,b=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},x=(e=t)=>{let n=Hn(e.props),r=e.target=Kn(e.props,m),a=Qn(r,e,h,p);r&&(o!==`svg`&&Wn(r)?o=`svg`:o!==`mathml`&&Gn(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(b(e,r,a),Zn(e,!1)))},S=e=>{let t=()=>{if(zn.get(e)===t){if(zn.delete(e),Hn(e.props)){let t=_(e.el)||n;b(e,t,e.anchor),Zn(e,!0)}x(e)}};zn.set(e,t),G(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Un(t.props)||a&&a.pendingBranch){S(t);return}v&&(b(t,n,i),Zn(t,!0)),x()}else{t.el=e.el;let r=t.anchor=e.anchor,u=zn.get(e);if(u){u.flags|=8,zn.delete(e),S(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Hn(e.props),_=g?n:p,b=g?r:h;if(o===`svg`||Wn(p)?o=`svg`:(o===`mathml`||Gn(p))&&(o=`mathml`),y?(f(e.dynamicChildren,y,_,i,a,o,s),$i(e,t,!0)):c||d(e,t,_,b,i,a,o,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Jn(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=Kn(t.props,m);e&&(t.target=e,Jn(t,e,null,l,0))}else g&&Jn(t,p,h,l,1);Zn(t,v)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=Hn(f),m=a||!p,h=zn.get(e);if(h&&(h.flags|=8,zn.delete(e)),d&&(i(l),i(u)),a&&i(c),!h&&(p||d)&&o&16)for(let e=0;e{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}var U=[Function,Array],tr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:U,onEnter:U,onAfterEnter:U,onEnterCancelled:U,onBeforeLeave:U,onLeave:U,onAfterLeave:U,onLeaveCancelled:U,onBeforeAppear:U,onAppear:U,onAfterAppear:U,onAppearCancelled:U},nr=e=>{let t=e.subTree;return t.component?nr(t.component):t},rr={name:`BaseTransition`,props:tr,setup(e,{slots:t}){let n=Pa(),r=er();return()=>{let i=t.default&&dr(t.default(),!0),a=i&&i.length?ir(i):n.subTree?Ea():void 0;if(!a)return;let o=F(e),{mode:s}=o;if(r.isLeaving)return cr(a);let c=lr(a);if(!c)return cr(a);let l=sr(c,o,r,n,e=>l=e);c.type!==K&&ur(c,l);let u=n.subTree&&lr(n.subTree);if(u&&u.type!==K&&!va(u,c)&&nr(n).type!==K){let e=sr(u,o,r,n);if(ur(u,e),s===`out-in`&&c.type!==K)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},cr(a);s===`in-out`&&c.type!==K?e.delayLeave=(e,t,n)=>{let i=or(r,u);i[String(u.key)]=u,e[H]=()=>{t(),e[H]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return a}}};function ir(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==K){t=n;break}}return t}var ar=rr;function or(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function sr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=or(n,e),C=(e,t)=>{e&&R(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted){if(a)r=_||c;else return}t[H]&&t[H](!0);let i=S[x];i&&va(e,i)&&i.el[H]&&i.el[H](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted){if(a)r=v||l,i=y||u,o=b||f;else return}let s=!1;t[$n]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[$n]=void 0)};let c=t[$n].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[$n]&&t[$n](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[H]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[H]=void 0,S[i]===e&&delete S[i])};let o=t[H].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=sr(e,t,n,r,i);return i&&i(a),a}};return T}function cr(e){if(yr(e))return e=wa(e),e.children=null,e}function lr(e){if(!yr(e))return Vn(e.type)&&e.children?ir(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&g(n.default))return n.default()}}function ur(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;let n=e.component.subTree;ur(Vn(n.type)&&lr(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function dr(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;egr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(vr(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&gr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?qa(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,h=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=F(v),b=v===t?i:e=>!mr(h,e)&&u(y,e),x=(e,t)=>!(t&&mr(h,t));if(m!=null&&m!==p){if(_r(n),_(m))h[m]=null,b(m)&&(v[m]=null);else if(I(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(h[e.k]=null)}}if(g(p))sn(p,f,12,[l,h]);else{let t=_(p),n=I(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:h[p]:x(p)||!e.k?p.value:h[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)h[p]=[s],b(p)&&(v[p]=h[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(h[e.k]=t)}}else t?(h[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(h[e.k]=l))};if(l){let t=()=>{i(),hr.delete(e)};t.id=-1,hr.set(e,t),G(t,r)}else _r(e),i()}}}function _r(e){let t=hr.get(e);t&&(t.flags|=8,hr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var vr=e=>!!e.type.__asyncLoader,yr=e=>e.type.__isKeepAlive,br={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=Pa(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),G(()=>{a.isDeactivated=!1,a.a&&A(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Y(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;na(t.m),na(t.a),l(e,f,null,1,s),G(()=>{t.da&&A(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Y(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Er(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=Ja(vr(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!va(t,o))?p(t):o&&Er(o),i.delete(e),a.delete(e)}Fn(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>xr(e,t)),t&&m(e=>!xr(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(ia(n.subTree.type)?G(()=>{i.set(g,Dr(n.subTree))},n.subTree.suspense):i.set(g,Dr(n.subTree)))};return jr(_),Nr(_),Pr(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Dr(t);if(e.type===i.type&&e.key===i.key){Er(i);let e=i.component.da;e&&G(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!_a(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Dr(r);if(s.type===K)return o=null,s;let c=s.type,l=Ja(vr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!xr(u,l))||d&&l&&xr(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=wa(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&ur(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,ia(r.type)?r:s}}};function xr(e,t){return d(e)?e.some(e=>xr(e,t)):_(e)?e.split(`,`).includes(t):h(e)?(e.lastIndex=0,e.test(t)):!1}function Sr(e,t){wr(e,`a`,t)}function Cr(e,t){wr(e,`da`,t)}function wr(e,t,n=X){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Or(t,r,n),n){let e=n.parent;for(;e&&e.parent;)yr(e.parent.vnode)&&Tr(r,t,n,e),e=e.parent}}function Tr(e,t,n,r){let i=Or(t,e,r,!0);Fr(()=>{c(r[t],i)},n)}function Er(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Dr(e){return e.shapeFlag&128?e.ssContent:e}function Or(e,t,n=X,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||(t.__weh=(...r)=>{Ue();let i=La(n),a=R(t,n,e,r);return i(),We(),a});return r?i.unshift(a):i.push(a),a}}var kr=e=>(t,n=X)=>{(!Ba||e===`sp`)&&Or(e,(...e)=>t(...e),n)},Ar=kr(`bm`),jr=kr(`m`),Mr=kr(`bu`),Nr=kr(`u`),Pr=kr(`bum`),Fr=kr(`um`),Ir=kr(`sp`),Lr=kr(`rtg`),Rr=kr(`rtc`);function zr(e,t=X){Or(`ec`,e,t)}var Br=`components`;function Vr(e,t){return Wr(Br,e,!0,t)||e}var Hr=Symbol.for(`v-ndc`);function Ur(e){return _(e)?Wr(Br,e,!1)||e:e||Hr}function Wr(e,t,n=!0,r=!1){let i=B||X;if(i){let n=i.type;if(e===Br){let e=Ja(n,!1);if(e&&(e===t||e===D(t)||e===re(D(t))))return n}let a=Gr(i[e]||n[e],t)||Gr(i.appContext[e],t);return!a&&r?n:a}}function Gr(e,t){return e&&(e[t]||e[D(t)]||e[re(D(t))])}function Kr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||_(e)){let n=o&&Rt(e),r=!1,s=!1;n&&(r=!P(e),s=zt(e),e=nt(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}var Jr=e=>e?za(e)?qa(e):Jr(e.parent):null,Yr=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{vn(e.update)}),$nextTick:e=>e.n||(e.n=gn.bind(e.proxy)),$watch:e=>Ln.bind(e)}),Xr=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),Zr={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(Xr(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else $r&&(s[n]=0)}let d=Yr[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return Xr(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||Xr(n,c)||u(o,c)||u(i,c)||u(Yr,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function Qr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var $r=!0;function ei(e){let t=ii(e),n=e.proxy,i=e.ctx;$r=!1,t.beforeCreate&&ni(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:h,updated:_,activated:v,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:E,errorCaptured:te,serverPrefetch:D,expose:ne,inheritAttrs:O,components:re,directives:ie,filters:k}=t;if(u&&ti(u,i,null),s)for(let e in s){let t=s[e];g(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);y(t)&&(e.data=Pt(t))}if($r=!0,o)for(let e in o){let t=o[e],a=Z({get:g(t)?t.bind(n,n):g(t.get)?t.get.bind(n,n):r,set:!g(t)&&g(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)ri(c[e],i,n,e);if(l){let e=g(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{An(t,e[t])})}f&&ni(f,e,`c`);function A(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(A(Ar,p),A(jr,m),A(Mr,h),A(Nr,_),A(Sr,v),A(Cr,b),A(zr,te),A(Rr,ee),A(Lr,E),A(Pr,S),A(Fr,w),A(Ir,D),d(ne)){if(ne.length){let t=e.exposed||(e.exposed={});ne.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={})}T&&e.render===r&&(e.render=T),O!=null&&(e.inheritAttrs=O),re&&(e.components=re),ie&&(e.directives=ie),D&&pr(e)}function ti(e,t,n=r){d(e)&&(e=li(e));for(let n in e){let r=e[n],i;i=y(r)?`default`in r?V(r.from||n,r.default,!0):V(r.from||n):V(r),I(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function ni(e,t,n){R(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function ri(e,t,n,r){let i=r.includes(`.`)?Rn(n,r):()=>n[r];if(_(e)){let n=t[e];g(n)&&Fn(i,n)}else if(g(e))Fn(i,e.bind(n));else if(y(e)){if(d(e))e.forEach(e=>ri(e,t,n,r));else{let r=g(e.handler)?e.handler.bind(n):t[e.handler];g(r)&&Fn(i,r,e)}}}function ii(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>ai(c,e,o,!0)),ai(c,t,o)),y(t)&&a.set(t,c),c}function ai(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&ai(e,a,n,!0),i&&i.forEach(t=>ai(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=oi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var oi={data:si,props:di,emits:di,methods:ui,computed:ui,beforeCreate:W,created:W,beforeMount:W,mounted:W,beforeUpdate:W,updated:W,beforeDestroy:W,beforeUnmount:W,destroyed:W,unmounted:W,activated:W,deactivated:W,errorCaptured:W,serverPrefetch:W,components:ui,directives:ui,watch:fi,provide:si,inject:ci};function si(e,t){return t?e?function(){return s(g(e)?e.call(this,this):e,g(t)?t.call(this,this):t)}:t:e}function ci(e,t){return ui(li(e),li(t))}function li(e){if(d(e)){let t={};for(let n=0;n{let l,u=t,d;return Pn(()=>{let t=e[a];k(l,t)&&(l=t,c())}),{get(){return s(),r.get?r.get(l):l},set(e){let s=r.set?r.set(e):e;if(!k(s,l)&&!(u!==t&&k(e,u)))return;let f=i.vnode.props,p=!!(f&&(n in f||a in f||o in f)&&(`onUpdate:${n}`in f||`onUpdate:${a}`in f||`onUpdate:${o}`in f));p||(l=e,c()),i.emit(`update:${n}`,s),k(e,u)&&(k(e,s)&&!k(s,d)||p&&u!==t&&!k(s,l))&&c(),u=e,d=s}}});return c[Symbol.iterator]=()=>{let e=0;return{next(){return e<2?{value:e++?s||t:c,done:!1}:{done:!0}}}},c}var vi=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${D(t)}Modifiers`]||e[`${O(t)}Modifiers`];function yi(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&vi(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>_(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(D(n))];!l&&o&&(l=i[c=ie(O(n))]),l&&R(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,R(u,e,6,a)}}var bi=new WeakMap;function xi(e,t,n=!1){let r=n?bi:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!g(e)){let r=e=>{let n=xi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(y(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),y(e)&&r.set(e,o),o)}function Si(e,t){return!e||!a(t)?!1:(t=t.slice(2),t=t===`Once`?t:t.replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,O(t))||u(e,t))}function Ci(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=En(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Da(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Da(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:wi(c)}}catch(t){la.length=0,cn(t,e,1),v=J(K)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ti(y,a)),b=wa(b,y,!1,!0))}return n.dirs&&(b=wa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&ur(Vn(b.type)&&lr(b)||b,n.transition),v=b,En(_),v}var wi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Ei(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Di(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;tObject.create(Ai),Mi=e=>Object.getPrototypeOf(e)===Ai;function Ni(e,t,n,r=!1){let i={},a=ji();e.propsDefaults=Object.create(null),Fi(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);e.props=n?r?i:Ft(i):e.type.props?i:a,e.attrs=a}function Pi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{p=!0;let[t,n]=Ri(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return y(e)&&a.set(e,n),n;if(d(c))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Vi=e=>d(e)?e.map(Da):[Da(e)],Hi=(e,t,n)=>{if(t._n)return t;let r=Dn((...e)=>Vi(t(...e)),n);return r._c=!1,r},Ui=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Bi(n))continue;let i=e[n];if(g(i))t[n]=Hi(n,i,r);else if(i!=null){let e=Vi(i);t[n]=()=>e}}},Wi=(e,t)=>{let n=Vi(t);e.slots.default=()=>n},Gi=(e,t,n)=>{for(let r in t)(n||!Bi(r))&&(e[r]=t[r])},Ki=(e,t,n)=>{let r=e.slots=ji();if(e.vnode.shapeFlag&32){let e=t._;e?(Gi(r,t,n),n&&ae(r,`_`,e,!0)):Ui(t,r)}else t&&Wi(e,t)},qi=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:Gi(a,n,r):(o=!n.$stable,Ui(n,a)),s=n}else n&&(Wi(e,n),s={default:1});if(o)for(let e in a)!Bi(e)&&s[e]==null&&delete a[e]},G=aa;function Ji(e){return Yi(e)}function Yi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!va(e,t)&&(r=ve(e),pe(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case sa:y(e,t,n,r);break;case K:b(e,t,n,r);break;case ca:e==null&&x(t,n,r,o);break;case oa:re(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?ie(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,xe)}u!=null&&i?gr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&gr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)T(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),D(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},T=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&te(e.children,d,null,r,i,Xi(e,a),s,u),_&&kn(e,null,r,`created`),E(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!ee(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Y(f,r,e)}_&&kn(e,null,r,`beforeMount`);let v=Qi(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&G(()=>{try{f&&Y(f,r,e),v&&g.enter(d),_&&kn(e,null,r,`mounted`)}finally{}},i)},E=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t{for(let l=c;l{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&Zi(r,!1),(g=h.onVnodeBeforeUpdate)&&Y(g,r,n,e),f&&kn(n,e,r,`beforeUpdate`),r&&Zi(r,!0),d&&(!e.dynamicChildren||e.dynamicChildren.length!==d.length)&&(u=0,s=!1,d=null),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?ne(e.dynamicChildren,d,l,r,i,Xi(n,a),o):s||ce(e,n,l,null,r,i,Xi(n,a),o,!1),u>0){if(u&16)O(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t{g&&Y(g,r,n,e),f&&kn(n,e,r,`updated`)},i)},ne=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(n!==r){if(n!==t)for(let t in n)!ee(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(ee(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},re=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),te(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(ne(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&$i(e,t,!0)):ce(e,t,n,f,i,a,s,c,l)},ie=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):ae(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Na(e,r,i);if(yr(e)&&(s.ctx.renderer=xe),Va(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,oe,o),!e.el){let r=s.subTree=J(K);b(null,r,t,n),e.placeholder=r.el}}else oe(s,e,t,n,i,a,o)},ae=(e,t,n)=>{let r=t.component=e.component;if(Ei(e,t,n)){if(r.asyncDep&&!r.asyncResolved){se(r,t,n);return}r.next=t,r.update()}else t.el=e.el,r.vnode=t},oe=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=ta(e);if(n){t&&(t.el=c.el,se(e,t,o)),n.asyncDep.then(()=>{G(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;Zi(e,!1),t?(t.el=c.el,se(e,t,o)):t=c,n&&A(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Y(d,s,t,c),Zi(e,!0);let f=Ci(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ve(p),e,i,a),t.el=f.el,u===null&&ki(e,f.el),r&&G(r,i),(d=t.props&&t.props.onVnodeUpdated)&&G(()=>Y(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=vr(t);if(Zi(e,!1),l&&A(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Y(o,d,t),Zi(e,!0),s&&Ce){let t=()=>{e.subTree=Ci(e),Ce(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Ci(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&G(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;G(()=>Y(o,d,e),i)}(t.shapeFlag&256||d&&vr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&G(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Oe(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>vn(u),Zi(e,!0),l()},se=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Pi(e,t.props,r,n),qi(e,t.children,n),Ue(),xn(e),We()},ce=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){de(l,d,n,r,i,a,o,s,c);return}if(f&256){ue(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&_e(l,i,a),d!==l&&p(n,d)):u&16?m&16?de(l,d,n,r,i,a,o,s,c):_e(l,i,a,!0):(u&8&&p(n,``),m&16&&te(d,n,r,i,a,o,s,c))},ue=(e,t,r,i,a,o,s,c,l)=>{e=e||n,t=t||n;let u=e.length,d=t.length,f=Math.min(u,d),p=0;for(;pd?_e(e,a,o,!0,!1,f):te(t,r,i,a,o,s,c,l,f)},de=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?Oa(t[u]):Da(t[u]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?Oa(t[p]):Da(t[p]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=ep)for(;u<=f;)pe(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?Oa(t[u]):Da(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u=b){pe(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&va(n,t[_])){i=_;break}i===void 0?pe(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?ea(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){fe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,xe);return}if(c===oa){o(a,t,n);for(let e=0;el.enter(a),i));else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{let e=a._isLeaving||!!a[H];a._isLeaving&&a[H](!0),l.persisted&&!e?u():r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}}else o(a,t,n)},pe=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ue(),gr(s,null,n,e,!0),We()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!vr(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Y(_,t,e),u&6)ge(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&kn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,xe,r):l&&!l.hasOnce&&(a!==oa||d>0&&d&64)?_e(l,t,n,!1,!0):(a===oa&&d&384||!i&&u&16)&&_e(c,t,n),r&&me(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&G(()=>{_&&Y(_,t,e),h&&kn(e,null,t,`unmounted`),v&&(e.el=null)},n)},me=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===oa){he(n,r);return}if(t===ca){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},he=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},ge=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;na(c),na(l),r&&A(r),i.stop(),a&&(a.flags|=8,pe(o,e,t,n)),s&&G(s,t),G(()=>{e.isUnmounted=!0},t)},_e=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return ve(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Bn];return n?h(n):t},ye=!1,be=(e,t,n)=>{let r;e==null?t._vnode&&(pe(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ye||(ye=!0,xn(r),Sn(),ye=!1)},xe={p:v,um:pe,m:fe,r:me,mt:k,mc:te,pc:ce,pbc:ne,n:ve,o:e},Se,Ce;return i&&([Se,Ce]=i(xe)),{render:be,hydrate:Se,createApp:hi(be,Se)}}function Xi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function Zi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Qi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function $i(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-->0;)n[a]=o,o=t[o];return n}function ta(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ta(t)}function na(e){if(e)for(let t=0;te.__isSuspense;function aa(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):bn(e)}var oa=Symbol.for(`v-fgt`),sa=Symbol.for(`v-txt`),K=Symbol.for(`v-cmt`),ca=Symbol.for(`v-stc`),la=[],q=null;function ua(e=!1){la.push(q=e?null:[])}function da(){la.pop(),q=la[la.length-1]||null}var fa=1;function pa(e,t=!1){fa+=e,e<0&&q&&t&&(q.hasOnce=!0)}function ma(e){return e.dynamicChildren=fa>0?q||n:null,da(),fa>0&&q&&q.push(e),e}function ha(e,t,n,r,i,a){return ma(xa(e,t,n,r,i,a,!0))}function ga(e,t,n,r,i){return ma(J(e,t,n,r,i,!0))}function _a(e){return e?e.__v_isVNode===!0:!1}function va(e,t){return e.type===t.type&&e.key===t.key}var ya=({key:e})=>e==null?null:e,ba=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:_(e)||I(e)||g(e)?{i:B,r:e,k:t,f:!!n}:e);function xa(e,t=null,n=null,r=0,i=null,a=e===oa?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ya(t),ref:t&&ba(t),scopeId:Tn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:B};return s?(ka(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=_(n)?8:16),fa>0&&!o&&q&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&q.push(c),c}var J=Sa;function Sa(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Hr)&&(e=K),_a(e)){let r=wa(e,t,!0);return n&&ka(r,n),fa>0&&!a&&q&&(r.shapeFlag&6?q[q.indexOf(e)]=r:q.push(r)),r.patchFlag=-2,r}if(Ya(e)&&(e=e.__vccOpts),t){t=Ca(t);let{class:e,style:n}=t;e&&!_(e)&&(t.class=he(e)),y(n)&&(Bt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=_(e)?1:ia(e)?128:Vn(e)?64:y(e)?4:g(e)?2:0;return xa(e,t,n,r,i,o,a,!0)}function Ca(e){return e?Bt(e)||Mi(e)?s({},e):e:null}function wa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Aa(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ya(l),ref:t&&t.ref?n&&a?d(a)?a.concat(ba(t)):[a,ba(t)]:ba(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==oa?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&wa(e.ssContent),ssFallback:e.ssFallback&&wa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&ur(u,c.clone(u)),u}function Ta(e=` `,t=0){return J(sa,null,e,t)}function Ea(e=``,t=!1){return t?(ua(),ga(K,null,e)):J(K,null,e)}function Da(e){return e==null||typeof e==`boolean`?J(K):d(e)?J(oa,null,e.slice()):_a(e)?Oa(e):J(sa,null,String(e))}function Oa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:wa(e)}function ka(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`){if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),ka(e,n()),n._c&&(n._d=!0));return}{n=32;let r=t._;!r&&!Mi(t)?t._ctx=B:r===3&&B&&(B.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}}else if(g(t)){if(r&65){ka(e,{default:t});return}t={default:t,_ctx:B},n=32}else t=String(t),r&64?(n=16,t=[Ta(t)]):n=8;e.children=t,e.shapeFlag|=n}function Aa(...e){let t={};for(let n=0;nX||B,Fa,Ia;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Fa=t(`__VUE_INSTANCE_SETTERS__`,e=>X=e),Ia=t(`__VUE_SSR_SETTERS__`,e=>Ba=e)}var La=e=>{let t=X;return Fa(e),e.scope.on(),()=>{e.scope.off(),Fa(t)}},Ra=()=>{X&&X.scope.off(),Fa(null)};function za(e){return e.vnode.shapeFlag&4}var Ba=!1;function Va(e,t=!1,n=!1){t&&Ia(t);let{props:r,children:i}=e.vnode,a=za(e);Ni(e,r,a,t),Ki(e,i,n||t);let o=a?Ha(e,t):void 0;return t&&Ia(!1),o}function Ha(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zr);let{setup:r}=n;if(r){Ue();let n=e.setupContext=r.length>1?Ka(e):null,i=La(e),a=sn(r,e,0,[e.props,n]),o=b(a);if(We(),i(),(o||e.sp)&&!vr(e)&&pr(e),o){if(a.then(Ra,Ra),t)return a.then(n=>{Ia(!0);try{Ua(e,n,t)}finally{Ia(!1)}}).catch(t=>{cn(t,e,0)});e.asyncDep=a}else Ua(e,a,t)}else Wa(e,t)}function Ua(e,t,n){g(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:y(t)&&(e.setupState=Yt(t)),Wa(e,n)}function Wa(e,t,n){let i=e.type;e.render||(e.render=i.render||r);{let t=La(e);Ue();try{ei(e)}finally{We(),t()}}}var Ga={get(e,t){return N(e,`get`,``),e[t]}};function Ka(e){return{attrs:new Proxy(e.attrs,Ga),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function qa(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Vt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yr)return Yr[n](e)},has(e,t){return t in e||t in Yr}})):e.proxy}function Ja(e,t=!0){return g(e)?e.displayName||e.name:e.name||t&&e.__name}function Ya(e){return g(e)&&`__vccOpts`in e}var Z=(e,t)=>$t(e,t,Ba);function Xa(e,t,n){try{pa(-1);let r=arguments.length;return r===2?y(t)&&!d(t)?_a(t)?J(e,null,[t]):J(e,t):J(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_a(n)&&(n=[n]),J(e,t,n))}finally{pa(1)}}var Za=`3.5.41`,Qa=void 0,$a=typeof window<`u`&&window.trustedTypes;if($a)try{Qa=$a.createPolicy(`vue`,{createHTML:e=>e})}catch{}var eo=Qa?e=>Qa.createHTML(e):e=>e,to=`http://www.w3.org/2000/svg`,no=`http://www.w3.org/1998/Math/MathML`,ro=typeof document<`u`?document:null,io=ro&&ro.createElement(`template`),ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i=t===`svg`?ro.createElementNS(to,e):t===`mathml`?ro.createElementNS(no,e):n?ro.createElement(e,{is:n}):ro.createElement(e);return e===`select`&&r&&r.multiple!=null&&i.setAttribute(`multiple`,r.multiple),i},createText:e=>ro.createTextNode(e),createComment:e=>ro.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ro.querySelector(e),setScopeId(e,t){e.setAttribute(t,``)},insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;if(i&&(i===a||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==a&&(i=i.nextSibling););else{io.innerHTML=eo(r===`svg`?`${e}`:r===`mathml`?`${e}`:e);let i=io.content;if(r===`svg`||r===`mathml`){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},oo=`transition`,so=`animation`,co=Symbol(`_vtc`),lo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uo=s({},tr,lo),fo=(e=>(e.displayName=`Transition`,e.props=uo,e))((e,{slots:t})=>Xa(ar,ho(e),t)),po=(e,t=[])=>{d(e)?e.forEach(e=>e(...t)):e&&e(...t)},mo=e=>e?d(e)?e.some(e=>e.length>1):e.length>1:!1;function ho(e){let t={};for(let n in e)n in lo||(t[n]=e[n]);if(e.css===!1)return t;let{name:n=`v`,type:r,duration:i,enterFromClass:a=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=a,appearActiveClass:u=o,appearToClass:d=c,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,h=go(i),g=h&&h[0],_=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:C=v,onAppear:w=y,onAppearCancelled:T=b}=t,ee=(e,t,n,r)=>{e._enterCancelled=r,yo(e,t?d:c),yo(e,t?u:o),n&&n()},E=(e,t)=>{e._isLeaving=!1,yo(e,f),yo(e,m),yo(e,p),t&&t()},te=e=>(t,n)=>{let i=e?w:y,o=()=>ee(t,e,n);po(i,[t,o]),bo(()=>{yo(t,e?l:a),vo(t,e?d:c),mo(i)||So(t,r,g,o)})};return s(t,{onBeforeEnter(e){po(v,[e]),vo(e,a),vo(e,o)},onBeforeAppear(e){po(C,[e]),vo(e,l),vo(e,u)},onEnter:te(!1),onAppear:te(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>E(e,t);vo(e,f),e._enterCancelled?(vo(e,p),Eo(e)):(Eo(e),vo(e,p)),bo(()=>{e._isLeaving&&(yo(e,f),vo(e,m),mo(x)||So(e,r,_,n))}),po(x,[e,n])},onEnterCancelled(e){ee(e,!1,void 0,!0),po(b,[e])},onAppearCancelled(e){ee(e,!0,void 0,!0),po(T,[e])},onLeaveCancelled(e){E(e),po(S,[e])}})}function go(e){if(e==null)return null;if(y(e))return[_o(e.enter),_o(e.leave)];{let t=_o(e);return[t,t]}}function _o(e){return se(e)}function vo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[co]||(e[co]=new Set)).add(t)}function yo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[co];n&&(n.delete(t),n.size||(e[co]=void 0))}function bo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}var xo=0;function So(e,t,n,r){let i=e._endId=++xo,a=()=>{i===e._endId&&r()};if(n!=null)return setTimeout(a,n);let{type:o,timeout:s,propCount:c}=Co(e,t);if(!o)return r();let l=o+`end`,u=0,d=()=>{e.removeEventListener(l,f),a()},f=t=>{t.target===e&&++u>=c&&d()};setTimeout(()=>{u(n[e]||``).split(`, `),i=r(`${oo}Delay`),a=r(`${oo}Duration`),o=wo(i,a),s=r(`${so}Delay`),c=r(`${so}Duration`),l=wo(s,c),u=null,d=0,f=0;t===oo?o>0&&(u=oo,d=o,f=a.length):t===so?l>0&&(u=so,d=l,f=c.length):(d=Math.max(o,l),u=d>0?o>l?oo:so:null,f=u?u===oo?a.length:c.length:0);let p=u===oo&&/\b(?:transform|all)(?:,|$)/.test(r(`${oo}Property`).toString());return{type:u,timeout:d,propCount:f,hasTransform:p}}function wo(e,t){for(;e.lengthTo(t)+To(e[n])))}function To(e){return e===`auto`?0:Number(e.slice(0,-1).replace(`,`,`.`))*1e3}function Eo(e){return(e?e.ownerDocument:document).body.offsetHeight}function Do(e,t,n){let r=e[co];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==null?e.removeAttribute(`class`):n?e.setAttribute(`class`,t):e.className=t}var Oo=Symbol(`_vod`),ko=Symbol(`_vsh`),Ao={name:`show`,beforeMount(e,{value:t},{transition:n}){e[Oo]=e.style.display===`none`?``:e.style.display,n&&t?n.beforeEnter(e):jo(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),jo(e,!0),r.enter(e)):r.leave(e,()=>{jo(e,!1)}):jo(e,t))},beforeUnmount(e,{value:t}){jo(e,t)}};function jo(e,t){e.style.display=t?e[Oo]:`none`,e[ko]=!t}var Mo=Symbol(``),No=/(?:^|;)\s*display\s*:/;function Po(e,t,n){let r=e.style,i=_(n),a=!1;if(n&&!i){if(t){if(_(t))for(let e of t.split(`;`)){let t=e.slice(0,e.indexOf(`:`)).trim();n[t]==null&&Io(r,t,``)}else for(let e in t)n[e]==null&&Io(r,e,``)}for(let i in n){i===`display`&&(a=!0);let o=n[i];o==null?Io(r,i,``):Bo(e,i,!_(t)&&t?t[i]:void 0,o)||Io(r,i,o)}}else if(i){if(t!==n){let e=r[Mo];e&&(n+=`;`+e),r.cssText=n,a=No.test(n)}}else t&&e.removeAttribute(`style`);Oo in e&&(e[Oo]=a?r.display:``,e[ko]&&(r.display=`none`))}var Fo=/\s*!important$/;function Io(e,t,n){if(d(n))n.forEach(n=>Io(e,t,n));else if(n==null&&(n=``),t.startsWith(`--`))e.setProperty(t,n);else{let r=zo(e,t);Fo.test(n)?e.setProperty(O(r),n.replace(Fo,``),`important`):e[r]=n}}var Lo=[`Webkit`,`Moz`,`ms`],Ro={};function zo(e,t){let n=Ro[t];if(n)return n;let r=D(t);if(r!==`filter`&&r in e)return Ro[t]=r;r=re(r);for(let n=0;nZo||(Qo.then(()=>Zo=0),Zo=Date.now());function es(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=n.attached)return;let r=n.value;if(d(r)){let n=e.stopImmediatePropagation;e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0};let i=r.slice(),a=[e];for(let n=0;ne.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ns=(e,t,n,r,i,s)=>{let c=i===`svg`;t===`class`?Do(e,r,c):t===`style`?Po(e,n,r):a(t)?o(t)||qo(e,t,n,r,s):(t[0]===`.`?(t=t.slice(1),1):t[0]===`^`?(t=t.slice(1),0):rs(e,t,r,c))?(Uo(e,t,r),!e.tagName.includes(`-`)&&(t===`value`||t===`checked`||t===`selected`)&&Ho(e,t,r,c,s,t!==`value`)):e._isVueCE&&(is(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!_(r)))?Uo(e,D(t),r,s,t):(t===`true-value`?e._trueValue=r:t===`false-value`&&(e._falseValue=r),Ho(e,t,r,c))};function rs(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t in e&&ts(t)&&g(n));if(t===`spellcheck`||t===`draggable`||t===`translate`||t===`autocorrect`||t===`sandbox`&&e.tagName===`IFRAME`||t===`form`||t===`list`&&e.tagName===`INPUT`||t===`type`&&e.tagName===`TEXTAREA`)return!1;if(t===`width`||t===`height`){let t=e.tagName;if(t===`IMG`||t===`VIDEO`||t===`CANVAS`||t===`SOURCE`)return!1}return ts(t)&&_(n)?!1:t in e}function is(e,t){let n=e._def.props;if(!n)return!1;let r=D(t);return Array.isArray(n)?n.some(e=>D(e)===r):Object.keys(n).some(e=>D(e)===r)}var as={esc:`escape`,space:` `,up:`arrow-up`,left:`arrow-left`,right:`arrow-right`,down:`arrow-down`,delete:`backspace`},os=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(`.`);return n[r]||(n[r]=(n=>{if(!(`key`in n))return;let r=O(n.key);if(t.some(e=>e===r||as[e]===r))return e(n)}))},ss=s({patchProp:ns},ao),cs;function ls(){return cs||(cs=Ji(ss))}var us=((...e)=>{let t=ls().createApp(...e),{mount:n}=t;return t.mount=e=>{let r=fs(e);if(!r)return;let i=t._component;!g(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent=``);let a=n(r,!1,ds(r));return r instanceof Element&&(r.removeAttribute(`v-cloak`),r.setAttribute(`data-v-app`,``)),a},t});function ds(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLElement==`function`&&e instanceof MathMLElement)return`mathml`}function fs(e){return _(e)?document.querySelector(e):e}function ps(){}var ms=Object.assign,hs=typeof window<`u`,gs=e=>typeof e==`object`&&!!e,_s=e=>e!=null,vs=e=>typeof e==`function`,ys=e=>gs(e)&&vs(e.then)&&vs(e.catch),bs=e=>typeof e==`number`||/^\d+(\.\d+)?$/.test(e),xs=()=>hs?/ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()):!1;function Ss(e,t){let n=t.split(`.`),r=e;return n.forEach(e=>{var t;r=gs(r)?(t=r[e])==null?``:t:``}),r}function Cs(e,t,n){return t.reduce((t,r)=>((!n||e[r]!==void 0)&&(t[r]=e[r]),t),{})}var ws=e=>Array.isArray(e)?e:[e],Ts=[Number,String],Es={type:Boolean,default:!0},Ds=e=>({type:e,required:!0}),Os=e=>({type:Number,default:e}),ks=e=>({type:Ts,default:e}),As=e=>({type:String,default:e}),js=typeof window<`u`;function Ms(e){return js?requestAnimationFrame(e):-1}function Ns(e){js&&cancelAnimationFrame(e)}function Ps(e){Ms(()=>Ms(e))}var Fs=e=>e===window,Is=(e,t)=>({top:0,left:0,right:e,bottom:t,width:e,height:t}),Ls=e=>{let t=L(e);if(Fs(t)){let e=t.innerWidth,n=t.innerHeight;return Is(e,n)}return t!=null&&t.getBoundingClientRect?t.getBoundingClientRect():Is(0,0)};function Rs(e){let t=V(e,null);if(t){let e=Pa(),{link:n,unlink:r,internalChildren:i}=t;return n(e),Fr(()=>r(e)),{parent:t,index:Z(()=>i.indexOf(e))}}return{parent:null,index:Wt(-1)}}function zs(e){let t=[],n=e=>{Array.isArray(e)&&e.forEach(e=>{var r;_a(e)&&(t.push(e),(r=e.component)!=null&&r.subTree&&(t.push(e.component.subTree),n(e.component.subTree.children)),e.children&&n(e.children))})};return n(e),t}var Bs=(e,t)=>{let n=e.indexOf(t);return n===-1?e.findIndex(e=>t.key!==void 0&&t.key!==null&&e.type===t.type&&e.key===t.key):n};function Vs(e,t,n){let r=zs(e.subTree.children);n.sort((e,t)=>Bs(r,e.vnode)-Bs(r,t.vnode));let i=n.map(e=>e.proxy);t.sort((e,t)=>i.indexOf(e)-i.indexOf(t))}function Hs(e){let t=Pt([]),n=Pt([]),r=Pa();return{children:t,linkChildren:i=>{An(e,Object.assign({link:e=>{e.proxy&&(n.push(e),t.push(e.proxy),Vs(r,t,n))},unlink:e=>{let r=n.indexOf(e);t.splice(r,1),n.splice(r,1)},children:t,internalChildren:n},i))}}}function Us(e){let t;jr(()=>{e(),gn(()=>{t=!0})}),Sr(()=>{t&&e()})}function Ws(e,t,n={}){if(!js)return;let{target:r=window,passive:i=!1,capture:a=!1}=n,o=!1,s,c=n=>{if(o)return;let r=L(n);r&&!s&&(r.addEventListener(e,t,{capture:a,passive:i}),s=!0)},l=n=>{if(o)return;let r=L(n);r&&s&&(r.removeEventListener(e,t,a),s=!1)};Fr(()=>l(r)),Cr(()=>l(r)),Us(()=>c(r));let u;return I(r)&&(u=Fn(r,(e,t)=>{l(t),c(e)})),()=>{u==null||u(),l(r),o=!0}}var Gs,Ks;function qs(){if(!Gs&&(Gs=Wt(0),Ks=Wt(0),js)){let e=()=>{Gs.value=window.innerWidth,Ks.value=window.innerHeight};e(),window.addEventListener(`resize`,e,{passive:!0}),window.addEventListener(`orientationchange`,e,{passive:!0})}return{width:Gs,height:Ks}}var Js=/scroll|auto|overlay/i,Ys=js?window:void 0;function Xs(e){return e.tagName!==`HTML`&&e.tagName!==`BODY`&&e.nodeType===1}function Zs(e,t=Ys){let n=e;for(;n&&n!==t&&Xs(n);){let{overflowY:e}=window.getComputedStyle(n);if(Js.test(e))return n;n=n.parentNode}return t}function Qs(e,t=Ys){let n=Wt();return jr(()=>{e.value&&(n.value=Zs(e.value,t))}),n}var $s;function ec(){if(!$s&&($s=Wt(`visible`),js)){let e=()=>{$s.value=document.hidden?`hidden`:`visible`};e(),window.addEventListener(`visibilitychange`,e)}return $s}var tc=Symbol(`van-field`);function nc(e){let t=`scrollTop`in e?e.scrollTop:e.pageYOffset;return Math.max(t,0)}function rc(e,t){`scrollTop`in e?e.scrollTop=t:e.scrollTo(e.scrollX,t)}function ic(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}function ac(e){rc(window,e),rc(document.body,e)}function oc(e,t){if(e===window)return 0;let n=t?nc(t):ic();return Ls(e).top+n}var sc=xs();function cc(){sc&&ac(ic())}var lc=e=>e.stopPropagation();function uc(e,t){(typeof e.cancelable!=`boolean`||e.cancelable)&&e.preventDefault(),t&&lc(e)}function dc(e){let t=L(e);if(!t)return!1;let n=window.getComputedStyle(t),r=n.display===`none`,i=t.offsetParent===null&&n.position!==`fixed`;return r||i}var{width:fc,height:pc}=qs();function mc(e){if(_s(e))return bs(e)?`${e}px`:String(e)}function hc(e){if(_s(e)){if(Array.isArray(e))return{width:mc(e[0]),height:mc(e[1])};let t=mc(e);return{width:t,height:t}}}function gc(e){let t={};return e!==void 0&&(t.zIndex=+e),t}var _c;function vc(){if(!_c){let e=document.documentElement,t=e.style.fontSize||window.getComputedStyle(e).fontSize;_c=parseFloat(t)}return _c}function yc(e){return e=e.replace(/rem/g,``),+e*vc()}function bc(e){return e=e.replace(/vw/g,``),+e*fc.value/100}function xc(e){return e=e.replace(/vh/g,``),+e*pc.value/100}function Sc(e){if(typeof e==`number`)return e;if(hs){if(e.includes(`rem`))return yc(e);if(e.includes(`vw`))return bc(e);if(e.includes(`vh`))return xc(e)}return parseFloat(e)}var Cc=/-(\w)/g,wc=e=>e.replace(Cc,(e,t)=>t.toUpperCase()),Tc=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase().replace(/^-/,``),Ec=(e,t,n)=>Math.min(Math.max(e,t),n);function Dc(e,t,n){let r=e.indexOf(t);return r===-1?e:t===`-`&&r!==0?e.slice(0,r):e.slice(0,r+1)+e.slice(r).replace(n,``)}function Oc(e,t=!0,n=!0){e=t?Dc(e,`.`,/\./g):e.split(`.`)[0],e=n?Dc(e,`-`,/-/g):e.replace(/-/,``);let r=t?/[^-0-9.]/g:/[^-0-9]/g;return e.replace(r,``)}var{hasOwnProperty:kc}=Object.prototype;function Ac(e,t,n){let r=t[n];_s(r)&&(e[n]=!kc.call(e,n)||!gs(r)?r:jc(Object(e[n]),r))}function jc(e,t){return Object.keys(t).forEach(n=>{Ac(e,t,n)}),e}var Mc={name:`姓名`,tel:`电话`,save:`保存`,clear:`清空`,undo:`撤销`,cancel:`取消`,confirm:`确认`,delete:`删除`,loading:`加载中...`,noCoupon:`暂无优惠券`,nameEmpty:`请填写姓名`,addContact:`添加联系人`,telInvalid:`请填写正确的电话`,vanCalendar:{end:`结束`,start:`开始`,title:`日期选择`,weekdays:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],monthTitle:(e,t)=>`${e}\u5E74${t}\u6708`,rangePrompt:e=>`\u6700\u591A\u9009\u62E9 ${e} \u5929`},vanCascader:{select:`请选择`},vanPagination:{prev:`上一页`,next:`下一页`},vanPullRefresh:{pulling:`下拉即可刷新...`,loosing:`释放即可刷新...`},vanSubmitBar:{label:`合计:`},vanCoupon:{unlimited:`无门槛`,discount:e=>`${e}\u6298`,condition:e=>`\u6EE1${e}\u5143\u53EF\u7528`},vanCouponCell:{title:`优惠券`,count:e=>`${e}\u5F20\u53EF\u7528`},vanCouponList:{exchange:`兑换`,close:`不使用`,enable:`可用`,disabled:`不可用`,placeholder:`输入优惠码`},vanAddressEdit:{area:`地区`,areaEmpty:`请选择地区`,addressEmpty:`请填写详细地址`,addressDetail:`详细地址`,defaultAddress:`设为默认收货地址`},vanAddressList:{add:`新增地址`}},Nc=Wt(`zh-CN`),Pc=Pt({"zh-CN":Mc}),Fc={messages(){return Pc[Nc.value]},use(e,t){Nc.value=e,this.add({[e]:t})},add(e={}){jc(Pc,e)}};function Ic(e){let t=wc(e)+`.`;return(e,...n)=>{let r=Fc.messages(),i=Ss(r,t+e)||Ss(r,e);return vs(i)?i(...n):i}}function Lc(e,t){return t?typeof t==`string`?` ${e}--${t}`:Array.isArray(t)?t.reduce((t,n)=>t+Lc(e,n),``):Object.keys(t).reduce((n,r)=>n+(t[r]?Lc(e,r):``),``):``}function Rc(e){return(t,n)=>(t&&typeof t!=`string`&&(n=t,t=``),t=t?`${e}__${t}`:e,`${t}${Lc(t,n)}`)}function zc(e){let t=`van-${e}`;return[t,Rc(t),Ic(t)]}function Bc(e){return e.install=t=>{let{name:n}=e;n&&(t.component(n,e),t.component(wc(`-${n}`),e))},e}var Vc=2e3,Hc=()=>++Vc,Uc=e=>{Vc=e},[Wc,Gc]=zc(`config-provider`),Kc=Symbol(Wc),qc={tag:As(`div`),theme:As(`light`),zIndex:Number,themeVars:Object,themeVarsDark:Object,themeVarsLight:Object,themeVarsScope:As(`local`),iconPrefix:String};function Jc(e){return e.replace(/([a-zA-Z])(\d)/g,`$1-$2`)}function Yc(e){let t={};return Object.keys(e).forEach(n=>{let r=Jc(Tc(n));t[`--van-${r}`]=e[n]}),t}function Xc(e={},t={}){Object.keys(e).forEach(n=>{e[n]!==t[n]&&document.documentElement.style.setProperty(n,e[n])}),Object.keys(t).forEach(t=>{e[t]||document.documentElement.style.removeProperty(t)})}var Zc=Bc(fr({name:Wc,props:qc,setup(e,{slots:t}){let n=Z(()=>Yc(ms({},e.themeVars,e.theme===`dark`?e.themeVarsDark:e.themeVarsLight)));if(hs){let t=()=>{document.documentElement.classList.add(`van-theme-${e.theme}`)},r=(t=e.theme)=>{document.documentElement.classList.remove(`van-theme-${t}`)};Fn(()=>e.theme,(e,n)=>{n&&r(n),t()},{immediate:!0}),Sr(t),Cr(r),Pr(r),Fn(n,(t,n)=>{e.themeVarsScope===`global`&&Xc(t,n)}),Fn(()=>e.themeVarsScope,(e,t)=>{t===`global`&&Xc({},n.value),e===`global`&&Xc(n.value,{})}),e.themeVarsScope===`global`&&Xc(n.value,{})}return An(Kc,e),Nn(()=>{e.zIndex!==void 0&&Uc(e.zIndex)}),()=>J(e.tag,{class:Gc(),style:e.themeVarsScope===`local`?n.value:void 0},{default:()=>{var e;return[(e=t.default)==null?void 0:e.call(t)]}})}}));function Qc(e=`(min-width: 768px)`){let t=typeof window<`u`&&window.matchMedia?window.matchMedia(e):null,n=Wt(t?t.matches:!1),r=e=>{n.value=e.matches};return jr(()=>{var e;return t==null||(e=t.addEventListener)==null?void 0:e.call(t,`change`,r)}),Pr(()=>{var e;return t==null||(e=t.removeEventListener)==null?void 0:e.call(t,`change`,r)}),n}function $c(){let e=typeof window<`u`&&window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,t=Wt(e?e.matches:!1),n=e=>{t.value=e.matches};return jr(()=>{var t;return e==null||(t=e.addEventListener)==null?void 0:t.call(e,`change`,n)}),Pr(()=>{var t;return e==null||(t=e.removeEventListener)==null?void 0:t.call(e,`change`,n)}),t}function el(e,t){let n,r=!1,i=()=>{n!==void 0&&window.clearInterval(n),n=void 0},a=()=>{i(),n=window.setInterval(()=>{document.visibilityState===`visible`&&e()},t)},o=()=>{r&&(document.visibilityState===`visible`?(e(),a()):i())};jr(()=>{r=!0,a(),document.addEventListener(`visibilitychange`,o)}),Sr(()=>{r||(r=!0,e(),a())}),Cr(()=>{r=!1,i()}),Pr(()=>{r=!1,i(),document.removeEventListener(`visibilitychange`,o)})}var tl=fr({__name:`App`,setup(e){let t=$c(),n=Z(()=>t.value?`dark`:`light`),r={primaryColor:`#2f6fff`,successColor:`#13b77a`,warningColor:`#e8870c`,dangerColor:`#ff5358`,navBarBackground:`var(--app-bg)`,navBarTitleTextColor:`var(--app-text)`,cellGroupInsetRadius:`14px`,buttonRadius:`12px`,buttonLargeHeight:`48px`},i={primaryColor:`#5b8cff`,successColor:`#2fd19a`,warningColor:`#ffae4a`,dangerColor:`#ff6b70`,background:`#0b1222`,background2:`#131c31`,background3:`#18223b`,textColor:`#e6ecfb`,textColor2:`#8b97b6`,textColor3:`#6c7898`,borderColor:`rgba(140,160,210,.16)`,activeColor:`#1c2743`};return(e,t)=>{let a=Vr(`RouterView`);return ua(),ga(L(Zc),{theme:n.value,"theme-vars-scope":`global`,"theme-vars":r,"theme-vars-dark":i},{default:Dn(()=>[J(a)]),_:1},8,[`theme`])}}});function nl(e){"@babel/helpers - typeof";return nl=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},nl(e)}function rl(e,t){if(nl(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(nl(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function il(e){var t=rl(e,`string`);return nl(t)==`symbol`?t:t+``}function al(e,t,n){return(t=il(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ol(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function sl(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&ol(e.default)}var Q=Object.assign;function cl(e,t){let n={};for(let r in t){let i=t[r];n[r]=ul(i)?i.map(e):e(i)}return n}var ll=()=>{},ul=Array.isArray;function dl(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var fl=Symbol(``);function pl(e,t){return Q(Error(),{type:e,[fl]:!0},t)}function ml(e,t){return e instanceof Error&&fl in e&&(t==null||!!(e.type&t))}var hl=Symbol(``),gl=Symbol(``),_l=Symbol(``),vl=Symbol(``),yl=Symbol(``);function bl(){return V(_l)}function xl(e){return V(vl)}var Sl=typeof document<`u`,Cl=/#/g,wl=/&/g,Tl=/\//g,El=/=/g,Dl=/\?/g,Ol=/\+/g,kl=/%5B/g,Al=/%5D/g,jl=/%5E/g,Ml=/%60/g,Nl=/%7B/g,Pl=/%7C/g,Fl=/%7D/g,Il=/%20/g;function Ll(e){return e==null?``:encodeURI(``+e).replace(Pl,`|`).replace(kl,`[`).replace(Al,`]`)}function Rl(e){return Ll(e).replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function zl(e){return Ll(e).replace(Ol,`%2B`).replace(Il,`+`).replace(Cl,`%23`).replace(wl,`%26`).replace(Ml,"`").replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function Bl(e){return zl(e).replace(El,`%3D`)}function Vl(e){return Ll(e).replace(Cl,`%23`).replace(Dl,`%3F`)}function Hl(e){return Vl(e).replace(Tl,`%2F`)}function Ul(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var Wl=/\/$/,Gl=e=>e.replace(Wl,``);function Kl(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r=r||t.slice(0,s),o=t.slice(s,t.length)),r=eu(r==null?t:r,n),{fullPath:r+a+o,path:r,query:i,hash:Ul(o)}}function ql(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Jl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function Yl(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&Xl(t.matched[r],n.matched[i])&&Zl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Xl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Zl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ql(e[n],t[n]))return!1;return!0}function Ql(e,t){return ul(e)?$l(e,t):ul(t)?$l(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function $l(e,t){return ul(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function eu(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o1&&a--;else break}return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var tu={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0};function nu(e){if(!e){if(Sl){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^/]+/,``)}else e=`/`}return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Gl(e)}var ru=/^[^#]+#/;function iu(e,t){return e.replace(ru,`#`)+t}function au(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var ou=()=>({left:window.scrollX,top:window.scrollY});function su(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=au(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function cu(e,t){return(history.state?history.state.position-t:-1)+e}var lu=new Map;function uu(e,t){lu.set(e,t)}function du(e){let t=lu.get(e);return lu.delete(e),t}function fu(e){return typeof e==`string`||e&&typeof e==`object`}function pu(e){return typeof e==`string`||typeof e==`symbol`}function mu(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;ee&&zl(e)):[r&&zl(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function gu(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=ul(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function _u(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function vu(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(pl(4,{from:n,to:t})):e instanceof Error?c(e):fu(e)?c(pl(2,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function yu(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(t===`beforeRouteEnter`||o.instances[e]){if(ol(s)){let c=(s.__vccOpts||s)[t];c&&a.push(vu(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=sl(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&vu(c,n,r,o,e,i)()}))}}}return a}function bu(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;oXl(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>Xl(e,s))||i.push(s))}return[n,r,i]}var xu=()=>location.protocol+`//`+location.host;function Su(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Jl(n,``)}return Jl(n,e)+r+i}function Cu(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=Su(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:`pop`,direction:u?u>0?`forward`:`back`:``})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(Q({},e.state,{scroll:ou()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function wu(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ou():null}}function Tu(e){let{history:t,location:n}=window,r={value:Su(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:xu()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,Q({},t.state,wu(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=Q({},i.value,t.state,{forward:e,scroll:ou()});a(o.current,o,!0),a(e,Q({},wu(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function Eu(e){e=nu(e);let t=Tu(e),n=Cu(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=Q({location:``,base:e,go:r,createHref:iu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Du(e){return e=location.host?e||location.pathname+location.search:``,e.includes(`#`)||(e+=`#`),Eu(e)}var Ou={type:0,value:``},ku=/[a-zA-Z0-9_]/;function Au(e){if(!e)return[[]];if(e===`/`)return[[Ou]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=0,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&(n===0?a.push({type:0,value:l}):n===1||n===2||n===3?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:1,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),l=``)}function f(){l+=c}for(;st.length?t.length===1&&t[0]===80?1:-1:0}function Iu(e,t){let n=0,r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}var Ru={strict:!1,end:!0,sensitive:!1};function zu(e,t,n){let r=Q(Pu(Au(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Bu(e,t){let n=[],r=new Map;t=dl(Ru,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Hu(e);s.aliasOf=r&&r.record;let l=dl(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Hu(Q({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=zu(t,n,l),r?r.alias.push(d):(f=f||d,f!==d&&f.alias.push(d),i&&e.name&&!Wu(d)&&o(e.name)),Ju(d)&&c(d),s.children){let e=s.children;for(let t=0;t{o(f)}:ll}function o(e){if(pu(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=Ku(e,n);n.splice(t,0,e),e.record.name&&!Wu(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw pl(1,{location:e});s=i.record.name,a=Q(Vu(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&Vu(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name,i.keys.forEach(e=>{e.optional&&!a[e.name]&&delete a[e.name]}));else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw pl(1,{location:e,currentLocation:t});s=i.record.name,a=Q({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Gu(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function Vu(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Hu(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Uu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Uu(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Wu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Gu(e){return e.reduce((e,t)=>Q(e,t.meta),{})}function Ku(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Iu(e,t[i])<0?r=i:n=i+1}let i=qu(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function qu(e){let t=e;for(;t=t.parent;)if(Ju(t)&&Iu(e,t)===0)return t}function Ju({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Yu(e){let t=V(_l),n=V(vl),r=Z(()=>{let n=L(e.to);return t.resolve(n)}),i=Z(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(Xl.bind(null,i));if(o>-1)return o;let s=ed(e[t-2]);return t>1&&ed(i)===s&&a[a.length-1].path!==s?a.findIndex(Xl.bind(null,e[t-2])):o}),a=Z(()=>i.value>-1&&$u(n.params,r.value.params)),o=Z(()=>i.value>-1&&i.value===n.matched.length-1&&Zl(n.params,r.value.params));function s(n={}){if(Qu(n)){let n=t[L(e.replace)?`replace`:`push`](L(e.to)).catch(ll);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:Z(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Xu(e){return e.length===1?e[0]:e}var Zu=fr({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:Yu,setup(e,{slots:t}){let n=Pt(Yu(e)),{options:r}=V(_l),i=Z(()=>({[td(e.activeClass,r.linkActiveClass,`router-link-active`)]:n.isActive,[td(e.exactActiveClass,r.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let r=t.default&&Xu(t.default(n));return e.custom?r:Xa(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}});function Qu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(e.button===void 0||e.button===0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function $u(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!ul(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function ed(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var td=(e,t,n)=>e==null?t==null?n:t:e,nd=fr({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let r=V(yl),i=Z(()=>e.route||r.value),a=V(gl,0),o=Z(()=>{let e=L(a),{matched:t}=i.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),s=Z(()=>i.value.matched[o.value]);An(gl,Z(()=>o.value+1)),An(hl,s),An(yl,i);let c=Wt();return Fn(()=>[c.value,s.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!Xl(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let r=i.value,a=e.name,o=s.value,l=o&&o.components[a];if(!l)return rd(n.default,{Component:l,route:r});let u=o.props[a],d=Xa(l,Q({},u?u===!0?r.params:typeof u==`function`?u(r):u:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(o.instances[a]=null)},ref:c}));return rd(n.default,{Component:d,route:r})||d}}});function rd(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var id=nd;function ad(e){let t=Bu(e.routes,e),n=e.parseQuery||mu,r=e.stringifyQuery||hu,i=e.history,a=_u(),o=_u(),s=_u(),c=Gt(tu),l=tu;Sl&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let u=cl.bind(null,e=>``+e),d=cl.bind(null,Hl),f=cl.bind(null,Ul);function p(e,n){let r,i;return pu(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function m(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function h(){return t.getRoutes().map(e=>e.record)}function g(e){return!!t.getRecordMatcher(e)}function _(e,a){if(a=Q({},a||c.value),typeof e==`string`){let r=Kl(n,e,a.path),o=t.resolve({path:r.path},a),s=i.createHref(r.fullPath);return Q(r,o,{params:f(o.params),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=Q({},e,{path:Kl(n,e.path,a.path).path});else{let t=Q({},e.params);for(let e in t)t[e]==null&&delete t[e];o=Q({},e,{params:d(t)}),a.params=d(a.params)}let s=t.resolve(o,a),l=e.hash||``;s.params=u(f(s.params));let p=ql(r,Q({},e,{hash:Rl(l),path:s.path})),m=i.createHref(p);return Q({fullPath:p,hash:l,query:r===hu?gu(e.query):e.query||{}},s,{redirectedFrom:void 0,href:m})}function v(e){return typeof e==`string`?Kl(n,e,c.value.path):Q({},e)}function y(e,t){if(l!==e)return pl(8,{from:t,to:e})}function b(e){return C(e)}function x(e){return b(Q(v(e),{replace:!0}))}function S(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=v(i):{path:i},i.params={}),Q({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function C(e,t){let n=l=_(e),i=c.value,a=e.state,o=e.force,s=e.replace===!0,u=S(n,i);if(u)return C(Q(v(u),{state:typeof u==`object`?Q({},a,u.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&Yl(r,i,n)&&(f=pl(16,{to:d,from:i}),oe(i,i,!0,!1)),(f?Promise.resolve(f):ee(d,i)).catch(e=>ml(e)?ml(e,2)?e:ae(e):k(e,d,i)).then(e=>{if(e){if(ml(e,2))return C(Q({replace:s},v(e.to),{state:typeof e.to==`object`?Q({},a,e.to.state):a,force:o}),t||d)}else e=te(d,i,!0,s,a);return E(d,i,e),e})}function w(e,t){let n=y(e,t);return n?Promise.reject(n):Promise.resolve()}function T(e){let t=le.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function ee(e,t){let n,[r,i,s]=bu(e,t);n=yu(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(vu(r,e,t))});let c=w.bind(null,e,t);return n.push(c),de(n).then(()=>{n=[];for(let r of a.list())n.push(vu(r,e,t));return n.push(c),de(n)}).then(()=>{n=yu(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(vu(r,e,t))});return n.push(c),de(n)}).then(()=>{n=[];for(let r of s)if(r.beforeEnter){if(ul(r.beforeEnter))for(let i of r.beforeEnter)n.push(vu(i,e,t));else n.push(vu(r.beforeEnter,e,t))}return n.push(c),de(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=yu(s,`beforeRouteEnter`,e,t,T),n.push(c),de(n))).then(()=>{n=[];for(let r of o.list())n.push(vu(r,e,t));return n.push(c),de(n)}).catch(e=>ml(e,8)?e:Promise.reject(e))}function E(e,t,n){s.list().forEach(r=>T(()=>r(e,t,n)))}function te(e,t,n,r,a){let o=y(e,t);if(o)return o;let s=t===tu,l=Sl?history.state:{};n&&(r||s?i.replace(e.fullPath,Q({scroll:s&&l&&l.scroll},a)):i.push(e.fullPath,a)),c.value=e,oe(e,t,n,s),ae()}let D;function ne(){D||(D=i.listen((e,t,n)=>{if(!ue.listening)return;let r=_(e),a=S(r,ue.currentRoute.value);if(a){C(Q(a,{replace:!0,force:!0}),r).catch(ll);return}l=r;let o=c.value;Sl&&uu(cu(o.fullPath,n.delta),ou()),ee(r,o).catch(e=>ml(e,12)?e:ml(e,2)?(C(Q(v(e.to),{force:!0}),r).then(e=>{ml(e,20)&&!n.delta&&n.type===`pop`&&i.go(-1,!1)}).catch(ll),Promise.reject()):(n.delta&&i.go(-n.delta,!1),k(e,r,o))).then(e=>{e=e||te(r,o,!1),e&&(n.delta&&!ml(e,8)?i.go(-n.delta,!1):n.type===`pop`&&ml(e,20)&&i.go(-1,!1)),E(r,o,e)}).catch(ll)}))}let O=_u(),re=_u(),ie;function k(e,t,n){ae(e);let r=re.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function A(){return ie&&c.value!==tu?Promise.resolve():new Promise((e,t)=>{O.add([e,t])})}function ae(e){return ie||(ie=!e,ne(),O.list().forEach(([t,n])=>e?n(e):t()),O.reset()),e}function oe(t,n,r,i){let{scrollBehavior:a}=e;if(!Sl||!a)return Promise.resolve();let o=!r&&du(cu(t.fullPath,0))||(i||!r)&&history.state&&history.state.scroll||null;return gn().then(()=>a(t,n,o)).then(e=>t===c.value&&e&&su(e)).catch(e=>t===c.value&&k(e,t,n))}let se=e=>i.go(e),ce,le=new Set,ue={currentRoute:c,listening:!0,addRoute:p,removeRoute:m,clearRoutes:t.clearRoutes,hasRoute:g,getRoutes:h,resolve:_,options:e,push:b,replace:x,go:se,back:()=>se(-1),forward:()=>se(1),beforeEach:a.add,beforeResolve:o.add,afterEach:s.add,onError:re.add,isReady:A,install(e){e.component(`RouterLink`,Zu),e.component(`RouterView`,id),e.config.globalProperties.$router=ue,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>L(c)}),Sl&&!ce&&c.value===tu&&(ce=!0,b(i.location).catch(e=>{}));let t={};for(let e in tu)Object.defineProperty(t,e,{get:()=>c.value[e],enumerable:!0});e.provide(_l,ue),e.provide(vl,Ft(t)),e.provide(yl,c);let n=e.unmount;le.add(e),e.unmount=function(){le.delete(e),le.size<1&&(l=tu,D&&D(),D=null,c.value=tu,ce=!1,ie=!1),n()}}};function de(e){return e.reduce((e,t)=>e.then(()=>T(t)),Promise.resolve())}return ue}var od=`/api/v2/mobile`,sd=`wecom-mobile-token`,cd=class extends Error{constructor(e,t,n,r=0){super(n),al(this,`status`,void 0),al(this,`code`,void 0),al(this,`retryAfter`,void 0),this.status=e,this.code=t,this.retryAfter=r}},ld=null;function ud(e){ld=e}function dd(){try{return window.localStorage.getItem(sd)||``}catch{return``}}function fd(e){try{e?window.localStorage.setItem(sd,e):window.localStorage.removeItem(sd)}catch{}}var pd=``;function md(){return pd||dd()}function hd(e){pd=e,fd(e)}function gd(e,t){let n=t&&typeof t==`object`&&`detail`in t?t.detail:t;if(n&&typeof n==`object`&&!Array.isArray(n)){let t=n;return new cd(e,String(t.code||`http_${e}`),String(t.message||_d(e)),Number(t.retry_after||0))}return Array.isArray(n)?new cd(e,`validation`,`填写的内容不符合要求,请检查后重试`):typeof n==`string`&&n.trim()?new cd(e,`http_${e}`,n.trim()):new cd(e,`http_${e}`,_d(e))}function _d(e){return e===401?`登录已失效,请重新登录`:e===403?`没有权限执行这个操作`:e===404?`内容不存在或已被删除`:e===429?`操作太频繁,请稍后再试`:e>=500?`服务暂时不可用,请稍后重试`:`请求失败(HTTP ${e})`}async function vd(e,t,n={}){let{body:r,query:i,auth:a=!0,signal:o}=n,s=new URLSearchParams;for(let[e,t]of Object.entries(i||{}))t!=null&&t!==``&&s.set(e,String(t));let c=`${od}${t}${s.toString()?`?${s}`:``}`,l={Accept:`application/json`};r!==void 0&&(l[`Content-Type`]=`application/json`);let u=md();a&&u&&(l.Authorization=`Bearer ${u}`);let d;try{d=await fetch(c,{method:e,headers:l,body:r===void 0?void 0:JSON.stringify(r),signal:o,cache:`no-store`,credentials:`omit`})}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?e:new cd(0,`network`,`网络连接失败,请检查网络后重试`)}let f=null,p=await d.text();if(p)try{f=JSON.parse(p)}catch{f=null}if(!d.ok){let e=gd(d.status,f);throw d.status===401&&a&&(ld==null||ld()),e}return f}var yd=e=>encodeURIComponent(e),bd={login(e,t,n){return vd(`POST`,`/auth/login`,{auth:!1,body:{account:e,password:t,client_name:n}})},logout(){return vd(`POST`,`/auth/logout`)},me(){return vd(`GET`,`/me`)},accounts(e=``){return vd(`GET`,`/accounts`,{query:{q:e}})},desktops(e){return vd(`GET`,`/desktops`,{query:{account_id:e}})},sendCommand(e,t,n){return vd(`POST`,`/desktops/${yd(e)}/commands`,{body:{type:t},query:{account_id:n}})},command(e){return vd(`GET`,`/commands/${yd(e)}`)},conversations(e){return vd(`GET`,`/conversations`,{query:{account_id:e.accountId,q:e.q,scope:e.scope,cursor:e.cursor,limit:e.limit},signal:e.signal})},block(e,t,n){return vd(`PUT`,`/blacklist/${yd(e.account)}/${yd(e.conversation)}`,{body:{name:e.name||``,account_name:e.account_name||``,reason:t},query:{account_id:n}})},unblock(e,t,n){return vd(`DELETE`,`/blacklist/${yd(e)}/${yd(t)}`,{query:{account_id:n}})}},xd=`wecom-mobile-view-account`;function Sd(){try{let e=window.localStorage.getItem(xd),t=e?JSON.parse(e):null;return t&&typeof t.id==`number`?t:null}catch{return null}}var $=Pt({token:md(),me:null,viewed:Sd()}),Cd=Z(()=>{var e;return(e=$.me)==null?void 0:e.permissions}),wd=Z(()=>{let e=$.me,t=$.viewed;if(e&&t&&e.permissions.control_all&&t.id!==e.account.id)return t.id}),Td=Z(()=>{var e,t;return wd.value!==void 0&&$.viewed?$.viewed.nickname||$.viewed.sn||`客服 #${$.viewed.id}`:((e=$.me)==null?void 0:e.account.nickname)||((t=$.me)==null?void 0:t.account.sn)||``});function Ed(e){$.viewed=e?{id:e.id,nickname:e.nickname,sn:e.sn}:null;try{$.viewed?window.localStorage.setItem(xd,JSON.stringify($.viewed)):window.localStorage.removeItem(xd)}catch{}}function Dd(e,t){hd(e),$.token=e,$.me=t,t.permissions.control_all||Ed(null)}function Od(){hd(``),$.token=``,$.me=null,Ed(null)}async function kd(){let e=await bd.me();return $.me=e,e.permissions.control_all||Ed(null),e}async function Ad(){try{await bd.logout()}catch{}Od()}var jd=`modulepreload`,Md=function(e){return`/m/`+e},Nd={},Pd=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute(`nonce`));function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=Md(t,n),t=s(t),t in Nd)return;Nd[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:jd,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Fd=ad({history:Du(),routes:[{path:`/login`,name:`login`,component:()=>Pd(()=>import(`./LoginView-Br8nsdQM.js`),__vite__mapDeps([0,1,2,3,4])),meta:{public:!0}},{path:`/`,component:()=>Pd(()=>import(`./ShellView-DcQ5QK1V.js`),__vite__mapDeps([5,1,6,7])),children:[{path:``,redirect:{name:`desktops`}},{path:`desktops`,name:`desktops`,component:()=>Pd(()=>import(`./DevicesView-DVXAQfpT.js`),__vite__mapDeps([8,1,9,6,2,10,3,11,12]))},{path:`conversations`,name:`conversations`,component:()=>Pd(()=>import(`./ConversationsView-BMjeHKhk.js`),__vite__mapDeps([13,1,6,9,2,3,10,11,14]))},{path:`me`,name:`me`,component:()=>Pd(()=>import(`./MeView-BOWGunAt.js`),__vite__mapDeps([15,1,9,6,2,16]))}]},{path:`/:pathMatch(.*)*`,redirect:{name:`desktops`}}]});Fd.beforeEach(async e=>{if(e.meta.public)return!0;if(!$.token)return{name:`login`,query:{redirect:e.fullPath}};if(!$.me)try{await kd()}catch(t){if(t instanceof cd&&t.status===401)return Od(),{name:`login`,query:{redirect:e.fullPath}}}return!0}),ud(()=>{Od();let e=Fd.currentRoute.value;e.matched.length&&e.name!==`login`&&Fd.replace({name:`login`,query:{redirect:e.fullPath,expired:`1`}})});var Id=`wecom-mobile-chunk-reload`;Fd.onError((e,t)=>{let n=String((e==null?void 0:e.message)||e);if(!/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(n))return;let r=0;try{r=Number(window.sessionStorage.getItem(Id)||0),window.sessionStorage.setItem(Id,String(Date.now()))}catch{}Date.now()-r>1e4&&(window.location.assign(`/m/#${t.fullPath}`),window.location.reload())}),us(tl).use(Fd).mount(`#app`),`serviceWorker`in navigator&&window.isSecureContext&&window.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/m/sw.js`,{scope:`/m/`}).catch(()=>void 0)});export{hs as $,ac as A,Mr as At,Hs as B,_i as Bt,Sc as C,fr as Ct,dc as D,gn as Dt,nc as E,Aa as Et,Ns as F,ua as Ft,Qs as G,Pt as Gt,ec as H,Nn as Ht,Ps as I,An as It,Ds as J,he as Jt,Os as K,Wt as Kt,Zs as L,Kr as Lt,pc as M,jr as Mt,fc as N,Fr as Nt,uc as O,Sr as Ot,tc as P,Nr as Pt,ms as Q,Us as R,Vr as Rt,gc as S,J as St,ic as T,V as Tt,Rs as U,Dn as Ut,Ws as V,Fn as Vt,Ls as W,On as Wt,Ts as X,As as Y,Se as Yt,Es as Z,zc as _,ga as _t,Ed as a,ps as at,Oc as b,qr as bt,cd as c,fo as ct,bl as d,os as dt,_s as et,el as f,oa as ft,Bc as g,xa as gt,Hc as h,Z as ht,Ad as i,ys as it,rc as j,Cr as jt,cc as k,Pr as kt,bd as l,us as lt,Kc as m,Xn as mt,$ as n,bs as nt,wd as o,Cs as ot,Qc as p,br as pt,ks as q,L as qt,Dd as r,gs as rt,Td as s,ws as st,Cd as t,vs as tt,xl as u,Ao as ut,mc as v,Ea as vt,oc as w,Pa as wt,hc as x,Ta as xt,Ec as y,ha as yt,Ms as z,Ur as zt}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css new file mode 100644 index 000000000..da7edc533 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css @@ -0,0 +1 @@ +:root,:host{--van-black:#000;--van-white:#fff;--van-gray-1:#f7f8fa;--van-gray-2:#f2f3f5;--van-gray-3:#ebedf0;--van-gray-4:#dcdee0;--van-gray-5:#c8c9cc;--van-gray-6:#969799;--van-gray-7:#646566;--van-gray-8:#323233;--van-red:#ee0a24;--van-blue:#1989fa;--van-orange:#ff976a;--van-orange-dark:#ed6a0c;--van-orange-light:#fffbe8;--van-green:#07c160;--van-gradient-red:linear-gradient(to right, #ff6034, #ee0a24);--van-gradient-orange:linear-gradient(to right, #ffd01e, #ff8917);--van-primary-color:var(--van-blue);--van-success-color:var(--van-green);--van-danger-color:var(--van-red);--van-warning-color:var(--van-orange);--van-text-color:var(--van-gray-8);--van-text-color-2:var(--van-gray-6);--van-text-color-3:var(--van-gray-5);--van-active-color:var(--van-gray-2);--van-active-opacity:.6;--van-disabled-opacity:.5;--van-background:var(--van-gray-1);--van-background-2:var(--van-white);--van-background-3:var(--van-white);--van-padding-base:4px;--van-padding-xs:8px;--van-padding-sm:12px;--van-padding-md:16px;--van-padding-lg:24px;--van-padding-xl:32px;--van-font-bold:600;--van-font-size-xs:10px;--van-font-size-sm:12px;--van-font-size-md:14px;--van-font-size-lg:16px;--van-line-height-xs:14px;--van-line-height-sm:18px;--van-line-height-md:20px;--van-line-height-lg:22px;--van-base-font:-apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;--van-price-font:avenir-heavy, "PingFang SC", helvetica neue, arial, sans-serif;--van-duration-base:.3s;--van-duration-fast:.2s;--van-ease-out:ease-out;--van-ease-in:ease-in;--van-border-color:var(--van-gray-3);--van-border-width:1px;--van-radius-sm:2px;--van-radius-md:4px;--van-radius-lg:8px;--van-radius-max:999px}.van-theme-dark{--van-text-color:#f5f5f5;--van-text-color-2:#707070;--van-text-color-3:#4d4d4d;--van-border-color:#3a3a3c;--van-active-color:#3a3a3c;--van-background:#000;--van-background-2:#1c1c1e;--van-background-3:#37363b}html{-webkit-tap-highlight-color:transparent}body{font-family:var(--van-base-font);margin:0}a{text-decoration:none}input,button,textarea{color:inherit;font:inherit}a:focus,input:focus,button:focus,textarea:focus,[class*=van-]:focus{outline:none}ol,ul{margin:0;padding:0;list-style:none}@keyframes van-slide-up-enter{0%{transform:translateY(100%)}}@keyframes van-slide-up-leave{to{transform:translateY(100%)}}@keyframes van-slide-down-enter{0%{transform:translateY(-100%)}}@keyframes van-slide-down-leave{to{transform:translateY(-100%)}}@keyframes van-slide-left-enter{0%{transform:translate(-100%)}}@keyframes van-slide-left-leave{to{transform:translate(-100%)}}@keyframes van-slide-right-enter{0%{transform:translate(100%)}}@keyframes van-slide-right-leave{to{transform:translate(100%)}}@keyframes van-fade-in{0%{opacity:0}to{opacity:1}}@keyframes van-fade-out{0%{opacity:1}to{opacity:0}}@keyframes van-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.van-fade-enter-active{animation:var(--van-duration-base) van-fade-in both var(--van-ease-out)}.van-fade-leave-active{animation:var(--van-duration-base) van-fade-out both var(--van-ease-in)}.van-slide-up-enter-active{animation:van-slide-up-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-up-leave-active{animation:van-slide-up-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-down-enter-active{animation:van-slide-down-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-down-leave-active{animation:van-slide-down-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-left-enter-active{animation:van-slide-left-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-left-leave-active{animation:van-slide-left-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-right-enter-active{animation:van-slide-right-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-right-leave-active{animation:van-slide-right-leave var(--van-duration-base) both var(--van-ease-in)}.van-clearfix:after{clear:both;content:"";display:table}.van-ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.van-multi-ellipsis--l2{text-overflow:ellipsis;-webkit-line-clamp:2;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-multi-ellipsis--l3{text-overflow:ellipsis;-webkit-line-clamp:3;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-safe-area-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-safe-area-bottom{padding-bottom:constant(safe-area-inset-bottom);padding-bottom:env(safe-area-inset-bottom)}.van-haptics-feedback{cursor:pointer}.van-haptics-feedback:active{opacity:var(--van-active-opacity)}[class*=van-hairline]:after{box-sizing:border-box;content:" ";pointer-events:none;border:0 solid var(--van-border-color);position:absolute;top:-50%;bottom:-50%;left:-50%;right:-50%;transform:scale(.5)}.van-hairline,.van-hairline--top,.van-hairline--left,.van-hairline--right,.van-hairline--bottom,.van-hairline--surround,.van-hairline--top-bottom{position:relative}.van-hairline--top:after{border-top-width:var(--van-border-width)}.van-hairline--left:after{border-left-width:var(--van-border-width)}.van-hairline--right:after{border-right-width:var(--van-border-width)}.van-hairline--bottom:after{border-bottom-width:var(--van-border-width)}.van-hairline--top-bottom:after,.van-hairline-unset--top-bottom:after{border-width:var(--van-border-width) 0}.van-hairline--surround:after{border-width:var(--van-border-width)}:root,:host{--van-badge-size:16px;--van-badge-color:var(--van-white);--van-badge-padding:0 3px;--van-badge-font-size:var(--van-font-size-sm);--van-badge-font-weight:var(--van-font-bold);--van-badge-border-width:var(--van-border-width);--van-badge-background:var(--van-danger-color);--van-badge-dot-color:var(--van-danger-color);--van-badge-dot-size:8px;--van-badge-font:-apple-system-font, helvetica neue, arial, sans-serif}.van-badge{box-sizing:border-box;min-width:var(--van-badge-size);padding:var(--van-badge-padding);color:var(--van-badge-color);font-weight:var(--van-badge-font-weight);font-size:var(--van-badge-font-size);font-family:var(--van-badge-font);text-align:center;background:var(--van-badge-background);border:var(--van-badge-border-width) solid var(--van-background-2);border-radius:var(--van-radius-max);line-height:1.2;display:inline-block}.van-badge--fixed{transform-origin:100%;position:absolute}.van-badge--top-left{top:0;left:0;transform:translate(-50%,-50%)}.van-badge--top-right{top:0;right:0;transform:translate(50%,-50%)}.van-badge--bottom-left{bottom:0;left:0;transform:translate(-50%,50%)}.van-badge--bottom-right{bottom:0;right:0;transform:translate(50%,50%)}.van-badge--dot{width:var(--van-badge-dot-size);min-width:0;height:var(--van-badge-dot-size);background:var(--van-badge-dot-color);border:none;border-radius:100%;padding:0}.van-badge__wrapper{display:inline-block;position:relative}.van-icon{font:14px/1 vant-icon;font:normal normal normal 14px/1 var(--van-icon-font-family,"vant-icon");font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;display:inline-block;position:relative}.van-icon:before{display:inline-block}.van-icon-arrow-double-left:before{content:""}.van-icon-arrow-double-right:before{content:""}.van-icon-contact:before{content:""}.van-icon-notes:before{content:""}.van-icon-records:before{content:""}.van-icon-cash-back-record:before{content:""}.van-icon-newspaper:before{content:""}.van-icon-discount:before{content:""}.van-icon-completed:before{content:""}.van-icon-user:before{content:""}.van-icon-description:before{content:""}.van-icon-list-switch:before{content:""}.van-icon-list-switching:before{content:""}.van-icon-link-o:before{content:""}.van-icon-miniprogram-o:before{content:""}.van-icon-qq:before{content:""}.van-icon-wechat-moments:before{content:""}.van-icon-weibo:before{content:""}.van-icon-cash-o:before{content:""}.van-icon-guide-o:before{content:""}.van-icon-invitation:before{content:""}.van-icon-shield-o:before{content:""}.van-icon-exchange:before{content:""}.van-icon-eye:before{content:""}.van-icon-enlarge:before{content:""}.van-icon-expand-o:before{content:""}.van-icon-eye-o:before{content:""}.van-icon-expand:before{content:""}.van-icon-filter-o:before{content:""}.van-icon-fire:before{content:""}.van-icon-fail:before{content:""}.van-icon-failure:before{content:""}.van-icon-fire-o:before{content:""}.van-icon-flag-o:before{content:""}.van-icon-font:before{content:""}.van-icon-font-o:before{content:""}.van-icon-gem-o:before{content:""}.van-icon-flower-o:before{content:""}.van-icon-gem:before{content:""}.van-icon-gift-card:before{content:""}.van-icon-friends:before{content:""}.van-icon-friends-o:before{content:""}.van-icon-gold-coin:before{content:""}.van-icon-gold-coin-o:before{content:""}.van-icon-good-job-o:before{content:""}.van-icon-gift:before{content:""}.van-icon-gift-o:before{content:""}.van-icon-gift-card-o:before{content:""}.van-icon-good-job:before{content:""}.van-icon-home-o:before{content:""}.van-icon-goods-collect:before{content:""}.van-icon-graphic:before{content:""}.van-icon-goods-collect-o:before{content:""}.van-icon-hot-o:before{content:""}.van-icon-info:before{content:""}.van-icon-hotel-o:before{content:""}.van-icon-info-o:before{content:""}.van-icon-hot-sale-o:before{content:""}.van-icon-hot:before{content:""}.van-icon-like:before{content:""}.van-icon-idcard:before{content:""}.van-icon-like-o:before{content:""}.van-icon-hot-sale:before{content:""}.van-icon-location-o:before{content:""}.van-icon-location:before{content:""}.van-icon-label:before{content:""}.van-icon-lock:before{content:""}.van-icon-label-o:before{content:""}.van-icon-map-marked:before{content:""}.van-icon-logistics:before{content:""}.van-icon-manager:before{content:""}.van-icon-more:before{content:""}.van-icon-live:before{content:""}.van-icon-manager-o:before{content:""}.van-icon-medal:before{content:""}.van-icon-more-o:before{content:""}.van-icon-music-o:before{content:""}.van-icon-music:before{content:""}.van-icon-new-arrival-o:before{content:""}.van-icon-medal-o:before{content:""}.van-icon-new-o:before{content:""}.van-icon-free-postage:before{content:""}.van-icon-newspaper-o:before{content:""}.van-icon-new-arrival:before{content:""}.van-icon-minus:before{content:""}.van-icon-orders-o:before{content:""}.van-icon-new:before{content:""}.van-icon-paid:before{content:""}.van-icon-notes-o:before{content:""}.van-icon-other-pay:before{content:""}.van-icon-pause-circle:before{content:""}.van-icon-pause:before{content:""}.van-icon-pause-circle-o:before{content:""}.van-icon-peer-pay:before{content:""}.van-icon-pending-payment:before{content:""}.van-icon-passed:before{content:""}.van-icon-plus:before{content:""}.van-icon-phone-circle-o:before{content:""}.van-icon-phone-o:before{content:""}.van-icon-printer:before{content:""}.van-icon-photo-fail:before{content:""}.van-icon-phone:before{content:""}.van-icon-photo-o:before{content:""}.van-icon-play-circle:before{content:""}.van-icon-play:before{content:""}.van-icon-phone-circle:before{content:""}.van-icon-point-gift-o:before{content:""}.van-icon-point-gift:before{content:""}.van-icon-play-circle-o:before{content:""}.van-icon-shrink:before{content:""}.van-icon-photo:before{content:""}.van-icon-qr:before{content:""}.van-icon-qr-invalid:before{content:""}.van-icon-question-o:before{content:""}.van-icon-revoke:before{content:""}.van-icon-replay:before{content:""}.van-icon-service:before{content:""}.van-icon-question:before{content:""}.van-icon-search:before{content:""}.van-icon-refund-o:before{content:""}.van-icon-service-o:before{content:""}.van-icon-scan:before{content:""}.van-icon-share:before{content:""}.van-icon-send-gift-o:before{content:""}.van-icon-share-o:before{content:""}.van-icon-setting:before{content:""}.van-icon-points:before{content:""}.van-icon-photograph:before{content:""}.van-icon-shop:before{content:""}.van-icon-shop-o:before{content:""}.van-icon-shop-collect-o:before{content:""}.van-icon-shop-collect:before{content:""}.van-icon-smile:before{content:""}.van-icon-shopping-cart-o:before{content:""}.van-icon-sign:before{content:""}.van-icon-sort:before{content:""}.van-icon-star-o:before{content:""}.van-icon-smile-comment-o:before{content:""}.van-icon-stop:before{content:""}.van-icon-stop-circle-o:before{content:""}.van-icon-smile-o:before{content:""}.van-icon-star:before{content:""}.van-icon-success:before{content:""}.van-icon-stop-circle:before{content:""}.van-icon-records-o:before{content:""}.van-icon-shopping-cart:before{content:""}.van-icon-tosend:before{content:""}.van-icon-todo-list:before{content:""}.van-icon-thumb-circle-o:before{content:""}.van-icon-thumb-circle:before{content:""}.van-icon-umbrella-circle:before{content:""}.van-icon-underway:before{content:""}.van-icon-upgrade:before{content:""}.van-icon-todo-list-o:before{content:""}.van-icon-tv-o:before{content:""}.van-icon-underway-o:before{content:""}.van-icon-user-o:before{content:""}.van-icon-vip-card-o:before{content:""}.van-icon-vip-card:before{content:""}.van-icon-send-gift:before{content:""}.van-icon-wap-home:before{content:""}.van-icon-wap-nav:before{content:""}.van-icon-volume-o:before{content:""}.van-icon-video:before{content:""}.van-icon-wap-home-o:before{content:""}.van-icon-volume:before{content:""}.van-icon-warning:before{content:""}.van-icon-weapp-nav:before{content:""}.van-icon-wechat-pay:before{content:""}.van-icon-warning-o:before{content:""}.van-icon-wechat:before{content:""}.van-icon-setting-o:before{content:""}.van-icon-youzan-shield:before{content:""}.van-icon-warn-o:before{content:""}.van-icon-smile-comment:before{content:""}.van-icon-user-circle-o:before{content:""}.van-icon-video-o:before{content:""}.van-icon-add-square:before{content:""}.van-icon-add:before{content:""}.van-icon-arrow-down:before{content:""}.van-icon-arrow-up:before{content:""}.van-icon-arrow:before{content:""}.van-icon-after-sale:before{content:""}.van-icon-add-o:before{content:""}.van-icon-alipay:before{content:""}.van-icon-ascending:before{content:""}.van-icon-apps-o:before{content:""}.van-icon-aim:before{content:""}.van-icon-award:before{content:""}.van-icon-arrow-left:before{content:""}.van-icon-award-o:before{content:""}.van-icon-audio:before{content:""}.van-icon-bag-o:before{content:""}.van-icon-balance-list:before{content:""}.van-icon-back-top:before{content:""}.van-icon-bag:before{content:""}.van-icon-balance-pay:before{content:""}.van-icon-balance-o:before{content:""}.van-icon-bar-chart-o:before{content:""}.van-icon-bars:before{content:""}.van-icon-balance-list-o:before{content:""}.van-icon-birthday-cake-o:before{content:""}.van-icon-bookmark:before{content:""}.van-icon-bill:before{content:""}.van-icon-bell:before{content:""}.van-icon-browsing-history-o:before{content:""}.van-icon-browsing-history:before{content:""}.van-icon-bookmark-o:before{content:""}.van-icon-bulb-o:before{content:""}.van-icon-bullhorn-o:before{content:""}.van-icon-bill-o:before{content:""}.van-icon-calendar-o:before{content:""}.van-icon-brush-o:before{content:""}.van-icon-card:before{content:""}.van-icon-cart-o:before{content:""}.van-icon-cart-circle:before{content:""}.van-icon-cart-circle-o:before{content:""}.van-icon-cart:before{content:""}.van-icon-cash-on-deliver:before{content:""}.van-icon-cash-back-record-o:before{content:""}.van-icon-cashier-o:before{content:""}.van-icon-chart-trending-o:before{content:""}.van-icon-certificate:before{content:""}.van-icon-chat:before{content:""}.van-icon-clear:before{content:""}.van-icon-chat-o:before{content:""}.van-icon-checked:before{content:""}.van-icon-clock:before{content:""}.van-icon-clock-o:before{content:""}.van-icon-close:before{content:""}.van-icon-closed-eye:before{content:""}.van-icon-circle:before{content:""}.van-icon-cluster-o:before{content:""}.van-icon-column:before{content:""}.van-icon-comment-circle-o:before{content:""}.van-icon-cluster:before{content:""}.van-icon-comment:before{content:""}.van-icon-comment-o:before{content:""}.van-icon-comment-circle:before{content:""}.van-icon-completed-o:before{content:""}.van-icon-credit-pay:before{content:""}.van-icon-coupon:before{content:""}.van-icon-debit-pay:before{content:""}.van-icon-coupon-o:before{content:""}.van-icon-contact-o:before{content:""}.van-icon-descending:before{content:""}.van-icon-desktop-o:before{content:""}.van-icon-diamond-o:before{content:""}.van-icon-description-o:before{content:""}.van-icon-delete:before{content:""}.van-icon-diamond:before{content:""}.van-icon-delete-o:before{content:""}.van-icon-cross:before{content:""}.van-icon-edit:before{content:""}.van-icon-ellipsis:before{content:""}.van-icon-down:before{content:""}.van-icon-discount-o:before{content:""}.van-icon-ecard-pay:before{content:""}.van-icon-envelop-o:before{content:""}@font-face{font-weight:400;font-family:vant-icon;font-style:normal;font-display:auto;src:url(data:font/woff2;charset=utf-8;base64,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)format("woff2")}.van-icon__image{object-fit:contain;width:1em;height:1em;display:block}:root,:host{--van-nav-bar-height:46px;--van-nav-bar-background:var(--van-background-2);--van-nav-bar-arrow-size:16px;--van-nav-bar-icon-color:var(--van-primary-color);--van-nav-bar-text-color:var(--van-primary-color);--van-nav-bar-title-font-size:var(--van-font-size-lg);--van-nav-bar-title-text-color:var(--van-text-color);--van-nav-bar-z-index:1;--van-nav-bar-disabled-opacity:var(--van-disabled-opacity)}.van-nav-bar{z-index:var(--van-nav-bar-z-index);line-height:var(--van-line-height-lg);text-align:center;background:var(--van-nav-bar-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.van-nav-bar--fixed{width:100%;position:fixed;top:0;left:0}.van-nav-bar--safe-area-inset-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-nav-bar .van-icon{color:var(--van-nav-bar-icon-color)}.van-nav-bar__content{height:var(--van-nav-bar-height);align-items:center;display:flex;position:relative}.van-nav-bar__arrow{margin-right:var(--van-padding-base);font-size:var(--van-nav-bar-arrow-size)}.van-nav-bar__title{max-width:60%;color:var(--van-nav-bar-title-text-color);font-weight:var(--van-font-bold);font-size:var(--van-nav-bar-title-font-size);margin:0 auto}.van-nav-bar__left,.van-nav-bar__right{padding:0 var(--van-padding-md);font-size:var(--van-font-size-md);align-items:center;display:flex;position:absolute;top:0;bottom:0}.van-nav-bar__left--disabled,.van-nav-bar__right--disabled{cursor:not-allowed;opacity:var(--van-nav-bar-disabled-opacity)}.van-nav-bar__left{left:0}.van-nav-bar__right{right:0}.van-nav-bar__text{color:var(--van-nav-bar-text-color)}:root,:host{--van-tabbar-height:50px;--van-tabbar-z-index:1;--van-tabbar-background:var(--van-background-2)}.van-tabbar{z-index:var(--van-tabbar-z-index);box-sizing:content-box;width:100%;height:var(--van-tabbar-height);background:var(--van-tabbar-background);display:flex}.van-tabbar--fixed{position:fixed;bottom:0;left:0}:root,:host{--van-tabbar-item-font-size:var(--van-font-size-sm);--van-tabbar-item-text-color:var(--van-text-color);--van-tabbar-item-active-color:var(--van-primary-color);--van-tabbar-item-active-background:var(--van-background-2);--van-tabbar-item-line-height:1;--van-tabbar-item-icon-size:22px;--van-tabbar-item-icon-margin-bottom:var(--van-padding-base)}.van-tabbar-item{color:var(--van-tabbar-item-text-color);font-size:var(--van-tabbar-item-font-size);line-height:var(--van-tabbar-item-line-height);cursor:pointer;flex-direction:column;flex:1;justify-content:center;align-items:center;display:flex}.van-tabbar-item__icon{margin-bottom:var(--van-tabbar-item-icon-margin-bottom);font-size:var(--van-tabbar-item-icon-size)}.van-tabbar-item__icon .van-icon{display:block}.van-tabbar-item__icon .van-badge{margin-top:var(--van-padding-base)}.van-tabbar-item__icon img{height:20px;display:block}.van-tabbar-item--active{color:var(--van-tabbar-item-active-color);background-color:var(--van-tabbar-item-active-background)}:root,:host{--van-cell-font-size:var(--van-font-size-md);--van-cell-line-height:24px;--van-cell-vertical-padding:10px;--van-cell-horizontal-padding:var(--van-padding-md);--van-cell-text-color:var(--van-text-color);--van-cell-background:var(--van-background-2);--van-cell-border-color:var(--van-border-color);--van-cell-active-color:var(--van-active-color);--van-cell-required-color:var(--van-danger-color);--van-cell-label-color:var(--van-text-color-2);--van-cell-label-font-size:var(--van-font-size-sm);--van-cell-label-line-height:var(--van-line-height-sm);--van-cell-label-margin-top:var(--van-padding-base);--van-cell-value-color:var(--van-text-color-2);--van-cell-value-font-size:inherit;--van-cell-icon-size:16px;--van-cell-right-icon-color:var(--van-gray-6);--van-cell-large-vertical-padding:var(--van-padding-sm);--van-cell-large-title-font-size:var(--van-font-size-lg);--van-cell-large-label-font-size:var(--van-font-size-md);--van-cell-large-value-font-size:inherit}.van-cell{box-sizing:border-box;width:100%;padding:var(--van-cell-vertical-padding) var(--van-cell-horizontal-padding);color:var(--van-cell-text-color);font-size:var(--van-cell-font-size);line-height:var(--van-cell-line-height);background:var(--van-cell-background);display:flex;position:relative;overflow:hidden}.van-cell:after{box-sizing:border-box;content:" ";pointer-events:none;right:var(--van-padding-md);bottom:0;left:var(--van-padding-md);border-bottom:1px solid var(--van-cell-border-color);position:absolute;transform:scaleY(.5)}.van-cell:last-child:after,.van-cell--borderless:after{display:none}.van-cell__label{margin-top:var(--van-cell-label-margin-top);color:var(--van-cell-label-color);font-size:var(--van-cell-label-font-size);line-height:var(--van-cell-label-line-height)}.van-cell__title,.van-cell__value{flex:1}.van-cell__value{color:var(--van-cell-value-color);font-size:var(--van-cell-value-font-size);text-align:right;vertical-align:middle;word-wrap:break-word;position:relative;overflow:hidden}.van-cell__left-icon,.van-cell__right-icon{height:var(--van-cell-line-height);font-size:var(--van-cell-icon-size);line-height:var(--van-cell-line-height)}.van-cell__left-icon{margin-right:var(--van-padding-base)}.van-cell__right-icon{margin-left:var(--van-padding-base);color:var(--van-cell-right-icon-color)}.van-cell--clickable{cursor:pointer}.van-cell--clickable:active{background-color:var(--van-cell-active-color)}.van-cell--required{overflow:visible}.van-cell--required:before{left:var(--van-padding-xs);color:var(--van-cell-required-color);font-size:var(--van-cell-font-size);content:"*";position:absolute}.van-cell--center{align-items:center}.van-cell--large{padding-top:var(--van-cell-large-vertical-padding);padding-bottom:var(--van-cell-large-vertical-padding)}.van-cell--large .van-cell__title{font-size:var(--van-cell-large-title-font-size)}.van-cell--large .van-cell__label{font-size:var(--van-cell-large-label-font-size)}.van-cell--large .van-cell__value{font-size:var(--van-cell-large-value-font-size)}:root,:host{--van-field-label-width:6.2em;--van-field-label-color:var(--van-text-color);--van-field-label-margin-right:var(--van-padding-sm);--van-field-input-text-color:var(--van-text-color);--van-field-input-error-text-color:var(--van-danger-color);--van-field-input-disabled-text-color:var(--van-text-color-3);--van-field-placeholder-text-color:var(--van-text-color-3);--van-field-icon-size:18px;--van-field-clear-icon-size:18px;--van-field-clear-icon-color:var(--van-gray-5);--van-field-right-icon-color:var(--van-gray-6);--van-field-error-message-color:var(--van-danger-color);--van-field-error-message-font-size:12px;--van-field-text-area-min-height:60px;--van-field-word-limit-color:var(--van-gray-7);--van-field-word-limit-font-size:var(--van-font-size-sm);--van-field-word-limit-line-height:16px;--van-field-disabled-text-color:var(--van-text-color-3);--van-field-required-mark-color:var(--van-red)}.van-field{flex-wrap:wrap}.van-field__label{box-sizing:border-box;width:var(--van-field-label-width);margin-right:var(--van-field-label-margin-right);color:var(--van-field-label-color);text-align:left;word-wrap:break-word;flex:none}.van-field__label--center{text-align:center}.van-field__label--right{text-align:right}.van-field__label--top{text-align:left;width:100%;margin-bottom:var(--van-padding-base);overflow-wrap:break-word;display:flex}.van-field__label--required:before{color:var(--van-field-required-mark-color);content:"*";margin-right:2px}.van-field--disabled .van-field__label{color:var(--van-field-disabled-text-color)}.van-field__value{overflow:visible}.van-field__body{align-items:center;display:flex}.van-field__control{box-sizing:border-box;width:100%;min-width:0;color:var(--van-field-input-text-color);line-height:inherit;text-align:left;resize:none;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto;background-color:#0000;border:0;margin:0;padding:0;display:block}.van-field__control::placeholder{color:var(--van-field-placeholder-text-color)}.van-field__control:-moz-read-only{cursor:default}.van-field__control:read-only{cursor:default}.van-field__control:disabled{color:var(--van-field-input-disabled-text-color);cursor:not-allowed;opacity:1;-webkit-text-fill-color:var(--van-field-input-disabled-text-color)}.van-field__control--center{text-align:center;justify-content:center}.van-field__control--right{text-align:right;justify-content:flex-end}.van-field__control--custom{min-height:var(--van-cell-line-height);align-items:center;display:flex}.van-field__control--error::-webkit-input-placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--error,.van-field__control--error::placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--min-height{min-height:var(--van-field-text-area-min-height)}.van-field__control[type=date],.van-field__control[type=time],.van-field__control[type=datetime-local]{min-height:var(--van-cell-line-height)}.van-field__control[type=search]{-webkit-appearance:none}.van-field__clear,.van-field__icon,.van-field__button,.van-field__right-icon{flex-shrink:0}.van-field__clear,.van-field__right-icon{margin-right:calc(var(--van-padding-xs) * -1);padding:0 var(--van-padding-xs);line-height:inherit}.van-field__clear{color:var(--van-field-clear-icon-color);font-size:var(--van-field-clear-icon-size);cursor:pointer}.van-field__left-icon .van-icon,.van-field__right-icon .van-icon{font-size:var(--van-field-icon-size);line-height:inherit;display:block}.van-field__left-icon{margin-right:var(--van-padding-base)}.van-field__right-icon{color:var(--van-field-right-icon-color)}.van-field__button{padding-left:var(--van-padding-xs)}.van-field__error-message{color:var(--van-field-error-message-color);font-size:var(--van-field-error-message-font-size);text-align:left}.van-field__error-message--center{text-align:center}.van-field__error-message--right{text-align:right}.van-field__word-limit{margin-top:var(--van-padding-base);color:var(--van-field-word-limit-color);font-size:var(--van-field-word-limit-font-size);line-height:var(--van-field-word-limit-line-height);text-align:right}:root,:host{--van-cell-group-background:var(--van-background-2);--van-cell-group-title-color:var(--van-text-color-2);--van-cell-group-title-padding:var(--van-padding-md) var(--van-padding-md);--van-cell-group-title-font-size:var(--van-font-size-md);--van-cell-group-title-line-height:16px;--van-cell-group-inset-padding:0 var(--van-padding-md);--van-cell-group-inset-radius:var(--van-radius-lg);--van-cell-group-inset-title-padding:var(--van-padding-md) var(--van-padding-md)}.van-cell-group{background:var(--van-cell-group-background)}.van-cell-group--inset{margin:var(--van-cell-group-inset-padding);border-radius:var(--van-cell-group-inset-radius);overflow:hidden}.van-cell-group__title{padding:var(--van-cell-group-title-padding);color:var(--van-cell-group-title-color);font-size:var(--van-cell-group-title-font-size);line-height:var(--van-cell-group-title-line-height)}.van-cell-group__title--inset{padding:var(--van-cell-group-inset-title-padding)}:root,:host{--van-loading-text-color:var(--van-text-color-2);--van-loading-text-font-size:var(--van-font-size-md);--van-loading-spinner-color:var(--van-gray-5);--van-loading-spinner-size:30px;--van-loading-spinner-duration:.8s}.van-loading{color:var(--van-loading-spinner-color);vertical-align:middle;font-size:0;position:relative}.van-loading__spinner{width:var(--van-loading-spinner-size);max-width:100%;height:var(--van-loading-spinner-size);vertical-align:middle;max-height:100%;animation:van-rotate var(--van-loading-spinner-duration) linear infinite;display:inline-block;position:relative}.van-loading__spinner--spinner{animation-timing-function:steps(12,end)}.van-loading__spinner--circular{animation-duration:2s}.van-loading__line{width:100%;height:100%;position:absolute;top:0;left:0}.van-loading__line:before{content:" ";background-color:currentColor;border-radius:40%;width:2px;height:25%;margin:0 auto;display:block}.van-loading__circular{width:100%;height:100%;display:block}.van-loading__circular circle{stroke:currentColor;stroke-width:3px;stroke-linecap:round;animation:1.5s ease-in-out infinite van-circular}.van-loading__text{margin-left:var(--van-padding-xs);color:var(--van-loading-text-color);font-size:var(--van-loading-text-font-size);vertical-align:middle;display:inline-block}.van-loading--vertical{flex-direction:column;align-items:center;display:flex}.van-loading--vertical .van-loading__text{margin:var(--van-padding-xs) 0 0}@keyframes van-circular{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-40px}to{stroke-dasharray:90 150;stroke-dashoffset:-120px}}.van-loading__line--1{opacity:1;transform:rotate(30deg)}.van-loading__line--2{opacity:.9375;transform:rotate(60deg)}.van-loading__line--3{opacity:.875;transform:rotate(90deg)}.van-loading__line--4{opacity:.8125;transform:rotate(120deg)}.van-loading__line--5{opacity:.75;transform:rotate(150deg)}.van-loading__line--6{opacity:.6875;transform:rotate(180deg)}.van-loading__line--7{opacity:.625;transform:rotate(210deg)}.van-loading__line--8{opacity:.5625;transform:rotate(240deg)}.van-loading__line--9{opacity:.5;transform:rotate(270deg)}.van-loading__line--10{opacity:.4375;transform:rotate(300deg)}.van-loading__line--11{opacity:.375;transform:rotate(330deg)}.van-loading__line--12{opacity:.3125;transform:rotate(360deg)}:root,:host{--van-button-mini-height:24px;--van-button-mini-padding:0 var(--van-padding-base);--van-button-mini-font-size:var(--van-font-size-xs);--van-button-small-height:32px;--van-button-small-padding:0 var(--van-padding-xs);--van-button-small-font-size:var(--van-font-size-sm);--van-button-normal-padding:0 15px;--van-button-normal-font-size:var(--van-font-size-md);--van-button-large-height:50px;--van-button-default-height:44px;--van-button-default-line-height:1.2;--van-button-default-font-size:var(--van-font-size-lg);--van-button-default-color:var(--van-text-color);--van-button-default-background:var(--van-background-2);--van-button-default-border-color:var(--van-gray-4);--van-button-primary-color:var(--van-white);--van-button-primary-background:var(--van-primary-color);--van-button-primary-border-color:var(--van-primary-color);--van-button-success-color:var(--van-white);--van-button-success-background:var(--van-success-color);--van-button-success-border-color:var(--van-success-color);--van-button-danger-color:var(--van-white);--van-button-danger-background:var(--van-danger-color);--van-button-danger-border-color:var(--van-danger-color);--van-button-warning-color:var(--van-white);--van-button-warning-background:var(--van-warning-color);--van-button-warning-border-color:var(--van-warning-color);--van-button-border-width:var(--van-border-width);--van-button-radius:var(--van-radius-md);--van-button-round-radius:var(--van-radius-max);--van-button-plain-background:var(--van-white);--van-button-disabled-opacity:var(--van-disabled-opacity);--van-button-icon-size:1.2em;--van-button-loading-icon-size:20px}.van-theme-dark{--van-button-plain-background:transparent}.van-button{box-sizing:border-box;height:var(--van-button-default-height);font-size:var(--van-button-default-font-size);line-height:var(--van-button-default-line-height);text-align:center;border-radius:var(--van-button-radius);cursor:pointer;transition:opacity var(--van-duration-fast);-webkit-appearance:none;-webkit-font-smoothing:auto;margin:0;padding:0;display:inline-block;position:relative}.van-button:before{background:var(--van-black);border:inherit;border-color:var(--van-black);border-radius:inherit;opacity:0;content:" ";box-sizing:content-box;width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.van-button:active:before{opacity:.1}.van-button--loading:before,.van-button--disabled:before{display:none}.van-button--default{color:var(--van-button-default-color);background:var(--van-button-default-background);border:var(--van-button-border-width) solid var(--van-button-default-border-color)}.van-button--primary{color:var(--van-button-primary-color);background:var(--van-button-primary-background);border:var(--van-button-border-width) solid var(--van-button-primary-border-color)}.van-button--success{color:var(--van-button-success-color);background:var(--van-button-success-background);border:var(--van-button-border-width) solid var(--van-button-success-border-color)}.van-button--danger{color:var(--van-button-danger-color);background:var(--van-button-danger-background);border:var(--van-button-border-width) solid var(--van-button-danger-border-color)}.van-button--warning{color:var(--van-button-warning-color);background:var(--van-button-warning-background);border:var(--van-button-border-width) solid var(--van-button-warning-border-color)}.van-button--plain{background:var(--van-button-plain-background)}.van-button--plain.van-button--primary{color:var(--van-button-primary-background)}.van-button--plain.van-button--success{color:var(--van-button-success-background)}.van-button--plain.van-button--danger{color:var(--van-button-danger-background)}.van-button--plain.van-button--warning{color:var(--van-button-warning-background)}.van-button--large{width:100%;height:var(--van-button-large-height)}.van-button--normal{padding:var(--van-button-normal-padding);font-size:var(--van-button-normal-font-size)}.van-button--small{height:var(--van-button-small-height);padding:var(--van-button-small-padding);font-size:var(--van-button-small-font-size)}.van-button__loading{color:inherit;font-size:inherit}.van-button__loading .van-loading__spinner{color:currentColor;width:var(--van-button-loading-icon-size);height:var(--van-button-loading-icon-size)}.van-button--mini{height:var(--van-button-mini-height);padding:var(--van-button-mini-padding);font-size:var(--van-button-mini-font-size)}.van-button--mini+.van-button--mini{margin-left:var(--van-padding-base)}.van-button--block{width:100%;display:block}.van-button--disabled{cursor:not-allowed;opacity:var(--van-button-disabled-opacity)}.van-button--loading{cursor:default}.van-button--round{border-radius:var(--van-button-round-radius)}.van-button--square{border-radius:0}.van-button__content{justify-content:center;align-items:center;height:100%;display:flex}.van-button__content:before{content:" "}.van-button__icon{font-size:var(--van-button-icon-size);line-height:inherit}.van-button__icon+.van-button__text,.van-button__loading+.van-button__text,.van-button__text+.van-button__icon,.van-button__text+.van-button__loading{margin-left:var(--van-padding-base)}.van-button--hairline{border-width:0}.van-button--hairline:after{border-color:inherit;border-radius:calc(var(--van-button-radius) * 2)}.van-button--hairline.van-button--round:after{border-radius:var(--van-button-round-radius)}.van-button--hairline.van-button--square:after{border-radius:0}:root,:host{--van-search-padding:10px var(--van-padding-sm);--van-search-background:var(--van-background-2);--van-search-content-background:var(--van-background);--van-search-input-height:34px;--van-search-label-padding:0 5px;--van-search-label-color:var(--van-text-color);--van-search-label-font-size:var(--van-font-size-md);--van-search-left-icon-color:var(--van-gray-6);--van-search-action-padding:0 var(--van-padding-xs);--van-search-action-text-color:var(--van-text-color);--van-search-action-font-size:var(--van-font-size-md)}.van-search{box-sizing:border-box;padding:var(--van-search-padding);background:var(--van-search-background);align-items:center;display:flex}.van-search__content{padding-left:var(--van-padding-sm);background:var(--van-search-content-background);border-radius:var(--van-radius-sm);flex:1;display:flex}.van-search__content--round{border-radius:var(--van-radius-max)}.van-search__label{padding:var(--van-search-label-padding);color:var(--van-search-label-color);font-size:var(--van-search-label-font-size);line-height:var(--van-search-input-height)}.van-search__field{padding:0 var(--van-padding-xs) 0 0;height:var(--van-search-input-height);background-color:#0000;flex:1;align-items:center}.van-search__field .van-field__left-icon{color:var(--van-search-left-icon-color)}.van-search__field--with-message{align-items:flex-start;height:auto;padding-top:5px;padding-bottom:5px}.van-search--show-action{padding-right:0}.van-search input::-webkit-search-decoration{display:none}.van-search input::-webkit-search-cancel-button{display:none}.van-search input::-webkit-search-results-button{display:none}.van-search input::-webkit-search-results-decoration{display:none}.van-search__action{padding:var(--van-search-action-padding);color:var(--van-search-action-text-color);font-size:var(--van-search-action-font-size);line-height:var(--van-search-input-height);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.van-search__action:active{background-color:var(--van-active-color)}:root,:host{--van-list-text-color:var(--van-text-color-2);--van-list-text-font-size:var(--van-font-size-md);--van-list-text-line-height:50px;--van-list-loading-icon-size:16px}.van-list__loading,.van-list__finished-text,.van-list__error-text{color:var(--van-list-text-color);font-size:var(--van-list-text-font-size);line-height:var(--van-list-text-line-height);text-align:center}.van-list__placeholder{pointer-events:none;height:0}.van-list__loading-icon .van-loading__spinner{width:var(--van-list-loading-icon-size);height:var(--van-list-loading-icon-size)}:root,:host{--van-pull-refresh-head-height:50px;--van-pull-refresh-head-font-size:var(--van-font-size-md);--van-pull-refresh-head-text-color:var(--van-text-color-2);--van-pull-refresh-loading-icon-size:16px}.van-pull-refresh{overflow:hidden}.van-pull-refresh__track{height:100%;transition-property:transform;position:relative}.van-pull-refresh__head{width:100%;height:var(--van-pull-refresh-head-height);color:var(--van-pull-refresh-head-text-color);font-size:var(--van-pull-refresh-head-font-size);line-height:var(--van-pull-refresh-head-height);text-align:center;position:absolute;left:0;overflow:hidden;transform:translateY(-100%)}.van-pull-refresh__loading .van-loading__spinner{width:var(--van-pull-refresh-loading-icon-size);height:var(--van-pull-refresh-loading-icon-size)}:root,:host{--van-tag-padding:0 var(--van-padding-base);--van-tag-text-color:var(--van-white);--van-tag-font-size:var(--van-font-size-sm);--van-tag-radius:2px;--van-tag-line-height:16px;--van-tag-medium-padding:2px 6px;--van-tag-large-padding:var(--van-padding-base) var(--van-padding-xs);--van-tag-large-radius:var(--van-radius-md);--van-tag-large-font-size:var(--van-font-size-md);--van-tag-round-radius:var(--van-radius-max);--van-tag-danger-color:var(--van-danger-color);--van-tag-primary-color:var(--van-primary-color);--van-tag-success-color:var(--van-success-color);--van-tag-warning-color:var(--van-warning-color);--van-tag-default-color:var(--van-gray-6);--van-tag-plain-background:var(--van-background-2)}.van-tag{padding:var(--van-tag-padding);color:var(--van-tag-text-color);font-size:var(--van-tag-font-size);line-height:var(--van-tag-line-height);border-radius:var(--van-tag-radius);align-items:center;display:inline-flex;position:relative}.van-tag--default{background:var(--van-tag-default-color)}.van-tag--default.van-tag--plain{color:var(--van-tag-default-color)}.van-tag--danger{background:var(--van-tag-danger-color)}.van-tag--danger.van-tag--plain{color:var(--van-tag-danger-color)}.van-tag--primary{background:var(--van-tag-primary-color)}.van-tag--primary.van-tag--plain{color:var(--van-tag-primary-color)}.van-tag--success{background:var(--van-tag-success-color)}.van-tag--success.van-tag--plain{color:var(--van-tag-success-color)}.van-tag--warning{background:var(--van-tag-warning-color)}.van-tag--warning.van-tag--plain{color:var(--van-tag-warning-color)}.van-tag--plain{background:var(--van-tag-plain-background);border-color:currentColor}.van-tag--plain:before{border:1px solid;border-color:inherit;border-radius:inherit;content:"";pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.van-tag--medium{padding:var(--van-tag-medium-padding)}.van-tag--large{padding:var(--van-tag-large-padding);font-size:var(--van-tag-large-font-size);border-radius:var(--van-tag-large-radius)}.van-tag--mark{border-radius:0 var(--van-tag-round-radius) var(--van-tag-round-radius) 0}.van-tag--mark:after{content:"";width:2px;display:block}.van-tag--round{border-radius:var(--van-tag-round-radius)}.van-tag__close{margin-left:2px}:root,:host{--van-sticky-z-index:99}.van-sticky--fixed{z-index:var(--van-sticky-z-index);position:fixed}:root,:host{--van-swipe-indicator-size:6px;--van-swipe-indicator-margin:var(--van-padding-sm);--van-swipe-indicator-active-opacity:1;--van-swipe-indicator-inactive-opacity:.3;--van-swipe-indicator-active-background:var(--van-primary-color);--van-swipe-indicator-inactive-background:var(--van-border-color)}.van-swipe{cursor:-webkit-grab;cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;overflow:hidden;transform:translateZ(0)}.van-swipe__track{height:100%;transition-property:transform;display:flex}.van-swipe__track--vertical{flex-direction:column}.van-swipe__indicators{bottom:var(--van-swipe-indicator-margin);display:flex;position:absolute;left:50%;transform:translate(-50%)}.van-swipe__indicators--vertical{top:50%;bottom:auto;left:var(--van-swipe-indicator-margin);flex-direction:column;transform:translateY(-50%)}.van-swipe__indicators--vertical .van-swipe__indicator:not(:last-child){margin-bottom:var(--van-swipe-indicator-size)}.van-swipe__indicator{width:var(--van-swipe-indicator-size);height:var(--van-swipe-indicator-size);background-color:var(--van-swipe-indicator-inactive-background);opacity:var(--van-swipe-indicator-inactive-opacity);transition:opacity var(--van-duration-fast),background-color var(--van-duration-fast);border-radius:100%}.van-swipe__indicator:not(:last-child){margin-right:var(--van-swipe-indicator-size)}.van-swipe__indicator--active{background-color:var(--van-swipe-indicator-active-background);opacity:var(--van-swipe-indicator-active-opacity)}:root,:host{--van-tab-text-color:var(--van-gray-7);--van-tab-active-text-color:var(--van-text-color);--van-tab-disabled-text-color:var(--van-text-color-3);--van-tab-font-size:var(--van-font-size-md);--van-tab-line-height:var(--van-line-height-md);--van-tabs-default-color:var(--van-primary-color);--van-tabs-line-height:44px;--van-tabs-card-height:30px;--van-tabs-nav-background:var(--van-background-2);--van-tabs-bottom-bar-width:40px;--van-tabs-bottom-bar-height:3px;--van-tabs-bottom-bar-color:var(--van-primary-color)}.van-tab{box-sizing:border-box;padding:0 var(--van-padding-base);color:var(--van-tab-text-color);font-size:var(--van-tab-font-size);line-height:var(--van-tab-line-height);cursor:pointer;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.van-tab--active{color:var(--van-tab-active-text-color);font-weight:var(--van-font-bold)}.van-tab--disabled{color:var(--van-tab-disabled-text-color);cursor:not-allowed}.van-tab--grow{padding:0 var(--van-padding-sm);flex:1 0 auto}.van-tab--shrink{padding:0 var(--van-padding-xs);flex:none}.van-tab--card{color:var(--van-tabs-default-color);border-right:var(--van-border-width) solid var(--van-tabs-default-color)}.van-tab--card:last-child{border-right:none}.van-tab--card.van-tab--active{color:var(--van-white);background-color:var(--van-tabs-default-color)}.van-tab--card.van-tab--disabled{color:var(--van-tab-disabled-text-color)}.van-tab__text--ellipsis{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-tabs{position:relative}.van-tabs__wrap{overflow:hidden}.van-tabs__wrap--page-top{position:fixed}.van-tabs__wrap--content-bottom{top:auto;bottom:0}.van-tabs__nav{background:var(--van-tabs-nav-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;position:relative}.van-tabs__nav--complete{-webkit-overflow-scrolling:touch;overflow-x:auto;overflow-y:hidden}.van-tabs__nav--complete::-webkit-scrollbar{display:none}.van-tabs__nav--line{box-sizing:content-box;height:100%;padding-bottom:15px}.van-tabs__nav--line.van-tabs__nav--shrink,.van-tabs__nav--line.van-tabs__nav--complete{padding-right:var(--van-padding-xs);padding-left:var(--van-padding-xs)}.van-tabs__nav--card{box-sizing:border-box;height:var(--van-tabs-card-height);margin:0 var(--van-padding-md);border:var(--van-border-width) solid var(--van-tabs-default-color);border-radius:var(--van-radius-sm)}.van-tabs__nav--card.van-tabs__nav--shrink{display:inline-flex}.van-tabs__line{z-index:1;width:var(--van-tabs-bottom-bar-width);height:var(--van-tabs-bottom-bar-height);background:var(--van-tabs-bottom-bar-color);border-radius:var(--van-tabs-bottom-bar-height);position:absolute;bottom:15px;left:0}.van-tabs__track{will-change:left;width:100%;height:100%;display:flex;position:relative}.van-tabs__content--animated{overflow:hidden}.van-tabs--line .van-tabs__wrap{height:var(--van-tabs-line-height)}.van-tabs--card>.van-tabs__wrap{height:var(--van-tabs-card-height)}.van-swipe-item{flex-shrink:0;width:100%;height:100%;position:relative}.van-tab__panel,.van-tab__panel-wrapper{box-sizing:border-box;flex-shrink:0;width:100%}.van-tab__panel-wrapper--inactive{height:0;overflow:visible}:root,:host{--van-overlay-z-index:1;--van-overlay-background:#000000b3}.van-overlay{z-index:var(--van-overlay-z-index);background:var(--van-overlay-background);width:100%;height:100%;position:fixed;top:0;left:0}:root,:host{--van-popup-background:var(--van-background-2);--van-popup-transition:transform var(--van-duration-base);--van-popup-round-radius:16px;--van-popup-close-icon-size:22px;--van-popup-close-icon-color:var(--van-gray-5);--van-popup-close-icon-margin:16px;--van-popup-close-icon-z-index:1}.van-overflow-hidden{overflow:hidden!important}.van-popup{box-sizing:border-box;background:var(--van-popup-background);max-height:100%;transition:var(--van-popup-transition);-webkit-overflow-scrolling:touch;position:fixed;overflow-y:auto}.van-popup--center{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100vw - var(--van-padding-md) * 2);margin:0 auto;top:50%;left:0;right:0;transform:translateY(-50%)}.van-popup--center.van-popup--round{border-radius:var(--van-popup-round-radius)}.van-popup--top{width:100%;top:0;left:0}.van-popup--top.van-popup--round{border-radius:0 0 var(--van-popup-round-radius) var(--van-popup-round-radius)}.van-popup--right{top:50%;right:0;transform:translateY(-50%)}.van-popup--right.van-popup--round{border-radius:var(--van-popup-round-radius) 0 0 var(--van-popup-round-radius)}.van-popup--bottom{width:100%;bottom:0;left:0}.van-popup--bottom.van-popup--round{border-radius:var(--van-popup-round-radius) var(--van-popup-round-radius) 0 0}.van-popup--left{top:50%;left:0;transform:translateY(-50%)}.van-popup--left.van-popup--round{border-radius:0 var(--van-popup-round-radius) var(--van-popup-round-radius) 0}.van-popup-slide-top-enter-active,.van-popup-slide-left-enter-active,.van-popup-slide-right-enter-active,.van-popup-slide-bottom-enter-active{transition-timing-function:var(--van-ease-out)}.van-popup-slide-top-leave-active,.van-popup-slide-left-leave-active,.van-popup-slide-right-leave-active,.van-popup-slide-bottom-leave-active{transition-timing-function:var(--van-ease-in)}.van-popup-slide-top-enter-from,.van-popup-slide-top-leave-active{transform:translateY(-100%)}.van-popup-slide-right-enter-from,.van-popup-slide-right-leave-active{transform:translate(100%,-50%)}.van-popup-slide-bottom-enter-from,.van-popup-slide-bottom-leave-active{transform:translateY(100%)}.van-popup-slide-left-enter-from,.van-popup-slide-left-leave-active{transform:translate(-100%,-50%)}.van-popup__close-icon{z-index:var(--van-popup-close-icon-z-index);color:var(--van-popup-close-icon-color);font-size:var(--van-popup-close-icon-size);position:absolute}.van-popup__close-icon--top-left{top:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--top-right{top:var(--van-popup-close-icon-margin);right:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-left{bottom:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-right{right:var(--van-popup-close-icon-margin);bottom:var(--van-popup-close-icon-margin)}:root,:host{--van-empty-padding:var(--van-padding-xl) 0;--van-empty-image-size:160px;--van-empty-description-margin-top:var(--van-padding-md);--van-empty-description-padding:0 60px;--van-empty-description-color:var(--van-text-color-2);--van-empty-description-font-size:var(--van-font-size-md);--van-empty-description-line-height:var(--van-line-height-md);--van-empty-bottom-margin-top:24px}.van-empty{box-sizing:border-box;padding:var(--van-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-empty__image{width:var(--van-empty-image-size);height:var(--van-empty-image-size)}.van-empty__image img{width:100%;height:100%}.van-empty__description{margin-top:var(--van-empty-description-margin-top);padding:var(--van-empty-description-padding);color:var(--van-empty-description-color);font-size:var(--van-empty-description-font-size);line-height:var(--van-empty-description-line-height)}.van-empty__bottom{margin-top:var(--van-empty-bottom-margin-top)}.van-theme-dark .van-empty{opacity:.5}:root,:host{--van-notice-bar-height:40px;--van-notice-bar-padding:0 var(--van-padding-md);--van-notice-bar-wrapable-padding:var(--van-padding-xs) var(--van-padding-md);--van-notice-bar-text-color:var(--van-orange-dark);--van-notice-bar-font-size:var(--van-font-size-md);--van-notice-bar-line-height:24px;--van-notice-bar-background:var(--van-orange-light);--van-notice-bar-icon-size:16px;--van-notice-bar-icon-min-width:24px}.van-notice-bar{height:var(--van-notice-bar-height);padding:var(--van-notice-bar-padding);color:var(--van-notice-bar-text-color);font-size:var(--van-notice-bar-font-size);line-height:var(--van-notice-bar-line-height);background:var(--van-notice-bar-background);align-items:center;display:flex;position:relative}.van-notice-bar__left-icon,.van-notice-bar__right-icon{min-width:var(--van-notice-bar-icon-min-width);font-size:var(--van-notice-bar-icon-size)}.van-notice-bar__right-icon{text-align:right;cursor:pointer}.van-notice-bar__wrap{flex:1;align-items:center;height:100%;display:flex;position:relative;overflow:hidden}.van-notice-bar__content{white-space:nowrap;transition-timing-function:linear;position:absolute}.van-notice-bar__content.van-ellipsis{max-width:100%}.van-notice-bar--wrapable{height:auto;padding:var(--van-notice-bar-wrapable-padding)}.van-notice-bar--wrapable .van-notice-bar__wrap{height:auto}.van-notice-bar--wrapable .van-notice-bar__content{white-space:normal;word-wrap:break-word;position:relative}:root,:host{--van-skeleton-title-width:40%}.van-skeleton-title{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-title--round{border-radius:var(--van-radius-max)}.van-skeleton-title{width:var(--van-skeleton-title-width);margin:0}.van-skeleton-title+.van-skeleton-paragraph{margin-top:20px}:root,:host{--van-skeleton-avatar-size:32px;--van-skeleton-avatar-background:var(--van-active-color)}.van-skeleton-avatar{width:var(--van-skeleton-avatar-size);height:var(--van-skeleton-avatar-size);margin-right:var(--van-padding-md);background:var(--van-skeleton-avatar-background);flex-shrink:0}.van-skeleton-avatar--round{border-radius:var(--van-radius-max)}.van-skeleton-avatar+.van-skeleton__content{padding-top:var(--van-padding-xs)}:root,:host{--van-skeleton-paragraph-height:16px;--van-skeleton-paragraph-background:var(--van-active-color);--van-skeleton-paragraph-margin-top:var(--van-padding-sm)}.van-skeleton-paragraph{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-paragraph--round{border-radius:var(--van-radius-max)}.van-skeleton-paragraph:not(:first-child){margin-top:var(--van-skeleton-paragraph-margin-top)}:root,:host{--van-skeleton-duration:1.2s}.van-skeleton{padding:0 var(--van-padding-md);display:flex}.van-skeleton__content{width:100%}.van-skeleton--animate{animation:van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite}@keyframes van-skeleton-blink{50%{opacity:.6}}:root,:host{--van-action-bar-background:var(--van-background-2);--van-action-bar-height:50px}.van-action-bar{box-sizing:content-box;height:var(--van-action-bar-height);background:var(--van-action-bar-background);align-items:center;display:flex;position:fixed;bottom:0;left:0;right:0}:root,:host{--van-action-bar-button-height:40px;--van-action-bar-button-warning-color:var(--van-gradient-orange);--van-action-bar-button-danger-color:var(--van-gradient-red)}.van-action-bar-button{height:var(--van-action-bar-button-height);font-weight:var(--van-font-bold);font-size:var(--van-font-size-md);border:none;border-radius:0;flex:1}.van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max);margin-left:5px}.van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max);margin-right:5px}.van-action-bar-button--warning{background:var(--van-action-bar-button-warning-color)}.van-action-bar-button--danger{background:var(--van-action-bar-button-danger-color)}@media (max-width:321px){.van-action-bar-button{font-size:13px}}:root,:host{--van-dialog-width:320px;--van-dialog-small-screen-width:90%;--van-dialog-font-size:var(--van-font-size-lg);--van-dialog-transition:var(--van-duration-base);--van-dialog-radius:16px;--van-dialog-background:var(--van-background-2);--van-dialog-header-font-weight:var(--van-font-bold);--van-dialog-header-line-height:24px;--van-dialog-header-padding-top:26px;--van-dialog-header-isolated-padding:var(--van-padding-lg) 0;--van-dialog-message-padding:var(--van-padding-lg);--van-dialog-message-font-size:var(--van-font-size-md);--van-dialog-message-line-height:var(--van-line-height-md);--van-dialog-message-max-height:60vh;--van-dialog-has-title-message-text-color:var(--van-gray-7);--van-dialog-has-title-message-padding-top:var(--van-padding-xs);--van-dialog-button-height:48px;--van-dialog-round-button-height:36px;--van-dialog-confirm-button-text-color:var(--van-primary-color)}.van-dialog{width:var(--van-dialog-width);font-size:var(--van-dialog-font-size);background:var(--van-dialog-background);border-radius:var(--van-dialog-radius);-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:var(--van-dialog-transition);transition-property:transform,opacity;top:45%;overflow:hidden}@media (max-width:321px){.van-dialog{width:var(--van-dialog-small-screen-width)}}.van-dialog__header{color:var(--van-text-color);padding-top:var(--van-dialog-header-padding-top);font-weight:var(--van-dialog-header-font-weight);line-height:var(--van-dialog-header-line-height);text-align:center}.van-dialog__header--isolated{padding:var(--van-dialog-header-isolated-padding)}.van-dialog__content--isolated{align-items:center;min-height:104px;display:flex}.van-dialog__message{color:var(--van-text-color);max-height:var(--van-dialog-message-max-height);padding:26px var(--van-dialog-message-padding);font-size:var(--van-dialog-message-font-size);line-height:var(--van-dialog-message-line-height);white-space:pre-wrap;text-align:center;word-wrap:break-word;-webkit-overflow-scrolling:touch;flex:1;overflow-y:auto}.van-dialog__message--has-title{padding-top:var(--van-dialog-has-title-message-padding-top);color:var(--van-dialog-has-title-message-text-color)}.van-dialog__message--left{text-align:left}.van-dialog__message--right{text-align:right}.van-dialog__message--justify{text-align:justify}.van-dialog__footer{-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;overflow:hidden}.van-dialog__confirm,.van-dialog__cancel{height:var(--van-dialog-button-height);border:0;border-radius:0;flex:1;margin:0}.van-dialog__confirm,.van-dialog__confirm:active{color:var(--van-dialog-confirm-button-text-color)}.van-dialog--round-button .van-dialog__footer{height:auto;padding:var(--van-padding-xs) var(--van-padding-lg) var(--van-padding-md);position:relative}.van-dialog--round-button .van-dialog__message{padding-bottom:var(--van-padding-md);color:var(--van-text-color)}.van-dialog--round-button .van-dialog__confirm,.van-dialog--round-button .van-dialog__cancel{height:var(--van-dialog-round-button-height)}.van-dialog--round-button .van-dialog__confirm{color:var(--van-white)}.van-dialog--round-button .van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max)}.van-dialog--round-button .van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max)}.van-dialog-bounce-enter-from{opacity:0;transform:translateY(-50%)scale(.7)}.van-dialog-bounce-leave-active{opacity:0;transform:translateY(-50%)scale(.9)}:root,:host{--van-toast-max-width:70%;--van-toast-font-size:var(--van-font-size-md);--van-toast-text-color:var(--van-white);--van-toast-loading-icon-color:var(--van-white);--van-toast-line-height:var(--van-line-height-md);--van-toast-radius:var(--van-radius-lg);--van-toast-background:#000000b3;--van-toast-icon-size:36px;--van-toast-text-min-width:96px;--van-toast-text-padding:var(--van-padding-xs) var(--van-padding-sm);--van-toast-default-padding:var(--van-padding-md);--van-toast-default-width:88px;--van-toast-default-min-height:88px;--van-toast-position-top-distance:20%;--van-toast-position-bottom-distance:20%}.van-toast{box-sizing:content-box;transition:all var(--van-duration-fast);width:var(--van-toast-default-width);max-width:var(--van-toast-max-width);min-height:var(--van-toast-default-min-height);padding:var(--van-toast-default-padding);color:var(--van-toast-text-color);font-size:var(--van-toast-font-size);line-height:var(--van-toast-line-height);white-space:pre-wrap;word-break:break-all;text-align:center;background:var(--van-toast-background);border-radius:var(--van-toast-radius);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-toast--break-normal{word-break:normal;word-wrap:normal}.van-toast--break-word{word-break:normal;word-wrap:break-word}.van-toast--unclickable{cursor:not-allowed;overflow:hidden}.van-toast--unclickable *{pointer-events:none}.van-toast--text,.van-toast--html{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;min-width:var(--van-toast-text-min-width);min-height:0;padding:var(--van-toast-text-padding)}.van-toast--text .van-toast__text,.van-toast--html .van-toast__text{margin-top:0}.van-toast--top{top:var(--van-toast-position-top-distance)}.van-toast--bottom{top:auto;bottom:var(--van-toast-position-bottom-distance)}.van-toast__icon{font-size:var(--van-toast-icon-size)}.van-toast__loading{padding:var(--van-padding-base);color:var(--van-toast-loading-icon-color)}.van-toast__text{margin-top:var(--van-padding-xs)}:root{--app-bg:#f4f7fd;--app-surface:#fff;--app-surface-2:#f6f8fe;--app-border:#6e89c92e;--app-text:#1d315f;--app-text-2:#3f527d;--app-muted:#7b89ab;--app-primary:#2f6fff;--app-primary-soft:#2f6fff1a;--app-success:#0f9e69;--app-success-soft:#13b77a1f;--app-warning:#c46f00;--app-warning-soft:#ff981d24;--app-danger:#e5383e;--app-danger-soft:#ff53581f;--app-muted-soft:#7b89ab24;--app-shadow:0 10px 28px #4e63a214;--app-radius:16px;--app-font:"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;--app-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root{--app-bg:#0b1222;--app-surface:#131c31;--app-surface-2:#18223b;--app-border:#8ca0d229;--app-text:#e6ecfb;--app-text-2:#bcc6e0;--app-muted:#8b97b6;--app-primary:#6f9bff;--app-primary-soft:#5b8cff29;--app-success:#2fd19a;--app-success-soft:#2fd19a24;--app-warning:#ffb45a;--app-warning-soft:#ffae4a24;--app-danger:#ff7a7f;--app-danger-soft:#ff6b7026;--app-muted-soft:#8390b029;--app-shadow:0 10px 28px #00000047;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*,:before,:after{box-sizing:border-box}html,body{background:var(--app-bg);color:var(--app-text);font-family:var(--app-font);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body,#app{min-height:100vh;min-height:100dvh}button,input,textarea{font-family:inherit}.app-card{background:var(--app-surface);border:1px solid var(--app-border);border-radius:var(--app-radius);box-shadow:var(--app-shadow)}.app-muted{color:var(--app-muted)}.app-mono{font-family:var(--app-mono);word-break:break-all;font-size:12px}.tone-success{color:var(--app-success)}.tone-warning{color:var(--app-warning)}.tone-danger{color:var(--app-danger)}.tone-muted{color:var(--app-muted)}.tone-primary{color:var(--app-primary)}.van-nav-bar .van-nav-bar__title{font-weight:600}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js new file mode 100644 index 000000000..2d6926fa3 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js @@ -0,0 +1 @@ +import{c as e}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{Dt as t,Kt as n,M as r,Mt as i,N as a,St as o,Vt as s,W as c,at as l,it as u}from"./index-C_mEe-tZ.js";function d(e,{args:t=[],done:n,canceled:r,error:i}){if(e){let a=e.apply(null,t);u(a)?a.then(e=>{e?n():r&&r()}).catch(i||l):a?n():r&&r()}else n()}var f=(o,l)=>{let u=n(),d=()=>{u.value=c(o).height};return i(()=>{if(t(d),l)for(let e=1;e<=3;e++)setTimeout(d,100*e)}),e(()=>t(d)),s([a,r],d),u};function p(e,t){let n=f(e,!0);return e=>o(`div`,{class:t(`placeholder`),style:{height:n.value?`${n.value}px`:void 0}},[e()])}export{d as n,p as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png differ diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png differ diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png differ diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png differ diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png differ diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/index.html b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/index.html new file mode 100644 index 000000000..3c05519b4 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + + + +
+ + + diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest new file mode 100644 index 000000000..2376a055c --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话黑名单。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/sw.js b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/deploy/mobile-http-20260928/payload/admin-web/apps/mobile-h5/dist/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/deploy/mobile-http-20260928/payload/wechat_rpa/remote_api.py b/deploy/mobile-http-20260928/payload/wechat_rpa/remote_api.py new file mode 100644 index 000000000..69ad3e384 --- /dev/null +++ b/deploy/mobile-http-20260928/payload/wechat_rpa/remote_api.py @@ -0,0 +1,804 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台与桌面命令通道的 HTTP 接口。 + + /api/v2/mobile/* 手机 / 平板 H5 用,ZYT 账号密码登录(终端 9,由本服务代登) + /api/v2/desktop/commands/* 桌面端长轮询领取指令、回执结果、顺带上报状态 + /api/v2/desktop/blacklist* 桌面端同步服务端会话黑名单 + +手机令牌和桌面令牌分别由 ``mobile_sessions`` 和 ``desktop_sessions`` 签发,互相 +不能冒用:手机端拿不到归档上传之类的桌面接口,桌面端也调不了手机接口。 +""" + +from __future__ import annotations + +import asyncio +import ipaddress +import os +import threading +import time +from collections import defaultdict, deque +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request +from pydantic import BaseModel, Field + +import zyt_auth +from remote_store import ( + COMMAND_TYPES, + MOBILE_IDENTITY_RECHECK_SECONDS, + PERM_BLACKLIST_ADD, + PERM_BLACKLIST_REMOVE, + PERM_REMOTE_ALL, + PERM_REMOTE_CONTROL, + RemoteStore, + account_label, + account_permissions, + valid_device_id, +) + +# 桌面端长轮询最长挂起时间。uvicorn 停机时会等在途请求结束,这里必须明显短于 +# compose 里 20 秒的 stop_grace_period,否则每次发版都会被强杀;指令本身靠进程内 +# 通知即时送达,挂起时长只影响"没事时多久回来报一次状态"。 +MAX_POLL_WAIT_SECONDS = 15 +# 挂起期间多久回库复查一次;进程内通知失效(多进程部署)时的兜底。 +POLL_RECHECK_SECONDS = 5 + + +def _error(status_code: int, code: str, message: str, **extra: Any) -> HTTPException: + return HTTPException(status_code=status_code, detail={"code": code, "message": message, **extra}) + + +def _bearer(request: Request) -> str: + raw = request.headers.get("authorization", "") + return raw[7:].strip() if raw.lower().startswith("bearer ") else "" + + +def trusted_client_ip(request: Request) -> str: + """做安全判断用的客户端地址。 + + X-Forwarded-For 的第一段是客户端自己填的,可以伪造;X-Real-IP 由 Nginx 按 + $remote_addr 覆盖写入,API 端口又只监听本机回环,只有 Nginx 能连进来。 + """ + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return real_ip + return request.client.host if request.client else "" + + +def _is_private_address(value: str) -> bool: + try: + address = ipaddress.ip_address(str(value or "").strip()) + except ValueError: + return False + return address.is_private or address.is_loopback or address.is_link_local + + +def request_scheme(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-proto", "").split(",")[0].strip().lower() + return forwarded or request.url.scheme.lower() + + +def https_required(request: Request) -> bool: + """手机端登录是否必须走 HTTPS。 + + MOBILE_REQUIRE_HTTPS:never(默认,内外网都允许明文 HTTP 登录,按用户要求)/ + auto(公网地址必须 HTTPS,内网可用 HTTP)/ always。配好 HTTPS 入口后建议改成 + auto:明文 HTTP 下 ZYT 密码和手机会话令牌在公网上可被截获。 + + auto 模式下没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker + 网桥(172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 + (开发时直接访问 127.0.0.1)才在没有这个头时被信任。 + """ + policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "never").strip().lower() + if policy == "never" or request_scheme(request) == "https": + return False + if policy == "always": + return True + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return not _is_private_address(real_ip) + peer = request.client.host if request.client else "" + try: + return not ipaddress.ip_address(peer).is_loopback + except ValueError: + return True + + +class LoginAttempt: + __slots__ = ("ip", "account", "finished") + + def __init__(self, ip: str, account: str): + self.ip, self.account, self.finished = ip, account, False + + +class MobileLoginGuard: + """手机登录限流。 + + ZYT 按请求来源 IP 计数锁定(默认 30 分钟内错 5 次锁 30 分钟),而手机端的 + 登录全部由本服务代发,在 ZYT 看来都来自服务器出口 IP——如果公司电脑也走同一 + 个出口,锁住的就是全公司的 ZYT 登录。所以这里除了按账号、按客户端 IP 限流, + 还给"真正发到 ZYT 的密码错误"设了一个全局预算,并在 ZYT 已锁定时直接短路。 + + 限流必须把"正在进行"的尝试也算进去:先检查、等 ZYT 回来再记账的话,一次并发 + 发来十几个请求会全部通过检查,一起打到 ZYT 上。`begin()` 在锁里占位, + `finish()` 按结果转成失败记录或释放。 + """ + + def __init__( + self, *, account_limit: int = 5, account_window: int = 600, + ip_limit: int = 10, ip_window: int = 600, + upstream_budget: int | None = None, upstream_window: int = 1800, + ): + if upstream_budget is None: + try: + upstream_budget = int(os.environ.get("MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET", "4")) + except ValueError: + upstream_budget = 4 + self.account_limit, self.account_window = account_limit, account_window + self.ip_limit, self.ip_window = ip_limit, ip_window + self.upstream_budget, self.upstream_window = max(0, upstream_budget), upstream_window + self._accounts: dict[str, deque[float]] = {} + self._ips: dict[str, deque[float]] = {} + self._upstream: deque[float] = deque() + self._pending_accounts: dict[str, int] = defaultdict(int) + self._pending_ips: dict[str, int] = defaultdict(int) + self._pending_upstream = 0 + self._locked_until = 0.0 + self._lock = threading.Lock() + + @staticmethod + def _recent(book: dict[str, deque[float]], key: str, window: int, now: float) -> deque[float]: + """剪掉过期记录;空了就把这个键删掉,随机账号名刷不大内存。""" + items = book.get(key) + if items is None: + return deque() + while items and items[0] <= now - window: + items.popleft() + if not items: + del book[key] + return items + + @staticmethod + def _minutes(seconds: float) -> int: + return max(1, int((seconds + 59) // 60)) + + def begin(self, ip: str, account: str) -> LoginAttempt: + now = time.time() + with self._lock: + if self._locked_until > now: + wait = self._locked_until - now + raise _error( + 429, "zyt_locked", + f"ZYT 已暂时锁定登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + failures = self._recent(self._accounts, account, self.account_window, now) + if len(failures) >= self.account_limit: + wait = failures[0] + self.account_window - now + raise _error( + 429, "rate_limited", + f"该账号登录失败次数过多,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(failures) + self._pending_accounts.get(account, 0) >= self.account_limit: + raise _error(429, "rate_limited", "该账号正在登录,请稍候几秒再试", retry_after=5) + failures = self._recent(self._ips, ip, self.ip_window, now) + if len(failures) + self._pending_ips.get(ip, 0) >= self.ip_limit: + wait = (failures[0] + self.ip_window - now) if failures else 5 + raise _error( + 429, "rate_limited", + f"登录尝试过于频繁,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + while self._upstream and self._upstream[0] <= now - self.upstream_window: + self._upstream.popleft() + if self.upstream_budget: + if len(self._upstream) >= self.upstream_budget: + wait = self._upstream[0] + self.upstream_window - now + raise _error( + 429, "upstream_budget", + "近期手机端密码错误次数较多,为避免 ZYT 锁定服务器出口 IP," + f"已暂停手机登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(self._upstream) + self._pending_upstream >= self.upstream_budget: + raise _error(429, "rate_limited", "同时登录的人较多,请几秒后再试", retry_after=5) + self._pending_accounts[account] += 1 + self._pending_ips[ip] += 1 + self._pending_upstream += 1 + return LoginAttempt(ip, account) + + def finish(self, attempt: LoginAttempt, outcome: str) -> None: + """outcome:credentials(密码错)、unknown_account(账号不存在)、success(ZYT 认可了密码)、 + other(网络、终端未开通等,不记账)。""" + if attempt.finished: + return + attempt.finished = True + now = time.time() + with self._lock: + for book, key in ((self._pending_accounts, attempt.account), (self._pending_ips, attempt.ip)): + book[key] -= 1 + if book[key] <= 0: + del book[key] + self._pending_upstream = max(0, self._pending_upstream - 1) + if outcome in ("credentials", "unknown_account"): + self._accounts.setdefault(attempt.account, deque()).append(now) + self._ips.setdefault(attempt.ip, deque()).append(now) + # ZYT 只给"账号存在、密码不对"累计出口 IP 的失败次数。 + if outcome == "credentials": + self._upstream.append(now) + elif outcome == "success": + self._accounts.pop(attempt.account, None) + # 密码校验通过时 ZYT 会清零该出口 IP 的失败计数(AdminAccountSafeCache::relieve)。 + self._upstream.clear() + + def locked(self, seconds: int) -> None: + with self._lock: + self._locked_until = max(self._locked_until, time.time() + max(60, int(seconds))) + + +class ZytMobileAuth: + """默认实现:真的去调 ZYT。测试里换成假的。""" + + def __init__(self, api_url: Callable[[], str]): + self._api_url = api_url + + def login(self, account: str, password: str) -> str: + return zyt_auth.login_admin( + account, password, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def identity(self, token: str) -> dict[str, Any]: + return zyt_auth.verify_admin_session( + token, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def logout(self, token: str) -> None: + zyt_auth.logout_admin(token, api_url=self._api_url()) + + +class _Waiter: + __slots__ = ("key", "loop", "event") + + def __init__(self, key: tuple[int, str]): + self.key = key + self.loop = asyncio.get_running_loop() + self.event = asyncio.Event() + + async def wait(self, timeout: float) -> None: + try: + await asyncio.wait_for(self.event.wait(), timeout=max(0.0, timeout)) + except asyncio.TimeoutError: + pass + + +class CommandNotifier: + """进程内唤醒:有新指令或黑名单变化时,立刻结束对应电脑挂着的长轮询。 + + 先登记再查库:登记和查库之间落库的指令也能把它叫醒,不会白等一个复查周期。 + 唤醒走 ``call_soon_threadsafe``,发通知的请求和挂起的长轮询不在同一个事件 + 循环(线程)里也不会漏掉。 + """ + + def __init__(self) -> None: + self._waiters: dict[tuple[int, str], set[_Waiter]] = {} + self._lock = threading.Lock() + + def register(self, account_id: int, device_id: str) -> _Waiter: + waiter = _Waiter((int(account_id), str(device_id))) + with self._lock: + self._waiters.setdefault(waiter.key, set()).add(waiter) + return waiter + + def unregister(self, waiter: _Waiter) -> None: + with self._lock: + waiters = self._waiters.get(waiter.key) + if waiters is not None: + waiters.discard(waiter) + if not waiters: + self._waiters.pop(waiter.key, None) + + def notify(self, account_id: int, device_id: str | None = None) -> None: + with self._lock: + targets = [ + waiter + for key, waiters in self._waiters.items() + if key[0] == int(account_id) and (device_id is None or key[1] == device_id) + for waiter in waiters + ] + for waiter in targets: + try: + waiter.loop.call_soon_threadsafe(waiter.event.set) + except RuntimeError: + # 事件循环已经关闭(请求刚好结束),没有人需要被叫醒。 + continue + + +class MobilePrincipal: + def __init__(self, row: dict[str, Any], token: str): + self.row = row + self.token = token + self.permissions = account_permissions(row) + + @property + def account_id(self) -> int: + return int(self.row["id"]) + + @property + def session_id(self) -> int: + return int(self.row["session_id"]) + + @property + def label(self) -> str: + return f"{account_label(self.row)}(手机)" + + def me(self) -> dict[str, Any]: + return { + "account": { + "id": self.account_id, + "nickname": str(self.row.get("nickname") or ""), + "sn": str(self.row.get("zyt_sn") or ""), + "avatar": str(self.row.get("avatar") or ""), + }, + "permissions": { + "control": self.permissions["control"], + "control_all": self.permissions["control_all"], + "blacklist_add": self.permissions["blacklist_add"], + "blacklist_remove": self.permissions["blacklist_remove"], + "root": self.permissions["root"], + "known": self.permissions["known"], + }, + "permission_codes": { + "control": PERM_REMOTE_CONTROL, + "control_all": PERM_REMOTE_ALL, + "blacklist_add": PERM_BLACKLIST_ADD, + "blacklist_remove": PERM_BLACKLIST_REMOVE, + }, + "session": { + "created_at": str(self.row.get("session_created_at") or ""), + "expires_at": int(self.row.get("session_expires_at") or 0), + }, + } + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class MobileLoginBody(BaseModel): + account: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=1, max_length=128, repr=False) + client_name: str = Field(default="", max_length=120) + + +class CommandBody(BaseModel): + type: str = Field(min_length=1, max_length=32) + + +class BlacklistBody(BaseModel): + name: str = Field(default="", max_length=191) + account_name: str = Field(default="", max_length=191) + reason: str = Field(default="", max_length=200) + + +class StatusBody(BaseModel): + listening: bool = False + state: str = Field(default="", max_length=32) + transport: str = Field(default="", max_length=16) + wecom_connected: bool = False + pending_count: int = Field(default=0, ge=0, le=1_000_000) + app_version: str = Field(default="", max_length=64) + capabilities: list[str] = Field(default_factory=list, max_length=16) + + +class DesktopPollBody(BaseModel): + wait: int = Field(default=15, ge=0, le=60) + status: StatusBody | None = None + # 桌面端当前持有的黑名单版本;与服务端不一致时立即返回,让它去同步。 + blacklist_version: int | None = Field(default=None, ge=0) + + +class AckBody(BaseModel): + ok: bool + result: dict[str, Any] = Field(default_factory=dict) + error: str = Field(default="", max_length=500) + status: StatusBody | None = None + + +class DesktopBlacklistSyncBody(BaseModel): + add: list[dict[str, Any]] = Field(default_factory=list, max_length=500) + + +def register_remote_routes( + app: FastAPI, + database: Any, + *, + current_desktop: Callable[..., Any], + zyt_api_url: Callable[[], str], + zyt_mobile_auth: Any = None, + login_guard: MobileLoginGuard | None = None, +) -> RemoteStore: + store = RemoteStore(database) + store.initialize() + app.state.remote_store = store + notifier = CommandNotifier() + app.state.remote_notifier = notifier + guard = login_guard or MobileLoginGuard() + auth = zyt_mobile_auth or ZytMobileAuth(zyt_api_url) + identity_locks: dict[int, asyncio.Lock] = {} + + # ── 手机端鉴权 ─────────────────────────────────────────────────────── + async def _recheck_identity(row: dict[str, Any], request: Request) -> dict[str, Any] | None: + # 同一会话并发的几个请求只复核一次;用完即删,锁表不随会话数增长。 + session_id = int(row["session_id"]) + lock = identity_locks.setdefault(session_id, asyncio.Lock()) + try: + async with lock: + return await _recheck_identity_locked(session_id, request) + finally: + if not lock.locked() and identity_locks.get(session_id) is lock: + identity_locks.pop(session_id, None) + + async def _recheck_identity_locked(session_id: int, request: Request) -> dict[str, Any] | None: + fresh = await asyncio.to_thread(store.mobile_session, _bearer(request)) + if fresh is None: + return None + if time.time() - int(fresh["identity_checked_at"] or 0) < MOBILE_IDENTITY_RECHECK_SECONDS: + return fresh + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, session_id) + try: + if not zyt_token: + raise zyt_auth.ZytLoginError("ZYT 会话缺失", kind="rejected") + identity = await asyncio.to_thread(auth.identity, zyt_token) + if str(identity.get("user_id") or "") != str(fresh["zyt_user_id"]): + raise zyt_auth.ZytLoginError("ZYT 身份与手机会话不一致", kind="rejected") + except zyt_auth.ZytAuthError as exc: + if getattr(exc, "kind", "") == "unavailable": + # ZYT 暂时连不上不算下线:沿用上次同步的权限,一分钟后再试。 + await asyncio.to_thread(store.mark_identity_checked, session_id, retry_in=60) + return fresh + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="zyt_invalid", ip=trusted_client_ip(request), + ) + return None + try: + await asyncio.to_thread(store.refresh_identity, session_id, identity) + except PermissionError: + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="account_disabled", ip=trusted_client_ip(request), + ) + return None + return await asyncio.to_thread(store.mobile_session, _bearer(request)) + + async def current_mobile(request: Request) -> MobilePrincipal: + token = _bearer(request) + row = await asyncio.to_thread(store.mobile_session, token) if token else None + if row is not None and time.time() - int(row["identity_checked_at"] or 0) >= MOBILE_IDENTITY_RECHECK_SECONDS: + row = await _recheck_identity(row, request) + if row is None: + raise _error(401, "session_expired", "登录已失效,请重新登录") + return MobilePrincipal(row, token) + + def target_account(principal: MobilePrincipal, account_id: int | None) -> dict[str, Any]: + if account_id is None or int(account_id) == principal.account_id: + return principal.row + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", "只能查看和控制自己的电脑") + account = store.account(int(account_id)) + if account is None or str(account.get("status")) != "active": + raise _error(404, "account_not_found", "客服账号不存在或已停用") + return account + + # ── 手机端:登录 ───────────────────────────────────────────────────── + @app.post("/api/v2/mobile/auth/login") + async def mobile_login(body: MobileLoginBody, request: Request) -> dict: + ip = trusted_client_ip(request) + if https_required(request): + raise _error(403, "https_required", "请通过 HTTPS 地址访问手机控制台,明文连接不能提交密码") + account_key = body.account.strip().lower() + attempt = guard.begin(ip, account_key) + outcome = "other" + try: + zyt_token = await asyncio.to_thread(auth.login, body.account.strip(), body.password) + outcome = "success" + except zyt_auth.ZytLoginError as exc: + if exc.kind in ("credentials", "unknown_account"): + outcome = exc.kind + database.audit(None, "mobile.login.failed", f"account={account_key[:64]}", ip) + raise _error(401, "invalid_credentials", "账号或密码错误") from exc + if exc.kind == "password_change_required": + outcome = "success" + raise _error(403, "password_change_required", str(exc)) from exc + if exc.kind == "locked": + guard.locked(exc.retry_after or 1800) + database.audit(None, "mobile.login.locked", f"account={account_key[:64]}", ip) + raise _error(429, "zyt_locked", str(exc), retry_after=exc.retry_after) from exc + if exc.kind == "disabled": + raise _error(403, "account_disabled", str(exc)) from exc + if exc.kind == "terminal_unsupported": + raise _error(503, "terminal_unsupported", str(exc)) from exc + if exc.kind == "unavailable": + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + raise _error(400, "login_rejected", str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + finally: + guard.finish(attempt, outcome) + try: + identity = await asyncio.to_thread(auth.identity, zyt_token) + account = await asyncio.to_thread(store.upsert_account, identity) + except zyt_auth.ZytLoginError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + status_code = {"terminal_unsupported": 503, "disabled": 403}.get(exc.kind, 502) + code = {"terminal_unsupported": "terminal_unsupported", + "disabled": "account_disabled"}.get(exc.kind, "zyt_unavailable") + raise _error(status_code, code, str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + except PermissionError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(403, "account_disabled", str(exc)) from exc + except ValueError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + token, session = await asyncio.to_thread( + store.create_mobile_session, int(account["id"]), zyt_token=zyt_token, + client_name=body.client_name, user_agent=request.headers.get("user-agent", ""), ip=ip, + ) + row = await asyncio.to_thread(store.mobile_session, token) + principal = MobilePrincipal(row, token) + return {"access_token": token, "expires_at": session["expires_at"], "me": principal.me()} + + @app.post("/api/v2/mobile/auth/logout") + async def mobile_logout(request: Request, principal: MobilePrincipal = Depends(current_mobile)) -> dict: + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, principal.session_id) + await asyncio.to_thread( + store.revoke_mobile_session, session_id=principal.session_id, reason="logout", + ip=trusted_client_ip(request), + ) + if zyt_token: + await asyncio.to_thread(auth.logout, zyt_token) + return {"ok": True} + + @app.get("/api/v2/mobile/me") + async def mobile_me(principal: MobilePrincipal = Depends(current_mobile)) -> dict: + return principal.me() + + # ── 手机端:电脑 ───────────────────────────────────────────────────── + @app.get("/api/v2/mobile/accounts") + async def mobile_accounts( + q: str = Query(default="", max_length=64), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", f"查看全部客服电脑需要 ZYT 权限 {PERM_REMOTE_ALL}") + return {"items": await asyncio.to_thread(store.accounts_overview, q)} + + @app.get("/api/v2/mobile/desktops") + async def mobile_desktops( + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + items = await asyncio.to_thread(store.devices, int(account["id"])) + return { + "account": { + "id": int(account["id"]), + "nickname": str(account.get("nickname") or ""), + "sn": str(account.get("zyt_sn") or ""), + }, + "items": items, + "server_time": int(time.time()), + } + + @app.post("/api/v2/mobile/desktops/{device_id}/commands") + async def mobile_create_command( + device_id: str, body: CommandBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + if not principal.permissions["control"]: + raise _error(403, "forbidden", f"远程启停需要 ZYT 权限 {PERM_REMOTE_CONTROL}") + if body.type not in COMMAND_TYPES: + raise _error(400, "invalid_command", "不支持的指令") + try: + device_id = valid_device_id(device_id) + except ValueError as exc: + raise _error(404, "device_not_found", "电脑不存在") from exc + device = await asyncio.to_thread(store.device, int(account["id"]), device_id) + if device is None: + raise _error(404, "device_not_found", "电脑不存在或已被管理端下线") + if not device["online"]: + raise _error(409, "device_offline", "电脑离线:客服软件没有运行,无法远程拉起") + if not device["remote_capable"]: + raise _error(409, "client_outdated", "该电脑上的客服软件版本过低,请升级后再远程控制") + try: + command = await asyncio.to_thread( + store.create_command, account_id=int(account["id"]), device_id=device_id, + command_type=body.type, actor_account_id=principal.account_id, + actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "device_not_found", str(exc)) from exc + notifier.notify(int(account["id"]), device_id) + return {"command": command} + + @app.get("/api/v2/mobile/commands/{command_id}") + async def mobile_command( + command_id: str, principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + command = await asyncio.to_thread(store.command, command_id) + if command is None: + raise _error(404, "command_not_found", "指令不存在") + if command["account_id"] != principal.account_id and not principal.permissions["control_all"]: + raise _error(404, "command_not_found", "指令不存在") + command.pop("account_id", None) + return {"command": command} + + # ── 手机端:会话与黑名单 ───────────────────────────────────────────── + @app.get("/api/v2/mobile/conversations") + async def mobile_conversations( + account_id: int | None = Query(default=None, ge=1), + q: str = Query(default="", max_length=100), + scope: str = Query(default="all", pattern="^(all|blocked)$"), + cursor: str = Query(default="", max_length=200), + limit: int = Query(default=30, ge=1, le=100), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + try: + return await asyncio.to_thread( + store.conversations, str(account["tenant_id"]), query=q, scope=scope, + cursor=cursor, limit=limit, + ) + except ValueError as exc: + raise _error(400, "invalid_cursor", str(exc)) from exc + + @app.put("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_add( + account: str, conversation: str, body: BlacklistBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_add"]: + raise _error(403, "forbidden", f"加入黑名单需要 ZYT 权限 {PERM_BLACKLIST_ADD}") + try: + entry, version = await asyncio.to_thread( + store.add_blacklist, str(target["tenant_id"]), + {"account": account, "conversation": conversation, **body.model_dump()}, + source="mobile", actor_label=principal.label, + actor_account_id=principal.account_id, ip=trusted_client_ip(request), + ) + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"entry": entry, "version": version} + + @app.delete("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_remove( + account: str, conversation: str, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_remove"]: + raise _error(403, "forbidden", f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管") + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(target["tenant_id"]), account, conversation, + source="mobile", actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"ok": True, "version": version} + + # ── 桌面端:命令通道 ───────────────────────────────────────────────── + def _desktop_label(account: Any) -> str: + return f"{account_label(account)}(电脑 {str(account['device_name'] or '')[:40]})" + + async def _report(account: Any, status: StatusBody | None) -> None: + if status is None: + return + await asyncio.to_thread( + store.report_status, int(account["id"]), str(account["device_id"]), + status.model_dump(), app_version=str(account["app_version"] or ""), + ) + + @app.post("/api/v2/desktop/commands/poll") + async def desktop_poll( + body: DesktopPollBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + account_id = int(account["id"]) + device_id = str(account["device_id"]) + tenant = str(account["tenant_id"]) + await _report(account, body.status) + wait = float(min(body.wait, MAX_POLL_WAIT_SECONDS)) + deadline = time.monotonic() + wait + while True: + waiter = notifier.register(account_id, device_id) + try: + commands = await asyncio.to_thread(store.take_commands, account_id, device_id) + version = await asyncio.to_thread(store.blacklist_version, tenant) + remaining = deadline - time.monotonic() + if ( + commands + or (body.blacklist_version is not None and version != body.blacklist_version) + or remaining <= 0 + or await request.is_disconnected() + ): + break + await waiter.wait(min(remaining, POLL_RECHECK_SECONDS)) + finally: + notifier.unregister(waiter) + return {"commands": commands, "blacklist_version": version, "server_time": int(time.time())} + + @app.post("/api/v2/desktop/commands/{command_id}/ack") + async def desktop_ack( + command_id: str, body: AckBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + await _report(account, body.status) + try: + command = await asyncio.to_thread( + store.ack_command, account_id=int(account["id"]), + device_id=str(account["device_id"]), command_id=command_id, ok=body.ok, + result=body.result, error=body.error, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "command_not_found", str(exc)) from exc + return {"ok": True, "command": command} + + @app.post("/api/v2/desktop/status") + async def desktop_status(body: StatusBody, account: Any = Depends(current_desktop)) -> dict: + await _report(account, body) + return {"ok": True} + + # ── 桌面端:黑名单 ─────────────────────────────────────────────────── + @app.get("/api/v2/desktop/blacklist") + async def desktop_blacklist(account: Any = Depends(current_desktop)) -> dict: + version, items = await asyncio.to_thread(store.blacklist, str(account["tenant_id"])) + return {"version": version, "items": items} + + @app.post("/api/v2/desktop/blacklist/sync") + async def desktop_blacklist_sync( + body: DesktopBlacklistSyncBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + # 电脑前的人加黑名单不要求 ZYT 权限:拉黑只会让系统少发消息,是安全方向; + # 离线时加的、升级前本地就有的条目也都从这里补传。 + before = await asyncio.to_thread(store.blacklist_version, str(account["tenant_id"])) + version, items, rejected = await asyncio.to_thread( + store.sync_desktop_blacklist, str(account["tenant_id"]), body.add, + actor_label=_desktop_label(account), actor_account_id=int(account["id"]), + ip=trusted_client_ip(request), + ) + if version != before: + notifier.notify(int(account["id"])) + return {"version": version, "items": items, "rejected": rejected} + + @app.delete("/api/v2/desktop/blacklist/{account_key}/{conversation:path}") + async def desktop_blacklist_remove( + account_key: str, conversation: str, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + if not account_permissions(account)["blacklist_remove"]: + raise _error( + 403, "forbidden", + f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管在手机控制台操作", + ) + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(account["tenant_id"]), account_key, conversation, + source="desktop", actor_label=_desktop_label(account), ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(account["id"])) + return {"ok": True, "version": version} + + return store diff --git a/deploy/mobile-http-20260928/prepare_bundle.py b/deploy/mobile-http-20260928/prepare_bundle.py new file mode 100644 index 000000000..c05b39c69 --- /dev/null +++ b/deploy/mobile-http-20260928/prepare_bundle.py @@ -0,0 +1,97 @@ +"""Assemble the server bundle locally: payload + manifest + tar.gz. No upload, no deployment. + + python prepare_bundle.py + +Sources: C:/kefu/wechat_rpa/remote_api.py and the rebuilt C:/kefu/admin-web/apps/mobile-h5/dist. +Production for these files is exactly the mobile-remote-20260928 payload, so the replaced file is +diffed against that payload and the diff is written to payload-diff.txt for review. +""" +from pathlib import Path +import difflib, hashlib, json, shutil, tarfile + +folder = Path(__file__).resolve().parent +kefu = folder.parent.parent +previous = folder.parent / 'mobile-remote-20260928' / 'payload' +plan = json.loads((folder / 'release-plan.json').read_text(encoding='utf-8')) +base = json.loads((folder / 'server-baseline.json').read_text(encoding='utf-8')) +sha = lambda data: hashlib.sha256(data).hexdigest() +lf = lambda data: data.replace(b'\r\n', b'\n') +frontend = plan['frontendDir'] +BANNER = '只能在公司内网登录'.encode() + +# 1. The recorded baseline is what the previous release put on the server. +for name, wanted in base['expected'].items(): + assert sha((previous / name).read_bytes()) == wanted, 'Baseline differs from the deployed payload: ' + name + +# 2. Fresh payload from the working trees; record the backend diff. +payload = folder / 'payload' +if payload.exists(): + shutil.rmtree(payload) +diff_lines = [] +for name in plan['backendFiles'] + plan['newBackendFiles'] + plan['deployFiles']: + data = lf((kefu / name).read_bytes()) + assert b'\r' not in data, 'Stray carriage return: ' + name + if name.endswith('.py'): + compile(data, name, 'exec') + before = previous / name + if before.exists(): + diff_lines += difflib.unified_diff(before.read_text(encoding='utf-8').splitlines(), + data.decode('utf-8').splitlines(), + 'production/' + name, 'release/' + name, lineterm='') + target = payload / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) +(folder / 'payload-diff.txt').write_text('\n'.join(diff_lines) + '\n', encoding='utf-8') +dist = kefu / frontend +assert (dist / 'index.html').is_file(), 'Build the phone console first: cd admin-web/apps/mobile-h5 && pnpm build' +for text in dist.rglob('*'): + if text.is_file(): + data = text.read_bytes() + if text.suffix in {'.html', '.js', '.css', '.json', '.webmanifest'}: + assert b'\r\n' not in data, 'CRLF in build output: ' + str(text) + assert BANNER not in data, 'Old intranet-only banner still in build: ' + str(text) +shutil.copytree(dist, payload / frontend) + +tests = folder / 'tests' +if tests.exists(): + shutil.rmtree(tests) +tests.mkdir() +for module in plan['serverTestModules']: + data = lf((kefu / 'wechat_rpa' / f'{module}.py').read_bytes()) + compile(data, module, 'exec') + (tests / f'{module}.py').write_bytes(data) + +scripts = ['release-plan.json', 'server-baseline.json', 'Dockerfile.overlay', 'remote_server_guard.py', + '01-preflight.sh', '02-build.sh', '03-deploy.sh', '04-verify.sh', 'rollback.sh'] + +# 3. Manifest over everything the server will verify. +manifest = [] +for path in sorted(p for p in list(payload.rglob('*')) + list(tests.rglob('*')) if p.is_file()): + data = path.read_bytes() + manifest.append({'path': path.relative_to(folder).as_posix(), 'sha256': sha(data), 'bytes': len(data)}) +(folder / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8') + +# 4. Server bundle (LF line endings for shell scripts) and archive. +bundle = folder / 'server-bundle' +if bundle.exists(): + shutil.rmtree(bundle) +bundle.mkdir() +shutil.copytree(payload, bundle / 'payload') +shutil.copytree(tests, bundle / 'tests') +for name in scripts + ['manifest.json']: + (bundle / name).write_bytes(lf((folder / name).read_bytes())) +archive = folder / 'mobile-http-server-bundle.tar.gz' +if archive.exists(): + archive.unlink() +with tarfile.open(archive, 'x:gz') as tf: + for p in sorted(bundle.rglob('*')): + if p.is_file(): + arc = p.relative_to(bundle).as_posix(); assert not arc.startswith(('/', '..')) + info = tf.gettarinfo(str(p), arcname=arc) + info.mode = 0o755 if arc.endswith('.sh') else 0o644 + info.uid = info.gid = 0 + info.uname = info.gname = '' + with p.open('rb') as handle: + tf.addfile(info, handle) +print(json.dumps({'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes()), + 'files': len(manifest), 'diff_lines': len(diff_lines), 'uploaded': False}, ensure_ascii=False, indent=1)) diff --git a/deploy/mobile-http-20260928/release-plan.json b/deploy/mobile-http-20260928/release-plan.json new file mode 100644 index 000000000..e57b78740 --- /dev/null +++ b/deploy/mobile-http-20260928/release-plan.json @@ -0,0 +1,23 @@ +{ + "release": "mobile-http-20260928", + "candidateRoot": "C:/kefu", + "backendFiles": [ + "wechat_rpa/remote_api.py" + ], + "newBackendFiles": [], + "deployFiles": [], + "frontendDir": "admin-web/apps/mobile-h5/dist", + "serverTestModules": [ + "test_remote_api", + "test_zyt_auth", + "test_backend" + ], + "imageTestModules": [ + "test_archive_api" + ], + "schemaMigration": "none", + "restartServices": [ + "api" + ], + "notes": "User request 2026-09-28: the phone console must also work over plain HTTP from outside the office. MOBILE_REQUIRE_HTTPS now defaults to never (auto/always still available once HTTPS exists); the /m/ login page no longer shows the intranet-only banner. No database change." +} diff --git a/deploy/mobile-http-20260928/remote_server_guard.py b/deploy/mobile-http-20260928/remote_server_guard.py new file mode 100644 index 000000000..2187ccf7e --- /dev/null +++ b/deploy/mobile-http-20260928/remote_server_guard.py @@ -0,0 +1,138 @@ +"""Read-only checks for mobile-http-20260928: baseline / candidate / deployed. + +Prints hashes and status only; never reads chat content, patients or credentials. +""" +from pathlib import Path +import argparse, hashlib, json, subprocess, urllib.error, urllib.request + +parser = argparse.ArgumentParser() +parser.add_argument('mode', choices=['baseline', 'candidate', 'deployed']) +args = parser.parse_args() +RELEASE = 'mobile-http-20260928' +BANNER = '只能在公司内网登录' +folder = Path(__file__).resolve().parent +project = Path('/opt/im-admin') +base = json.loads((folder / 'server-baseline.json').read_text()) +plan = json.loads((folder / 'release-plan.json').read_text()) +manifest = json.loads((folder / 'manifest.json').read_text()) +payload_rows = [row for row in manifest if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +sha = lambda data: hashlib.sha256(data).hexdigest() +image = lambda ref: subprocess.check_output(['docker', 'image', 'inspect', ref, '--format', '{{.Id}}'], text=True).strip() + +# The unpacked bundle itself must be intact. +for row in manifest: + path = (folder / row['path']).resolve() + assert path.is_relative_to(folder), row['path'] + assert path.stat().st_size == row['bytes'] and sha(path.read_bytes()) == row['sha256'], row['path'] + +# project-relative path -> expected sha256 on the host +if args.mode == 'baseline': + expected_host = dict(base['expected']) +else: + expected_host = {row['path'][len('payload/'):]: row['sha256'] for row in payload_rows} +container_code = {k: v for k, v in expected_host.items() if k.startswith(('wechat_rpa/', 'admin-web/'))} +frontend_files = sorted(k[len(frontend) + 1:] for k in expected_host if k.startswith(frontend + '/')) + + +def in_container(code, target, ref=None): + command = (['docker', 'run', '--rm', '-i', '--network=none', '--entrypoint', 'python', ref, '-c', code] + if ref else ['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code]) + return json.loads(subprocess.check_output(command, input=json.dumps(target).encode())) + + +HASHES = ('from pathlib import Path;import hashlib,json,sys;names=json.load(sys.stdin);' + 'print(json.dumps({n:(hashlib.sha256((Path("/app")/n).read_bytes()).hexdigest() ' + 'if (Path("/app")/n).is_file() else None) for n in names}))') +# /m/ 目录里的完整文件清单(不能残留旧版带横幅的页面文件)和横幅文字检查。 +FRONTEND = ('from pathlib import Path;import json,sys;q=json.load(sys.stdin);root=Path("/app")/q["root"];' + 'files=sorted(p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file());' + 'print(json.dumps({"files":files,"banner":[f for f in files if q["banner"] in ' + '(root/f).read_bytes().decode("utf-8","ignore")]}))') +FRONTEND_QUERY = {'root': frontend, 'banner': BANNER} + +current = image('zyt/wecom-admin:current') +result = {'mode': args.mode, 'release': RELEASE, 'image': current} +# 只重建 api:网关和知识库 worker 继续跑它们自己的镜像,部署后核对它们没有被动过。 +OTHERS = ['im-admin-gateway-1', 'im-admin-knowledge-worker-1'] + + +def inspect(container): + raw = json.loads(subprocess.check_output(['docker', 'inspect', container]))[0] + return {'id': raw['Id'], 'image': raw['Image'], 'running': raw['State']['Running'], + 'health': raw['State'].get('Health', {}).get('Status', 'not_configured')} + + +if args.mode == 'baseline': + api = inspect('im-admin-api-1') + assert api['image'] == current, 'API container is not on zyt/wecom-admin:current' + assert api['running'] and api['health'] == 'healthy', 'API container is not healthy' + others = {name: inspect(name) for name in OTHERS} + for name, state in others.items(): + assert state['running'], 'Not running: ' + name + result['others'] = others + +if args.mode in ('baseline', 'deployed'): + for name, wanted in expected_host.items(): + assert sha((project / name).read_bytes()) == wanted, 'Host drift: ' + name + actual = in_container(HASHES, sorted(container_code)) + assert actual == container_code, 'Running api container differs from expected code' + result['files'] = len(expected_host) + +if args.mode == 'candidate': + candidate = 'zyt/wecom-admin:' + RELEASE + actual = in_container(HASHES, sorted(container_code), candidate) + assert actual == container_code, 'Candidate image differs from manifest' + listing = in_container(FRONTEND, FRONTEND_QUERY, candidate) + assert listing['files'] == frontend_files, 'Candidate /m/ has extra or missing files' + assert not listing['banner'], listing['banner'] + result['candidate'] = image(candidate) + result['files'] = len(container_code) + +if args.mode == 'deployed': + wanted = image('zyt/wecom-admin:' + RELEASE) + assert current == wanted, 'current tag does not point at the release image' + raw = json.loads(subprocess.check_output(['docker', 'inspect', 'im-admin-api-1']))[0] + assert raw['Image'] == wanted and raw['State']['Running'] and raw['RestartCount'] == 0 + assert raw['State'].get('Health', {}).get('Status') == 'healthy' + recorded = folder / 'baseline-check.json' + if recorded.exists(): + before = json.loads(recorded.read_text())['others'] + for name in OTHERS: + now = inspect(name) + assert now['id'] == before[name]['id'] and now['running'], 'Unrelated container changed: ' + name + result['others_unchanged'] = OTHERS + host_files = sorted(p.relative_to(project / frontend).as_posix() for p in (project / frontend).rglob('*') if p.is_file()) + assert host_files == frontend_files, 'Host /m/ has extra or missing files' + listing = in_container(FRONTEND, FRONTEND_QUERY) + assert listing['files'] == frontend_files and not listing['banner'], listing + for url in ['http://127.0.0.1:18766/api/v2/health', 'http://127.0.0.1:18770/health']: + with urllib.request.urlopen(url, timeout=15) as response: + assert response.status == 200, url + # New routes still refuse anonymous callers. No login is attempted. + for method, path in [('GET', '/api/v2/mobile/me'), ('GET', '/api/v2/mobile/desktops'), + ('POST', '/api/v2/desktop/commands/poll'), ('GET', '/api/v2/desktop/blacklist')]: + request = urllib.request.Request('http://127.0.0.1:18766' + path, method=method, + data=b'{}' if method == 'POST' else None, + headers={'Content-Type': 'application/json'}) + try: + urllib.request.urlopen(request, timeout=15) + except urllib.error.HTTPError as exc: + assert exc.code == 401, (path, exc.code) + else: + raise AssertionError('Accessible without authentication: ' + path) + with urllib.request.urlopen('http://127.0.0.1:18766/m/', timeout=15) as response: + body = response.read() + assert response.status == 200 and '企微客服助手'.encode() in body, 'mobile console entry missing' + # 本次目的:公网来源的明文 HTTP 登录不再被拒绝(在运行中的容器里、用它的真实环境变量判断)。 + code = ("import remote_api,json;from starlette.requests import Request;" + "r=Request({'type':'http','method':'POST','path':'/api/v2/mobile/auth/login','root_path':''," + "'headers':[(b'x-real-ip',b'114.114.114.114')],'scheme':'http','query_string':b''," + "'client':('172.18.0.1',40000),'server':('127.0.0.1',8766)});" + "print(json.dumps({'public_http_login_refused':remote_api.https_required(r)}))") + policy = json.loads(subprocess.check_output(['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code])) + assert policy == {'public_http_login_refused': False}, policy + result['policy'] = policy + +result['status'] = 'passed' +print(json.dumps(result, ensure_ascii=False)) diff --git a/deploy/mobile-http-20260928/rollback.sh b/deploy/mobile-http-20260928/rollback.sh new file mode 100644 index 000000000..c78ea26dd --- /dev/null +++ b/deploy/mobile-http-20260928/rollback.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# 恢复 remote_api.py 和原 /m/ 目录,切回旧镜像(mobile-remote-20260928),只重建 api。不涉及数据库。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd /opt/im-admin/deploy/im-admin +test -d "$release/source-before" +test "$(docker image inspect zyt/wecom-admin:before-mobile-http-20260928 --format '{{.Id}}')" = "$(cat "$release/image-before.txt")" +python3 - <<'PY' +from pathlib import Path +import hashlib, json, os, shutil +r = Path('/home/ps/mobile-http-20260928'); project = Path('/opt/im-admin') +base = json.loads((r / 'server-baseline.json').read_text()) +plan = json.loads((r / 'release-plan.json').read_text()) +sha = lambda data: hashlib.sha256(data).hexdigest() +for name in plan['backendFiles'] + plan['deployFiles']: + data = (r / 'source-before' / name).read_bytes(); assert sha(data) == base['expected'][name] + target = project / name; tmp = target.with_name(target.name + '.rollback-http.tmp') + if tmp.exists(): + tmp.unlink() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, target.stat().st_mode & 0o777); os.replace(tmp, target) +frontend = plan['frontendDir'] +saved = r / 'source-before' / frontend +for name, wanted in base['expected'].items(): + if name.startswith(frontend + '/'): + assert sha((saved / name[len(frontend) + 1:]).read_bytes()) == wanted, name +restore = project / (frontend + '.rollback-http.tmp') +if restore.exists(): + shutil.rmtree(restore) +shutil.copytree(saved, restore) +for leftover in [project / frontend, project / (frontend + '.mobile-http.tmp')]: + if leftover.exists(): + shutil.rmtree(leftover) +old = project / (frontend + '.mobile-http.old') +if old.exists(): + shutil.rmtree(old) +os.replace(restore, project / frontend) +PY +docker tag zyt/wecom-admin:before-mobile-http-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +curl -fsS -o /dev/null http://127.0.0.1:18766/api/v2/health +curl -fsS -o /dev/null http://127.0.0.1:18770/health +printf '%s\n' 'Previous image, remote_api.py and /m/ restored.' diff --git a/deploy/mobile-http-20260928/server-baseline.json b/deploy/mobile-http-20260928/server-baseline.json new file mode 100644 index 000000000..c587fe501 --- /dev/null +++ b/deploy/mobile-http-20260928/server-baseline.json @@ -0,0 +1,40 @@ +{ + "host": "192.168.110.111", + "user": "ps", + "project": "/opt/im-admin", + "compose": "/opt/im-admin/deploy/im-admin", + "checked_at": "2026-09-28", + "source": "Production as deployed by mobile-remote-20260928 (15:21): remote_api.py and the 28 /m/ files from that release payload; remote_api.py hash re-read from the host before building this release.", + "expected": { + "admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js": "97abcbc7c960add5c448947dad40ea8ca6ca2ae18069a632b62b61723e43f24f", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js": "8c06092db48aca97b0d3fb35871315e4434b5ad5ccd9c5d44a7cfc3ba343db22", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js": "7a648b566b9c00c1c8d1b6d2ca9737ca237e521f0e25065cedf86e82fa2471d8", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js": "c145f55146ff5373b293e2922b021f55c6851fa34db65fdacb64139fb88c4fa8", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js": "ddb9c705e5cc4822e9166686a7eab319c772540981134f30a9f850fc85ac8692", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js": "393394f3545f3224264be9d1eb87c72b6ce474651b3794b3839f43bfbd69aa86", + "admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js": "d6a7fc2e067a2adcf92b498812422755d5ac8f00acfcf07fd935201eee35c056", + "admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js": "63a10401ab035b495ba24c55f9b1bd7ef54c76c6d6bc549f4b0eda62dbd915ac", + "admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css": "b561a20e6d76366db7634b028098ed9f848f665b95f0ffe538f02abce87e8991", + "admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js": "ded80bbda052a9da5f7beaab6af536258b706f2bf0688331fa8bccd9e23355c6", + "admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js": "711a690039d1fe0d648c285da7bd1ef5b62afe8249d10ad7e76208318fc3094d", + "admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js": "8a2afb226d06ece5a929c2c85c4c73bbf68977b4ccde960aa7f6a5ae4614af62", + "admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js": "168066192a95a203466245545388b756dfd035c466b007e65ccadc1c94f3fd80", + "admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "admin-web/apps/mobile-h5/dist/icons/favicon-64.png": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "admin-web/apps/mobile-h5/dist/icons/icon-192.png": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "admin-web/apps/mobile-h5/dist/icons/icon-512.png": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "admin-web/apps/mobile-h5/dist/index.html": "d11023af89db5c20bde0c4eaaa35fd9ad192a85353165c0a675e139ae5d94f13", + "admin-web/apps/mobile-h5/dist/manifest.webmanifest": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "admin-web/apps/mobile-h5/dist/sw.js": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "wechat_rpa/remote_api.py": "843f0a814a0de4f8078f7f9f23a4ac92388d7556c9fe378987f67eda46c55c5b" + }, + "must_not_exist": [] +} diff --git a/deploy/mobile-http-20260928/server-bundle/01-preflight.sh b/deploy/mobile-http-20260928/server-bundle/01-preflight.sh new file mode 100644 index 000000000..2ddb9643b --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/01-preflight.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# 基线核对、备份将被替换的源文件和 /m/ 目录、给当前镜像打回滚标签。只读线上 + 写本目录。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +test ! -e baseline-check.json +python3 remote_server_guard.py baseline > baseline-check.json.tmp +mv baseline-check.json.tmp baseline-check.json +cat baseline-check.json +test ! -e source-before +test ! -e preflight-complete +if docker image inspect zyt/wecom-admin:before-mobile-http-20260928 >/dev/null 2>&1; then + echo 'Refuse to replace existing rollback tag' >&2; exit 1 +fi +mkdir -m 700 source-before +python3 - <<'PY' +from pathlib import Path +import json, shutil +r = Path('/home/ps/mobile-http-20260928'); project = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + target = r / 'source-before' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(project / name, target) +shutil.copytree(project / plan['frontendDir'], r / 'source-before' / plan['frontendDir']) +PY +docker tag zyt/wecom-admin:current zyt/wecom-admin:before-mobile-http-20260928 +docker image inspect zyt/wecom-admin:current --format '{{.Id}}' > image-before.txt +printf '%s\n' ready > preflight-complete +echo "preflight complete; rollback image tagged before-mobile-http-20260928 ($(cat image-before.txt))" diff --git a/deploy/mobile-http-20260928/server-bundle/02-build.sh b/deploy/mobile-http-20260928/server-bundle/02-build.sh new file mode 100644 index 000000000..4662eea14 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/02-build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# 构建候选镜像 + 离线跑测试 + 核对候选镜像文件。线上服务不受影响。 +# 本次不改数据库(只改登录的 HTTPS 默认策略和 /m/ 静态文件),不做整库备份;回滚靠旧镜像和 source-before。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +test -f preflight-complete +test ! -e candidate-passed +python3 remote_server_guard.py baseline +test "$(docker image inspect zyt/wecom-admin:before-mobile-http-20260928 --format '{{.Id}}')" = "$(cat image-before.txt)" +docker build --network=none --pull=false -f Dockerfile.overlay -t zyt/wecom-admin:mobile-http-20260928 . > candidate-build.log 2>&1 +# serverTestModules 随包放在 tests/;imageTestModules 用镜像自带的测试文件(PYTHONPATH 后一项)。 +# -P:不把工作目录 /app/wechat_rpa 放到 sys.path 最前面,否则镜像里同名的旧测试会盖住随包的新测试。 +mapfile -t modules < <(python3 -c 'import json;p=json.load(open("release-plan.json"));print("\n".join(p["serverTestModules"]+p.get("imageTestModules",[])))') +test "${#modules[@]}" -gt 0 +docker run --rm --network=none --read-only --tmpfs /tmp:size=512m,mode=1777 --tmpfs /data:size=256m,mode=1777 \ + -v "$release/tests:/release-tests:ro" -e PYTHONPATH=/release-tests:/app/wechat_rpa \ + zyt/wecom-admin:mobile-http-20260928 python -X utf8 -B -P -m unittest "${modules[@]}" -q > candidate-tests.log 2>&1 +tail -n 3 candidate-tests.log +python3 remote_server_guard.py candidate +printf '%s\n' passed > candidate-passed diff --git a/deploy/mobile-http-20260928/server-bundle/03-deploy.sh b/deploy/mobile-http-20260928/server-bundle/03-deploy.sh new file mode 100644 index 000000000..fbb06a557 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/03-deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# 替换 remote_api.py 和整个 /m/ 目录;切换镜像,只重建 api 容器。任何一步失败自动调用 rollback.sh。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +test -f candidate-passed +python3 remote_server_guard.py baseline +cd /opt/im-admin/deploy/im-admin +docker compose config --quiet +rollback_on_error() { rc=$?; trap - ERR; bash "$release/rollback.sh"; exit "$rc"; } +trap rollback_on_error ERR +python3 - <<'PY' +from pathlib import Path +import json, hashlib, os, shutil +r = Path('/home/ps/mobile-http-20260928'); target = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +rows = [row for row in json.loads((r / 'manifest.json').read_text()) if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +# 容器从镜像里读 /m/,宿主上的目录只给以后整包重建用;仍然整体换名,避免留下半套文件。 +staging = target / (frontend + '.mobile-http.tmp'); assert not staging.exists() +old = target / (frontend + '.mobile-http.old'); assert not old.exists() +for row in rows: + rel = row['path'][len('payload/'):] + data = (r / row['path']).read_bytes(); assert hashlib.sha256(data).hexdigest() == row['sha256'], rel + if rel.startswith(frontend + '/'): + dest = staging / rel[len(frontend) + 1:] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data); os.chmod(dest, 0o644) + continue + dest = (target / rel).resolve(); assert dest.is_relative_to(target) and dest.exists() + tmp = dest.with_name(dest.name + '.mobile-http.tmp'); assert not tmp.exists() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, dest.stat().st_mode & 0o777); os.replace(tmp, dest) +for directory in [staging] + [p for p in staging.rglob('*') if p.is_dir()]: + os.chmod(directory, 0o755) +final = target / frontend +os.replace(final, old) +os.replace(staging, final) +shutil.rmtree(old) # 原目录已在 source-before 里 +PY +docker tag zyt/wecom-admin:mobile-http-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +bash "$release/04-verify.sh" +trap - ERR +printf '%s\n' 'Plain-HTTP phone login enabled, deployed and verified.' diff --git a/deploy/mobile-http-20260928/server-bundle/04-verify.sh b/deploy/mobile-http-20260928/server-bundle/04-verify.sh new file mode 100644 index 000000000..117df1ba0 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/04-verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd "$release" +python3 remote_server_guard.py deployed | tee deployed-verification.json diff --git a/deploy/mobile-http-20260928/server-bundle/Dockerfile.overlay b/deploy/mobile-http-20260928/server-bundle/Dockerfile.overlay new file mode 100644 index 000000000..769afeb35 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/Dockerfile.overlay @@ -0,0 +1,9 @@ +ARG BASE_IMAGE=zyt/wecom-admin:before-mobile-http-20260928 +FROM ${BASE_IMAGE} +# 先删掉旧版 /m/ 目录再放新版,镜像里不残留旧的(带内网横幅的)页面文件。 +USER root +RUN rm -rf /app/admin-web/apps/mobile-h5/dist +USER app +COPY --chown=app:app payload/wechat_rpa/remote_api.py /app/wechat_rpa/remote_api.py +COPY --chown=app:app payload/admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ +WORKDIR /app/wechat_rpa diff --git a/deploy/mobile-http-20260928/server-bundle/manifest.json b/deploy/mobile-http-20260928/server-bundle/manifest.json new file mode 100644 index 000000000..ada9d72f1 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/manifest.json @@ -0,0 +1,162 @@ +[ + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js", + "sha256": "8d6186020c3d8562f581ea579742dd497649203a880b9c13cc904462b134bc60", + "bytes": 2308 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css", + "sha256": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "bytes": 359 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js", + "sha256": "226d4cde03c5434f20aa96c84cfb33538af084d79805348a089e343c5056281f", + "bytes": 14528 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png", + "sha256": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "bytes": 16743 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js", + "sha256": "b0d4b420bc5197a5df6d918e740e2b90b108c6e06ba930c3fd8e97fabe010556", + "bytes": 4490 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js", + "sha256": "75cd3809eb0d7db6c994302dd296674eadd366a53a4feb97a759173d70546e80", + "bytes": 28526 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css", + "sha256": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "bytes": 4220 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css", + "sha256": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "bytes": 3983 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js", + "sha256": "237de4f76004cbf16e5928060cf009fbbb3beec826006a876ddbede0ee9512f7", + "bytes": 10460 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js", + "sha256": "da55fbacb606cdb51f6eb09fb8bf4963fe249e869d653f0fb71d3c0da9ed7b4c", + "bytes": 8422 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js", + "sha256": "a98c816d31f9a937c5418dd48d25bc5b7b96fb3084d58571565d0810f420ced9", + "bytes": 14363 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-C_mEe-tZ.js", + "sha256": "6c6620136850dbdfa6ea22a9fa6c320f3f5d33150b83f1d1b9597e0894f5d0de", + "bytes": 117757 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css", + "sha256": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "bytes": 97051 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js", + "sha256": "29bdcbe8030ef2e169b2ea453cef160f2b3c86b9a83718971023a9862e10bd45", + "bytes": 6336 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css", + "sha256": "15ecdd5cb294bb51eef05e4843ac3a65f6e1718cbf16206547a876c52c5e8a2d", + "bytes": 1107 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css", + "sha256": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "bytes": 927 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js", + "sha256": "9e5ff0c332dfae54d9f8e1c70b0a896e48aef80e8b45101092c08a59dfddf521", + "bytes": 3465 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css", + "sha256": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "bytes": 130 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js", + "sha256": "13f70e99d3eccfa014f14dc313a195b4f24200929bf60142f6fd190ef33f2f3d", + "bytes": 2939 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js", + "sha256": "530e97b69f045bf3c2798b79c1f49bd3ee69f53a657bad39825cb69d68552c9c", + "bytes": 607 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png", + "sha256": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "bytes": 24681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png", + "sha256": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "bytes": 5439 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png", + "sha256": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "bytes": 26433 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png", + "sha256": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "bytes": 144310 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png", + "sha256": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "bytes": 95961 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/index.html", + "sha256": "6f335e7804fe9a1ce7bb5df388d2af81c7d919ddebb3beaee9533553a36adf04", + "bytes": 1357 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest", + "sha256": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "bytes": 684 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/sw.js", + "sha256": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "bytes": 339 + }, + { + "path": "payload/wechat_rpa/remote_api.py", + "sha256": "e8a51c1701a05d54713f45325e03481ad22609bc721455aa5ddef0d3ed5ae29a", + "bytes": 37707 + }, + { + "path": "tests/test_backend.py", + "sha256": "7473ec5583c97941e132ade7a1f66abcf54fa6108ec6bb2a182f0152964fff61", + "bytes": 66279 + }, + { + "path": "tests/test_remote_api.py", + "sha256": "479cb9b042dc14707af718734c33688013203d4154f36199b7b0ced656653c39", + "bytes": 38523 + }, + { + "path": "tests/test_zyt_auth.py", + "sha256": "40ab2d37f51780af26497e22948ef665c92b7f3707f6ba30a6d711bf5b15dfa6", + "bytes": 4907 + } +] \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css new file mode 100644 index 000000000..03b9e2d0d --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css @@ -0,0 +1 @@ +.switcher[data-v-91cf283c]{max-height:80vh}.switcher-body[data-v-91cf283c]{padding:16px 0 12px}.switcher-body h3[data-v-91cf283c]{margin:0 16px 8px;font-size:17px}.switcher-state[data-v-91cf283c]{place-items:center;padding:32px 0;display:grid}.switcher-list[data-v-91cf283c]{margin-top:12px}.current[data-v-91cf283c]{color:var(--app-primary);margin-left:6px} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js new file mode 100644 index 000000000..0537ea489 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{c as n,d as r,l as i,u as a}from"./format-2xch3ZbY.js";import{n as o,o as s,s as c,t as l}from"./cell-group-CEKvg8Wf.js";import{n as u,r as d,t as f}from"./field-Bs9V8Rb1.js";import{$ as p,Bt as m,Ct as h,Dt as g,E as _,Et as v,Ft as y,G as b,Gt as x,Jt as S,K as C,Kt as w,Lt as T,Mt as E,Nt as ee,O as D,Q as O,St as k,Ut as A,V as te,Vt as j,X as M,Y as N,Yt as ne,Z as re,_ as P,_t as F,a as ie,c as ae,et as oe,ft as se,g as I,gt as L,ht as ce,l as le,n as ue,o as R,ot as z,q as B,qt as V,rt as de,vt as fe,wt as pe,x as me,yt as H}from"./index-C_mEe-tZ.js";var U=0;function he(e){e?(U||document.body.classList.add(`van-toast--unclickable`),U++):U&&(U--,U||document.body.classList.remove(`van-toast--unclickable`))}var[ge,W]=P(`toast`),_e=[`show`,`overlay`,`teleport`,`transition`,`overlayClass`,`overlayStyle`,`closeOnClickOverlay`,`zIndex`],ve={icon:String,show:Boolean,type:N(`text`),overlay:Boolean,message:M,iconSize:M,duration:C(2e3),position:N(`middle`),teleport:[String,Object],wordBreak:String,className:null,iconPrefix:String,transition:N(`van-fade`),loadingType:String,forbidClick:Boolean,overlayClass:null,overlayStyle:Object,closeOnClick:Boolean,closeOnClickOverlay:Boolean,zIndex:M},ye=h({name:ge,props:ve,emits:[`update:show`],setup(t,{emit:n,slots:r}){let i,o=!1,c=()=>{let e=t.show&&t.forbidClick;o!==e&&(o=e,he(o))},l=e=>n(`update:show`,e),u=()=>{t.closeOnClick&&l(!1)},d=()=>clearTimeout(i),f=()=>{let{icon:n,type:r,iconSize:i,iconPrefix:a,loadingType:o}=t;if(n||r===`success`||r===`fail`)return k(e,{name:n||r,size:i,class:W(`icon`),classPrefix:a},null);if(r===`loading`)return k(s,{class:W(`loading`),size:i,type:o},null)},p=()=>{let{type:e,message:n}=t;if(r.message)return k(`div`,{class:W(`text`)},[r.message()]);if(oe(n)&&n!==``)return e===`html`?k(`div`,{key:0,class:W(`text`),innerHTML:String(n)},null):k(`div`,{class:W(`text`)},[n])};return j(()=>[t.show,t.forbidClick],c),j(()=>[t.show,t.type,t.message,t.duration],()=>{d(),t.show&&t.duration>0&&(i=setTimeout(()=>{l(!1)},t.duration))}),E(c),ee(c),()=>k(a,v({class:[W([t.position,t.wordBreak===`normal`?`break-normal`:t.wordBreak,{[t.type]:!t.icon}]),t.className],lockScroll:!1,onClick:u,onClosed:d,"onUpdate:show":l},z(t,_e)),{default:()=>[f(),p()]})}}),be={icon:``,type:`text`,message:``,className:``,overlay:!1,onClose:void 0,onOpened:void 0,duration:2e3,teleport:`body`,iconSize:void 0,iconPrefix:void 0,position:`middle`,transition:`van-fade`,forbidClick:!1,loadingType:void 0,overlayClass:``,overlayStyle:void 0,closeOnClick:!1,closeOnClickOverlay:!1},G=[],K=O({},be),xe=new Map;function Se(e){return de(e)?e:{message:e}}function Ce(){let{instance:e,unmount:t}=n({setup(){let e=w(``),{open:t,state:n,close:r,toggle:a}=i(),o=()=>{},s=()=>k(ye,v(n,{onClosed:o,"onUpdate:show":a}),null);return j(e,e=>{n.message=e}),pe().render=s,{open:t,close:r,message:e}}});return e}function we(){if(!G.length){let e=Ce();G.push(e)}return G[G.length-1]}function Te(e={}){if(!p)return{};let t=we(),n=Se(e);return t.open(O({},K,xe.get(n.type||K.type),n)),t}var[Ee,q]=P(`empty`),De={image:N(`default`),imageSize:[Number,String,Array],description:String},Oe=h({name:Ee,props:De,setup(e,{slots:t}){let n=()=>{let n=t.description?t.description():e.description;if(n)return k(`p`,{class:q(`description`)},[n])},r=()=>{if(t.default)return k(`div`,{class:q(`bottom`)},[t.default()])},i=d(),a=e=>`${i}-${e}`,o=e=>`url(#${a(e)})`,s=(e,t,n)=>k(`stop`,{"stop-color":e,offset:`${t}%`,"stop-opacity":n},null),c=(e,t)=>[s(e,0),s(t,100)],l=e=>[k(`defs`,null,[k(`radialGradient`,{id:a(e),cx:`50%`,cy:`54%`,fx:`50%`,fy:`54%`,r:`297%`,gradientTransform:`matrix(-.16 0 0 -.33 .58 .72)`,"data-allow-mismatch":`attribute`},[s(`#EBEDF0`,0),s(`#F2F3F5`,100,.3)])]),k(`ellipse`,{fill:o(e),opacity:`.8`,cx:`80`,cy:`140`,rx:`46`,ry:`8`,"data-allow-mismatch":`attribute`},null)],u=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`a`),x1:`64%`,y1:`100%`,x2:`64%`,"data-allow-mismatch":`attribute`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M36 131V53H16v20H2v58h34z`,fill:o(`a`)},null),k(`path`,{d:`M123 15h22v14h9v77h-31V15z`,fill:o(`a`)},null)])],f=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`b`),x1:`64%`,y1:`97%`,x2:`64%`,y2:`0%`,"data-allow-mismatch":`attribute`},[s(`#F2F3F5`,0,.3),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M87 6c3 0 7 3 8 6a8 8 0 1 1-1 16H80a7 7 0 0 1-8-6c0-4 3-7 6-7 0-5 4-9 9-9Z`,fill:o(`b`)},null),k(`path`,{d:`M19 23c2 0 3 1 4 3 2 0 4 2 4 4a4 4 0 0 1-4 3v1h-7v-1l-1 1c-2 0-3-2-3-4 0-1 1-3 3-3 0-2 2-4 4-4Z`,fill:o(`b`)},null)])],p=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{id:a(1),x1:`64%`,y1:`100%`,x2:`64%`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)]),k(`linearGradient`,{id:a(2),x1:`50%`,x2:`50%`,y2:`84%`},[s(`#EBEDF0`,0),s(`#DCDEE0`,100,0)]),k(`linearGradient`,{id:a(3),x1:`100%`,x2:`100%`,y2:`100%`},[c(`#EAEDF0`,`#DCDEE0`)]),k(`radialGradient`,{id:a(4),cx:`50%`,cy:`0%`,fx:`50%`,fy:`0%`,r:`100%`,gradientTransform:`matrix(0 1 -.54 0 .5 -.5)`},[s(`#EBEDF0`,0),s(`#FFF`,100,0)])]),k(`g`,{fill:`none`},[u(),k(`path`,{fill:o(4),d:`M0 139h160v21H0z`,"data-allow-mismatch":`attribute`},null),k(`path`,{d:`M80 54a7 7 0 0 1 3 13v27l-2 2h-2a2 2 0 0 1-2-2V67a7 7 0 0 1 3-13z`,fill:o(2),"data-allow-mismatch":`attribute`},null),k(`g`,{opacity:`.6`,"stroke-linecap":`round`,"stroke-width":`7`,"data-allow-mismatch":`children`},[k(`path`,{d:`M64 47a19 19 0 0 0-5 13c0 5 2 10 5 13`,stroke:o(3)},null),k(`path`,{d:`M53 36a34 34 0 0 0 0 48`,stroke:o(3)},null),k(`path`,{d:`M95 73a19 19 0 0 0 6-13c0-5-2-9-6-13`,stroke:o(3)},null),k(`path`,{d:`M106 84a34 34 0 0 0 0-48`,stroke:o(3)},null)]),k(`g`,{transform:`translate(31 105)`},[k(`rect`,{fill:`#EBEDF0`,width:`98`,height:`34`,rx:`2`},null),k(`rect`,{fill:`#FFF`,x:`9`,y:`8`,width:`80`,height:`18`,rx:`1.1`},null),k(`rect`,{fill:`#EBEDF0`,x:`15`,y:`12`,width:`18`,height:`6`,rx:`1.1`},null)])])]),m=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(5)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`95%`,y1:`48%`,x2:`5.5%`,y2:`51%`,id:a(6)},[c(`#EAEDF1`,`#DCDEE0`)]),k(`linearGradient`,{y1:`45%`,x2:`100%`,y2:`54%`,id:a(7)},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),k(`g`,{transform:`translate(36 50)`,fill:`none`},[k(`g`,{transform:`translate(8)`},[k(`rect`,{fill:`#EBEDF0`,opacity:`.6`,x:`38`,y:`13`,width:`36`,height:`53`,rx:`2`},null),k(`rect`,{fill:o(5),width:`64`,height:`66`,rx:`2`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#FFF`,x:`6`,y:`6`,width:`52`,height:`55`,rx:`1`},null),k(`g`,{transform:`translate(15 17)`,fill:o(6),"data-allow-mismatch":`attribute`},[k(`rect`,{width:`34`,height:`6`,rx:`1`},null),k(`path`,{d:`M0 14h34v6H0z`},null),k(`rect`,{y:`28`,width:`34`,height:`6`,rx:`1`},null)])]),k(`rect`,{fill:o(7),y:`61`,width:`88`,height:`28`,rx:`1`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#F7F8FA`,x:`29`,y:`72`,width:`30`,height:`6`,rx:`1`},null)])]),h=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,null,[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(8),"data-allow-mismatch":`attribute`},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),l(`c`),k(`path`,{d:`m59 60 21 21 21-21h3l9 9v3L92 93l21 21v3l-9 9h-3l-21-21-21 21h-3l-9-9v-3l21-21-21-21v-3l9-9h3Z`,fill:o(8),"data-allow-mismatch":`attribute`},null)]),g=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,y1:`100%`,x2:`50%`,id:a(9)},[c(`#EEE`,`#D8D8D8`)]),k(`linearGradient`,{x1:`100%`,y1:`50%`,y2:`50%`,id:a(10)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(11)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(12)},[c(`#FFF`,`#F7F8FA`)])]),u(),f(),l(`d`),k(`g`,{transform:`rotate(-45 113 -4)`,fill:`none`,"data-allow-mismatch":`children`},[k(`rect`,{fill:o(9),x:`24`,y:`52.8`,width:`5.8`,height:`19`,rx:`1`},null),k(`rect`,{fill:o(10),x:`22.1`,y:`67.3`,width:`9.9`,height:`28`,rx:`1`},null),k(`circle`,{stroke:o(11),"stroke-width":`8`,cx:`27`,cy:`27`,r:`27`},null),k(`circle`,{fill:o(12),cx:`27`,cy:`27`,r:`16`},null),k(`path`,{d:`M37 7c-8 0-15 5-16 12`,stroke:o(11),"stroke-width":`3`,opacity:`.5`,"stroke-linecap":`round`,transform:`rotate(45 29 13)`},null)])]),_=()=>{var n;if(t.image)return t.image();let r={error:h,search:g,network:p,default:m};return((n=r[e.image])==null?void 0:n.call(r))||k(`img`,{src:e.image},null)};return()=>k(`div`,{class:q()},[k(`div`,{class:q(`image`),style:me(e.imageSize)},[_()]),n(),r()])}}),J=I(Oe),[ke,Y,Ae]=P(`pull-refresh`),X=50,je=[`pulling`,`loosing`,`success`],Me={disabled:Boolean,modelValue:Boolean,headHeight:B(X),successText:String,pullingText:String,loosingText:String,loadingText:String,pullDistance:M,successDuration:B(500),animationDuration:B(300)},Ne=h({name:ke,props:Me,emits:[`change`,`refresh`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i,a=w(),o=w(),c=b(a),l=x({status:`normal`,distance:0,duration:0}),u=r(),d=()=>{if(e.headHeight!==X)return{height:`${e.headHeight}px`}},f=()=>l.status!==`loading`&&l.status!==`success`&&!e.disabled,p=t=>{let n=+(e.pullDistance||e.headHeight);return t>n&&(t=t{let i=+(e.pullDistance||e.headHeight);l.distance=n,r?l.status=`loading`:n===0?l.status=`normal`:n{let{status:t}=l;return t===`normal`?``:e[`${t}Text`]||Ae(t)},v=()=>{let{status:e,distance:t}=l;if(n[e])return n[e]({distance:t});let r=[];return je.includes(e)&&r.push(k(`div`,{class:Y(`text`)},[h()])),e===`loading`&&r.push(k(s,{class:Y(`loading`)},{default:h})),r},y=()=>{l.status=`success`,setTimeout(()=>{m(0)},+e.successDuration)},S=e=>{i=_(c.value)===0,i&&(l.duration=0,u.start(e))},C=e=>{f()&&S(e)},T=e=>{if(f()){i||S(e);let{deltaY:t}=u;u.move(e),i&&t.value>=0&&u.isVertical()&&(D(e),m(p(t.value)))}},E=()=>{i&&u.deltaY.value&&f()&&(l.duration=+e.animationDuration,l.status===`loosing`?(m(+e.headHeight,!0),t(`update:modelValue`,!0),g(()=>t(`refresh`))):m(0))};return j(()=>e.modelValue,t=>{l.duration=+e.animationDuration,t?m(+e.headHeight,!0):n.success||e.successText?y():m(0,!1)}),te(`touchmove`,T,{target:o}),()=>{var e;let t={transitionDuration:`${l.duration}ms`,transform:l.distance?`translate3d(0,${l.distance}px, 0)`:``};return k(`div`,{ref:a,class:Y()},[k(`div`,{ref:o,class:Y(`track`),style:t,onTouchstartPassive:C,onTouchend:E,onTouchcancel:E},[k(`div`,{class:Y(`head`),style:d()},[v()]),(e=n.default)==null?void 0:e.call(n)])])}}}),Pe=I(Ne),[Fe,Z,Q]=P(`search`),Ie=O({},u,{label:String,shape:N(`square`),leftIcon:N(`search`),clearable:re,actionText:String,background:String,showAction:Boolean}),Le=h({name:Fe,props:Ie,emits:[`blur`,`focus`,`clear`,`search`,`cancel`,`clickInput`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(e,{emit:t,slots:n,attrs:r}){let i=d(),a=w(),o=()=>{n.action||(t(`update:modelValue`,``),t(`cancel`))},s=n=>{n.keyCode===13&&(D(n),t(`search`,e.modelValue))},l=()=>e.id||`${i}-input`,p=()=>{if(n.label||e.label)return k(`label`,{class:Z(`label`),for:l(),"data-allow-mismatch":`attribute`},[n.label?n.label():e.label])},m=()=>{if(e.showAction){let t=e.actionText||Q(`cancel`);return k(`div`,{class:Z(`action`),role:`button`,tabindex:0,onClick:o},[n.action?n.action():t])}},h=()=>{var e;return(e=a.value)==null?void 0:e.blur()},g=()=>{var e;return(e=a.value)==null?void 0:e.focus()},_=e=>t(`blur`,e),y=e=>t(`focus`,e),b=e=>t(`clear`,e),x=e=>t(`clickInput`,e),S=e=>t(`clickLeftIcon`,e),C=e=>t(`clickRightIcon`,e),T=Object.keys(u),E=()=>{let i=O({},r,z(e,T),{id:l()});return k(f,v({ref:a,type:`search`,class:Z(`field`,{"with-message":i.errorMessage}),border:!1,labelAlign:`left`,onBlur:_,onFocus:y,onClear:b,onKeypress:s,onClickInput:x,onClickLeftIcon:S,onClickRightIcon:C,"onUpdate:modelValue":e=>t(`update:modelValue`,e)},i),z(n,[`left-icon`,`right-icon`]))};return c({focus:g,blur:h}),()=>{var t;return k(`div`,{class:Z({"show-action":e.showAction}),style:{background:e.background}},[(t=n.left)==null?void 0:t.call(n),k(`div`,{class:Z(`content`,e.shape)},[p(),E()]),m()])}}}),$=I(Le),Re={class:`switcher-body`},ze={key:0,class:`switcher-state`},Be={key:0,class:`current`},Ve=t(h({__name:`AccountSwitcher`,props:{show:{type:Boolean,default:!1},showModifiers:{}},emits:[`update:show`],setup(e){let t=m(e,`show`),n=w(``),r=w([]),i=w(!1),c=w(``),u=0,d,f=ce(()=>{var e;return(e=ue.me)==null?void 0:e.account});async function p(){let e=++u;i.value=!0,c.value=``;try{let{items:t}=await le.accounts(n.value.trim());e===u&&(r.value=t.filter(e=>{var t;return e.id!==((t=f.value)==null?void 0:t.id)}))}catch(t){e===u&&(c.value=t instanceof ae?t.message:`加载失败`)}finally{e===u&&(i.value=!1)}}j(t,e=>{e&&p()}),j(n,()=>{window.clearTimeout(d),d=window.setTimeout(p,300)});function h(e){ie(e),t.value=!1}function g(e){return e.device_count?e.online?e.listening?`监听中`:`已停止`:`离线`:`没有电脑`}function _(e){return e.online?e.listening?`tone-success`:`tone-warning`:`tone-muted`}return(e,u)=>(y(),F(V(a),{show:t.value,"onUpdate:show":u[2]||(u[2]=e=>t.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`switcher`},{default:A(()=>[L(`div`,Re,[u[3]||(u[3]=L(`h3`,null,`切换客服`,-1)),k(V($),{modelValue:n.value,"onUpdate:modelValue":u[0]||(u[0]=e=>n.value=e),shape:`round`,placeholder:`搜索客服姓名或账号`},null,8,[`modelValue`]),k(V(l),{inset:``},{default:A(()=>[k(V(o),{title:`我自己`,label:f.value?`${f.value.nickname||f.value.sn}`:``,icon:`user-o`,clickable:``,value:V(R)===void 0?`当前`:``,onClick:u[1]||(u[1]=e=>h(null))},null,8,[`label`,`value`])]),_:1}),i.value&&!r.value.length?(y(),H(`div`,ze,[k(V(s),{size:`20`})])):c.value?(y(),F(V(J),{key:1,image:`network`,description:c.value},null,8,[`description`])):r.value.length?(y(),F(V(l),{key:3,inset:``,class:`switcher-list`},{default:A(()=>[(y(!0),H(se,null,T(r.value,e=>(y(),F(V(o),{key:e.id,title:e.nickname||e.sn||`客服 #${e.id}`,label:e.sn,clickable:``,onClick:t=>h(e)},{value:A(()=>[L(`span`,{class:S(_(e))},ne(g(e)),3),V(R)===e.id?(y(),H(`span`,Be,`当前`)):fe(``,!0)]),_:2},1032,[`title`,`label`,`onClick`]))),128))]),_:1})):(y(),F(V(J),{key:2,image:`search`,description:`没有找到其他客服`}))])]),_:1},8,[`show`]))}}),[[`__scopeId`,`data-v-91cf283c`]]);export{Te as a,J as i,$ as n,Pe as r,Ve as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js new file mode 100644 index 000000000..441f01f3e --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,n as r,p as i,r as a,t as o}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n as s}from"./use-placeholder-D9ZemjXL.js";import{a as c,d as l,i as u,o as d,s as f,u as p}from"./format-2xch3ZbY.js";import{a as m,n as h,o as g,s as _,t as v}from"./cell-group-CEKvg8Wf.js";import{r as y,t as b}from"./field-Bs9V8Rb1.js";import{a as x,i as S,n as ee,r as C,t as w}from"./AccountSwitcher-DHX0QDeT.js";import{$ as te,A as ne,At as T,B as E,C as D,Ct as O,D as k,Dt as A,E as j,Et as M,F as N,Ft as P,G as re,Gt as ie,H as ae,Ht as F,I,It as oe,J as se,Jt as ce,Kt as L,Lt as le,M as ue,Mt as de,N as fe,O as pe,Ot as me,Pt as he,Q as ge,R as _e,S as ve,St as R,Tt as ye,U as be,Ut as z,V as xe,Vt as B,W as V,Wt as Se,X as H,Y as Ce,Yt as U,Z as W,_ as G,_t as K,bt as we,c as Te,et as Ee,ft as De,g as Oe,gt as q,ht as J,j as ke,jt as Ae,kt as je,l as Me,n as Ne,o as Pe,ot as Fe,p as Ie,q as Y,qt as X,s as Le,t as Re,ut as ze,v as Be,vt as Z,w as Ve,wt as He,xt as Q,y as Ue,yt as $,z as We}from"./index-C_mEe-tZ.js";var Ge=Array.isArray,Ke=e=>typeof e==`string`,qe=e=>typeof e==`object`&&!!e,Je=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ye=/\B([A-Z])/g,Xe=Je(e=>e.replace(Ye,`-$1`).toLowerCase());function Ze(e){if(Ge(e)){let t={};for(let n=0;ne.startsWith(`/*`)?``:e).split(Qe).forEach(e=>{if(e){let n=e.split($e);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function nt(e){if(!e)return``;if(Ke(e))return e;let t=``;for(let n in e){let r=e[n];if(Ke(r)||typeof r==`number`){let e=n.startsWith(`--`)?n:Xe(n);t+=`${e}:${r};`}}return t}function rt(e){let t=``;if(Ke(e))t=e;else if(Ge(e))for(let n=0;nt||!o&&at?i=We(u):r&&(i=We(r))}return u(),l}function ot(){let e=L([]),t=[];return T(()=>{e.value=[]}),[e,n=>(t[n]||(t[n]=t=>{e.value[n]=t}),t[n])]}function st(e,t){if(!te||!window.IntersectionObserver)return;let n=new IntersectionObserver(e=>{t(e[0].intersectionRatio>0)},{root:document.body}),r=()=>{e.value&&n.observe(e.value)},i=()=>{e.value&&n.unobserve(e.value)};Ae(i),je(i),_e(r)}var[ct,lt]=G(`sticky`),ut={zIndex:H,position:Ce(`top`),container:Object,offsetTop:Y(0),offsetBottom:Y(0)},dt=O({name:ct,props:ut,emits:[`scroll`,`change`],setup(e,{emit:t,slots:n}){let r=L(),i=re(r),a=ie({fixed:!1,width:0,height:0,transform:0}),o=L(!1),s=J(()=>D(e.position===`top`?e.offsetTop:e.offsetBottom)),c=J(()=>{if(o.value)return;let{fixed:e,height:t,width:n}=a;if(e)return{width:`${n}px`,height:`${t}px`}}),l=J(()=>{if(!a.fixed||o.value)return;let t=ge(ve(e.zIndex),{width:`${a.width}px`,height:`${a.height}px`,[e.position]:`${s.value}px`});return a.transform&&(t.transform=`translate3d(0, ${a.transform}px, 0)`),t}),u=e=>t(`scroll`,{scrollTop:e,isFixed:a.fixed}),d=()=>{if(!r.value||k(r))return;let{container:t,position:n}=e,i=V(r),o=j(window);if(a.width=i.width,a.height=i.height,n===`top`){if(t){let e=V(t),n=e.bottom-s.value-a.height;a.fixed=s.value>i.top&&e.bottom>0,a.transform=n<0?n:0}else a.fixed=s.value>i.top}else{let{clientHeight:e}=document.documentElement;if(t){let n=V(t),r=e-n.top-s.value-a.height;a.fixed=e-s.valuen.top,a.transform=r<0?-r:0}else a.fixed=e-s.valuea.fixed,e=>t(`change`,e)),xe(`scroll`,d,{target:i,passive:!0}),st(r,d),B([fe,ue],()=>{r.value&&!k(r)&&a.fixed&&(o.value=!0,A(()=>{let e=V(r);a.width=e.width,a.height=e.height,o.value=!1}))}),()=>{var e;return R(`div`,{ref:r,style:c.value},[R(`div`,{class:lt({fixed:a.fixed&&!o.value}),style:l.value},[(e=n.default)==null?void 0:e.call(n)])])}}}),ft=Oe(dt),[pt,mt]=G(`swipe`),ht={loop:W,width:H,height:H,vertical:Boolean,autoplay:Y(0),duration:Y(500),touchable:W,lazyRender:Boolean,initialSwipe:Y(0),indicatorColor:String,showIndicators:W,stopPropagation:W},gt=Symbol(pt),_t=O({name:pt,props:ht,emits:[`change`,`dragStart`,`dragEnd`],setup(e,{emit:n,slots:r}){let i=L(),a=L(),o=ie({rect:null,width:0,height:0,offset:0,active:0,swiping:!1}),s=!1,c=l(),{children:u,linkChildren:d}=E(gt),f=J(()=>u.length),p=J(()=>o[e.vertical?`height`:`width`]),m=J(()=>e.vertical?c.deltaY.value:c.deltaX.value),h=J(()=>o.rect?(e.vertical?o.rect.height:o.rect.width)-p.value*f.value:0),g=J(()=>p.value?Math.ceil(Math.abs(h.value)/p.value):f.value),v=J(()=>f.value*p.value),y=J(()=>(o.active+f.value)%f.value),b=J(()=>{let t=e.vertical?`vertical`:`horizontal`;return c.direction.value===t}),x=J(()=>{let t={transitionDuration:`${o.swiping?0:e.duration}ms`,transform:`translate${e.vertical?`Y`:`X`}(${+o.offset.toFixed(2)}px)`};if(p.value){let n=e.vertical?`height`:`width`,r=e.vertical?`width`:`height`;t[n]=`${v.value}px`,t[r]=e[r]?`${e[r]}px`:``}return t}),S=t=>{let{active:n}=o;return t?e.loop?Ue(n+t,-1,f.value):Ue(n+t,0,g.value):n},ee=(t,n=0)=>{let r=t*p.value;e.loop||(r=Math.min(r,-h.value));let i=n-r;return e.loop||(i=Ue(i,h.value,0)),i},C=({pace:t=0,offset:r=0,emitChange:i})=>{if(f.value<=1)return;let{active:a}=o,s=S(t),c=ee(s,r);if(e.loop){if(u[0]&&c!==h.value){let e=c0;u[f.value-1].setOffset(e?-v.value:0)}}o.active=s,o.offset=c,i&&s!==a&&n(`change`,y.value)},w=()=>{o.swiping=!0,o.active<=-1?C({pace:f.value}):o.active>=f.value&&C({pace:-f.value})},te=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:-1,emitChange:!0})})},ne=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:1,emitChange:!0})})},T,D=()=>clearTimeout(T),O=()=>{D(),+e.autoplay>0&&f.value>1&&(T=setTimeout(()=>{ne(),O()},+e.autoplay))},j=(t=+e.initialSwipe)=>{if(!i.value)return;let n=()=>{var n,r;if(!k(i)){let t={width:i.value.offsetWidth,height:i.value.offsetHeight};o.rect=t,o.width=+((n=e.width)==null?t.width:n),o.height=+((r=e.height)==null?t.height:r)}f.value&&(t=Math.min(f.value-1,t),t===-1&&(t=f.value-1)),o.active=t,o.swiping=!0,o.offset=ee(t),u.forEach(e=>{e.setOffset(0)}),O()};k(i)?A().then(n):n()},M=()=>j(o.active),N,P=t=>{!e.touchable||t.touches.length>1||(c.start(t),s=!1,N=Date.now(),D(),w())},re=t=>{e.touchable&&o.swiping&&(c.move(t),b.value&&(!e.loop&&(o.active===0&&m.value>0||o.active===f.value-1&&m.value<0)||(pe(t,e.stopPropagation),C({offset:m.value}),s||(n(`dragStart`,{index:y.value}),s=!0))))},F=()=>{if(!e.touchable||!o.swiping)return;let t=Date.now()-N,r=m.value/t;if((Math.abs(r)>.25||Math.abs(m.value)>p.value/2)&&b.value){let t=e.vertical?c.offsetY.value:c.offsetX.value,n=0;n=e.loop?t>0?m.value>0?-1:1:0:-Math[m.value>0?`ceil`:`floor`](m.value/p.value),C({pace:n,emitChange:!0})}else m.value&&C({pace:0});s=!1,o.swiping=!1,n(`dragEnd`,{index:y.value}),O()},oe=(t,n={})=>{w(),c.reset(),I(()=>{let r;r=e.loop&&t===f.value?o.active===0?0:t:t%f.value,n.immediate?I(()=>{o.swiping=!1}):o.swiping=!1,C({pace:r-o.active,emitChange:!0})})},se=(t,n)=>{let r=n===y.value,i=r?{backgroundColor:e.indicatorColor}:void 0;return R(`i`,{style:i,class:mt(`indicator`,{active:r})},null)},ce=()=>{if(r.indicator)return r.indicator({active:y.value,total:f.value});if(e.showIndicators&&f.value>1)return R(`div`,{class:mt(`indicators`,{vertical:e.vertical})},[Array(f.value).fill(``).map(se)])};return _({prev:te,next:ne,state:o,resize:M,swipeTo:oe}),d({size:p,props:e,count:f,activeIndicator:y}),B(()=>e.initialSwipe,e=>j(+e)),B(f,()=>j(o.active)),B(()=>e.autoplay,O),B([fe,ue,()=>e.width,()=>e.height],M),B(ae(),e=>{e===`visible`?O():D()}),de(j),me(()=>j(o.active)),t(()=>j(o.active)),Ae(D),je(D),xe(`touchmove`,re,{target:a}),()=>{var t;return R(`div`,{ref:i,class:mt()},[R(`div`,{ref:a,style:x.value,class:mt(`track`,{vertical:e.vertical}),onTouchstartPassive:P,onTouchend:F,onTouchcancel:F},[(t=r.default)==null?void 0:t.call(r)]),ce()])}}}),vt=Oe(_t),[yt,bt]=G(`tabs`),xt=O({name:yt,props:{count:se(Number),inited:Boolean,animated:Boolean,duration:se(H),swipeable:Boolean,lazyRender:Boolean,currentIndex:se(Number)},emits:[`change`],setup(e,{emit:t,slots:n}){let r=L(),i=e=>t(`change`,e),a=()=>{var t;let a=(t=n.default)==null?void 0:t.call(n);return e.animated||e.swipeable?R(vt,{ref:r,loop:!1,class:bt(`track`),duration:e.duration*1e3,touchable:e.swipeable,lazyRender:e.lazyRender,showIndicators:!1,onChange:i},{default:()=>[a]}):a},o=t=>{let n=r.value;n&&n.state.active!==t&&n.swipeTo(t,{immediate:!e.inited})};return B(()=>e.currentIndex,o),de(()=>{o(e.currentIndex)}),_({swipeRef:r}),()=>R(`div`,{class:bt(`content`,{animated:e.animated||e.swipeable})},[a()])}}),[St,Ct]=G(`tabs`),wt={type:Ce(`line`),color:String,border:Boolean,sticky:Boolean,shrink:Boolean,active:Y(0),duration:Y(.3),animated:Boolean,ellipsis:W,swipeable:Boolean,scrollspy:Boolean,offsetTop:Y(0),background:String,lazyRender:W,showHeader:W,lineWidth:H,lineHeight:H,beforeChange:Function,swipeThreshold:Y(5),titleActiveColor:String,titleInactiveColor:String},Tt=Symbol(St),Et=O({name:St,props:wt,emits:[`change`,`scroll`,`rendered`,`clickTab`,`update:active`],setup(e,{emit:r,slots:a}){let o,c,l,u,d,f=L(),p=L(),m=L(),h=L(),g=y(),v=re(f),[b,x]=ot(),{children:S,linkChildren:ee}=E(Tt),C=ie({inited:!1,position:``,lineStyle:{},currentIndex:-1}),w=J(()=>S.length>+e.swipeThreshold||!e.ellipsis||e.shrink),te=J(()=>({borderColor:e.color,background:e.background})),T=(e,t)=>{var n;return(n=e.name)==null?t:n},O=J(()=>{let e=S[C.currentIndex];if(e)return T(e,C.currentIndex)}),j=J(()=>D(e.offsetTop)),M=J(()=>e.sticky?j.value+o:0),N=t=>{let n=p.value,r=b.value;if(!w.value||!n||!r||!r[C.currentIndex])return;let i=r[C.currentIndex].$el,a=i.offsetLeft-(n.offsetWidth-i.offsetWidth)/2;u&&u(),u=it(n,a,t?0:+e.duration)},P=()=>{let t=C.inited;A(()=>{let n=b.value;if(!n||!n[C.currentIndex]||e.type!==`line`||k(f.value))return;let r=n[C.currentIndex].$el,{lineWidth:i,lineHeight:a}=e,o=r.offsetLeft+r.offsetWidth/2,s={width:Be(i),backgroundColor:e.color,transform:`translateX(${o}px) translateX(-50%)`};if(t&&(s.transitionDuration=`${e.duration}s`),Ee(a)){let e=Be(a);s.height=e,s.borderRadius=e}C.lineStyle=s})},ae=e=>{let t=e=0&&e{let i=ae(t);if(!Ee(i))return;let a=S[i],o=T(a,i),s=C.currentIndex!==null;C.currentIndex!==i&&(C.currentIndex=i,n||N(),P()),o!==e.active&&(r(`update:active`,o),s&&r(`change`,o,a.title)),l&&!e.scrollspy&&ne(Math.ceil(Ve(f.value)-j.value))},I=(e,t)=>{let n=S.findIndex((t,n)=>T(t,n)===e);F(n===-1?0:n,t)},oe=(t=!1)=>{if(e.scrollspy){let n=S[C.currentIndex].$el;if(n&&v.value){let r=Ve(n,v.value)-M.value;c=!0,d&&d(),d=at(v.value,r,t?0:+e.duration,()=>{c=!1})}}},se=(t,i,a)=>{let{title:o,disabled:c}=S[i],l=T(S[i],i);c||(s(e.beforeChange,{args:[l],done:()=>{F(i),oe()}}),n(t)),r(`clickTab`,{name:l,title:o,event:a,disabled:c})},ce=e=>{l=e.isFixed,r(`scroll`,e)},le=e=>{A(()=>{I(e),oe(!0)})},ue=()=>{for(let e=0;eM.value)return e===0?0:e-1}return S.length-1},de=()=>{if(e.scrollspy&&!c){let e=ue();F(e)}},pe=()=>{if(e.type===`line`&&S.length)return R(`div`,{class:Ct(`line`),style:C.lineStyle},null)},he=()=>{var t,n,r;let{type:o,border:s,sticky:c}=e,l=[R(`div`,{ref:c?void 0:m,class:[Ct(`wrap`),{[i]:o===`line`&&s}]},[R(`div`,{ref:p,role:`tablist`,class:Ct(`nav`,[o,{shrink:e.shrink,complete:w.value}]),style:te.value,"aria-orientation":`horizontal`},[(t=a[`nav-left`])==null?void 0:t.call(a),S.map(e=>e.renderTitle(se)),pe(),(n=a[`nav-right`])==null?void 0:n.call(a)])]),(r=a[`nav-bottom`])==null?void 0:r.call(a)];return c?R(`div`,{ref:m},[l]):l},ge=()=>{P(),A(()=>{var e,t;N(!0),(t=(e=h.value)==null?void 0:e.swipeRef.value)==null||t.resize()})};return B(()=>[e.color,e.duration,e.lineWidth,e.lineHeight],P),B(fe,ge),B(()=>e.active,e=>{e!==O.value&&I(e)}),B(()=>S.length,()=>{C.inited&&(I(e.active),P(),A(()=>{N(!0)}))}),_({resize:ge,scrollTo:le}),me(P),t(P),_e(()=>{I(e.active,!0),A(()=>{C.inited=!0,m.value&&(o=V(m.value).height),N(!0)})}),st(f,P),xe(`scroll`,de,{target:v,passive:!0}),ee({id:g,props:e,setLine:P,scrollable:w,onRendered:(e,t)=>r(`rendered`,e,t),currentName:O,setTitleRefs:x,scrollIntoView:N}),()=>R(`div`,{ref:f,class:Ct([e.type])},[e.showHeader?e.sticky?R(ft,{container:f.value,offsetTop:j.value,onScroll:ce},{default:()=>[he()]}):he():null,R(xt,{ref:h,count:S.length,inited:C.inited,animated:e.animated,duration:e.duration,swipeable:e.swipeable,lazyRender:e.lazyRender,currentIndex:C.currentIndex,onChange:F},{default:()=>{var e;return[(e=a.default)==null?void 0:e.call(a)]}})])}}),Dt=Symbol(),Ot=Symbol(),kt=()=>ye(Ot,null),At=e=>{let t=kt();oe(Dt,e),oe(Ot,J(()=>(t==null||t.value)&&e.value))},[jt,Mt]=G(`tab`),Nt=O({name:jt,props:{id:String,dot:Boolean,type:String,color:String,title:String,badge:H,shrink:Boolean,isActive:Boolean,disabled:Boolean,controls:String,scrollable:Boolean,activeColor:String,inactiveColor:String,showZeroBadge:W},setup(e,{slots:t}){let n=J(()=>{let t={},{type:n,color:r,disabled:i,isActive:a,activeColor:o,inactiveColor:s}=e;r&&n===`card`&&(t.borderColor=r,i||(a?t.backgroundColor=r:t.color=r));let c=a?o:s;return c&&(t.color=c),t}),r=()=>{let n=R(`span`,{class:Mt(`text`,{ellipsis:!e.scrollable})},[t.title?t.title():e.title]);return e.dot||Ee(e.badge)&&e.badge!==``?R(a,{dot:e.dot,content:e.badge,showZero:e.showZeroBadge},{default:()=>[n]}):n};return()=>R(`div`,{id:e.id,role:`tab`,class:[Mt([e.type,{grow:e.scrollable&&!e.shrink,shrink:e.shrink,active:e.isActive,disabled:e.disabled}])],style:n.value,tabindex:e.disabled?void 0:e.isActive?0:-1,"aria-selected":e.isActive,"aria-disabled":e.disabled||void 0,"aria-controls":e.controls,"data-allow-mismatch":`attribute`},[r()])}}),[Pt,Ft]=G(`swipe-item`),It=O({name:Pt,setup(e,{slots:t}){let n,r=ie({offset:0,inited:!1,mounted:!1}),{parent:i,index:a}=be(gt);if(!i)return;let o=J(()=>{let e={},{vertical:t}=i.props;return i.size.value&&(e[t?`height`:`width`]=`${i.size.value}px`),r.offset&&(e.transform=`translate${t?`Y`:`X`}(${r.offset}px)`),e}),s=J(()=>{let{loop:e,lazyRender:t}=i.props;if(!t||n)return!0;if(!r.mounted)return!1;let o=i.activeIndicator.value,s=i.count.value-1,c=o===0&&e?s:o-1,l=o===s&&e?0:o+1;return n=a.value===o||a.value===c||a.value===l,n});return de(()=>{A(()=>{r.mounted=!0})}),_({setOffset:e=>{r.offset=e}}),()=>{var e;return R(`div`,{class:Ft(),style:o.value},[s.value?(e=t.default)==null?void 0:e.call(t):null])}}}),Lt=Oe(It),[Rt,zt]=G(`tab`),Bt=ge({},e,{dot:Boolean,name:H,badge:H,title:String,disabled:Boolean,titleClass:null,titleStyle:[String,Object],showZeroBadge:W}),Vt=O({name:Rt,props:Bt,setup(e,{slots:t}){let n=y(),r=L(!1),i=He(),{parent:a,index:o}=be(Tt);if(!a)return;let s=()=>{var t;return(t=e.name)==null?o.value:t},c=()=>{r.value=!0,a.props.lazyRender&&A(()=>{a.onRendered(s(),e.title)})},l=J(()=>{let e=s()===a.currentName.value;return e&&!r.value&&c(),e}),u=L(``),d=L(``);F(()=>{let{titleClass:t,titleStyle:n}=e;u.value=t?rt(t):``,d.value=n&&typeof n!=`string`?nt(Ze(n)):n});let f=r=>R(Nt,M({key:n,id:`${a.id}-${o.value}`,ref:a.setTitleRefs(o.value),style:d.value,class:u.value,isActive:l.value,controls:n,scrollable:a.scrollable.value,activeColor:a.props.titleActiveColor,inactiveColor:a.props.titleInactiveColor,onClick:e=>r(i.proxy,o.value,e)},Fe(a.props,[`type`,`color`,`shrink`]),Fe(e,[`dot`,`badge`,`title`,`disabled`,`showZeroBadge`])),{title:t.title}),p=L(!l.value);return B(l,e=>{e?p.value=!1:I(()=>{p.value=!0})}),B(()=>e.title,()=>{a.setLine(),a.scrollIntoView()}),At(l),_({id:n,renderTitle:f}),()=>{var e;let i=`${a.id}-${o.value}`,{animated:s,swipeable:c,scrollspy:u,lazyRender:d}=a.props;if(!t.default&&!s)return;let f=u||l.value;if(s||c)return R(Lt,{id:n,role:`tabpanel`,class:zt(`panel-wrapper`,{inactive:p.value}),tabindex:l.value?0:-1,"aria-hidden":!l.value,"aria-labelledby":i,"data-allow-mismatch":`attribute`},{default:()=>{var e;return[R(`div`,{class:zt(`panel`)},[(e=t.default)==null?void 0:e.call(t)])]}});let m=r.value||u||!d?(e=t.default)==null?void 0:e.call(t):null;return Se(R(`div`,{id:n,role:`tabpanel`,class:zt(`panel`),tabindex:f?0:-1,"aria-labelledby":i,"data-allow-mismatch":`attribute`},[m]),[[ze,f]])}}}),Ht=Oe(Vt),Ut=Oe(Et),[Wt,Gt,Kt]=G(`list`),qt={error:Boolean,offset:Y(300),loading:Boolean,disabled:Boolean,finished:Boolean,scroller:Object,errorText:String,direction:Ce(`down`),loadingText:{type:String,default:``},finishedText:String,immediateCheck:W},Jt=O({name:Wt,props:qt,emits:[`load`,`update:error`,`update:loading`],setup(e,{emit:t,slots:n}){let r=L(e.loading),i=L(),a=L(),o=kt(),s=re(i),c=J(()=>e.scroller||s.value),l=()=>{A(()=>{if(r.value||e.finished||e.disabled||e.error||(o==null?void 0:o.value)===!1)return;let{direction:n}=e,s=+e.offset,l=V(c);if(!l.height||k(i))return;let u=!1,d=V(a);u=n===`up`?l.top-d.top<=s:d.bottom-l.bottom<=s,u&&(r.value=!0,t(`update:loading`,!0),t(`load`))})},u=()=>{if(e.finished){let t=n.finished?n.finished():e.finishedText;if(t)return R(`div`,{class:Gt(`finished-text`)},[t])}},d=()=>{t(`update:error`,!1),l()},f=()=>{if(e.error){let t=n.error?n.error():e.errorText;if(t)return R(`div`,{role:`button`,class:Gt(`error-text`),tabindex:0,onClick:d},[t])}},p=()=>{if(r.value&&!e.finished&&!e.disabled)return R(`div`,{class:Gt(`loading`)},[n.loading?n.loading():e.loadingText!=null&&R(g,{class:Gt(`loading-icon`)},{default:()=>[e.loadingText||Kt(`loading`)]})])};return B(()=>[e.loading,e.finished,e.error],l),o&&B(o,e=>{e&&l()}),he(()=>{r.value=e.loading}),de(()=>{e.immediateCheck&&l()}),_({check:l}),xe(`scroll`,l,{target:c,passive:!0}),()=>{var t;let o=(t=n.default)==null?void 0:t.call(n),s=R(`div`,{ref:a,class:Gt(`placeholder`)},null);return R(`div`,{ref:i,role:`feed`,class:Gt(),"aria-busy":r.value},[e.direction===`down`?o:s,p(),u(),f(),e.direction===`up`?o:s])}}}),Yt=Oe(Jt),Xt={class:`detail`},Zt={class:`detail-head`},Qt={class:`avatar`,"aria-hidden":`true`},$t={class:`detail-title`},en={class:`app-muted`},tn={key:0,class:`badge`},nn={class:`app-mono`},rn={key:0,class:`state state--blocked`},an={key:1,class:`note`},on={key:1,class:`state`},sn={key:1,class:`note`},cn=o(O({__name:`ConversationDetail`,props:{conversation:{},accountId:{},canAdd:{type:Boolean},canRemove:{type:Boolean},addCode:{},removeCode:{}},emits:[`updated`],setup(e,{emit:t}){let n=e,i=t,a=L(``),o=L(!1);B(()=>`${n.conversation.account}|${n.conversation.conversation}`,()=>{a.value=``});let s=J(()=>n.conversation.blacklist),l=J(()=>{var e;let t=(e=s.value)==null?void 0:e.source;return t===`mobile`?`手机控制台`:t===`desktop`?`客服电脑`:t||`—`});async function d(){if(!o.value){try{await f({title:`加入黑名单?`,message:`加入后,系统不再向「${n.conversation.name}」自动发送任何消息,只能由客服手动处理。`,confirmButtonText:`加入黑名单`,confirmButtonColor:`var(--app-danger)`})}catch{return}o.value=!0;try{let{entry:e}=await Me.block(n.conversation,a.value.trim(),n.accountId);i(`updated`,{...n.conversation,blocked:!0,blacklist:e}),x({message:`已加入黑名单,电脑同步后立即生效`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}async function p(){if(!o.value){try{await f({title:`移出黑名单?`,message:`移出后,系统会恢复向「${n.conversation.name}」自动回复。`,confirmButtonText:`移出`})}catch{return}o.value=!0;try{await Me.unblock(n.conversation.account,n.conversation.conversation,n.accountId),i(`updated`,{...n.conversation,blocked:!1,blacklist:null}),x({message:`已移出黑名单`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}return(t,n)=>(P(),$(`section`,Xt,[q(`header`,Zt,[q(`span`,Qt,U(X(u)(e.conversation.name)),1),q(`div`,$t,[q(`h2`,null,U(e.conversation.name),1),q(`p`,en,U(e.conversation.type_label),1)]),e.conversation.blocked?(P(),$(`span`,tn,[R(X(r),{name:`shield-o`}),n[1]||(n[1]=Q(` 黑名单`,-1))])):Z(``,!0)]),R(X(v),{inset:``,class:`group`},{default:z(()=>[R(X(h),{title:`企业微信账号`,value:e.conversation.account_name||`—`,label:e.conversation.account},null,8,[`value`,`label`]),R(X(h),{title:`最近消息`,value:e.conversation.last_message_at?X(c)(e.conversation.last_message_at):`—`},null,8,[`value`]),R(X(h),{title:`会话标识`},{label:z(()=>[q(`span`,nn,U(e.conversation.conversation),1)]),_:1})]),_:1}),e.conversation.blocked&&s.value?(P(),$(`div`,rn,[q(`h3`,null,[R(X(r),{name:`shield-o`}),n[2]||(n[2]=Q(` 已加入黑名单`,-1))]),n[8]||(n[8]=q(`p`,null,`系统不会向这个会话自动发送消息;电脑离线时,下次联网自动同步。`,-1)),q(`dl`,null,[q(`div`,null,[n[3]||(n[3]=q(`dt`,null,`原因`,-1)),q(`dd`,null,U(s.value.reason||`未填写`),1)]),q(`div`,null,[n[4]||(n[4]=q(`dt`,null,`操作人`,-1)),q(`dd`,null,U(s.value.created_by||`—`),1)]),q(`div`,null,[n[5]||(n[5]=q(`dt`,null,`时间`,-1)),q(`dd`,null,U(X(c)(s.value.created_at)),1)]),q(`div`,null,[n[6]||(n[6]=q(`dt`,null,`来源`,-1)),q(`dd`,null,U(l.value),1)])]),e.canRemove?(P(),K(X(m),{key:0,block:``,round:``,plain:``,type:`primary`,icon:`revoke`,loading:o.value,onClick:p},{default:z(()=>[...n[7]||(n[7]=[Q(` 移出黑名单 `,-1)])]),_:1},8,[`loading`])):(P(),$(`p`,an,`移出黑名单需要主管权限(`+U(e.removeCode)+`),请联系主管处理。`,1))])):(P(),$(`div`,on,[q(`h3`,null,[R(X(r),{name:`info-o`}),n[9]||(n[9]=Q(` 正常接收自动回复`,-1))]),n[11]||(n[11]=q(`p`,null,`客户投诉、需要人工跟进或不适合机器人回复的会话,可以加入黑名单。`,-1)),e.canAdd?(P(),$(De,{key:0},[R(X(v),{inset:``,class:`reason`},{default:z(()=>[R(X(b),{modelValue:a.value,"onUpdate:modelValue":n[0]||(n[0]=e=>a.value=e),label:`原因`,placeholder:`选填,例如:客户要求人工服务`,maxlength:`200`,"show-word-limit":``,rows:`2`,autosize:``,type:`textarea`},null,8,[`modelValue`])]),_:1}),R(X(m),{block:``,round:``,type:`danger`,icon:`shield-o`,loading:o.value,onClick:d},{default:z(()=>[...n[10]||(n[10]=[Q(` 加入黑名单 `,-1)])]),_:1},8,[`loading`])],64)):(P(),$(`p`,sn,`加入黑名单需要 ZYT 权限(`+U(e.addCode)+`),请联系管理员开通。`,1))]))]))}}),[[`__scopeId`,`data-v-70465908`]]),ln={class:`body`},un={class:`line`},dn={class:`name`},fn={class:`time`},pn={class:`line sub`},mn={class:`meta`},hn={key:0,class:`blocked`},gn=o(O({__name:`ConversationRow`,props:{item:{},active:{type:Boolean}},emits:[`select`],setup(e){let t=e,n=J(()=>t.item.type===`group`?`group`:t.item.type===`direct_wechat`?`wechat`:`wecom`);return(t,r)=>(P(),$(`button`,{type:`button`,class:ce([`row`,{active:e.active}]),onClick:r[0]||(r[0]=n=>t.$emit(`select`,e.item))},[q(`span`,{class:ce([`avatar`,`avatar--${n.value}`]),"aria-hidden":`true`},U(X(u)(e.item.name)),3),q(`span`,ln,[q(`span`,un,[q(`strong`,dn,U(e.item.name),1),q(`span`,fn,U(e.item.last_message_at?X(c)(e.item.last_message_at):``),1)]),q(`span`,pn,[q(`span`,mn,[Q(U(e.item.type_label),1),e.item.account_name?(P(),$(De,{key:0},[Q(` · `+U(e.item.account_name),1)],64)):Z(``,!0)]),e.item.blocked?(P(),$(`span`,hn,`已拉黑`)):Z(``,!0)])])],2))}}),[[`__scopeId`,`data-v-645c253b`]]),_n={class:`page`},vn={class:`list-pane`},yn={class:`toolbar`},bn={key:0,class:`detail-pane app-card`},xn=o(O({name:`ConversationsView`,__name:`ConversationsView`,setup(e){let t=Ie(),n=L(``),i=L(`all`),a=L([]),o=L(``),s=L(!1),c=L(!1),l=L(!1),u=L(``),f=L(!1),h=L(0),g=L(null),_=L(!1),v=L(!1),y=0,b=null,x,te=J(()=>{var e;return(e=Ne.me)==null?void 0:e.permission_codes}),ne=J(()=>Pe.value===void 0?`会话`:`${Le.value} 的会话`),T=e=>`${e.account}|${e.conversation}`;function E(){y+=1,b==null||b.abort(),b=null,a.value=[],o.value=``,s.value=!1,l.value=!1,u.value=``,c.value=!1,D()}async function D(){if(s.value)return;let e=y;b=new AbortController,c.value=!0;try{let r=await Me.conversations({accountId:Pe.value,q:n.value.trim(),scope:i.value,cursor:o.value,limit:30,signal:b.signal});if(e!==y)return;let c=new Set(a.value.map(T));a.value=a.value.concat(r.items.filter(e=>!c.has(T(e)))),o.value=r.next_cursor,s.value=!r.has_more,h.value=r.blocked_total,l.value=!1,t.value&&!g.value&&a.value.length&&(g.value=a.value[0])}catch(t){if(e!==y||t instanceof DOMException&&t.name===`AbortError`)return;l.value=!0,u.value=t instanceof Te?t.message:`加载失败`}finally{e===y&&(c.value=!1,f.value=!1)}}function O(){f.value=!0,E()}function k(e){g.value=e,t.value||(_.value=!0)}function A(e){var t;let n=a.value.findIndex(t=>T(t)===T(e)),r=n>=0?a.value[n].blocked:(t=g.value)==null?void 0:t.blocked;n>=0&&(i.value===`blocked`&&!e.blocked?a.value.splice(n,1):a.value.splice(n,1,e)),r!==e.blocked&&(h.value+=e.blocked?1:-1),g.value=e}return B(n,()=>{window.clearTimeout(x),x=window.setTimeout(E,350)}),B(i,E),B(Pe,()=>{g.value=null,_.value=!1,E()}),E(),(e,o)=>{var y,b,x,E,j,M,N,re;return P(),$(`div`,_n,[R(X(d),{title:ne.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},we({_:2},[(y=X(Re))!=null&&y.control_all?{name:`right`,fn:z(()=>[q(`button`,{type:`button`,class:`nav-action`,onClick:o[0]||(o[0]=e=>v.value=!0)},[R(X(r),{name:`friends-o`}),o[9]||(o[9]=Q(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),q(`div`,{class:ce(X(t)?`split`:`single`)},[q(`section`,vn,[q(`div`,yn,[R(X(ee),{modelValue:n.value,"onUpdate:modelValue":o[1]||(o[1]=e=>n.value=e),shape:`round`,placeholder:`搜索会话名称`,clearable:!0},null,8,[`modelValue`]),R(X(Ut),{active:i.value,"onUpdate:active":o[2]||(o[2]=e=>i.value=e),shrink:``,border:!1,class:`tabs`},{default:z(()=>[R(X(Ht),{name:`all`,title:`全部会话`}),R(X(Ht),{name:`blocked`,title:h.value?`黑名单 ${h.value}`:`黑名单`},null,8,[`title`])]),_:1},8,[`active`])]),R(X(C),{modelValue:f.value,"onUpdate:modelValue":o[6]||(o[6]=e=>f.value=e),class:`list-body`,onRefresh:O},{default:z(()=>[R(X(Yt),{loading:c.value,"onUpdate:loading":o[3]||(o[3]=e=>c.value=e),error:l.value,"onUpdate:error":o[4]||(o[4]=e=>l.value=e),finished:s.value,"error-text":`${u.value},点击重试`,"finished-text":a.value.length?`没有更多了`:``,"immediate-check":!1,onLoad:D},{default:z(()=>[(P(!0),$(De,null,le(a.value,e=>(P(),K(gn,{key:T(e),item:e,active:X(t)&&g.value!==null&&T(g.value)===T(e),onSelect:k},null,8,[`item`,`active`]))),128))]),_:1},8,[`loading`,`error`,`finished`,`error-text`,`finished-text`]),s.value&&!a.value.length&&!l.value?(P(),K(X(S),{key:0,image:n.value?`search`:`default`,description:n.value?`没有找到匹配的会话`:i.value===`blocked`?`黑名单是空的`:`还没有归档的会话:电脑开启聊天备份后会自动出现`},{default:z(()=>[n.value?(P(),K(X(m),{key:0,round:``,size:`small`,onClick:o[5]||(o[5]=e=>n.value=``)},{default:z(()=>[...o[10]||(o[10]=[Q(`清除搜索`,-1)])]),_:1})):Z(``,!0)]),_:1},8,[`image`,`description`])):Z(``,!0)]),_:1},8,[`modelValue`])]),X(t)?(P(),$(`section`,bn,[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((b=X(Re))!=null&&b.blacklist_add),"can-remove":!!((x=X(Re))!=null&&x.blacklist_remove),"add-code":(E=(j=te.value)==null?void 0:j.blacklist_add)==null?``:E,"remove-code":(M=(N=te.value)==null?void 0:N.blacklist_remove)==null?``:M,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):(P(),K(X(S),{key:1,image:`default`,description:`选择左侧的会话查看详情`}))])):Z(``,!0)],2),X(t)?Z(``,!0):(P(),K(X(p),{key:0,show:_.value,"onUpdate:show":o[7]||(o[7]=e=>_.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`sheet`},{default:z(()=>{var e,t,n,r,i,a;return[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((e=X(Re))!=null&&e.blacklist_add),"can-remove":!!((t=X(Re))!=null&&t.blacklist_remove),"add-code":(n=(r=te.value)==null?void 0:r.blacklist_add)==null?``:n,"remove-code":(i=(a=te.value)==null?void 0:a.blacklist_remove)==null?``:i,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):Z(``,!0)]}),_:1},8,[`show`])),(re=X(Re))!=null&&re.control_all?(P(),K(w,{key:1,show:v.value,"onUpdate:show":o[8]||(o[8]=e=>v.value=e)},null,8,[`show`])):Z(``,!0)])}}}),[[`__scopeId`,`data-v-cc7f80e6`]]);export{xn as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css new file mode 100644 index 000000000..464684d56 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css @@ -0,0 +1 @@ +.detail[data-v-70465908]{padding-bottom:16px}.detail-head[data-v-70465908]{align-items:center;gap:12px;padding:16px;display:flex}.avatar[data-v-70465908]{width:52px;height:52px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:14px;flex:none;place-items:center;font-size:22px;font-weight:600;display:grid}.detail-title[data-v-70465908]{flex:1;min-width:0}.detail-title h2[data-v-70465908]{text-overflow:ellipsis;white-space:nowrap;margin:0 0 4px;font-size:18px;overflow:hidden}.detail-title p[data-v-70465908]{margin:0;font-size:13px}.badge[data-v-70465908]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.group[data-v-70465908]{margin-bottom:12px}.state[data-v-70465908]{background:var(--app-surface);border:1px solid var(--app-border);border-radius:14px;margin:0 16px;padding:16px}.state--blocked[data-v-70465908]{border-color:var(--app-danger-soft);background:linear-gradient(0deg, var(--app-danger-soft), var(--app-danger-soft)), var(--app-surface)}.state h3[data-v-70465908]{align-items:center;gap:6px;margin:0 0 6px;font-size:15px;display:flex}.state--blocked h3[data-v-70465908]{color:var(--app-danger)}.state>p[data-v-70465908]{color:var(--app-text-2);margin:0 0 12px;font-size:13px;line-height:1.6}.state dl[data-v-70465908]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin:0 0 14px;display:grid}.state dt[data-v-70465908]{color:var(--app-muted);font-size:12px}.state dd[data-v-70465908]{word-break:break-all;margin:2px 0 0;font-size:14px}.reason[data-v-70465908]{margin:0 0 12px}.reason[data-v-70465908] .van-cell-group--inset{margin:0}.state[data-v-70465908] .van-cell-group--inset{margin:0 0 12px}.note[data-v-70465908]{color:var(--app-muted);margin:0;font-size:13px;line-height:1.6}.row[data-v-645c253b]{border:0;border-bottom:1px solid var(--app-border);background:var(--app-surface);width:100%;min-height:64px;color:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;padding:10px 16px;display:flex}.row[data-v-645c253b]:active,.row.active[data-v-645c253b]{background:var(--app-primary-soft)}.avatar[data-v-645c253b]{border-radius:12px;flex:none;place-items:center;width:42px;height:42px;font-size:17px;font-weight:600;display:grid}.avatar--wecom[data-v-645c253b]{color:#2f6fff;background:#2f6fff1f}.avatar--wechat[data-v-645c253b]{color:#0f9e69;background:#13b77a24}.avatar--group[data-v-645c253b]{color:#7a5cff;background:#8a6bff24}.body[data-v-645c253b]{flex:1;gap:4px;min-width:0;display:grid}.line[data-v-645c253b]{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.name[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.time[data-v-645c253b]{color:var(--app-muted);flex:none;font-size:12px}.sub[data-v-645c253b]{color:var(--app-muted);font-size:12px}.meta[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.blocked[data-v-645c253b]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;padding:1px 8px;font-weight:600}.page[data-v-cc7f80e6]{min-height:100%}.nav-action[data-v-cc7f80e6]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.toolbar[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top));z-index:2;background:var(--app-bg);position:-webkit-sticky;position:sticky}.toolbar[data-v-cc7f80e6] .van-search{background:0 0;padding-bottom:4px}.tabs[data-v-cc7f80e6]{--van-tabs-nav-background:transparent;padding:0 4px}.list-body[data-v-cc7f80e6]{min-height:60vh}.single .list-pane[data-v-cc7f80e6]{max-width:720px;margin:0 auto}.split[data-v-cc7f80e6]{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;max-width:1180px;margin:0 auto;padding:0 24px 24px;display:grid}.split .list-body[data-v-cc7f80e6]{border-radius:16px;overflow:clip}.detail-pane[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top) + 12px);min-height:320px;margin-top:12px;position:-webkit-sticky;position:sticky}.sheet[data-v-cc7f80e6]{max-height:88vh;padding-top:8px} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css new file mode 100644 index 000000000..617ba43f7 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css @@ -0,0 +1 @@ +.panel[data-v-bcd7f731]{padding:16px}.panel-head[data-v-bcd7f731]{align-items:flex-start;gap:12px;display:flex}.panel-icon[data-v-bcd7f731]{width:44px;height:44px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:12px;flex:none;place-items:center;font-size:22px;display:grid}.panel-title[data-v-bcd7f731]{flex:1;min-width:0}.panel-title h2[data-v-bcd7f731]{text-overflow:ellipsis;white-space:nowrap;margin:2px 0 4px;font-size:17px;font-weight:600;overflow:hidden}.panel-title p[data-v-bcd7f731]{margin:0;font-size:13px;line-height:1.45}.pill[data-v-bcd7f731]{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.pill .dot[data-v-bcd7f731]{background:currentColor;border-radius:50%;width:7px;height:7px}.pill--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.pill--success .dot[data-v-bcd7f731]{box-shadow:0 0 0 3px var(--app-success-soft);animation:2.2s ease-in-out infinite breathe-bcd7f731}.pill--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.pill--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.pill--muted[data-v-bcd7f731],.pill--primary[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}@keyframes breathe-bcd7f731{50%{opacity:.45}}.stats[data-v-bcd7f731]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0 0;display:grid}.stat[data-v-bcd7f731]{background:var(--app-surface-2);border:1px solid var(--app-border);border-radius:12px;padding:10px 12px}.stat dt[data-v-bcd7f731]{color:var(--app-muted);font-size:12px}.stat dd[data-v-bcd7f731]{margin:4px 0 0;font-size:15px;font-weight:600}.meta[data-v-bcd7f731]{margin:12px 0 0;font-size:12px}.progress[data-v-bcd7f731]{border-radius:12px;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45;display:flex}.progress--primary[data-v-bcd7f731]{color:var(--app-primary);background:var(--app-primary-soft)}.progress--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.progress--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.progress--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.progress--muted[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}.actions[data-v-bcd7f731]{margin-top:16px}.blocked[data-v-bcd7f731]{margin:10px 4px 0;font-size:13px;line-height:1.5}.page[data-v-8ead1f63]{min-height:100%}.page-body[data-v-8ead1f63]{min-height:calc(100vh - 96px);min-height:calc(100dvh - 96px)}.content[data-v-8ead1f63]{max-width:720px;margin:0 auto;padding:12px 16px 24px}.stack[data-v-8ead1f63]{gap:12px;display:grid}.skeleton-card[data-v-8ead1f63]{padding:16px 0}.empty-hint[data-v-8ead1f63]{max-width:280px;color:var(--app-muted);text-align:center;margin:0 auto;font-size:13px;line-height:1.6}.nav-action[data-v-8ead1f63]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.stale[data-v-8ead1f63]{margin:0 4px;font-size:12px}.split[data-v-8ead1f63]{grid-template-columns:minmax(240px,320px) minmax(0,1fr);align-items:start;gap:16px;max-width:1080px;margin:0 auto;padding:16px 24px 24px;display:grid}.split-list[data-v-8ead1f63]{gap:10px;display:grid}.device-row[data-v-8ead1f63]{text-align:left;width:100%;color:inherit;cursor:pointer;align-items:center;gap:12px;padding:12px 14px;display:flex}.device-row.active[data-v-8ead1f63]{border-color:var(--app-primary);box-shadow:0 0 0 2px var(--app-primary-soft)}.device-row-icon[data-v-8ead1f63]{color:var(--app-primary);font-size:20px}.device-row-text[data-v-8ead1f63]{gap:2px;min-width:0;display:grid}.device-row-text strong[data-v-8ead1f63]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;overflow:hidden}.device-row-text small[data-v-8ead1f63]{font-size:12px} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js new file mode 100644 index 000000000..ae83f281b --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n,o as r,r as i,s as a,t as o}from"./format-2xch3ZbY.js";import{a as s,o as c}from"./cell-group-CEKvg8Wf.js";import{a as l,i as u,r as d,t as f}from"./AccountSwitcher-DHX0QDeT.js";import{Ct as p,Et as m,Ft as h,Jt as g,Kt as _,Lt as v,St as y,Ut as b,Vt as x,X as S,Y as C,Yt as w,Z as T,_ as E,_t as D,bt as O,c as k,f as A,ft as j,g as M,gt as N,ht as P,kt as F,l as I,n as L,o as R,p as z,q as B,qt as V,s as ee,t as H,v as U,vt as W,x as te,xt as G,yt as K}from"./index-C_mEe-tZ.js";var[ne,re]=E(`skeleton-title`),ie=p({name:ne,props:{round:Boolean,titleWidth:S},setup(e){return()=>y(`h3`,{class:re([{round:e.round}]),style:{width:U(e.titleWidth)}},null)}}),q=M(ie),[J,Y]=E(`skeleton-avatar`),ae={avatarSize:S,avatarShape:C(`round`)},oe=p({name:J,props:ae,setup(e){return()=>y(`div`,{class:Y([e.avatarShape]),style:te(e.avatarSize)},null)}}),se=M(oe),X=`100%`,ce={round:Boolean,rowWidth:{type:S,default:X}},[le,ue]=E(`skeleton-paragraph`),de=p({name:le,props:ce,setup(e){return()=>y(`div`,{class:ue([{round:e.round}]),style:{width:e.rowWidth}},null)}}),fe=M(de),[pe,Z]=E(`skeleton`),me=`60%`,he={row:B(0),round:Boolean,title:Boolean,titleWidth:S,avatar:Boolean,avatarSize:S,avatarShape:C(`round`),loading:T,animate:T,rowWidth:{type:[Number,String,Array],default:X}},ge=p({name:pe,inheritAttrs:!1,props:he,setup(e,{slots:t,attrs:n}){let r=()=>{if(e.avatar)return y(se,{avatarShape:e.avatarShape,avatarSize:e.avatarSize},null)},i=()=>{if(e.title)return y(q,{round:e.round,titleWidth:e.titleWidth},null)},a=t=>{let{rowWidth:n}=e;return n===`100%`&&t===e.row-1?me:Array.isArray(n)?n[t]:n},o=()=>Array(+e.row).fill(``).map((t,n)=>y(fe,{key:n,round:e.round,rowWidth:U(a(n))},null)),s=()=>t.template?t.template():y(j,null,[r(),y(`div`,{class:Z(`content`)},[i(),o()])]);return()=>{var r;return e.loading?y(`div`,m({class:Z({animate:e.animate,round:e.round})},n),[s()]):(r=t.default)==null?void 0:r.call(t)}}}),_e=M(ge),ve={class:`panel-head`},ye={class:`panel-icon`,"aria-hidden":`true`},be={class:`panel-title`},xe={class:`app-muted`},Se={class:`stats`},Ce={class:`stat`},we={class:`stat`},Te={class:`stat`},Ee={class:`stat`},De={class:`meta app-muted`},Oe={key:0},ke={key:1},Ae={class:`actions`},je={key:0,class:`blocked app-muted`},Q=t(p({__name:`DevicePanel`,props:{device:{},accountId:{},canControl:{type:Boolean},controlCode:{}},emits:[`changed`],setup(t,{emit:r}){let u=t,d=r,f=_(!1),p=_(null),m,v=0,S=P(()=>n(u.device)),C=P(()=>u.device.status),T=P(()=>{var e;return!!((e=C.value)!=null&&e.listening)}),E=P(()=>T.value?`stop_listening`:`start_listening`),O=P(()=>p.value?o(p.value):null),A=P(()=>{var e;return f.value||!!((e=O.value)!=null&&e.busy)}),j=P(()=>u.device.online?u.device.remote_capable?u.canControl?``:`你的 ZYT 账号没有远程启停权限(${u.controlCode}),请联系管理员开通。`:`电脑上的客服软件版本过低,升级到最新版后才能远程控制。`:`电脑离线:客服软件没有运行,无法远程拉起。请到电脑上打开软件。`);function M(){m!==void 0&&window.clearTimeout(m),m=void 0}async function L(){let e=p.value;if(e&&!e.final){try{let{command:t}=await I.command(e.id);if(p.value=t,t.final){d(`changed`);let e=o(t).text;l({message:e,type:t.status===`succeeded`?`success`:`fail`});return}}catch(e){if(e instanceof k&&e.status===404)return}Date.now()u.device.last_command,e=>{if(!e)return;if(p.value&&p.value.id===e.id){!p.value.final&&e.final&&(p.value=e);return}let t=e.expires_at*1e3>Date.now();!e.final&&t?R(e):!p.value&&e.final&&t&&(p.value=e)},{immediate:!0}),F(M);async function z(){if(A.value||j.value)return;let e=E.value===`stop_listening`;try{await a({title:e?`停止自动回复?`:`启动自动回复?`,message:e?`「${u.device.device_name||`这台电脑`}」将停止监听,新消息不再自动回复。`:`「${u.device.device_name||`这台电脑`}」将开始监听,并按电脑上的设置自动回复新消息。`,confirmButtonText:e?`停止`:`启动`,confirmButtonColor:e?`var(--app-danger)`:`var(--app-primary)`})}catch{return}f.value=!0;try{let{command:e}=await I.sendCommand(u.device.device_id,E.value,u.accountId);R(e)}catch(e){l({message:e instanceof k?e.message:`指令发送失败,请重试`,type:`fail`}),d(`changed`)}finally{f.value=!1}}return(n,r)=>{var a,o,l,u,d;return h(),K(`article`,{class:g([`panel app-card`,`panel--${S.value.tone}`])},[N(`header`,ve,[N(`div`,ye,[y(V(e),{name:`desktop-o`})]),N(`div`,be,[N(`h2`,null,w(t.device.device_name||`未命名电脑`),1),N(`p`,xe,w(S.value.hint),1)]),N(`span`,{class:g([`pill`,`pill--${S.value.tone}`])},[r[0]||(r[0]=N(`span`,{class:`dot`,"aria-hidden":`true`},null,-1)),G(` `+w(S.value.label),1)],2)]),N(`dl`,Se,[N(`div`,Ce,[r[1]||(r[1]=N(`dt`,null,`自动回复`,-1)),N(`dd`,{class:g(T.value?`tone-success`:`tone-muted`)},w(C.value?T.value?`监听中`:`已停止`:`—`),3)]),N(`div`,we,[r[2]||(r[2]=N(`dt`,null,`企业微信`,-1)),N(`dd`,{class:g((a=C.value)!=null&&a.wecom_connected?`tone-success`:`tone-muted`)},w(C.value?C.value.wecom_connected?`已连接`:`未连接`:`—`),3)]),N(`div`,Te,[r[3]||(r[3]=N(`dt`,null,`回复通道`,-1)),N(`dd`,null,w(((o=C.value)==null?void 0:o.transport_label)||`—`),1)]),N(`div`,Ee,[r[4]||(r[4]=N(`dt`,null,`待处理`,-1)),N(`dd`,{class:g((((l=C.value)==null?void 0:l.pending_count)||0)>0?`tone-warning`:``)},w(C.value?`${C.value.pending_count} 条`:`—`),3)])]),N(`p`,De,[N(`span`,null,`版本 `+w(t.device.app_version||`未知`),1),t.device.online&&((u=C.value)==null?void 0:u.reported_seconds)!==null&&((d=C.value)==null?void 0:d.reported_seconds)!==void 0?(h(),K(`span`,Oe,` · 状态 `+w(V(i)(C.value.reported_seconds))+`前上报 `,1)):!t.device.online&&t.device.last_seen_seconds!==null?(h(),K(`span`,ke,` · `+w(V(i)(t.device.last_seen_seconds))+`前在线 `,1)):W(``,!0)]),O.value?(h(),K(`div`,{key:0,class:g([`progress`,`progress--${O.value.tone}`]),role:`status`,"aria-live":`polite`},[O.value.busy?(h(),D(V(c),{key:0,size:`16`})):(h(),D(V(e),{key:1,name:O.value.tone===`success`?`passed`:`info-o`},null,8,[`name`])),N(`span`,null,w(O.value.text),1)],2)):W(``,!0),N(`div`,Ae,[y(V(s),{block:``,round:``,type:T.value?`danger`:`primary`,plain:T.value,icon:T.value?`pause-circle-o`:`play-circle-o`,disabled:!!j.value,loading:A.value,"loading-text":f.value?`正在下发…`:`等待电脑执行…`,onClick:z},{default:b(()=>[G(w(T.value?`停止自动回复`:`启动自动回复`),1)]),_:1},8,[`type`,`plain`,`icon`,`disabled`,`loading`,`loading-text`]),j.value?(h(),K(`p`,je,w(j.value),1)):W(``,!0)])],2)}}}),[[`__scopeId`,`data-v-bcd7f731`]]),Me={class:`page`},Ne={key:0,class:`content`},Pe={class:`app-card skeleton-card`},Fe={key:1,class:`content`},Ie={key:2,class:`content`},Le={class:`empty-hint`},$={key:3,class:`split`},Re={class:`split-list`,"aria-label":`电脑列表`},ze=[`onClick`],Be={class:`device-row-text`},Ve={key:0,class:`stale app-muted`},He={class:`split-detail`},Ue={key:4,class:`content stack`},We={key:0,class:`stale app-muted`},Ge=t(p({name:`DevicesView`,__name:`DevicesView`,setup(t){let i=z(),a=_(null),o=_(!1),c=_(!1),l=_(``),p=_(``),m=_(!1),S=0,C=P(()=>{var e,t;return(e=(t=a.value)==null?void 0:t.items)==null?[]:e}),T=P(()=>{var e,t;return(e=(t=C.value.find(e=>e.device_id===p.value))==null?C.value[0]:t)==null?null:e}),E=P(()=>{var e;return!!((e=H.value)!=null&&e.control)}),M=P(()=>{var e,t;return(e=(t=L.me)==null?void 0:t.permission_codes.control)==null?`wecom_rpa.remote/control`:e}),F=P(()=>R.value===void 0?`我的电脑`:`${ee.value} 的电脑`);async function B(){let e=++S;a.value||(o.value=!0);try{let t=await I.desktops(R.value);if(e!==S)return;a.value=t,l.value=``}catch(t){if(e!==S)return;l.value=t instanceof k?t.message:`加载失败,请重试`}finally{e===S&&(o.value=!1,c.value=!1)}}async function U(){c.value=!0,await B()}return x(R,()=>{a.value=null,p.value=``,B()},{immediate:!0}),A(B,5e3),(t,_)=>{var x,S;return h(),K(`div`,Me,[y(V(r),{title:F.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},O({_:2},[(x=V(H))!=null&&x.control_all?{name:`right`,fn:b(()=>[N(`button`,{type:`button`,class:`nav-action`,onClick:_[0]||(_[0]=e=>m.value=!0)},[y(V(e),{name:`friends-o`}),_[3]||(_[3]=G(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),y(V(d),{modelValue:c.value,"onUpdate:modelValue":_[1]||(_[1]=e=>c.value=e),class:`page-body`,onRefresh:U},{default:b(()=>[o.value&&!a.value?(h(),K(`div`,Ne,[N(`div`,Pe,[y(V(_e),{title:``,row:4})])])):l.value&&!a.value?(h(),K(`div`,Fe,[y(V(u),{image:`network`,description:l.value},{default:b(()=>[y(V(s),{round:``,type:`primary`,size:`small`,onClick:B},{default:b(()=>[..._[4]||(_[4]=[G(`重新加载`,-1)])]),_:1})]),_:1},8,[`description`])])):C.value.length?V(i)?(h(),K(`div`,$,[N(`aside`,Re,[(h(!0),K(j,null,v(C.value,t=>{var r;return h(),K(`button`,{key:t.device_id,type:`button`,class:g([`device-row app-card`,{active:((r=T.value)==null?void 0:r.device_id)===t.device_id}]),onClick:e=>p.value=t.device_id},[y(V(e),{name:`desktop-o`,class:`device-row-icon`}),N(`span`,Be,[N(`strong`,null,w(t.device_name||`未命名电脑`),1),N(`small`,{class:g(`tone-${V(n)(t).tone}`)},w(V(n)(t).label),3)])],10,ze)}),128)),l.value?(h(),K(`p`,Ve,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0)]),N(`section`,He,[T.value?(h(),D(Q,{key:T.value.device_id,device:T.value,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`])):W(``,!0)])])):(h(),K(`div`,Ue,[l.value?(h(),K(`p`,We,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0),(h(!0),K(j,null,v(C.value,e=>(h(),D(Q,{key:e.device_id,device:e,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`]))),128))])):(h(),K(`div`,Ie,[y(V(u),{image:`default`,description:`还没有登录过企微客服助手的电脑`},{default:b(()=>[N(`p`,Le,` 在电脑上打开企微客服助手,用`+w(V(R)===void 0?`同一个`:`该客服的`)+` ZYT 账号登录后,这里会显示电脑状态。 `,1)]),_:1})]))]),_:1},8,[`modelValue`]),(S=V(H))!=null&&S.control_all?(h(),D(f,{key:0,show:m.value,"onUpdate:show":_[2]||(_[2]=e=>m.value=e)},null,8,[`show`])):W(``,!0)])}}}),[[`__scopeId`,`data-v-8ead1f63`]]);export{Ge as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js new file mode 100644 index 000000000..63630d4bd --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js @@ -0,0 +1 @@ +import{c as e,m as t,n,t as r}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{a as i,s as a,t as o}from"./cell-group-CEKvg8Wf.js";import{t as s}from"./field-Bs9V8Rb1.js";import{B as c,Ct as l,Ft as u,Gt as d,I as f,Kt as p,O as m,R as h,St as g,Ut as _,V as v,Vt as y,W as b,Wt as x,X as S,Yt as C,Z as w,_ as T,_t as E,c as D,d as O,et as k,g as A,gt as j,ht as M,l as N,q as P,qt as F,r as I,u as L,ut as R,vt as z,xt as B,yt as V,z as H}from"./index-C_mEe-tZ.js";var[U,W]=T(`form`),G=l({name:U,props:{colon:Boolean,disabled:Boolean,readonly:Boolean,required:[Boolean,String],showError:Boolean,labelWidth:S,labelAlign:String,inputAlign:String,scrollToError:Boolean,scrollToErrorPosition:String,validateFirst:Boolean,submitOnEnter:w,showErrorMessage:w,errorMessageAlign:String,validateTrigger:{type:[String,Array],default:`onBlur`}},emits:[`submit`,`failed`],setup(e,{emit:n,slots:r}){let{children:i,linkChildren:o}=c(t),s=e=>e?i.filter(t=>e.includes(t.name)):i,l=e=>new Promise((t,n)=>{let r=[];s(e).reduce((e,t)=>e.then(()=>{if(!r.length)return t.validate().then(e=>{e&&r.push(e)})}),Promise.resolve()).then(()=>{r.length?n(r):t()})}),u=e=>new Promise((t,n)=>{let r=s(e);Promise.all(r.map(e=>e.validate())).then(e=>{e=e.filter(Boolean),e.length?n(e):t()})}),d=e=>{let t=i.find(t=>t.name===e);return t?new Promise((e,n)=>{t.validate().then(t=>{t?n(t):e()})}):Promise.reject()},f=t=>typeof t==`string`?d(t):e.validateFirst?l(t):u(t),p=e=>{typeof e==`string`&&(e=[e]),s(e).forEach(e=>{e.resetValidation()})},h=()=>i.reduce((e,t)=>(e[t.name]=t.getValidationStatus(),e),{}),_=(e,t)=>{i.some(n=>n.name===e&&(n.$el.scrollIntoView(t),!0))},v=()=>i.reduce((e,t)=>(t.name!==void 0&&(e[t.name]=t.formValue.value),e),{}),y=()=>{let t=v();f().then(()=>n(`submit`,t)).catch(r=>{n(`failed`,{values:t,errors:r});let{scrollToError:i,scrollToErrorPosition:a}=e;i&&r[0].name&&_(r[0].name,a?{block:a}:void 0)})},b=e=>{m(e),y()};return o({props:e}),a({submit:y,validate:f,getValues:v,scrollToField:_,resetValidation:p,getValidationStatus:h}),()=>{var e;return g(`form`,{class:W(),onSubmit:b},[(e=r.default)==null?void 0:e.call(r)])}}}),K=A(G),[q,J]=T(`notice-bar`),Y={text:String,mode:String,color:String,delay:P(1),speed:P(60),leftIcon:String,wrapable:Boolean,background:String,scrollable:{type:Boolean,default:null}},X=l({name:q,props:Y,emits:[`close`,`replay`],setup(t,{emit:r,slots:i}){let o=0,s=0,c,l=p(),u=p(),m=d({show:!0,offset:0,duration:0}),_=()=>{if(i[`left-icon`])return i[`left-icon`]();if(t.leftIcon)return g(n,{class:J(`left-icon`),name:t.leftIcon},null)},S=()=>{if(t.mode===`closeable`)return`cross`;if(t.mode===`link`)return`arrow`},C=e=>{t.mode===`closeable`&&(m.show=!1,r(`close`,e))},w=()=>{if(i[`right-icon`])return i[`right-icon`]();let e=S();if(e)return g(n,{name:e,class:J(`right-icon`),onClick:C},null)},T=()=>{m.offset=o,m.duration=0,H(()=>{f(()=>{m.offset=-s,m.duration=(s+o)/+t.speed,r(`replay`)})})},E=()=>{let e=t.scrollable===!1&&!t.wrapable,n={transform:m.offset?`translateX(${m.offset}px)`:``,transitionDuration:`${m.duration}s`};return g(`div`,{ref:l,role:`marquee`,class:J(`wrap`)},[g(`div`,{ref:u,style:n,class:[J(`content`),{"van-ellipsis":e}],onTransitionend:T},[i.default?i.default():t.text])])},D=()=>{let{delay:e,speed:n,scrollable:r}=t,i=k(e)?e*1e3:0;o=0,s=0,m.offset=0,m.duration=0,clearTimeout(c),c=setTimeout(()=>{if(!l.value||!u.value||r===!1)return;let e=b(l).width,t=b(u).width;(r||t>e)&&f(()=>{o=e,s=t,m.offset=-s,m.duration=s/+n})},i)};return e(D),h(D),v(`pageshow`,D),a({reset:D}),y(()=>[t.text,t.scrollable],D),()=>{let{color:e,wrapable:n,background:r}=t;return x(g(`div`,{role:`alert`,class:J({wrapable:n}),style:{color:e,background:r}},[_(),E(),w()]),[[R,m.show]])}}}),Z=A(X),Q=`/m/assets/brand-mark-CPUQS9Am.png`,$={class:`login`},ee={class:`login-card`},te={class:`login-head`},ne=[`src`],re={key:0,class:`login-error`,role:`alert`},ie={class:`login-actions`},ae=r(l({__name:`LoginView`,setup(e){let t=L(),n=O(),r=p(``),a=p(``),c=p(!1),l=p(``),d=M(()=>t.query.expired===`1`);function f(){let e=navigator.userAgent,t=/iPad/.test(e)?`iPad`:/iPhone/.test(e)?`iPhone`:/HarmonyOS/i.test(e)?`鸿蒙`:/Android/.test(e)?`Android`:/Windows/.test(e)?`Windows`:/Mac OS/.test(e)?`Mac`:`浏览器`,n=/wxwork/i.test(e)?`企业微信`:/MicroMessenger/i.test(e)?`微信`:``;return n?`${t} · ${n}`:t}function m(){let e=t.query.redirect;return typeof e==`string`&&e.startsWith(`/`)&&!e.startsWith(`//`)&&!e.startsWith(`/login`)?e:`/desktops`}async function h(){if(!c.value){l.value=``,c.value=!0;try{let e=await N.login(r.value.trim(),a.value,f());a.value=``,I(e.access_token,e.me),await n.replace(m())}catch(e){e instanceof D?(l.value=e.message,e.code===`invalid_credentials`&&(a.value=``)):l.value=`登录失败,请稍后重试`}finally{c.value=!1}}}return(e,t)=>(u(),V(`main`,$,[j(`section`,ee,[j(`header`,te,[j(`img`,{class:`login-mark`,src:F(Q),alt:`甄养堂`,width:`72`,height:`72`},null,8,ne),t[2]||(t[2]=j(`h1`,null,`企微客服助手`,-1)),t[3]||(t[3]=j(`p`,null,`手机控制台 · 使用 ZYT 账号登录`,-1))]),d.value?(u(),E(F(Z),{key:0,class:`login-notice`,"left-icon":`info-o`,scrollable:!1,wrapable:``,text:`登录已失效或已在管理端被下线,请重新登录。`})):z(``,!0),g(F(K),{class:`login-form`,onSubmit:h},{default:_(()=>[g(F(o),{inset:``},{default:_(()=>[g(F(s),{modelValue:r.value,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),name:`account`,label:`账号`,placeholder:`ZYT 登录账号`,autocomplete:`username`,autocapitalize:`off`,spellcheck:!1,enterkeyhint:`next`,clearable:``,rules:[{required:!0,message:`请输入账号`}]},null,8,[`modelValue`]),g(F(s),{modelValue:a.value,"onUpdate:modelValue":t[1]||(t[1]=e=>a.value=e),type:`password`,name:`password`,label:`密码`,placeholder:`ZYT 登录密码`,autocomplete:`current-password`,enterkeyhint:`go`,rules:[{required:!0,message:`请输入密码`}]},null,8,[`modelValue`])]),_:1}),l.value?(u(),V(`p`,re,C(l.value),1)):z(``,!0),j(`div`,ie,[g(F(i),{round:``,block:``,type:`primary`,"native-type":`submit`,loading:c.value,"loading-text":`正在登录…`},{default:_(()=>[...t[4]||(t[4]=[B(` 登录 `,-1)])]),_:1},8,[`loading`])])]),_:1}),t[5]||(t[5]=j(`p`,{class:`login-foot`},`密码只用于向 ZYT 验证身份,不会保存在手机上。`,-1))])]))}}),[[`__scopeId`,`data-v-004201f6`]]);export{ae as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css new file mode 100644 index 000000000..54f80a155 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css @@ -0,0 +1 @@ +.login[data-v-004201f6]{min-height:100vh;min-height:100dvh;padding:calc(env(safe-area-inset-top) + 24px) 16px calc(env(safe-area-inset-bottom) + 24px);background:radial-gradient(120% 60% at 50% 0%, var(--app-primary-soft), transparent 70%), var(--app-bg);justify-content:center;align-items:center;display:flex}.login-card[data-v-004201f6]{width:100%;max-width:420px}.login-head[data-v-004201f6]{text-align:center;margin-bottom:24px}.login-mark[data-v-004201f6]{box-shadow:var(--app-shadow);border-radius:20px}.login-head h1[data-v-004201f6]{letter-spacing:.5px;margin:16px 0 6px;font-size:24px;font-weight:700}.login-head p[data-v-004201f6]{color:var(--app-muted);margin:0;font-size:14px}.login-notice[data-v-004201f6]{border-radius:12px;margin:0 16px 12px}.login-form[data-v-004201f6] .van-cell-group--inset,.login-form[data-v-004201f6]{margin:0}.login-error[data-v-004201f6]{color:var(--app-danger);margin:12px 16px 0;font-size:14px;line-height:1.5}.login-actions[data-v-004201f6]{margin:20px 16px 0}.login-foot[data-v-004201f6]{text-align:center;color:var(--app-muted);margin:16px 16px 0;font-size:12px} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css new file mode 100644 index 000000000..5045f6a8e --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css @@ -0,0 +1 @@ +.content[data-v-1710faf3]{max-width:720px;margin:0 auto;padding:12px 0 32px}.profile[data-v-1710faf3]{align-items:center;gap:14px;margin:0 16px;padding:16px;display:flex}.avatar[data-v-1710faf3]{color:#fff;background:linear-gradient(135deg,#2f6fff,#5a65ff);border-radius:50%;flex:none;place-items:center;width:54px;height:54px;font-size:22px;font-weight:700;display:grid}.profile-text[data-v-1710faf3]{flex:1;min-width:0}.profile-text h2[data-v-1710faf3]{margin:0 0 4px;font-size:18px}.profile-text p[data-v-1710faf3]{margin:0;font-size:13px}.root[data-v-1710faf3]{color:var(--app-warning);background:var(--app-warning-soft);border-radius:999px;flex:none;padding:4px 10px;font-size:12px;font-weight:600}.section-title[data-v-1710faf3]{color:var(--app-muted);margin:24px 32px 8px;font-size:13px;font-weight:600}.hint[data-v-1710faf3]{margin:8px 32px 0;font-size:12px;line-height:1.6}.logout[data-v-1710faf3]{margin:32px 16px 0} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js new file mode 100644 index 000000000..ee886d01c --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{a as n,i as r,o as i,s as a}from"./format-2xch3ZbY.js";import{a as o,n as s,t as c}from"./cell-group-CEKvg8Wf.js";import{Ct as l,Ft as u,Jt as d,Kt as f,Lt as p,St as m,Ut as h,Yt as g,_t as _,d as v,ft as y,gt as b,ht as x,i as S,n as C,qt as w,vt as T,xt as E,yt as D}from"./index-C_mEe-tZ.js";var O={class:`page`},k={key:0,class:`content`},A={class:`profile app-card`},j={class:`avatar`,"aria-hidden":`true`},M={class:`profile-text`},N={class:`app-muted`},P={key:0,class:`root`},F={class:`hint app-muted`},I={class:`logout`},L=t(l({__name:`MeView`,setup(t){let l=v(),L=f(!1),R=x(()=>C.me),z=x(()=>{let e=R.value;if(!e)return[];let{permissions:t,permission_codes:n}=e;return[{title:`查看本人电脑与会话`,code:`登录即可`,granted:!0},{title:`远程启停本人电脑`,code:n.control,granted:t.control},{title:`查看并控制所有客服电脑`,code:n.control_all,granted:t.control_all},{title:`会话加入黑名单`,code:n.blacklist_add,granted:t.blacklist_add},{title:`会话移出黑名单`,code:n.blacklist_remove,granted:t.blacklist_remove}]}),B=x(()=>{var e;let t=(e=R.value)==null?void 0:e.session.expires_at;return t?new Date(t*1e3).toISOString():``}),V=/iPhone|iPad|iPod/.test(navigator.userAgent);async function H(){try{await a({title:`退出登录?`,message:`退出后需要重新输入 ZYT 账号密码。`,confirmButtonText:`退出`})}catch{return}L.value=!0,await S(),await l.replace({name:`login`})}return(t,a)=>(u(),D(`div`,O,[m(w(i),{title:`我的`,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1}),R.value?(u(),D(`div`,k,[b(`section`,A,[b(`span`,j,g(w(r)(R.value.account.nickname||R.value.account.sn)),1),b(`div`,M,[b(`h2`,null,g(R.value.account.nickname||R.value.account.sn),1),b(`p`,N,`ZYT 账号 `+g(R.value.account.sn||`—`),1)]),R.value.permissions.root?(u(),D(`span`,P,`超级管理员`)):T(``,!0)]),a[1]||(a[1]=b(`h3`,{class:`section-title`},`手机端权限`,-1)),m(w(c),{inset:``},{default:h(()=>[(u(!0),D(y,null,p(z.value,t=>(u(),_(w(s),{key:t.title,title:t.title,label:t.code,center:``},{value:h(()=>[b(`span`,{class:d(t.granted?`tone-success`:`tone-muted`)},[m(w(e),{name:t.granted?`passed`:`close`},null,8,[`name`]),E(` `+g(t.granted?`已开通`:`未开通`),1)],2)]),_:2},1032,[`title`,`label`]))),128))]),_:1}),a[2]||(a[2]=b(`p`,{class:`hint app-muted`},` 权限在 ZYT「权限管理 → 角色管理」里勾选「企微客服助手」下的项目,调整后 10 分钟内自动生效,或退出重新登录。 `,-1)),a[3]||(a[3]=b(`h3`,{class:`section-title`},`登录信息`,-1)),m(w(c),{inset:``},{default:h(()=>[m(w(s),{title:`登录时间`,value:w(n)(R.value.session.created_at)},null,8,[`value`]),m(w(s),{title:`最长有效至`,value:B.value?w(n)(B.value):`—`,label:`ZYT 登录失效后会提前退出`},null,8,[`value`])]),_:1}),a[4]||(a[4]=b(`h3`,{class:`section-title`},`添加到主屏幕`,-1)),b(`p`,F,[w(V)?(u(),D(y,{key:0},[E(`在 Safari 中点底部「分享」按钮,选择「添加到主屏幕」,之后像 App 一样打开。`)],64)):(u(),D(y,{key:1},[E(`在浏览器菜单中选择「添加到主屏幕」或「安装应用」,之后像 App 一样打开。`)],64))]),b(`div`,I,[m(w(o),{block:``,round:``,type:`danger`,plain:``,loading:L.value,onClick:H},{default:h(()=>[...a[0]||(a[0]=[E(`退出登录`,-1)])]),_:1},8,[`loading`])])])):T(``,!0)]))}}),[[`__scopeId`,`data-v-1710faf3`]]);export{L as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css new file mode 100644 index 000000000..a8ec81749 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css @@ -0,0 +1 @@ +.shell[data-v-5972669c]{min-height:100vh;min-height:100dvh}.shell-tabbar[data-v-5972669c]{border-top:1px solid var(--app-border)} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js new file mode 100644 index 000000000..2aaa7cd8d --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js @@ -0,0 +1 @@ +import{a as e,n as t,o as n,p as r,r as i,t as a}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n as o,t as s}from"./use-placeholder-D9ZemjXL.js";import{B as c,Ct as l,Et as u,Ft as d,Kt as f,Q as p,Rt as m,S as h,St as g,U as _,Ut as v,X as y,Z as b,_ as x,_t as S,g as C,ht as w,pt as T,q as E,qt as D,rt as O,wt as k,xt as A,yt as j,zt as M}from"./index-C_mEe-tZ.js";var[N,P]=x(`tabbar`),F={route:Boolean,fixed:b,border:b,zIndex:y,placeholder:Boolean,activeColor:String,beforeChange:Function,inactiveColor:String,modelValue:E(0),safeAreaInsetBottom:{type:Boolean,default:null}},I=Symbol(N),L=l({name:N,props:F,emits:[`change`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i=f(),{linkChildren:a}=c(I),l=s(i,P),u=()=>{var t;return(t=e.safeAreaInsetBottom)==null?e.fixed:t},d=()=>{var t;let{fixed:a,zIndex:o,border:s}=e;return g(`div`,{ref:i,role:`tablist`,style:h(o),class:[P({fixed:a}),{[r]:s,"van-safe-area-bottom":u()}]},[(t=n.default)==null?void 0:t.call(n)])};return a({props:e,setActive:(n,r)=>{o(e.beforeChange,{args:[n],done(){t(`update:modelValue`,n),t(`change`,n),r()}})}}),()=>e.fixed&&e.placeholder?l(d):d()}}),R=C(L),[z,B]=x(`tabbar-item`),V=p({},e,{dot:Boolean,icon:String,name:y,badge:y,badgeProps:Object,iconPrefix:String}),H=l({name:z,props:V,emits:[`click`],setup(e,{emit:r,slots:a}){let o=n(),s=k().proxy,{parent:c,index:l}=_(I);if(!c)return;let d=w(()=>{var t;let{route:n,modelValue:r}=c.props;if(n&&`$route`in s){let{$route:t}=s,{to:n}=e,r=O(n)?n:{path:n};return t.matched.some(e=>{let t=`path`in r&&r.path===e.path,n=`name`in r&&r.name===e.name;return t||n})}return((t=e.name)==null?l.value:t)===r}),f=t=>{var n;d.value||c.setActive((n=e.name)==null?l.value:n,o),r(`click`,t)},p=()=>{if(a.icon)return a.icon({active:d.value});if(e.icon)return g(t,{name:e.icon,classPrefix:e.iconPrefix},null)};return()=>{var t;let{dot:n,badge:r}=e,{activeColor:o,inactiveColor:s}=c.props,l=d.value?o:s;return g(`div`,{role:`tab`,class:B({active:d.value}),style:{color:l},tabindex:0,"aria-selected":d.value,onClick:f},[g(i,u({dot:n,class:B(`icon`),content:r},e.badgeProps),{default:p}),g(`div`,{class:B(`text`)},[(t=a.default)==null?void 0:t.call(a,{active:d.value})])])}}}),U=C(H),W={class:`shell`},G=a(l({__name:`ShellView`,setup(e){return(e,t)=>{let n=m(`RouterView`);return d(),j(`div`,W,[g(n,null,{default:v(({Component:e})=>[(d(),S(T,{include:[`DevicesView`,`ConversationsView`]},[(d(),S(M(e)))],1024))]),_:1}),g(D(R),{route:``,placeholder:``,"safe-area-inset-bottom":``,class:`shell-tabbar`},{default:v(()=>[g(D(U),{replace:``,to:{name:`desktops`},icon:`desktop-o`},{default:v(()=>[...t[0]||(t[0]=[A(`电脑`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`conversations`},icon:`chat-o`},{default:v(()=>[...t[1]||(t[1]=[A(`会话`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`me`},icon:`user-o`},{default:v(()=>[...t[2]||(t[2]=[A(`我的`,-1)])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-5972669c`]]);export{G as default}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js new file mode 100644 index 000000000..68f1479be --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js @@ -0,0 +1 @@ +import{Ct as e,Et as t,St as n,Tt as r,Vt as i,X as a,Y as o,Z as s,_ as c,et as l,g as u,ht as d,m as f,nt as p,v as m,wt as h}from"./index-C_mEe-tZ.js";var g=`van-hairline`,_=`${g}--top`,v=`${g}--left`;`${g}`;var y=`${g}--bottom`,b=`${g}--surround`,x=`${g}--top-bottom`;`${g}`;var S=`van-haptics-feedback`,C=Symbol(`van-form`),w=Symbol();function T(e){let t=r(w,null);t&&i(t,t=>{t&&e()})}var E={to:[String,Object],url:String,replace:Boolean};function D({to:e,url:t,replace:n,$router:r}){e&&r?r[n?`replace`:`push`](e):t&&(n?location.replace(t):location.href=t)}function O(){let e=h().proxy;return()=>D(e)}var[k,A]=c(`badge`),j={dot:Boolean,max:a,tag:o(`div`),color:String,offset:Array,content:a,showZero:s,position:o(`top-right`)},M=e({name:k,props:j,setup(e,{slots:t}){let r=()=>{if(t.content)return!0;let{content:n,showZero:r}=e;return l(n)&&n!==``&&(r||n!==0&&n!==`0`)},i=()=>{let{dot:n,max:i,content:a}=e;if(!n&&r())return t.content?t.content():l(i)&&p(a)&&+a>+i?`${i}+`:a},a=e=>e.startsWith(`-`)?e.replace(`-`,``):`-${e}`,o=d(()=>{let n={background:e.color};if(e.offset){let[r,i]=e.offset,{position:o}=e,[s,c]=o.split(`-`);t.default?(n[s]=typeof i==`number`?m(s===`top`?i:-i):s===`top`?m(i):a(i),n[c]=typeof r==`number`?m(c===`left`?r:-r):c===`left`?m(r):a(r)):(n.marginTop=m(i),n.marginLeft=m(r))}return n}),s=()=>{if(r()||e.dot)return n(`div`,{class:A([e.position,{dot:e.dot,fixed:!!t.default}]),style:o.value},[i()])};return()=>{if(t.default){let{tag:r}=e;return n(r,{class:A(`wrapper`)},{default:()=>[t.default(),s()]})}return s()}}}),N=u(M),[P,F]=c(`icon`),I=e=>e==null?void 0:e.includes(`/`),L={dot:Boolean,tag:o(`i`),name:String,size:a,badge:a,color:String,badgeProps:Object,classPrefix:String},R=e({name:P,props:L,setup(e,{slots:i}){let a=r(f,null),o=d(()=>e.classPrefix||(a==null?void 0:a.iconPrefix)||F());return()=>{let{tag:r,dot:a,name:s,size:c,badge:l,color:u}=e,d=I(s);return n(N,t({dot:a,tag:r,class:[o.value,d?``:`${o.value}-${s}`],style:{color:u,fontSize:m(c)},content:l},e.badgeProps),{default:()=>{var e;return[(e=i.default)==null?void 0:e.call(i),d&&n(`img`,{class:F(`image`),src:s},null)]}})}}}),z=u(R),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{E as a,T as c,b as d,_ as f,S as h,D as i,y as l,C as m,z as n,O as o,x as p,N as r,w as s,B as t,v as u}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png differ diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js new file mode 100644 index 000000000..6642657c6 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js @@ -0,0 +1 @@ +import{a as e,d as t,n,o as r,p as i}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{Ct as a,Et as o,O as s,Q as c,St as l,X as u,Y as d,Z as f,_ as p,et as m,ft as h,g,ht as _,v,wt as y,x as b}from"./index-C_mEe-tZ.js";function x(e){let t=y();t&&c(t.proxy,e)}var[S,C]=p(`loading`),w=Array(12).fill(null).map((e,t)=>l(`i`,{class:C(`line`,String(t+1))},null)),T=l(`svg`,{class:C(`circular`),viewBox:`25 25 50 50`},[l(`circle`,{cx:`50`,cy:`50`,r:`20`,fill:`none`},null)]),E={size:u,type:d(`circular`),color:String,vertical:Boolean,textSize:u,textColor:String},D=a({name:S,props:E,setup(e,{slots:t}){let n=_(()=>c({color:e.color},b(e.size))),r=()=>{let r=e.type===`spinner`?w:T;return l(`span`,{class:C(`spinner`,e.type),style:n.value},[t.icon?t.icon():r])},i=()=>{var n;if(t.default)return l(`span`,{class:C(`text`),style:{fontSize:v(e.textSize),color:(n=e.textColor)==null?e.color:n}},[t.default()])};return()=>{let{type:t,vertical:n}=e;return l(`div`,{class:C([t,{vertical:n}]),"aria-live":`polite`,"aria-busy":!0},[r(),i()])}}}),O=g(D),[k,A]=p(`button`),j=c({},e,{tag:d(`button`),text:String,icon:String,type:d(`default`),size:d(`normal`),color:String,block:Boolean,plain:Boolean,round:Boolean,square:Boolean,loading:Boolean,hairline:Boolean,disabled:Boolean,iconPrefix:String,nativeType:d(`button`),loadingSize:u,loadingText:String,loadingType:String,iconPosition:d(`left`)}),M=a({name:k,props:j,emits:[`click`],setup(e,{emit:i,slots:a}){let o=r(),c=()=>a.loading?a.loading():l(O,{size:e.loadingSize,type:e.loadingType,class:A(`loading`)},null),u=()=>{if(e.loading)return c();if(a.icon)return l(`div`,{class:A(`icon`)},[a.icon()]);if(e.icon)return l(n,{name:e.icon,class:A(`icon`),classPrefix:e.iconPrefix},null)},d=()=>{let t;if(t=e.loading?e.loadingText:a.default?a.default():e.text,t)return l(`span`,{class:A(`text`)},[t])},f=()=>{let{color:t,plain:n}=e;if(t){let e={color:n?t:`white`};return n||(e.background=t),t.includes(`gradient`)?e.border=0:e.borderColor=t,e}},p=t=>{e.loading?s(t):e.disabled||(i(`click`,t),o())};return()=>{let{tag:n,type:r,size:i,block:a,round:o,plain:s,square:c,loading:m,disabled:h,hairline:g,nativeType:_,iconPosition:v}=e,y=[A([r,i,{plain:s,block:a,round:o,square:c,loading:m,disabled:h,hairline:g}]),{[t]:g}];return l(n,{type:_,class:y,style:f(),disabled:h,onClick:p},{default:()=>[l(`div`,{class:A(`content`)},[v===`left`&&u(),d(),v===`right`&&u()])]})}}}),N=g(M),P=()=>{var e;let{scopeId:t}=((e=y())==null?void 0:e.vnode)||{};return t?{[t]:``}:null},[F,I]=p(`cell`),L={tag:d(`div`),icon:String,size:String,title:u,value:u,label:u,center:Boolean,isLink:Boolean,border:f,iconPrefix:String,valueClass:null,labelClass:null,titleClass:null,titleStyle:null,arrowDirection:String,required:{type:[Boolean,String],default:null},clickable:{type:Boolean,default:null}},R=c({},L,e),z=a({name:F,props:R,setup(e,{slots:t}){let i=r(),a=()=>{if(t.label||m(e.label))return l(`div`,{class:[I(`label`),e.labelClass]},[t.label?t.label():e.label])},o=()=>{var n;if(t.title||m(e.title)){let r=(n=t.title)==null?void 0:n.call(t);return Array.isArray(r)&&r.length===0?void 0:l(`div`,{class:[I(`title`),e.titleClass],style:e.titleStyle},[r||l(`span`,null,[e.title]),a()])}},s=()=>{let n=t.value||t.default;if(n||m(e.value))return l(`div`,{class:[I(`value`),e.valueClass]},[n?n():l(`span`,null,[e.value])])},c=()=>{if(t.icon)return t.icon();if(e.icon)return l(n,{name:e.icon,class:I(`left-icon`),classPrefix:e.iconPrefix},null)},u=()=>{if(t[`right-icon`])return t[`right-icon`]();if(e.isLink){let t=e.arrowDirection&&e.arrowDirection!==`right`?`arrow-${e.arrowDirection}`:`arrow`;return l(n,{name:t,class:I(`right-icon`)},null)}};return()=>{var n,r,a;let{tag:d,size:f,center:p,border:m,isLink:h,required:g}=e,_=!!((a=(r=(n=e.clickable)==null?e.to:n)==null?e.url:r)==null?h:a),v={center:p,required:!!g,clickable:_,borderless:!m};return f&&(v[f]=!!f),l(d,{class:I(v),role:_?`button`:void 0,tabindex:_?0:void 0,onClick:_?i:void 0},{default:()=>{var e;return[c(),o(),s(),u(),(e=t.extra)==null?void 0:e.call(t)]}})}}}),B=g(z),[V,H]=p(`cell-group`),U=a({name:V,inheritAttrs:!1,props:{title:String,inset:Boolean,border:f},setup(e,{slots:t,attrs:n}){let r=()=>{var r;return l(`div`,o({class:[H({inset:e.inset}),{[i]:e.border&&!e.inset}]},n,P()),[(r=t.default)==null?void 0:r.call(t)])},a=()=>l(`div`,{class:H(`title`,{inset:e.inset})},[t.title?t.title():e.title]);return()=>e.title||t.title?l(h,null,[a(),r()]):r()}}),W=g(U);export{N as a,P as i,B as n,O as o,L as r,x as s,W as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js new file mode 100644 index 000000000..2e2483612 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js @@ -0,0 +1 @@ +import{m as e,n as t}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n,r,s as i}from"./cell-group-CEKvg8Wf.js";import{A as a,Ct as o,Dt as s,Et as c,Gt as ee,It as l,Kt as u,Mt as d,O as f,P as te,Q as p,St as m,T as h,U as g,V as _,Vt as v,X as y,Y as b,_ as x,b as S,et as C,g as w,ht as T,it as E,k as ne,q as D,rt as O,st as k,tt as A,v as j,wt as M,xt as re,y as ie}from"./index-C_mEe-tZ.js";var N=0;function P(){let e=M(),{name:t=`unknown`}=(e==null?void 0:e.type)||{};return`${t}-${++N}`}function F(e){return Array.isArray(e)?!e.length:e!==0&&!e}function ae(e,t){if(F(e)){if(t.required)return!1;if(t.validateEmpty===!1)return!0}return!(t.pattern&&!t.pattern.test(String(e)))}function oe(e,t){return new Promise(n=>{let r=t.validator(e,t);if(E(r)){r.then(n);return}n(r)})}function I(e,t){let{message:n}=t;return A(n)?n(e,t):n||``}function se({target:e}){e.composing=!0}function L({target:e}){e.composing&&(e.composing=!1,e.dispatchEvent(new Event(`input`)))}function ce(e,t){let n=h();e.style.height=`auto`;let r=e.scrollHeight;if(O(t)){let{maxHeight:e,minHeight:n}=t;e!==void 0&&(r=Math.min(r,e)),n!==void 0&&(r=Math.max(r,n))}r&&(e.style.height=`${r}px`,a(n))}function le(e,t){return e===`number`&&(e=`text`,t!=null||(t=`decimal`)),e===`digit`&&(e=`tel`,t!=null||(t=`numeric`)),{type:e,inputmode:t}}function R(e){return[...e].length}function z(e,t){return[...e].slice(0,t).join(``)}var[B,V]=x(`field`),H={id:String,name:String,leftIcon:String,rightIcon:String,autofocus:Boolean,clearable:Boolean,maxlength:y,max:Number,min:Number,formatter:Function,clearIcon:b(`clear`),modelValue:D(``),inputAlign:String,placeholder:String,autocomplete:String,autocapitalize:String,autocorrect:String,errorMessage:String,enterkeyhint:String,clearTrigger:b(`focus`),formatTrigger:b(`onChange`),spellcheck:{type:Boolean,default:null},error:{type:Boolean,default:null},disabled:{type:Boolean,default:null},readonly:{type:Boolean,default:null},inputmode:String},U=p({},r,H,{rows:y,type:b(`text`),rules:Array,autosize:[Boolean,Object],labelWidth:y,labelClass:null,labelAlign:String,showWordLimit:Boolean,errorMessageAlign:String,colon:{type:Boolean,default:null}}),W=o({name:B,props:U,emits:[`blur`,`focus`,`clear`,`keypress`,`clickInput`,`endValidate`,`startValidate`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(r,{emit:a,slots:o}){let p=P(),h=ee({status:`unvalidated`,focused:!1,validateMessage:``}),y=u(),b=u(),x=u(),{parent:w}=g(e),E=()=>{var e;return String((e=r.modelValue)==null?``:e)},D=e=>{if(C(r[e]))return r[e];if(w&&C(w.props[e]))return w.props[e]},O=T(()=>{let e=D(`readonly`);if(r.clearable&&!e){let e=E()!==``,t=r.clearTrigger===`always`||r.clearTrigger===`focus`&&h.focused;return e&&t}return!1}),A=T(()=>x.value&&o.input?x.value():r.modelValue),M=T(()=>{var e;let t=D(`required`);return t===`auto`?(e=r.rules)==null?void 0:e.some(e=>e.required):t}),N=e=>e.reduce((e,t)=>e.then(()=>{if(h.status===`failed`)return;let{value:e}=A;if(t.formatter&&(e=t.formatter(e,t)),!ae(e,t)){h.status=`failed`,h.validateMessage=I(e,t);return}if(t.validator)return F(e)&&t.validateEmpty===!1?void 0:oe(e,t).then(n=>{n&&typeof n==`string`?(h.status=`failed`,h.validateMessage=n):n===!1&&(h.status=`failed`,h.validateMessage=I(e,t))})}),Promise.resolve()),B=()=>{h.status=`unvalidated`,h.validateMessage=``},H=()=>a(`endValidate`,{status:h.status,message:h.validateMessage}),U=(e=r.rules)=>new Promise(t=>{B(),e?(a(`startValidate`),N(e).then(()=>{h.status===`failed`?(t({name:r.name,message:h.validateMessage}),H()):(h.status=`passed`,t(),H())})):t()}),W=e=>{if(w&&r.rules){let{validateTrigger:t}=w.props,n=k(t).includes(e),i=r.rules.filter(t=>t.trigger?k(t.trigger).includes(e):n);i.length&&U(i)}},G=e=>{var t;let{maxlength:n}=r;if(C(n)&&R(e)>+n){let r=E();if(r&&R(r)===+n)return r;let i=(t=y.value)==null?void 0:t.selectionEnd;if(h.focused&&i){let t=[...e],r=t.length-+n;return i=R(e.slice(0,i)),t.splice(i-r,r),t.join(``)}return z(e,+n)}return e},K=(e,t=`onChange`)=>{var n,i;let o=e;e=G(e);let s=o.length-e.length;if(r.type===`number`||r.type===`digit`){let a=r.type===`number`;if(e=S(e,a,a),t===`onBlur`&&e!==``&&(r.min!==void 0||r.max!==void 0)){let t=ie(+e,(n=r.min)==null?-1/0:n,(i=r.max)==null?1/0:i);+e!==t&&(e=t.toString())}}let c=0;if(r.formatter&&t===r.formatTrigger){let{formatter:t,maxlength:n}=r;if(e=t(e),C(n)&&R(e)>+n&&(e=z(e,+n)),y.value&&h.focused){let{selectionEnd:e}=y.value,n=z(o,e);c=t(n).length-n.length}}if(y.value&&y.value.value!==e){if(h.focused){let{selectionStart:t,selectionEnd:n}=y.value;if(y.value.value=e,C(t)&&C(n)){let r=e.length;s?(t-=s,n-=s):c&&(t+=c,n+=c),y.value.setSelectionRange(Math.min(t,r),Math.min(n,r))}}else y.value.value=e}e!==r.modelValue&&a(`update:modelValue`,e)},ue=e=>{e.target.composing||K(e.target.value)},q=()=>{var e;return(e=y.value)==null?void 0:e.blur()},J=()=>{var e;return(e=y.value)==null?void 0:e.focus()},Y=()=>{let e=y.value;r.type===`textarea`&&r.autosize&&e&&ce(e,r.autosize)},de=e=>{h.focused=!0,a(`focus`,e),s(Y),D(`readonly`)&&q()},fe=e=>{h.focused=!1,K(E(),`onBlur`),a(`blur`,e),!D(`readonly`)&&(W(`onBlur`),s(Y),ne())},X=e=>a(`clickInput`,e),pe=e=>a(`clickLeftIcon`,e),me=e=>a(`clickRightIcon`,e),he=e=>{f(e),a(`update:modelValue`,``),a(`clear`,e)},Z=T(()=>{if(typeof r.error==`boolean`)return r.error;if(w&&w.props.showError&&h.status===`failed`)return!0}),ge=T(()=>{let e=D(`labelWidth`),t=D(`labelAlign`);if(e&&t!==`top`)return{width:j(e)}}),Q=e=>{e.keyCode===13&&(!(w&&w.props.submitOnEnter)&&r.type!==`textarea`&&f(e),r.type===`search`&&q()),a(`keypress`,e)},$=()=>r.id||`${p}-input`,_e=()=>h.status,ve=()=>{let e=V(`control`,[D(`inputAlign`),{error:Z.value,custom:!!o.input,"min-height":r.type===`textarea`&&!r.autosize}]);if(o.input)return m(`div`,{class:e,onClick:X},[o.input()]);let t={id:$(),ref:y,name:r.name,rows:r.rows===void 0?void 0:+r.rows,class:e,disabled:D(`disabled`),readonly:D(`readonly`),autofocus:r.autofocus,placeholder:r.placeholder,autocomplete:r.autocomplete,autocapitalize:r.autocapitalize,autocorrect:r.autocorrect,enterkeyhint:r.enterkeyhint,spellcheck:r.spellcheck,"aria-labelledby":r.label?`${p}-label`:void 0,"data-allow-mismatch":`attribute`,onBlur:fe,onFocus:de,onInput:ue,onClick:X,onChange:L,onKeypress:Q,onCompositionend:L,onCompositionstart:se};return r.type===`textarea`?m(`textarea`,c(t,{inputmode:r.inputmode}),null):m(`input`,c(le(r.type,r.inputmode),t),null)},ye=()=>{let e=o[`left-icon`];if(r.leftIcon||e)return m(`div`,{class:V(`left-icon`),onClick:pe},[e?e():m(t,{name:r.leftIcon,classPrefix:r.iconPrefix},null)])},be=()=>{let e=o[`right-icon`];if(r.rightIcon||e)return m(`div`,{class:V(`right-icon`),onClick:me},[e?e():m(t,{name:r.rightIcon,classPrefix:r.iconPrefix},null)])},xe=()=>{if(r.showWordLimit&&r.maxlength){let e=R(E());return m(`div`,{class:V(`word-limit`)},[m(`span`,{class:V(`word-num`)},[e]),re(`/`),r.maxlength])}},Se=()=>{if(w&&w.props.showErrorMessage===!1)return;let e=r.errorMessage||h.validateMessage;if(e){let t=o[`error-message`],n=D(`errorMessageAlign`);return m(`div`,{class:V(`error-message`,n)},[t?t({message:e}):e])}},Ce=()=>{let e=D(`labelWidth`),t=D(`labelAlign`),n=D(`colon`)?`:`:``;if(o.label)return[o.label(),n];if(r.label)return m(`label`,{id:`${p}-label`,for:o.input?void 0:$(),"data-allow-mismatch":`attribute`,onClick:e=>{f(e),J()},style:t===`top`&&e?{width:j(e)}:void 0},[r.label+n])},we=()=>[m(`div`,{class:V(`body`)},[ve(),O.value&&m(t,{ref:b,name:r.clearIcon,class:V(`clear`)},null),be(),o.button&&m(`div`,{class:V(`button`)},[o.button()])]),xe(),Se()];return i({blur:q,focus:J,validate:U,formValue:A,resetValidation:B,getValidationStatus:_e,adjustTextareaSize:Y}),l(te,{customValue:x,resetValidation:B,validateWithTrigger:W}),v(()=>r.modelValue,()=>{K(E()),B(),W(`onChange`),s(Y)}),d(()=>{K(E(),r.formatTrigger),s(Y)}),_(`touchstart`,he,{target:T(()=>{var e;return(e=b.value)==null?void 0:e.$el})}),()=>{let e=D(`disabled`),t=D(`labelAlign`),i=ye(),a=()=>{let e=Ce();return t===`top`?[i,e].filter(Boolean):e||[]};return m(n,{size:r.size,class:V({error:Z.value,disabled:e,[`label-${t}`]:t}),center:r.center,border:r.border,isLink:!e&&r.isLink,clickable:!e&&r.clickable,onClick:e?e=>e.stopImmediatePropagation():void 0,titleStyle:ge.value,valueClass:V(`value`),titleClass:[V(`label`,[t,{required:M.value}]),r.labelClass],arrowDirection:r.arrowDirection},{icon:i&&t!==`top`?()=>i:null,title:a,value:we,extra:o.extra})}}}),G=w(W);export{H as n,P as r,G as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js new file mode 100644 index 000000000..c9c7f9e94 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js @@ -0,0 +1 @@ +import{a as e,f as t,h as n,l as r,n as i,o as a,s as o,u as s}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{n as c,t as l}from"./use-placeholder-D9ZemjXL.js";import{a as u,i as d,s as f}from"./cell-group-CEKvg8Wf.js";import{$ as p,B as m,Ct as h,Dt as ee,Et as g,Gt as _,It as te,Kt as v,L as y,Mt as b,O as x,Ot as ne,Q as S,R as C,S as w,St as T,U as E,V as re,Vt as D,Wt as O,X as k,Y as A,Z as j,_ as M,at as N,ct as ie,dt as P,et as ae,ft as oe,g as F,h as se,ht as I,jt as ce,kt as le,lt as ue,mt as de,ot as fe,tt as pe,ut as L,v as me}from"./index-C_mEe-tZ.js";var[R,z]=M(`action-bar`),B=Symbol(R),he=h({name:R,props:{placeholder:Boolean,safeAreaInsetBottom:j},setup(e,{slots:t}){let n=v(),r=l(n,z),{linkChildren:i}=m(B);i();let a=()=>{var r;return T(`div`,{ref:n,class:[z(),{"van-safe-area-bottom":e.safeAreaInsetBottom}]},[(r=t.default)==null?void 0:r.call(t)])};return()=>e.placeholder?r(a):a()}}),ge=F(he),[_e,ve]=M(`action-bar-button`),ye=S({},e,{type:String,text:String,icon:String,color:String,loading:Boolean,disabled:Boolean}),be=h({name:_e,props:ye,setup(e,{slots:t}){let n=a(),{parent:r,index:i}=E(B),o=I(()=>{if(r){let e=r.children[i.value-1];return!(e&&`isButton`in e)}}),s=I(()=>{if(r){let e=r.children[i.value+1];return!(e&&`isButton`in e)}});return f({isButton:!0}),()=>{let{type:r,icon:i,text:a,color:c,loading:l,disabled:d}=e;return T(u,{class:ve([r,{last:s.value,first:o.value}]),size:`large`,type:r,icon:i,color:c,loading:l,disabled:d,onClick:n},{default:()=>[t.default?t.default():a]})}}}),V=F(be),H={show:Boolean,zIndex:k,overlay:j,duration:k,teleport:[String,Object],lockScroll:j,lazyRender:j,beforeClose:Function,overlayProps:Object,overlayStyle:Object,overlayClass:null,transitionAppear:Boolean,closeOnClickOverlay:j},xe=Object.keys(H);function Se(e,t){return e>t?`horizontal`:t>e?`vertical`:``}function U(){let e=v(0),t=v(0),n=v(0),r=v(0),i=v(0),a=v(0),o=v(``),s=v(!0),c=()=>o.value===`vertical`,l=()=>o.value===`horizontal`,u=()=>{n.value=0,r.value=0,i.value=0,a.value=0,o.value=``,s.value=!0};return{move:(c=>{let l=c.touches[0];n.value=(l.clientX<0?0:l.clientX)-e.value,r.value=l.clientY-t.value,i.value=Math.abs(n.value),a.value=Math.abs(r.value),(!o.value||i.value<10&&a.value<10)&&(o.value=Se(i.value,a.value)),s.value&&(i.value>5||a.value>5)&&(s.value=!1)}),start:(n=>{u(),e.value=n.touches[0].clientX,t.value=n.touches[0].clientY}),reset:u,startX:e,startY:t,deltaX:n,deltaY:r,offsetX:i,offsetY:a,direction:o,isVertical:c,isHorizontal:l,isTap:s}}var W=0,G=`van-overflow-hidden`;function Ce(e,t){let n=U(),r=t=>{n.move(t);let r=n.deltaY.value>0?`10`:`01`,i=y(t.target,e.value);for(;i.scrollHeight<=i.offsetHeight&&i!==e.value&&i.parentElement;)i=y(i.parentElement,e.value);let{scrollHeight:a,offsetHeight:o,scrollTop:s}=i,c=`11`;s===0?c=o>=a?`00`:`01`:s+o>=a&&(c=`10`),c!==`11`&&n.isVertical()&&!(parseInt(c,2)&parseInt(r,2))&&x(t,!0)},i=()=>{document.addEventListener(`touchstart`,n.start),document.addEventListener(`touchmove`,r,{passive:!1}),W||document.body.classList.add(G),W++},a=()=>{W&&(document.removeEventListener(`touchstart`,n.start),document.removeEventListener(`touchmove`,r),W--,W||document.body.classList.remove(G))},o=()=>t()&&i(),s=()=>t()&&a();C(o),ce(s),le(s),D(t,e=>{e?i():a()})}function K(e){let t=v(!1);return D(e,e=>{e&&(t.value=e)},{immediate:!0}),e=>()=>t.value?e():null}var[we,Te]=M(`overlay`),Ee=h({name:we,inheritAttrs:!1,props:{show:Boolean,zIndex:k,duration:k,className:null,lockScroll:j,lazyRender:j,customStyle:Object,teleport:[String,Object]},setup(e,{attrs:t,slots:n}){let r=v(),i=K(()=>e.show||!e.lazyRender),a=t=>{e.lockScroll&&x(t,!0)},o=i(()=>{var i;let a=S(w(e.zIndex),e.customStyle);return ae(e.duration)&&(a.animationDuration=`${e.duration}s`),O(T(`div`,g({ref:r,style:a,class:[Te(),e.className]},t),[(i=n.default)==null?void 0:i.call(n)]),[[L,e.show]])});return re(`touchmove`,a,{target:r}),()=>{let t=T(ie,{name:`van-fade`,appear:!0},{default:o});return e.teleport?T(de,{to:e.teleport},{default:()=>[t]}):t}}}),De=F(Ee),Oe=S({},H,{round:Boolean,position:A(`center`),closeIcon:A(`cross`),closeable:Boolean,transition:String,iconPrefix:String,closeOnPopstate:Boolean,closeIconPosition:A(`top-right`),destroyOnClose:Boolean,safeAreaInsetTop:Boolean,safeAreaInsetBottom:Boolean}),[ke,q]=M(`popup`),Ae=h({name:ke,inheritAttrs:!1,props:Oe,emits:[`open`,`close`,`opened`,`closed`,`keydown`,`update:show`,`clickOverlay`,`clickCloseIcon`],setup(e,{emit:t,attrs:r,slots:a}){let s,l,u=v(),p=v(),m=K(()=>e.show||!e.lazyRender),h=I(()=>{let t={zIndex:u.value};if(ae(e.duration)){let n=e.position===`center`?`animationDuration`:`transitionDuration`;t[n]=`${e.duration}s`}return t}),_=()=>{s||(s=!0,u.value=e.zIndex===void 0?se():+e.zIndex,t(`open`))},y=()=>{s&&c(e.beforeClose,{done(){s=!1,t(`close`),t(`update:show`,!1)}})},x=n=>{t(`clickOverlay`,n),e.closeOnClickOverlay&&y()},C=()=>{if(e.overlay){let t=S({show:e.show,class:e.overlayClass,zIndex:u.value,duration:e.duration,customStyle:e.overlayStyle,role:e.closeOnClickOverlay?`button`:void 0,tabindex:e.closeOnClickOverlay?0:void 0},e.overlayProps);return T(De,g(t,d(),{onClick:x}),{default:a[`overlay-content`]})}},w=e=>{t(`clickCloseIcon`,e),y()},E=()=>{if(e.closeable)return T(i,{role:`button`,tabindex:0,name:e.closeIcon,class:[q(`close-icon`,e.closeIconPosition),n],classPrefix:e.iconPrefix,onClick:w},null)},k,A=()=>{k&&clearTimeout(k),k=setTimeout(()=>{t(`opened`)})},j=()=>t(`closed`),M=e=>t(`keydown`,e),N=m(()=>{var t;let{destroyOnClose:n,round:i,position:o,safeAreaInsetTop:s,safeAreaInsetBottom:c,show:l}=e;if(l||!n)return O(T(`div`,g({ref:p,style:h.value,role:`dialog`,tabindex:0,class:[q({round:i,[o]:o}),{"van-safe-area-top":s,"van-safe-area-bottom":c}],onKeydown:M},r,d()),[(t=a.default)==null?void 0:t.call(a),E()]),[[L,l]])}),P=()=>{let{position:t,transition:n,transitionAppear:r}=e,i=t===`center`?`van-fade`:`van-popup-slide-${t}`;return T(ie,{name:n||i,appear:r,onAfterEnter:A,onAfterLeave:j},{default:N})};return D(()=>e.show,e=>{e&&!s&&(_(),r.tabindex===0&&ee(()=>{var e;(e=p.value)==null||e.focus()})),!e&&s&&(s=!1,t(`close`))}),f({popupRef:p}),Ce(p,()=>e.show&&e.lockScroll),re(`popstate`,()=>{e.closeOnPopstate&&(y(),l=!1)}),b(()=>{e.show&&_()}),ne(()=>{l&&(t(`update:show`,!0),l=!1)}),ce(()=>{e.show&&e.teleport&&(y(),l=!0)}),te(o,()=>e.show),()=>e.teleport?T(de,{to:e.teleport},{default:()=>[C(),P()]}):T(oe,null,[C(),P()])}}),J=F(Ae);function je(){let e=_({show:!1}),t=t=>{e.show=t},n=n=>{S(e,n,{transitionAppear:!0}),t(!0)},r=()=>t(!1);return f({open:n,close:r,toggle:t}),{open:n,close:r,state:e,toggle:t}}function Me(e){let t=ue(e),n=document.createElement(`div`);return document.body.appendChild(n),{instance:t.mount(n),unmount(){t.unmount(),document.body.removeChild(n)}}}var[Ne,Y,X]=M(`dialog`),Pe=S({},H,{title:String,theme:String,width:k,message:[String,Function],callback:Function,allowHtml:Boolean,className:null,transition:A(`van-dialog-bounce`),messageAlign:String,closeOnPopstate:j,showCancelButton:Boolean,cancelButtonText:String,cancelButtonColor:String,cancelButtonDisabled:Boolean,confirmButtonText:String,confirmButtonColor:String,confirmButtonDisabled:Boolean,showConfirmButton:j,closeOnClickOverlay:Boolean,keyboardEnabled:j,destroyOnClose:Boolean}),Fe=[...xe,`transition`,`closeOnPopstate`,`destroyOnClose`],Ie=h({name:Ne,props:Pe,emits:[`confirm`,`cancel`,`keydown`,`update:show`],setup(e,{emit:n,slots:r}){let i=v(),a=_({confirm:!1,cancel:!1}),o=e=>n(`update:show`,e),l=t=>{var n;o(!1),(n=e.callback)==null||n.call(e,t)},d=t=>()=>{e.show&&(n(t),e.beforeClose?(a[t]=!0,c(e.beforeClose,{args:[t],done(){l(t),a[t]=!1},canceled(){a[t]=!1}})):l(t))},f=d(`cancel`),p=d(`confirm`),m=P(t=>{var r,a;e.keyboardEnabled&&t.target===((a=(r=i.value)==null?void 0:r.popupRef)==null?void 0:a.value)&&({Enter:e.showConfirmButton?p:N,Escape:e.showCancelButton?f:N}[t.key](),n(`keydown`,t))},[`enter`,`esc`]),h=()=>{let t=r.title?r.title():e.title;if(t)return T(`div`,{class:Y(`header`,{isolated:!e.message&&!r.default})},[t])},ee=t=>{let{message:n,allowHtml:r,messageAlign:i}=e,a=Y(`message`,{"has-title":t,[i]:i}),o=pe(n)?n():n;return r&&typeof o==`string`?T(`div`,{class:a,innerHTML:o},null):T(`div`,{class:a},[o])},te=()=>{if(r.default)return T(`div`,{class:Y(`content`)},[r.default()]);let{title:t,message:n,allowHtml:i}=e;if(n){let e=!!(t||r.title);return T(`div`,{key:+!!i,class:Y(`content`,{isolated:!e})},[ee(e)])}},y=()=>T(`div`,{class:[t,Y(`footer`)]},[e.showCancelButton&&T(u,{size:`large`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),style:{color:e.cancelButtonColor},loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(u,{size:`large`,text:e.confirmButtonText||X(`confirm`),class:[Y(`confirm`),{[s]:e.showCancelButton}],style:{color:e.confirmButtonColor},loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]),b=()=>T(ge,{class:Y(`footer`)},{default:()=>[e.showCancelButton&&T(V,{type:`warning`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),color:e.cancelButtonColor,loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(V,{type:`danger`,text:e.confirmButtonText||X(`confirm`),class:Y(`confirm`),color:e.confirmButtonColor,loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]}),x=()=>r.footer?r.footer():e.theme===`round-button`?b():y();return()=>{let{width:t,title:n,theme:r,message:a,className:s}=e;return T(J,g({ref:i,role:`dialog`,class:[Y([r]),s],style:{width:me(t)},tabindex:0,"aria-labelledby":n||a,onKeydown:m,"onUpdate:show":o},fe(e,Fe)),{default:()=>[h(),te(),x()]})}}}),Z,Le=S({},{title:``,width:``,theme:null,message:``,overlay:!0,callback:null,teleport:`body`,className:``,allowHtml:!1,lockScroll:!0,transition:void 0,beforeClose:null,overlayClass:``,overlayStyle:void 0,messageAlign:``,cancelButtonText:``,cancelButtonColor:null,cancelButtonDisabled:!1,confirmButtonText:``,confirmButtonColor:null,confirmButtonDisabled:!1,showConfirmButton:!0,showCancelButton:!1,closeOnPopstate:!0,closeOnClickOverlay:!1,destroyOnClose:!1});function Re(){let e={setup(){let{state:e,toggle:t}=je();return()=>T(Ie,g(e,{"onUpdate:show":t}),null)}};({instance:Z}=Me(e))}function ze(e){return p?new Promise((t,n)=>{Z||Re(),Z.open(S({},Le,e,{callback:e=>{(e===`confirm`?t:n)(e)}}))}):Promise.resolve(void 0)}var Be=e=>ze(S({showCancelButton:!0},e)),[Ve,Q]=M(`nav-bar`),He=h({name:Ve,props:{title:String,fixed:Boolean,zIndex:k,border:j,leftText:String,rightText:String,leftDisabled:Boolean,rightDisabled:Boolean,leftArrow:Boolean,placeholder:Boolean,safeAreaInsetTop:Boolean,clickable:j},emits:[`clickLeft`,`clickRight`],setup(e,{emit:t,slots:n}){let a=v(),o=l(a,Q),s=n=>{e.leftDisabled||t(`clickLeft`,n)},c=n=>{e.rightDisabled||t(`clickRight`,n)},u=()=>n.left?n.left():[e.leftArrow&&T(i,{class:Q(`arrow`),name:`arrow-left`},null),e.leftText&&T(`span`,{class:Q(`text`)},[e.leftText])],d=()=>n.right?n.right():T(`span`,{class:Q(`text`)},[e.rightText]),f=()=>{let{title:t,fixed:i,border:o,zIndex:l}=e,f=w(l),p=e.leftArrow||e.leftText||n.left,m=e.rightText||n.right;return T(`div`,{ref:a,style:f,class:[Q({fixed:i}),{[r]:o,"van-safe-area-top":e.safeAreaInsetTop}]},[T(`div`,{class:Q(`content`)},[p&&T(`div`,{class:[Q(`left`,{disabled:e.leftDisabled}),e.clickable&&!e.leftDisabled?`van-haptics-feedback`:``],onClick:s},[u()]),T(`div`,{class:[Q(`title`),`van-ellipsis`]},[n.title?n.title():t]),m&&T(`div`,{class:[Q(`right`,{disabled:e.rightDisabled}),e.clickable&&!e.rightDisabled?`van-haptics-feedback`:``],onClick:c},[d()])])])};return()=>e.fixed&&e.placeholder?o(f):f()}}),Ue=F(He),$=e=>String(e).padStart(2,`0`);function We(e){let t=String(e||``).trim();if(!t)return null;let n=new Date(t);return Number.isNaN(n.getTime())?null:n}function Ge(e,t=new Date){let n=We(e);if(!n)return`—`;let r=Math.round((t.getTime()-n.getTime())/1e3);if(r<45&&r>-60)return`刚刚`;if(r>=45&&r<3600)return`${Math.max(1,Math.round(r/60))} 分钟前`;let i=`${$(n.getHours())}:${$(n.getMinutes())}`,a=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),o=864e5;return n.getTime()>=a&&n.getTime()=a-o&&n.getTime()i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>S(e)===`[object Map]`,p=e=>S(e)===`[object Set]`,m=e=>S(e)===`[object Date]`,h=e=>S(e)===`[object RegExp]`,g=e=>typeof e==`function`,_=e=>typeof e==`string`,v=e=>typeof e==`symbol`,y=e=>typeof e==`object`&&!!e,b=e=>(y(e)||g(e))&&g(e.then)&&g(e.catch),x=Object.prototype.toString,S=e=>x.call(e),C=e=>S(e).slice(8,-1),w=e=>S(e)===`[object Object]`,T=e=>_(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,ee=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),E=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},te=/-\w/g,D=E(e=>e.replace(te,e=>e.slice(1).toUpperCase())),ne=/\B([A-Z])/g,O=E(e=>e.replace(ne,`-$1`).toLowerCase()),re=E(e=>e.charAt(0).toUpperCase()+e.slice(1)),ie=E(e=>e?`on${re(e)}`:``),k=(e,t)=>!Object.is(e,t),A=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},oe=e=>{let t=parseFloat(e);return isNaN(t)?e:t},se=e=>{let t=_(e)?Number(e):NaN;return isNaN(t)?e:t},ce,le=()=>ce||(ce=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{});function ue(e){if(d(e)){let t={};for(let n=0;n{if(e){let n=e.split(fe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function he(e){let t=``;if(_(e))t=e;else if(d(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Se=e=>_(e)?e:e==null?``:d(e)||y(e)&&(e.toString===x||!g(e.toString))?xe(e)?Se(e.value):JSON.stringify(e,Ce,2):String(e),Ce=(e,t)=>xe(t)?Ce(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[we(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>we(e))}:v(t)?we(t):y(t)&&!d(t)&&!w(t)?String(t):t,we=(e,t=``)=>{var n;return v(e)?`Symbol(${(n=e.description)==null?t:n})`:e},j,Te=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&j&&(j.active?(this.parent=j,this.index=(j.scopes||(j.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes){let n=this.scopes.slice();for(e=0,t=n.length;e0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(je){let e=je;for(je=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ae;){let t=Ae;for(Ae=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}function Fe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ie(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),ze(r),Be(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Re(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Re(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ke)||(e.globalVersion=Ke,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Le(e))))return;e.flags|=2;let t=e.dep,n=M,r=Ve;M=e,Ve=!0;try{Fe(e);let n=e.fn(e._value);(t.version===0||k(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{M=n,Ve=r,Ie(e),e.flags&=-3}}function ze(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ze(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Be(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var Ve=!0,He=[];function Ue(){He.push(Ve),Ve=!1}function We(){let e=He.pop();Ve=e===void 0||e}function Ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;try{t()}finally{M=e}}}var Ke=0,qe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Je=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!M||!Ve||M===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==M)t=this.activeLink=new qe(M,this),M.deps?(t.prevDep=M.depsTail,M.depsTail.nextDep=t,M.depsTail=t):M.deps=M.depsTail=t,Ye(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=M.depsTail,t.nextDep=void 0,M.depsTail.nextDep=t,M.depsTail=t,M.deps===t&&(M.deps=e)}return t}trigger(e){this.version++,Ke++,this.notify(e)}notify(e){Ne();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pe()}}};function Ye(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Ye(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var Xe=new WeakMap,Ze=Symbol(``),Qe=Symbol(``),$e=Symbol(``);function N(e,t,n){if(Ve&&M){let t=Xe.get(e);t||Xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Je),r.map=t,r.key=n),r.track()}}function et(e,t,n,r,i,a){let o=Xe.get(e);if(!o){Ke++;return}let s=e=>{e&&e.trigger()};if(Ne(),t===`clear`)o.forEach(s);else{let i=d(e),a=i&&T(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===$e||!v(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get($e)),t){case`add`:i?a&&s(o.get(`length`)):(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`delete`:i||(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`set`:f(e)&&s(o.get(Ze))}}Pe()}function tt(e){let t=F(e);return t===e?t:(N(t,`iterate`,$e),P(e)?t:t.map(Ht))}function nt(e){return N(e=F(e),`iterate`,$e),e}function rt(e,t){return zt(e)?Ut(Rt(e)?Ht(t):t):Ht(t)}var it={__proto__:null,[Symbol.iterator](){return at(this,Symbol.iterator,e=>rt(this,e))},concat(...e){return tt(this).concat(...e.map(e=>d(e)?tt(e):e))},entries(){return at(this,`entries`,e=>(e[1]=rt(this,e[1]),e))},every(e,t){return st(this,`every`,e,t,void 0,arguments)},filter(e,t){return st(this,`filter`,e,t,e=>e.map(e=>rt(this,e)),arguments)},find(e,t){return st(this,`find`,e,t,e=>rt(this,e),arguments)},findIndex(e,t){return st(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return st(this,`findLast`,e,t,e=>rt(this,e),arguments)},findLastIndex(e,t){return st(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return st(this,`forEach`,e,t,void 0,arguments)},includes(...e){return lt(this,`includes`,e)},indexOf(...e){return lt(this,`indexOf`,e)},join(e){return tt(this).join(e)},lastIndexOf(...e){return lt(this,`lastIndexOf`,e)},map(e,t){return st(this,`map`,e,t,void 0,arguments)},pop(){return ut(this,`pop`)},push(...e){return ut(this,`push`,e)},reduce(e,...t){return ct(this,`reduce`,e,t)},reduceRight(e,...t){return ct(this,`reduceRight`,e,t)},shift(){return ut(this,`shift`)},some(e,t){return st(this,`some`,e,t,void 0,arguments)},splice(...e){return ut(this,`splice`,e)},toReversed(){return tt(this).toReversed()},toSorted(e){return tt(this).toSorted(e)},toSpliced(...e){return tt(this).toSpliced(...e)},unshift(...e){return ut(this,`unshift`,e)},values(){return at(this,`values`,e=>rt(this,e))}};function at(e,t,n){let r=nt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var ot=Array.prototype;function st(e,t,n,r,i,a){let o=nt(e),s=o!==e&&!P(e),c=o[t];if(c!==ot[t]){let t=c.apply(e,a);return s?Ht(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,rt(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function ct(e,t,n,r){let i=nt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=rt(e,t)),n.call(this,t,rt(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?rt(e,c):c}function lt(e,t,n){let r=F(e);N(r,`iterate`,$e);let i=r[t](...n);return(i===-1||i===!1)&&Bt(n[0])?(n[0]=F(n[0]),r[t](...n)):i}function ut(e,t,n=[]){Ue(),Ne();let r=F(e)[t].apply(e,n);return Pe(),We(),r}var dt=e(`__proto__,__v_isRef,__isVue`),ft=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(v));function pt(e){v(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.hasOwnProperty(e)}var mt=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?Mt:jt:i?At:kt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=it[t]))return e;if(t===`hasOwnProperty`)return pt}let o=Reflect.get(e,t,I(e)?e:n);if((v(t)?ft.has(t):dt(t))||(r||N(e,`get`,t),i))return o;if(I(o)){let e=a&&T(t)?o:o.value;return r&&y(e)?It(e):e}return y(o)?r?It(o):Pt(o):o}},ht=class extends mt{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&T(t);if(!this._isShallow){let e=zt(i);if(!P(n)&&!zt(n)&&(i=F(i),n=F(n)),!a&&I(i)&&!I(n))return e||(i.value=n),!0}let o=a?Number(t)e,xt=e=>Reflect.getPrototypeOf(e);function St(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a),c=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?bt:t?Ut:Ht;return!t&&N(a,`iterate`,l?Qe:Ze),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Ct(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function wt(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(k(n,a)&&N(i,`get`,n),N(i,`get`,a));let{has:o}=xt(i),s=t?bt:e?Ut:Ht;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(F(t),`iterate`,Ze),t.size},has(t){let n=this.__v_raw,r=F(n),i=F(t);return e||(k(t,i)&&N(r,`has`,t),N(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=F(a),s=t?bt:e?Ut:Ht;return!e&&N(o,`iterate`,Ze),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Ct(`add`),set:Ct(`set`),delete:Ct(`delete`),clear:Ct(`clear`)}:{add(e){let n=F(this),r=xt(n),i=F(e),a=!t&&!P(e)&&!zt(e)?i:e;return r.has.call(n,a)||k(e,a)&&r.has.call(n,e)||k(i,a)&&r.has.call(n,i)||(n.add(a),et(n,`add`,a,a)),this},set(e,n){!t&&!P(n)&&!zt(n)&&(n=F(n));let r=F(this),{has:i,get:a}=xt(r),o=i.call(r,e);o||(e=F(e),o=i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?k(n,s)&&et(r,`set`,e,n,s):et(r,`add`,e,n),this},delete(e){let t=F(this),{has:n,get:r}=xt(t),i=n.call(t,e);i||(e=F(e),i=n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&et(t,`delete`,e,void 0,a),o},clear(){let e=F(this),t=e.size!==0,n=e.clear();return t&&et(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=St(r,e,t)}),n}function Tt(e,t){let n=wt(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var Et={get:Tt(!1,!1)},Dt={get:Tt(!1,!0)},Ot={get:Tt(!0,!1)},kt=new WeakMap,At=new WeakMap,jt=new WeakMap,Mt=new WeakMap;function Nt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function Pt(e){return zt(e)?e:Lt(e,!1,_t,Et,kt)}function Ft(e){return Lt(e,!1,yt,Dt,At)}function It(e){return Lt(e,!0,vt,Ot,jt)}function Lt(e,t,n,r,i){if(!y(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let a=i.get(e);if(a)return a;let o=Nt(C(e));if(o===0)return e;let s=new Proxy(e,o===2?r:n);return i.set(e,s),s}function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function P(e){return!!(e&&e.__v_isShallow)}function Bt(e){return e?!!e.__v_raw:!1}function F(e){let t=e&&e.__v_raw;return t?F(t):e}function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&ae(e,`__v_skip`,!0),e}var Ht=e=>y(e)?Pt(e):e,Ut=e=>y(e)?It(e):e;function I(e){return e?e.__v_isRef===!0:!1}function Wt(e){return Kt(e,!1)}function Gt(e){return Kt(e,!0)}function Kt(e,t){return I(e)?e:new qt(e,t)}var qt=class{constructor(e,t){this.dep=new Je,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:F(e),this._value=t?e:Ht(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?e:F(e),k(e,t)&&(this._rawValue=e,this._value=n?e:Ht(e),this.dep.trigger())}};function L(e){return I(e)?e.value:e}var Jt={get:(e,t,n)=>t===`__v_raw`?e:L(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return I(i)&&!I(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return Rt(e)?e:new Proxy(e,Jt)}var Xt=class{constructor(e){this.__v_isRef=!0,this._value=void 0;let t=this.dep=new Je,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}};function Zt(e){return new Xt(e)}var Qt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Je(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ke-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return Me(this,!0),!0}get value(){let e=this.dep.track();return Re(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function $t(e,t,n=!1){let r,i;return g(e)?r=e:(r=e.get,i=e.set),new Qt(r,i,n)}var en={},tn=new WeakMap,nn=void 0;function rn(e,t=!1,n=nn){if(n){let t=tn.get(n);t||tn.set(n,t=[]),t.push(e)}}function an(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:P(e)||o===!1||o===0?on(e,1):on(e),m,h,_,v,y=!1,b=!1;if(I(e)?(h=()=>e.value,y=P(e)):Rt(e)?(h=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Rt(e)||P(e)),h=()=>e.map(e=>{if(I(e))return e.value;if(Rt(e))return p(e);if(g(e))return f?f(e,2):e()})):h=g(e)?n?f?()=>f(e,2):e:()=>{if(_){Ue();try{_()}finally{We()}}let t=nn;nn=m;try{return f?f(e,3,[v]):e(v)}finally{nn=t}}:r,n&&o){let e=h,t=o===!0?1/0:o;h=()=>on(e(),t)}let x=Ee(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{let n=e(...t);return S(),n}}let C=b?Array(e.length).fill(en):en,w=e=>{if(m.flags&1&&(m.dirty||e)){if(n){let t=m.run();if(e||o||y||(b?t.some((e,t)=>k(e,C[t])):k(t,C))){_&&_();let e=nn;nn=m;try{let e=[t,C===en?void 0:b&&C[0]===en?[]:C,v];C=t,f?f(n,3,e):n(...e)}finally{nn=e}}}else m.run()}};return u&&u(w),m=new Oe(h),m.scheduler=l?()=>l(w,!1):w,v=e=>rn(e,!1,m),_=m.onStop=()=>{let e=tn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();tn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function on(e,t=1/0,n){if(t<=0||!y(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,I(e))on(e.value,t,n);else if(d(e))for(let r=0;r{on(e,t,n)});else if(w(e)){for(let r in e)on(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&on(e[r],t,n)}return e}function sn(e,t,n,r){try{return r?e(...r):e()}catch(e){cn(e,t,n)}}function R(e,t,n,r){if(g(e)){let i=sn(e,t,n,r);return i&&b(i)&&i.catch(e=>{cn(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a>>1,i=z[r],a=Cn(i);a=Cn(n)?z.push(e):z.splice(_n(t),0,e),e.flags|=1,yn()}}function yn(){hn||(hn=mn.then(wn))}function bn(e){if(!d(e))fn&&e.id===-1?fn.splice(pn+1,0,e):e.flags&1||(dn.push(e),e.flags|=1);else for(let t=0;tCn(e)-Cn(t));if(dn.length=0,fn){for(let t=0;te.id==null?e.flags&2?-1:1/0:e.id;function wn(e){try{for(un=0;un{r._d&&pa(-1);let i=En(t),a=la.length,o;try{o=e(...n)}finally{for(let e=la.length;e>a;e--)da();En(i),r._d&&pa(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function On(e,n){if(B===null)return e;let r=qa(B),i=e.dirs||(e.dirs=[]);for(let e=0;e1)return n&&g(t)?t.call(r&&r.proxy):t}}var jn=Symbol.for(`v-scx`),Mn=()=>V(jn);function Nn(e,t){return In(e,null,t)}function Pn(e,t){return In(e,null,{flush:`sync`})}function Fn(e,t,n){return In(e,t,n)}function In(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ba){if(c===`sync`){let e=Mn();f=e.__watcherHandles||(e.__watcherHandles=[])}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=X;u.call=(e,t,n)=>R(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{G(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():vn(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=an(e,n,u);return Ba&&(f?f.push(h):d&&h()),h}function Ln(e,t,n){let r=this.proxy,i=_(e)?e.includes(`.`)?Rn(r,e):()=>r[e]:e.bind(r,r),a;g(t)?a=t:(a=t.handler,n=t);let o=La(this),s=In(i,a.bind(r),n);return o(),s}function Rn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;ee.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===``),Un=e=>e&&(e.defer||e.defer===``),Wn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Gn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Kn=(e,t)=>{let n=e&&e.to;return _(n)?t?t(n):null:n},qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g,parentNode:_}}=l,v=Hn(t.props),{dynamicChildren:y}=t,b=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},x=(e=t)=>{let n=Hn(e.props),r=e.target=Kn(e.props,m),a=Qn(r,e,h,p);r&&(o!==`svg`&&Wn(r)?o=`svg`:o!==`mathml`&&Gn(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(b(e,r,a),Zn(e,!1)))},S=e=>{let t=()=>{if(zn.get(e)===t){if(zn.delete(e),Hn(e.props)){let t=_(e.el)||n;b(e,t,e.anchor),Zn(e,!0)}x(e)}};zn.set(e,t),G(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Un(t.props)||a&&a.pendingBranch){S(t);return}v&&(b(t,n,i),Zn(t,!0)),x()}else{t.el=e.el;let r=t.anchor=e.anchor,u=zn.get(e);if(u){u.flags|=8,zn.delete(e),S(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Hn(e.props),_=g?n:p,b=g?r:h;if(o===`svg`||Wn(p)?o=`svg`:(o===`mathml`||Gn(p))&&(o=`mathml`),y?(f(e.dynamicChildren,y,_,i,a,o,s),$i(e,t,!0)):c||d(e,t,_,b,i,a,o,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Jn(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=Kn(t.props,m);e&&(t.target=e,Jn(t,e,null,l,0))}else g&&Jn(t,p,h,l,1);Zn(t,v)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=Hn(f),m=a||!p,h=zn.get(e);if(h&&(h.flags|=8,zn.delete(e)),d&&(i(l),i(u)),a&&i(c),!h&&(p||d)&&o&16)for(let e=0;e{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}var U=[Function,Array],tr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:U,onEnter:U,onAfterEnter:U,onEnterCancelled:U,onBeforeLeave:U,onLeave:U,onAfterLeave:U,onLeaveCancelled:U,onBeforeAppear:U,onAppear:U,onAfterAppear:U,onAppearCancelled:U},nr=e=>{let t=e.subTree;return t.component?nr(t.component):t},rr={name:`BaseTransition`,props:tr,setup(e,{slots:t}){let n=Pa(),r=er();return()=>{let i=t.default&&dr(t.default(),!0),a=i&&i.length?ir(i):n.subTree?Ea():void 0;if(!a)return;let o=F(e),{mode:s}=o;if(r.isLeaving)return cr(a);let c=lr(a);if(!c)return cr(a);let l=sr(c,o,r,n,e=>l=e);c.type!==K&&ur(c,l);let u=n.subTree&&lr(n.subTree);if(u&&u.type!==K&&!va(u,c)&&nr(n).type!==K){let e=sr(u,o,r,n);if(ur(u,e),s===`out-in`&&c.type!==K)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},cr(a);s===`in-out`&&c.type!==K?e.delayLeave=(e,t,n)=>{let i=or(r,u);i[String(u.key)]=u,e[H]=()=>{t(),e[H]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return a}}};function ir(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==K){t=n;break}}return t}var ar=rr;function or(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function sr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=or(n,e),C=(e,t)=>{e&&R(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted){if(a)r=_||c;else return}t[H]&&t[H](!0);let i=S[x];i&&va(e,i)&&i.el[H]&&i.el[H](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted){if(a)r=v||l,i=y||u,o=b||f;else return}let s=!1;t[$n]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[$n]=void 0)};let c=t[$n].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[$n]&&t[$n](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[H]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[H]=void 0,S[i]===e&&delete S[i])};let o=t[H].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=sr(e,t,n,r,i);return i&&i(a),a}};return T}function cr(e){if(yr(e))return e=wa(e),e.children=null,e}function lr(e){if(!yr(e))return Vn(e.type)&&e.children?ir(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&g(n.default))return n.default()}}function ur(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;let n=e.component.subTree;ur(Vn(n.type)&&lr(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function dr(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;egr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(vr(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&gr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?qa(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,h=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=F(v),b=v===t?i:e=>!mr(h,e)&&u(y,e),x=(e,t)=>!(t&&mr(h,t));if(m!=null&&m!==p){if(_r(n),_(m))h[m]=null,b(m)&&(v[m]=null);else if(I(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(h[e.k]=null)}}if(g(p))sn(p,f,12,[l,h]);else{let t=_(p),n=I(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:h[p]:x(p)||!e.k?p.value:h[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)h[p]=[s],b(p)&&(v[p]=h[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(h[e.k]=t)}}else t?(h[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(h[e.k]=l))};if(l){let t=()=>{i(),hr.delete(e)};t.id=-1,hr.set(e,t),G(t,r)}else _r(e),i()}}}function _r(e){let t=hr.get(e);t&&(t.flags|=8,hr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var vr=e=>!!e.type.__asyncLoader,yr=e=>e.type.__isKeepAlive,br={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=Pa(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),G(()=>{a.isDeactivated=!1,a.a&&A(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Y(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;na(t.m),na(t.a),l(e,f,null,1,s),G(()=>{t.da&&A(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Y(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Er(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=Ja(vr(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!va(t,o))?p(t):o&&Er(o),i.delete(e),a.delete(e)}Fn(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>xr(e,t)),t&&m(e=>!xr(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(ia(n.subTree.type)?G(()=>{i.set(g,Dr(n.subTree))},n.subTree.suspense):i.set(g,Dr(n.subTree)))};return jr(_),Nr(_),Pr(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Dr(t);if(e.type===i.type&&e.key===i.key){Er(i);let e=i.component.da;e&&G(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!_a(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Dr(r);if(s.type===K)return o=null,s;let c=s.type,l=Ja(vr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!xr(u,l))||d&&l&&xr(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=wa(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&ur(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,ia(r.type)?r:s}}};function xr(e,t){return d(e)?e.some(e=>xr(e,t)):_(e)?e.split(`,`).includes(t):h(e)?(e.lastIndex=0,e.test(t)):!1}function Sr(e,t){wr(e,`a`,t)}function Cr(e,t){wr(e,`da`,t)}function wr(e,t,n=X){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Or(t,r,n),n){let e=n.parent;for(;e&&e.parent;)yr(e.parent.vnode)&&Tr(r,t,n,e),e=e.parent}}function Tr(e,t,n,r){let i=Or(t,e,r,!0);Fr(()=>{c(r[t],i)},n)}function Er(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Dr(e){return e.shapeFlag&128?e.ssContent:e}function Or(e,t,n=X,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||(t.__weh=(...r)=>{Ue();let i=La(n),a=R(t,n,e,r);return i(),We(),a});return r?i.unshift(a):i.push(a),a}}var kr=e=>(t,n=X)=>{(!Ba||e===`sp`)&&Or(e,(...e)=>t(...e),n)},Ar=kr(`bm`),jr=kr(`m`),Mr=kr(`bu`),Nr=kr(`u`),Pr=kr(`bum`),Fr=kr(`um`),Ir=kr(`sp`),Lr=kr(`rtg`),Rr=kr(`rtc`);function zr(e,t=X){Or(`ec`,e,t)}var Br=`components`;function Vr(e,t){return Wr(Br,e,!0,t)||e}var Hr=Symbol.for(`v-ndc`);function Ur(e){return _(e)?Wr(Br,e,!1)||e:e||Hr}function Wr(e,t,n=!0,r=!1){let i=B||X;if(i){let n=i.type;if(e===Br){let e=Ja(n,!1);if(e&&(e===t||e===D(t)||e===re(D(t))))return n}let a=Gr(i[e]||n[e],t)||Gr(i.appContext[e],t);return!a&&r?n:a}}function Gr(e,t){return e&&(e[t]||e[D(t)]||e[re(D(t))])}function Kr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||_(e)){let n=o&&Rt(e),r=!1,s=!1;n&&(r=!P(e),s=zt(e),e=nt(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}var Jr=e=>e?za(e)?qa(e):Jr(e.parent):null,Yr=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{vn(e.update)}),$nextTick:e=>e.n||(e.n=gn.bind(e.proxy)),$watch:e=>Ln.bind(e)}),Xr=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),Zr={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(Xr(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else $r&&(s[n]=0)}let d=Yr[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return Xr(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||Xr(n,c)||u(o,c)||u(i,c)||u(Yr,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function Qr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var $r=!0;function ei(e){let t=ii(e),n=e.proxy,i=e.ctx;$r=!1,t.beforeCreate&&ni(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:h,updated:_,activated:v,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:E,errorCaptured:te,serverPrefetch:D,expose:ne,inheritAttrs:O,components:re,directives:ie,filters:k}=t;if(u&&ti(u,i,null),s)for(let e in s){let t=s[e];g(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);y(t)&&(e.data=Pt(t))}if($r=!0,o)for(let e in o){let t=o[e],a=Z({get:g(t)?t.bind(n,n):g(t.get)?t.get.bind(n,n):r,set:!g(t)&&g(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)ri(c[e],i,n,e);if(l){let e=g(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{An(t,e[t])})}f&&ni(f,e,`c`);function A(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(A(Ar,p),A(jr,m),A(Mr,h),A(Nr,_),A(Sr,v),A(Cr,b),A(zr,te),A(Rr,ee),A(Lr,E),A(Pr,S),A(Fr,w),A(Ir,D),d(ne)){if(ne.length){let t=e.exposed||(e.exposed={});ne.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={})}T&&e.render===r&&(e.render=T),O!=null&&(e.inheritAttrs=O),re&&(e.components=re),ie&&(e.directives=ie),D&&pr(e)}function ti(e,t,n=r){d(e)&&(e=li(e));for(let n in e){let r=e[n],i;i=y(r)?`default`in r?V(r.from||n,r.default,!0):V(r.from||n):V(r),I(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function ni(e,t,n){R(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function ri(e,t,n,r){let i=r.includes(`.`)?Rn(n,r):()=>n[r];if(_(e)){let n=t[e];g(n)&&Fn(i,n)}else if(g(e))Fn(i,e.bind(n));else if(y(e)){if(d(e))e.forEach(e=>ri(e,t,n,r));else{let r=g(e.handler)?e.handler.bind(n):t[e.handler];g(r)&&Fn(i,r,e)}}}function ii(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>ai(c,e,o,!0)),ai(c,t,o)),y(t)&&a.set(t,c),c}function ai(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&ai(e,a,n,!0),i&&i.forEach(t=>ai(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=oi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var oi={data:si,props:di,emits:di,methods:ui,computed:ui,beforeCreate:W,created:W,beforeMount:W,mounted:W,beforeUpdate:W,updated:W,beforeDestroy:W,beforeUnmount:W,destroyed:W,unmounted:W,activated:W,deactivated:W,errorCaptured:W,serverPrefetch:W,components:ui,directives:ui,watch:fi,provide:si,inject:ci};function si(e,t){return t?e?function(){return s(g(e)?e.call(this,this):e,g(t)?t.call(this,this):t)}:t:e}function ci(e,t){return ui(li(e),li(t))}function li(e){if(d(e)){let t={};for(let n=0;n{let l,u=t,d;return Pn(()=>{let t=e[a];k(l,t)&&(l=t,c())}),{get(){return s(),r.get?r.get(l):l},set(e){let s=r.set?r.set(e):e;if(!k(s,l)&&!(u!==t&&k(e,u)))return;let f=i.vnode.props,p=!!(f&&(n in f||a in f||o in f)&&(`onUpdate:${n}`in f||`onUpdate:${a}`in f||`onUpdate:${o}`in f));p||(l=e,c()),i.emit(`update:${n}`,s),k(e,u)&&(k(e,s)&&!k(s,d)||p&&u!==t&&!k(s,l))&&c(),u=e,d=s}}});return c[Symbol.iterator]=()=>{let e=0;return{next(){return e<2?{value:e++?s||t:c,done:!1}:{done:!0}}}},c}var vi=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${D(t)}Modifiers`]||e[`${O(t)}Modifiers`];function yi(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&vi(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>_(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(D(n))];!l&&o&&(l=i[c=ie(O(n))]),l&&R(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,R(u,e,6,a)}}var bi=new WeakMap;function xi(e,t,n=!1){let r=n?bi:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!g(e)){let r=e=>{let n=xi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(y(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),y(e)&&r.set(e,o),o)}function Si(e,t){return!e||!a(t)?!1:(t=t.slice(2),t=t===`Once`?t:t.replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,O(t))||u(e,t))}function Ci(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=En(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Da(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Da(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:wi(c)}}catch(t){la.length=0,cn(t,e,1),v=J(K)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ti(y,a)),b=wa(b,y,!1,!0))}return n.dirs&&(b=wa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&ur(Vn(b.type)&&lr(b)||b,n.transition),v=b,En(_),v}var wi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Ei(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Di(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;tObject.create(Ai),Mi=e=>Object.getPrototypeOf(e)===Ai;function Ni(e,t,n,r=!1){let i={},a=ji();e.propsDefaults=Object.create(null),Fi(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);e.props=n?r?i:Ft(i):e.type.props?i:a,e.attrs=a}function Pi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{p=!0;let[t,n]=Ri(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return y(e)&&a.set(e,n),n;if(d(c))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Vi=e=>d(e)?e.map(Da):[Da(e)],Hi=(e,t,n)=>{if(t._n)return t;let r=Dn((...e)=>Vi(t(...e)),n);return r._c=!1,r},Ui=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Bi(n))continue;let i=e[n];if(g(i))t[n]=Hi(n,i,r);else if(i!=null){let e=Vi(i);t[n]=()=>e}}},Wi=(e,t)=>{let n=Vi(t);e.slots.default=()=>n},Gi=(e,t,n)=>{for(let r in t)(n||!Bi(r))&&(e[r]=t[r])},Ki=(e,t,n)=>{let r=e.slots=ji();if(e.vnode.shapeFlag&32){let e=t._;e?(Gi(r,t,n),n&&ae(r,`_`,e,!0)):Ui(t,r)}else t&&Wi(e,t)},qi=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:Gi(a,n,r):(o=!n.$stable,Ui(n,a)),s=n}else n&&(Wi(e,n),s={default:1});if(o)for(let e in a)!Bi(e)&&s[e]==null&&delete a[e]},G=aa;function Ji(e){return Yi(e)}function Yi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!va(e,t)&&(r=ve(e),pe(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case sa:y(e,t,n,r);break;case K:b(e,t,n,r);break;case ca:e==null&&x(t,n,r,o);break;case oa:re(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?ie(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,xe)}u!=null&&i?gr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&gr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)T(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),D(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},T=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&te(e.children,d,null,r,i,Xi(e,a),s,u),_&&kn(e,null,r,`created`),E(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!ee(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Y(f,r,e)}_&&kn(e,null,r,`beforeMount`);let v=Qi(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&G(()=>{try{f&&Y(f,r,e),v&&g.enter(d),_&&kn(e,null,r,`mounted`)}finally{}},i)},E=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t{for(let l=c;l{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&Zi(r,!1),(g=h.onVnodeBeforeUpdate)&&Y(g,r,n,e),f&&kn(n,e,r,`beforeUpdate`),r&&Zi(r,!0),d&&(!e.dynamicChildren||e.dynamicChildren.length!==d.length)&&(u=0,s=!1,d=null),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?ne(e.dynamicChildren,d,l,r,i,Xi(n,a),o):s||ce(e,n,l,null,r,i,Xi(n,a),o,!1),u>0){if(u&16)O(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t{g&&Y(g,r,n,e),f&&kn(n,e,r,`updated`)},i)},ne=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(n!==r){if(n!==t)for(let t in n)!ee(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(ee(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},re=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),te(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(ne(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&$i(e,t,!0)):ce(e,t,n,f,i,a,s,c,l)},ie=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):ae(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Na(e,r,i);if(yr(e)&&(s.ctx.renderer=xe),Va(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,oe,o),!e.el){let r=s.subTree=J(K);b(null,r,t,n),e.placeholder=r.el}}else oe(s,e,t,n,i,a,o)},ae=(e,t,n)=>{let r=t.component=e.component;if(Ei(e,t,n)){if(r.asyncDep&&!r.asyncResolved){se(r,t,n);return}r.next=t,r.update()}else t.el=e.el,r.vnode=t},oe=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=ta(e);if(n){t&&(t.el=c.el,se(e,t,o)),n.asyncDep.then(()=>{G(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;Zi(e,!1),t?(t.el=c.el,se(e,t,o)):t=c,n&&A(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Y(d,s,t,c),Zi(e,!0);let f=Ci(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ve(p),e,i,a),t.el=f.el,u===null&&ki(e,f.el),r&&G(r,i),(d=t.props&&t.props.onVnodeUpdated)&&G(()=>Y(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=vr(t);if(Zi(e,!1),l&&A(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Y(o,d,t),Zi(e,!0),s&&Ce){let t=()=>{e.subTree=Ci(e),Ce(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Ci(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&G(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;G(()=>Y(o,d,e),i)}(t.shapeFlag&256||d&&vr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&G(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Oe(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>vn(u),Zi(e,!0),l()},se=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Pi(e,t.props,r,n),qi(e,t.children,n),Ue(),xn(e),We()},ce=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){de(l,d,n,r,i,a,o,s,c);return}if(f&256){ue(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&_e(l,i,a),d!==l&&p(n,d)):u&16?m&16?de(l,d,n,r,i,a,o,s,c):_e(l,i,a,!0):(u&8&&p(n,``),m&16&&te(d,n,r,i,a,o,s,c))},ue=(e,t,r,i,a,o,s,c,l)=>{e=e||n,t=t||n;let u=e.length,d=t.length,f=Math.min(u,d),p=0;for(;pd?_e(e,a,o,!0,!1,f):te(t,r,i,a,o,s,c,l,f)},de=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?Oa(t[u]):Da(t[u]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?Oa(t[p]):Da(t[p]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=ep)for(;u<=f;)pe(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?Oa(t[u]):Da(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u=b){pe(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&va(n,t[_])){i=_;break}i===void 0?pe(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?ea(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){fe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,xe);return}if(c===oa){o(a,t,n);for(let e=0;el.enter(a),i));else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{let e=a._isLeaving||!!a[H];a._isLeaving&&a[H](!0),l.persisted&&!e?u():r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}}else o(a,t,n)},pe=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ue(),gr(s,null,n,e,!0),We()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!vr(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Y(_,t,e),u&6)ge(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&kn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,xe,r):l&&!l.hasOnce&&(a!==oa||d>0&&d&64)?_e(l,t,n,!1,!0):(a===oa&&d&384||!i&&u&16)&&_e(c,t,n),r&&me(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&G(()=>{_&&Y(_,t,e),h&&kn(e,null,t,`unmounted`),v&&(e.el=null)},n)},me=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===oa){he(n,r);return}if(t===ca){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},he=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},ge=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;na(c),na(l),r&&A(r),i.stop(),a&&(a.flags|=8,pe(o,e,t,n)),s&&G(s,t),G(()=>{e.isUnmounted=!0},t)},_e=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return ve(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Bn];return n?h(n):t},ye=!1,be=(e,t,n)=>{let r;e==null?t._vnode&&(pe(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ye||(ye=!0,xn(r),Sn(),ye=!1)},xe={p:v,um:pe,m:fe,r:me,mt:k,mc:te,pc:ce,pbc:ne,n:ve,o:e},Se,Ce;return i&&([Se,Ce]=i(xe)),{render:be,hydrate:Se,createApp:hi(be,Se)}}function Xi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function Zi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Qi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function $i(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-->0;)n[a]=o,o=t[o];return n}function ta(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ta(t)}function na(e){if(e)for(let t=0;te.__isSuspense;function aa(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):bn(e)}var oa=Symbol.for(`v-fgt`),sa=Symbol.for(`v-txt`),K=Symbol.for(`v-cmt`),ca=Symbol.for(`v-stc`),la=[],q=null;function ua(e=!1){la.push(q=e?null:[])}function da(){la.pop(),q=la[la.length-1]||null}var fa=1;function pa(e,t=!1){fa+=e,e<0&&q&&t&&(q.hasOnce=!0)}function ma(e){return e.dynamicChildren=fa>0?q||n:null,da(),fa>0&&q&&q.push(e),e}function ha(e,t,n,r,i,a){return ma(xa(e,t,n,r,i,a,!0))}function ga(e,t,n,r,i){return ma(J(e,t,n,r,i,!0))}function _a(e){return e?e.__v_isVNode===!0:!1}function va(e,t){return e.type===t.type&&e.key===t.key}var ya=({key:e})=>e==null?null:e,ba=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:_(e)||I(e)||g(e)?{i:B,r:e,k:t,f:!!n}:e);function xa(e,t=null,n=null,r=0,i=null,a=e===oa?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ya(t),ref:t&&ba(t),scopeId:Tn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:B};return s?(ka(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=_(n)?8:16),fa>0&&!o&&q&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&q.push(c),c}var J=Sa;function Sa(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Hr)&&(e=K),_a(e)){let r=wa(e,t,!0);return n&&ka(r,n),fa>0&&!a&&q&&(r.shapeFlag&6?q[q.indexOf(e)]=r:q.push(r)),r.patchFlag=-2,r}if(Ya(e)&&(e=e.__vccOpts),t){t=Ca(t);let{class:e,style:n}=t;e&&!_(e)&&(t.class=he(e)),y(n)&&(Bt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=_(e)?1:ia(e)?128:Vn(e)?64:y(e)?4:g(e)?2:0;return xa(e,t,n,r,i,o,a,!0)}function Ca(e){return e?Bt(e)||Mi(e)?s({},e):e:null}function wa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Aa(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ya(l),ref:t&&t.ref?n&&a?d(a)?a.concat(ba(t)):[a,ba(t)]:ba(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==oa?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&wa(e.ssContent),ssFallback:e.ssFallback&&wa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&ur(u,c.clone(u)),u}function Ta(e=` `,t=0){return J(sa,null,e,t)}function Ea(e=``,t=!1){return t?(ua(),ga(K,null,e)):J(K,null,e)}function Da(e){return e==null||typeof e==`boolean`?J(K):d(e)?J(oa,null,e.slice()):_a(e)?Oa(e):J(sa,null,String(e))}function Oa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:wa(e)}function ka(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`){if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),ka(e,n()),n._c&&(n._d=!0));return}{n=32;let r=t._;!r&&!Mi(t)?t._ctx=B:r===3&&B&&(B.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}}else if(g(t)){if(r&65){ka(e,{default:t});return}t={default:t,_ctx:B},n=32}else t=String(t),r&64?(n=16,t=[Ta(t)]):n=8;e.children=t,e.shapeFlag|=n}function Aa(...e){let t={};for(let n=0;nX||B,Fa,Ia;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Fa=t(`__VUE_INSTANCE_SETTERS__`,e=>X=e),Ia=t(`__VUE_SSR_SETTERS__`,e=>Ba=e)}var La=e=>{let t=X;return Fa(e),e.scope.on(),()=>{e.scope.off(),Fa(t)}},Ra=()=>{X&&X.scope.off(),Fa(null)};function za(e){return e.vnode.shapeFlag&4}var Ba=!1;function Va(e,t=!1,n=!1){t&&Ia(t);let{props:r,children:i}=e.vnode,a=za(e);Ni(e,r,a,t),Ki(e,i,n||t);let o=a?Ha(e,t):void 0;return t&&Ia(!1),o}function Ha(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zr);let{setup:r}=n;if(r){Ue();let n=e.setupContext=r.length>1?Ka(e):null,i=La(e),a=sn(r,e,0,[e.props,n]),o=b(a);if(We(),i(),(o||e.sp)&&!vr(e)&&pr(e),o){if(a.then(Ra,Ra),t)return a.then(n=>{Ia(!0);try{Ua(e,n,t)}finally{Ia(!1)}}).catch(t=>{cn(t,e,0)});e.asyncDep=a}else Ua(e,a,t)}else Wa(e,t)}function Ua(e,t,n){g(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:y(t)&&(e.setupState=Yt(t)),Wa(e,n)}function Wa(e,t,n){let i=e.type;e.render||(e.render=i.render||r);{let t=La(e);Ue();try{ei(e)}finally{We(),t()}}}var Ga={get(e,t){return N(e,`get`,``),e[t]}};function Ka(e){return{attrs:new Proxy(e.attrs,Ga),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function qa(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Vt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yr)return Yr[n](e)},has(e,t){return t in e||t in Yr}})):e.proxy}function Ja(e,t=!0){return g(e)?e.displayName||e.name:e.name||t&&e.__name}function Ya(e){return g(e)&&`__vccOpts`in e}var Z=(e,t)=>$t(e,t,Ba);function Xa(e,t,n){try{pa(-1);let r=arguments.length;return r===2?y(t)&&!d(t)?_a(t)?J(e,null,[t]):J(e,t):J(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_a(n)&&(n=[n]),J(e,t,n))}finally{pa(1)}}var Za=`3.5.41`,Qa=void 0,$a=typeof window<`u`&&window.trustedTypes;if($a)try{Qa=$a.createPolicy(`vue`,{createHTML:e=>e})}catch{}var eo=Qa?e=>Qa.createHTML(e):e=>e,to=`http://www.w3.org/2000/svg`,no=`http://www.w3.org/1998/Math/MathML`,ro=typeof document<`u`?document:null,io=ro&&ro.createElement(`template`),ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i=t===`svg`?ro.createElementNS(to,e):t===`mathml`?ro.createElementNS(no,e):n?ro.createElement(e,{is:n}):ro.createElement(e);return e===`select`&&r&&r.multiple!=null&&i.setAttribute(`multiple`,r.multiple),i},createText:e=>ro.createTextNode(e),createComment:e=>ro.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ro.querySelector(e),setScopeId(e,t){e.setAttribute(t,``)},insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;if(i&&(i===a||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==a&&(i=i.nextSibling););else{io.innerHTML=eo(r===`svg`?`${e}`:r===`mathml`?`${e}`:e);let i=io.content;if(r===`svg`||r===`mathml`){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},oo=`transition`,so=`animation`,co=Symbol(`_vtc`),lo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uo=s({},tr,lo),fo=(e=>(e.displayName=`Transition`,e.props=uo,e))((e,{slots:t})=>Xa(ar,ho(e),t)),po=(e,t=[])=>{d(e)?e.forEach(e=>e(...t)):e&&e(...t)},mo=e=>e?d(e)?e.some(e=>e.length>1):e.length>1:!1;function ho(e){let t={};for(let n in e)n in lo||(t[n]=e[n]);if(e.css===!1)return t;let{name:n=`v`,type:r,duration:i,enterFromClass:a=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=a,appearActiveClass:u=o,appearToClass:d=c,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,h=go(i),g=h&&h[0],_=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:C=v,onAppear:w=y,onAppearCancelled:T=b}=t,ee=(e,t,n,r)=>{e._enterCancelled=r,yo(e,t?d:c),yo(e,t?u:o),n&&n()},E=(e,t)=>{e._isLeaving=!1,yo(e,f),yo(e,m),yo(e,p),t&&t()},te=e=>(t,n)=>{let i=e?w:y,o=()=>ee(t,e,n);po(i,[t,o]),bo(()=>{yo(t,e?l:a),vo(t,e?d:c),mo(i)||So(t,r,g,o)})};return s(t,{onBeforeEnter(e){po(v,[e]),vo(e,a),vo(e,o)},onBeforeAppear(e){po(C,[e]),vo(e,l),vo(e,u)},onEnter:te(!1),onAppear:te(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>E(e,t);vo(e,f),e._enterCancelled?(vo(e,p),Eo(e)):(Eo(e),vo(e,p)),bo(()=>{e._isLeaving&&(yo(e,f),vo(e,m),mo(x)||So(e,r,_,n))}),po(x,[e,n])},onEnterCancelled(e){ee(e,!1,void 0,!0),po(b,[e])},onAppearCancelled(e){ee(e,!0,void 0,!0),po(T,[e])},onLeaveCancelled(e){E(e),po(S,[e])}})}function go(e){if(e==null)return null;if(y(e))return[_o(e.enter),_o(e.leave)];{let t=_o(e);return[t,t]}}function _o(e){return se(e)}function vo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[co]||(e[co]=new Set)).add(t)}function yo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[co];n&&(n.delete(t),n.size||(e[co]=void 0))}function bo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}var xo=0;function So(e,t,n,r){let i=e._endId=++xo,a=()=>{i===e._endId&&r()};if(n!=null)return setTimeout(a,n);let{type:o,timeout:s,propCount:c}=Co(e,t);if(!o)return r();let l=o+`end`,u=0,d=()=>{e.removeEventListener(l,f),a()},f=t=>{t.target===e&&++u>=c&&d()};setTimeout(()=>{u(n[e]||``).split(`, `),i=r(`${oo}Delay`),a=r(`${oo}Duration`),o=wo(i,a),s=r(`${so}Delay`),c=r(`${so}Duration`),l=wo(s,c),u=null,d=0,f=0;t===oo?o>0&&(u=oo,d=o,f=a.length):t===so?l>0&&(u=so,d=l,f=c.length):(d=Math.max(o,l),u=d>0?o>l?oo:so:null,f=u?u===oo?a.length:c.length:0);let p=u===oo&&/\b(?:transform|all)(?:,|$)/.test(r(`${oo}Property`).toString());return{type:u,timeout:d,propCount:f,hasTransform:p}}function wo(e,t){for(;e.lengthTo(t)+To(e[n])))}function To(e){return e===`auto`?0:Number(e.slice(0,-1).replace(`,`,`.`))*1e3}function Eo(e){return(e?e.ownerDocument:document).body.offsetHeight}function Do(e,t,n){let r=e[co];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==null?e.removeAttribute(`class`):n?e.setAttribute(`class`,t):e.className=t}var Oo=Symbol(`_vod`),ko=Symbol(`_vsh`),Ao={name:`show`,beforeMount(e,{value:t},{transition:n}){e[Oo]=e.style.display===`none`?``:e.style.display,n&&t?n.beforeEnter(e):jo(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),jo(e,!0),r.enter(e)):r.leave(e,()=>{jo(e,!1)}):jo(e,t))},beforeUnmount(e,{value:t}){jo(e,t)}};function jo(e,t){e.style.display=t?e[Oo]:`none`,e[ko]=!t}var Mo=Symbol(``),No=/(?:^|;)\s*display\s*:/;function Po(e,t,n){let r=e.style,i=_(n),a=!1;if(n&&!i){if(t){if(_(t))for(let e of t.split(`;`)){let t=e.slice(0,e.indexOf(`:`)).trim();n[t]==null&&Io(r,t,``)}else for(let e in t)n[e]==null&&Io(r,e,``)}for(let i in n){i===`display`&&(a=!0);let o=n[i];o==null?Io(r,i,``):Bo(e,i,!_(t)&&t?t[i]:void 0,o)||Io(r,i,o)}}else if(i){if(t!==n){let e=r[Mo];e&&(n+=`;`+e),r.cssText=n,a=No.test(n)}}else t&&e.removeAttribute(`style`);Oo in e&&(e[Oo]=a?r.display:``,e[ko]&&(r.display=`none`))}var Fo=/\s*!important$/;function Io(e,t,n){if(d(n))n.forEach(n=>Io(e,t,n));else if(n==null&&(n=``),t.startsWith(`--`))e.setProperty(t,n);else{let r=zo(e,t);Fo.test(n)?e.setProperty(O(r),n.replace(Fo,``),`important`):e[r]=n}}var Lo=[`Webkit`,`Moz`,`ms`],Ro={};function zo(e,t){let n=Ro[t];if(n)return n;let r=D(t);if(r!==`filter`&&r in e)return Ro[t]=r;r=re(r);for(let n=0;nZo||(Qo.then(()=>Zo=0),Zo=Date.now());function es(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=n.attached)return;let r=n.value;if(d(r)){let n=e.stopImmediatePropagation;e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0};let i=r.slice(),a=[e];for(let n=0;ne.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ns=(e,t,n,r,i,s)=>{let c=i===`svg`;t===`class`?Do(e,r,c):t===`style`?Po(e,n,r):a(t)?o(t)||qo(e,t,n,r,s):(t[0]===`.`?(t=t.slice(1),1):t[0]===`^`?(t=t.slice(1),0):rs(e,t,r,c))?(Uo(e,t,r),!e.tagName.includes(`-`)&&(t===`value`||t===`checked`||t===`selected`)&&Ho(e,t,r,c,s,t!==`value`)):e._isVueCE&&(is(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!_(r)))?Uo(e,D(t),r,s,t):(t===`true-value`?e._trueValue=r:t===`false-value`&&(e._falseValue=r),Ho(e,t,r,c))};function rs(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t in e&&ts(t)&&g(n));if(t===`spellcheck`||t===`draggable`||t===`translate`||t===`autocorrect`||t===`sandbox`&&e.tagName===`IFRAME`||t===`form`||t===`list`&&e.tagName===`INPUT`||t===`type`&&e.tagName===`TEXTAREA`)return!1;if(t===`width`||t===`height`){let t=e.tagName;if(t===`IMG`||t===`VIDEO`||t===`CANVAS`||t===`SOURCE`)return!1}return ts(t)&&_(n)?!1:t in e}function is(e,t){let n=e._def.props;if(!n)return!1;let r=D(t);return Array.isArray(n)?n.some(e=>D(e)===r):Object.keys(n).some(e=>D(e)===r)}var as={esc:`escape`,space:` `,up:`arrow-up`,left:`arrow-left`,right:`arrow-right`,down:`arrow-down`,delete:`backspace`},os=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(`.`);return n[r]||(n[r]=(n=>{if(!(`key`in n))return;let r=O(n.key);if(t.some(e=>e===r||as[e]===r))return e(n)}))},ss=s({patchProp:ns},ao),cs;function ls(){return cs||(cs=Ji(ss))}var us=((...e)=>{let t=ls().createApp(...e),{mount:n}=t;return t.mount=e=>{let r=fs(e);if(!r)return;let i=t._component;!g(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent=``);let a=n(r,!1,ds(r));return r instanceof Element&&(r.removeAttribute(`v-cloak`),r.setAttribute(`data-v-app`,``)),a},t});function ds(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLElement==`function`&&e instanceof MathMLElement)return`mathml`}function fs(e){return _(e)?document.querySelector(e):e}function ps(){}var ms=Object.assign,hs=typeof window<`u`,gs=e=>typeof e==`object`&&!!e,_s=e=>e!=null,vs=e=>typeof e==`function`,ys=e=>gs(e)&&vs(e.then)&&vs(e.catch),bs=e=>typeof e==`number`||/^\d+(\.\d+)?$/.test(e),xs=()=>hs?/ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()):!1;function Ss(e,t){let n=t.split(`.`),r=e;return n.forEach(e=>{var t;r=gs(r)?(t=r[e])==null?``:t:``}),r}function Cs(e,t,n){return t.reduce((t,r)=>((!n||e[r]!==void 0)&&(t[r]=e[r]),t),{})}var ws=e=>Array.isArray(e)?e:[e],Ts=[Number,String],Es={type:Boolean,default:!0},Ds=e=>({type:e,required:!0}),Os=e=>({type:Number,default:e}),ks=e=>({type:Ts,default:e}),As=e=>({type:String,default:e}),js=typeof window<`u`;function Ms(e){return js?requestAnimationFrame(e):-1}function Ns(e){js&&cancelAnimationFrame(e)}function Ps(e){Ms(()=>Ms(e))}var Fs=e=>e===window,Is=(e,t)=>({top:0,left:0,right:e,bottom:t,width:e,height:t}),Ls=e=>{let t=L(e);if(Fs(t)){let e=t.innerWidth,n=t.innerHeight;return Is(e,n)}return t!=null&&t.getBoundingClientRect?t.getBoundingClientRect():Is(0,0)};function Rs(e){let t=V(e,null);if(t){let e=Pa(),{link:n,unlink:r,internalChildren:i}=t;return n(e),Fr(()=>r(e)),{parent:t,index:Z(()=>i.indexOf(e))}}return{parent:null,index:Wt(-1)}}function zs(e){let t=[],n=e=>{Array.isArray(e)&&e.forEach(e=>{var r;_a(e)&&(t.push(e),(r=e.component)!=null&&r.subTree&&(t.push(e.component.subTree),n(e.component.subTree.children)),e.children&&n(e.children))})};return n(e),t}var Bs=(e,t)=>{let n=e.indexOf(t);return n===-1?e.findIndex(e=>t.key!==void 0&&t.key!==null&&e.type===t.type&&e.key===t.key):n};function Vs(e,t,n){let r=zs(e.subTree.children);n.sort((e,t)=>Bs(r,e.vnode)-Bs(r,t.vnode));let i=n.map(e=>e.proxy);t.sort((e,t)=>i.indexOf(e)-i.indexOf(t))}function Hs(e){let t=Pt([]),n=Pt([]),r=Pa();return{children:t,linkChildren:i=>{An(e,Object.assign({link:e=>{e.proxy&&(n.push(e),t.push(e.proxy),Vs(r,t,n))},unlink:e=>{let r=n.indexOf(e);t.splice(r,1),n.splice(r,1)},children:t,internalChildren:n},i))}}}function Us(e){let t;jr(()=>{e(),gn(()=>{t=!0})}),Sr(()=>{t&&e()})}function Ws(e,t,n={}){if(!js)return;let{target:r=window,passive:i=!1,capture:a=!1}=n,o=!1,s,c=n=>{if(o)return;let r=L(n);r&&!s&&(r.addEventListener(e,t,{capture:a,passive:i}),s=!0)},l=n=>{if(o)return;let r=L(n);r&&s&&(r.removeEventListener(e,t,a),s=!1)};Fr(()=>l(r)),Cr(()=>l(r)),Us(()=>c(r));let u;return I(r)&&(u=Fn(r,(e,t)=>{l(t),c(e)})),()=>{u==null||u(),l(r),o=!0}}var Gs,Ks;function qs(){if(!Gs&&(Gs=Wt(0),Ks=Wt(0),js)){let e=()=>{Gs.value=window.innerWidth,Ks.value=window.innerHeight};e(),window.addEventListener(`resize`,e,{passive:!0}),window.addEventListener(`orientationchange`,e,{passive:!0})}return{width:Gs,height:Ks}}var Js=/scroll|auto|overlay/i,Ys=js?window:void 0;function Xs(e){return e.tagName!==`HTML`&&e.tagName!==`BODY`&&e.nodeType===1}function Zs(e,t=Ys){let n=e;for(;n&&n!==t&&Xs(n);){let{overflowY:e}=window.getComputedStyle(n);if(Js.test(e))return n;n=n.parentNode}return t}function Qs(e,t=Ys){let n=Wt();return jr(()=>{e.value&&(n.value=Zs(e.value,t))}),n}var $s;function ec(){if(!$s&&($s=Wt(`visible`),js)){let e=()=>{$s.value=document.hidden?`hidden`:`visible`};e(),window.addEventListener(`visibilitychange`,e)}return $s}var tc=Symbol(`van-field`);function nc(e){let t=`scrollTop`in e?e.scrollTop:e.pageYOffset;return Math.max(t,0)}function rc(e,t){`scrollTop`in e?e.scrollTop=t:e.scrollTo(e.scrollX,t)}function ic(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}function ac(e){rc(window,e),rc(document.body,e)}function oc(e,t){if(e===window)return 0;let n=t?nc(t):ic();return Ls(e).top+n}var sc=xs();function cc(){sc&&ac(ic())}var lc=e=>e.stopPropagation();function uc(e,t){(typeof e.cancelable!=`boolean`||e.cancelable)&&e.preventDefault(),t&&lc(e)}function dc(e){let t=L(e);if(!t)return!1;let n=window.getComputedStyle(t),r=n.display===`none`,i=t.offsetParent===null&&n.position!==`fixed`;return r||i}var{width:fc,height:pc}=qs();function mc(e){if(_s(e))return bs(e)?`${e}px`:String(e)}function hc(e){if(_s(e)){if(Array.isArray(e))return{width:mc(e[0]),height:mc(e[1])};let t=mc(e);return{width:t,height:t}}}function gc(e){let t={};return e!==void 0&&(t.zIndex=+e),t}var _c;function vc(){if(!_c){let e=document.documentElement,t=e.style.fontSize||window.getComputedStyle(e).fontSize;_c=parseFloat(t)}return _c}function yc(e){return e=e.replace(/rem/g,``),+e*vc()}function bc(e){return e=e.replace(/vw/g,``),+e*fc.value/100}function xc(e){return e=e.replace(/vh/g,``),+e*pc.value/100}function Sc(e){if(typeof e==`number`)return e;if(hs){if(e.includes(`rem`))return yc(e);if(e.includes(`vw`))return bc(e);if(e.includes(`vh`))return xc(e)}return parseFloat(e)}var Cc=/-(\w)/g,wc=e=>e.replace(Cc,(e,t)=>t.toUpperCase()),Tc=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase().replace(/^-/,``),Ec=(e,t,n)=>Math.min(Math.max(e,t),n);function Dc(e,t,n){let r=e.indexOf(t);return r===-1?e:t===`-`&&r!==0?e.slice(0,r):e.slice(0,r+1)+e.slice(r).replace(n,``)}function Oc(e,t=!0,n=!0){e=t?Dc(e,`.`,/\./g):e.split(`.`)[0],e=n?Dc(e,`-`,/-/g):e.replace(/-/,``);let r=t?/[^-0-9.]/g:/[^-0-9]/g;return e.replace(r,``)}var{hasOwnProperty:kc}=Object.prototype;function Ac(e,t,n){let r=t[n];_s(r)&&(e[n]=!kc.call(e,n)||!gs(r)?r:jc(Object(e[n]),r))}function jc(e,t){return Object.keys(t).forEach(n=>{Ac(e,t,n)}),e}var Mc={name:`姓名`,tel:`电话`,save:`保存`,clear:`清空`,undo:`撤销`,cancel:`取消`,confirm:`确认`,delete:`删除`,loading:`加载中...`,noCoupon:`暂无优惠券`,nameEmpty:`请填写姓名`,addContact:`添加联系人`,telInvalid:`请填写正确的电话`,vanCalendar:{end:`结束`,start:`开始`,title:`日期选择`,weekdays:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],monthTitle:(e,t)=>`${e}\u5E74${t}\u6708`,rangePrompt:e=>`\u6700\u591A\u9009\u62E9 ${e} \u5929`},vanCascader:{select:`请选择`},vanPagination:{prev:`上一页`,next:`下一页`},vanPullRefresh:{pulling:`下拉即可刷新...`,loosing:`释放即可刷新...`},vanSubmitBar:{label:`合计:`},vanCoupon:{unlimited:`无门槛`,discount:e=>`${e}\u6298`,condition:e=>`\u6EE1${e}\u5143\u53EF\u7528`},vanCouponCell:{title:`优惠券`,count:e=>`${e}\u5F20\u53EF\u7528`},vanCouponList:{exchange:`兑换`,close:`不使用`,enable:`可用`,disabled:`不可用`,placeholder:`输入优惠码`},vanAddressEdit:{area:`地区`,areaEmpty:`请选择地区`,addressEmpty:`请填写详细地址`,addressDetail:`详细地址`,defaultAddress:`设为默认收货地址`},vanAddressList:{add:`新增地址`}},Nc=Wt(`zh-CN`),Pc=Pt({"zh-CN":Mc}),Fc={messages(){return Pc[Nc.value]},use(e,t){Nc.value=e,this.add({[e]:t})},add(e={}){jc(Pc,e)}};function Ic(e){let t=wc(e)+`.`;return(e,...n)=>{let r=Fc.messages(),i=Ss(r,t+e)||Ss(r,e);return vs(i)?i(...n):i}}function Lc(e,t){return t?typeof t==`string`?` ${e}--${t}`:Array.isArray(t)?t.reduce((t,n)=>t+Lc(e,n),``):Object.keys(t).reduce((n,r)=>n+(t[r]?Lc(e,r):``),``):``}function Rc(e){return(t,n)=>(t&&typeof t!=`string`&&(n=t,t=``),t=t?`${e}__${t}`:e,`${t}${Lc(t,n)}`)}function zc(e){let t=`van-${e}`;return[t,Rc(t),Ic(t)]}function Bc(e){return e.install=t=>{let{name:n}=e;n&&(t.component(n,e),t.component(wc(`-${n}`),e))},e}var Vc=2e3,Hc=()=>++Vc,Uc=e=>{Vc=e},[Wc,Gc]=zc(`config-provider`),Kc=Symbol(Wc),qc={tag:As(`div`),theme:As(`light`),zIndex:Number,themeVars:Object,themeVarsDark:Object,themeVarsLight:Object,themeVarsScope:As(`local`),iconPrefix:String};function Jc(e){return e.replace(/([a-zA-Z])(\d)/g,`$1-$2`)}function Yc(e){let t={};return Object.keys(e).forEach(n=>{let r=Jc(Tc(n));t[`--van-${r}`]=e[n]}),t}function Xc(e={},t={}){Object.keys(e).forEach(n=>{e[n]!==t[n]&&document.documentElement.style.setProperty(n,e[n])}),Object.keys(t).forEach(t=>{e[t]||document.documentElement.style.removeProperty(t)})}var Zc=Bc(fr({name:Wc,props:qc,setup(e,{slots:t}){let n=Z(()=>Yc(ms({},e.themeVars,e.theme===`dark`?e.themeVarsDark:e.themeVarsLight)));if(hs){let t=()=>{document.documentElement.classList.add(`van-theme-${e.theme}`)},r=(t=e.theme)=>{document.documentElement.classList.remove(`van-theme-${t}`)};Fn(()=>e.theme,(e,n)=>{n&&r(n),t()},{immediate:!0}),Sr(t),Cr(r),Pr(r),Fn(n,(t,n)=>{e.themeVarsScope===`global`&&Xc(t,n)}),Fn(()=>e.themeVarsScope,(e,t)=>{t===`global`&&Xc({},n.value),e===`global`&&Xc(n.value,{})}),e.themeVarsScope===`global`&&Xc(n.value,{})}return An(Kc,e),Nn(()=>{e.zIndex!==void 0&&Uc(e.zIndex)}),()=>J(e.tag,{class:Gc(),style:e.themeVarsScope===`local`?n.value:void 0},{default:()=>{var e;return[(e=t.default)==null?void 0:e.call(t)]}})}}));function Qc(e=`(min-width: 768px)`){let t=typeof window<`u`&&window.matchMedia?window.matchMedia(e):null,n=Wt(t?t.matches:!1),r=e=>{n.value=e.matches};return jr(()=>{var e;return t==null||(e=t.addEventListener)==null?void 0:e.call(t,`change`,r)}),Pr(()=>{var e;return t==null||(e=t.removeEventListener)==null?void 0:e.call(t,`change`,r)}),n}function $c(){let e=typeof window<`u`&&window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,t=Wt(e?e.matches:!1),n=e=>{t.value=e.matches};return jr(()=>{var t;return e==null||(t=e.addEventListener)==null?void 0:t.call(e,`change`,n)}),Pr(()=>{var t;return e==null||(t=e.removeEventListener)==null?void 0:t.call(e,`change`,n)}),t}function el(e,t){let n,r=!1,i=()=>{n!==void 0&&window.clearInterval(n),n=void 0},a=()=>{i(),n=window.setInterval(()=>{document.visibilityState===`visible`&&e()},t)},o=()=>{r&&(document.visibilityState===`visible`?(e(),a()):i())};jr(()=>{r=!0,a(),document.addEventListener(`visibilitychange`,o)}),Sr(()=>{r||(r=!0,e(),a())}),Cr(()=>{r=!1,i()}),Pr(()=>{r=!1,i(),document.removeEventListener(`visibilitychange`,o)})}var tl=fr({__name:`App`,setup(e){let t=$c(),n=Z(()=>t.value?`dark`:`light`),r={primaryColor:`#2f6fff`,successColor:`#13b77a`,warningColor:`#e8870c`,dangerColor:`#ff5358`,navBarBackground:`var(--app-bg)`,navBarTitleTextColor:`var(--app-text)`,cellGroupInsetRadius:`14px`,buttonRadius:`12px`,buttonLargeHeight:`48px`},i={primaryColor:`#5b8cff`,successColor:`#2fd19a`,warningColor:`#ffae4a`,dangerColor:`#ff6b70`,background:`#0b1222`,background2:`#131c31`,background3:`#18223b`,textColor:`#e6ecfb`,textColor2:`#8b97b6`,textColor3:`#6c7898`,borderColor:`rgba(140,160,210,.16)`,activeColor:`#1c2743`};return(e,t)=>{let a=Vr(`RouterView`);return ua(),ga(L(Zc),{theme:n.value,"theme-vars-scope":`global`,"theme-vars":r,"theme-vars-dark":i},{default:Dn(()=>[J(a)]),_:1},8,[`theme`])}}});function nl(e){"@babel/helpers - typeof";return nl=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},nl(e)}function rl(e,t){if(nl(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(nl(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function il(e){var t=rl(e,`string`);return nl(t)==`symbol`?t:t+``}function al(e,t,n){return(t=il(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ol(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function sl(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&ol(e.default)}var Q=Object.assign;function cl(e,t){let n={};for(let r in t){let i=t[r];n[r]=ul(i)?i.map(e):e(i)}return n}var ll=()=>{},ul=Array.isArray;function dl(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var fl=Symbol(``);function pl(e,t){return Q(Error(),{type:e,[fl]:!0},t)}function ml(e,t){return e instanceof Error&&fl in e&&(t==null||!!(e.type&t))}var hl=Symbol(``),gl=Symbol(``),_l=Symbol(``),vl=Symbol(``),yl=Symbol(``);function bl(){return V(_l)}function xl(e){return V(vl)}var Sl=typeof document<`u`,Cl=/#/g,wl=/&/g,Tl=/\//g,El=/=/g,Dl=/\?/g,Ol=/\+/g,kl=/%5B/g,Al=/%5D/g,jl=/%5E/g,Ml=/%60/g,Nl=/%7B/g,Pl=/%7C/g,Fl=/%7D/g,Il=/%20/g;function Ll(e){return e==null?``:encodeURI(``+e).replace(Pl,`|`).replace(kl,`[`).replace(Al,`]`)}function Rl(e){return Ll(e).replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function zl(e){return Ll(e).replace(Ol,`%2B`).replace(Il,`+`).replace(Cl,`%23`).replace(wl,`%26`).replace(Ml,"`").replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function Bl(e){return zl(e).replace(El,`%3D`)}function Vl(e){return Ll(e).replace(Cl,`%23`).replace(Dl,`%3F`)}function Hl(e){return Vl(e).replace(Tl,`%2F`)}function Ul(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var Wl=/\/$/,Gl=e=>e.replace(Wl,``);function Kl(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r=r||t.slice(0,s),o=t.slice(s,t.length)),r=eu(r==null?t:r,n),{fullPath:r+a+o,path:r,query:i,hash:Ul(o)}}function ql(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Jl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function Yl(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&Xl(t.matched[r],n.matched[i])&&Zl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Xl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Zl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ql(e[n],t[n]))return!1;return!0}function Ql(e,t){return ul(e)?$l(e,t):ul(t)?$l(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function $l(e,t){return ul(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function eu(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o1&&a--;else break}return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var tu={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0};function nu(e){if(!e){if(Sl){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^/]+/,``)}else e=`/`}return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Gl(e)}var ru=/^[^#]+#/;function iu(e,t){return e.replace(ru,`#`)+t}function au(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var ou=()=>({left:window.scrollX,top:window.scrollY});function su(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=au(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function cu(e,t){return(history.state?history.state.position-t:-1)+e}var lu=new Map;function uu(e,t){lu.set(e,t)}function du(e){let t=lu.get(e);return lu.delete(e),t}function fu(e){return typeof e==`string`||e&&typeof e==`object`}function pu(e){return typeof e==`string`||typeof e==`symbol`}function mu(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;ee&&zl(e)):[r&&zl(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function gu(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=ul(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function _u(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function vu(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(pl(4,{from:n,to:t})):e instanceof Error?c(e):fu(e)?c(pl(2,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function yu(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(t===`beforeRouteEnter`||o.instances[e]){if(ol(s)){let c=(s.__vccOpts||s)[t];c&&a.push(vu(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=sl(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&vu(c,n,r,o,e,i)()}))}}}return a}function bu(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;oXl(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>Xl(e,s))||i.push(s))}return[n,r,i]}var xu=()=>location.protocol+`//`+location.host;function Su(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Jl(n,``)}return Jl(n,e)+r+i}function Cu(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=Su(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:`pop`,direction:u?u>0?`forward`:`back`:``})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(Q({},e.state,{scroll:ou()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function wu(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ou():null}}function Tu(e){let{history:t,location:n}=window,r={value:Su(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:xu()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,Q({},t.state,wu(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=Q({},i.value,t.state,{forward:e,scroll:ou()});a(o.current,o,!0),a(e,Q({},wu(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function Eu(e){e=nu(e);let t=Tu(e),n=Cu(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=Q({location:``,base:e,go:r,createHref:iu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Du(e){return e=location.host?e||location.pathname+location.search:``,e.includes(`#`)||(e+=`#`),Eu(e)}var Ou={type:0,value:``},ku=/[a-zA-Z0-9_]/;function Au(e){if(!e)return[[]];if(e===`/`)return[[Ou]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=0,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&(n===0?a.push({type:0,value:l}):n===1||n===2||n===3?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:1,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),l=``)}function f(){l+=c}for(;st.length?t.length===1&&t[0]===80?1:-1:0}function Iu(e,t){let n=0,r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}var Ru={strict:!1,end:!0,sensitive:!1};function zu(e,t,n){let r=Q(Pu(Au(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Bu(e,t){let n=[],r=new Map;t=dl(Ru,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Hu(e);s.aliasOf=r&&r.record;let l=dl(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Hu(Q({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=zu(t,n,l),r?r.alias.push(d):(f=f||d,f!==d&&f.alias.push(d),i&&e.name&&!Wu(d)&&o(e.name)),Ju(d)&&c(d),s.children){let e=s.children;for(let t=0;t{o(f)}:ll}function o(e){if(pu(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=Ku(e,n);n.splice(t,0,e),e.record.name&&!Wu(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw pl(1,{location:e});s=i.record.name,a=Q(Vu(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&Vu(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name,i.keys.forEach(e=>{e.optional&&!a[e.name]&&delete a[e.name]}));else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw pl(1,{location:e,currentLocation:t});s=i.record.name,a=Q({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Gu(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function Vu(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Hu(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Uu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Uu(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Wu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Gu(e){return e.reduce((e,t)=>Q(e,t.meta),{})}function Ku(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Iu(e,t[i])<0?r=i:n=i+1}let i=qu(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function qu(e){let t=e;for(;t=t.parent;)if(Ju(t)&&Iu(e,t)===0)return t}function Ju({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Yu(e){let t=V(_l),n=V(vl),r=Z(()=>{let n=L(e.to);return t.resolve(n)}),i=Z(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(Xl.bind(null,i));if(o>-1)return o;let s=ed(e[t-2]);return t>1&&ed(i)===s&&a[a.length-1].path!==s?a.findIndex(Xl.bind(null,e[t-2])):o}),a=Z(()=>i.value>-1&&$u(n.params,r.value.params)),o=Z(()=>i.value>-1&&i.value===n.matched.length-1&&Zl(n.params,r.value.params));function s(n={}){if(Qu(n)){let n=t[L(e.replace)?`replace`:`push`](L(e.to)).catch(ll);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:Z(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Xu(e){return e.length===1?e[0]:e}var Zu=fr({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:Yu,setup(e,{slots:t}){let n=Pt(Yu(e)),{options:r}=V(_l),i=Z(()=>({[td(e.activeClass,r.linkActiveClass,`router-link-active`)]:n.isActive,[td(e.exactActiveClass,r.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let r=t.default&&Xu(t.default(n));return e.custom?r:Xa(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}});function Qu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(e.button===void 0||e.button===0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function $u(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!ul(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function ed(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var td=(e,t,n)=>e==null?t==null?n:t:e,nd=fr({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let r=V(yl),i=Z(()=>e.route||r.value),a=V(gl,0),o=Z(()=>{let e=L(a),{matched:t}=i.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),s=Z(()=>i.value.matched[o.value]);An(gl,Z(()=>o.value+1)),An(hl,s),An(yl,i);let c=Wt();return Fn(()=>[c.value,s.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!Xl(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let r=i.value,a=e.name,o=s.value,l=o&&o.components[a];if(!l)return rd(n.default,{Component:l,route:r});let u=o.props[a],d=Xa(l,Q({},u?u===!0?r.params:typeof u==`function`?u(r):u:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(o.instances[a]=null)},ref:c}));return rd(n.default,{Component:d,route:r})||d}}});function rd(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var id=nd;function ad(e){let t=Bu(e.routes,e),n=e.parseQuery||mu,r=e.stringifyQuery||hu,i=e.history,a=_u(),o=_u(),s=_u(),c=Gt(tu),l=tu;Sl&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let u=cl.bind(null,e=>``+e),d=cl.bind(null,Hl),f=cl.bind(null,Ul);function p(e,n){let r,i;return pu(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function m(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function h(){return t.getRoutes().map(e=>e.record)}function g(e){return!!t.getRecordMatcher(e)}function _(e,a){if(a=Q({},a||c.value),typeof e==`string`){let r=Kl(n,e,a.path),o=t.resolve({path:r.path},a),s=i.createHref(r.fullPath);return Q(r,o,{params:f(o.params),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=Q({},e,{path:Kl(n,e.path,a.path).path});else{let t=Q({},e.params);for(let e in t)t[e]==null&&delete t[e];o=Q({},e,{params:d(t)}),a.params=d(a.params)}let s=t.resolve(o,a),l=e.hash||``;s.params=u(f(s.params));let p=ql(r,Q({},e,{hash:Rl(l),path:s.path})),m=i.createHref(p);return Q({fullPath:p,hash:l,query:r===hu?gu(e.query):e.query||{}},s,{redirectedFrom:void 0,href:m})}function v(e){return typeof e==`string`?Kl(n,e,c.value.path):Q({},e)}function y(e,t){if(l!==e)return pl(8,{from:t,to:e})}function b(e){return C(e)}function x(e){return b(Q(v(e),{replace:!0}))}function S(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=v(i):{path:i},i.params={}),Q({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function C(e,t){let n=l=_(e),i=c.value,a=e.state,o=e.force,s=e.replace===!0,u=S(n,i);if(u)return C(Q(v(u),{state:typeof u==`object`?Q({},a,u.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&Yl(r,i,n)&&(f=pl(16,{to:d,from:i}),oe(i,i,!0,!1)),(f?Promise.resolve(f):ee(d,i)).catch(e=>ml(e)?ml(e,2)?e:ae(e):k(e,d,i)).then(e=>{if(e){if(ml(e,2))return C(Q({replace:s},v(e.to),{state:typeof e.to==`object`?Q({},a,e.to.state):a,force:o}),t||d)}else e=te(d,i,!0,s,a);return E(d,i,e),e})}function w(e,t){let n=y(e,t);return n?Promise.reject(n):Promise.resolve()}function T(e){let t=le.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function ee(e,t){let n,[r,i,s]=bu(e,t);n=yu(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(vu(r,e,t))});let c=w.bind(null,e,t);return n.push(c),de(n).then(()=>{n=[];for(let r of a.list())n.push(vu(r,e,t));return n.push(c),de(n)}).then(()=>{n=yu(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(vu(r,e,t))});return n.push(c),de(n)}).then(()=>{n=[];for(let r of s)if(r.beforeEnter){if(ul(r.beforeEnter))for(let i of r.beforeEnter)n.push(vu(i,e,t));else n.push(vu(r.beforeEnter,e,t))}return n.push(c),de(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=yu(s,`beforeRouteEnter`,e,t,T),n.push(c),de(n))).then(()=>{n=[];for(let r of o.list())n.push(vu(r,e,t));return n.push(c),de(n)}).catch(e=>ml(e,8)?e:Promise.reject(e))}function E(e,t,n){s.list().forEach(r=>T(()=>r(e,t,n)))}function te(e,t,n,r,a){let o=y(e,t);if(o)return o;let s=t===tu,l=Sl?history.state:{};n&&(r||s?i.replace(e.fullPath,Q({scroll:s&&l&&l.scroll},a)):i.push(e.fullPath,a)),c.value=e,oe(e,t,n,s),ae()}let D;function ne(){D||(D=i.listen((e,t,n)=>{if(!ue.listening)return;let r=_(e),a=S(r,ue.currentRoute.value);if(a){C(Q(a,{replace:!0,force:!0}),r).catch(ll);return}l=r;let o=c.value;Sl&&uu(cu(o.fullPath,n.delta),ou()),ee(r,o).catch(e=>ml(e,12)?e:ml(e,2)?(C(Q(v(e.to),{force:!0}),r).then(e=>{ml(e,20)&&!n.delta&&n.type===`pop`&&i.go(-1,!1)}).catch(ll),Promise.reject()):(n.delta&&i.go(-n.delta,!1),k(e,r,o))).then(e=>{e=e||te(r,o,!1),e&&(n.delta&&!ml(e,8)?i.go(-n.delta,!1):n.type===`pop`&&ml(e,20)&&i.go(-1,!1)),E(r,o,e)}).catch(ll)}))}let O=_u(),re=_u(),ie;function k(e,t,n){ae(e);let r=re.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function A(){return ie&&c.value!==tu?Promise.resolve():new Promise((e,t)=>{O.add([e,t])})}function ae(e){return ie||(ie=!e,ne(),O.list().forEach(([t,n])=>e?n(e):t()),O.reset()),e}function oe(t,n,r,i){let{scrollBehavior:a}=e;if(!Sl||!a)return Promise.resolve();let o=!r&&du(cu(t.fullPath,0))||(i||!r)&&history.state&&history.state.scroll||null;return gn().then(()=>a(t,n,o)).then(e=>t===c.value&&e&&su(e)).catch(e=>t===c.value&&k(e,t,n))}let se=e=>i.go(e),ce,le=new Set,ue={currentRoute:c,listening:!0,addRoute:p,removeRoute:m,clearRoutes:t.clearRoutes,hasRoute:g,getRoutes:h,resolve:_,options:e,push:b,replace:x,go:se,back:()=>se(-1),forward:()=>se(1),beforeEach:a.add,beforeResolve:o.add,afterEach:s.add,onError:re.add,isReady:A,install(e){e.component(`RouterLink`,Zu),e.component(`RouterView`,id),e.config.globalProperties.$router=ue,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>L(c)}),Sl&&!ce&&c.value===tu&&(ce=!0,b(i.location).catch(e=>{}));let t={};for(let e in tu)Object.defineProperty(t,e,{get:()=>c.value[e],enumerable:!0});e.provide(_l,ue),e.provide(vl,Ft(t)),e.provide(yl,c);let n=e.unmount;le.add(e),e.unmount=function(){le.delete(e),le.size<1&&(l=tu,D&&D(),D=null,c.value=tu,ce=!1,ie=!1),n()}}};function de(e){return e.reduce((e,t)=>e.then(()=>T(t)),Promise.resolve())}return ue}var od=`/api/v2/mobile`,sd=`wecom-mobile-token`,cd=class extends Error{constructor(e,t,n,r=0){super(n),al(this,`status`,void 0),al(this,`code`,void 0),al(this,`retryAfter`,void 0),this.status=e,this.code=t,this.retryAfter=r}},ld=null;function ud(e){ld=e}function dd(){try{return window.localStorage.getItem(sd)||``}catch{return``}}function fd(e){try{e?window.localStorage.setItem(sd,e):window.localStorage.removeItem(sd)}catch{}}var pd=``;function md(){return pd||dd()}function hd(e){pd=e,fd(e)}function gd(e,t){let n=t&&typeof t==`object`&&`detail`in t?t.detail:t;if(n&&typeof n==`object`&&!Array.isArray(n)){let t=n;return new cd(e,String(t.code||`http_${e}`),String(t.message||_d(e)),Number(t.retry_after||0))}return Array.isArray(n)?new cd(e,`validation`,`填写的内容不符合要求,请检查后重试`):typeof n==`string`&&n.trim()?new cd(e,`http_${e}`,n.trim()):new cd(e,`http_${e}`,_d(e))}function _d(e){return e===401?`登录已失效,请重新登录`:e===403?`没有权限执行这个操作`:e===404?`内容不存在或已被删除`:e===429?`操作太频繁,请稍后再试`:e>=500?`服务暂时不可用,请稍后重试`:`请求失败(HTTP ${e})`}async function vd(e,t,n={}){let{body:r,query:i,auth:a=!0,signal:o}=n,s=new URLSearchParams;for(let[e,t]of Object.entries(i||{}))t!=null&&t!==``&&s.set(e,String(t));let c=`${od}${t}${s.toString()?`?${s}`:``}`,l={Accept:`application/json`};r!==void 0&&(l[`Content-Type`]=`application/json`);let u=md();a&&u&&(l.Authorization=`Bearer ${u}`);let d;try{d=await fetch(c,{method:e,headers:l,body:r===void 0?void 0:JSON.stringify(r),signal:o,cache:`no-store`,credentials:`omit`})}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?e:new cd(0,`network`,`网络连接失败,请检查网络后重试`)}let f=null,p=await d.text();if(p)try{f=JSON.parse(p)}catch{f=null}if(!d.ok){let e=gd(d.status,f);throw d.status===401&&a&&(ld==null||ld()),e}return f}var yd=e=>encodeURIComponent(e),bd={login(e,t,n){return vd(`POST`,`/auth/login`,{auth:!1,body:{account:e,password:t,client_name:n}})},logout(){return vd(`POST`,`/auth/logout`)},me(){return vd(`GET`,`/me`)},accounts(e=``){return vd(`GET`,`/accounts`,{query:{q:e}})},desktops(e){return vd(`GET`,`/desktops`,{query:{account_id:e}})},sendCommand(e,t,n){return vd(`POST`,`/desktops/${yd(e)}/commands`,{body:{type:t},query:{account_id:n}})},command(e){return vd(`GET`,`/commands/${yd(e)}`)},conversations(e){return vd(`GET`,`/conversations`,{query:{account_id:e.accountId,q:e.q,scope:e.scope,cursor:e.cursor,limit:e.limit},signal:e.signal})},block(e,t,n){return vd(`PUT`,`/blacklist/${yd(e.account)}/${yd(e.conversation)}`,{body:{name:e.name||``,account_name:e.account_name||``,reason:t},query:{account_id:n}})},unblock(e,t,n){return vd(`DELETE`,`/blacklist/${yd(e)}/${yd(t)}`,{query:{account_id:n}})}},xd=`wecom-mobile-view-account`;function Sd(){try{let e=window.localStorage.getItem(xd),t=e?JSON.parse(e):null;return t&&typeof t.id==`number`?t:null}catch{return null}}var $=Pt({token:md(),me:null,viewed:Sd()}),Cd=Z(()=>{var e;return(e=$.me)==null?void 0:e.permissions}),wd=Z(()=>{let e=$.me,t=$.viewed;if(e&&t&&e.permissions.control_all&&t.id!==e.account.id)return t.id}),Td=Z(()=>{var e,t;return wd.value!==void 0&&$.viewed?$.viewed.nickname||$.viewed.sn||`客服 #${$.viewed.id}`:((e=$.me)==null?void 0:e.account.nickname)||((t=$.me)==null?void 0:t.account.sn)||``});function Ed(e){$.viewed=e?{id:e.id,nickname:e.nickname,sn:e.sn}:null;try{$.viewed?window.localStorage.setItem(xd,JSON.stringify($.viewed)):window.localStorage.removeItem(xd)}catch{}}function Dd(e,t){hd(e),$.token=e,$.me=t,t.permissions.control_all||Ed(null)}function Od(){hd(``),$.token=``,$.me=null,Ed(null)}async function kd(){let e=await bd.me();return $.me=e,e.permissions.control_all||Ed(null),e}async function Ad(){try{await bd.logout()}catch{}Od()}var jd=`modulepreload`,Md=function(e){return`/m/`+e},Nd={},Pd=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute(`nonce`));function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=Md(t,n),t=s(t),t in Nd)return;Nd[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:jd,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Fd=ad({history:Du(),routes:[{path:`/login`,name:`login`,component:()=>Pd(()=>import(`./LoginView-Br8nsdQM.js`),__vite__mapDeps([0,1,2,3,4])),meta:{public:!0}},{path:`/`,component:()=>Pd(()=>import(`./ShellView-DcQ5QK1V.js`),__vite__mapDeps([5,1,6,7])),children:[{path:``,redirect:{name:`desktops`}},{path:`desktops`,name:`desktops`,component:()=>Pd(()=>import(`./DevicesView-DVXAQfpT.js`),__vite__mapDeps([8,1,9,6,2,10,3,11,12]))},{path:`conversations`,name:`conversations`,component:()=>Pd(()=>import(`./ConversationsView-BMjeHKhk.js`),__vite__mapDeps([13,1,6,9,2,3,10,11,14]))},{path:`me`,name:`me`,component:()=>Pd(()=>import(`./MeView-BOWGunAt.js`),__vite__mapDeps([15,1,9,6,2,16]))}]},{path:`/:pathMatch(.*)*`,redirect:{name:`desktops`}}]});Fd.beforeEach(async e=>{if(e.meta.public)return!0;if(!$.token)return{name:`login`,query:{redirect:e.fullPath}};if(!$.me)try{await kd()}catch(t){if(t instanceof cd&&t.status===401)return Od(),{name:`login`,query:{redirect:e.fullPath}}}return!0}),ud(()=>{Od();let e=Fd.currentRoute.value;e.matched.length&&e.name!==`login`&&Fd.replace({name:`login`,query:{redirect:e.fullPath,expired:`1`}})});var Id=`wecom-mobile-chunk-reload`;Fd.onError((e,t)=>{let n=String((e==null?void 0:e.message)||e);if(!/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(n))return;let r=0;try{r=Number(window.sessionStorage.getItem(Id)||0),window.sessionStorage.setItem(Id,String(Date.now()))}catch{}Date.now()-r>1e4&&(window.location.assign(`/m/#${t.fullPath}`),window.location.reload())}),us(tl).use(Fd).mount(`#app`),`serviceWorker`in navigator&&window.isSecureContext&&window.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/m/sw.js`,{scope:`/m/`}).catch(()=>void 0)});export{hs as $,ac as A,Mr as At,Hs as B,_i as Bt,Sc as C,fr as Ct,dc as D,gn as Dt,nc as E,Aa as Et,Ns as F,ua as Ft,Qs as G,Pt as Gt,ec as H,Nn as Ht,Ps as I,An as It,Ds as J,he as Jt,Os as K,Wt as Kt,Zs as L,Kr as Lt,pc as M,jr as Mt,fc as N,Fr as Nt,uc as O,Sr as Ot,tc as P,Nr as Pt,ms as Q,Us as R,Vr as Rt,gc as S,J as St,ic as T,V as Tt,Rs as U,Dn as Ut,Ws as V,Fn as Vt,Ls as W,On as Wt,Ts as X,As as Y,Se as Yt,Es as Z,zc as _,ga as _t,Ed as a,ps as at,Oc as b,qr as bt,cd as c,fo as ct,bl as d,os as dt,_s as et,el as f,oa as ft,Bc as g,xa as gt,Hc as h,Z as ht,Ad as i,ys as it,rc as j,Cr as jt,cc as k,Pr as kt,bd as l,us as lt,Kc as m,Xn as mt,$ as n,bs as nt,wd as o,Cs as ot,Qc as p,br as pt,ks as q,L as qt,Dd as r,gs as rt,Td as s,ws as st,Cd as t,vs as tt,xl as u,Ao as ut,mc as v,Ea as vt,oc as w,Pa as wt,hc as x,Ta as xt,Ec as y,ha as yt,Ms as z,Ur as zt}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css new file mode 100644 index 000000000..da7edc533 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css @@ -0,0 +1 @@ +:root,:host{--van-black:#000;--van-white:#fff;--van-gray-1:#f7f8fa;--van-gray-2:#f2f3f5;--van-gray-3:#ebedf0;--van-gray-4:#dcdee0;--van-gray-5:#c8c9cc;--van-gray-6:#969799;--van-gray-7:#646566;--van-gray-8:#323233;--van-red:#ee0a24;--van-blue:#1989fa;--van-orange:#ff976a;--van-orange-dark:#ed6a0c;--van-orange-light:#fffbe8;--van-green:#07c160;--van-gradient-red:linear-gradient(to right, #ff6034, #ee0a24);--van-gradient-orange:linear-gradient(to right, #ffd01e, #ff8917);--van-primary-color:var(--van-blue);--van-success-color:var(--van-green);--van-danger-color:var(--van-red);--van-warning-color:var(--van-orange);--van-text-color:var(--van-gray-8);--van-text-color-2:var(--van-gray-6);--van-text-color-3:var(--van-gray-5);--van-active-color:var(--van-gray-2);--van-active-opacity:.6;--van-disabled-opacity:.5;--van-background:var(--van-gray-1);--van-background-2:var(--van-white);--van-background-3:var(--van-white);--van-padding-base:4px;--van-padding-xs:8px;--van-padding-sm:12px;--van-padding-md:16px;--van-padding-lg:24px;--van-padding-xl:32px;--van-font-bold:600;--van-font-size-xs:10px;--van-font-size-sm:12px;--van-font-size-md:14px;--van-font-size-lg:16px;--van-line-height-xs:14px;--van-line-height-sm:18px;--van-line-height-md:20px;--van-line-height-lg:22px;--van-base-font:-apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;--van-price-font:avenir-heavy, "PingFang SC", helvetica neue, arial, sans-serif;--van-duration-base:.3s;--van-duration-fast:.2s;--van-ease-out:ease-out;--van-ease-in:ease-in;--van-border-color:var(--van-gray-3);--van-border-width:1px;--van-radius-sm:2px;--van-radius-md:4px;--van-radius-lg:8px;--van-radius-max:999px}.van-theme-dark{--van-text-color:#f5f5f5;--van-text-color-2:#707070;--van-text-color-3:#4d4d4d;--van-border-color:#3a3a3c;--van-active-color:#3a3a3c;--van-background:#000;--van-background-2:#1c1c1e;--van-background-3:#37363b}html{-webkit-tap-highlight-color:transparent}body{font-family:var(--van-base-font);margin:0}a{text-decoration:none}input,button,textarea{color:inherit;font:inherit}a:focus,input:focus,button:focus,textarea:focus,[class*=van-]:focus{outline:none}ol,ul{margin:0;padding:0;list-style:none}@keyframes van-slide-up-enter{0%{transform:translateY(100%)}}@keyframes van-slide-up-leave{to{transform:translateY(100%)}}@keyframes van-slide-down-enter{0%{transform:translateY(-100%)}}@keyframes van-slide-down-leave{to{transform:translateY(-100%)}}@keyframes van-slide-left-enter{0%{transform:translate(-100%)}}@keyframes van-slide-left-leave{to{transform:translate(-100%)}}@keyframes van-slide-right-enter{0%{transform:translate(100%)}}@keyframes van-slide-right-leave{to{transform:translate(100%)}}@keyframes van-fade-in{0%{opacity:0}to{opacity:1}}@keyframes van-fade-out{0%{opacity:1}to{opacity:0}}@keyframes van-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.van-fade-enter-active{animation:var(--van-duration-base) van-fade-in both var(--van-ease-out)}.van-fade-leave-active{animation:var(--van-duration-base) van-fade-out both var(--van-ease-in)}.van-slide-up-enter-active{animation:van-slide-up-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-up-leave-active{animation:van-slide-up-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-down-enter-active{animation:van-slide-down-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-down-leave-active{animation:van-slide-down-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-left-enter-active{animation:van-slide-left-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-left-leave-active{animation:van-slide-left-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-right-enter-active{animation:van-slide-right-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-right-leave-active{animation:van-slide-right-leave var(--van-duration-base) both var(--van-ease-in)}.van-clearfix:after{clear:both;content:"";display:table}.van-ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.van-multi-ellipsis--l2{text-overflow:ellipsis;-webkit-line-clamp:2;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-multi-ellipsis--l3{text-overflow:ellipsis;-webkit-line-clamp:3;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-safe-area-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-safe-area-bottom{padding-bottom:constant(safe-area-inset-bottom);padding-bottom:env(safe-area-inset-bottom)}.van-haptics-feedback{cursor:pointer}.van-haptics-feedback:active{opacity:var(--van-active-opacity)}[class*=van-hairline]:after{box-sizing:border-box;content:" ";pointer-events:none;border:0 solid var(--van-border-color);position:absolute;top:-50%;bottom:-50%;left:-50%;right:-50%;transform:scale(.5)}.van-hairline,.van-hairline--top,.van-hairline--left,.van-hairline--right,.van-hairline--bottom,.van-hairline--surround,.van-hairline--top-bottom{position:relative}.van-hairline--top:after{border-top-width:var(--van-border-width)}.van-hairline--left:after{border-left-width:var(--van-border-width)}.van-hairline--right:after{border-right-width:var(--van-border-width)}.van-hairline--bottom:after{border-bottom-width:var(--van-border-width)}.van-hairline--top-bottom:after,.van-hairline-unset--top-bottom:after{border-width:var(--van-border-width) 0}.van-hairline--surround:after{border-width:var(--van-border-width)}:root,:host{--van-badge-size:16px;--van-badge-color:var(--van-white);--van-badge-padding:0 3px;--van-badge-font-size:var(--van-font-size-sm);--van-badge-font-weight:var(--van-font-bold);--van-badge-border-width:var(--van-border-width);--van-badge-background:var(--van-danger-color);--van-badge-dot-color:var(--van-danger-color);--van-badge-dot-size:8px;--van-badge-font:-apple-system-font, helvetica neue, arial, sans-serif}.van-badge{box-sizing:border-box;min-width:var(--van-badge-size);padding:var(--van-badge-padding);color:var(--van-badge-color);font-weight:var(--van-badge-font-weight);font-size:var(--van-badge-font-size);font-family:var(--van-badge-font);text-align:center;background:var(--van-badge-background);border:var(--van-badge-border-width) solid var(--van-background-2);border-radius:var(--van-radius-max);line-height:1.2;display:inline-block}.van-badge--fixed{transform-origin:100%;position:absolute}.van-badge--top-left{top:0;left:0;transform:translate(-50%,-50%)}.van-badge--top-right{top:0;right:0;transform:translate(50%,-50%)}.van-badge--bottom-left{bottom:0;left:0;transform:translate(-50%,50%)}.van-badge--bottom-right{bottom:0;right:0;transform:translate(50%,50%)}.van-badge--dot{width:var(--van-badge-dot-size);min-width:0;height:var(--van-badge-dot-size);background:var(--van-badge-dot-color);border:none;border-radius:100%;padding:0}.van-badge__wrapper{display:inline-block;position:relative}.van-icon{font:14px/1 vant-icon;font:normal normal normal 14px/1 var(--van-icon-font-family,"vant-icon");font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;display:inline-block;position:relative}.van-icon:before{display:inline-block}.van-icon-arrow-double-left:before{content:""}.van-icon-arrow-double-right:before{content:""}.van-icon-contact:before{content:""}.van-icon-notes:before{content:""}.van-icon-records:before{content:""}.van-icon-cash-back-record:before{content:""}.van-icon-newspaper:before{content:""}.van-icon-discount:before{content:""}.van-icon-completed:before{content:""}.van-icon-user:before{content:""}.van-icon-description:before{content:""}.van-icon-list-switch:before{content:""}.van-icon-list-switching:before{content:""}.van-icon-link-o:before{content:""}.van-icon-miniprogram-o:before{content:""}.van-icon-qq:before{content:""}.van-icon-wechat-moments:before{content:""}.van-icon-weibo:before{content:""}.van-icon-cash-o:before{content:""}.van-icon-guide-o:before{content:""}.van-icon-invitation:before{content:""}.van-icon-shield-o:before{content:""}.van-icon-exchange:before{content:""}.van-icon-eye:before{content:""}.van-icon-enlarge:before{content:""}.van-icon-expand-o:before{content:""}.van-icon-eye-o:before{content:""}.van-icon-expand:before{content:""}.van-icon-filter-o:before{content:""}.van-icon-fire:before{content:""}.van-icon-fail:before{content:""}.van-icon-failure:before{content:""}.van-icon-fire-o:before{content:""}.van-icon-flag-o:before{content:""}.van-icon-font:before{content:""}.van-icon-font-o:before{content:""}.van-icon-gem-o:before{content:""}.van-icon-flower-o:before{content:""}.van-icon-gem:before{content:""}.van-icon-gift-card:before{content:""}.van-icon-friends:before{content:""}.van-icon-friends-o:before{content:""}.van-icon-gold-coin:before{content:""}.van-icon-gold-coin-o:before{content:""}.van-icon-good-job-o:before{content:""}.van-icon-gift:before{content:""}.van-icon-gift-o:before{content:""}.van-icon-gift-card-o:before{content:""}.van-icon-good-job:before{content:""}.van-icon-home-o:before{content:""}.van-icon-goods-collect:before{content:""}.van-icon-graphic:before{content:""}.van-icon-goods-collect-o:before{content:""}.van-icon-hot-o:before{content:""}.van-icon-info:before{content:""}.van-icon-hotel-o:before{content:""}.van-icon-info-o:before{content:""}.van-icon-hot-sale-o:before{content:""}.van-icon-hot:before{content:""}.van-icon-like:before{content:""}.van-icon-idcard:before{content:""}.van-icon-like-o:before{content:""}.van-icon-hot-sale:before{content:""}.van-icon-location-o:before{content:""}.van-icon-location:before{content:""}.van-icon-label:before{content:""}.van-icon-lock:before{content:""}.van-icon-label-o:before{content:""}.van-icon-map-marked:before{content:""}.van-icon-logistics:before{content:""}.van-icon-manager:before{content:""}.van-icon-more:before{content:""}.van-icon-live:before{content:""}.van-icon-manager-o:before{content:""}.van-icon-medal:before{content:""}.van-icon-more-o:before{content:""}.van-icon-music-o:before{content:""}.van-icon-music:before{content:""}.van-icon-new-arrival-o:before{content:""}.van-icon-medal-o:before{content:""}.van-icon-new-o:before{content:""}.van-icon-free-postage:before{content:""}.van-icon-newspaper-o:before{content:""}.van-icon-new-arrival:before{content:""}.van-icon-minus:before{content:""}.van-icon-orders-o:before{content:""}.van-icon-new:before{content:""}.van-icon-paid:before{content:""}.van-icon-notes-o:before{content:""}.van-icon-other-pay:before{content:""}.van-icon-pause-circle:before{content:""}.van-icon-pause:before{content:""}.van-icon-pause-circle-o:before{content:""}.van-icon-peer-pay:before{content:""}.van-icon-pending-payment:before{content:""}.van-icon-passed:before{content:""}.van-icon-plus:before{content:""}.van-icon-phone-circle-o:before{content:""}.van-icon-phone-o:before{content:""}.van-icon-printer:before{content:""}.van-icon-photo-fail:before{content:""}.van-icon-phone:before{content:""}.van-icon-photo-o:before{content:""}.van-icon-play-circle:before{content:""}.van-icon-play:before{content:""}.van-icon-phone-circle:before{content:""}.van-icon-point-gift-o:before{content:""}.van-icon-point-gift:before{content:""}.van-icon-play-circle-o:before{content:""}.van-icon-shrink:before{content:""}.van-icon-photo:before{content:""}.van-icon-qr:before{content:""}.van-icon-qr-invalid:before{content:""}.van-icon-question-o:before{content:""}.van-icon-revoke:before{content:""}.van-icon-replay:before{content:""}.van-icon-service:before{content:""}.van-icon-question:before{content:""}.van-icon-search:before{content:""}.van-icon-refund-o:before{content:""}.van-icon-service-o:before{content:""}.van-icon-scan:before{content:""}.van-icon-share:before{content:""}.van-icon-send-gift-o:before{content:""}.van-icon-share-o:before{content:""}.van-icon-setting:before{content:""}.van-icon-points:before{content:""}.van-icon-photograph:before{content:""}.van-icon-shop:before{content:""}.van-icon-shop-o:before{content:""}.van-icon-shop-collect-o:before{content:""}.van-icon-shop-collect:before{content:""}.van-icon-smile:before{content:""}.van-icon-shopping-cart-o:before{content:""}.van-icon-sign:before{content:""}.van-icon-sort:before{content:""}.van-icon-star-o:before{content:""}.van-icon-smile-comment-o:before{content:""}.van-icon-stop:before{content:""}.van-icon-stop-circle-o:before{content:""}.van-icon-smile-o:before{content:""}.van-icon-star:before{content:""}.van-icon-success:before{content:""}.van-icon-stop-circle:before{content:""}.van-icon-records-o:before{content:""}.van-icon-shopping-cart:before{content:""}.van-icon-tosend:before{content:""}.van-icon-todo-list:before{content:""}.van-icon-thumb-circle-o:before{content:""}.van-icon-thumb-circle:before{content:""}.van-icon-umbrella-circle:before{content:""}.van-icon-underway:before{content:""}.van-icon-upgrade:before{content:""}.van-icon-todo-list-o:before{content:""}.van-icon-tv-o:before{content:""}.van-icon-underway-o:before{content:""}.van-icon-user-o:before{content:""}.van-icon-vip-card-o:before{content:""}.van-icon-vip-card:before{content:""}.van-icon-send-gift:before{content:""}.van-icon-wap-home:before{content:""}.van-icon-wap-nav:before{content:""}.van-icon-volume-o:before{content:""}.van-icon-video:before{content:""}.van-icon-wap-home-o:before{content:""}.van-icon-volume:before{content:""}.van-icon-warning:before{content:""}.van-icon-weapp-nav:before{content:""}.van-icon-wechat-pay:before{content:""}.van-icon-warning-o:before{content:""}.van-icon-wechat:before{content:""}.van-icon-setting-o:before{content:""}.van-icon-youzan-shield:before{content:""}.van-icon-warn-o:before{content:""}.van-icon-smile-comment:before{content:""}.van-icon-user-circle-o:before{content:""}.van-icon-video-o:before{content:""}.van-icon-add-square:before{content:""}.van-icon-add:before{content:""}.van-icon-arrow-down:before{content:""}.van-icon-arrow-up:before{content:""}.van-icon-arrow:before{content:""}.van-icon-after-sale:before{content:""}.van-icon-add-o:before{content:""}.van-icon-alipay:before{content:""}.van-icon-ascending:before{content:""}.van-icon-apps-o:before{content:""}.van-icon-aim:before{content:""}.van-icon-award:before{content:""}.van-icon-arrow-left:before{content:""}.van-icon-award-o:before{content:""}.van-icon-audio:before{content:""}.van-icon-bag-o:before{content:""}.van-icon-balance-list:before{content:""}.van-icon-back-top:before{content:""}.van-icon-bag:before{content:""}.van-icon-balance-pay:before{content:""}.van-icon-balance-o:before{content:""}.van-icon-bar-chart-o:before{content:""}.van-icon-bars:before{content:""}.van-icon-balance-list-o:before{content:""}.van-icon-birthday-cake-o:before{content:""}.van-icon-bookmark:before{content:""}.van-icon-bill:before{content:""}.van-icon-bell:before{content:""}.van-icon-browsing-history-o:before{content:""}.van-icon-browsing-history:before{content:""}.van-icon-bookmark-o:before{content:""}.van-icon-bulb-o:before{content:""}.van-icon-bullhorn-o:before{content:""}.van-icon-bill-o:before{content:""}.van-icon-calendar-o:before{content:""}.van-icon-brush-o:before{content:""}.van-icon-card:before{content:""}.van-icon-cart-o:before{content:""}.van-icon-cart-circle:before{content:""}.van-icon-cart-circle-o:before{content:""}.van-icon-cart:before{content:""}.van-icon-cash-on-deliver:before{content:""}.van-icon-cash-back-record-o:before{content:""}.van-icon-cashier-o:before{content:""}.van-icon-chart-trending-o:before{content:""}.van-icon-certificate:before{content:""}.van-icon-chat:before{content:""}.van-icon-clear:before{content:""}.van-icon-chat-o:before{content:""}.van-icon-checked:before{content:""}.van-icon-clock:before{content:""}.van-icon-clock-o:before{content:""}.van-icon-close:before{content:""}.van-icon-closed-eye:before{content:""}.van-icon-circle:before{content:""}.van-icon-cluster-o:before{content:""}.van-icon-column:before{content:""}.van-icon-comment-circle-o:before{content:""}.van-icon-cluster:before{content:""}.van-icon-comment:before{content:""}.van-icon-comment-o:before{content:""}.van-icon-comment-circle:before{content:""}.van-icon-completed-o:before{content:""}.van-icon-credit-pay:before{content:""}.van-icon-coupon:before{content:""}.van-icon-debit-pay:before{content:""}.van-icon-coupon-o:before{content:""}.van-icon-contact-o:before{content:""}.van-icon-descending:before{content:""}.van-icon-desktop-o:before{content:""}.van-icon-diamond-o:before{content:""}.van-icon-description-o:before{content:""}.van-icon-delete:before{content:""}.van-icon-diamond:before{content:""}.van-icon-delete-o:before{content:""}.van-icon-cross:before{content:""}.van-icon-edit:before{content:""}.van-icon-ellipsis:before{content:""}.van-icon-down:before{content:""}.van-icon-discount-o:before{content:""}.van-icon-ecard-pay:before{content:""}.van-icon-envelop-o:before{content:""}@font-face{font-weight:400;font-family:vant-icon;font-style:normal;font-display:auto;src:url(data:font/woff2;charset=utf-8;base64,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)format("woff2")}.van-icon__image{object-fit:contain;width:1em;height:1em;display:block}:root,:host{--van-nav-bar-height:46px;--van-nav-bar-background:var(--van-background-2);--van-nav-bar-arrow-size:16px;--van-nav-bar-icon-color:var(--van-primary-color);--van-nav-bar-text-color:var(--van-primary-color);--van-nav-bar-title-font-size:var(--van-font-size-lg);--van-nav-bar-title-text-color:var(--van-text-color);--van-nav-bar-z-index:1;--van-nav-bar-disabled-opacity:var(--van-disabled-opacity)}.van-nav-bar{z-index:var(--van-nav-bar-z-index);line-height:var(--van-line-height-lg);text-align:center;background:var(--van-nav-bar-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.van-nav-bar--fixed{width:100%;position:fixed;top:0;left:0}.van-nav-bar--safe-area-inset-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-nav-bar .van-icon{color:var(--van-nav-bar-icon-color)}.van-nav-bar__content{height:var(--van-nav-bar-height);align-items:center;display:flex;position:relative}.van-nav-bar__arrow{margin-right:var(--van-padding-base);font-size:var(--van-nav-bar-arrow-size)}.van-nav-bar__title{max-width:60%;color:var(--van-nav-bar-title-text-color);font-weight:var(--van-font-bold);font-size:var(--van-nav-bar-title-font-size);margin:0 auto}.van-nav-bar__left,.van-nav-bar__right{padding:0 var(--van-padding-md);font-size:var(--van-font-size-md);align-items:center;display:flex;position:absolute;top:0;bottom:0}.van-nav-bar__left--disabled,.van-nav-bar__right--disabled{cursor:not-allowed;opacity:var(--van-nav-bar-disabled-opacity)}.van-nav-bar__left{left:0}.van-nav-bar__right{right:0}.van-nav-bar__text{color:var(--van-nav-bar-text-color)}:root,:host{--van-tabbar-height:50px;--van-tabbar-z-index:1;--van-tabbar-background:var(--van-background-2)}.van-tabbar{z-index:var(--van-tabbar-z-index);box-sizing:content-box;width:100%;height:var(--van-tabbar-height);background:var(--van-tabbar-background);display:flex}.van-tabbar--fixed{position:fixed;bottom:0;left:0}:root,:host{--van-tabbar-item-font-size:var(--van-font-size-sm);--van-tabbar-item-text-color:var(--van-text-color);--van-tabbar-item-active-color:var(--van-primary-color);--van-tabbar-item-active-background:var(--van-background-2);--van-tabbar-item-line-height:1;--van-tabbar-item-icon-size:22px;--van-tabbar-item-icon-margin-bottom:var(--van-padding-base)}.van-tabbar-item{color:var(--van-tabbar-item-text-color);font-size:var(--van-tabbar-item-font-size);line-height:var(--van-tabbar-item-line-height);cursor:pointer;flex-direction:column;flex:1;justify-content:center;align-items:center;display:flex}.van-tabbar-item__icon{margin-bottom:var(--van-tabbar-item-icon-margin-bottom);font-size:var(--van-tabbar-item-icon-size)}.van-tabbar-item__icon .van-icon{display:block}.van-tabbar-item__icon .van-badge{margin-top:var(--van-padding-base)}.van-tabbar-item__icon img{height:20px;display:block}.van-tabbar-item--active{color:var(--van-tabbar-item-active-color);background-color:var(--van-tabbar-item-active-background)}:root,:host{--van-cell-font-size:var(--van-font-size-md);--van-cell-line-height:24px;--van-cell-vertical-padding:10px;--van-cell-horizontal-padding:var(--van-padding-md);--van-cell-text-color:var(--van-text-color);--van-cell-background:var(--van-background-2);--van-cell-border-color:var(--van-border-color);--van-cell-active-color:var(--van-active-color);--van-cell-required-color:var(--van-danger-color);--van-cell-label-color:var(--van-text-color-2);--van-cell-label-font-size:var(--van-font-size-sm);--van-cell-label-line-height:var(--van-line-height-sm);--van-cell-label-margin-top:var(--van-padding-base);--van-cell-value-color:var(--van-text-color-2);--van-cell-value-font-size:inherit;--van-cell-icon-size:16px;--van-cell-right-icon-color:var(--van-gray-6);--van-cell-large-vertical-padding:var(--van-padding-sm);--van-cell-large-title-font-size:var(--van-font-size-lg);--van-cell-large-label-font-size:var(--van-font-size-md);--van-cell-large-value-font-size:inherit}.van-cell{box-sizing:border-box;width:100%;padding:var(--van-cell-vertical-padding) var(--van-cell-horizontal-padding);color:var(--van-cell-text-color);font-size:var(--van-cell-font-size);line-height:var(--van-cell-line-height);background:var(--van-cell-background);display:flex;position:relative;overflow:hidden}.van-cell:after{box-sizing:border-box;content:" ";pointer-events:none;right:var(--van-padding-md);bottom:0;left:var(--van-padding-md);border-bottom:1px solid var(--van-cell-border-color);position:absolute;transform:scaleY(.5)}.van-cell:last-child:after,.van-cell--borderless:after{display:none}.van-cell__label{margin-top:var(--van-cell-label-margin-top);color:var(--van-cell-label-color);font-size:var(--van-cell-label-font-size);line-height:var(--van-cell-label-line-height)}.van-cell__title,.van-cell__value{flex:1}.van-cell__value{color:var(--van-cell-value-color);font-size:var(--van-cell-value-font-size);text-align:right;vertical-align:middle;word-wrap:break-word;position:relative;overflow:hidden}.van-cell__left-icon,.van-cell__right-icon{height:var(--van-cell-line-height);font-size:var(--van-cell-icon-size);line-height:var(--van-cell-line-height)}.van-cell__left-icon{margin-right:var(--van-padding-base)}.van-cell__right-icon{margin-left:var(--van-padding-base);color:var(--van-cell-right-icon-color)}.van-cell--clickable{cursor:pointer}.van-cell--clickable:active{background-color:var(--van-cell-active-color)}.van-cell--required{overflow:visible}.van-cell--required:before{left:var(--van-padding-xs);color:var(--van-cell-required-color);font-size:var(--van-cell-font-size);content:"*";position:absolute}.van-cell--center{align-items:center}.van-cell--large{padding-top:var(--van-cell-large-vertical-padding);padding-bottom:var(--van-cell-large-vertical-padding)}.van-cell--large .van-cell__title{font-size:var(--van-cell-large-title-font-size)}.van-cell--large .van-cell__label{font-size:var(--van-cell-large-label-font-size)}.van-cell--large .van-cell__value{font-size:var(--van-cell-large-value-font-size)}:root,:host{--van-field-label-width:6.2em;--van-field-label-color:var(--van-text-color);--van-field-label-margin-right:var(--van-padding-sm);--van-field-input-text-color:var(--van-text-color);--van-field-input-error-text-color:var(--van-danger-color);--van-field-input-disabled-text-color:var(--van-text-color-3);--van-field-placeholder-text-color:var(--van-text-color-3);--van-field-icon-size:18px;--van-field-clear-icon-size:18px;--van-field-clear-icon-color:var(--van-gray-5);--van-field-right-icon-color:var(--van-gray-6);--van-field-error-message-color:var(--van-danger-color);--van-field-error-message-font-size:12px;--van-field-text-area-min-height:60px;--van-field-word-limit-color:var(--van-gray-7);--van-field-word-limit-font-size:var(--van-font-size-sm);--van-field-word-limit-line-height:16px;--van-field-disabled-text-color:var(--van-text-color-3);--van-field-required-mark-color:var(--van-red)}.van-field{flex-wrap:wrap}.van-field__label{box-sizing:border-box;width:var(--van-field-label-width);margin-right:var(--van-field-label-margin-right);color:var(--van-field-label-color);text-align:left;word-wrap:break-word;flex:none}.van-field__label--center{text-align:center}.van-field__label--right{text-align:right}.van-field__label--top{text-align:left;width:100%;margin-bottom:var(--van-padding-base);overflow-wrap:break-word;display:flex}.van-field__label--required:before{color:var(--van-field-required-mark-color);content:"*";margin-right:2px}.van-field--disabled .van-field__label{color:var(--van-field-disabled-text-color)}.van-field__value{overflow:visible}.van-field__body{align-items:center;display:flex}.van-field__control{box-sizing:border-box;width:100%;min-width:0;color:var(--van-field-input-text-color);line-height:inherit;text-align:left;resize:none;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto;background-color:#0000;border:0;margin:0;padding:0;display:block}.van-field__control::placeholder{color:var(--van-field-placeholder-text-color)}.van-field__control:-moz-read-only{cursor:default}.van-field__control:read-only{cursor:default}.van-field__control:disabled{color:var(--van-field-input-disabled-text-color);cursor:not-allowed;opacity:1;-webkit-text-fill-color:var(--van-field-input-disabled-text-color)}.van-field__control--center{text-align:center;justify-content:center}.van-field__control--right{text-align:right;justify-content:flex-end}.van-field__control--custom{min-height:var(--van-cell-line-height);align-items:center;display:flex}.van-field__control--error::-webkit-input-placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--error,.van-field__control--error::placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--min-height{min-height:var(--van-field-text-area-min-height)}.van-field__control[type=date],.van-field__control[type=time],.van-field__control[type=datetime-local]{min-height:var(--van-cell-line-height)}.van-field__control[type=search]{-webkit-appearance:none}.van-field__clear,.van-field__icon,.van-field__button,.van-field__right-icon{flex-shrink:0}.van-field__clear,.van-field__right-icon{margin-right:calc(var(--van-padding-xs) * -1);padding:0 var(--van-padding-xs);line-height:inherit}.van-field__clear{color:var(--van-field-clear-icon-color);font-size:var(--van-field-clear-icon-size);cursor:pointer}.van-field__left-icon .van-icon,.van-field__right-icon .van-icon{font-size:var(--van-field-icon-size);line-height:inherit;display:block}.van-field__left-icon{margin-right:var(--van-padding-base)}.van-field__right-icon{color:var(--van-field-right-icon-color)}.van-field__button{padding-left:var(--van-padding-xs)}.van-field__error-message{color:var(--van-field-error-message-color);font-size:var(--van-field-error-message-font-size);text-align:left}.van-field__error-message--center{text-align:center}.van-field__error-message--right{text-align:right}.van-field__word-limit{margin-top:var(--van-padding-base);color:var(--van-field-word-limit-color);font-size:var(--van-field-word-limit-font-size);line-height:var(--van-field-word-limit-line-height);text-align:right}:root,:host{--van-cell-group-background:var(--van-background-2);--van-cell-group-title-color:var(--van-text-color-2);--van-cell-group-title-padding:var(--van-padding-md) var(--van-padding-md);--van-cell-group-title-font-size:var(--van-font-size-md);--van-cell-group-title-line-height:16px;--van-cell-group-inset-padding:0 var(--van-padding-md);--van-cell-group-inset-radius:var(--van-radius-lg);--van-cell-group-inset-title-padding:var(--van-padding-md) var(--van-padding-md)}.van-cell-group{background:var(--van-cell-group-background)}.van-cell-group--inset{margin:var(--van-cell-group-inset-padding);border-radius:var(--van-cell-group-inset-radius);overflow:hidden}.van-cell-group__title{padding:var(--van-cell-group-title-padding);color:var(--van-cell-group-title-color);font-size:var(--van-cell-group-title-font-size);line-height:var(--van-cell-group-title-line-height)}.van-cell-group__title--inset{padding:var(--van-cell-group-inset-title-padding)}:root,:host{--van-loading-text-color:var(--van-text-color-2);--van-loading-text-font-size:var(--van-font-size-md);--van-loading-spinner-color:var(--van-gray-5);--van-loading-spinner-size:30px;--van-loading-spinner-duration:.8s}.van-loading{color:var(--van-loading-spinner-color);vertical-align:middle;font-size:0;position:relative}.van-loading__spinner{width:var(--van-loading-spinner-size);max-width:100%;height:var(--van-loading-spinner-size);vertical-align:middle;max-height:100%;animation:van-rotate var(--van-loading-spinner-duration) linear infinite;display:inline-block;position:relative}.van-loading__spinner--spinner{animation-timing-function:steps(12,end)}.van-loading__spinner--circular{animation-duration:2s}.van-loading__line{width:100%;height:100%;position:absolute;top:0;left:0}.van-loading__line:before{content:" ";background-color:currentColor;border-radius:40%;width:2px;height:25%;margin:0 auto;display:block}.van-loading__circular{width:100%;height:100%;display:block}.van-loading__circular circle{stroke:currentColor;stroke-width:3px;stroke-linecap:round;animation:1.5s ease-in-out infinite van-circular}.van-loading__text{margin-left:var(--van-padding-xs);color:var(--van-loading-text-color);font-size:var(--van-loading-text-font-size);vertical-align:middle;display:inline-block}.van-loading--vertical{flex-direction:column;align-items:center;display:flex}.van-loading--vertical .van-loading__text{margin:var(--van-padding-xs) 0 0}@keyframes van-circular{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-40px}to{stroke-dasharray:90 150;stroke-dashoffset:-120px}}.van-loading__line--1{opacity:1;transform:rotate(30deg)}.van-loading__line--2{opacity:.9375;transform:rotate(60deg)}.van-loading__line--3{opacity:.875;transform:rotate(90deg)}.van-loading__line--4{opacity:.8125;transform:rotate(120deg)}.van-loading__line--5{opacity:.75;transform:rotate(150deg)}.van-loading__line--6{opacity:.6875;transform:rotate(180deg)}.van-loading__line--7{opacity:.625;transform:rotate(210deg)}.van-loading__line--8{opacity:.5625;transform:rotate(240deg)}.van-loading__line--9{opacity:.5;transform:rotate(270deg)}.van-loading__line--10{opacity:.4375;transform:rotate(300deg)}.van-loading__line--11{opacity:.375;transform:rotate(330deg)}.van-loading__line--12{opacity:.3125;transform:rotate(360deg)}:root,:host{--van-button-mini-height:24px;--van-button-mini-padding:0 var(--van-padding-base);--van-button-mini-font-size:var(--van-font-size-xs);--van-button-small-height:32px;--van-button-small-padding:0 var(--van-padding-xs);--van-button-small-font-size:var(--van-font-size-sm);--van-button-normal-padding:0 15px;--van-button-normal-font-size:var(--van-font-size-md);--van-button-large-height:50px;--van-button-default-height:44px;--van-button-default-line-height:1.2;--van-button-default-font-size:var(--van-font-size-lg);--van-button-default-color:var(--van-text-color);--van-button-default-background:var(--van-background-2);--van-button-default-border-color:var(--van-gray-4);--van-button-primary-color:var(--van-white);--van-button-primary-background:var(--van-primary-color);--van-button-primary-border-color:var(--van-primary-color);--van-button-success-color:var(--van-white);--van-button-success-background:var(--van-success-color);--van-button-success-border-color:var(--van-success-color);--van-button-danger-color:var(--van-white);--van-button-danger-background:var(--van-danger-color);--van-button-danger-border-color:var(--van-danger-color);--van-button-warning-color:var(--van-white);--van-button-warning-background:var(--van-warning-color);--van-button-warning-border-color:var(--van-warning-color);--van-button-border-width:var(--van-border-width);--van-button-radius:var(--van-radius-md);--van-button-round-radius:var(--van-radius-max);--van-button-plain-background:var(--van-white);--van-button-disabled-opacity:var(--van-disabled-opacity);--van-button-icon-size:1.2em;--van-button-loading-icon-size:20px}.van-theme-dark{--van-button-plain-background:transparent}.van-button{box-sizing:border-box;height:var(--van-button-default-height);font-size:var(--van-button-default-font-size);line-height:var(--van-button-default-line-height);text-align:center;border-radius:var(--van-button-radius);cursor:pointer;transition:opacity var(--van-duration-fast);-webkit-appearance:none;-webkit-font-smoothing:auto;margin:0;padding:0;display:inline-block;position:relative}.van-button:before{background:var(--van-black);border:inherit;border-color:var(--van-black);border-radius:inherit;opacity:0;content:" ";box-sizing:content-box;width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.van-button:active:before{opacity:.1}.van-button--loading:before,.van-button--disabled:before{display:none}.van-button--default{color:var(--van-button-default-color);background:var(--van-button-default-background);border:var(--van-button-border-width) solid var(--van-button-default-border-color)}.van-button--primary{color:var(--van-button-primary-color);background:var(--van-button-primary-background);border:var(--van-button-border-width) solid var(--van-button-primary-border-color)}.van-button--success{color:var(--van-button-success-color);background:var(--van-button-success-background);border:var(--van-button-border-width) solid var(--van-button-success-border-color)}.van-button--danger{color:var(--van-button-danger-color);background:var(--van-button-danger-background);border:var(--van-button-border-width) solid var(--van-button-danger-border-color)}.van-button--warning{color:var(--van-button-warning-color);background:var(--van-button-warning-background);border:var(--van-button-border-width) solid var(--van-button-warning-border-color)}.van-button--plain{background:var(--van-button-plain-background)}.van-button--plain.van-button--primary{color:var(--van-button-primary-background)}.van-button--plain.van-button--success{color:var(--van-button-success-background)}.van-button--plain.van-button--danger{color:var(--van-button-danger-background)}.van-button--plain.van-button--warning{color:var(--van-button-warning-background)}.van-button--large{width:100%;height:var(--van-button-large-height)}.van-button--normal{padding:var(--van-button-normal-padding);font-size:var(--van-button-normal-font-size)}.van-button--small{height:var(--van-button-small-height);padding:var(--van-button-small-padding);font-size:var(--van-button-small-font-size)}.van-button__loading{color:inherit;font-size:inherit}.van-button__loading .van-loading__spinner{color:currentColor;width:var(--van-button-loading-icon-size);height:var(--van-button-loading-icon-size)}.van-button--mini{height:var(--van-button-mini-height);padding:var(--van-button-mini-padding);font-size:var(--van-button-mini-font-size)}.van-button--mini+.van-button--mini{margin-left:var(--van-padding-base)}.van-button--block{width:100%;display:block}.van-button--disabled{cursor:not-allowed;opacity:var(--van-button-disabled-opacity)}.van-button--loading{cursor:default}.van-button--round{border-radius:var(--van-button-round-radius)}.van-button--square{border-radius:0}.van-button__content{justify-content:center;align-items:center;height:100%;display:flex}.van-button__content:before{content:" "}.van-button__icon{font-size:var(--van-button-icon-size);line-height:inherit}.van-button__icon+.van-button__text,.van-button__loading+.van-button__text,.van-button__text+.van-button__icon,.van-button__text+.van-button__loading{margin-left:var(--van-padding-base)}.van-button--hairline{border-width:0}.van-button--hairline:after{border-color:inherit;border-radius:calc(var(--van-button-radius) * 2)}.van-button--hairline.van-button--round:after{border-radius:var(--van-button-round-radius)}.van-button--hairline.van-button--square:after{border-radius:0}:root,:host{--van-search-padding:10px var(--van-padding-sm);--van-search-background:var(--van-background-2);--van-search-content-background:var(--van-background);--van-search-input-height:34px;--van-search-label-padding:0 5px;--van-search-label-color:var(--van-text-color);--van-search-label-font-size:var(--van-font-size-md);--van-search-left-icon-color:var(--van-gray-6);--van-search-action-padding:0 var(--van-padding-xs);--van-search-action-text-color:var(--van-text-color);--van-search-action-font-size:var(--van-font-size-md)}.van-search{box-sizing:border-box;padding:var(--van-search-padding);background:var(--van-search-background);align-items:center;display:flex}.van-search__content{padding-left:var(--van-padding-sm);background:var(--van-search-content-background);border-radius:var(--van-radius-sm);flex:1;display:flex}.van-search__content--round{border-radius:var(--van-radius-max)}.van-search__label{padding:var(--van-search-label-padding);color:var(--van-search-label-color);font-size:var(--van-search-label-font-size);line-height:var(--van-search-input-height)}.van-search__field{padding:0 var(--van-padding-xs) 0 0;height:var(--van-search-input-height);background-color:#0000;flex:1;align-items:center}.van-search__field .van-field__left-icon{color:var(--van-search-left-icon-color)}.van-search__field--with-message{align-items:flex-start;height:auto;padding-top:5px;padding-bottom:5px}.van-search--show-action{padding-right:0}.van-search input::-webkit-search-decoration{display:none}.van-search input::-webkit-search-cancel-button{display:none}.van-search input::-webkit-search-results-button{display:none}.van-search input::-webkit-search-results-decoration{display:none}.van-search__action{padding:var(--van-search-action-padding);color:var(--van-search-action-text-color);font-size:var(--van-search-action-font-size);line-height:var(--van-search-input-height);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.van-search__action:active{background-color:var(--van-active-color)}:root,:host{--van-list-text-color:var(--van-text-color-2);--van-list-text-font-size:var(--van-font-size-md);--van-list-text-line-height:50px;--van-list-loading-icon-size:16px}.van-list__loading,.van-list__finished-text,.van-list__error-text{color:var(--van-list-text-color);font-size:var(--van-list-text-font-size);line-height:var(--van-list-text-line-height);text-align:center}.van-list__placeholder{pointer-events:none;height:0}.van-list__loading-icon .van-loading__spinner{width:var(--van-list-loading-icon-size);height:var(--van-list-loading-icon-size)}:root,:host{--van-pull-refresh-head-height:50px;--van-pull-refresh-head-font-size:var(--van-font-size-md);--van-pull-refresh-head-text-color:var(--van-text-color-2);--van-pull-refresh-loading-icon-size:16px}.van-pull-refresh{overflow:hidden}.van-pull-refresh__track{height:100%;transition-property:transform;position:relative}.van-pull-refresh__head{width:100%;height:var(--van-pull-refresh-head-height);color:var(--van-pull-refresh-head-text-color);font-size:var(--van-pull-refresh-head-font-size);line-height:var(--van-pull-refresh-head-height);text-align:center;position:absolute;left:0;overflow:hidden;transform:translateY(-100%)}.van-pull-refresh__loading .van-loading__spinner{width:var(--van-pull-refresh-loading-icon-size);height:var(--van-pull-refresh-loading-icon-size)}:root,:host{--van-tag-padding:0 var(--van-padding-base);--van-tag-text-color:var(--van-white);--van-tag-font-size:var(--van-font-size-sm);--van-tag-radius:2px;--van-tag-line-height:16px;--van-tag-medium-padding:2px 6px;--van-tag-large-padding:var(--van-padding-base) var(--van-padding-xs);--van-tag-large-radius:var(--van-radius-md);--van-tag-large-font-size:var(--van-font-size-md);--van-tag-round-radius:var(--van-radius-max);--van-tag-danger-color:var(--van-danger-color);--van-tag-primary-color:var(--van-primary-color);--van-tag-success-color:var(--van-success-color);--van-tag-warning-color:var(--van-warning-color);--van-tag-default-color:var(--van-gray-6);--van-tag-plain-background:var(--van-background-2)}.van-tag{padding:var(--van-tag-padding);color:var(--van-tag-text-color);font-size:var(--van-tag-font-size);line-height:var(--van-tag-line-height);border-radius:var(--van-tag-radius);align-items:center;display:inline-flex;position:relative}.van-tag--default{background:var(--van-tag-default-color)}.van-tag--default.van-tag--plain{color:var(--van-tag-default-color)}.van-tag--danger{background:var(--van-tag-danger-color)}.van-tag--danger.van-tag--plain{color:var(--van-tag-danger-color)}.van-tag--primary{background:var(--van-tag-primary-color)}.van-tag--primary.van-tag--plain{color:var(--van-tag-primary-color)}.van-tag--success{background:var(--van-tag-success-color)}.van-tag--success.van-tag--plain{color:var(--van-tag-success-color)}.van-tag--warning{background:var(--van-tag-warning-color)}.van-tag--warning.van-tag--plain{color:var(--van-tag-warning-color)}.van-tag--plain{background:var(--van-tag-plain-background);border-color:currentColor}.van-tag--plain:before{border:1px solid;border-color:inherit;border-radius:inherit;content:"";pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.van-tag--medium{padding:var(--van-tag-medium-padding)}.van-tag--large{padding:var(--van-tag-large-padding);font-size:var(--van-tag-large-font-size);border-radius:var(--van-tag-large-radius)}.van-tag--mark{border-radius:0 var(--van-tag-round-radius) var(--van-tag-round-radius) 0}.van-tag--mark:after{content:"";width:2px;display:block}.van-tag--round{border-radius:var(--van-tag-round-radius)}.van-tag__close{margin-left:2px}:root,:host{--van-sticky-z-index:99}.van-sticky--fixed{z-index:var(--van-sticky-z-index);position:fixed}:root,:host{--van-swipe-indicator-size:6px;--van-swipe-indicator-margin:var(--van-padding-sm);--van-swipe-indicator-active-opacity:1;--van-swipe-indicator-inactive-opacity:.3;--van-swipe-indicator-active-background:var(--van-primary-color);--van-swipe-indicator-inactive-background:var(--van-border-color)}.van-swipe{cursor:-webkit-grab;cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;overflow:hidden;transform:translateZ(0)}.van-swipe__track{height:100%;transition-property:transform;display:flex}.van-swipe__track--vertical{flex-direction:column}.van-swipe__indicators{bottom:var(--van-swipe-indicator-margin);display:flex;position:absolute;left:50%;transform:translate(-50%)}.van-swipe__indicators--vertical{top:50%;bottom:auto;left:var(--van-swipe-indicator-margin);flex-direction:column;transform:translateY(-50%)}.van-swipe__indicators--vertical .van-swipe__indicator:not(:last-child){margin-bottom:var(--van-swipe-indicator-size)}.van-swipe__indicator{width:var(--van-swipe-indicator-size);height:var(--van-swipe-indicator-size);background-color:var(--van-swipe-indicator-inactive-background);opacity:var(--van-swipe-indicator-inactive-opacity);transition:opacity var(--van-duration-fast),background-color var(--van-duration-fast);border-radius:100%}.van-swipe__indicator:not(:last-child){margin-right:var(--van-swipe-indicator-size)}.van-swipe__indicator--active{background-color:var(--van-swipe-indicator-active-background);opacity:var(--van-swipe-indicator-active-opacity)}:root,:host{--van-tab-text-color:var(--van-gray-7);--van-tab-active-text-color:var(--van-text-color);--van-tab-disabled-text-color:var(--van-text-color-3);--van-tab-font-size:var(--van-font-size-md);--van-tab-line-height:var(--van-line-height-md);--van-tabs-default-color:var(--van-primary-color);--van-tabs-line-height:44px;--van-tabs-card-height:30px;--van-tabs-nav-background:var(--van-background-2);--van-tabs-bottom-bar-width:40px;--van-tabs-bottom-bar-height:3px;--van-tabs-bottom-bar-color:var(--van-primary-color)}.van-tab{box-sizing:border-box;padding:0 var(--van-padding-base);color:var(--van-tab-text-color);font-size:var(--van-tab-font-size);line-height:var(--van-tab-line-height);cursor:pointer;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.van-tab--active{color:var(--van-tab-active-text-color);font-weight:var(--van-font-bold)}.van-tab--disabled{color:var(--van-tab-disabled-text-color);cursor:not-allowed}.van-tab--grow{padding:0 var(--van-padding-sm);flex:1 0 auto}.van-tab--shrink{padding:0 var(--van-padding-xs);flex:none}.van-tab--card{color:var(--van-tabs-default-color);border-right:var(--van-border-width) solid var(--van-tabs-default-color)}.van-tab--card:last-child{border-right:none}.van-tab--card.van-tab--active{color:var(--van-white);background-color:var(--van-tabs-default-color)}.van-tab--card.van-tab--disabled{color:var(--van-tab-disabled-text-color)}.van-tab__text--ellipsis{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-tabs{position:relative}.van-tabs__wrap{overflow:hidden}.van-tabs__wrap--page-top{position:fixed}.van-tabs__wrap--content-bottom{top:auto;bottom:0}.van-tabs__nav{background:var(--van-tabs-nav-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;position:relative}.van-tabs__nav--complete{-webkit-overflow-scrolling:touch;overflow-x:auto;overflow-y:hidden}.van-tabs__nav--complete::-webkit-scrollbar{display:none}.van-tabs__nav--line{box-sizing:content-box;height:100%;padding-bottom:15px}.van-tabs__nav--line.van-tabs__nav--shrink,.van-tabs__nav--line.van-tabs__nav--complete{padding-right:var(--van-padding-xs);padding-left:var(--van-padding-xs)}.van-tabs__nav--card{box-sizing:border-box;height:var(--van-tabs-card-height);margin:0 var(--van-padding-md);border:var(--van-border-width) solid var(--van-tabs-default-color);border-radius:var(--van-radius-sm)}.van-tabs__nav--card.van-tabs__nav--shrink{display:inline-flex}.van-tabs__line{z-index:1;width:var(--van-tabs-bottom-bar-width);height:var(--van-tabs-bottom-bar-height);background:var(--van-tabs-bottom-bar-color);border-radius:var(--van-tabs-bottom-bar-height);position:absolute;bottom:15px;left:0}.van-tabs__track{will-change:left;width:100%;height:100%;display:flex;position:relative}.van-tabs__content--animated{overflow:hidden}.van-tabs--line .van-tabs__wrap{height:var(--van-tabs-line-height)}.van-tabs--card>.van-tabs__wrap{height:var(--van-tabs-card-height)}.van-swipe-item{flex-shrink:0;width:100%;height:100%;position:relative}.van-tab__panel,.van-tab__panel-wrapper{box-sizing:border-box;flex-shrink:0;width:100%}.van-tab__panel-wrapper--inactive{height:0;overflow:visible}:root,:host{--van-overlay-z-index:1;--van-overlay-background:#000000b3}.van-overlay{z-index:var(--van-overlay-z-index);background:var(--van-overlay-background);width:100%;height:100%;position:fixed;top:0;left:0}:root,:host{--van-popup-background:var(--van-background-2);--van-popup-transition:transform var(--van-duration-base);--van-popup-round-radius:16px;--van-popup-close-icon-size:22px;--van-popup-close-icon-color:var(--van-gray-5);--van-popup-close-icon-margin:16px;--van-popup-close-icon-z-index:1}.van-overflow-hidden{overflow:hidden!important}.van-popup{box-sizing:border-box;background:var(--van-popup-background);max-height:100%;transition:var(--van-popup-transition);-webkit-overflow-scrolling:touch;position:fixed;overflow-y:auto}.van-popup--center{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100vw - var(--van-padding-md) * 2);margin:0 auto;top:50%;left:0;right:0;transform:translateY(-50%)}.van-popup--center.van-popup--round{border-radius:var(--van-popup-round-radius)}.van-popup--top{width:100%;top:0;left:0}.van-popup--top.van-popup--round{border-radius:0 0 var(--van-popup-round-radius) var(--van-popup-round-radius)}.van-popup--right{top:50%;right:0;transform:translateY(-50%)}.van-popup--right.van-popup--round{border-radius:var(--van-popup-round-radius) 0 0 var(--van-popup-round-radius)}.van-popup--bottom{width:100%;bottom:0;left:0}.van-popup--bottom.van-popup--round{border-radius:var(--van-popup-round-radius) var(--van-popup-round-radius) 0 0}.van-popup--left{top:50%;left:0;transform:translateY(-50%)}.van-popup--left.van-popup--round{border-radius:0 var(--van-popup-round-radius) var(--van-popup-round-radius) 0}.van-popup-slide-top-enter-active,.van-popup-slide-left-enter-active,.van-popup-slide-right-enter-active,.van-popup-slide-bottom-enter-active{transition-timing-function:var(--van-ease-out)}.van-popup-slide-top-leave-active,.van-popup-slide-left-leave-active,.van-popup-slide-right-leave-active,.van-popup-slide-bottom-leave-active{transition-timing-function:var(--van-ease-in)}.van-popup-slide-top-enter-from,.van-popup-slide-top-leave-active{transform:translateY(-100%)}.van-popup-slide-right-enter-from,.van-popup-slide-right-leave-active{transform:translate(100%,-50%)}.van-popup-slide-bottom-enter-from,.van-popup-slide-bottom-leave-active{transform:translateY(100%)}.van-popup-slide-left-enter-from,.van-popup-slide-left-leave-active{transform:translate(-100%,-50%)}.van-popup__close-icon{z-index:var(--van-popup-close-icon-z-index);color:var(--van-popup-close-icon-color);font-size:var(--van-popup-close-icon-size);position:absolute}.van-popup__close-icon--top-left{top:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--top-right{top:var(--van-popup-close-icon-margin);right:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-left{bottom:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-right{right:var(--van-popup-close-icon-margin);bottom:var(--van-popup-close-icon-margin)}:root,:host{--van-empty-padding:var(--van-padding-xl) 0;--van-empty-image-size:160px;--van-empty-description-margin-top:var(--van-padding-md);--van-empty-description-padding:0 60px;--van-empty-description-color:var(--van-text-color-2);--van-empty-description-font-size:var(--van-font-size-md);--van-empty-description-line-height:var(--van-line-height-md);--van-empty-bottom-margin-top:24px}.van-empty{box-sizing:border-box;padding:var(--van-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-empty__image{width:var(--van-empty-image-size);height:var(--van-empty-image-size)}.van-empty__image img{width:100%;height:100%}.van-empty__description{margin-top:var(--van-empty-description-margin-top);padding:var(--van-empty-description-padding);color:var(--van-empty-description-color);font-size:var(--van-empty-description-font-size);line-height:var(--van-empty-description-line-height)}.van-empty__bottom{margin-top:var(--van-empty-bottom-margin-top)}.van-theme-dark .van-empty{opacity:.5}:root,:host{--van-notice-bar-height:40px;--van-notice-bar-padding:0 var(--van-padding-md);--van-notice-bar-wrapable-padding:var(--van-padding-xs) var(--van-padding-md);--van-notice-bar-text-color:var(--van-orange-dark);--van-notice-bar-font-size:var(--van-font-size-md);--van-notice-bar-line-height:24px;--van-notice-bar-background:var(--van-orange-light);--van-notice-bar-icon-size:16px;--van-notice-bar-icon-min-width:24px}.van-notice-bar{height:var(--van-notice-bar-height);padding:var(--van-notice-bar-padding);color:var(--van-notice-bar-text-color);font-size:var(--van-notice-bar-font-size);line-height:var(--van-notice-bar-line-height);background:var(--van-notice-bar-background);align-items:center;display:flex;position:relative}.van-notice-bar__left-icon,.van-notice-bar__right-icon{min-width:var(--van-notice-bar-icon-min-width);font-size:var(--van-notice-bar-icon-size)}.van-notice-bar__right-icon{text-align:right;cursor:pointer}.van-notice-bar__wrap{flex:1;align-items:center;height:100%;display:flex;position:relative;overflow:hidden}.van-notice-bar__content{white-space:nowrap;transition-timing-function:linear;position:absolute}.van-notice-bar__content.van-ellipsis{max-width:100%}.van-notice-bar--wrapable{height:auto;padding:var(--van-notice-bar-wrapable-padding)}.van-notice-bar--wrapable .van-notice-bar__wrap{height:auto}.van-notice-bar--wrapable .van-notice-bar__content{white-space:normal;word-wrap:break-word;position:relative}:root,:host{--van-skeleton-title-width:40%}.van-skeleton-title{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-title--round{border-radius:var(--van-radius-max)}.van-skeleton-title{width:var(--van-skeleton-title-width);margin:0}.van-skeleton-title+.van-skeleton-paragraph{margin-top:20px}:root,:host{--van-skeleton-avatar-size:32px;--van-skeleton-avatar-background:var(--van-active-color)}.van-skeleton-avatar{width:var(--van-skeleton-avatar-size);height:var(--van-skeleton-avatar-size);margin-right:var(--van-padding-md);background:var(--van-skeleton-avatar-background);flex-shrink:0}.van-skeleton-avatar--round{border-radius:var(--van-radius-max)}.van-skeleton-avatar+.van-skeleton__content{padding-top:var(--van-padding-xs)}:root,:host{--van-skeleton-paragraph-height:16px;--van-skeleton-paragraph-background:var(--van-active-color);--van-skeleton-paragraph-margin-top:var(--van-padding-sm)}.van-skeleton-paragraph{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-paragraph--round{border-radius:var(--van-radius-max)}.van-skeleton-paragraph:not(:first-child){margin-top:var(--van-skeleton-paragraph-margin-top)}:root,:host{--van-skeleton-duration:1.2s}.van-skeleton{padding:0 var(--van-padding-md);display:flex}.van-skeleton__content{width:100%}.van-skeleton--animate{animation:van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite}@keyframes van-skeleton-blink{50%{opacity:.6}}:root,:host{--van-action-bar-background:var(--van-background-2);--van-action-bar-height:50px}.van-action-bar{box-sizing:content-box;height:var(--van-action-bar-height);background:var(--van-action-bar-background);align-items:center;display:flex;position:fixed;bottom:0;left:0;right:0}:root,:host{--van-action-bar-button-height:40px;--van-action-bar-button-warning-color:var(--van-gradient-orange);--van-action-bar-button-danger-color:var(--van-gradient-red)}.van-action-bar-button{height:var(--van-action-bar-button-height);font-weight:var(--van-font-bold);font-size:var(--van-font-size-md);border:none;border-radius:0;flex:1}.van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max);margin-left:5px}.van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max);margin-right:5px}.van-action-bar-button--warning{background:var(--van-action-bar-button-warning-color)}.van-action-bar-button--danger{background:var(--van-action-bar-button-danger-color)}@media (max-width:321px){.van-action-bar-button{font-size:13px}}:root,:host{--van-dialog-width:320px;--van-dialog-small-screen-width:90%;--van-dialog-font-size:var(--van-font-size-lg);--van-dialog-transition:var(--van-duration-base);--van-dialog-radius:16px;--van-dialog-background:var(--van-background-2);--van-dialog-header-font-weight:var(--van-font-bold);--van-dialog-header-line-height:24px;--van-dialog-header-padding-top:26px;--van-dialog-header-isolated-padding:var(--van-padding-lg) 0;--van-dialog-message-padding:var(--van-padding-lg);--van-dialog-message-font-size:var(--van-font-size-md);--van-dialog-message-line-height:var(--van-line-height-md);--van-dialog-message-max-height:60vh;--van-dialog-has-title-message-text-color:var(--van-gray-7);--van-dialog-has-title-message-padding-top:var(--van-padding-xs);--van-dialog-button-height:48px;--van-dialog-round-button-height:36px;--van-dialog-confirm-button-text-color:var(--van-primary-color)}.van-dialog{width:var(--van-dialog-width);font-size:var(--van-dialog-font-size);background:var(--van-dialog-background);border-radius:var(--van-dialog-radius);-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:var(--van-dialog-transition);transition-property:transform,opacity;top:45%;overflow:hidden}@media (max-width:321px){.van-dialog{width:var(--van-dialog-small-screen-width)}}.van-dialog__header{color:var(--van-text-color);padding-top:var(--van-dialog-header-padding-top);font-weight:var(--van-dialog-header-font-weight);line-height:var(--van-dialog-header-line-height);text-align:center}.van-dialog__header--isolated{padding:var(--van-dialog-header-isolated-padding)}.van-dialog__content--isolated{align-items:center;min-height:104px;display:flex}.van-dialog__message{color:var(--van-text-color);max-height:var(--van-dialog-message-max-height);padding:26px var(--van-dialog-message-padding);font-size:var(--van-dialog-message-font-size);line-height:var(--van-dialog-message-line-height);white-space:pre-wrap;text-align:center;word-wrap:break-word;-webkit-overflow-scrolling:touch;flex:1;overflow-y:auto}.van-dialog__message--has-title{padding-top:var(--van-dialog-has-title-message-padding-top);color:var(--van-dialog-has-title-message-text-color)}.van-dialog__message--left{text-align:left}.van-dialog__message--right{text-align:right}.van-dialog__message--justify{text-align:justify}.van-dialog__footer{-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;overflow:hidden}.van-dialog__confirm,.van-dialog__cancel{height:var(--van-dialog-button-height);border:0;border-radius:0;flex:1;margin:0}.van-dialog__confirm,.van-dialog__confirm:active{color:var(--van-dialog-confirm-button-text-color)}.van-dialog--round-button .van-dialog__footer{height:auto;padding:var(--van-padding-xs) var(--van-padding-lg) var(--van-padding-md);position:relative}.van-dialog--round-button .van-dialog__message{padding-bottom:var(--van-padding-md);color:var(--van-text-color)}.van-dialog--round-button .van-dialog__confirm,.van-dialog--round-button .van-dialog__cancel{height:var(--van-dialog-round-button-height)}.van-dialog--round-button .van-dialog__confirm{color:var(--van-white)}.van-dialog--round-button .van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max)}.van-dialog--round-button .van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max)}.van-dialog-bounce-enter-from{opacity:0;transform:translateY(-50%)scale(.7)}.van-dialog-bounce-leave-active{opacity:0;transform:translateY(-50%)scale(.9)}:root,:host{--van-toast-max-width:70%;--van-toast-font-size:var(--van-font-size-md);--van-toast-text-color:var(--van-white);--van-toast-loading-icon-color:var(--van-white);--van-toast-line-height:var(--van-line-height-md);--van-toast-radius:var(--van-radius-lg);--van-toast-background:#000000b3;--van-toast-icon-size:36px;--van-toast-text-min-width:96px;--van-toast-text-padding:var(--van-padding-xs) var(--van-padding-sm);--van-toast-default-padding:var(--van-padding-md);--van-toast-default-width:88px;--van-toast-default-min-height:88px;--van-toast-position-top-distance:20%;--van-toast-position-bottom-distance:20%}.van-toast{box-sizing:content-box;transition:all var(--van-duration-fast);width:var(--van-toast-default-width);max-width:var(--van-toast-max-width);min-height:var(--van-toast-default-min-height);padding:var(--van-toast-default-padding);color:var(--van-toast-text-color);font-size:var(--van-toast-font-size);line-height:var(--van-toast-line-height);white-space:pre-wrap;word-break:break-all;text-align:center;background:var(--van-toast-background);border-radius:var(--van-toast-radius);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-toast--break-normal{word-break:normal;word-wrap:normal}.van-toast--break-word{word-break:normal;word-wrap:break-word}.van-toast--unclickable{cursor:not-allowed;overflow:hidden}.van-toast--unclickable *{pointer-events:none}.van-toast--text,.van-toast--html{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;min-width:var(--van-toast-text-min-width);min-height:0;padding:var(--van-toast-text-padding)}.van-toast--text .van-toast__text,.van-toast--html .van-toast__text{margin-top:0}.van-toast--top{top:var(--van-toast-position-top-distance)}.van-toast--bottom{top:auto;bottom:var(--van-toast-position-bottom-distance)}.van-toast__icon{font-size:var(--van-toast-icon-size)}.van-toast__loading{padding:var(--van-padding-base);color:var(--van-toast-loading-icon-color)}.van-toast__text{margin-top:var(--van-padding-xs)}:root{--app-bg:#f4f7fd;--app-surface:#fff;--app-surface-2:#f6f8fe;--app-border:#6e89c92e;--app-text:#1d315f;--app-text-2:#3f527d;--app-muted:#7b89ab;--app-primary:#2f6fff;--app-primary-soft:#2f6fff1a;--app-success:#0f9e69;--app-success-soft:#13b77a1f;--app-warning:#c46f00;--app-warning-soft:#ff981d24;--app-danger:#e5383e;--app-danger-soft:#ff53581f;--app-muted-soft:#7b89ab24;--app-shadow:0 10px 28px #4e63a214;--app-radius:16px;--app-font:"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;--app-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root{--app-bg:#0b1222;--app-surface:#131c31;--app-surface-2:#18223b;--app-border:#8ca0d229;--app-text:#e6ecfb;--app-text-2:#bcc6e0;--app-muted:#8b97b6;--app-primary:#6f9bff;--app-primary-soft:#5b8cff29;--app-success:#2fd19a;--app-success-soft:#2fd19a24;--app-warning:#ffb45a;--app-warning-soft:#ffae4a24;--app-danger:#ff7a7f;--app-danger-soft:#ff6b7026;--app-muted-soft:#8390b029;--app-shadow:0 10px 28px #00000047;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*,:before,:after{box-sizing:border-box}html,body{background:var(--app-bg);color:var(--app-text);font-family:var(--app-font);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body,#app{min-height:100vh;min-height:100dvh}button,input,textarea{font-family:inherit}.app-card{background:var(--app-surface);border:1px solid var(--app-border);border-radius:var(--app-radius);box-shadow:var(--app-shadow)}.app-muted{color:var(--app-muted)}.app-mono{font-family:var(--app-mono);word-break:break-all;font-size:12px}.tone-success{color:var(--app-success)}.tone-warning{color:var(--app-warning)}.tone-danger{color:var(--app-danger)}.tone-muted{color:var(--app-muted)}.tone-primary{color:var(--app-primary)}.van-nav-bar .van-nav-bar__title{font-weight:600}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js new file mode 100644 index 000000000..2d6926fa3 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js @@ -0,0 +1 @@ +import{c as e}from"./_plugin-vue_export-helper-4o9rbdkf.js";import{Dt as t,Kt as n,M as r,Mt as i,N as a,St as o,Vt as s,W as c,at as l,it as u}from"./index-C_mEe-tZ.js";function d(e,{args:t=[],done:n,canceled:r,error:i}){if(e){let a=e.apply(null,t);u(a)?a.then(e=>{e?n():r&&r()}).catch(i||l):a?n():r&&r()}else n()}var f=(o,l)=>{let u=n(),d=()=>{u.value=c(o).height};return i(()=>{if(t(d),l)for(let e=1;e<=3;e++)setTimeout(d,100*e)}),e(()=>t(d)),s([a,r],d),u};function p(e,t){let n=f(e,!0);return e=>o(`div`,{class:t(`placeholder`),style:{height:n.value?`${n.value}px`:void 0}},[e()])}export{d as n,p as t}; \ No newline at end of file diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png differ diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png differ diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png differ diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png differ diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png differ diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html new file mode 100644 index 000000000..3c05519b4 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + + + +
+ + + diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest new file mode 100644 index 000000000..2376a055c --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话黑名单。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/deploy/mobile-http-20260928/server-bundle/payload/wechat_rpa/remote_api.py b/deploy/mobile-http-20260928/server-bundle/payload/wechat_rpa/remote_api.py new file mode 100644 index 000000000..69ad3e384 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/payload/wechat_rpa/remote_api.py @@ -0,0 +1,804 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台与桌面命令通道的 HTTP 接口。 + + /api/v2/mobile/* 手机 / 平板 H5 用,ZYT 账号密码登录(终端 9,由本服务代登) + /api/v2/desktop/commands/* 桌面端长轮询领取指令、回执结果、顺带上报状态 + /api/v2/desktop/blacklist* 桌面端同步服务端会话黑名单 + +手机令牌和桌面令牌分别由 ``mobile_sessions`` 和 ``desktop_sessions`` 签发,互相 +不能冒用:手机端拿不到归档上传之类的桌面接口,桌面端也调不了手机接口。 +""" + +from __future__ import annotations + +import asyncio +import ipaddress +import os +import threading +import time +from collections import defaultdict, deque +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request +from pydantic import BaseModel, Field + +import zyt_auth +from remote_store import ( + COMMAND_TYPES, + MOBILE_IDENTITY_RECHECK_SECONDS, + PERM_BLACKLIST_ADD, + PERM_BLACKLIST_REMOVE, + PERM_REMOTE_ALL, + PERM_REMOTE_CONTROL, + RemoteStore, + account_label, + account_permissions, + valid_device_id, +) + +# 桌面端长轮询最长挂起时间。uvicorn 停机时会等在途请求结束,这里必须明显短于 +# compose 里 20 秒的 stop_grace_period,否则每次发版都会被强杀;指令本身靠进程内 +# 通知即时送达,挂起时长只影响"没事时多久回来报一次状态"。 +MAX_POLL_WAIT_SECONDS = 15 +# 挂起期间多久回库复查一次;进程内通知失效(多进程部署)时的兜底。 +POLL_RECHECK_SECONDS = 5 + + +def _error(status_code: int, code: str, message: str, **extra: Any) -> HTTPException: + return HTTPException(status_code=status_code, detail={"code": code, "message": message, **extra}) + + +def _bearer(request: Request) -> str: + raw = request.headers.get("authorization", "") + return raw[7:].strip() if raw.lower().startswith("bearer ") else "" + + +def trusted_client_ip(request: Request) -> str: + """做安全判断用的客户端地址。 + + X-Forwarded-For 的第一段是客户端自己填的,可以伪造;X-Real-IP 由 Nginx 按 + $remote_addr 覆盖写入,API 端口又只监听本机回环,只有 Nginx 能连进来。 + """ + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return real_ip + return request.client.host if request.client else "" + + +def _is_private_address(value: str) -> bool: + try: + address = ipaddress.ip_address(str(value or "").strip()) + except ValueError: + return False + return address.is_private or address.is_loopback or address.is_link_local + + +def request_scheme(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-proto", "").split(",")[0].strip().lower() + return forwarded or request.url.scheme.lower() + + +def https_required(request: Request) -> bool: + """手机端登录是否必须走 HTTPS。 + + MOBILE_REQUIRE_HTTPS:never(默认,内外网都允许明文 HTTP 登录,按用户要求)/ + auto(公网地址必须 HTTPS,内网可用 HTTP)/ always。配好 HTTPS 入口后建议改成 + auto:明文 HTTP 下 ZYT 密码和手机会话令牌在公网上可被截获。 + + auto 模式下没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker + 网桥(172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 + (开发时直接访问 127.0.0.1)才在没有这个头时被信任。 + """ + policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "never").strip().lower() + if policy == "never" or request_scheme(request) == "https": + return False + if policy == "always": + return True + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return not _is_private_address(real_ip) + peer = request.client.host if request.client else "" + try: + return not ipaddress.ip_address(peer).is_loopback + except ValueError: + return True + + +class LoginAttempt: + __slots__ = ("ip", "account", "finished") + + def __init__(self, ip: str, account: str): + self.ip, self.account, self.finished = ip, account, False + + +class MobileLoginGuard: + """手机登录限流。 + + ZYT 按请求来源 IP 计数锁定(默认 30 分钟内错 5 次锁 30 分钟),而手机端的 + 登录全部由本服务代发,在 ZYT 看来都来自服务器出口 IP——如果公司电脑也走同一 + 个出口,锁住的就是全公司的 ZYT 登录。所以这里除了按账号、按客户端 IP 限流, + 还给"真正发到 ZYT 的密码错误"设了一个全局预算,并在 ZYT 已锁定时直接短路。 + + 限流必须把"正在进行"的尝试也算进去:先检查、等 ZYT 回来再记账的话,一次并发 + 发来十几个请求会全部通过检查,一起打到 ZYT 上。`begin()` 在锁里占位, + `finish()` 按结果转成失败记录或释放。 + """ + + def __init__( + self, *, account_limit: int = 5, account_window: int = 600, + ip_limit: int = 10, ip_window: int = 600, + upstream_budget: int | None = None, upstream_window: int = 1800, + ): + if upstream_budget is None: + try: + upstream_budget = int(os.environ.get("MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET", "4")) + except ValueError: + upstream_budget = 4 + self.account_limit, self.account_window = account_limit, account_window + self.ip_limit, self.ip_window = ip_limit, ip_window + self.upstream_budget, self.upstream_window = max(0, upstream_budget), upstream_window + self._accounts: dict[str, deque[float]] = {} + self._ips: dict[str, deque[float]] = {} + self._upstream: deque[float] = deque() + self._pending_accounts: dict[str, int] = defaultdict(int) + self._pending_ips: dict[str, int] = defaultdict(int) + self._pending_upstream = 0 + self._locked_until = 0.0 + self._lock = threading.Lock() + + @staticmethod + def _recent(book: dict[str, deque[float]], key: str, window: int, now: float) -> deque[float]: + """剪掉过期记录;空了就把这个键删掉,随机账号名刷不大内存。""" + items = book.get(key) + if items is None: + return deque() + while items and items[0] <= now - window: + items.popleft() + if not items: + del book[key] + return items + + @staticmethod + def _minutes(seconds: float) -> int: + return max(1, int((seconds + 59) // 60)) + + def begin(self, ip: str, account: str) -> LoginAttempt: + now = time.time() + with self._lock: + if self._locked_until > now: + wait = self._locked_until - now + raise _error( + 429, "zyt_locked", + f"ZYT 已暂时锁定登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + failures = self._recent(self._accounts, account, self.account_window, now) + if len(failures) >= self.account_limit: + wait = failures[0] + self.account_window - now + raise _error( + 429, "rate_limited", + f"该账号登录失败次数过多,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(failures) + self._pending_accounts.get(account, 0) >= self.account_limit: + raise _error(429, "rate_limited", "该账号正在登录,请稍候几秒再试", retry_after=5) + failures = self._recent(self._ips, ip, self.ip_window, now) + if len(failures) + self._pending_ips.get(ip, 0) >= self.ip_limit: + wait = (failures[0] + self.ip_window - now) if failures else 5 + raise _error( + 429, "rate_limited", + f"登录尝试过于频繁,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + while self._upstream and self._upstream[0] <= now - self.upstream_window: + self._upstream.popleft() + if self.upstream_budget: + if len(self._upstream) >= self.upstream_budget: + wait = self._upstream[0] + self.upstream_window - now + raise _error( + 429, "upstream_budget", + "近期手机端密码错误次数较多,为避免 ZYT 锁定服务器出口 IP," + f"已暂停手机登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(self._upstream) + self._pending_upstream >= self.upstream_budget: + raise _error(429, "rate_limited", "同时登录的人较多,请几秒后再试", retry_after=5) + self._pending_accounts[account] += 1 + self._pending_ips[ip] += 1 + self._pending_upstream += 1 + return LoginAttempt(ip, account) + + def finish(self, attempt: LoginAttempt, outcome: str) -> None: + """outcome:credentials(密码错)、unknown_account(账号不存在)、success(ZYT 认可了密码)、 + other(网络、终端未开通等,不记账)。""" + if attempt.finished: + return + attempt.finished = True + now = time.time() + with self._lock: + for book, key in ((self._pending_accounts, attempt.account), (self._pending_ips, attempt.ip)): + book[key] -= 1 + if book[key] <= 0: + del book[key] + self._pending_upstream = max(0, self._pending_upstream - 1) + if outcome in ("credentials", "unknown_account"): + self._accounts.setdefault(attempt.account, deque()).append(now) + self._ips.setdefault(attempt.ip, deque()).append(now) + # ZYT 只给"账号存在、密码不对"累计出口 IP 的失败次数。 + if outcome == "credentials": + self._upstream.append(now) + elif outcome == "success": + self._accounts.pop(attempt.account, None) + # 密码校验通过时 ZYT 会清零该出口 IP 的失败计数(AdminAccountSafeCache::relieve)。 + self._upstream.clear() + + def locked(self, seconds: int) -> None: + with self._lock: + self._locked_until = max(self._locked_until, time.time() + max(60, int(seconds))) + + +class ZytMobileAuth: + """默认实现:真的去调 ZYT。测试里换成假的。""" + + def __init__(self, api_url: Callable[[], str]): + self._api_url = api_url + + def login(self, account: str, password: str) -> str: + return zyt_auth.login_admin( + account, password, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def identity(self, token: str) -> dict[str, Any]: + return zyt_auth.verify_admin_session( + token, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def logout(self, token: str) -> None: + zyt_auth.logout_admin(token, api_url=self._api_url()) + + +class _Waiter: + __slots__ = ("key", "loop", "event") + + def __init__(self, key: tuple[int, str]): + self.key = key + self.loop = asyncio.get_running_loop() + self.event = asyncio.Event() + + async def wait(self, timeout: float) -> None: + try: + await asyncio.wait_for(self.event.wait(), timeout=max(0.0, timeout)) + except asyncio.TimeoutError: + pass + + +class CommandNotifier: + """进程内唤醒:有新指令或黑名单变化时,立刻结束对应电脑挂着的长轮询。 + + 先登记再查库:登记和查库之间落库的指令也能把它叫醒,不会白等一个复查周期。 + 唤醒走 ``call_soon_threadsafe``,发通知的请求和挂起的长轮询不在同一个事件 + 循环(线程)里也不会漏掉。 + """ + + def __init__(self) -> None: + self._waiters: dict[tuple[int, str], set[_Waiter]] = {} + self._lock = threading.Lock() + + def register(self, account_id: int, device_id: str) -> _Waiter: + waiter = _Waiter((int(account_id), str(device_id))) + with self._lock: + self._waiters.setdefault(waiter.key, set()).add(waiter) + return waiter + + def unregister(self, waiter: _Waiter) -> None: + with self._lock: + waiters = self._waiters.get(waiter.key) + if waiters is not None: + waiters.discard(waiter) + if not waiters: + self._waiters.pop(waiter.key, None) + + def notify(self, account_id: int, device_id: str | None = None) -> None: + with self._lock: + targets = [ + waiter + for key, waiters in self._waiters.items() + if key[0] == int(account_id) and (device_id is None or key[1] == device_id) + for waiter in waiters + ] + for waiter in targets: + try: + waiter.loop.call_soon_threadsafe(waiter.event.set) + except RuntimeError: + # 事件循环已经关闭(请求刚好结束),没有人需要被叫醒。 + continue + + +class MobilePrincipal: + def __init__(self, row: dict[str, Any], token: str): + self.row = row + self.token = token + self.permissions = account_permissions(row) + + @property + def account_id(self) -> int: + return int(self.row["id"]) + + @property + def session_id(self) -> int: + return int(self.row["session_id"]) + + @property + def label(self) -> str: + return f"{account_label(self.row)}(手机)" + + def me(self) -> dict[str, Any]: + return { + "account": { + "id": self.account_id, + "nickname": str(self.row.get("nickname") or ""), + "sn": str(self.row.get("zyt_sn") or ""), + "avatar": str(self.row.get("avatar") or ""), + }, + "permissions": { + "control": self.permissions["control"], + "control_all": self.permissions["control_all"], + "blacklist_add": self.permissions["blacklist_add"], + "blacklist_remove": self.permissions["blacklist_remove"], + "root": self.permissions["root"], + "known": self.permissions["known"], + }, + "permission_codes": { + "control": PERM_REMOTE_CONTROL, + "control_all": PERM_REMOTE_ALL, + "blacklist_add": PERM_BLACKLIST_ADD, + "blacklist_remove": PERM_BLACKLIST_REMOVE, + }, + "session": { + "created_at": str(self.row.get("session_created_at") or ""), + "expires_at": int(self.row.get("session_expires_at") or 0), + }, + } + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class MobileLoginBody(BaseModel): + account: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=1, max_length=128, repr=False) + client_name: str = Field(default="", max_length=120) + + +class CommandBody(BaseModel): + type: str = Field(min_length=1, max_length=32) + + +class BlacklistBody(BaseModel): + name: str = Field(default="", max_length=191) + account_name: str = Field(default="", max_length=191) + reason: str = Field(default="", max_length=200) + + +class StatusBody(BaseModel): + listening: bool = False + state: str = Field(default="", max_length=32) + transport: str = Field(default="", max_length=16) + wecom_connected: bool = False + pending_count: int = Field(default=0, ge=0, le=1_000_000) + app_version: str = Field(default="", max_length=64) + capabilities: list[str] = Field(default_factory=list, max_length=16) + + +class DesktopPollBody(BaseModel): + wait: int = Field(default=15, ge=0, le=60) + status: StatusBody | None = None + # 桌面端当前持有的黑名单版本;与服务端不一致时立即返回,让它去同步。 + blacklist_version: int | None = Field(default=None, ge=0) + + +class AckBody(BaseModel): + ok: bool + result: dict[str, Any] = Field(default_factory=dict) + error: str = Field(default="", max_length=500) + status: StatusBody | None = None + + +class DesktopBlacklistSyncBody(BaseModel): + add: list[dict[str, Any]] = Field(default_factory=list, max_length=500) + + +def register_remote_routes( + app: FastAPI, + database: Any, + *, + current_desktop: Callable[..., Any], + zyt_api_url: Callable[[], str], + zyt_mobile_auth: Any = None, + login_guard: MobileLoginGuard | None = None, +) -> RemoteStore: + store = RemoteStore(database) + store.initialize() + app.state.remote_store = store + notifier = CommandNotifier() + app.state.remote_notifier = notifier + guard = login_guard or MobileLoginGuard() + auth = zyt_mobile_auth or ZytMobileAuth(zyt_api_url) + identity_locks: dict[int, asyncio.Lock] = {} + + # ── 手机端鉴权 ─────────────────────────────────────────────────────── + async def _recheck_identity(row: dict[str, Any], request: Request) -> dict[str, Any] | None: + # 同一会话并发的几个请求只复核一次;用完即删,锁表不随会话数增长。 + session_id = int(row["session_id"]) + lock = identity_locks.setdefault(session_id, asyncio.Lock()) + try: + async with lock: + return await _recheck_identity_locked(session_id, request) + finally: + if not lock.locked() and identity_locks.get(session_id) is lock: + identity_locks.pop(session_id, None) + + async def _recheck_identity_locked(session_id: int, request: Request) -> dict[str, Any] | None: + fresh = await asyncio.to_thread(store.mobile_session, _bearer(request)) + if fresh is None: + return None + if time.time() - int(fresh["identity_checked_at"] or 0) < MOBILE_IDENTITY_RECHECK_SECONDS: + return fresh + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, session_id) + try: + if not zyt_token: + raise zyt_auth.ZytLoginError("ZYT 会话缺失", kind="rejected") + identity = await asyncio.to_thread(auth.identity, zyt_token) + if str(identity.get("user_id") or "") != str(fresh["zyt_user_id"]): + raise zyt_auth.ZytLoginError("ZYT 身份与手机会话不一致", kind="rejected") + except zyt_auth.ZytAuthError as exc: + if getattr(exc, "kind", "") == "unavailable": + # ZYT 暂时连不上不算下线:沿用上次同步的权限,一分钟后再试。 + await asyncio.to_thread(store.mark_identity_checked, session_id, retry_in=60) + return fresh + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="zyt_invalid", ip=trusted_client_ip(request), + ) + return None + try: + await asyncio.to_thread(store.refresh_identity, session_id, identity) + except PermissionError: + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="account_disabled", ip=trusted_client_ip(request), + ) + return None + return await asyncio.to_thread(store.mobile_session, _bearer(request)) + + async def current_mobile(request: Request) -> MobilePrincipal: + token = _bearer(request) + row = await asyncio.to_thread(store.mobile_session, token) if token else None + if row is not None and time.time() - int(row["identity_checked_at"] or 0) >= MOBILE_IDENTITY_RECHECK_SECONDS: + row = await _recheck_identity(row, request) + if row is None: + raise _error(401, "session_expired", "登录已失效,请重新登录") + return MobilePrincipal(row, token) + + def target_account(principal: MobilePrincipal, account_id: int | None) -> dict[str, Any]: + if account_id is None or int(account_id) == principal.account_id: + return principal.row + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", "只能查看和控制自己的电脑") + account = store.account(int(account_id)) + if account is None or str(account.get("status")) != "active": + raise _error(404, "account_not_found", "客服账号不存在或已停用") + return account + + # ── 手机端:登录 ───────────────────────────────────────────────────── + @app.post("/api/v2/mobile/auth/login") + async def mobile_login(body: MobileLoginBody, request: Request) -> dict: + ip = trusted_client_ip(request) + if https_required(request): + raise _error(403, "https_required", "请通过 HTTPS 地址访问手机控制台,明文连接不能提交密码") + account_key = body.account.strip().lower() + attempt = guard.begin(ip, account_key) + outcome = "other" + try: + zyt_token = await asyncio.to_thread(auth.login, body.account.strip(), body.password) + outcome = "success" + except zyt_auth.ZytLoginError as exc: + if exc.kind in ("credentials", "unknown_account"): + outcome = exc.kind + database.audit(None, "mobile.login.failed", f"account={account_key[:64]}", ip) + raise _error(401, "invalid_credentials", "账号或密码错误") from exc + if exc.kind == "password_change_required": + outcome = "success" + raise _error(403, "password_change_required", str(exc)) from exc + if exc.kind == "locked": + guard.locked(exc.retry_after or 1800) + database.audit(None, "mobile.login.locked", f"account={account_key[:64]}", ip) + raise _error(429, "zyt_locked", str(exc), retry_after=exc.retry_after) from exc + if exc.kind == "disabled": + raise _error(403, "account_disabled", str(exc)) from exc + if exc.kind == "terminal_unsupported": + raise _error(503, "terminal_unsupported", str(exc)) from exc + if exc.kind == "unavailable": + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + raise _error(400, "login_rejected", str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + finally: + guard.finish(attempt, outcome) + try: + identity = await asyncio.to_thread(auth.identity, zyt_token) + account = await asyncio.to_thread(store.upsert_account, identity) + except zyt_auth.ZytLoginError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + status_code = {"terminal_unsupported": 503, "disabled": 403}.get(exc.kind, 502) + code = {"terminal_unsupported": "terminal_unsupported", + "disabled": "account_disabled"}.get(exc.kind, "zyt_unavailable") + raise _error(status_code, code, str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + except PermissionError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(403, "account_disabled", str(exc)) from exc + except ValueError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + token, session = await asyncio.to_thread( + store.create_mobile_session, int(account["id"]), zyt_token=zyt_token, + client_name=body.client_name, user_agent=request.headers.get("user-agent", ""), ip=ip, + ) + row = await asyncio.to_thread(store.mobile_session, token) + principal = MobilePrincipal(row, token) + return {"access_token": token, "expires_at": session["expires_at"], "me": principal.me()} + + @app.post("/api/v2/mobile/auth/logout") + async def mobile_logout(request: Request, principal: MobilePrincipal = Depends(current_mobile)) -> dict: + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, principal.session_id) + await asyncio.to_thread( + store.revoke_mobile_session, session_id=principal.session_id, reason="logout", + ip=trusted_client_ip(request), + ) + if zyt_token: + await asyncio.to_thread(auth.logout, zyt_token) + return {"ok": True} + + @app.get("/api/v2/mobile/me") + async def mobile_me(principal: MobilePrincipal = Depends(current_mobile)) -> dict: + return principal.me() + + # ── 手机端:电脑 ───────────────────────────────────────────────────── + @app.get("/api/v2/mobile/accounts") + async def mobile_accounts( + q: str = Query(default="", max_length=64), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", f"查看全部客服电脑需要 ZYT 权限 {PERM_REMOTE_ALL}") + return {"items": await asyncio.to_thread(store.accounts_overview, q)} + + @app.get("/api/v2/mobile/desktops") + async def mobile_desktops( + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + items = await asyncio.to_thread(store.devices, int(account["id"])) + return { + "account": { + "id": int(account["id"]), + "nickname": str(account.get("nickname") or ""), + "sn": str(account.get("zyt_sn") or ""), + }, + "items": items, + "server_time": int(time.time()), + } + + @app.post("/api/v2/mobile/desktops/{device_id}/commands") + async def mobile_create_command( + device_id: str, body: CommandBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + if not principal.permissions["control"]: + raise _error(403, "forbidden", f"远程启停需要 ZYT 权限 {PERM_REMOTE_CONTROL}") + if body.type not in COMMAND_TYPES: + raise _error(400, "invalid_command", "不支持的指令") + try: + device_id = valid_device_id(device_id) + except ValueError as exc: + raise _error(404, "device_not_found", "电脑不存在") from exc + device = await asyncio.to_thread(store.device, int(account["id"]), device_id) + if device is None: + raise _error(404, "device_not_found", "电脑不存在或已被管理端下线") + if not device["online"]: + raise _error(409, "device_offline", "电脑离线:客服软件没有运行,无法远程拉起") + if not device["remote_capable"]: + raise _error(409, "client_outdated", "该电脑上的客服软件版本过低,请升级后再远程控制") + try: + command = await asyncio.to_thread( + store.create_command, account_id=int(account["id"]), device_id=device_id, + command_type=body.type, actor_account_id=principal.account_id, + actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "device_not_found", str(exc)) from exc + notifier.notify(int(account["id"]), device_id) + return {"command": command} + + @app.get("/api/v2/mobile/commands/{command_id}") + async def mobile_command( + command_id: str, principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + command = await asyncio.to_thread(store.command, command_id) + if command is None: + raise _error(404, "command_not_found", "指令不存在") + if command["account_id"] != principal.account_id and not principal.permissions["control_all"]: + raise _error(404, "command_not_found", "指令不存在") + command.pop("account_id", None) + return {"command": command} + + # ── 手机端:会话与黑名单 ───────────────────────────────────────────── + @app.get("/api/v2/mobile/conversations") + async def mobile_conversations( + account_id: int | None = Query(default=None, ge=1), + q: str = Query(default="", max_length=100), + scope: str = Query(default="all", pattern="^(all|blocked)$"), + cursor: str = Query(default="", max_length=200), + limit: int = Query(default=30, ge=1, le=100), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + try: + return await asyncio.to_thread( + store.conversations, str(account["tenant_id"]), query=q, scope=scope, + cursor=cursor, limit=limit, + ) + except ValueError as exc: + raise _error(400, "invalid_cursor", str(exc)) from exc + + @app.put("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_add( + account: str, conversation: str, body: BlacklistBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_add"]: + raise _error(403, "forbidden", f"加入黑名单需要 ZYT 权限 {PERM_BLACKLIST_ADD}") + try: + entry, version = await asyncio.to_thread( + store.add_blacklist, str(target["tenant_id"]), + {"account": account, "conversation": conversation, **body.model_dump()}, + source="mobile", actor_label=principal.label, + actor_account_id=principal.account_id, ip=trusted_client_ip(request), + ) + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"entry": entry, "version": version} + + @app.delete("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_remove( + account: str, conversation: str, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_remove"]: + raise _error(403, "forbidden", f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管") + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(target["tenant_id"]), account, conversation, + source="mobile", actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"ok": True, "version": version} + + # ── 桌面端:命令通道 ───────────────────────────────────────────────── + def _desktop_label(account: Any) -> str: + return f"{account_label(account)}(电脑 {str(account['device_name'] or '')[:40]})" + + async def _report(account: Any, status: StatusBody | None) -> None: + if status is None: + return + await asyncio.to_thread( + store.report_status, int(account["id"]), str(account["device_id"]), + status.model_dump(), app_version=str(account["app_version"] or ""), + ) + + @app.post("/api/v2/desktop/commands/poll") + async def desktop_poll( + body: DesktopPollBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + account_id = int(account["id"]) + device_id = str(account["device_id"]) + tenant = str(account["tenant_id"]) + await _report(account, body.status) + wait = float(min(body.wait, MAX_POLL_WAIT_SECONDS)) + deadline = time.monotonic() + wait + while True: + waiter = notifier.register(account_id, device_id) + try: + commands = await asyncio.to_thread(store.take_commands, account_id, device_id) + version = await asyncio.to_thread(store.blacklist_version, tenant) + remaining = deadline - time.monotonic() + if ( + commands + or (body.blacklist_version is not None and version != body.blacklist_version) + or remaining <= 0 + or await request.is_disconnected() + ): + break + await waiter.wait(min(remaining, POLL_RECHECK_SECONDS)) + finally: + notifier.unregister(waiter) + return {"commands": commands, "blacklist_version": version, "server_time": int(time.time())} + + @app.post("/api/v2/desktop/commands/{command_id}/ack") + async def desktop_ack( + command_id: str, body: AckBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + await _report(account, body.status) + try: + command = await asyncio.to_thread( + store.ack_command, account_id=int(account["id"]), + device_id=str(account["device_id"]), command_id=command_id, ok=body.ok, + result=body.result, error=body.error, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "command_not_found", str(exc)) from exc + return {"ok": True, "command": command} + + @app.post("/api/v2/desktop/status") + async def desktop_status(body: StatusBody, account: Any = Depends(current_desktop)) -> dict: + await _report(account, body) + return {"ok": True} + + # ── 桌面端:黑名单 ─────────────────────────────────────────────────── + @app.get("/api/v2/desktop/blacklist") + async def desktop_blacklist(account: Any = Depends(current_desktop)) -> dict: + version, items = await asyncio.to_thread(store.blacklist, str(account["tenant_id"])) + return {"version": version, "items": items} + + @app.post("/api/v2/desktop/blacklist/sync") + async def desktop_blacklist_sync( + body: DesktopBlacklistSyncBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + # 电脑前的人加黑名单不要求 ZYT 权限:拉黑只会让系统少发消息,是安全方向; + # 离线时加的、升级前本地就有的条目也都从这里补传。 + before = await asyncio.to_thread(store.blacklist_version, str(account["tenant_id"])) + version, items, rejected = await asyncio.to_thread( + store.sync_desktop_blacklist, str(account["tenant_id"]), body.add, + actor_label=_desktop_label(account), actor_account_id=int(account["id"]), + ip=trusted_client_ip(request), + ) + if version != before: + notifier.notify(int(account["id"])) + return {"version": version, "items": items, "rejected": rejected} + + @app.delete("/api/v2/desktop/blacklist/{account_key}/{conversation:path}") + async def desktop_blacklist_remove( + account_key: str, conversation: str, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + if not account_permissions(account)["blacklist_remove"]: + raise _error( + 403, "forbidden", + f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管在手机控制台操作", + ) + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(account["tenant_id"]), account_key, conversation, + source="desktop", actor_label=_desktop_label(account), ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(account["id"])) + return {"ok": True, "version": version} + + return store diff --git a/deploy/mobile-http-20260928/server-bundle/release-plan.json b/deploy/mobile-http-20260928/server-bundle/release-plan.json new file mode 100644 index 000000000..e57b78740 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/release-plan.json @@ -0,0 +1,23 @@ +{ + "release": "mobile-http-20260928", + "candidateRoot": "C:/kefu", + "backendFiles": [ + "wechat_rpa/remote_api.py" + ], + "newBackendFiles": [], + "deployFiles": [], + "frontendDir": "admin-web/apps/mobile-h5/dist", + "serverTestModules": [ + "test_remote_api", + "test_zyt_auth", + "test_backend" + ], + "imageTestModules": [ + "test_archive_api" + ], + "schemaMigration": "none", + "restartServices": [ + "api" + ], + "notes": "User request 2026-09-28: the phone console must also work over plain HTTP from outside the office. MOBILE_REQUIRE_HTTPS now defaults to never (auto/always still available once HTTPS exists); the /m/ login page no longer shows the intranet-only banner. No database change." +} diff --git a/deploy/mobile-http-20260928/server-bundle/remote_server_guard.py b/deploy/mobile-http-20260928/server-bundle/remote_server_guard.py new file mode 100644 index 000000000..2187ccf7e --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/remote_server_guard.py @@ -0,0 +1,138 @@ +"""Read-only checks for mobile-http-20260928: baseline / candidate / deployed. + +Prints hashes and status only; never reads chat content, patients or credentials. +""" +from pathlib import Path +import argparse, hashlib, json, subprocess, urllib.error, urllib.request + +parser = argparse.ArgumentParser() +parser.add_argument('mode', choices=['baseline', 'candidate', 'deployed']) +args = parser.parse_args() +RELEASE = 'mobile-http-20260928' +BANNER = '只能在公司内网登录' +folder = Path(__file__).resolve().parent +project = Path('/opt/im-admin') +base = json.loads((folder / 'server-baseline.json').read_text()) +plan = json.loads((folder / 'release-plan.json').read_text()) +manifest = json.loads((folder / 'manifest.json').read_text()) +payload_rows = [row for row in manifest if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +sha = lambda data: hashlib.sha256(data).hexdigest() +image = lambda ref: subprocess.check_output(['docker', 'image', 'inspect', ref, '--format', '{{.Id}}'], text=True).strip() + +# The unpacked bundle itself must be intact. +for row in manifest: + path = (folder / row['path']).resolve() + assert path.is_relative_to(folder), row['path'] + assert path.stat().st_size == row['bytes'] and sha(path.read_bytes()) == row['sha256'], row['path'] + +# project-relative path -> expected sha256 on the host +if args.mode == 'baseline': + expected_host = dict(base['expected']) +else: + expected_host = {row['path'][len('payload/'):]: row['sha256'] for row in payload_rows} +container_code = {k: v for k, v in expected_host.items() if k.startswith(('wechat_rpa/', 'admin-web/'))} +frontend_files = sorted(k[len(frontend) + 1:] for k in expected_host if k.startswith(frontend + '/')) + + +def in_container(code, target, ref=None): + command = (['docker', 'run', '--rm', '-i', '--network=none', '--entrypoint', 'python', ref, '-c', code] + if ref else ['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code]) + return json.loads(subprocess.check_output(command, input=json.dumps(target).encode())) + + +HASHES = ('from pathlib import Path;import hashlib,json,sys;names=json.load(sys.stdin);' + 'print(json.dumps({n:(hashlib.sha256((Path("/app")/n).read_bytes()).hexdigest() ' + 'if (Path("/app")/n).is_file() else None) for n in names}))') +# /m/ 目录里的完整文件清单(不能残留旧版带横幅的页面文件)和横幅文字检查。 +FRONTEND = ('from pathlib import Path;import json,sys;q=json.load(sys.stdin);root=Path("/app")/q["root"];' + 'files=sorted(p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file());' + 'print(json.dumps({"files":files,"banner":[f for f in files if q["banner"] in ' + '(root/f).read_bytes().decode("utf-8","ignore")]}))') +FRONTEND_QUERY = {'root': frontend, 'banner': BANNER} + +current = image('zyt/wecom-admin:current') +result = {'mode': args.mode, 'release': RELEASE, 'image': current} +# 只重建 api:网关和知识库 worker 继续跑它们自己的镜像,部署后核对它们没有被动过。 +OTHERS = ['im-admin-gateway-1', 'im-admin-knowledge-worker-1'] + + +def inspect(container): + raw = json.loads(subprocess.check_output(['docker', 'inspect', container]))[0] + return {'id': raw['Id'], 'image': raw['Image'], 'running': raw['State']['Running'], + 'health': raw['State'].get('Health', {}).get('Status', 'not_configured')} + + +if args.mode == 'baseline': + api = inspect('im-admin-api-1') + assert api['image'] == current, 'API container is not on zyt/wecom-admin:current' + assert api['running'] and api['health'] == 'healthy', 'API container is not healthy' + others = {name: inspect(name) for name in OTHERS} + for name, state in others.items(): + assert state['running'], 'Not running: ' + name + result['others'] = others + +if args.mode in ('baseline', 'deployed'): + for name, wanted in expected_host.items(): + assert sha((project / name).read_bytes()) == wanted, 'Host drift: ' + name + actual = in_container(HASHES, sorted(container_code)) + assert actual == container_code, 'Running api container differs from expected code' + result['files'] = len(expected_host) + +if args.mode == 'candidate': + candidate = 'zyt/wecom-admin:' + RELEASE + actual = in_container(HASHES, sorted(container_code), candidate) + assert actual == container_code, 'Candidate image differs from manifest' + listing = in_container(FRONTEND, FRONTEND_QUERY, candidate) + assert listing['files'] == frontend_files, 'Candidate /m/ has extra or missing files' + assert not listing['banner'], listing['banner'] + result['candidate'] = image(candidate) + result['files'] = len(container_code) + +if args.mode == 'deployed': + wanted = image('zyt/wecom-admin:' + RELEASE) + assert current == wanted, 'current tag does not point at the release image' + raw = json.loads(subprocess.check_output(['docker', 'inspect', 'im-admin-api-1']))[0] + assert raw['Image'] == wanted and raw['State']['Running'] and raw['RestartCount'] == 0 + assert raw['State'].get('Health', {}).get('Status') == 'healthy' + recorded = folder / 'baseline-check.json' + if recorded.exists(): + before = json.loads(recorded.read_text())['others'] + for name in OTHERS: + now = inspect(name) + assert now['id'] == before[name]['id'] and now['running'], 'Unrelated container changed: ' + name + result['others_unchanged'] = OTHERS + host_files = sorted(p.relative_to(project / frontend).as_posix() for p in (project / frontend).rglob('*') if p.is_file()) + assert host_files == frontend_files, 'Host /m/ has extra or missing files' + listing = in_container(FRONTEND, FRONTEND_QUERY) + assert listing['files'] == frontend_files and not listing['banner'], listing + for url in ['http://127.0.0.1:18766/api/v2/health', 'http://127.0.0.1:18770/health']: + with urllib.request.urlopen(url, timeout=15) as response: + assert response.status == 200, url + # New routes still refuse anonymous callers. No login is attempted. + for method, path in [('GET', '/api/v2/mobile/me'), ('GET', '/api/v2/mobile/desktops'), + ('POST', '/api/v2/desktop/commands/poll'), ('GET', '/api/v2/desktop/blacklist')]: + request = urllib.request.Request('http://127.0.0.1:18766' + path, method=method, + data=b'{}' if method == 'POST' else None, + headers={'Content-Type': 'application/json'}) + try: + urllib.request.urlopen(request, timeout=15) + except urllib.error.HTTPError as exc: + assert exc.code == 401, (path, exc.code) + else: + raise AssertionError('Accessible without authentication: ' + path) + with urllib.request.urlopen('http://127.0.0.1:18766/m/', timeout=15) as response: + body = response.read() + assert response.status == 200 and '企微客服助手'.encode() in body, 'mobile console entry missing' + # 本次目的:公网来源的明文 HTTP 登录不再被拒绝(在运行中的容器里、用它的真实环境变量判断)。 + code = ("import remote_api,json;from starlette.requests import Request;" + "r=Request({'type':'http','method':'POST','path':'/api/v2/mobile/auth/login','root_path':''," + "'headers':[(b'x-real-ip',b'114.114.114.114')],'scheme':'http','query_string':b''," + "'client':('172.18.0.1',40000),'server':('127.0.0.1',8766)});" + "print(json.dumps({'public_http_login_refused':remote_api.https_required(r)}))") + policy = json.loads(subprocess.check_output(['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code])) + assert policy == {'public_http_login_refused': False}, policy + result['policy'] = policy + +result['status'] = 'passed' +print(json.dumps(result, ensure_ascii=False)) diff --git a/deploy/mobile-http-20260928/server-bundle/rollback.sh b/deploy/mobile-http-20260928/server-bundle/rollback.sh new file mode 100644 index 000000000..c78ea26dd --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/rollback.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# 恢复 remote_api.py 和原 /m/ 目录,切回旧镜像(mobile-remote-20260928),只重建 api。不涉及数据库。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-http-20260928 +release=/home/ps/mobile-http-20260928 +cd /opt/im-admin/deploy/im-admin +test -d "$release/source-before" +test "$(docker image inspect zyt/wecom-admin:before-mobile-http-20260928 --format '{{.Id}}')" = "$(cat "$release/image-before.txt")" +python3 - <<'PY' +from pathlib import Path +import hashlib, json, os, shutil +r = Path('/home/ps/mobile-http-20260928'); project = Path('/opt/im-admin') +base = json.loads((r / 'server-baseline.json').read_text()) +plan = json.loads((r / 'release-plan.json').read_text()) +sha = lambda data: hashlib.sha256(data).hexdigest() +for name in plan['backendFiles'] + plan['deployFiles']: + data = (r / 'source-before' / name).read_bytes(); assert sha(data) == base['expected'][name] + target = project / name; tmp = target.with_name(target.name + '.rollback-http.tmp') + if tmp.exists(): + tmp.unlink() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, target.stat().st_mode & 0o777); os.replace(tmp, target) +frontend = plan['frontendDir'] +saved = r / 'source-before' / frontend +for name, wanted in base['expected'].items(): + if name.startswith(frontend + '/'): + assert sha((saved / name[len(frontend) + 1:]).read_bytes()) == wanted, name +restore = project / (frontend + '.rollback-http.tmp') +if restore.exists(): + shutil.rmtree(restore) +shutil.copytree(saved, restore) +for leftover in [project / frontend, project / (frontend + '.mobile-http.tmp')]: + if leftover.exists(): + shutil.rmtree(leftover) +old = project / (frontend + '.mobile-http.old') +if old.exists(): + shutil.rmtree(old) +os.replace(restore, project / frontend) +PY +docker tag zyt/wecom-admin:before-mobile-http-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +curl -fsS -o /dev/null http://127.0.0.1:18766/api/v2/health +curl -fsS -o /dev/null http://127.0.0.1:18770/health +printf '%s\n' 'Previous image, remote_api.py and /m/ restored.' diff --git a/deploy/mobile-http-20260928/server-bundle/server-baseline.json b/deploy/mobile-http-20260928/server-bundle/server-baseline.json new file mode 100644 index 000000000..c587fe501 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/server-baseline.json @@ -0,0 +1,40 @@ +{ + "host": "192.168.110.111", + "user": "ps", + "project": "/opt/im-admin", + "compose": "/opt/im-admin/deploy/im-admin", + "checked_at": "2026-09-28", + "source": "Production as deployed by mobile-remote-20260928 (15:21): remote_api.py and the 28 /m/ files from that release payload; remote_api.py hash re-read from the host before building this release.", + "expected": { + "admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js": "97abcbc7c960add5c448947dad40ea8ca6ca2ae18069a632b62b61723e43f24f", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js": "8c06092db48aca97b0d3fb35871315e4434b5ad5ccd9c5d44a7cfc3ba343db22", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js": "7a648b566b9c00c1c8d1b6d2ca9737ca237e521f0e25065cedf86e82fa2471d8", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js": "c145f55146ff5373b293e2922b021f55c6851fa34db65fdacb64139fb88c4fa8", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js": "ddb9c705e5cc4822e9166686a7eab319c772540981134f30a9f850fc85ac8692", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js": "393394f3545f3224264be9d1eb87c72b6ce474651b3794b3839f43bfbd69aa86", + "admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js": "d6a7fc2e067a2adcf92b498812422755d5ac8f00acfcf07fd935201eee35c056", + "admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js": "63a10401ab035b495ba24c55f9b1bd7ef54c76c6d6bc549f4b0eda62dbd915ac", + "admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css": "b561a20e6d76366db7634b028098ed9f848f665b95f0ffe538f02abce87e8991", + "admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js": "ded80bbda052a9da5f7beaab6af536258b706f2bf0688331fa8bccd9e23355c6", + "admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js": "711a690039d1fe0d648c285da7bd1ef5b62afe8249d10ad7e76208318fc3094d", + "admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js": "8a2afb226d06ece5a929c2c85c4c73bbf68977b4ccde960aa7f6a5ae4614af62", + "admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js": "168066192a95a203466245545388b756dfd035c466b007e65ccadc1c94f3fd80", + "admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "admin-web/apps/mobile-h5/dist/icons/favicon-64.png": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "admin-web/apps/mobile-h5/dist/icons/icon-192.png": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "admin-web/apps/mobile-h5/dist/icons/icon-512.png": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "admin-web/apps/mobile-h5/dist/index.html": "d11023af89db5c20bde0c4eaaa35fd9ad192a85353165c0a675e139ae5d94f13", + "admin-web/apps/mobile-h5/dist/manifest.webmanifest": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "admin-web/apps/mobile-h5/dist/sw.js": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "wechat_rpa/remote_api.py": "843f0a814a0de4f8078f7f9f23a4ac92388d7556c9fe378987f67eda46c55c5b" + }, + "must_not_exist": [] +} diff --git a/deploy/mobile-http-20260928/server-bundle/tests/test_backend.py b/deploy/mobile-http-20260928/server-bundle/tests/test_backend.py new file mode 100644 index 000000000..6a460176b --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/tests/test_backend.py @@ -0,0 +1,1527 @@ +# -*- coding: utf-8 -*- +"""配置后台与桌面端同步的本地闭环测试。""" + +from __future__ import annotations + +import json +import hashlib +import shutil +import model_protocol +import socket +import sqlite3 +import tempfile +import threading +import unittest +import urllib.error +import urllib.request +from unittest import mock +from pathlib import Path + +from test_support import local_state_redirect +import admin_backend +import app_version +import backend_client +import zyt_auth + + +class BackendIntegrationTest(unittest.TestCase): + + + def test_development_diagnostics_are_cloud_controlled_and_redacted(self) -> None: + config = { + "AI_DEVELOPMENT_MODE": True, + "AI_PROVIDER_TYPE": "openai", + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_MCP_SERVERS": [ + { + "headers": {"Authorization": "Bearer hidden-auth"}, + "env": {"ACCESS_TOKEN": "hidden-token"}, + "args": [ + "--token", + "hidden-argument", + "--password=hidden-inline", + "Bearer hidden-bearer", + ], + } + ], + } + response = {"version": 8, "updated_at": "now"} + diagnostics = backend_client._config_diagnostics( + config, + response, + "https://cloud.example/api/v1/desktop/config?api_key=query-secret", + ) + rendered = "\n".join(diagnostics) + self.assertIn("https://cloud.example/api/v1/desktop/config", rendered) + self.assertIn("云端配置版本: v8", rendered) + self.assertIn('"AI_MODEL": "test-model"', rendered) + for secret in ( + "top-secret-key", + "hidden-auth", + "hidden-token", + "hidden-argument", + "hidden-inline", + "hidden-bearer", + "query-secret", + ): + self.assertNotIn(secret, rendered) + self.assertEqual( + backend_client._config_diagnostics( + {**config, "AI_DEVELOPMENT_MODE": False}, response, "https://cloud.example/config" + ), + [], + ) + + def test_model_request_diagnostics_never_print_the_api_key(self) -> None: + import ai_chat + import ai_config + + previous = { + "AI_DEVELOPMENT_MODE": getattr(ai_config, "AI_DEVELOPMENT_MODE", False), + "AI_API_BASE": ai_config.AI_API_BASE, + "AI_API_KEY": ai_config.AI_API_KEY, + "AI_MODEL": ai_config.AI_MODEL, + } + try: + ai_config.AI_DEVELOPMENT_MODE = True + ai_config.AI_API_BASE = "https://api.example/v1" + ai_config.AI_API_KEY = "never-print-this-key" + ai_config.AI_MODEL = "diagnostic-model" + with mock.patch("builtins.print") as printer: + ai_chat._log_request_diagnostics( + "https://api.example/v1/chat/completions", "OpenAI 兼容" + ) + rendered = "\n".join( + " ".join(str(item) for item in call.args) + for call in printer.call_args_list + ) + self.assertIn("https://api.example/v1/chat/completions", rendered) + self.assertIn("diagnostic-model", rendered) + self.assertNotIn("never-print-this-key", rendered) + finally: + for key, value in previous.items(): + setattr(ai_config, key, value) + + def test_model_connection_uses_unsaved_values_and_saved_key(self) -> None: + current = { + "AI_API_BASE": "https://saved.example/v1", + "AI_API_KEY": "saved-secret", + "AI_MODEL": "saved-model", + "AI_TIMEOUT": 120, + } + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://new.example/v1", + "AI_API_KEY": "", + "AI_MODEL": "new-model", + "AI_TIMEOUT": "180", + }, + current, + ) + self.assertEqual(config["endpoint"], "https://new.example/v1/chat/completions") + self.assertEqual(config["api_key"], "saved-secret") + self.assertEqual(config["model"], "new-model") + self.assertEqual(config["timeout"], 60) + + def test_model_connection_success_does_not_expose_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"choices": [{"message": {"content": "OK"}}]} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(200, response) + ) as call: + result = admin_backend.test_model_connection(config) + self.assertTrue(result["ok"]) + self.assertEqual(result["http_status"], 200) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + self.assertEqual(call.call_args.args[0], config["endpoint"]) + self.assertEqual( + call.call_args.kwargs["headers"]["Authorization"], "Bearer top-secret-key" + ) + payload = call.call_args.kwargs["payload"] + self.assertEqual(payload["model"], "test-model") + + def test_model_connection_error_redacts_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"error": {"message": "invalid top-secret-key"}} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(401, response) + ): + result = admin_backend.test_model_connection(config) + self.assertFalse(result["ok"]) + self.assertEqual(result["http_status"], 401) + self.assertIn("API Key 无效", result["message"]) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + + def test_dify_and_comfyui_use_provider_specific_endpoints(self) -> None: + dify = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "dify", + "AI_API_BASE": "https://dify.example/v1", + "AI_API_KEY": "app-secret", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(dify["endpoint"], "https://dify.example/v1/chat-messages") + self.assertEqual(dify["provider_type"], "dify") + + comfyui = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "comfyui", + "AI_API_BASE": "http://127.0.0.1:8188", + "AI_API_KEY": "", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(comfyui["endpoint"], "http://127.0.0.1:8188/system_stats") + with mock.patch( + "admin_backend._perform_http_request", + return_value=(200, b'{"system": {"os": "windows"}, "devices": []}'), + ) as call: + result = admin_backend.test_model_connection(comfyui) + self.assertTrue(result["ok"]) + self.assertEqual(call.call_args.kwargs["method"], "GET") + self.assertIsNone(call.call_args.kwargs["payload"]) + + def test_desktop_ai_respects_explicit_dify_provider(self) -> None: + import ai_chat + import ai_config + + old_provider = ai_config.AI_PROVIDER_TYPE + old_base = ai_config.AI_API_BASE + try: + ai_config.AI_PROVIDER_TYPE = "dify" + ai_config.AI_API_BASE = "https://dify.example/v1" + self.assertTrue(ai_chat._is_dify_endpoint()) + self.assertEqual( + ai_chat._completions_url(), + "https://dify.example/v1/chat-messages", + ) + ai_config.AI_PROVIDER_TYPE = "openai" + self.assertFalse(ai_chat._is_dify_endpoint()) + finally: + ai_config.AI_PROVIDER_TYPE = old_provider + ai_config.AI_API_BASE = old_base + + + def test_release_status_detects_optional_and_forced_updates(self) -> None: + optional = app_version.release_status( + {"latest_version": "99.0.0", "force_upgrade": False} + ) + self.assertTrue(optional["update_available"]) + self.assertFalse(optional["force_upgrade"]) + + forced = app_version.release_status( + {"latest_version": "2.0.0", "force_upgrade": True} + ) + self.assertTrue(forced["force_upgrade"]) + + current = app_version.release_status( + {"latest_version": app_version.APP_VERSION, "force_upgrade": True} + ) + self.assertFalse(current["update_available"]) + self.assertFalse(current["force_upgrade"]) + + downgrade = app_version.release_status( + {"latest_version": "1.0.1", "force_upgrade": True} + ) + self.assertFalse(downgrade["update_available"]) + self.assertFalse(downgrade["force_upgrade"]) + + def test_release_form_validation(self) -> None: + release = admin_backend.validate_release_form( + { + "latest_version": "v1.2.3", + "download_url": "https://example.com/client.exe", + "release_notes": "修复已知问题", + "force_upgrade": "1", + } + ) + self.assertEqual(release["latest_version"], "1.2.3") + self.assertTrue(release["force_upgrade"]) + automatic = admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "package_sha256": "a" * 64, + "package_size": "1024", + "auto_install": "1", + "release_channel": "stable", + "rollout_percent": "25", + "status": "published", + } + ) + self.assertTrue(automatic["auto_install"]) + self.assertEqual(automatic["rollout_percent"], 25) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2", "force_upgrade": "1"} + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "auto_install": "1", + } + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2.3", "force_upgrade": "1"} + ) + + def test_pbkdf2_fallback_matches_standard_library(self) -> None: + password = b"FallbackPassword123" + salt = b"0123456789abcdef" + expected = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000) + native_pbkdf2 = admin_backend.hashlib.pbkdf2_hmac + try: + admin_backend.hashlib.pbkdf2_hmac = None + actual = admin_backend.pbkdf2_sha256(password, salt, 1_000) + finally: + admin_backend.hashlib.pbkdf2_hmac = native_pbkdf2 + self.assertEqual(actual, expected) + + def test_stale_runtime_file_is_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + runtime_file = root / "backend_runtime.json" + runtime_file.write_text( + json.dumps( + { + "pid": 0, + "host": "127.0.0.1", + "port": 8765, + "server_url": "http://127.0.0.1:8765", + "local_sync_token": "stale-token", + } + ), + encoding="utf-8", + ) + old_runtime_file = backend_client.RUNTIME_FILE + try: + backend_client.RUNTIME_FILE = runtime_file + self.assertEqual(backend_client.discover_local_runtime(), {}) + self.assertEqual(backend_client.discover_local_sync_token(), "") + finally: + backend_client.RUNTIME_FILE = old_runtime_file + + def test_local_agent_name_overrides_cloud_and_blank_follows_cloud(self) -> None: + import ai_config + + old_agent = ai_config.AI_AGENT_NAME + old_cloud = ai_config.AI_CLOUD_AGENT_NAME + old_local = ai_config.AI_LOCAL_AGENT_NAME + old_prompt = ai_config.AI_SYSTEM_PROMPT + try: + ai_config.AI_CLOUD_AGENT_NAME = "旧云端客服" + ai_config.AI_LOCAL_AGENT_NAME = "" + ai_config.AI_AGENT_NAME = "旧云端客服" + ai_config.set_local_agent_name_override("本机客服", persist=False) + ai_config.apply_settings({"AI_AGENT_NAME": "新云端客服"}, persist=False) + self.assertEqual(ai_config.AI_CLOUD_AGENT_NAME, "新云端客服") + self.assertEqual(ai_config.AI_AGENT_NAME, "本机客服") + ai_config.set_local_agent_name_override("", persist=False) + self.assertEqual(ai_config.AI_AGENT_NAME, "新云端客服") + finally: + ai_config.AI_AGENT_NAME = old_agent + ai_config.AI_CLOUD_AGENT_NAME = old_cloud + ai_config.AI_LOCAL_AGENT_NAME = old_local + ai_config.AI_SYSTEM_PROMPT = old_prompt + + +class DesktopConfigFallbackTest(unittest.TestCase): + """桌面端拉配置:新接口优先,老接口兜底。 + + 客户端和服务端是分批更新的,两边版本对不齐是常态而不是意外。直接把路径改 + 成 v2,那些服务端还没升级的部署会当场同步失败——而且失败得很安静,界面上 + 只是配置一直显示旧的,没人会立刻发现。 + """ + + def _fail(self, message: str): + return backend_client.BackendError(message) + + def test_startup_auth_config_is_read_from_management_and_cached(self) -> None: + calls = [] + saved = {} + + def fake(method, server, path, **kwargs): + calls.append((method, server, path)) + return 200, { + "zyt_api_url": "https://account.example.com/zyt/", + "terminal": 7, + "configured": True, + } + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", lambda: {}), + mock.patch.object(backend_client, "save_settings", saved.update), + ): + config = backend_client.fetch_desktop_auth_config("https://admin.example.com/") + + self.assertEqual( + calls, + [("GET", "https://admin.example.com", "/api/v2/desktop/auth/config")], + ) + self.assertEqual(config["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["server_url"], "https://admin.example.com") + + def test_startup_auth_config_rejects_an_incomplete_response(self) -> None: + with mock.patch.object( + backend_client, "_request", return_value=(200, {"terminal": 7}) + ): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client.fetch_desktop_auth_config("https://admin.example.com") + self.assertIn("登录 API", str(caught.exception)) + + def test_zyt_admin_login_uses_admin_endpoint_and_desktop_terminal(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append((method, server, path, kwargs.get("payload"))) + return 200, { + "code": 1, + "msg": "登录成功", + "data": {"token": "admin-desktop-token", "name": "管理员"}, + } + + with mock.patch.object(backend_client, "_request", fake): + token, user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(token, "admin-desktop-token") + self.assertEqual(user["name"], "管理员") + self.assertEqual(calls[0][2], "/adminapi/login/account") + self.assertEqual(calls[0][3]["terminal"], 7) + self.assertEqual(calls[0][3]["account"], "admin") + + def test_zyt_admin_login_falls_back_while_server_has_no_terminal_7(self) -> None: + terminals = [] + + def fake(method, server, path, **kwargs): + terminal = kwargs["payload"]["terminal"] + terminals.append(terminal) + if terminal == 7: + return 200, {"code": 0, "msg": "terminal必须在 1,2 范围内"} + return 200, {"code": 1, "data": {"token": "legacy-admin-token"}} + + with mock.patch.object(backend_client, "_request", fake): + token, _user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(terminals, [7, 1]) + self.assertEqual(token, "legacy-admin-token") + + def test_desktop_login_can_remember_password_with_dpapi_protection(self) -> None: + settings = backend_client.default_settings() + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, + "_protect_secret", + side_effect=lambda value: f"protected:{value}", + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=True, + ) + + self.assertTrue(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["zyt_account"], "admin") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:secret" + ) + self.assertNotIn("secret", json.dumps(saved[-1], ensure_ascii=False).replace( + "protected:secret", "" + )) + + def test_desktop_login_unchecked_forgets_the_saved_password(self) -> None: + settings = backend_client.default_settings() + settings["remembered_password_protected"] = "protected:old-password" + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected:token" + ), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=False, + ) + + self.assertFalse(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["remembered_password_protected"], "") + + def test_forced_logout_keeps_explicitly_remembered_password(self) -> None: + settings = backend_client.default_settings() + settings.update( + { + "zyt_account": "admin", + "remember_login": True, + "remembered_password_protected": "protected:password", + "zyt_token_protected": "protected:zyt-token", + "desktop_token_protected": "protected:desktop-token", + } + ) + saved = [] + with ( + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + ): + backend_client._clear_desktop_credentials(settings) + + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:password" + ) + + def test_session_heartbeat_never_silently_reauthenticates(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + } + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object(backend_client, "save_settings"), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object( + backend_client, + "_request", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + with self.assertRaises(backend_client.AuthenticationError): + backend_client.validate_desktop_session() + exchange.assert_not_called() + + def test_session_heartbeat_persists_the_renewed_expiration(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "desktop_expires_at": 1200, + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_request", + return_value=( + 200, + { + "id": 1, + "sn": "admin", + "expires_at": 50_000, + }, + ), + ), + ): + account = backend_client.validate_desktop_session() + + self.assertEqual(account["id"], 1) + self.assertEqual(saved[-1]["desktop_expires_at"], 50_000) + + def test_revoked_unexpired_session_requires_password_again(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 2000, + "zyt_account": "admin", + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "validate_desktop_session", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + self.assertIsNone(backend_client.restore_desktop_session()) + + exchange.assert_not_called() + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["zyt_account"], "admin") + + def test_naturally_expired_session_can_refresh_from_zyt(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-old", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 999, + } + saved = [] + + def unprotect(value): + return { + "protected-old": "old-token", + "protected-zyt": "zyt-token", + }.get(value, "") + + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object(backend_client, "_unprotect_secret", side_effect=unprotect), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected-new" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "new-token", + "expires_in": 1800, + "account": {"id": 1, "sn": "admin"}, + }, + ), + ): + account = backend_client.restore_desktop_session() + + self.assertEqual(account, {"id": 1, "sn": "admin"}) + self.assertEqual(saved[-1]["desktop_token_protected"], "protected-new") + self.assertEqual(saved[-1]["desktop_expires_at"], 2800) + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"config": {}, "version": 3} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + self.assertEqual(used, "/api/v2/desktop/config") + self.assertEqual(response["version"], 3) + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """服务端还是老版本时,必须照常同步,不能报错。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("HTTP 404", status=404) + return 200, {"config": {}, "version": 7} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config", "/api/v1/desktop/config"]) + self.assertEqual(used, "/api/v1/desktop/config") + self.assertEqual(response["version"], 7) + + def test_the_fallback_is_decided_by_status_not_by_wording(self) -> None: + """老后台对未知 GET 路径回的是"页面不存在",未知 POST 才回"接口不存在"。 + + 一个字之差。曾经这里靠文案匹配,结果对着真的老后台跑就整个失效了—— + 判据必须是状态码。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("页面不存在", status=404) + return 200, {"config": {}} + + with mock.patch.object(backend_client, "_request", fake): + _, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(used, "/api/v1/desktop/config") + + def test_a_connection_failure_is_not_retried_on_the_old_path(self) -> None: + """连不上就是连不上,换个路径同样连不上。 + + 真正的危害不是多发一个请求,而是把"服务器宕了"这个明确故障,伪装成 + "接口不存在"往下走——最后一层抛出来的是老接口的错误,人会去查一个根本 + 没坏的地方。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + # 连不上根本没拿到响应,所以没有状态码 + raise backend_client.BackendError("无法连接后台:timed out") + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"], "不该退回去重试") + self.assertIn("无法连接", str(caught.exception)) + + def test_a_bad_sync_key_surfaces_instead_of_falling_back(self) -> None: + """401/403 说明服务在、只是凭证不对。这是要人去改配置的错误,不能吞。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.AuthenticationError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + + def test_a_404_on_both_paths_reports_the_last_error(self) -> None: + def fake(method, server, path, **kwargs): + raise backend_client.BackendError("HTTP 404", status=404) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + def test_the_legacy_constant_still_points_at_a_real_path(self) -> None: + """DESKTOP_CONFIG_PATH 可能被别处引用,不能变成空字符串。""" + self.assertIn(backend_client.DESKTOP_CONFIG_PATH, backend_client.DESKTOP_CONFIG_PATHS) + self.assertTrue(backend_client.DESKTOP_CONFIG_PATH.startswith("/api/")) + + +class ZytAdminTokenVerificationTest(unittest.TestCase): + def test_backend_verifies_the_admin_desktop_session_endpoint(self) -> None: + session_response = mock.MagicMock() + session_response.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user_id": "admin:1", + "sn": "admin", + "nickname": "管理员", + "mobile": "", + "avatar": "", + "terminal": 7, + "status": "active", + "root": False, + "role_ids": [2], + "permissions": ["tcm.diagnosis/lists"], + }, + } + ).encode("utf-8") + with mock.patch.object( + zyt_auth.urllib.request, + "urlopen", + return_value=session_response, + ) as opened: + identity = zyt_auth.verify_desktop_token( + "admin-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 1) + request = opened.call_args_list[0].args[0] + self.assertEqual( + request.full_url, + "https://admin.example.com/adminapi/desktop/session", + ) + self.assertEqual(request.get_header("Token"), "admin-token") + self.assertEqual(identity["user_id"], "admin:1") + self.assertTrue(identity["permissions_known"]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + def test_old_server_falls_back_to_current_admin_profile(self) -> None: + profile = mock.MagicMock() + profile.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user": { + "id": 9, + "account": "doctor9", + "name": "九号医助", + "phone": "13800138000", + "avatar": "https://img.example/a.png", + "disable": 0, + "root": 0, + "role_ids": [2], + } + ,"permissions": ["tcm.diagnosis/lists"] + }, + } + ).encode("utf-8") + missing = urllib.error.HTTPError( + "https://admin.example.com/adminapi/desktop/session", + 404, + "Not Found", + None, + None, + ) + + with mock.patch.object( + zyt_auth.urllib.request, "urlopen", side_effect=[missing, profile] + ) as opened: + identity = zyt_auth.verify_desktop_token( + "legacy-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 2) + fallback_request = opened.call_args_list[1].args[0] + self.assertEqual( + fallback_request.full_url, + "https://admin.example.com/adminapi/auth.admin/mySelf", + ) + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(identity["nickname"], "九号医助") + self.assertEqual(identity["terminal"], 7) + self.assertEqual(identity["role_ids"], [2]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + +class ModelCallReportFallbackTest(unittest.TestCase): + """调用留痕上报:和拉配置同一套 v2 → v1 顺序。 + + 补这组测试是因为老后台(8765)退役前卡在这一条上:配置同步早有 v2, + 上报却只有 v1。桌面端只要改指到 8766,配置照常同步、回复照常发,唯独 + 调用记录一条都不进库——静悄悄地丢掉的正是出事后用来解释"这句话怎么来的" + 那份证据。 + """ + + def _settings(self): + return {"server_url": "http://x"} + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls"], "新接口在就不该再打老的") + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """还指着老后台(8765)的客户端必须照常上报,不能悄悄丢数据。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("接口不存在", status=404) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls", "/api/v1/model/calls"]) + + def test_a_401_does_not_fall_back(self) -> None: + """凭证不对换个路径重试同样不对,只会多打一个请求。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertFalse(ok) + self.assertEqual(calls, ["/api/v2/model/calls"]) + + def test_a_failure_never_raises_into_the_reply_path(self) -> None: + """上报是观测。它出问题绝不能把已经生成好的回复流程带下水。""" + def boom(*_args, **_kwargs): + raise RuntimeError("断网") + + with ( + mock.patch.object(backend_client, "_request", boom), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + self.assertFalse(backend_client.report_model_call({"task_id": "t1"})) + + +class GatewayUrlDerivationTest(unittest.TestCase): + """网关地址由后台算出来告诉客户端。 + + 这是"只改一个域名"的关键。桌面端配置里只有后台地址一项,网关在哪、模型怎么 + 编排全部由后台决定——运维换网关位置只动后台一处,不用挨个改客户端。 + """ + + def test_direct_port_access_points_at_the_local_gateway_port(self) -> None: + """本机开发:三个服务各占一个端口,网关就在同机的 8770。""" + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "127.0.0.1:8765"), + "http://127.0.0.1:8770/v1/answer", + ) + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "192.168.1.20:8766"), + "http://192.168.1.20:8770/v1/answer", + ) + + def test_a_proxied_domain_gets_a_same_origin_path(self) -> None: + """反代后面只有 80/443 对外,公网根本连不到 8770。 + + 这两种推法必须分开。用一套规则套两种部署,总有一边是错的——而且错得很 + 安静:客户端拿到一个连不上的地址,表现成"模型不回复"。 + """ + self.assertEqual( + admin_backend.derive_gateway_url("", "https", "xchat.example.com"), + "https://xchat.example.com/gateway/v1/answer", + ) + + def test_an_absolute_override_is_used_as_is(self) -> None: + """网关在别的域名或别的机器上时用这个。""" + self.assertEqual( + admin_backend.derive_gateway_url( + "https://gw.example.com/v1/answer", "https", "xchat.example.com" + ), + "https://gw.example.com/v1/answer", + ) + + def test_a_path_override_hangs_off_the_backend_domain(self) -> None: + self.assertEqual( + admin_backend.derive_gateway_url( + "/model-gw/v1/answer", "https", "xchat.example.com" + ), + "https://xchat.example.com/model-gw/v1/answer", + ) + + def test_a_missing_host_still_yields_something_usable(self) -> None: + """拿不到 Host 时给本机地址,而不是空串。 + + 空串会让客户端把 gateway 当成"没配",静默回落到本机单模型——而本机 + 没有密钥,最后报的是一个看不懂的 401。 + """ + self.assertTrue( + admin_backend.derive_gateway_url("", "http", "").startswith("http://") + ) + + def test_an_override_without_a_scheme_is_refused_on_save(self) -> None: + """漏了 http:// 会被拼成 https://你的域名/gw.example.com/v1/answer。 + + 那是个没意义的地址,而且失败要等到桌面端下次发模型请求才暴露。 + """ + base = { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + } + with self.assertRaises(ValueError) as caught: + admin_backend.validate_config_form( + {**base, "AI_GATEWAY_URL": "gw.example.com/v1/answer"}, {} + ) + self.assertIn("http", str(caught.exception)) + + def test_an_empty_override_is_accepted_and_means_auto(self) -> None: + config = admin_backend.validate_config_form( + { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + "AI_GATEWAY_URL": "", + }, + {}, + ) + self.assertEqual(config["AI_GATEWAY_URL"], "") + + +class GatewayArrivesBySyncTest(unittest.TestCase): + """桌面端不用手工配网关——同步时后台会把地址带过来。""" + + def _settings(self, response, existing=None): + """跑一遍 _apply_config_response,返回它写下的设置。""" + saved = {} + base = backend_client.default_settings() + if existing: + base.update(existing) + + with ( + mock.patch.object(backend_client, "load_settings", lambda: dict(base)), + mock.patch.object(backend_client, "save_settings", saved.update), + mock.patch("ai_config.apply_settings", lambda cfg, persist=True: cfg), + ): + backend_client._apply_config_response(response, dict(base)) + return saved + + def test_the_synced_gateway_address_lands_in_the_settings(self) -> None: + saved = self._settings( + { + "version": 3, + "config": {}, + "gateway": {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + } + ) + self.assertEqual( + saved["gateway"], + {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + ) + + def test_an_old_backend_without_a_gateway_block_keeps_the_last_one(self) -> None: + """老版本后端不返回这一段。清空等于把已经能用的客户端打回"没有网关"。""" + existing = {"gateway": {"enabled": True, "url": "https://old.example.com/v1/answer"}} + saved = self._settings({"version": 4, "config": {}}, existing=existing) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + def test_a_blank_url_from_the_backend_is_ignored(self) -> None: + """后台配错回了空地址时,宁可保持原样也不要把客户端弄成没网关。""" + existing = {"gateway": {"enabled": True, "url": "https://good.example.com/v1/answer"}} + saved = self._settings( + {"version": 5, "config": {}, "gateway": {"enabled": True, "url": " "}}, + existing=existing, + ) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + +class EndpointModeTest(unittest.TestCase): + """接口地址:自动补全 vs 原样使用。 + + 加这个开关是因为 auto 那套拼接规则只覆盖得了"服务商标准形状"。自建服务的路径 + 常常不按套路,比如 `https://api.example.com/custom/llm/invoke`——auto 会把它 + 拼成 `.../invoke/chat/completions`,请求发到一个不存在的地址,报 404,而排查的 + 人会去怀疑密钥和网络。 + + 为什么用显式开关而不是"更聪明的猜测":`https://api.example.com/openai` 到底是 + 前缀还是完整端点,光看地址分不出来。猜错的两种方向都会静默地把请求发歪。 + """ + + def test_auto_completes_the_path_for_each_kind(self) -> None: + cases = [ + ("openai", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"), + ("openai", "https://api.deepseek.com", "https://api.deepseek.com/chat/completions"), + ("claude", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"), + ("dify", "https://api.dify.ai/v1", "https://api.dify.ai/v1/chat-messages"), + ] + for kind, base, expected in cases: + with self.subTest(kind=kind, base=base): + self.assertEqual(model_protocol.endpoint_url(kind, base), expected) + + def test_auto_does_not_double_append_a_complete_url(self) -> None: + """已经写全的标准地址不该再被拼一次。""" + full = "https://api.openai.com/v1/chat/completions" + self.assertEqual(model_protocol.endpoint_url("openai", full), full) + + def test_exact_uses_the_address_verbatim(self) -> None: + odd = "https://api.example.com/custom/llm/invoke" + self.assertEqual(model_protocol.endpoint_url("openai", odd), f"{odd}/chat/completions") + self.assertEqual(model_protocol.endpoint_url("openai", odd, "exact"), odd) + + def test_exact_wins_for_every_kind(self) -> None: + """exact 是"别动我的地址",不该被接口类型的规则翻掉。""" + odd = "https://gw.internal/llm" + for kind in ("openai", "claude", "dify", "comfyui"): + with self.subTest(kind=kind): + self.assertEqual(model_protocol.endpoint_url(kind, odd, "exact"), odd) + + def test_a_trailing_slash_is_trimmed_in_both_modes(self) -> None: + self.assertEqual( + model_protocol.endpoint_url("openai", "https://x.com/llm/", "exact"), + "https://x.com/llm", + ) + + def test_the_connectivity_test_uses_the_same_rule_as_the_real_call(self) -> None: + """两边规则若不一致,测试会去戳一个和实际调用不同的地址。 + + 那是最没用的一种测试:测通了照样用不了,或者反过来。 + """ + odd = "https://api.example.com/custom/llm/invoke" + for mode in ("auto", "exact"): + with self.subTest(mode=mode): + self.assertEqual( + admin_backend._model_endpoint(odd, "openai", mode), + model_protocol.endpoint_url("openai", odd, mode), + ) + + def test_dify_upload_root_follows_the_mode(self) -> None: + """Dify 传图要拼 /files/upload,根地址得跟着 exact 走。""" + self.assertEqual( + model_protocol.dify_api_root("https://dify.internal/app/chat", "exact"), + "https://dify.internal/app/chat", + ) + + +class EndpointModeStorageTest(unittest.TestCase): + """存和读。老库要能平滑升级,老记录的行为必须一个字都不变。""" + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + self.uid = self.db.authenticate("admin", "Admin@123456")["id"] + + def _save(self, **extra): + item = { + "id": "p1", "name": "出口", "kind": "openai", + "base_url": "https://api.example.com/custom/llm/invoke", + "api_key": "sk-x", "model": "m", + } + item.update(extra) + return self.db.save_model_provider(item, self.uid, "1.1.1.1") + + def test_the_mode_round_trips(self) -> None: + self._save(endpoint_mode="exact") + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "exact") + self.assertEqual(item["endpoint"], "https://api.example.com/custom/llm/invoke") + + def test_the_default_is_auto(self) -> None: + """不填就是老行为。这个默认值是整件事零回归的前提。""" + self._save() + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "auto") + self.assertTrue(item["endpoint"].endswith("/chat/completions")) + + def test_an_unknown_mode_is_refused(self) -> None: + with self.assertRaises(ValueError) as caught: + self._save(endpoint_mode="随便写") + self.assertIn("地址模式", str(caught.exception)) + + def test_an_old_database_without_the_column_is_upgraded_in_place(self) -> None: + """已经在用的库不能因为加一列就要重建表——重建的每一步都可能丢数据。""" + path = self.root / "old.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_providers ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, + base_url TEXT NOT NULL, api_key_enc TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', capabilities TEXT NOT NULL DEFAULT 'text', + max_tokens INTEGER NOT NULL DEFAULT 500, temperature REAL NOT NULL DEFAULT 0.35, + timeout_ms INTEGER NOT NULL DEFAULT 30000, max_inflight INTEGER NOT NULL DEFAULT 32, + rpm_limit INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, + health TEXT NOT NULL DEFAULT 'unknown', health_checked_at TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by INTEGER); + INSERT INTO model_providers (id,name,kind,base_url,created_at,updated_at) + VALUES ('old-1','老出口','openai','https://api.openai.com/v1','x','x'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + item = next( + x for x in admin_backend.Database(path).model_providers() if x["id"] == "old-1" + ) + self.assertEqual(item["endpoint_mode"], "auto", "老记录必须还是老行为") + self.assertEqual(item["endpoint"], "https://api.openai.com/v1/chat/completions") + + def test_running_the_column_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.assertTrue(self.db.model_providers() is not None) + + +class ModelCallTraceabilityTest(unittest.TestCase): + """`model_calls` 存客户原话、模型原话、审核原因——用来"跟踪问题"。 + + 网关和桌面端各自都会往这张表报一次同一次调用(网关看得到候选和裁判, + 桌面端才知道审核规则命中原因)。两边共用 task_id,谁先落盘谁建行,后到 + 的一方只补自己独有的字段——这里测的就是这道合并逻辑不多写也不少写。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _record(self, **extra) -> dict: + record = { + "device_id": "dev-1", + "task_id": "task-1", + "roles_version": 1, + "judge_mode": "arbitrate", + "chosen": "openai-primary", + "judge": {"winner": "A", "score": 0.8, "risk": "low"}, + "candidates": [{"provider": "openai-primary", "text": "建议您注意休息"}], + "total_ms": 900, + "customer_text": "我这几天总是失眠", + "reply_text": "建议您注意休息", + "review_reason": "", + } + record.update(extra) + return record + + def _row(self, task_id: str) -> sqlite3.Row: + with self.db.connect() as con: + return con.execute( + "SELECT * FROM model_calls WHERE task_id = ?", (task_id,) + ).fetchone() + + def test_a_call_round_trips_customer_and_reply_text(self) -> None: + self.db.log_model_call(self._record()) + row = self._row("task-1") + self.assertEqual(row["customer_text"], "我这几天总是失眠") + self.assertEqual(row["reply_text"], "建议您注意休息") + self.assertEqual(row["review_reason"], "") + + def test_a_second_report_with_the_same_task_id_only_merges_review_reason(self) -> None: + """网关先落盘(候选、裁判都是权威数据),桌面端后到——不能把网关的数据覆盖掉。""" + self.db.log_model_call(self._record()) + self.db.log_model_call(self._record( + chosen="不该生效的值", + candidates=[{"provider": "不该生效的值", "text": "x"}], + total_ms=1, + review_reason="命中审核规则「诊断」", + )) + with self.db.connect() as con: + rows = con.execute( + "SELECT * FROM model_calls WHERE task_id='task-1'" + ).fetchall() + self.assertEqual(len(rows), 1, "同一个 task_id 只能落一行,不是两行") + row = self._row("task-1") + self.assertEqual(row["chosen"], "openai-primary", "第二次上报不该覆盖已有的候选数据") + self.assertEqual(row["review_reason"], "命中审核规则「诊断」", "审核原因只有桌面端知道,必须生效") + + def test_blank_task_ids_never_collide(self) -> None: + """本地非网关路径目前还给不出 task_id,留空时按老行为各插一行。""" + self.db.log_model_call(self._record(task_id="", customer_text="第一条")) + self.db.log_model_call(self._record(task_id="", customer_text="第二条")) + with self.db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id=''" + ).fetchone()["n"] + self.assertEqual(n, 2) + + def test_list_model_calls_finds_a_keyword_in_either_side_of_the_exchange(self) -> None: + self.db.log_model_call(self._record(task_id="t1", customer_text="想咨询糖尿病用药")) + self.db.log_model_call(self._record(task_id="t2", customer_text="今天天气不错", reply_text="是呀")) + found = self.db.list_model_calls(days=7, q="糖尿病") + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "t1") + + def test_list_model_calls_parses_candidates_back_into_a_list(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + found = self.db.list_model_calls(days=7) + self.assertEqual(found["items"][0]["candidates"][0]["provider"], "openai-primary") + + def test_list_model_calls_respects_the_days_window(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + with self.db.connect() as con: + con.execute( + "UPDATE model_calls SET created_at = '2000-01-01 00:00:00' WHERE task_id='t1'" + ) + con.commit() + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 0) + + def test_an_old_database_with_duplicate_task_ids_is_deduped_before_the_index_goes_on(self) -> None: + """老版本拿会话指纹当 task_id,同一个会话十几轮全共享一个值——建唯一索引前必须先清干净。""" + path = self.root / "old_calls.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p1','2026-01-01 00:00:01'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p2','2026-01-01 00:00:02'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p3','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + rows = con.execute( + "SELECT task_id, chosen FROM model_calls ORDER BY id" + ).fetchall() + self.assertEqual([r["task_id"] for r in rows], ["", "", "same-session"], "只留最新一行的 task_id") + self.assertEqual(rows[-1]["chosen"], "p3", "保留的必须是最新那一行") + # 索引确实建起来了:迁移后再报一次同名 task_id 应该走合并而不是新插一行 + db.log_model_call({"task_id": "same-session", "review_reason": "新原因"}) + with db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id='same-session'" + ).fetchone()["n"] + merged = con.execute( + "SELECT chosen, review_reason FROM model_calls WHERE task_id='same-session'" + ).fetchone() + self.assertEqual(n, 1, "唯一索引必须已经生效,不能再插出第二行") + self.assertEqual(merged["chosen"], "p3", "合并不该覆盖已有数据") + self.assertEqual(merged["review_reason"], "新原因") + + def test_running_the_model_calls_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.db.log_model_call(self._record(task_id="tx")) + self.assertEqual(self._row("tx")["chosen"], "openai-primary") + + +class GuardCallsStayOutOfTheCustomerLogTest(unittest.TestCase): + """界面识别调用不能混进"客服对话"的调用日志和调用统计。 + + 这是用户真实报上来的 bug:调用记录点开一看,客户消息和模型回复全是「(空)」, + 候选里全是 `{"state":"unknown","navigation_right_ratio":0,...}`——那是机器人 + 自己在看企业微信窗口,不是在回客户。守卫每轮轮询都要问一次模型,条数远多于 + 真实对话,不分开的话这张表根本没法用来查问题,裁判分布也在给错误的东西打分。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _log(self, task_id: str, purpose: str, **extra) -> None: + record = { + "device_id": "d1", "task_id": task_id, "chosen": "p1", + "judge": {"winner": "A", "score": 0.9, "risk": "low"}, + "candidates": [{"provider": "p1", "text": "文本"}], "total_ms": 100, + "customer_text": "客户说的", "reply_text": "模型答的", + "purpose": purpose, + } + record.update(extra) + self.db.log_model_call(record) + + def test_the_default_log_shows_only_customer_chats(self) -> None: + self._log("c1", "chat") + self._log("g1", "guard") + self._log("g2", "guard") + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "c1") + + def test_guard_calls_can_still_be_listed_on_purpose(self) -> None: + """内部调用照样在花钱,需要的时候得能翻出来看,不能查无此物。""" + self._log("c1", "chat") + self._log("g1", "guard") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + self.assertEqual(self.db.list_model_calls(days=7, purpose="")["total"], 2) + + def test_stats_ignore_guard_calls(self) -> None: + """裁判分布要量的是"发给客户的回复够不够好",不是"布局认得准不准"。""" + self._log("c1", "chat", judge={"winner": "A", "score": 0.9, "risk": "low"}) + for index in range(5): + self._log( + f"g{index}", "guard", + judge={"winner": "B", "score": 0.1, "risk": "high"}, + ) + stats = self.db.model_call_stats(7) + self.assertEqual(stats["total"], 1, "5 条界面识别不该被算成客服调用") + self.assertEqual(stats["judged"], 1) + self.assertAlmostEqual(stats["avg_score"], 0.9, places=3) + self.assertNotIn("high", stats["risk"], "守卫的低分不该污染风险占比") + + def test_a_call_without_an_explicit_purpose_counts_as_a_customer_chat(self) -> None: + """桌面端补报的那条路不带 purpose——默认必须是 chat,不能凭空消失。""" + self.db.log_model_call({ + "task_id": "c9", "chosen": "p1", "judge": {}, "candidates": [], + "customer_text": "在吗", "reply_text": "在的", + }) + self.assertEqual(self.db.list_model_calls(days=7)["total"], 1) + + def test_legacy_guard_rows_are_reclassified_by_their_fingerprint(self) -> None: + """老库里的守卫调用落的是默认 chat,要按候选内容的指纹认出来并改判。 + + 指纹用的是分类器专用的 JSON 字段名(布局守卫的 navigation_right_ratio、 + 会话行分类器的 reply_capable)——正常客服回复里不可能出现这些词。 + """ + path = self.root / "legacy.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (task_id,candidates_json,created_at) VALUES + ('L1','[{"provider":"a","text":"{\\"state\\":\\"unknown\\",\\"navigation_right_ratio\\":0}"}]','2026-01-01 00:00:01'), + ('L2','[{"provider":"a","text":"{\\"kind\\":\\"customer_chat\\",\\"reply_capable\\":true}"}]','2026-01-01 00:00:02'), + ('L3','[{"provider":"a","text":"忙完了,歇会儿吧,最近还好吗"}]','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + marks = { + row["task_id"]: row["purpose"] + for row in con.execute("SELECT task_id, purpose FROM model_calls") + } + self.assertEqual(marks["L1"], "guard", "布局守卫的记录要改判") + self.assertEqual(marks["L2"], "guard", "会话行分类器的记录也要改判") + self.assertEqual(marks["L3"], "chat", "真实客服回复绝不能被误判成内部调用") + + def test_the_reclassification_never_deletes_anything(self) -> None: + self._log("g1", "chat", candidates=[ + {"provider": "a", "text": '{"navigation_right_ratio":0.1}'} + ]) + before = self.db.list_model_calls(days=7, purpose="")["total"] + with self.db.connect() as con: + admin_backend.Database._backfill_guard_purpose(con) + con.commit() + after = self.db.list_model_calls(days=7, purpose="")["total"] + self.assertEqual(before, after, "只改分类标记,一行都不能少") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + + +def setUpModule(): + # 别让测试读到开发机上的真实桌面端配置——配过模型网关的机器会让 + # `ai_chat.current_provider()` 整体改走网关分支,一大片无关测试跟着变行为。 + local_state_redirect.start() + + +def tearDownModule(): + local_state_redirect.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-http-20260928/server-bundle/tests/test_remote_api.py b/deploy/mobile-http-20260928/server-bundle/tests/test_remote_api.py new file mode 100644 index 000000000..120d628b0 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/tests/test_remote_api.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_plain_http_login_is_allowed_by_default(self): + with mock.patch.dict(os.environ): + os.environ.pop("MOBILE_REQUIRE_HTTPS") + self.mobile_login(headers={"X-Real-IP": "114.114.114.114"}) + self.mobile_login("kefu2", headers={}) + self.assertEqual(self.zyt.login_calls, 2) + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-http-20260928/server-bundle/tests/test_zyt_auth.py b/deploy/mobile-http-20260928/server-bundle/tests/test_zyt_auth.py new file mode 100644 index 000000000..a5af82759 --- /dev/null +++ b/deploy/mobile-http-20260928/server-bundle/tests/test_zyt_auth.py @@ -0,0 +1,100 @@ +"""跨服务器桌面认证:只在路由缺失时兼容,拒绝结果不能降级。""" +import io +import json +import unittest +import urllib.error +from unittest import mock + +import zyt_auth + + +class DesktopVerificationRegressionTest(unittest.TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def session(self, **updates): + data = {"user_id": "admin:9", "terminal": 7, "status": "active", + "root": False, "role_ids": [2], "permissions": ["tcm.diagnosis/lists"]} + data.update(updates) + return {"code": 1, "data": data} + + def verify(self): + return zyt_auth.verify_desktop_token("test-token", api_url="https://identity.example.test") + + def missing(self, status=404): + return urllib.error.HTTPError("https://identity.example.test", status, "test", None, None) + + def test_success_never_requests_ip_bound_web_profile(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(self.session(root=True, permissions=["*"])), + self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertTrue(identity["root"]) + self.assertEqual(identity["role_ids"], [2]) + self.assertEqual(identity["permissions"], ["*"]) + self.assertTrue(identity["permissions_known"]) + + def test_missing_permissions_do_not_trigger_profile_request(self): + payload = self.session() + del payload["data"]["permissions"] + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertEqual(identity["permissions"], []) + self.assertFalse(identity["permissions_known"]) + + def test_empty_permissions_are_known(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(self.session(permissions=[]))): + identity = self.verify() + self.assertTrue(identity["permissions_known"]) + self.assertEqual(identity["permissions"], []) + + def test_only_missing_routes_allow_legacy_verification(self): + for status in (404, 405): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(status), self.response({"code": 1, "data": { + "user": {"id": 9, "disable": 0, "role_ids": [2]}, "permissions": []}}), + ]) as opened: + identity = self.verify() + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(opened.call_count, 2) + + def test_http_rejections_do_not_fall_back(self): + for status in (401, 403, 500, 502): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=self.missing(status)) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_invalid_identity_cannot_be_accepted_through_web_fallback(self): + failures = [{"code": -1, "msg": "登录已失效"}, {"code": "bad"}, [], + {"code": 1, "data": []}, self.session(user_id=""), + self.session(terminal=1), self.session(terminal="bad"), + self.session(status="disabled")] + for payload in failures: + with self.subTest(payload=payload), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(payload), self.response({"code": 1, "data": {"user": {"id": 9}}}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_bad_json_and_network_failure_do_not_fall_back(self): + for result in (io.BytesIO(b"error"), urllib.error.URLError("offline")): + with self.subTest(result=result), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[result]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_legacy_ip_error_identifies_missing_server_deployment(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(), self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]): + with self.assertRaisesRegex(zyt_auth.ZytAuthError, "/adminapi/desktop/session"): + self.verify() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-http-20260928/tests/test_backend.py b/deploy/mobile-http-20260928/tests/test_backend.py new file mode 100644 index 000000000..6a460176b --- /dev/null +++ b/deploy/mobile-http-20260928/tests/test_backend.py @@ -0,0 +1,1527 @@ +# -*- coding: utf-8 -*- +"""配置后台与桌面端同步的本地闭环测试。""" + +from __future__ import annotations + +import json +import hashlib +import shutil +import model_protocol +import socket +import sqlite3 +import tempfile +import threading +import unittest +import urllib.error +import urllib.request +from unittest import mock +from pathlib import Path + +from test_support import local_state_redirect +import admin_backend +import app_version +import backend_client +import zyt_auth + + +class BackendIntegrationTest(unittest.TestCase): + + + def test_development_diagnostics_are_cloud_controlled_and_redacted(self) -> None: + config = { + "AI_DEVELOPMENT_MODE": True, + "AI_PROVIDER_TYPE": "openai", + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_MCP_SERVERS": [ + { + "headers": {"Authorization": "Bearer hidden-auth"}, + "env": {"ACCESS_TOKEN": "hidden-token"}, + "args": [ + "--token", + "hidden-argument", + "--password=hidden-inline", + "Bearer hidden-bearer", + ], + } + ], + } + response = {"version": 8, "updated_at": "now"} + diagnostics = backend_client._config_diagnostics( + config, + response, + "https://cloud.example/api/v1/desktop/config?api_key=query-secret", + ) + rendered = "\n".join(diagnostics) + self.assertIn("https://cloud.example/api/v1/desktop/config", rendered) + self.assertIn("云端配置版本: v8", rendered) + self.assertIn('"AI_MODEL": "test-model"', rendered) + for secret in ( + "top-secret-key", + "hidden-auth", + "hidden-token", + "hidden-argument", + "hidden-inline", + "hidden-bearer", + "query-secret", + ): + self.assertNotIn(secret, rendered) + self.assertEqual( + backend_client._config_diagnostics( + {**config, "AI_DEVELOPMENT_MODE": False}, response, "https://cloud.example/config" + ), + [], + ) + + def test_model_request_diagnostics_never_print_the_api_key(self) -> None: + import ai_chat + import ai_config + + previous = { + "AI_DEVELOPMENT_MODE": getattr(ai_config, "AI_DEVELOPMENT_MODE", False), + "AI_API_BASE": ai_config.AI_API_BASE, + "AI_API_KEY": ai_config.AI_API_KEY, + "AI_MODEL": ai_config.AI_MODEL, + } + try: + ai_config.AI_DEVELOPMENT_MODE = True + ai_config.AI_API_BASE = "https://api.example/v1" + ai_config.AI_API_KEY = "never-print-this-key" + ai_config.AI_MODEL = "diagnostic-model" + with mock.patch("builtins.print") as printer: + ai_chat._log_request_diagnostics( + "https://api.example/v1/chat/completions", "OpenAI 兼容" + ) + rendered = "\n".join( + " ".join(str(item) for item in call.args) + for call in printer.call_args_list + ) + self.assertIn("https://api.example/v1/chat/completions", rendered) + self.assertIn("diagnostic-model", rendered) + self.assertNotIn("never-print-this-key", rendered) + finally: + for key, value in previous.items(): + setattr(ai_config, key, value) + + def test_model_connection_uses_unsaved_values_and_saved_key(self) -> None: + current = { + "AI_API_BASE": "https://saved.example/v1", + "AI_API_KEY": "saved-secret", + "AI_MODEL": "saved-model", + "AI_TIMEOUT": 120, + } + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://new.example/v1", + "AI_API_KEY": "", + "AI_MODEL": "new-model", + "AI_TIMEOUT": "180", + }, + current, + ) + self.assertEqual(config["endpoint"], "https://new.example/v1/chat/completions") + self.assertEqual(config["api_key"], "saved-secret") + self.assertEqual(config["model"], "new-model") + self.assertEqual(config["timeout"], 60) + + def test_model_connection_success_does_not_expose_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"choices": [{"message": {"content": "OK"}}]} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(200, response) + ) as call: + result = admin_backend.test_model_connection(config) + self.assertTrue(result["ok"]) + self.assertEqual(result["http_status"], 200) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + self.assertEqual(call.call_args.args[0], config["endpoint"]) + self.assertEqual( + call.call_args.kwargs["headers"]["Authorization"], "Bearer top-secret-key" + ) + payload = call.call_args.kwargs["payload"] + self.assertEqual(payload["model"], "test-model") + + def test_model_connection_error_redacts_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"error": {"message": "invalid top-secret-key"}} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(401, response) + ): + result = admin_backend.test_model_connection(config) + self.assertFalse(result["ok"]) + self.assertEqual(result["http_status"], 401) + self.assertIn("API Key 无效", result["message"]) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + + def test_dify_and_comfyui_use_provider_specific_endpoints(self) -> None: + dify = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "dify", + "AI_API_BASE": "https://dify.example/v1", + "AI_API_KEY": "app-secret", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(dify["endpoint"], "https://dify.example/v1/chat-messages") + self.assertEqual(dify["provider_type"], "dify") + + comfyui = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "comfyui", + "AI_API_BASE": "http://127.0.0.1:8188", + "AI_API_KEY": "", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(comfyui["endpoint"], "http://127.0.0.1:8188/system_stats") + with mock.patch( + "admin_backend._perform_http_request", + return_value=(200, b'{"system": {"os": "windows"}, "devices": []}'), + ) as call: + result = admin_backend.test_model_connection(comfyui) + self.assertTrue(result["ok"]) + self.assertEqual(call.call_args.kwargs["method"], "GET") + self.assertIsNone(call.call_args.kwargs["payload"]) + + def test_desktop_ai_respects_explicit_dify_provider(self) -> None: + import ai_chat + import ai_config + + old_provider = ai_config.AI_PROVIDER_TYPE + old_base = ai_config.AI_API_BASE + try: + ai_config.AI_PROVIDER_TYPE = "dify" + ai_config.AI_API_BASE = "https://dify.example/v1" + self.assertTrue(ai_chat._is_dify_endpoint()) + self.assertEqual( + ai_chat._completions_url(), + "https://dify.example/v1/chat-messages", + ) + ai_config.AI_PROVIDER_TYPE = "openai" + self.assertFalse(ai_chat._is_dify_endpoint()) + finally: + ai_config.AI_PROVIDER_TYPE = old_provider + ai_config.AI_API_BASE = old_base + + + def test_release_status_detects_optional_and_forced_updates(self) -> None: + optional = app_version.release_status( + {"latest_version": "99.0.0", "force_upgrade": False} + ) + self.assertTrue(optional["update_available"]) + self.assertFalse(optional["force_upgrade"]) + + forced = app_version.release_status( + {"latest_version": "2.0.0", "force_upgrade": True} + ) + self.assertTrue(forced["force_upgrade"]) + + current = app_version.release_status( + {"latest_version": app_version.APP_VERSION, "force_upgrade": True} + ) + self.assertFalse(current["update_available"]) + self.assertFalse(current["force_upgrade"]) + + downgrade = app_version.release_status( + {"latest_version": "1.0.1", "force_upgrade": True} + ) + self.assertFalse(downgrade["update_available"]) + self.assertFalse(downgrade["force_upgrade"]) + + def test_release_form_validation(self) -> None: + release = admin_backend.validate_release_form( + { + "latest_version": "v1.2.3", + "download_url": "https://example.com/client.exe", + "release_notes": "修复已知问题", + "force_upgrade": "1", + } + ) + self.assertEqual(release["latest_version"], "1.2.3") + self.assertTrue(release["force_upgrade"]) + automatic = admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "package_sha256": "a" * 64, + "package_size": "1024", + "auto_install": "1", + "release_channel": "stable", + "rollout_percent": "25", + "status": "published", + } + ) + self.assertTrue(automatic["auto_install"]) + self.assertEqual(automatic["rollout_percent"], 25) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2", "force_upgrade": "1"} + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "auto_install": "1", + } + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2.3", "force_upgrade": "1"} + ) + + def test_pbkdf2_fallback_matches_standard_library(self) -> None: + password = b"FallbackPassword123" + salt = b"0123456789abcdef" + expected = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000) + native_pbkdf2 = admin_backend.hashlib.pbkdf2_hmac + try: + admin_backend.hashlib.pbkdf2_hmac = None + actual = admin_backend.pbkdf2_sha256(password, salt, 1_000) + finally: + admin_backend.hashlib.pbkdf2_hmac = native_pbkdf2 + self.assertEqual(actual, expected) + + def test_stale_runtime_file_is_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + runtime_file = root / "backend_runtime.json" + runtime_file.write_text( + json.dumps( + { + "pid": 0, + "host": "127.0.0.1", + "port": 8765, + "server_url": "http://127.0.0.1:8765", + "local_sync_token": "stale-token", + } + ), + encoding="utf-8", + ) + old_runtime_file = backend_client.RUNTIME_FILE + try: + backend_client.RUNTIME_FILE = runtime_file + self.assertEqual(backend_client.discover_local_runtime(), {}) + self.assertEqual(backend_client.discover_local_sync_token(), "") + finally: + backend_client.RUNTIME_FILE = old_runtime_file + + def test_local_agent_name_overrides_cloud_and_blank_follows_cloud(self) -> None: + import ai_config + + old_agent = ai_config.AI_AGENT_NAME + old_cloud = ai_config.AI_CLOUD_AGENT_NAME + old_local = ai_config.AI_LOCAL_AGENT_NAME + old_prompt = ai_config.AI_SYSTEM_PROMPT + try: + ai_config.AI_CLOUD_AGENT_NAME = "旧云端客服" + ai_config.AI_LOCAL_AGENT_NAME = "" + ai_config.AI_AGENT_NAME = "旧云端客服" + ai_config.set_local_agent_name_override("本机客服", persist=False) + ai_config.apply_settings({"AI_AGENT_NAME": "新云端客服"}, persist=False) + self.assertEqual(ai_config.AI_CLOUD_AGENT_NAME, "新云端客服") + self.assertEqual(ai_config.AI_AGENT_NAME, "本机客服") + ai_config.set_local_agent_name_override("", persist=False) + self.assertEqual(ai_config.AI_AGENT_NAME, "新云端客服") + finally: + ai_config.AI_AGENT_NAME = old_agent + ai_config.AI_CLOUD_AGENT_NAME = old_cloud + ai_config.AI_LOCAL_AGENT_NAME = old_local + ai_config.AI_SYSTEM_PROMPT = old_prompt + + +class DesktopConfigFallbackTest(unittest.TestCase): + """桌面端拉配置:新接口优先,老接口兜底。 + + 客户端和服务端是分批更新的,两边版本对不齐是常态而不是意外。直接把路径改 + 成 v2,那些服务端还没升级的部署会当场同步失败——而且失败得很安静,界面上 + 只是配置一直显示旧的,没人会立刻发现。 + """ + + def _fail(self, message: str): + return backend_client.BackendError(message) + + def test_startup_auth_config_is_read_from_management_and_cached(self) -> None: + calls = [] + saved = {} + + def fake(method, server, path, **kwargs): + calls.append((method, server, path)) + return 200, { + "zyt_api_url": "https://account.example.com/zyt/", + "terminal": 7, + "configured": True, + } + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", lambda: {}), + mock.patch.object(backend_client, "save_settings", saved.update), + ): + config = backend_client.fetch_desktop_auth_config("https://admin.example.com/") + + self.assertEqual( + calls, + [("GET", "https://admin.example.com", "/api/v2/desktop/auth/config")], + ) + self.assertEqual(config["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["server_url"], "https://admin.example.com") + + def test_startup_auth_config_rejects_an_incomplete_response(self) -> None: + with mock.patch.object( + backend_client, "_request", return_value=(200, {"terminal": 7}) + ): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client.fetch_desktop_auth_config("https://admin.example.com") + self.assertIn("登录 API", str(caught.exception)) + + def test_zyt_admin_login_uses_admin_endpoint_and_desktop_terminal(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append((method, server, path, kwargs.get("payload"))) + return 200, { + "code": 1, + "msg": "登录成功", + "data": {"token": "admin-desktop-token", "name": "管理员"}, + } + + with mock.patch.object(backend_client, "_request", fake): + token, user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(token, "admin-desktop-token") + self.assertEqual(user["name"], "管理员") + self.assertEqual(calls[0][2], "/adminapi/login/account") + self.assertEqual(calls[0][3]["terminal"], 7) + self.assertEqual(calls[0][3]["account"], "admin") + + def test_zyt_admin_login_falls_back_while_server_has_no_terminal_7(self) -> None: + terminals = [] + + def fake(method, server, path, **kwargs): + terminal = kwargs["payload"]["terminal"] + terminals.append(terminal) + if terminal == 7: + return 200, {"code": 0, "msg": "terminal必须在 1,2 范围内"} + return 200, {"code": 1, "data": {"token": "legacy-admin-token"}} + + with mock.patch.object(backend_client, "_request", fake): + token, _user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(terminals, [7, 1]) + self.assertEqual(token, "legacy-admin-token") + + def test_desktop_login_can_remember_password_with_dpapi_protection(self) -> None: + settings = backend_client.default_settings() + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, + "_protect_secret", + side_effect=lambda value: f"protected:{value}", + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=True, + ) + + self.assertTrue(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["zyt_account"], "admin") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:secret" + ) + self.assertNotIn("secret", json.dumps(saved[-1], ensure_ascii=False).replace( + "protected:secret", "" + )) + + def test_desktop_login_unchecked_forgets_the_saved_password(self) -> None: + settings = backend_client.default_settings() + settings["remembered_password_protected"] = "protected:old-password" + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected:token" + ), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=False, + ) + + self.assertFalse(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["remembered_password_protected"], "") + + def test_forced_logout_keeps_explicitly_remembered_password(self) -> None: + settings = backend_client.default_settings() + settings.update( + { + "zyt_account": "admin", + "remember_login": True, + "remembered_password_protected": "protected:password", + "zyt_token_protected": "protected:zyt-token", + "desktop_token_protected": "protected:desktop-token", + } + ) + saved = [] + with ( + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + ): + backend_client._clear_desktop_credentials(settings) + + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:password" + ) + + def test_session_heartbeat_never_silently_reauthenticates(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + } + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object(backend_client, "save_settings"), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object( + backend_client, + "_request", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + with self.assertRaises(backend_client.AuthenticationError): + backend_client.validate_desktop_session() + exchange.assert_not_called() + + def test_session_heartbeat_persists_the_renewed_expiration(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "desktop_expires_at": 1200, + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_request", + return_value=( + 200, + { + "id": 1, + "sn": "admin", + "expires_at": 50_000, + }, + ), + ), + ): + account = backend_client.validate_desktop_session() + + self.assertEqual(account["id"], 1) + self.assertEqual(saved[-1]["desktop_expires_at"], 50_000) + + def test_revoked_unexpired_session_requires_password_again(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 2000, + "zyt_account": "admin", + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "validate_desktop_session", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + self.assertIsNone(backend_client.restore_desktop_session()) + + exchange.assert_not_called() + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["zyt_account"], "admin") + + def test_naturally_expired_session_can_refresh_from_zyt(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-old", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 999, + } + saved = [] + + def unprotect(value): + return { + "protected-old": "old-token", + "protected-zyt": "zyt-token", + }.get(value, "") + + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object(backend_client, "_unprotect_secret", side_effect=unprotect), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected-new" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "new-token", + "expires_in": 1800, + "account": {"id": 1, "sn": "admin"}, + }, + ), + ): + account = backend_client.restore_desktop_session() + + self.assertEqual(account, {"id": 1, "sn": "admin"}) + self.assertEqual(saved[-1]["desktop_token_protected"], "protected-new") + self.assertEqual(saved[-1]["desktop_expires_at"], 2800) + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"config": {}, "version": 3} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + self.assertEqual(used, "/api/v2/desktop/config") + self.assertEqual(response["version"], 3) + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """服务端还是老版本时,必须照常同步,不能报错。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("HTTP 404", status=404) + return 200, {"config": {}, "version": 7} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config", "/api/v1/desktop/config"]) + self.assertEqual(used, "/api/v1/desktop/config") + self.assertEqual(response["version"], 7) + + def test_the_fallback_is_decided_by_status_not_by_wording(self) -> None: + """老后台对未知 GET 路径回的是"页面不存在",未知 POST 才回"接口不存在"。 + + 一个字之差。曾经这里靠文案匹配,结果对着真的老后台跑就整个失效了—— + 判据必须是状态码。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("页面不存在", status=404) + return 200, {"config": {}} + + with mock.patch.object(backend_client, "_request", fake): + _, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(used, "/api/v1/desktop/config") + + def test_a_connection_failure_is_not_retried_on_the_old_path(self) -> None: + """连不上就是连不上,换个路径同样连不上。 + + 真正的危害不是多发一个请求,而是把"服务器宕了"这个明确故障,伪装成 + "接口不存在"往下走——最后一层抛出来的是老接口的错误,人会去查一个根本 + 没坏的地方。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + # 连不上根本没拿到响应,所以没有状态码 + raise backend_client.BackendError("无法连接后台:timed out") + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"], "不该退回去重试") + self.assertIn("无法连接", str(caught.exception)) + + def test_a_bad_sync_key_surfaces_instead_of_falling_back(self) -> None: + """401/403 说明服务在、只是凭证不对。这是要人去改配置的错误,不能吞。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.AuthenticationError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + + def test_a_404_on_both_paths_reports_the_last_error(self) -> None: + def fake(method, server, path, **kwargs): + raise backend_client.BackendError("HTTP 404", status=404) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + def test_the_legacy_constant_still_points_at_a_real_path(self) -> None: + """DESKTOP_CONFIG_PATH 可能被别处引用,不能变成空字符串。""" + self.assertIn(backend_client.DESKTOP_CONFIG_PATH, backend_client.DESKTOP_CONFIG_PATHS) + self.assertTrue(backend_client.DESKTOP_CONFIG_PATH.startswith("/api/")) + + +class ZytAdminTokenVerificationTest(unittest.TestCase): + def test_backend_verifies_the_admin_desktop_session_endpoint(self) -> None: + session_response = mock.MagicMock() + session_response.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user_id": "admin:1", + "sn": "admin", + "nickname": "管理员", + "mobile": "", + "avatar": "", + "terminal": 7, + "status": "active", + "root": False, + "role_ids": [2], + "permissions": ["tcm.diagnosis/lists"], + }, + } + ).encode("utf-8") + with mock.patch.object( + zyt_auth.urllib.request, + "urlopen", + return_value=session_response, + ) as opened: + identity = zyt_auth.verify_desktop_token( + "admin-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 1) + request = opened.call_args_list[0].args[0] + self.assertEqual( + request.full_url, + "https://admin.example.com/adminapi/desktop/session", + ) + self.assertEqual(request.get_header("Token"), "admin-token") + self.assertEqual(identity["user_id"], "admin:1") + self.assertTrue(identity["permissions_known"]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + def test_old_server_falls_back_to_current_admin_profile(self) -> None: + profile = mock.MagicMock() + profile.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user": { + "id": 9, + "account": "doctor9", + "name": "九号医助", + "phone": "13800138000", + "avatar": "https://img.example/a.png", + "disable": 0, + "root": 0, + "role_ids": [2], + } + ,"permissions": ["tcm.diagnosis/lists"] + }, + } + ).encode("utf-8") + missing = urllib.error.HTTPError( + "https://admin.example.com/adminapi/desktop/session", + 404, + "Not Found", + None, + None, + ) + + with mock.patch.object( + zyt_auth.urllib.request, "urlopen", side_effect=[missing, profile] + ) as opened: + identity = zyt_auth.verify_desktop_token( + "legacy-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 2) + fallback_request = opened.call_args_list[1].args[0] + self.assertEqual( + fallback_request.full_url, + "https://admin.example.com/adminapi/auth.admin/mySelf", + ) + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(identity["nickname"], "九号医助") + self.assertEqual(identity["terminal"], 7) + self.assertEqual(identity["role_ids"], [2]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + +class ModelCallReportFallbackTest(unittest.TestCase): + """调用留痕上报:和拉配置同一套 v2 → v1 顺序。 + + 补这组测试是因为老后台(8765)退役前卡在这一条上:配置同步早有 v2, + 上报却只有 v1。桌面端只要改指到 8766,配置照常同步、回复照常发,唯独 + 调用记录一条都不进库——静悄悄地丢掉的正是出事后用来解释"这句话怎么来的" + 那份证据。 + """ + + def _settings(self): + return {"server_url": "http://x"} + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls"], "新接口在就不该再打老的") + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """还指着老后台(8765)的客户端必须照常上报,不能悄悄丢数据。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("接口不存在", status=404) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls", "/api/v1/model/calls"]) + + def test_a_401_does_not_fall_back(self) -> None: + """凭证不对换个路径重试同样不对,只会多打一个请求。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertFalse(ok) + self.assertEqual(calls, ["/api/v2/model/calls"]) + + def test_a_failure_never_raises_into_the_reply_path(self) -> None: + """上报是观测。它出问题绝不能把已经生成好的回复流程带下水。""" + def boom(*_args, **_kwargs): + raise RuntimeError("断网") + + with ( + mock.patch.object(backend_client, "_request", boom), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + self.assertFalse(backend_client.report_model_call({"task_id": "t1"})) + + +class GatewayUrlDerivationTest(unittest.TestCase): + """网关地址由后台算出来告诉客户端。 + + 这是"只改一个域名"的关键。桌面端配置里只有后台地址一项,网关在哪、模型怎么 + 编排全部由后台决定——运维换网关位置只动后台一处,不用挨个改客户端。 + """ + + def test_direct_port_access_points_at_the_local_gateway_port(self) -> None: + """本机开发:三个服务各占一个端口,网关就在同机的 8770。""" + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "127.0.0.1:8765"), + "http://127.0.0.1:8770/v1/answer", + ) + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "192.168.1.20:8766"), + "http://192.168.1.20:8770/v1/answer", + ) + + def test_a_proxied_domain_gets_a_same_origin_path(self) -> None: + """反代后面只有 80/443 对外,公网根本连不到 8770。 + + 这两种推法必须分开。用一套规则套两种部署,总有一边是错的——而且错得很 + 安静:客户端拿到一个连不上的地址,表现成"模型不回复"。 + """ + self.assertEqual( + admin_backend.derive_gateway_url("", "https", "xchat.example.com"), + "https://xchat.example.com/gateway/v1/answer", + ) + + def test_an_absolute_override_is_used_as_is(self) -> None: + """网关在别的域名或别的机器上时用这个。""" + self.assertEqual( + admin_backend.derive_gateway_url( + "https://gw.example.com/v1/answer", "https", "xchat.example.com" + ), + "https://gw.example.com/v1/answer", + ) + + def test_a_path_override_hangs_off_the_backend_domain(self) -> None: + self.assertEqual( + admin_backend.derive_gateway_url( + "/model-gw/v1/answer", "https", "xchat.example.com" + ), + "https://xchat.example.com/model-gw/v1/answer", + ) + + def test_a_missing_host_still_yields_something_usable(self) -> None: + """拿不到 Host 时给本机地址,而不是空串。 + + 空串会让客户端把 gateway 当成"没配",静默回落到本机单模型——而本机 + 没有密钥,最后报的是一个看不懂的 401。 + """ + self.assertTrue( + admin_backend.derive_gateway_url("", "http", "").startswith("http://") + ) + + def test_an_override_without_a_scheme_is_refused_on_save(self) -> None: + """漏了 http:// 会被拼成 https://你的域名/gw.example.com/v1/answer。 + + 那是个没意义的地址,而且失败要等到桌面端下次发模型请求才暴露。 + """ + base = { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + } + with self.assertRaises(ValueError) as caught: + admin_backend.validate_config_form( + {**base, "AI_GATEWAY_URL": "gw.example.com/v1/answer"}, {} + ) + self.assertIn("http", str(caught.exception)) + + def test_an_empty_override_is_accepted_and_means_auto(self) -> None: + config = admin_backend.validate_config_form( + { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + "AI_GATEWAY_URL": "", + }, + {}, + ) + self.assertEqual(config["AI_GATEWAY_URL"], "") + + +class GatewayArrivesBySyncTest(unittest.TestCase): + """桌面端不用手工配网关——同步时后台会把地址带过来。""" + + def _settings(self, response, existing=None): + """跑一遍 _apply_config_response,返回它写下的设置。""" + saved = {} + base = backend_client.default_settings() + if existing: + base.update(existing) + + with ( + mock.patch.object(backend_client, "load_settings", lambda: dict(base)), + mock.patch.object(backend_client, "save_settings", saved.update), + mock.patch("ai_config.apply_settings", lambda cfg, persist=True: cfg), + ): + backend_client._apply_config_response(response, dict(base)) + return saved + + def test_the_synced_gateway_address_lands_in_the_settings(self) -> None: + saved = self._settings( + { + "version": 3, + "config": {}, + "gateway": {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + } + ) + self.assertEqual( + saved["gateway"], + {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + ) + + def test_an_old_backend_without_a_gateway_block_keeps_the_last_one(self) -> None: + """老版本后端不返回这一段。清空等于把已经能用的客户端打回"没有网关"。""" + existing = {"gateway": {"enabled": True, "url": "https://old.example.com/v1/answer"}} + saved = self._settings({"version": 4, "config": {}}, existing=existing) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + def test_a_blank_url_from_the_backend_is_ignored(self) -> None: + """后台配错回了空地址时,宁可保持原样也不要把客户端弄成没网关。""" + existing = {"gateway": {"enabled": True, "url": "https://good.example.com/v1/answer"}} + saved = self._settings( + {"version": 5, "config": {}, "gateway": {"enabled": True, "url": " "}}, + existing=existing, + ) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + +class EndpointModeTest(unittest.TestCase): + """接口地址:自动补全 vs 原样使用。 + + 加这个开关是因为 auto 那套拼接规则只覆盖得了"服务商标准形状"。自建服务的路径 + 常常不按套路,比如 `https://api.example.com/custom/llm/invoke`——auto 会把它 + 拼成 `.../invoke/chat/completions`,请求发到一个不存在的地址,报 404,而排查的 + 人会去怀疑密钥和网络。 + + 为什么用显式开关而不是"更聪明的猜测":`https://api.example.com/openai` 到底是 + 前缀还是完整端点,光看地址分不出来。猜错的两种方向都会静默地把请求发歪。 + """ + + def test_auto_completes_the_path_for_each_kind(self) -> None: + cases = [ + ("openai", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"), + ("openai", "https://api.deepseek.com", "https://api.deepseek.com/chat/completions"), + ("claude", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"), + ("dify", "https://api.dify.ai/v1", "https://api.dify.ai/v1/chat-messages"), + ] + for kind, base, expected in cases: + with self.subTest(kind=kind, base=base): + self.assertEqual(model_protocol.endpoint_url(kind, base), expected) + + def test_auto_does_not_double_append_a_complete_url(self) -> None: + """已经写全的标准地址不该再被拼一次。""" + full = "https://api.openai.com/v1/chat/completions" + self.assertEqual(model_protocol.endpoint_url("openai", full), full) + + def test_exact_uses_the_address_verbatim(self) -> None: + odd = "https://api.example.com/custom/llm/invoke" + self.assertEqual(model_protocol.endpoint_url("openai", odd), f"{odd}/chat/completions") + self.assertEqual(model_protocol.endpoint_url("openai", odd, "exact"), odd) + + def test_exact_wins_for_every_kind(self) -> None: + """exact 是"别动我的地址",不该被接口类型的规则翻掉。""" + odd = "https://gw.internal/llm" + for kind in ("openai", "claude", "dify", "comfyui"): + with self.subTest(kind=kind): + self.assertEqual(model_protocol.endpoint_url(kind, odd, "exact"), odd) + + def test_a_trailing_slash_is_trimmed_in_both_modes(self) -> None: + self.assertEqual( + model_protocol.endpoint_url("openai", "https://x.com/llm/", "exact"), + "https://x.com/llm", + ) + + def test_the_connectivity_test_uses_the_same_rule_as_the_real_call(self) -> None: + """两边规则若不一致,测试会去戳一个和实际调用不同的地址。 + + 那是最没用的一种测试:测通了照样用不了,或者反过来。 + """ + odd = "https://api.example.com/custom/llm/invoke" + for mode in ("auto", "exact"): + with self.subTest(mode=mode): + self.assertEqual( + admin_backend._model_endpoint(odd, "openai", mode), + model_protocol.endpoint_url("openai", odd, mode), + ) + + def test_dify_upload_root_follows_the_mode(self) -> None: + """Dify 传图要拼 /files/upload,根地址得跟着 exact 走。""" + self.assertEqual( + model_protocol.dify_api_root("https://dify.internal/app/chat", "exact"), + "https://dify.internal/app/chat", + ) + + +class EndpointModeStorageTest(unittest.TestCase): + """存和读。老库要能平滑升级,老记录的行为必须一个字都不变。""" + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + self.uid = self.db.authenticate("admin", "Admin@123456")["id"] + + def _save(self, **extra): + item = { + "id": "p1", "name": "出口", "kind": "openai", + "base_url": "https://api.example.com/custom/llm/invoke", + "api_key": "sk-x", "model": "m", + } + item.update(extra) + return self.db.save_model_provider(item, self.uid, "1.1.1.1") + + def test_the_mode_round_trips(self) -> None: + self._save(endpoint_mode="exact") + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "exact") + self.assertEqual(item["endpoint"], "https://api.example.com/custom/llm/invoke") + + def test_the_default_is_auto(self) -> None: + """不填就是老行为。这个默认值是整件事零回归的前提。""" + self._save() + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "auto") + self.assertTrue(item["endpoint"].endswith("/chat/completions")) + + def test_an_unknown_mode_is_refused(self) -> None: + with self.assertRaises(ValueError) as caught: + self._save(endpoint_mode="随便写") + self.assertIn("地址模式", str(caught.exception)) + + def test_an_old_database_without_the_column_is_upgraded_in_place(self) -> None: + """已经在用的库不能因为加一列就要重建表——重建的每一步都可能丢数据。""" + path = self.root / "old.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_providers ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, + base_url TEXT NOT NULL, api_key_enc TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', capabilities TEXT NOT NULL DEFAULT 'text', + max_tokens INTEGER NOT NULL DEFAULT 500, temperature REAL NOT NULL DEFAULT 0.35, + timeout_ms INTEGER NOT NULL DEFAULT 30000, max_inflight INTEGER NOT NULL DEFAULT 32, + rpm_limit INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, + health TEXT NOT NULL DEFAULT 'unknown', health_checked_at TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by INTEGER); + INSERT INTO model_providers (id,name,kind,base_url,created_at,updated_at) + VALUES ('old-1','老出口','openai','https://api.openai.com/v1','x','x'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + item = next( + x for x in admin_backend.Database(path).model_providers() if x["id"] == "old-1" + ) + self.assertEqual(item["endpoint_mode"], "auto", "老记录必须还是老行为") + self.assertEqual(item["endpoint"], "https://api.openai.com/v1/chat/completions") + + def test_running_the_column_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.assertTrue(self.db.model_providers() is not None) + + +class ModelCallTraceabilityTest(unittest.TestCase): + """`model_calls` 存客户原话、模型原话、审核原因——用来"跟踪问题"。 + + 网关和桌面端各自都会往这张表报一次同一次调用(网关看得到候选和裁判, + 桌面端才知道审核规则命中原因)。两边共用 task_id,谁先落盘谁建行,后到 + 的一方只补自己独有的字段——这里测的就是这道合并逻辑不多写也不少写。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _record(self, **extra) -> dict: + record = { + "device_id": "dev-1", + "task_id": "task-1", + "roles_version": 1, + "judge_mode": "arbitrate", + "chosen": "openai-primary", + "judge": {"winner": "A", "score": 0.8, "risk": "low"}, + "candidates": [{"provider": "openai-primary", "text": "建议您注意休息"}], + "total_ms": 900, + "customer_text": "我这几天总是失眠", + "reply_text": "建议您注意休息", + "review_reason": "", + } + record.update(extra) + return record + + def _row(self, task_id: str) -> sqlite3.Row: + with self.db.connect() as con: + return con.execute( + "SELECT * FROM model_calls WHERE task_id = ?", (task_id,) + ).fetchone() + + def test_a_call_round_trips_customer_and_reply_text(self) -> None: + self.db.log_model_call(self._record()) + row = self._row("task-1") + self.assertEqual(row["customer_text"], "我这几天总是失眠") + self.assertEqual(row["reply_text"], "建议您注意休息") + self.assertEqual(row["review_reason"], "") + + def test_a_second_report_with_the_same_task_id_only_merges_review_reason(self) -> None: + """网关先落盘(候选、裁判都是权威数据),桌面端后到——不能把网关的数据覆盖掉。""" + self.db.log_model_call(self._record()) + self.db.log_model_call(self._record( + chosen="不该生效的值", + candidates=[{"provider": "不该生效的值", "text": "x"}], + total_ms=1, + review_reason="命中审核规则「诊断」", + )) + with self.db.connect() as con: + rows = con.execute( + "SELECT * FROM model_calls WHERE task_id='task-1'" + ).fetchall() + self.assertEqual(len(rows), 1, "同一个 task_id 只能落一行,不是两行") + row = self._row("task-1") + self.assertEqual(row["chosen"], "openai-primary", "第二次上报不该覆盖已有的候选数据") + self.assertEqual(row["review_reason"], "命中审核规则「诊断」", "审核原因只有桌面端知道,必须生效") + + def test_blank_task_ids_never_collide(self) -> None: + """本地非网关路径目前还给不出 task_id,留空时按老行为各插一行。""" + self.db.log_model_call(self._record(task_id="", customer_text="第一条")) + self.db.log_model_call(self._record(task_id="", customer_text="第二条")) + with self.db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id=''" + ).fetchone()["n"] + self.assertEqual(n, 2) + + def test_list_model_calls_finds_a_keyword_in_either_side_of_the_exchange(self) -> None: + self.db.log_model_call(self._record(task_id="t1", customer_text="想咨询糖尿病用药")) + self.db.log_model_call(self._record(task_id="t2", customer_text="今天天气不错", reply_text="是呀")) + found = self.db.list_model_calls(days=7, q="糖尿病") + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "t1") + + def test_list_model_calls_parses_candidates_back_into_a_list(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + found = self.db.list_model_calls(days=7) + self.assertEqual(found["items"][0]["candidates"][0]["provider"], "openai-primary") + + def test_list_model_calls_respects_the_days_window(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + with self.db.connect() as con: + con.execute( + "UPDATE model_calls SET created_at = '2000-01-01 00:00:00' WHERE task_id='t1'" + ) + con.commit() + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 0) + + def test_an_old_database_with_duplicate_task_ids_is_deduped_before_the_index_goes_on(self) -> None: + """老版本拿会话指纹当 task_id,同一个会话十几轮全共享一个值——建唯一索引前必须先清干净。""" + path = self.root / "old_calls.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p1','2026-01-01 00:00:01'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p2','2026-01-01 00:00:02'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p3','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + rows = con.execute( + "SELECT task_id, chosen FROM model_calls ORDER BY id" + ).fetchall() + self.assertEqual([r["task_id"] for r in rows], ["", "", "same-session"], "只留最新一行的 task_id") + self.assertEqual(rows[-1]["chosen"], "p3", "保留的必须是最新那一行") + # 索引确实建起来了:迁移后再报一次同名 task_id 应该走合并而不是新插一行 + db.log_model_call({"task_id": "same-session", "review_reason": "新原因"}) + with db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id='same-session'" + ).fetchone()["n"] + merged = con.execute( + "SELECT chosen, review_reason FROM model_calls WHERE task_id='same-session'" + ).fetchone() + self.assertEqual(n, 1, "唯一索引必须已经生效,不能再插出第二行") + self.assertEqual(merged["chosen"], "p3", "合并不该覆盖已有数据") + self.assertEqual(merged["review_reason"], "新原因") + + def test_running_the_model_calls_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.db.log_model_call(self._record(task_id="tx")) + self.assertEqual(self._row("tx")["chosen"], "openai-primary") + + +class GuardCallsStayOutOfTheCustomerLogTest(unittest.TestCase): + """界面识别调用不能混进"客服对话"的调用日志和调用统计。 + + 这是用户真实报上来的 bug:调用记录点开一看,客户消息和模型回复全是「(空)」, + 候选里全是 `{"state":"unknown","navigation_right_ratio":0,...}`——那是机器人 + 自己在看企业微信窗口,不是在回客户。守卫每轮轮询都要问一次模型,条数远多于 + 真实对话,不分开的话这张表根本没法用来查问题,裁判分布也在给错误的东西打分。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _log(self, task_id: str, purpose: str, **extra) -> None: + record = { + "device_id": "d1", "task_id": task_id, "chosen": "p1", + "judge": {"winner": "A", "score": 0.9, "risk": "low"}, + "candidates": [{"provider": "p1", "text": "文本"}], "total_ms": 100, + "customer_text": "客户说的", "reply_text": "模型答的", + "purpose": purpose, + } + record.update(extra) + self.db.log_model_call(record) + + def test_the_default_log_shows_only_customer_chats(self) -> None: + self._log("c1", "chat") + self._log("g1", "guard") + self._log("g2", "guard") + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "c1") + + def test_guard_calls_can_still_be_listed_on_purpose(self) -> None: + """内部调用照样在花钱,需要的时候得能翻出来看,不能查无此物。""" + self._log("c1", "chat") + self._log("g1", "guard") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + self.assertEqual(self.db.list_model_calls(days=7, purpose="")["total"], 2) + + def test_stats_ignore_guard_calls(self) -> None: + """裁判分布要量的是"发给客户的回复够不够好",不是"布局认得准不准"。""" + self._log("c1", "chat", judge={"winner": "A", "score": 0.9, "risk": "low"}) + for index in range(5): + self._log( + f"g{index}", "guard", + judge={"winner": "B", "score": 0.1, "risk": "high"}, + ) + stats = self.db.model_call_stats(7) + self.assertEqual(stats["total"], 1, "5 条界面识别不该被算成客服调用") + self.assertEqual(stats["judged"], 1) + self.assertAlmostEqual(stats["avg_score"], 0.9, places=3) + self.assertNotIn("high", stats["risk"], "守卫的低分不该污染风险占比") + + def test_a_call_without_an_explicit_purpose_counts_as_a_customer_chat(self) -> None: + """桌面端补报的那条路不带 purpose——默认必须是 chat,不能凭空消失。""" + self.db.log_model_call({ + "task_id": "c9", "chosen": "p1", "judge": {}, "candidates": [], + "customer_text": "在吗", "reply_text": "在的", + }) + self.assertEqual(self.db.list_model_calls(days=7)["total"], 1) + + def test_legacy_guard_rows_are_reclassified_by_their_fingerprint(self) -> None: + """老库里的守卫调用落的是默认 chat,要按候选内容的指纹认出来并改判。 + + 指纹用的是分类器专用的 JSON 字段名(布局守卫的 navigation_right_ratio、 + 会话行分类器的 reply_capable)——正常客服回复里不可能出现这些词。 + """ + path = self.root / "legacy.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (task_id,candidates_json,created_at) VALUES + ('L1','[{"provider":"a","text":"{\\"state\\":\\"unknown\\",\\"navigation_right_ratio\\":0}"}]','2026-01-01 00:00:01'), + ('L2','[{"provider":"a","text":"{\\"kind\\":\\"customer_chat\\",\\"reply_capable\\":true}"}]','2026-01-01 00:00:02'), + ('L3','[{"provider":"a","text":"忙完了,歇会儿吧,最近还好吗"}]','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + marks = { + row["task_id"]: row["purpose"] + for row in con.execute("SELECT task_id, purpose FROM model_calls") + } + self.assertEqual(marks["L1"], "guard", "布局守卫的记录要改判") + self.assertEqual(marks["L2"], "guard", "会话行分类器的记录也要改判") + self.assertEqual(marks["L3"], "chat", "真实客服回复绝不能被误判成内部调用") + + def test_the_reclassification_never_deletes_anything(self) -> None: + self._log("g1", "chat", candidates=[ + {"provider": "a", "text": '{"navigation_right_ratio":0.1}'} + ]) + before = self.db.list_model_calls(days=7, purpose="")["total"] + with self.db.connect() as con: + admin_backend.Database._backfill_guard_purpose(con) + con.commit() + after = self.db.list_model_calls(days=7, purpose="")["total"] + self.assertEqual(before, after, "只改分类标记,一行都不能少") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + + +def setUpModule(): + # 别让测试读到开发机上的真实桌面端配置——配过模型网关的机器会让 + # `ai_chat.current_provider()` 整体改走网关分支,一大片无关测试跟着变行为。 + local_state_redirect.start() + + +def tearDownModule(): + local_state_redirect.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-http-20260928/tests/test_remote_api.py b/deploy/mobile-http-20260928/tests/test_remote_api.py new file mode 100644 index 000000000..120d628b0 --- /dev/null +++ b/deploy/mobile-http-20260928/tests/test_remote_api.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_plain_http_login_is_allowed_by_default(self): + with mock.patch.dict(os.environ): + os.environ.pop("MOBILE_REQUIRE_HTTPS") + self.mobile_login(headers={"X-Real-IP": "114.114.114.114"}) + self.mobile_login("kefu2", headers={}) + self.assertEqual(self.zyt.login_calls, 2) + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-http-20260928/tests/test_zyt_auth.py b/deploy/mobile-http-20260928/tests/test_zyt_auth.py new file mode 100644 index 000000000..a5af82759 --- /dev/null +++ b/deploy/mobile-http-20260928/tests/test_zyt_auth.py @@ -0,0 +1,100 @@ +"""跨服务器桌面认证:只在路由缺失时兼容,拒绝结果不能降级。""" +import io +import json +import unittest +import urllib.error +from unittest import mock + +import zyt_auth + + +class DesktopVerificationRegressionTest(unittest.TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def session(self, **updates): + data = {"user_id": "admin:9", "terminal": 7, "status": "active", + "root": False, "role_ids": [2], "permissions": ["tcm.diagnosis/lists"]} + data.update(updates) + return {"code": 1, "data": data} + + def verify(self): + return zyt_auth.verify_desktop_token("test-token", api_url="https://identity.example.test") + + def missing(self, status=404): + return urllib.error.HTTPError("https://identity.example.test", status, "test", None, None) + + def test_success_never_requests_ip_bound_web_profile(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(self.session(root=True, permissions=["*"])), + self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertTrue(identity["root"]) + self.assertEqual(identity["role_ids"], [2]) + self.assertEqual(identity["permissions"], ["*"]) + self.assertTrue(identity["permissions_known"]) + + def test_missing_permissions_do_not_trigger_profile_request(self): + payload = self.session() + del payload["data"]["permissions"] + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertEqual(identity["permissions"], []) + self.assertFalse(identity["permissions_known"]) + + def test_empty_permissions_are_known(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(self.session(permissions=[]))): + identity = self.verify() + self.assertTrue(identity["permissions_known"]) + self.assertEqual(identity["permissions"], []) + + def test_only_missing_routes_allow_legacy_verification(self): + for status in (404, 405): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(status), self.response({"code": 1, "data": { + "user": {"id": 9, "disable": 0, "role_ids": [2]}, "permissions": []}}), + ]) as opened: + identity = self.verify() + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(opened.call_count, 2) + + def test_http_rejections_do_not_fall_back(self): + for status in (401, 403, 500, 502): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=self.missing(status)) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_invalid_identity_cannot_be_accepted_through_web_fallback(self): + failures = [{"code": -1, "msg": "登录已失效"}, {"code": "bad"}, [], + {"code": 1, "data": []}, self.session(user_id=""), + self.session(terminal=1), self.session(terminal="bad"), + self.session(status="disabled")] + for payload in failures: + with self.subTest(payload=payload), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(payload), self.response({"code": 1, "data": {"user": {"id": 9}}}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_bad_json_and_network_failure_do_not_fall_back(self): + for result in (io.BytesIO(b"error"), urllib.error.URLError("offline")): + with self.subTest(result=result), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[result]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_legacy_ip_error_identifies_missing_server_deployment(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(), self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]): + with self.assertRaisesRegex(zyt_auth.ZytAuthError, "/adminapi/desktop/session"): + self.verify() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-ignore-20260929/01-preflight.sh b/deploy/mobile-ignore-20260929/01-preflight.sh new file mode 100644 index 000000000..eeeabef60 --- /dev/null +++ b/deploy/mobile-ignore-20260929/01-preflight.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# 基线核对、备份将被替换的源文件和 /m/ 目录、给当前镜像打回滚标签。只读线上 + 写本目录。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +test ! -e baseline-check.json +python3 remote_server_guard.py baseline > baseline-check.json.tmp +mv baseline-check.json.tmp baseline-check.json +cat baseline-check.json +test ! -e source-before +test ! -e preflight-complete +if docker image inspect zyt/wecom-admin:before-mobile-ignore-20260929 >/dev/null 2>&1; then + echo 'Refuse to replace existing rollback tag' >&2; exit 1 +fi +mkdir -m 700 source-before +python3 - <<'PY' +from pathlib import Path +import json, shutil +r = Path('/home/ps/mobile-ignore-20260929'); project = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + target = r / 'source-before' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(project / name, target) +shutil.copytree(project / plan['frontendDir'], r / 'source-before' / plan['frontendDir']) +PY +docker tag zyt/wecom-admin:current zyt/wecom-admin:before-mobile-ignore-20260929 +docker image inspect zyt/wecom-admin:current --format '{{.Id}}' > image-before.txt +printf '%s\n' ready > preflight-complete +echo "preflight complete; rollback image tagged before-mobile-ignore-20260929 ($(cat image-before.txt))" diff --git a/deploy/mobile-ignore-20260929/02-build.sh b/deploy/mobile-ignore-20260929/02-build.sh new file mode 100644 index 000000000..55dbb84bb --- /dev/null +++ b/deploy/mobile-ignore-20260929/02-build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# 构建候选镜像 + 离线跑测试 + 核对候选镜像文件。线上服务不受影响。 +# 本次不改数据库(只改 /m/ 静态文件里的文字),不做整库备份;回滚靠旧镜像和 source-before。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +test -f preflight-complete +test ! -e candidate-passed +python3 remote_server_guard.py baseline +test "$(docker image inspect zyt/wecom-admin:before-mobile-ignore-20260929 --format '{{.Id}}')" = "$(cat image-before.txt)" +docker build --network=none --pull=false -f Dockerfile.overlay -t zyt/wecom-admin:mobile-ignore-20260929 . > candidate-build.log 2>&1 +# serverTestModules 随包放在 tests/;imageTestModules 用镜像自带的测试文件(PYTHONPATH 后一项)。 +# -P:不把工作目录 /app/wechat_rpa 放到 sys.path 最前面,否则镜像里同名的旧测试会盖住随包的新测试。 +mapfile -t modules < <(python3 -c 'import json;p=json.load(open("release-plan.json"));print("\n".join(p["serverTestModules"]+p.get("imageTestModules",[])))') +test "${#modules[@]}" -gt 0 +docker run --rm --network=none --read-only --tmpfs /tmp:size=512m,mode=1777 --tmpfs /data:size=256m,mode=1777 \ + -v "$release/tests:/release-tests:ro" -e PYTHONPATH=/release-tests:/app/wechat_rpa \ + zyt/wecom-admin:mobile-ignore-20260929 python -X utf8 -B -P -m unittest "${modules[@]}" -q > candidate-tests.log 2>&1 +tail -n 3 candidate-tests.log +python3 remote_server_guard.py candidate +printf '%s\n' passed > candidate-passed diff --git a/deploy/mobile-ignore-20260929/03-deploy.sh b/deploy/mobile-ignore-20260929/03-deploy.sh new file mode 100644 index 000000000..c81d9f613 --- /dev/null +++ b/deploy/mobile-ignore-20260929/03-deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# 只替换整个 /m/ 目录(后端代码不变);切换镜像,只重建 api 容器。任何一步失败自动调用 rollback.sh。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +test -f candidate-passed +python3 remote_server_guard.py baseline +cd /opt/im-admin/deploy/im-admin +docker compose config --quiet +rollback_on_error() { rc=$?; trap - ERR; bash "$release/rollback.sh"; exit "$rc"; } +trap rollback_on_error ERR +python3 - <<'PY' +from pathlib import Path +import json, hashlib, os, shutil +r = Path('/home/ps/mobile-ignore-20260929'); target = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +rows = [row for row in json.loads((r / 'manifest.json').read_text()) if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +# 容器从镜像里读 /m/,宿主上的目录只给以后整包重建用;仍然整体换名,避免留下半套文件。 +staging = target / (frontend + '.mobile-ignore.tmp'); assert not staging.exists() +old = target / (frontend + '.mobile-ignore.old'); assert not old.exists() +for row in rows: + rel = row['path'][len('payload/'):] + data = (r / row['path']).read_bytes(); assert hashlib.sha256(data).hexdigest() == row['sha256'], rel + if rel.startswith(frontend + '/'): + dest = staging / rel[len(frontend) + 1:] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data); os.chmod(dest, 0o644) + continue + dest = (target / rel).resolve(); assert dest.is_relative_to(target) and dest.exists() + tmp = dest.with_name(dest.name + '.mobile-ignore.tmp'); assert not tmp.exists() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, dest.stat().st_mode & 0o777); os.replace(tmp, dest) +for directory in [staging] + [p for p in staging.rglob('*') if p.is_dir()]: + os.chmod(directory, 0o755) +final = target / frontend +os.replace(final, old) +os.replace(staging, final) +shutil.rmtree(old) # 原目录已在 source-before 里 +PY +docker tag zyt/wecom-admin:mobile-ignore-20260929 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +bash "$release/04-verify.sh" +trap - ERR +printf '%s\n' 'Phone console wording 黑名单 -> 忽略 deployed and verified.' diff --git a/deploy/mobile-ignore-20260929/04-verify.sh b/deploy/mobile-ignore-20260929/04-verify.sh new file mode 100644 index 000000000..fb71671b4 --- /dev/null +++ b/deploy/mobile-ignore-20260929/04-verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +python3 remote_server_guard.py deployed | tee deployed-verification.json diff --git a/deploy/mobile-ignore-20260929/Dockerfile.overlay b/deploy/mobile-ignore-20260929/Dockerfile.overlay new file mode 100644 index 000000000..e31a68c10 --- /dev/null +++ b/deploy/mobile-ignore-20260929/Dockerfile.overlay @@ -0,0 +1,8 @@ +ARG BASE_IMAGE=zyt/wecom-admin:before-mobile-ignore-20260929 +FROM ${BASE_IMAGE} +# 先删掉旧版 /m/ 目录再放新版,镜像里不残留旧的(带“黑名单”文字的)页面文件。 +USER root +RUN rm -rf /app/admin-web/apps/mobile-h5/dist +USER app +COPY --chown=app:app payload/admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ +WORKDIR /app/wechat_rpa diff --git a/deploy/mobile-ignore-20260929/manifest.json b/deploy/mobile-ignore-20260929/manifest.json new file mode 100644 index 000000000..e646a4f90 --- /dev/null +++ b/deploy/mobile-ignore-20260929/manifest.json @@ -0,0 +1,157 @@ +[ + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js", + "sha256": "2903b693612f3c118ad2142aadbd72b7df39f7e9264c2699d3a184118edf79e9", + "bytes": 2308 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css", + "sha256": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "bytes": 359 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js", + "sha256": "ef16f6b813d27b9dd615960fdef1e6e9a371483c611957b727f3eef766729db4", + "bytes": 14528 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png", + "sha256": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "bytes": 16743 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js", + "sha256": "889a5877c28e5d0cd1d3abec0e86ad6ca6615140a2270e977f58d6398de13fec", + "bytes": 4490 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js", + "sha256": "ecb3f12c2b4c6a2e11cf0300824237c237e5e08e3479445c942382fd9c5f5d96", + "bytes": 28481 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css", + "sha256": "b30e00c1e3c5d454be03e7a36622130f5dd14fe81f196cd6e226dd083d040617", + "bytes": 4220 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css", + "sha256": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "bytes": 3983 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js", + "sha256": "46136ea68c275d388a61345624b3511a7d3857d3b6591247602e89d99f5cecc8", + "bytes": 10460 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js", + "sha256": "67e7a6e35dd7abcaa8f61a02b448e24abe94362151fcd042b8706e4aebf7248c", + "bytes": 8422 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js", + "sha256": "5718f9373cb4af20835d41ecf49df053e85f29e835ec7857f806d453f2be7227", + "bytes": 14363 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-C_M75pH8.js", + "sha256": "fbedf701fea8bb30607032576c6afaa4de6a02308be22346010e43c8fa7b6ccb", + "bytes": 117757 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css", + "sha256": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "bytes": 97051 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js", + "sha256": "07735a478c62e626d73cb49b675182ef12683917b8d9ab6c18ad604a5ed051f6", + "bytes": 6336 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css", + "sha256": "15ecdd5cb294bb51eef05e4843ac3a65f6e1718cbf16206547a876c52c5e8a2d", + "bytes": 1107 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css", + "sha256": "08a41cb29e760c2040c22007c7fc5dd5669d84e7515f3c671072bf2241dae2bb", + "bytes": 927 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js", + "sha256": "3e2a4ec3c8ac8fc804c1c08dd3a913ec2fd298006f03643488dec37f08f74d0f", + "bytes": 3459 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css", + "sha256": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "bytes": 130 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js", + "sha256": "256c84cc64e04be449d156d1704efb4575d4e5d137af99f9a83397b2e768317c", + "bytes": 2939 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js", + "sha256": "0ed96441a0fb80d8e585d63838b200ea6435f8deb2641f1effee136f96ef2e76", + "bytes": 607 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png", + "sha256": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "bytes": 24681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png", + "sha256": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "bytes": 5439 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png", + "sha256": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "bytes": 26433 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png", + "sha256": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "bytes": 144310 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png", + "sha256": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "bytes": 95961 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/index.html", + "sha256": "d69ec239352bbbfcd48f6243ab704424ed9708c2dd332581a974fe3b4a3855fe", + "bytes": 1357 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest", + "sha256": "36885be5f98b3bba7e79780a32ab0bbbbe06ed1fc26a14950abc1bb6ffe6a697", + "bytes": 681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/sw.js", + "sha256": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "bytes": 339 + }, + { + "path": "tests/test_backend.py", + "sha256": "7473ec5583c97941e132ade7a1f66abcf54fa6108ec6bb2a182f0152964fff61", + "bytes": 66279 + }, + { + "path": "tests/test_remote_api.py", + "sha256": "479cb9b042dc14707af718734c33688013203d4154f36199b7b0ced656653c39", + "bytes": 38523 + }, + { + "path": "tests/test_zyt_auth.py", + "sha256": "40ab2d37f51780af26497e22948ef665c92b7f3707f6ba30a6d711bf5b15dfa6", + "bytes": 4907 + } +] \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/mobile-ignore-server-bundle.tar.gz b/deploy/mobile-ignore-20260929/mobile-ignore-server-bundle.tar.gz new file mode 100644 index 000000000..fd2265026 Binary files /dev/null and b/deploy/mobile-ignore-20260929/mobile-ignore-server-bundle.tar.gz differ diff --git a/deploy/mobile-ignore-20260929/payload-diff.txt b/deploy/mobile-ignore-20260929/payload-diff.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload-diff.txt @@ -0,0 +1 @@ + diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css new file mode 100644 index 000000000..03b9e2d0d --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css @@ -0,0 +1 @@ +.switcher[data-v-91cf283c]{max-height:80vh}.switcher-body[data-v-91cf283c]{padding:16px 0 12px}.switcher-body h3[data-v-91cf283c]{margin:0 16px 8px;font-size:17px}.switcher-state[data-v-91cf283c]{place-items:center;padding:32px 0;display:grid}.switcher-list[data-v-91cf283c]{margin-top:12px}.current[data-v-91cf283c]{color:var(--app-primary);margin-left:6px} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js new file mode 100644 index 000000000..8b8a72d4a --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{c as n,d as r,l as i,u as a}from"./format-Dt9-L1ne.js";import{n as o,o as s,s as c,t as l}from"./cell-group-B1mihubH.js";import{n as u,r as d,t as f}from"./field-BLYigdmn.js";import{$ as p,Bt as m,Ct as h,Dt as g,E as _,Et as v,Ft as y,G as b,Gt as x,Jt as S,K as C,Kt as w,Lt as T,Mt as E,Nt as ee,O as D,Q as O,St as k,Ut as A,V as te,Vt as j,X as M,Y as N,Yt as ne,Z as re,_ as P,_t as F,a as ie,c as ae,et as oe,ft as se,g as I,gt as L,ht as ce,l as le,n as ue,o as R,ot as z,q as B,qt as V,rt as de,vt as fe,wt as pe,x as me,yt as H}from"./index-C_M75pH8.js";var U=0;function he(e){e?(U||document.body.classList.add(`van-toast--unclickable`),U++):U&&(U--,U||document.body.classList.remove(`van-toast--unclickable`))}var[ge,W]=P(`toast`),_e=[`show`,`overlay`,`teleport`,`transition`,`overlayClass`,`overlayStyle`,`closeOnClickOverlay`,`zIndex`],ve={icon:String,show:Boolean,type:N(`text`),overlay:Boolean,message:M,iconSize:M,duration:C(2e3),position:N(`middle`),teleport:[String,Object],wordBreak:String,className:null,iconPrefix:String,transition:N(`van-fade`),loadingType:String,forbidClick:Boolean,overlayClass:null,overlayStyle:Object,closeOnClick:Boolean,closeOnClickOverlay:Boolean,zIndex:M},ye=h({name:ge,props:ve,emits:[`update:show`],setup(t,{emit:n,slots:r}){let i,o=!1,c=()=>{let e=t.show&&t.forbidClick;o!==e&&(o=e,he(o))},l=e=>n(`update:show`,e),u=()=>{t.closeOnClick&&l(!1)},d=()=>clearTimeout(i),f=()=>{let{icon:n,type:r,iconSize:i,iconPrefix:a,loadingType:o}=t;if(n||r===`success`||r===`fail`)return k(e,{name:n||r,size:i,class:W(`icon`),classPrefix:a},null);if(r===`loading`)return k(s,{class:W(`loading`),size:i,type:o},null)},p=()=>{let{type:e,message:n}=t;if(r.message)return k(`div`,{class:W(`text`)},[r.message()]);if(oe(n)&&n!==``)return e===`html`?k(`div`,{key:0,class:W(`text`),innerHTML:String(n)},null):k(`div`,{class:W(`text`)},[n])};return j(()=>[t.show,t.forbidClick],c),j(()=>[t.show,t.type,t.message,t.duration],()=>{d(),t.show&&t.duration>0&&(i=setTimeout(()=>{l(!1)},t.duration))}),E(c),ee(c),()=>k(a,v({class:[W([t.position,t.wordBreak===`normal`?`break-normal`:t.wordBreak,{[t.type]:!t.icon}]),t.className],lockScroll:!1,onClick:u,onClosed:d,"onUpdate:show":l},z(t,_e)),{default:()=>[f(),p()]})}}),be={icon:``,type:`text`,message:``,className:``,overlay:!1,onClose:void 0,onOpened:void 0,duration:2e3,teleport:`body`,iconSize:void 0,iconPrefix:void 0,position:`middle`,transition:`van-fade`,forbidClick:!1,loadingType:void 0,overlayClass:``,overlayStyle:void 0,closeOnClick:!1,closeOnClickOverlay:!1},G=[],K=O({},be),xe=new Map;function Se(e){return de(e)?e:{message:e}}function Ce(){let{instance:e,unmount:t}=n({setup(){let e=w(``),{open:t,state:n,close:r,toggle:a}=i(),o=()=>{},s=()=>k(ye,v(n,{onClosed:o,"onUpdate:show":a}),null);return j(e,e=>{n.message=e}),pe().render=s,{open:t,close:r,message:e}}});return e}function we(){if(!G.length){let e=Ce();G.push(e)}return G[G.length-1]}function Te(e={}){if(!p)return{};let t=we(),n=Se(e);return t.open(O({},K,xe.get(n.type||K.type),n)),t}var[Ee,q]=P(`empty`),De={image:N(`default`),imageSize:[Number,String,Array],description:String},Oe=h({name:Ee,props:De,setup(e,{slots:t}){let n=()=>{let n=t.description?t.description():e.description;if(n)return k(`p`,{class:q(`description`)},[n])},r=()=>{if(t.default)return k(`div`,{class:q(`bottom`)},[t.default()])},i=d(),a=e=>`${i}-${e}`,o=e=>`url(#${a(e)})`,s=(e,t,n)=>k(`stop`,{"stop-color":e,offset:`${t}%`,"stop-opacity":n},null),c=(e,t)=>[s(e,0),s(t,100)],l=e=>[k(`defs`,null,[k(`radialGradient`,{id:a(e),cx:`50%`,cy:`54%`,fx:`50%`,fy:`54%`,r:`297%`,gradientTransform:`matrix(-.16 0 0 -.33 .58 .72)`,"data-allow-mismatch":`attribute`},[s(`#EBEDF0`,0),s(`#F2F3F5`,100,.3)])]),k(`ellipse`,{fill:o(e),opacity:`.8`,cx:`80`,cy:`140`,rx:`46`,ry:`8`,"data-allow-mismatch":`attribute`},null)],u=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`a`),x1:`64%`,y1:`100%`,x2:`64%`,"data-allow-mismatch":`attribute`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M36 131V53H16v20H2v58h34z`,fill:o(`a`)},null),k(`path`,{d:`M123 15h22v14h9v77h-31V15z`,fill:o(`a`)},null)])],f=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`b`),x1:`64%`,y1:`97%`,x2:`64%`,y2:`0%`,"data-allow-mismatch":`attribute`},[s(`#F2F3F5`,0,.3),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M87 6c3 0 7 3 8 6a8 8 0 1 1-1 16H80a7 7 0 0 1-8-6c0-4 3-7 6-7 0-5 4-9 9-9Z`,fill:o(`b`)},null),k(`path`,{d:`M19 23c2 0 3 1 4 3 2 0 4 2 4 4a4 4 0 0 1-4 3v1h-7v-1l-1 1c-2 0-3-2-3-4 0-1 1-3 3-3 0-2 2-4 4-4Z`,fill:o(`b`)},null)])],p=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{id:a(1),x1:`64%`,y1:`100%`,x2:`64%`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)]),k(`linearGradient`,{id:a(2),x1:`50%`,x2:`50%`,y2:`84%`},[s(`#EBEDF0`,0),s(`#DCDEE0`,100,0)]),k(`linearGradient`,{id:a(3),x1:`100%`,x2:`100%`,y2:`100%`},[c(`#EAEDF0`,`#DCDEE0`)]),k(`radialGradient`,{id:a(4),cx:`50%`,cy:`0%`,fx:`50%`,fy:`0%`,r:`100%`,gradientTransform:`matrix(0 1 -.54 0 .5 -.5)`},[s(`#EBEDF0`,0),s(`#FFF`,100,0)])]),k(`g`,{fill:`none`},[u(),k(`path`,{fill:o(4),d:`M0 139h160v21H0z`,"data-allow-mismatch":`attribute`},null),k(`path`,{d:`M80 54a7 7 0 0 1 3 13v27l-2 2h-2a2 2 0 0 1-2-2V67a7 7 0 0 1 3-13z`,fill:o(2),"data-allow-mismatch":`attribute`},null),k(`g`,{opacity:`.6`,"stroke-linecap":`round`,"stroke-width":`7`,"data-allow-mismatch":`children`},[k(`path`,{d:`M64 47a19 19 0 0 0-5 13c0 5 2 10 5 13`,stroke:o(3)},null),k(`path`,{d:`M53 36a34 34 0 0 0 0 48`,stroke:o(3)},null),k(`path`,{d:`M95 73a19 19 0 0 0 6-13c0-5-2-9-6-13`,stroke:o(3)},null),k(`path`,{d:`M106 84a34 34 0 0 0 0-48`,stroke:o(3)},null)]),k(`g`,{transform:`translate(31 105)`},[k(`rect`,{fill:`#EBEDF0`,width:`98`,height:`34`,rx:`2`},null),k(`rect`,{fill:`#FFF`,x:`9`,y:`8`,width:`80`,height:`18`,rx:`1.1`},null),k(`rect`,{fill:`#EBEDF0`,x:`15`,y:`12`,width:`18`,height:`6`,rx:`1.1`},null)])])]),m=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(5)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`95%`,y1:`48%`,x2:`5.5%`,y2:`51%`,id:a(6)},[c(`#EAEDF1`,`#DCDEE0`)]),k(`linearGradient`,{y1:`45%`,x2:`100%`,y2:`54%`,id:a(7)},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),k(`g`,{transform:`translate(36 50)`,fill:`none`},[k(`g`,{transform:`translate(8)`},[k(`rect`,{fill:`#EBEDF0`,opacity:`.6`,x:`38`,y:`13`,width:`36`,height:`53`,rx:`2`},null),k(`rect`,{fill:o(5),width:`64`,height:`66`,rx:`2`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#FFF`,x:`6`,y:`6`,width:`52`,height:`55`,rx:`1`},null),k(`g`,{transform:`translate(15 17)`,fill:o(6),"data-allow-mismatch":`attribute`},[k(`rect`,{width:`34`,height:`6`,rx:`1`},null),k(`path`,{d:`M0 14h34v6H0z`},null),k(`rect`,{y:`28`,width:`34`,height:`6`,rx:`1`},null)])]),k(`rect`,{fill:o(7),y:`61`,width:`88`,height:`28`,rx:`1`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#F7F8FA`,x:`29`,y:`72`,width:`30`,height:`6`,rx:`1`},null)])]),h=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,null,[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(8),"data-allow-mismatch":`attribute`},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),l(`c`),k(`path`,{d:`m59 60 21 21 21-21h3l9 9v3L92 93l21 21v3l-9 9h-3l-21-21-21 21h-3l-9-9v-3l21-21-21-21v-3l9-9h3Z`,fill:o(8),"data-allow-mismatch":`attribute`},null)]),g=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,y1:`100%`,x2:`50%`,id:a(9)},[c(`#EEE`,`#D8D8D8`)]),k(`linearGradient`,{x1:`100%`,y1:`50%`,y2:`50%`,id:a(10)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(11)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(12)},[c(`#FFF`,`#F7F8FA`)])]),u(),f(),l(`d`),k(`g`,{transform:`rotate(-45 113 -4)`,fill:`none`,"data-allow-mismatch":`children`},[k(`rect`,{fill:o(9),x:`24`,y:`52.8`,width:`5.8`,height:`19`,rx:`1`},null),k(`rect`,{fill:o(10),x:`22.1`,y:`67.3`,width:`9.9`,height:`28`,rx:`1`},null),k(`circle`,{stroke:o(11),"stroke-width":`8`,cx:`27`,cy:`27`,r:`27`},null),k(`circle`,{fill:o(12),cx:`27`,cy:`27`,r:`16`},null),k(`path`,{d:`M37 7c-8 0-15 5-16 12`,stroke:o(11),"stroke-width":`3`,opacity:`.5`,"stroke-linecap":`round`,transform:`rotate(45 29 13)`},null)])]),_=()=>{var n;if(t.image)return t.image();let r={error:h,search:g,network:p,default:m};return((n=r[e.image])==null?void 0:n.call(r))||k(`img`,{src:e.image},null)};return()=>k(`div`,{class:q()},[k(`div`,{class:q(`image`),style:me(e.imageSize)},[_()]),n(),r()])}}),J=I(Oe),[ke,Y,Ae]=P(`pull-refresh`),X=50,je=[`pulling`,`loosing`,`success`],Me={disabled:Boolean,modelValue:Boolean,headHeight:B(X),successText:String,pullingText:String,loosingText:String,loadingText:String,pullDistance:M,successDuration:B(500),animationDuration:B(300)},Ne=h({name:ke,props:Me,emits:[`change`,`refresh`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i,a=w(),o=w(),c=b(a),l=x({status:`normal`,distance:0,duration:0}),u=r(),d=()=>{if(e.headHeight!==X)return{height:`${e.headHeight}px`}},f=()=>l.status!==`loading`&&l.status!==`success`&&!e.disabled,p=t=>{let n=+(e.pullDistance||e.headHeight);return t>n&&(t=t{let i=+(e.pullDistance||e.headHeight);l.distance=n,r?l.status=`loading`:n===0?l.status=`normal`:n{let{status:t}=l;return t===`normal`?``:e[`${t}Text`]||Ae(t)},v=()=>{let{status:e,distance:t}=l;if(n[e])return n[e]({distance:t});let r=[];return je.includes(e)&&r.push(k(`div`,{class:Y(`text`)},[h()])),e===`loading`&&r.push(k(s,{class:Y(`loading`)},{default:h})),r},y=()=>{l.status=`success`,setTimeout(()=>{m(0)},+e.successDuration)},S=e=>{i=_(c.value)===0,i&&(l.duration=0,u.start(e))},C=e=>{f()&&S(e)},T=e=>{if(f()){i||S(e);let{deltaY:t}=u;u.move(e),i&&t.value>=0&&u.isVertical()&&(D(e),m(p(t.value)))}},E=()=>{i&&u.deltaY.value&&f()&&(l.duration=+e.animationDuration,l.status===`loosing`?(m(+e.headHeight,!0),t(`update:modelValue`,!0),g(()=>t(`refresh`))):m(0))};return j(()=>e.modelValue,t=>{l.duration=+e.animationDuration,t?m(+e.headHeight,!0):n.success||e.successText?y():m(0,!1)}),te(`touchmove`,T,{target:o}),()=>{var e;let t={transitionDuration:`${l.duration}ms`,transform:l.distance?`translate3d(0,${l.distance}px, 0)`:``};return k(`div`,{ref:a,class:Y()},[k(`div`,{ref:o,class:Y(`track`),style:t,onTouchstartPassive:C,onTouchend:E,onTouchcancel:E},[k(`div`,{class:Y(`head`),style:d()},[v()]),(e=n.default)==null?void 0:e.call(n)])])}}}),Pe=I(Ne),[Fe,Z,Q]=P(`search`),Ie=O({},u,{label:String,shape:N(`square`),leftIcon:N(`search`),clearable:re,actionText:String,background:String,showAction:Boolean}),Le=h({name:Fe,props:Ie,emits:[`blur`,`focus`,`clear`,`search`,`cancel`,`clickInput`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(e,{emit:t,slots:n,attrs:r}){let i=d(),a=w(),o=()=>{n.action||(t(`update:modelValue`,``),t(`cancel`))},s=n=>{n.keyCode===13&&(D(n),t(`search`,e.modelValue))},l=()=>e.id||`${i}-input`,p=()=>{if(n.label||e.label)return k(`label`,{class:Z(`label`),for:l(),"data-allow-mismatch":`attribute`},[n.label?n.label():e.label])},m=()=>{if(e.showAction){let t=e.actionText||Q(`cancel`);return k(`div`,{class:Z(`action`),role:`button`,tabindex:0,onClick:o},[n.action?n.action():t])}},h=()=>{var e;return(e=a.value)==null?void 0:e.blur()},g=()=>{var e;return(e=a.value)==null?void 0:e.focus()},_=e=>t(`blur`,e),y=e=>t(`focus`,e),b=e=>t(`clear`,e),x=e=>t(`clickInput`,e),S=e=>t(`clickLeftIcon`,e),C=e=>t(`clickRightIcon`,e),T=Object.keys(u),E=()=>{let i=O({},r,z(e,T),{id:l()});return k(f,v({ref:a,type:`search`,class:Z(`field`,{"with-message":i.errorMessage}),border:!1,labelAlign:`left`,onBlur:_,onFocus:y,onClear:b,onKeypress:s,onClickInput:x,onClickLeftIcon:S,onClickRightIcon:C,"onUpdate:modelValue":e=>t(`update:modelValue`,e)},i),z(n,[`left-icon`,`right-icon`]))};return c({focus:g,blur:h}),()=>{var t;return k(`div`,{class:Z({"show-action":e.showAction}),style:{background:e.background}},[(t=n.left)==null?void 0:t.call(n),k(`div`,{class:Z(`content`,e.shape)},[p(),E()]),m()])}}}),$=I(Le),Re={class:`switcher-body`},ze={key:0,class:`switcher-state`},Be={key:0,class:`current`},Ve=t(h({__name:`AccountSwitcher`,props:{show:{type:Boolean,default:!1},showModifiers:{}},emits:[`update:show`],setup(e){let t=m(e,`show`),n=w(``),r=w([]),i=w(!1),c=w(``),u=0,d,f=ce(()=>{var e;return(e=ue.me)==null?void 0:e.account});async function p(){let e=++u;i.value=!0,c.value=``;try{let{items:t}=await le.accounts(n.value.trim());e===u&&(r.value=t.filter(e=>{var t;return e.id!==((t=f.value)==null?void 0:t.id)}))}catch(t){e===u&&(c.value=t instanceof ae?t.message:`加载失败`)}finally{e===u&&(i.value=!1)}}j(t,e=>{e&&p()}),j(n,()=>{window.clearTimeout(d),d=window.setTimeout(p,300)});function h(e){ie(e),t.value=!1}function g(e){return e.device_count?e.online?e.listening?`监听中`:`已停止`:`离线`:`没有电脑`}function _(e){return e.online?e.listening?`tone-success`:`tone-warning`:`tone-muted`}return(e,u)=>(y(),F(V(a),{show:t.value,"onUpdate:show":u[2]||(u[2]=e=>t.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`switcher`},{default:A(()=>[L(`div`,Re,[u[3]||(u[3]=L(`h3`,null,`切换客服`,-1)),k(V($),{modelValue:n.value,"onUpdate:modelValue":u[0]||(u[0]=e=>n.value=e),shape:`round`,placeholder:`搜索客服姓名或账号`},null,8,[`modelValue`]),k(V(l),{inset:``},{default:A(()=>[k(V(o),{title:`我自己`,label:f.value?`${f.value.nickname||f.value.sn}`:``,icon:`user-o`,clickable:``,value:V(R)===void 0?`当前`:``,onClick:u[1]||(u[1]=e=>h(null))},null,8,[`label`,`value`])]),_:1}),i.value&&!r.value.length?(y(),H(`div`,ze,[k(V(s),{size:`20`})])):c.value?(y(),F(V(J),{key:1,image:`network`,description:c.value},null,8,[`description`])):r.value.length?(y(),F(V(l),{key:3,inset:``,class:`switcher-list`},{default:A(()=>[(y(!0),H(se,null,T(r.value,e=>(y(),F(V(o),{key:e.id,title:e.nickname||e.sn||`客服 #${e.id}`,label:e.sn,clickable:``,onClick:t=>h(e)},{value:A(()=>[L(`span`,{class:S(_(e))},ne(g(e)),3),V(R)===e.id?(y(),H(`span`,Be,`当前`)):fe(``,!0)]),_:2},1032,[`title`,`label`,`onClick`]))),128))]),_:1})):(y(),F(V(J),{key:2,image:`search`,description:`没有找到其他客服`}))])]),_:1},8,[`show`]))}}),[[`__scopeId`,`data-v-91cf283c`]]);export{Te as a,J as i,$ as n,Pe as r,Ve as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css new file mode 100644 index 000000000..323e8d6a5 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css @@ -0,0 +1 @@ +.detail[data-v-bdbcc19d]{padding-bottom:16px}.detail-head[data-v-bdbcc19d]{align-items:center;gap:12px;padding:16px;display:flex}.avatar[data-v-bdbcc19d]{width:52px;height:52px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:14px;flex:none;place-items:center;font-size:22px;font-weight:600;display:grid}.detail-title[data-v-bdbcc19d]{flex:1;min-width:0}.detail-title h2[data-v-bdbcc19d]{text-overflow:ellipsis;white-space:nowrap;margin:0 0 4px;font-size:18px;overflow:hidden}.detail-title p[data-v-bdbcc19d]{margin:0;font-size:13px}.badge[data-v-bdbcc19d]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.group[data-v-bdbcc19d]{margin-bottom:12px}.state[data-v-bdbcc19d]{background:var(--app-surface);border:1px solid var(--app-border);border-radius:14px;margin:0 16px;padding:16px}.state--blocked[data-v-bdbcc19d]{border-color:var(--app-danger-soft);background:linear-gradient(0deg, var(--app-danger-soft), var(--app-danger-soft)), var(--app-surface)}.state h3[data-v-bdbcc19d]{align-items:center;gap:6px;margin:0 0 6px;font-size:15px;display:flex}.state--blocked h3[data-v-bdbcc19d]{color:var(--app-danger)}.state>p[data-v-bdbcc19d]{color:var(--app-text-2);margin:0 0 12px;font-size:13px;line-height:1.6}.state dl[data-v-bdbcc19d]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin:0 0 14px;display:grid}.state dt[data-v-bdbcc19d]{color:var(--app-muted);font-size:12px}.state dd[data-v-bdbcc19d]{word-break:break-all;margin:2px 0 0;font-size:14px}.reason[data-v-bdbcc19d]{margin:0 0 12px}.reason[data-v-bdbcc19d] .van-cell-group--inset{margin:0}.state[data-v-bdbcc19d] .van-cell-group--inset{margin:0 0 12px}.note[data-v-bdbcc19d]{color:var(--app-muted);margin:0;font-size:13px;line-height:1.6}.row[data-v-645c253b]{border:0;border-bottom:1px solid var(--app-border);background:var(--app-surface);width:100%;min-height:64px;color:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;padding:10px 16px;display:flex}.row[data-v-645c253b]:active,.row.active[data-v-645c253b]{background:var(--app-primary-soft)}.avatar[data-v-645c253b]{border-radius:12px;flex:none;place-items:center;width:42px;height:42px;font-size:17px;font-weight:600;display:grid}.avatar--wecom[data-v-645c253b]{color:#2f6fff;background:#2f6fff1f}.avatar--wechat[data-v-645c253b]{color:#0f9e69;background:#13b77a24}.avatar--group[data-v-645c253b]{color:#7a5cff;background:#8a6bff24}.body[data-v-645c253b]{flex:1;gap:4px;min-width:0;display:grid}.line[data-v-645c253b]{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.name[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.time[data-v-645c253b]{color:var(--app-muted);flex:none;font-size:12px}.sub[data-v-645c253b]{color:var(--app-muted);font-size:12px}.meta[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.blocked[data-v-645c253b]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;padding:1px 8px;font-weight:600}.page[data-v-4433756d]{min-height:100%}.nav-action[data-v-4433756d]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.toolbar[data-v-4433756d]{top:calc(46px + env(safe-area-inset-top));z-index:2;background:var(--app-bg);position:-webkit-sticky;position:sticky}.toolbar[data-v-4433756d] .van-search{background:0 0;padding-bottom:4px}.tabs[data-v-4433756d]{--van-tabs-nav-background:transparent;padding:0 4px}.list-body[data-v-4433756d]{min-height:60vh}.single .list-pane[data-v-4433756d]{max-width:720px;margin:0 auto}.split[data-v-4433756d]{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;max-width:1180px;margin:0 auto;padding:0 24px 24px;display:grid}.split .list-body[data-v-4433756d]{border-radius:16px;overflow:clip}.detail-pane[data-v-4433756d]{top:calc(46px + env(safe-area-inset-top) + 12px);min-height:320px;margin-top:12px;position:-webkit-sticky;position:sticky}.sheet[data-v-4433756d]{max-height:88vh;padding-top:8px} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js new file mode 100644 index 000000000..dc33e629c --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,n as r,p as i,r as a,t as o}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n as s}from"./use-placeholder-BHVOzwXa.js";import{a as c,d as l,i as u,o as d,s as f,u as p}from"./format-Dt9-L1ne.js";import{a as m,n as h,o as g,s as _,t as v}from"./cell-group-B1mihubH.js";import{r as y,t as b}from"./field-BLYigdmn.js";import{a as x,i as S,n as ee,r as C,t as w}from"./AccountSwitcher-DmKdvr8J.js";import{$ as te,A as ne,At as T,B as E,C as D,Ct as O,D as k,Dt as A,E as j,Et as M,F as N,Ft as P,G as re,Gt as ie,H as ae,Ht as F,I,It as oe,J as se,Jt as ce,Kt as L,Lt as le,M as ue,Mt as de,N as fe,O as pe,Ot as me,Pt as he,Q as ge,R as _e,S as ve,St as R,Tt as ye,U as be,Ut as z,V as xe,Vt as B,W as V,Wt as Se,X as H,Y as Ce,Yt as U,Z as W,_ as G,_t as K,bt as we,c as Te,et as Ee,ft as De,g as Oe,gt as q,ht as J,j as ke,jt as Ae,kt as je,l as Me,n as Ne,o as Pe,ot as Fe,p as Ie,q as Y,qt as X,s as Le,t as Re,ut as ze,v as Be,vt as Z,w as Ve,wt as He,xt as Q,y as Ue,yt as $,z as We}from"./index-C_M75pH8.js";var Ge=Array.isArray,Ke=e=>typeof e==`string`,qe=e=>typeof e==`object`&&!!e,Je=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ye=/\B([A-Z])/g,Xe=Je(e=>e.replace(Ye,`-$1`).toLowerCase());function Ze(e){if(Ge(e)){let t={};for(let n=0;ne.startsWith(`/*`)?``:e).split(Qe).forEach(e=>{if(e){let n=e.split($e);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function nt(e){if(!e)return``;if(Ke(e))return e;let t=``;for(let n in e){let r=e[n];if(Ke(r)||typeof r==`number`){let e=n.startsWith(`--`)?n:Xe(n);t+=`${e}:${r};`}}return t}function rt(e){let t=``;if(Ke(e))t=e;else if(Ge(e))for(let n=0;nt||!o&&at?i=We(u):r&&(i=We(r))}return u(),l}function ot(){let e=L([]),t=[];return T(()=>{e.value=[]}),[e,n=>(t[n]||(t[n]=t=>{e.value[n]=t}),t[n])]}function st(e,t){if(!te||!window.IntersectionObserver)return;let n=new IntersectionObserver(e=>{t(e[0].intersectionRatio>0)},{root:document.body}),r=()=>{e.value&&n.observe(e.value)},i=()=>{e.value&&n.unobserve(e.value)};Ae(i),je(i),_e(r)}var[ct,lt]=G(`sticky`),ut={zIndex:H,position:Ce(`top`),container:Object,offsetTop:Y(0),offsetBottom:Y(0)},dt=O({name:ct,props:ut,emits:[`scroll`,`change`],setup(e,{emit:t,slots:n}){let r=L(),i=re(r),a=ie({fixed:!1,width:0,height:0,transform:0}),o=L(!1),s=J(()=>D(e.position===`top`?e.offsetTop:e.offsetBottom)),c=J(()=>{if(o.value)return;let{fixed:e,height:t,width:n}=a;if(e)return{width:`${n}px`,height:`${t}px`}}),l=J(()=>{if(!a.fixed||o.value)return;let t=ge(ve(e.zIndex),{width:`${a.width}px`,height:`${a.height}px`,[e.position]:`${s.value}px`});return a.transform&&(t.transform=`translate3d(0, ${a.transform}px, 0)`),t}),u=e=>t(`scroll`,{scrollTop:e,isFixed:a.fixed}),d=()=>{if(!r.value||k(r))return;let{container:t,position:n}=e,i=V(r),o=j(window);if(a.width=i.width,a.height=i.height,n===`top`){if(t){let e=V(t),n=e.bottom-s.value-a.height;a.fixed=s.value>i.top&&e.bottom>0,a.transform=n<0?n:0}else a.fixed=s.value>i.top}else{let{clientHeight:e}=document.documentElement;if(t){let n=V(t),r=e-n.top-s.value-a.height;a.fixed=e-s.valuen.top,a.transform=r<0?-r:0}else a.fixed=e-s.valuea.fixed,e=>t(`change`,e)),xe(`scroll`,d,{target:i,passive:!0}),st(r,d),B([fe,ue],()=>{r.value&&!k(r)&&a.fixed&&(o.value=!0,A(()=>{let e=V(r);a.width=e.width,a.height=e.height,o.value=!1}))}),()=>{var e;return R(`div`,{ref:r,style:c.value},[R(`div`,{class:lt({fixed:a.fixed&&!o.value}),style:l.value},[(e=n.default)==null?void 0:e.call(n)])])}}}),ft=Oe(dt),[pt,mt]=G(`swipe`),ht={loop:W,width:H,height:H,vertical:Boolean,autoplay:Y(0),duration:Y(500),touchable:W,lazyRender:Boolean,initialSwipe:Y(0),indicatorColor:String,showIndicators:W,stopPropagation:W},gt=Symbol(pt),_t=O({name:pt,props:ht,emits:[`change`,`dragStart`,`dragEnd`],setup(e,{emit:n,slots:r}){let i=L(),a=L(),o=ie({rect:null,width:0,height:0,offset:0,active:0,swiping:!1}),s=!1,c=l(),{children:u,linkChildren:d}=E(gt),f=J(()=>u.length),p=J(()=>o[e.vertical?`height`:`width`]),m=J(()=>e.vertical?c.deltaY.value:c.deltaX.value),h=J(()=>o.rect?(e.vertical?o.rect.height:o.rect.width)-p.value*f.value:0),g=J(()=>p.value?Math.ceil(Math.abs(h.value)/p.value):f.value),v=J(()=>f.value*p.value),y=J(()=>(o.active+f.value)%f.value),b=J(()=>{let t=e.vertical?`vertical`:`horizontal`;return c.direction.value===t}),x=J(()=>{let t={transitionDuration:`${o.swiping?0:e.duration}ms`,transform:`translate${e.vertical?`Y`:`X`}(${+o.offset.toFixed(2)}px)`};if(p.value){let n=e.vertical?`height`:`width`,r=e.vertical?`width`:`height`;t[n]=`${v.value}px`,t[r]=e[r]?`${e[r]}px`:``}return t}),S=t=>{let{active:n}=o;return t?e.loop?Ue(n+t,-1,f.value):Ue(n+t,0,g.value):n},ee=(t,n=0)=>{let r=t*p.value;e.loop||(r=Math.min(r,-h.value));let i=n-r;return e.loop||(i=Ue(i,h.value,0)),i},C=({pace:t=0,offset:r=0,emitChange:i})=>{if(f.value<=1)return;let{active:a}=o,s=S(t),c=ee(s,r);if(e.loop){if(u[0]&&c!==h.value){let e=c0;u[f.value-1].setOffset(e?-v.value:0)}}o.active=s,o.offset=c,i&&s!==a&&n(`change`,y.value)},w=()=>{o.swiping=!0,o.active<=-1?C({pace:f.value}):o.active>=f.value&&C({pace:-f.value})},te=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:-1,emitChange:!0})})},ne=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:1,emitChange:!0})})},T,D=()=>clearTimeout(T),O=()=>{D(),+e.autoplay>0&&f.value>1&&(T=setTimeout(()=>{ne(),O()},+e.autoplay))},j=(t=+e.initialSwipe)=>{if(!i.value)return;let n=()=>{var n,r;if(!k(i)){let t={width:i.value.offsetWidth,height:i.value.offsetHeight};o.rect=t,o.width=+((n=e.width)==null?t.width:n),o.height=+((r=e.height)==null?t.height:r)}f.value&&(t=Math.min(f.value-1,t),t===-1&&(t=f.value-1)),o.active=t,o.swiping=!0,o.offset=ee(t),u.forEach(e=>{e.setOffset(0)}),O()};k(i)?A().then(n):n()},M=()=>j(o.active),N,P=t=>{!e.touchable||t.touches.length>1||(c.start(t),s=!1,N=Date.now(),D(),w())},re=t=>{e.touchable&&o.swiping&&(c.move(t),b.value&&(!e.loop&&(o.active===0&&m.value>0||o.active===f.value-1&&m.value<0)||(pe(t,e.stopPropagation),C({offset:m.value}),s||(n(`dragStart`,{index:y.value}),s=!0))))},F=()=>{if(!e.touchable||!o.swiping)return;let t=Date.now()-N,r=m.value/t;if((Math.abs(r)>.25||Math.abs(m.value)>p.value/2)&&b.value){let t=e.vertical?c.offsetY.value:c.offsetX.value,n=0;n=e.loop?t>0?m.value>0?-1:1:0:-Math[m.value>0?`ceil`:`floor`](m.value/p.value),C({pace:n,emitChange:!0})}else m.value&&C({pace:0});s=!1,o.swiping=!1,n(`dragEnd`,{index:y.value}),O()},oe=(t,n={})=>{w(),c.reset(),I(()=>{let r;r=e.loop&&t===f.value?o.active===0?0:t:t%f.value,n.immediate?I(()=>{o.swiping=!1}):o.swiping=!1,C({pace:r-o.active,emitChange:!0})})},se=(t,n)=>{let r=n===y.value,i=r?{backgroundColor:e.indicatorColor}:void 0;return R(`i`,{style:i,class:mt(`indicator`,{active:r})},null)},ce=()=>{if(r.indicator)return r.indicator({active:y.value,total:f.value});if(e.showIndicators&&f.value>1)return R(`div`,{class:mt(`indicators`,{vertical:e.vertical})},[Array(f.value).fill(``).map(se)])};return _({prev:te,next:ne,state:o,resize:M,swipeTo:oe}),d({size:p,props:e,count:f,activeIndicator:y}),B(()=>e.initialSwipe,e=>j(+e)),B(f,()=>j(o.active)),B(()=>e.autoplay,O),B([fe,ue,()=>e.width,()=>e.height],M),B(ae(),e=>{e===`visible`?O():D()}),de(j),me(()=>j(o.active)),t(()=>j(o.active)),Ae(D),je(D),xe(`touchmove`,re,{target:a}),()=>{var t;return R(`div`,{ref:i,class:mt()},[R(`div`,{ref:a,style:x.value,class:mt(`track`,{vertical:e.vertical}),onTouchstartPassive:P,onTouchend:F,onTouchcancel:F},[(t=r.default)==null?void 0:t.call(r)]),ce()])}}}),vt=Oe(_t),[yt,bt]=G(`tabs`),xt=O({name:yt,props:{count:se(Number),inited:Boolean,animated:Boolean,duration:se(H),swipeable:Boolean,lazyRender:Boolean,currentIndex:se(Number)},emits:[`change`],setup(e,{emit:t,slots:n}){let r=L(),i=e=>t(`change`,e),a=()=>{var t;let a=(t=n.default)==null?void 0:t.call(n);return e.animated||e.swipeable?R(vt,{ref:r,loop:!1,class:bt(`track`),duration:e.duration*1e3,touchable:e.swipeable,lazyRender:e.lazyRender,showIndicators:!1,onChange:i},{default:()=>[a]}):a},o=t=>{let n=r.value;n&&n.state.active!==t&&n.swipeTo(t,{immediate:!e.inited})};return B(()=>e.currentIndex,o),de(()=>{o(e.currentIndex)}),_({swipeRef:r}),()=>R(`div`,{class:bt(`content`,{animated:e.animated||e.swipeable})},[a()])}}),[St,Ct]=G(`tabs`),wt={type:Ce(`line`),color:String,border:Boolean,sticky:Boolean,shrink:Boolean,active:Y(0),duration:Y(.3),animated:Boolean,ellipsis:W,swipeable:Boolean,scrollspy:Boolean,offsetTop:Y(0),background:String,lazyRender:W,showHeader:W,lineWidth:H,lineHeight:H,beforeChange:Function,swipeThreshold:Y(5),titleActiveColor:String,titleInactiveColor:String},Tt=Symbol(St),Et=O({name:St,props:wt,emits:[`change`,`scroll`,`rendered`,`clickTab`,`update:active`],setup(e,{emit:r,slots:a}){let o,c,l,u,d,f=L(),p=L(),m=L(),h=L(),g=y(),v=re(f),[b,x]=ot(),{children:S,linkChildren:ee}=E(Tt),C=ie({inited:!1,position:``,lineStyle:{},currentIndex:-1}),w=J(()=>S.length>+e.swipeThreshold||!e.ellipsis||e.shrink),te=J(()=>({borderColor:e.color,background:e.background})),T=(e,t)=>{var n;return(n=e.name)==null?t:n},O=J(()=>{let e=S[C.currentIndex];if(e)return T(e,C.currentIndex)}),j=J(()=>D(e.offsetTop)),M=J(()=>e.sticky?j.value+o:0),N=t=>{let n=p.value,r=b.value;if(!w.value||!n||!r||!r[C.currentIndex])return;let i=r[C.currentIndex].$el,a=i.offsetLeft-(n.offsetWidth-i.offsetWidth)/2;u&&u(),u=it(n,a,t?0:+e.duration)},P=()=>{let t=C.inited;A(()=>{let n=b.value;if(!n||!n[C.currentIndex]||e.type!==`line`||k(f.value))return;let r=n[C.currentIndex].$el,{lineWidth:i,lineHeight:a}=e,o=r.offsetLeft+r.offsetWidth/2,s={width:Be(i),backgroundColor:e.color,transform:`translateX(${o}px) translateX(-50%)`};if(t&&(s.transitionDuration=`${e.duration}s`),Ee(a)){let e=Be(a);s.height=e,s.borderRadius=e}C.lineStyle=s})},ae=e=>{let t=e=0&&e{let i=ae(t);if(!Ee(i))return;let a=S[i],o=T(a,i),s=C.currentIndex!==null;C.currentIndex!==i&&(C.currentIndex=i,n||N(),P()),o!==e.active&&(r(`update:active`,o),s&&r(`change`,o,a.title)),l&&!e.scrollspy&&ne(Math.ceil(Ve(f.value)-j.value))},I=(e,t)=>{let n=S.findIndex((t,n)=>T(t,n)===e);F(n===-1?0:n,t)},oe=(t=!1)=>{if(e.scrollspy){let n=S[C.currentIndex].$el;if(n&&v.value){let r=Ve(n,v.value)-M.value;c=!0,d&&d(),d=at(v.value,r,t?0:+e.duration,()=>{c=!1})}}},se=(t,i,a)=>{let{title:o,disabled:c}=S[i],l=T(S[i],i);c||(s(e.beforeChange,{args:[l],done:()=>{F(i),oe()}}),n(t)),r(`clickTab`,{name:l,title:o,event:a,disabled:c})},ce=e=>{l=e.isFixed,r(`scroll`,e)},le=e=>{A(()=>{I(e),oe(!0)})},ue=()=>{for(let e=0;eM.value)return e===0?0:e-1}return S.length-1},de=()=>{if(e.scrollspy&&!c){let e=ue();F(e)}},pe=()=>{if(e.type===`line`&&S.length)return R(`div`,{class:Ct(`line`),style:C.lineStyle},null)},he=()=>{var t,n,r;let{type:o,border:s,sticky:c}=e,l=[R(`div`,{ref:c?void 0:m,class:[Ct(`wrap`),{[i]:o===`line`&&s}]},[R(`div`,{ref:p,role:`tablist`,class:Ct(`nav`,[o,{shrink:e.shrink,complete:w.value}]),style:te.value,"aria-orientation":`horizontal`},[(t=a[`nav-left`])==null?void 0:t.call(a),S.map(e=>e.renderTitle(se)),pe(),(n=a[`nav-right`])==null?void 0:n.call(a)])]),(r=a[`nav-bottom`])==null?void 0:r.call(a)];return c?R(`div`,{ref:m},[l]):l},ge=()=>{P(),A(()=>{var e,t;N(!0),(t=(e=h.value)==null?void 0:e.swipeRef.value)==null||t.resize()})};return B(()=>[e.color,e.duration,e.lineWidth,e.lineHeight],P),B(fe,ge),B(()=>e.active,e=>{e!==O.value&&I(e)}),B(()=>S.length,()=>{C.inited&&(I(e.active),P(),A(()=>{N(!0)}))}),_({resize:ge,scrollTo:le}),me(P),t(P),_e(()=>{I(e.active,!0),A(()=>{C.inited=!0,m.value&&(o=V(m.value).height),N(!0)})}),st(f,P),xe(`scroll`,de,{target:v,passive:!0}),ee({id:g,props:e,setLine:P,scrollable:w,onRendered:(e,t)=>r(`rendered`,e,t),currentName:O,setTitleRefs:x,scrollIntoView:N}),()=>R(`div`,{ref:f,class:Ct([e.type])},[e.showHeader?e.sticky?R(ft,{container:f.value,offsetTop:j.value,onScroll:ce},{default:()=>[he()]}):he():null,R(xt,{ref:h,count:S.length,inited:C.inited,animated:e.animated,duration:e.duration,swipeable:e.swipeable,lazyRender:e.lazyRender,currentIndex:C.currentIndex,onChange:F},{default:()=>{var e;return[(e=a.default)==null?void 0:e.call(a)]}})])}}),Dt=Symbol(),Ot=Symbol(),kt=()=>ye(Ot,null),At=e=>{let t=kt();oe(Dt,e),oe(Ot,J(()=>(t==null||t.value)&&e.value))},[jt,Mt]=G(`tab`),Nt=O({name:jt,props:{id:String,dot:Boolean,type:String,color:String,title:String,badge:H,shrink:Boolean,isActive:Boolean,disabled:Boolean,controls:String,scrollable:Boolean,activeColor:String,inactiveColor:String,showZeroBadge:W},setup(e,{slots:t}){let n=J(()=>{let t={},{type:n,color:r,disabled:i,isActive:a,activeColor:o,inactiveColor:s}=e;r&&n===`card`&&(t.borderColor=r,i||(a?t.backgroundColor=r:t.color=r));let c=a?o:s;return c&&(t.color=c),t}),r=()=>{let n=R(`span`,{class:Mt(`text`,{ellipsis:!e.scrollable})},[t.title?t.title():e.title]);return e.dot||Ee(e.badge)&&e.badge!==``?R(a,{dot:e.dot,content:e.badge,showZero:e.showZeroBadge},{default:()=>[n]}):n};return()=>R(`div`,{id:e.id,role:`tab`,class:[Mt([e.type,{grow:e.scrollable&&!e.shrink,shrink:e.shrink,active:e.isActive,disabled:e.disabled}])],style:n.value,tabindex:e.disabled?void 0:e.isActive?0:-1,"aria-selected":e.isActive,"aria-disabled":e.disabled||void 0,"aria-controls":e.controls,"data-allow-mismatch":`attribute`},[r()])}}),[Pt,Ft]=G(`swipe-item`),It=O({name:Pt,setup(e,{slots:t}){let n,r=ie({offset:0,inited:!1,mounted:!1}),{parent:i,index:a}=be(gt);if(!i)return;let o=J(()=>{let e={},{vertical:t}=i.props;return i.size.value&&(e[t?`height`:`width`]=`${i.size.value}px`),r.offset&&(e.transform=`translate${t?`Y`:`X`}(${r.offset}px)`),e}),s=J(()=>{let{loop:e,lazyRender:t}=i.props;if(!t||n)return!0;if(!r.mounted)return!1;let o=i.activeIndicator.value,s=i.count.value-1,c=o===0&&e?s:o-1,l=o===s&&e?0:o+1;return n=a.value===o||a.value===c||a.value===l,n});return de(()=>{A(()=>{r.mounted=!0})}),_({setOffset:e=>{r.offset=e}}),()=>{var e;return R(`div`,{class:Ft(),style:o.value},[s.value?(e=t.default)==null?void 0:e.call(t):null])}}}),Lt=Oe(It),[Rt,zt]=G(`tab`),Bt=ge({},e,{dot:Boolean,name:H,badge:H,title:String,disabled:Boolean,titleClass:null,titleStyle:[String,Object],showZeroBadge:W}),Vt=O({name:Rt,props:Bt,setup(e,{slots:t}){let n=y(),r=L(!1),i=He(),{parent:a,index:o}=be(Tt);if(!a)return;let s=()=>{var t;return(t=e.name)==null?o.value:t},c=()=>{r.value=!0,a.props.lazyRender&&A(()=>{a.onRendered(s(),e.title)})},l=J(()=>{let e=s()===a.currentName.value;return e&&!r.value&&c(),e}),u=L(``),d=L(``);F(()=>{let{titleClass:t,titleStyle:n}=e;u.value=t?rt(t):``,d.value=n&&typeof n!=`string`?nt(Ze(n)):n});let f=r=>R(Nt,M({key:n,id:`${a.id}-${o.value}`,ref:a.setTitleRefs(o.value),style:d.value,class:u.value,isActive:l.value,controls:n,scrollable:a.scrollable.value,activeColor:a.props.titleActiveColor,inactiveColor:a.props.titleInactiveColor,onClick:e=>r(i.proxy,o.value,e)},Fe(a.props,[`type`,`color`,`shrink`]),Fe(e,[`dot`,`badge`,`title`,`disabled`,`showZeroBadge`])),{title:t.title}),p=L(!l.value);return B(l,e=>{e?p.value=!1:I(()=>{p.value=!0})}),B(()=>e.title,()=>{a.setLine(),a.scrollIntoView()}),At(l),_({id:n,renderTitle:f}),()=>{var e;let i=`${a.id}-${o.value}`,{animated:s,swipeable:c,scrollspy:u,lazyRender:d}=a.props;if(!t.default&&!s)return;let f=u||l.value;if(s||c)return R(Lt,{id:n,role:`tabpanel`,class:zt(`panel-wrapper`,{inactive:p.value}),tabindex:l.value?0:-1,"aria-hidden":!l.value,"aria-labelledby":i,"data-allow-mismatch":`attribute`},{default:()=>{var e;return[R(`div`,{class:zt(`panel`)},[(e=t.default)==null?void 0:e.call(t)])]}});let m=r.value||u||!d?(e=t.default)==null?void 0:e.call(t):null;return Se(R(`div`,{id:n,role:`tabpanel`,class:zt(`panel`),tabindex:f?0:-1,"aria-labelledby":i,"data-allow-mismatch":`attribute`},[m]),[[ze,f]])}}}),Ht=Oe(Vt),Ut=Oe(Et),[Wt,Gt,Kt]=G(`list`),qt={error:Boolean,offset:Y(300),loading:Boolean,disabled:Boolean,finished:Boolean,scroller:Object,errorText:String,direction:Ce(`down`),loadingText:{type:String,default:``},finishedText:String,immediateCheck:W},Jt=O({name:Wt,props:qt,emits:[`load`,`update:error`,`update:loading`],setup(e,{emit:t,slots:n}){let r=L(e.loading),i=L(),a=L(),o=kt(),s=re(i),c=J(()=>e.scroller||s.value),l=()=>{A(()=>{if(r.value||e.finished||e.disabled||e.error||(o==null?void 0:o.value)===!1)return;let{direction:n}=e,s=+e.offset,l=V(c);if(!l.height||k(i))return;let u=!1,d=V(a);u=n===`up`?l.top-d.top<=s:d.bottom-l.bottom<=s,u&&(r.value=!0,t(`update:loading`,!0),t(`load`))})},u=()=>{if(e.finished){let t=n.finished?n.finished():e.finishedText;if(t)return R(`div`,{class:Gt(`finished-text`)},[t])}},d=()=>{t(`update:error`,!1),l()},f=()=>{if(e.error){let t=n.error?n.error():e.errorText;if(t)return R(`div`,{role:`button`,class:Gt(`error-text`),tabindex:0,onClick:d},[t])}},p=()=>{if(r.value&&!e.finished&&!e.disabled)return R(`div`,{class:Gt(`loading`)},[n.loading?n.loading():e.loadingText!=null&&R(g,{class:Gt(`loading-icon`)},{default:()=>[e.loadingText||Kt(`loading`)]})])};return B(()=>[e.loading,e.finished,e.error],l),o&&B(o,e=>{e&&l()}),he(()=>{r.value=e.loading}),de(()=>{e.immediateCheck&&l()}),_({check:l}),xe(`scroll`,l,{target:c,passive:!0}),()=>{var t;let o=(t=n.default)==null?void 0:t.call(n),s=R(`div`,{ref:a,class:Gt(`placeholder`)},null);return R(`div`,{ref:i,role:`feed`,class:Gt(),"aria-busy":r.value},[e.direction===`down`?o:s,p(),u(),f(),e.direction===`up`?o:s])}}}),Yt=Oe(Jt),Xt={class:`detail`},Zt={class:`detail-head`},Qt={class:`avatar`,"aria-hidden":`true`},$t={class:`detail-title`},en={class:`app-muted`},tn={key:0,class:`badge`},nn={class:`app-mono`},rn={key:0,class:`state state--blocked`},an={key:1,class:`note`},on={key:1,class:`state`},sn={key:1,class:`note`},cn=o(O({__name:`ConversationDetail`,props:{conversation:{},accountId:{},canAdd:{type:Boolean},canRemove:{type:Boolean},addCode:{},removeCode:{}},emits:[`updated`],setup(e,{emit:t}){let n=e,i=t,a=L(``),o=L(!1);B(()=>`${n.conversation.account}|${n.conversation.conversation}`,()=>{a.value=``});let s=J(()=>n.conversation.blacklist),l=J(()=>{var e;let t=(e=s.value)==null?void 0:e.source;return t===`mobile`?`手机控制台`:t===`desktop`?`客服电脑`:t||`—`});async function d(){if(!o.value){try{await f({title:`加入忽略?`,message:`加入后,系统不再向「${n.conversation.name}」自动发送任何消息,只能由客服手动处理。`,confirmButtonText:`加入忽略`,confirmButtonColor:`var(--app-danger)`})}catch{return}o.value=!0;try{let{entry:e}=await Me.block(n.conversation,a.value.trim(),n.accountId);i(`updated`,{...n.conversation,blocked:!0,blacklist:e}),x({message:`已加入忽略,电脑同步后立即生效`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}async function p(){if(!o.value){try{await f({title:`移出忽略?`,message:`移出后,系统会恢复向「${n.conversation.name}」自动回复。`,confirmButtonText:`移出`})}catch{return}o.value=!0;try{await Me.unblock(n.conversation.account,n.conversation.conversation,n.accountId),i(`updated`,{...n.conversation,blocked:!1,blacklist:null}),x({message:`已移出忽略`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}return(t,n)=>(P(),$(`section`,Xt,[q(`header`,Zt,[q(`span`,Qt,U(X(u)(e.conversation.name)),1),q(`div`,$t,[q(`h2`,null,U(e.conversation.name),1),q(`p`,en,U(e.conversation.type_label),1)]),e.conversation.blocked?(P(),$(`span`,tn,[R(X(r),{name:`shield-o`}),n[1]||(n[1]=Q(` 忽略`,-1))])):Z(``,!0)]),R(X(v),{inset:``,class:`group`},{default:z(()=>[R(X(h),{title:`企业微信账号`,value:e.conversation.account_name||`—`,label:e.conversation.account},null,8,[`value`,`label`]),R(X(h),{title:`最近消息`,value:e.conversation.last_message_at?X(c)(e.conversation.last_message_at):`—`},null,8,[`value`]),R(X(h),{title:`会话标识`},{label:z(()=>[q(`span`,nn,U(e.conversation.conversation),1)]),_:1})]),_:1}),e.conversation.blocked&&s.value?(P(),$(`div`,rn,[q(`h3`,null,[R(X(r),{name:`shield-o`}),n[2]||(n[2]=Q(` 已加入忽略`,-1))]),n[8]||(n[8]=q(`p`,null,`系统不会向这个会话自动发送消息;电脑离线时,下次联网自动同步。`,-1)),q(`dl`,null,[q(`div`,null,[n[3]||(n[3]=q(`dt`,null,`原因`,-1)),q(`dd`,null,U(s.value.reason||`未填写`),1)]),q(`div`,null,[n[4]||(n[4]=q(`dt`,null,`操作人`,-1)),q(`dd`,null,U(s.value.created_by||`—`),1)]),q(`div`,null,[n[5]||(n[5]=q(`dt`,null,`时间`,-1)),q(`dd`,null,U(X(c)(s.value.created_at)),1)]),q(`div`,null,[n[6]||(n[6]=q(`dt`,null,`来源`,-1)),q(`dd`,null,U(l.value),1)])]),e.canRemove?(P(),K(X(m),{key:0,block:``,round:``,plain:``,type:`primary`,icon:`revoke`,loading:o.value,onClick:p},{default:z(()=>[...n[7]||(n[7]=[Q(` 移出忽略 `,-1)])]),_:1},8,[`loading`])):(P(),$(`p`,an,`移出忽略需要主管权限(`+U(e.removeCode)+`),请联系主管处理。`,1))])):(P(),$(`div`,on,[q(`h3`,null,[R(X(r),{name:`info-o`}),n[9]||(n[9]=Q(` 正常接收自动回复`,-1))]),n[11]||(n[11]=q(`p`,null,`客户投诉、需要人工跟进或不适合机器人回复的会话,可以加入忽略。`,-1)),e.canAdd?(P(),$(De,{key:0},[R(X(v),{inset:``,class:`reason`},{default:z(()=>[R(X(b),{modelValue:a.value,"onUpdate:modelValue":n[0]||(n[0]=e=>a.value=e),label:`原因`,placeholder:`选填,例如:客户要求人工服务`,maxlength:`200`,"show-word-limit":``,rows:`2`,autosize:``,type:`textarea`},null,8,[`modelValue`])]),_:1}),R(X(m),{block:``,round:``,type:`danger`,icon:`shield-o`,loading:o.value,onClick:d},{default:z(()=>[...n[10]||(n[10]=[Q(` 加入忽略 `,-1)])]),_:1},8,[`loading`])],64)):(P(),$(`p`,sn,`加入忽略需要 ZYT 权限(`+U(e.addCode)+`),请联系管理员开通。`,1))]))]))}}),[[`__scopeId`,`data-v-bdbcc19d`]]),ln={class:`body`},un={class:`line`},dn={class:`name`},fn={class:`time`},pn={class:`line sub`},mn={class:`meta`},hn={key:0,class:`blocked`},gn=o(O({__name:`ConversationRow`,props:{item:{},active:{type:Boolean}},emits:[`select`],setup(e){let t=e,n=J(()=>t.item.type===`group`?`group`:t.item.type===`direct_wechat`?`wechat`:`wecom`);return(t,r)=>(P(),$(`button`,{type:`button`,class:ce([`row`,{active:e.active}]),onClick:r[0]||(r[0]=n=>t.$emit(`select`,e.item))},[q(`span`,{class:ce([`avatar`,`avatar--${n.value}`]),"aria-hidden":`true`},U(X(u)(e.item.name)),3),q(`span`,ln,[q(`span`,un,[q(`strong`,dn,U(e.item.name),1),q(`span`,fn,U(e.item.last_message_at?X(c)(e.item.last_message_at):``),1)]),q(`span`,pn,[q(`span`,mn,[Q(U(e.item.type_label),1),e.item.account_name?(P(),$(De,{key:0},[Q(` · `+U(e.item.account_name),1)],64)):Z(``,!0)]),e.item.blocked?(P(),$(`span`,hn,`已拉黑`)):Z(``,!0)])])],2))}}),[[`__scopeId`,`data-v-645c253b`]]),_n={class:`page`},vn={class:`list-pane`},yn={class:`toolbar`},bn={key:0,class:`detail-pane app-card`},xn=o(O({name:`ConversationsView`,__name:`ConversationsView`,setup(e){let t=Ie(),n=L(``),i=L(`all`),a=L([]),o=L(``),s=L(!1),c=L(!1),l=L(!1),u=L(``),f=L(!1),h=L(0),g=L(null),_=L(!1),v=L(!1),y=0,b=null,x,te=J(()=>{var e;return(e=Ne.me)==null?void 0:e.permission_codes}),ne=J(()=>Pe.value===void 0?`会话`:`${Le.value} 的会话`),T=e=>`${e.account}|${e.conversation}`;function E(){y+=1,b==null||b.abort(),b=null,a.value=[],o.value=``,s.value=!1,l.value=!1,u.value=``,c.value=!1,D()}async function D(){if(s.value)return;let e=y;b=new AbortController,c.value=!0;try{let r=await Me.conversations({accountId:Pe.value,q:n.value.trim(),scope:i.value,cursor:o.value,limit:30,signal:b.signal});if(e!==y)return;let c=new Set(a.value.map(T));a.value=a.value.concat(r.items.filter(e=>!c.has(T(e)))),o.value=r.next_cursor,s.value=!r.has_more,h.value=r.blocked_total,l.value=!1,t.value&&!g.value&&a.value.length&&(g.value=a.value[0])}catch(t){if(e!==y||t instanceof DOMException&&t.name===`AbortError`)return;l.value=!0,u.value=t instanceof Te?t.message:`加载失败`}finally{e===y&&(c.value=!1,f.value=!1)}}function O(){f.value=!0,E()}function k(e){g.value=e,t.value||(_.value=!0)}function A(e){var t;let n=a.value.findIndex(t=>T(t)===T(e)),r=n>=0?a.value[n].blocked:(t=g.value)==null?void 0:t.blocked;n>=0&&(i.value===`blocked`&&!e.blocked?a.value.splice(n,1):a.value.splice(n,1,e)),r!==e.blocked&&(h.value+=e.blocked?1:-1),g.value=e}return B(n,()=>{window.clearTimeout(x),x=window.setTimeout(E,350)}),B(i,E),B(Pe,()=>{g.value=null,_.value=!1,E()}),E(),(e,o)=>{var y,b,x,E,j,M,N,re;return P(),$(`div`,_n,[R(X(d),{title:ne.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},we({_:2},[(y=X(Re))!=null&&y.control_all?{name:`right`,fn:z(()=>[q(`button`,{type:`button`,class:`nav-action`,onClick:o[0]||(o[0]=e=>v.value=!0)},[R(X(r),{name:`friends-o`}),o[9]||(o[9]=Q(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),q(`div`,{class:ce(X(t)?`split`:`single`)},[q(`section`,vn,[q(`div`,yn,[R(X(ee),{modelValue:n.value,"onUpdate:modelValue":o[1]||(o[1]=e=>n.value=e),shape:`round`,placeholder:`搜索会话名称`,clearable:!0},null,8,[`modelValue`]),R(X(Ut),{active:i.value,"onUpdate:active":o[2]||(o[2]=e=>i.value=e),shrink:``,border:!1,class:`tabs`},{default:z(()=>[R(X(Ht),{name:`all`,title:`全部会话`}),R(X(Ht),{name:`blocked`,title:h.value?`忽略 ${h.value}`:`忽略`},null,8,[`title`])]),_:1},8,[`active`])]),R(X(C),{modelValue:f.value,"onUpdate:modelValue":o[6]||(o[6]=e=>f.value=e),class:`list-body`,onRefresh:O},{default:z(()=>[R(X(Yt),{loading:c.value,"onUpdate:loading":o[3]||(o[3]=e=>c.value=e),error:l.value,"onUpdate:error":o[4]||(o[4]=e=>l.value=e),finished:s.value,"error-text":`${u.value},点击重试`,"finished-text":a.value.length?`没有更多了`:``,"immediate-check":!1,onLoad:D},{default:z(()=>[(P(!0),$(De,null,le(a.value,e=>(P(),K(gn,{key:T(e),item:e,active:X(t)&&g.value!==null&&T(g.value)===T(e),onSelect:k},null,8,[`item`,`active`]))),128))]),_:1},8,[`loading`,`error`,`finished`,`error-text`,`finished-text`]),s.value&&!a.value.length&&!l.value?(P(),K(X(S),{key:0,image:n.value?`search`:`default`,description:n.value?`没有找到匹配的会话`:i.value===`blocked`?`忽略是空的`:`还没有归档的会话:电脑开启聊天备份后会自动出现`},{default:z(()=>[n.value?(P(),K(X(m),{key:0,round:``,size:`small`,onClick:o[5]||(o[5]=e=>n.value=``)},{default:z(()=>[...o[10]||(o[10]=[Q(`清除搜索`,-1)])]),_:1})):Z(``,!0)]),_:1},8,[`image`,`description`])):Z(``,!0)]),_:1},8,[`modelValue`])]),X(t)?(P(),$(`section`,bn,[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((b=X(Re))!=null&&b.blacklist_add),"can-remove":!!((x=X(Re))!=null&&x.blacklist_remove),"add-code":(E=(j=te.value)==null?void 0:j.blacklist_add)==null?``:E,"remove-code":(M=(N=te.value)==null?void 0:N.blacklist_remove)==null?``:M,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):(P(),K(X(S),{key:1,image:`default`,description:`选择左侧的会话查看详情`}))])):Z(``,!0)],2),X(t)?Z(``,!0):(P(),K(X(p),{key:0,show:_.value,"onUpdate:show":o[7]||(o[7]=e=>_.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`sheet`},{default:z(()=>{var e,t,n,r,i,a;return[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((e=X(Re))!=null&&e.blacklist_add),"can-remove":!!((t=X(Re))!=null&&t.blacklist_remove),"add-code":(n=(r=te.value)==null?void 0:r.blacklist_add)==null?``:n,"remove-code":(i=(a=te.value)==null?void 0:a.blacklist_remove)==null?``:i,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):Z(``,!0)]}),_:1},8,[`show`])),(re=X(Re))!=null&&re.control_all?(P(),K(w,{key:1,show:v.value,"onUpdate:show":o[8]||(o[8]=e=>v.value=e)},null,8,[`show`])):Z(``,!0)])}}}),[[`__scopeId`,`data-v-4433756d`]]);export{xn as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css new file mode 100644 index 000000000..617ba43f7 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css @@ -0,0 +1 @@ +.panel[data-v-bcd7f731]{padding:16px}.panel-head[data-v-bcd7f731]{align-items:flex-start;gap:12px;display:flex}.panel-icon[data-v-bcd7f731]{width:44px;height:44px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:12px;flex:none;place-items:center;font-size:22px;display:grid}.panel-title[data-v-bcd7f731]{flex:1;min-width:0}.panel-title h2[data-v-bcd7f731]{text-overflow:ellipsis;white-space:nowrap;margin:2px 0 4px;font-size:17px;font-weight:600;overflow:hidden}.panel-title p[data-v-bcd7f731]{margin:0;font-size:13px;line-height:1.45}.pill[data-v-bcd7f731]{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.pill .dot[data-v-bcd7f731]{background:currentColor;border-radius:50%;width:7px;height:7px}.pill--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.pill--success .dot[data-v-bcd7f731]{box-shadow:0 0 0 3px var(--app-success-soft);animation:2.2s ease-in-out infinite breathe-bcd7f731}.pill--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.pill--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.pill--muted[data-v-bcd7f731],.pill--primary[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}@keyframes breathe-bcd7f731{50%{opacity:.45}}.stats[data-v-bcd7f731]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0 0;display:grid}.stat[data-v-bcd7f731]{background:var(--app-surface-2);border:1px solid var(--app-border);border-radius:12px;padding:10px 12px}.stat dt[data-v-bcd7f731]{color:var(--app-muted);font-size:12px}.stat dd[data-v-bcd7f731]{margin:4px 0 0;font-size:15px;font-weight:600}.meta[data-v-bcd7f731]{margin:12px 0 0;font-size:12px}.progress[data-v-bcd7f731]{border-radius:12px;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45;display:flex}.progress--primary[data-v-bcd7f731]{color:var(--app-primary);background:var(--app-primary-soft)}.progress--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.progress--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.progress--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.progress--muted[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}.actions[data-v-bcd7f731]{margin-top:16px}.blocked[data-v-bcd7f731]{margin:10px 4px 0;font-size:13px;line-height:1.5}.page[data-v-8ead1f63]{min-height:100%}.page-body[data-v-8ead1f63]{min-height:calc(100vh - 96px);min-height:calc(100dvh - 96px)}.content[data-v-8ead1f63]{max-width:720px;margin:0 auto;padding:12px 16px 24px}.stack[data-v-8ead1f63]{gap:12px;display:grid}.skeleton-card[data-v-8ead1f63]{padding:16px 0}.empty-hint[data-v-8ead1f63]{max-width:280px;color:var(--app-muted);text-align:center;margin:0 auto;font-size:13px;line-height:1.6}.nav-action[data-v-8ead1f63]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.stale[data-v-8ead1f63]{margin:0 4px;font-size:12px}.split[data-v-8ead1f63]{grid-template-columns:minmax(240px,320px) minmax(0,1fr);align-items:start;gap:16px;max-width:1080px;margin:0 auto;padding:16px 24px 24px;display:grid}.split-list[data-v-8ead1f63]{gap:10px;display:grid}.device-row[data-v-8ead1f63]{text-align:left;width:100%;color:inherit;cursor:pointer;align-items:center;gap:12px;padding:12px 14px;display:flex}.device-row.active[data-v-8ead1f63]{border-color:var(--app-primary);box-shadow:0 0 0 2px var(--app-primary-soft)}.device-row-icon[data-v-8ead1f63]{color:var(--app-primary);font-size:20px}.device-row-text[data-v-8ead1f63]{gap:2px;min-width:0;display:grid}.device-row-text strong[data-v-8ead1f63]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;overflow:hidden}.device-row-text small[data-v-8ead1f63]{font-size:12px} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js new file mode 100644 index 000000000..fd0676b92 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n,o as r,r as i,s as a,t as o}from"./format-Dt9-L1ne.js";import{a as s,o as c}from"./cell-group-B1mihubH.js";import{a as l,i as u,r as d,t as f}from"./AccountSwitcher-DmKdvr8J.js";import{Ct as p,Et as m,Ft as h,Jt as g,Kt as _,Lt as v,St as y,Ut as b,Vt as x,X as S,Y as C,Yt as w,Z as T,_ as E,_t as D,bt as O,c as k,f as A,ft as j,g as M,gt as N,ht as P,kt as F,l as I,n as L,o as R,p as z,q as B,qt as V,s as ee,t as H,v as U,vt as W,x as te,xt as G,yt as K}from"./index-C_M75pH8.js";var[ne,re]=E(`skeleton-title`),ie=p({name:ne,props:{round:Boolean,titleWidth:S},setup(e){return()=>y(`h3`,{class:re([{round:e.round}]),style:{width:U(e.titleWidth)}},null)}}),q=M(ie),[J,Y]=E(`skeleton-avatar`),ae={avatarSize:S,avatarShape:C(`round`)},oe=p({name:J,props:ae,setup(e){return()=>y(`div`,{class:Y([e.avatarShape]),style:te(e.avatarSize)},null)}}),se=M(oe),X=`100%`,ce={round:Boolean,rowWidth:{type:S,default:X}},[le,ue]=E(`skeleton-paragraph`),de=p({name:le,props:ce,setup(e){return()=>y(`div`,{class:ue([{round:e.round}]),style:{width:e.rowWidth}},null)}}),fe=M(de),[pe,Z]=E(`skeleton`),me=`60%`,he={row:B(0),round:Boolean,title:Boolean,titleWidth:S,avatar:Boolean,avatarSize:S,avatarShape:C(`round`),loading:T,animate:T,rowWidth:{type:[Number,String,Array],default:X}},ge=p({name:pe,inheritAttrs:!1,props:he,setup(e,{slots:t,attrs:n}){let r=()=>{if(e.avatar)return y(se,{avatarShape:e.avatarShape,avatarSize:e.avatarSize},null)},i=()=>{if(e.title)return y(q,{round:e.round,titleWidth:e.titleWidth},null)},a=t=>{let{rowWidth:n}=e;return n===`100%`&&t===e.row-1?me:Array.isArray(n)?n[t]:n},o=()=>Array(+e.row).fill(``).map((t,n)=>y(fe,{key:n,round:e.round,rowWidth:U(a(n))},null)),s=()=>t.template?t.template():y(j,null,[r(),y(`div`,{class:Z(`content`)},[i(),o()])]);return()=>{var r;return e.loading?y(`div`,m({class:Z({animate:e.animate,round:e.round})},n),[s()]):(r=t.default)==null?void 0:r.call(t)}}}),_e=M(ge),ve={class:`panel-head`},ye={class:`panel-icon`,"aria-hidden":`true`},be={class:`panel-title`},xe={class:`app-muted`},Se={class:`stats`},Ce={class:`stat`},we={class:`stat`},Te={class:`stat`},Ee={class:`stat`},De={class:`meta app-muted`},Oe={key:0},ke={key:1},Ae={class:`actions`},je={key:0,class:`blocked app-muted`},Q=t(p({__name:`DevicePanel`,props:{device:{},accountId:{},canControl:{type:Boolean},controlCode:{}},emits:[`changed`],setup(t,{emit:r}){let u=t,d=r,f=_(!1),p=_(null),m,v=0,S=P(()=>n(u.device)),C=P(()=>u.device.status),T=P(()=>{var e;return!!((e=C.value)!=null&&e.listening)}),E=P(()=>T.value?`stop_listening`:`start_listening`),O=P(()=>p.value?o(p.value):null),A=P(()=>{var e;return f.value||!!((e=O.value)!=null&&e.busy)}),j=P(()=>u.device.online?u.device.remote_capable?u.canControl?``:`你的 ZYT 账号没有远程启停权限(${u.controlCode}),请联系管理员开通。`:`电脑上的客服软件版本过低,升级到最新版后才能远程控制。`:`电脑离线:客服软件没有运行,无法远程拉起。请到电脑上打开软件。`);function M(){m!==void 0&&window.clearTimeout(m),m=void 0}async function L(){let e=p.value;if(e&&!e.final){try{let{command:t}=await I.command(e.id);if(p.value=t,t.final){d(`changed`);let e=o(t).text;l({message:e,type:t.status===`succeeded`?`success`:`fail`});return}}catch(e){if(e instanceof k&&e.status===404)return}Date.now()u.device.last_command,e=>{if(!e)return;if(p.value&&p.value.id===e.id){!p.value.final&&e.final&&(p.value=e);return}let t=e.expires_at*1e3>Date.now();!e.final&&t?R(e):!p.value&&e.final&&t&&(p.value=e)},{immediate:!0}),F(M);async function z(){if(A.value||j.value)return;let e=E.value===`stop_listening`;try{await a({title:e?`停止自动回复?`:`启动自动回复?`,message:e?`「${u.device.device_name||`这台电脑`}」将停止监听,新消息不再自动回复。`:`「${u.device.device_name||`这台电脑`}」将开始监听,并按电脑上的设置自动回复新消息。`,confirmButtonText:e?`停止`:`启动`,confirmButtonColor:e?`var(--app-danger)`:`var(--app-primary)`})}catch{return}f.value=!0;try{let{command:e}=await I.sendCommand(u.device.device_id,E.value,u.accountId);R(e)}catch(e){l({message:e instanceof k?e.message:`指令发送失败,请重试`,type:`fail`}),d(`changed`)}finally{f.value=!1}}return(n,r)=>{var a,o,l,u,d;return h(),K(`article`,{class:g([`panel app-card`,`panel--${S.value.tone}`])},[N(`header`,ve,[N(`div`,ye,[y(V(e),{name:`desktop-o`})]),N(`div`,be,[N(`h2`,null,w(t.device.device_name||`未命名电脑`),1),N(`p`,xe,w(S.value.hint),1)]),N(`span`,{class:g([`pill`,`pill--${S.value.tone}`])},[r[0]||(r[0]=N(`span`,{class:`dot`,"aria-hidden":`true`},null,-1)),G(` `+w(S.value.label),1)],2)]),N(`dl`,Se,[N(`div`,Ce,[r[1]||(r[1]=N(`dt`,null,`自动回复`,-1)),N(`dd`,{class:g(T.value?`tone-success`:`tone-muted`)},w(C.value?T.value?`监听中`:`已停止`:`—`),3)]),N(`div`,we,[r[2]||(r[2]=N(`dt`,null,`企业微信`,-1)),N(`dd`,{class:g((a=C.value)!=null&&a.wecom_connected?`tone-success`:`tone-muted`)},w(C.value?C.value.wecom_connected?`已连接`:`未连接`:`—`),3)]),N(`div`,Te,[r[3]||(r[3]=N(`dt`,null,`回复通道`,-1)),N(`dd`,null,w(((o=C.value)==null?void 0:o.transport_label)||`—`),1)]),N(`div`,Ee,[r[4]||(r[4]=N(`dt`,null,`待处理`,-1)),N(`dd`,{class:g((((l=C.value)==null?void 0:l.pending_count)||0)>0?`tone-warning`:``)},w(C.value?`${C.value.pending_count} 条`:`—`),3)])]),N(`p`,De,[N(`span`,null,`版本 `+w(t.device.app_version||`未知`),1),t.device.online&&((u=C.value)==null?void 0:u.reported_seconds)!==null&&((d=C.value)==null?void 0:d.reported_seconds)!==void 0?(h(),K(`span`,Oe,` · 状态 `+w(V(i)(C.value.reported_seconds))+`前上报 `,1)):!t.device.online&&t.device.last_seen_seconds!==null?(h(),K(`span`,ke,` · `+w(V(i)(t.device.last_seen_seconds))+`前在线 `,1)):W(``,!0)]),O.value?(h(),K(`div`,{key:0,class:g([`progress`,`progress--${O.value.tone}`]),role:`status`,"aria-live":`polite`},[O.value.busy?(h(),D(V(c),{key:0,size:`16`})):(h(),D(V(e),{key:1,name:O.value.tone===`success`?`passed`:`info-o`},null,8,[`name`])),N(`span`,null,w(O.value.text),1)],2)):W(``,!0),N(`div`,Ae,[y(V(s),{block:``,round:``,type:T.value?`danger`:`primary`,plain:T.value,icon:T.value?`pause-circle-o`:`play-circle-o`,disabled:!!j.value,loading:A.value,"loading-text":f.value?`正在下发…`:`等待电脑执行…`,onClick:z},{default:b(()=>[G(w(T.value?`停止自动回复`:`启动自动回复`),1)]),_:1},8,[`type`,`plain`,`icon`,`disabled`,`loading`,`loading-text`]),j.value?(h(),K(`p`,je,w(j.value),1)):W(``,!0)])],2)}}}),[[`__scopeId`,`data-v-bcd7f731`]]),Me={class:`page`},Ne={key:0,class:`content`},Pe={class:`app-card skeleton-card`},Fe={key:1,class:`content`},Ie={key:2,class:`content`},Le={class:`empty-hint`},$={key:3,class:`split`},Re={class:`split-list`,"aria-label":`电脑列表`},ze=[`onClick`],Be={class:`device-row-text`},Ve={key:0,class:`stale app-muted`},He={class:`split-detail`},Ue={key:4,class:`content stack`},We={key:0,class:`stale app-muted`},Ge=t(p({name:`DevicesView`,__name:`DevicesView`,setup(t){let i=z(),a=_(null),o=_(!1),c=_(!1),l=_(``),p=_(``),m=_(!1),S=0,C=P(()=>{var e,t;return(e=(t=a.value)==null?void 0:t.items)==null?[]:e}),T=P(()=>{var e,t;return(e=(t=C.value.find(e=>e.device_id===p.value))==null?C.value[0]:t)==null?null:e}),E=P(()=>{var e;return!!((e=H.value)!=null&&e.control)}),M=P(()=>{var e,t;return(e=(t=L.me)==null?void 0:t.permission_codes.control)==null?`wecom_rpa.remote/control`:e}),F=P(()=>R.value===void 0?`我的电脑`:`${ee.value} 的电脑`);async function B(){let e=++S;a.value||(o.value=!0);try{let t=await I.desktops(R.value);if(e!==S)return;a.value=t,l.value=``}catch(t){if(e!==S)return;l.value=t instanceof k?t.message:`加载失败,请重试`}finally{e===S&&(o.value=!1,c.value=!1)}}async function U(){c.value=!0,await B()}return x(R,()=>{a.value=null,p.value=``,B()},{immediate:!0}),A(B,5e3),(t,_)=>{var x,S;return h(),K(`div`,Me,[y(V(r),{title:F.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},O({_:2},[(x=V(H))!=null&&x.control_all?{name:`right`,fn:b(()=>[N(`button`,{type:`button`,class:`nav-action`,onClick:_[0]||(_[0]=e=>m.value=!0)},[y(V(e),{name:`friends-o`}),_[3]||(_[3]=G(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),y(V(d),{modelValue:c.value,"onUpdate:modelValue":_[1]||(_[1]=e=>c.value=e),class:`page-body`,onRefresh:U},{default:b(()=>[o.value&&!a.value?(h(),K(`div`,Ne,[N(`div`,Pe,[y(V(_e),{title:``,row:4})])])):l.value&&!a.value?(h(),K(`div`,Fe,[y(V(u),{image:`network`,description:l.value},{default:b(()=>[y(V(s),{round:``,type:`primary`,size:`small`,onClick:B},{default:b(()=>[..._[4]||(_[4]=[G(`重新加载`,-1)])]),_:1})]),_:1},8,[`description`])])):C.value.length?V(i)?(h(),K(`div`,$,[N(`aside`,Re,[(h(!0),K(j,null,v(C.value,t=>{var r;return h(),K(`button`,{key:t.device_id,type:`button`,class:g([`device-row app-card`,{active:((r=T.value)==null?void 0:r.device_id)===t.device_id}]),onClick:e=>p.value=t.device_id},[y(V(e),{name:`desktop-o`,class:`device-row-icon`}),N(`span`,Be,[N(`strong`,null,w(t.device_name||`未命名电脑`),1),N(`small`,{class:g(`tone-${V(n)(t).tone}`)},w(V(n)(t).label),3)])],10,ze)}),128)),l.value?(h(),K(`p`,Ve,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0)]),N(`section`,He,[T.value?(h(),D(Q,{key:T.value.device_id,device:T.value,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`])):W(``,!0)])])):(h(),K(`div`,Ue,[l.value?(h(),K(`p`,We,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0),(h(!0),K(j,null,v(C.value,e=>(h(),D(Q,{key:e.device_id,device:e,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`]))),128))])):(h(),K(`div`,Ie,[y(V(u),{image:`default`,description:`还没有登录过企微客服助手的电脑`},{default:b(()=>[N(`p`,Le,` 在电脑上打开企微客服助手,用`+w(V(R)===void 0?`同一个`:`该客服的`)+` ZYT 账号登录后,这里会显示电脑状态。 `,1)]),_:1})]))]),_:1},8,[`modelValue`]),(S=V(H))!=null&&S.control_all?(h(),D(f,{key:0,show:m.value,"onUpdate:show":_[2]||(_[2]=e=>m.value=e)},null,8,[`show`])):W(``,!0)])}}}),[[`__scopeId`,`data-v-8ead1f63`]]);export{Ge as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js new file mode 100644 index 000000000..531d04c7d --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js @@ -0,0 +1 @@ +import{c as e,m as t,n,t as r}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{a as i,s as a,t as o}from"./cell-group-B1mihubH.js";import{t as s}from"./field-BLYigdmn.js";import{B as c,Ct as l,Ft as u,Gt as d,I as f,Kt as p,O as m,R as h,St as g,Ut as _,V as v,Vt as y,W as b,Wt as x,X as S,Yt as C,Z as w,_ as T,_t as E,c as D,d as O,et as k,g as A,gt as j,ht as M,l as N,q as P,qt as F,r as I,u as L,ut as R,vt as z,xt as B,yt as V,z as H}from"./index-C_M75pH8.js";var[U,W]=T(`form`),G=l({name:U,props:{colon:Boolean,disabled:Boolean,readonly:Boolean,required:[Boolean,String],showError:Boolean,labelWidth:S,labelAlign:String,inputAlign:String,scrollToError:Boolean,scrollToErrorPosition:String,validateFirst:Boolean,submitOnEnter:w,showErrorMessage:w,errorMessageAlign:String,validateTrigger:{type:[String,Array],default:`onBlur`}},emits:[`submit`,`failed`],setup(e,{emit:n,slots:r}){let{children:i,linkChildren:o}=c(t),s=e=>e?i.filter(t=>e.includes(t.name)):i,l=e=>new Promise((t,n)=>{let r=[];s(e).reduce((e,t)=>e.then(()=>{if(!r.length)return t.validate().then(e=>{e&&r.push(e)})}),Promise.resolve()).then(()=>{r.length?n(r):t()})}),u=e=>new Promise((t,n)=>{let r=s(e);Promise.all(r.map(e=>e.validate())).then(e=>{e=e.filter(Boolean),e.length?n(e):t()})}),d=e=>{let t=i.find(t=>t.name===e);return t?new Promise((e,n)=>{t.validate().then(t=>{t?n(t):e()})}):Promise.reject()},f=t=>typeof t==`string`?d(t):e.validateFirst?l(t):u(t),p=e=>{typeof e==`string`&&(e=[e]),s(e).forEach(e=>{e.resetValidation()})},h=()=>i.reduce((e,t)=>(e[t.name]=t.getValidationStatus(),e),{}),_=(e,t)=>{i.some(n=>n.name===e&&(n.$el.scrollIntoView(t),!0))},v=()=>i.reduce((e,t)=>(t.name!==void 0&&(e[t.name]=t.formValue.value),e),{}),y=()=>{let t=v();f().then(()=>n(`submit`,t)).catch(r=>{n(`failed`,{values:t,errors:r});let{scrollToError:i,scrollToErrorPosition:a}=e;i&&r[0].name&&_(r[0].name,a?{block:a}:void 0)})},b=e=>{m(e),y()};return o({props:e}),a({submit:y,validate:f,getValues:v,scrollToField:_,resetValidation:p,getValidationStatus:h}),()=>{var e;return g(`form`,{class:W(),onSubmit:b},[(e=r.default)==null?void 0:e.call(r)])}}}),K=A(G),[q,J]=T(`notice-bar`),Y={text:String,mode:String,color:String,delay:P(1),speed:P(60),leftIcon:String,wrapable:Boolean,background:String,scrollable:{type:Boolean,default:null}},X=l({name:q,props:Y,emits:[`close`,`replay`],setup(t,{emit:r,slots:i}){let o=0,s=0,c,l=p(),u=p(),m=d({show:!0,offset:0,duration:0}),_=()=>{if(i[`left-icon`])return i[`left-icon`]();if(t.leftIcon)return g(n,{class:J(`left-icon`),name:t.leftIcon},null)},S=()=>{if(t.mode===`closeable`)return`cross`;if(t.mode===`link`)return`arrow`},C=e=>{t.mode===`closeable`&&(m.show=!1,r(`close`,e))},w=()=>{if(i[`right-icon`])return i[`right-icon`]();let e=S();if(e)return g(n,{name:e,class:J(`right-icon`),onClick:C},null)},T=()=>{m.offset=o,m.duration=0,H(()=>{f(()=>{m.offset=-s,m.duration=(s+o)/+t.speed,r(`replay`)})})},E=()=>{let e=t.scrollable===!1&&!t.wrapable,n={transform:m.offset?`translateX(${m.offset}px)`:``,transitionDuration:`${m.duration}s`};return g(`div`,{ref:l,role:`marquee`,class:J(`wrap`)},[g(`div`,{ref:u,style:n,class:[J(`content`),{"van-ellipsis":e}],onTransitionend:T},[i.default?i.default():t.text])])},D=()=>{let{delay:e,speed:n,scrollable:r}=t,i=k(e)?e*1e3:0;o=0,s=0,m.offset=0,m.duration=0,clearTimeout(c),c=setTimeout(()=>{if(!l.value||!u.value||r===!1)return;let e=b(l).width,t=b(u).width;(r||t>e)&&f(()=>{o=e,s=t,m.offset=-s,m.duration=s/+n})},i)};return e(D),h(D),v(`pageshow`,D),a({reset:D}),y(()=>[t.text,t.scrollable],D),()=>{let{color:e,wrapable:n,background:r}=t;return x(g(`div`,{role:`alert`,class:J({wrapable:n}),style:{color:e,background:r}},[_(),E(),w()]),[[R,m.show]])}}}),Z=A(X),Q=`/m/assets/brand-mark-CPUQS9Am.png`,$={class:`login`},ee={class:`login-card`},te={class:`login-head`},ne=[`src`],re={key:0,class:`login-error`,role:`alert`},ie={class:`login-actions`},ae=r(l({__name:`LoginView`,setup(e){let t=L(),n=O(),r=p(``),a=p(``),c=p(!1),l=p(``),d=M(()=>t.query.expired===`1`);function f(){let e=navigator.userAgent,t=/iPad/.test(e)?`iPad`:/iPhone/.test(e)?`iPhone`:/HarmonyOS/i.test(e)?`鸿蒙`:/Android/.test(e)?`Android`:/Windows/.test(e)?`Windows`:/Mac OS/.test(e)?`Mac`:`浏览器`,n=/wxwork/i.test(e)?`企业微信`:/MicroMessenger/i.test(e)?`微信`:``;return n?`${t} · ${n}`:t}function m(){let e=t.query.redirect;return typeof e==`string`&&e.startsWith(`/`)&&!e.startsWith(`//`)&&!e.startsWith(`/login`)?e:`/desktops`}async function h(){if(!c.value){l.value=``,c.value=!0;try{let e=await N.login(r.value.trim(),a.value,f());a.value=``,I(e.access_token,e.me),await n.replace(m())}catch(e){e instanceof D?(l.value=e.message,e.code===`invalid_credentials`&&(a.value=``)):l.value=`登录失败,请稍后重试`}finally{c.value=!1}}}return(e,t)=>(u(),V(`main`,$,[j(`section`,ee,[j(`header`,te,[j(`img`,{class:`login-mark`,src:F(Q),alt:`甄养堂`,width:`72`,height:`72`},null,8,ne),t[2]||(t[2]=j(`h1`,null,`企微客服助手`,-1)),t[3]||(t[3]=j(`p`,null,`手机控制台 · 使用 ZYT 账号登录`,-1))]),d.value?(u(),E(F(Z),{key:0,class:`login-notice`,"left-icon":`info-o`,scrollable:!1,wrapable:``,text:`登录已失效或已在管理端被下线,请重新登录。`})):z(``,!0),g(F(K),{class:`login-form`,onSubmit:h},{default:_(()=>[g(F(o),{inset:``},{default:_(()=>[g(F(s),{modelValue:r.value,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),name:`account`,label:`账号`,placeholder:`ZYT 登录账号`,autocomplete:`username`,autocapitalize:`off`,spellcheck:!1,enterkeyhint:`next`,clearable:``,rules:[{required:!0,message:`请输入账号`}]},null,8,[`modelValue`]),g(F(s),{modelValue:a.value,"onUpdate:modelValue":t[1]||(t[1]=e=>a.value=e),type:`password`,name:`password`,label:`密码`,placeholder:`ZYT 登录密码`,autocomplete:`current-password`,enterkeyhint:`go`,rules:[{required:!0,message:`请输入密码`}]},null,8,[`modelValue`])]),_:1}),l.value?(u(),V(`p`,re,C(l.value),1)):z(``,!0),j(`div`,ie,[g(F(i),{round:``,block:``,type:`primary`,"native-type":`submit`,loading:c.value,"loading-text":`正在登录…`},{default:_(()=>[...t[4]||(t[4]=[B(` 登录 `,-1)])]),_:1},8,[`loading`])])]),_:1}),t[5]||(t[5]=j(`p`,{class:`login-foot`},`密码只用于向 ZYT 验证身份,不会保存在手机上。`,-1))])]))}}),[[`__scopeId`,`data-v-004201f6`]]);export{ae as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css new file mode 100644 index 000000000..54f80a155 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css @@ -0,0 +1 @@ +.login[data-v-004201f6]{min-height:100vh;min-height:100dvh;padding:calc(env(safe-area-inset-top) + 24px) 16px calc(env(safe-area-inset-bottom) + 24px);background:radial-gradient(120% 60% at 50% 0%, var(--app-primary-soft), transparent 70%), var(--app-bg);justify-content:center;align-items:center;display:flex}.login-card[data-v-004201f6]{width:100%;max-width:420px}.login-head[data-v-004201f6]{text-align:center;margin-bottom:24px}.login-mark[data-v-004201f6]{box-shadow:var(--app-shadow);border-radius:20px}.login-head h1[data-v-004201f6]{letter-spacing:.5px;margin:16px 0 6px;font-size:24px;font-weight:700}.login-head p[data-v-004201f6]{color:var(--app-muted);margin:0;font-size:14px}.login-notice[data-v-004201f6]{border-radius:12px;margin:0 16px 12px}.login-form[data-v-004201f6] .van-cell-group--inset,.login-form[data-v-004201f6]{margin:0}.login-error[data-v-004201f6]{color:var(--app-danger);margin:12px 16px 0;font-size:14px;line-height:1.5}.login-actions[data-v-004201f6]{margin:20px 16px 0}.login-foot[data-v-004201f6]{text-align:center;color:var(--app-muted);margin:16px 16px 0;font-size:12px} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css new file mode 100644 index 000000000..c9fbeae2d --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css @@ -0,0 +1 @@ +.content[data-v-a968ee67]{max-width:720px;margin:0 auto;padding:12px 0 32px}.profile[data-v-a968ee67]{align-items:center;gap:14px;margin:0 16px;padding:16px;display:flex}.avatar[data-v-a968ee67]{color:#fff;background:linear-gradient(135deg,#2f6fff,#5a65ff);border-radius:50%;flex:none;place-items:center;width:54px;height:54px;font-size:22px;font-weight:700;display:grid}.profile-text[data-v-a968ee67]{flex:1;min-width:0}.profile-text h2[data-v-a968ee67]{margin:0 0 4px;font-size:18px}.profile-text p[data-v-a968ee67]{margin:0;font-size:13px}.root[data-v-a968ee67]{color:var(--app-warning);background:var(--app-warning-soft);border-radius:999px;flex:none;padding:4px 10px;font-size:12px;font-weight:600}.section-title[data-v-a968ee67]{color:var(--app-muted);margin:24px 32px 8px;font-size:13px;font-weight:600}.hint[data-v-a968ee67]{margin:8px 32px 0;font-size:12px;line-height:1.6}.logout[data-v-a968ee67]{margin:32px 16px 0} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js new file mode 100644 index 000000000..adf43b057 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{a as n,i as r,o as i,s as a}from"./format-Dt9-L1ne.js";import{a as o,n as s,t as c}from"./cell-group-B1mihubH.js";import{Ct as l,Ft as u,Jt as d,Kt as f,Lt as p,St as m,Ut as h,Yt as g,_t as _,d as v,ft as y,gt as b,ht as x,i as S,n as C,qt as w,vt as T,xt as E,yt as D}from"./index-C_M75pH8.js";var O={class:`page`},k={key:0,class:`content`},A={class:`profile app-card`},j={class:`avatar`,"aria-hidden":`true`},M={class:`profile-text`},N={class:`app-muted`},P={key:0,class:`root`},F={class:`hint app-muted`},I={class:`logout`},L=t(l({__name:`MeView`,setup(t){let l=v(),L=f(!1),R=x(()=>C.me),z=x(()=>{let e=R.value;if(!e)return[];let{permissions:t,permission_codes:n}=e;return[{title:`查看本人电脑与会话`,code:`登录即可`,granted:!0},{title:`远程启停本人电脑`,code:n.control,granted:t.control},{title:`查看并控制所有客服电脑`,code:n.control_all,granted:t.control_all},{title:`会话加入忽略`,code:n.blacklist_add,granted:t.blacklist_add},{title:`会话移出忽略`,code:n.blacklist_remove,granted:t.blacklist_remove}]}),B=x(()=>{var e;let t=(e=R.value)==null?void 0:e.session.expires_at;return t?new Date(t*1e3).toISOString():``}),V=/iPhone|iPad|iPod/.test(navigator.userAgent);async function H(){try{await a({title:`退出登录?`,message:`退出后需要重新输入 ZYT 账号密码。`,confirmButtonText:`退出`})}catch{return}L.value=!0,await S(),await l.replace({name:`login`})}return(t,a)=>(u(),D(`div`,O,[m(w(i),{title:`我的`,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1}),R.value?(u(),D(`div`,k,[b(`section`,A,[b(`span`,j,g(w(r)(R.value.account.nickname||R.value.account.sn)),1),b(`div`,M,[b(`h2`,null,g(R.value.account.nickname||R.value.account.sn),1),b(`p`,N,`ZYT 账号 `+g(R.value.account.sn||`—`),1)]),R.value.permissions.root?(u(),D(`span`,P,`超级管理员`)):T(``,!0)]),a[1]||(a[1]=b(`h3`,{class:`section-title`},`手机端权限`,-1)),m(w(c),{inset:``},{default:h(()=>[(u(!0),D(y,null,p(z.value,t=>(u(),_(w(s),{key:t.title,title:t.title,label:t.code,center:``},{value:h(()=>[b(`span`,{class:d(t.granted?`tone-success`:`tone-muted`)},[m(w(e),{name:t.granted?`passed`:`close`},null,8,[`name`]),E(` `+g(t.granted?`已开通`:`未开通`),1)],2)]),_:2},1032,[`title`,`label`]))),128))]),_:1}),a[2]||(a[2]=b(`p`,{class:`hint app-muted`},` 权限在 ZYT「权限管理 → 角色管理」里勾选「企微客服助手」下的项目,调整后 10 分钟内自动生效,或退出重新登录。 `,-1)),a[3]||(a[3]=b(`h3`,{class:`section-title`},`登录信息`,-1)),m(w(c),{inset:``},{default:h(()=>[m(w(s),{title:`登录时间`,value:w(n)(R.value.session.created_at)},null,8,[`value`]),m(w(s),{title:`最长有效至`,value:B.value?w(n)(B.value):`—`,label:`ZYT 登录失效后会提前退出`},null,8,[`value`])]),_:1}),a[4]||(a[4]=b(`h3`,{class:`section-title`},`添加到主屏幕`,-1)),b(`p`,F,[w(V)?(u(),D(y,{key:0},[E(`在 Safari 中点底部「分享」按钮,选择「添加到主屏幕」,之后像 App 一样打开。`)],64)):(u(),D(y,{key:1},[E(`在浏览器菜单中选择「添加到主屏幕」或「安装应用」,之后像 App 一样打开。`)],64))]),b(`div`,I,[m(w(o),{block:``,round:``,type:`danger`,plain:``,loading:L.value,onClick:H},{default:h(()=>[...a[0]||(a[0]=[E(`退出登录`,-1)])]),_:1},8,[`loading`])])])):T(``,!0)]))}}),[[`__scopeId`,`data-v-a968ee67`]]);export{L as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css new file mode 100644 index 000000000..a8ec81749 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css @@ -0,0 +1 @@ +.shell[data-v-5972669c]{min-height:100vh;min-height:100dvh}.shell-tabbar[data-v-5972669c]{border-top:1px solid var(--app-border)} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js new file mode 100644 index 000000000..50a6278ca --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js @@ -0,0 +1 @@ +import{a as e,n as t,o as n,p as r,r as i,t as a}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n as o,t as s}from"./use-placeholder-BHVOzwXa.js";import{B as c,Ct as l,Et as u,Ft as d,Kt as f,Q as p,Rt as m,S as h,St as g,U as _,Ut as v,X as y,Z as b,_ as x,_t as S,g as C,ht as w,pt as T,q as E,qt as D,rt as O,wt as k,xt as A,yt as j,zt as M}from"./index-C_M75pH8.js";var[N,P]=x(`tabbar`),F={route:Boolean,fixed:b,border:b,zIndex:y,placeholder:Boolean,activeColor:String,beforeChange:Function,inactiveColor:String,modelValue:E(0),safeAreaInsetBottom:{type:Boolean,default:null}},I=Symbol(N),L=l({name:N,props:F,emits:[`change`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i=f(),{linkChildren:a}=c(I),l=s(i,P),u=()=>{var t;return(t=e.safeAreaInsetBottom)==null?e.fixed:t},d=()=>{var t;let{fixed:a,zIndex:o,border:s}=e;return g(`div`,{ref:i,role:`tablist`,style:h(o),class:[P({fixed:a}),{[r]:s,"van-safe-area-bottom":u()}]},[(t=n.default)==null?void 0:t.call(n)])};return a({props:e,setActive:(n,r)=>{o(e.beforeChange,{args:[n],done(){t(`update:modelValue`,n),t(`change`,n),r()}})}}),()=>e.fixed&&e.placeholder?l(d):d()}}),R=C(L),[z,B]=x(`tabbar-item`),V=p({},e,{dot:Boolean,icon:String,name:y,badge:y,badgeProps:Object,iconPrefix:String}),H=l({name:z,props:V,emits:[`click`],setup(e,{emit:r,slots:a}){let o=n(),s=k().proxy,{parent:c,index:l}=_(I);if(!c)return;let d=w(()=>{var t;let{route:n,modelValue:r}=c.props;if(n&&`$route`in s){let{$route:t}=s,{to:n}=e,r=O(n)?n:{path:n};return t.matched.some(e=>{let t=`path`in r&&r.path===e.path,n=`name`in r&&r.name===e.name;return t||n})}return((t=e.name)==null?l.value:t)===r}),f=t=>{var n;d.value||c.setActive((n=e.name)==null?l.value:n,o),r(`click`,t)},p=()=>{if(a.icon)return a.icon({active:d.value});if(e.icon)return g(t,{name:e.icon,classPrefix:e.iconPrefix},null)};return()=>{var t;let{dot:n,badge:r}=e,{activeColor:o,inactiveColor:s}=c.props,l=d.value?o:s;return g(`div`,{role:`tab`,class:B({active:d.value}),style:{color:l},tabindex:0,"aria-selected":d.value,onClick:f},[g(i,u({dot:n,class:B(`icon`),content:r},e.badgeProps),{default:p}),g(`div`,{class:B(`text`)},[(t=a.default)==null?void 0:t.call(a,{active:d.value})])])}}}),U=C(H),W={class:`shell`},G=a(l({__name:`ShellView`,setup(e){return(e,t)=>{let n=m(`RouterView`);return d(),j(`div`,W,[g(n,null,{default:v(({Component:e})=>[(d(),S(T,{include:[`DevicesView`,`ConversationsView`]},[(d(),S(M(e)))],1024))]),_:1}),g(D(R),{route:``,placeholder:``,"safe-area-inset-bottom":``,class:`shell-tabbar`},{default:v(()=>[g(D(U),{replace:``,to:{name:`desktops`},icon:`desktop-o`},{default:v(()=>[...t[0]||(t[0]=[A(`电脑`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`conversations`},icon:`chat-o`},{default:v(()=>[...t[1]||(t[1]=[A(`会话`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`me`},icon:`user-o`},{default:v(()=>[...t[2]||(t[2]=[A(`我的`,-1)])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-5972669c`]]);export{G as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js new file mode 100644 index 000000000..21f72e323 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js @@ -0,0 +1 @@ +import{Ct as e,Et as t,St as n,Tt as r,Vt as i,X as a,Y as o,Z as s,_ as c,et as l,g as u,ht as d,m as f,nt as p,v as m,wt as h}from"./index-C_M75pH8.js";var g=`van-hairline`,_=`${g}--top`,v=`${g}--left`;`${g}`;var y=`${g}--bottom`,b=`${g}--surround`,x=`${g}--top-bottom`;`${g}`;var S=`van-haptics-feedback`,C=Symbol(`van-form`),w=Symbol();function T(e){let t=r(w,null);t&&i(t,t=>{t&&e()})}var E={to:[String,Object],url:String,replace:Boolean};function D({to:e,url:t,replace:n,$router:r}){e&&r?r[n?`replace`:`push`](e):t&&(n?location.replace(t):location.href=t)}function O(){let e=h().proxy;return()=>D(e)}var[k,A]=c(`badge`),j={dot:Boolean,max:a,tag:o(`div`),color:String,offset:Array,content:a,showZero:s,position:o(`top-right`)},M=e({name:k,props:j,setup(e,{slots:t}){let r=()=>{if(t.content)return!0;let{content:n,showZero:r}=e;return l(n)&&n!==``&&(r||n!==0&&n!==`0`)},i=()=>{let{dot:n,max:i,content:a}=e;if(!n&&r())return t.content?t.content():l(i)&&p(a)&&+a>+i?`${i}+`:a},a=e=>e.startsWith(`-`)?e.replace(`-`,``):`-${e}`,o=d(()=>{let n={background:e.color};if(e.offset){let[r,i]=e.offset,{position:o}=e,[s,c]=o.split(`-`);t.default?(n[s]=typeof i==`number`?m(s===`top`?i:-i):s===`top`?m(i):a(i),n[c]=typeof r==`number`?m(c===`left`?r:-r):c===`left`?m(r):a(r)):(n.marginTop=m(i),n.marginLeft=m(r))}return n}),s=()=>{if(r()||e.dot)return n(`div`,{class:A([e.position,{dot:e.dot,fixed:!!t.default}]),style:o.value},[i()])};return()=>{if(t.default){let{tag:r}=e;return n(r,{class:A(`wrapper`)},{default:()=>[t.default(),s()]})}return s()}}}),N=u(M),[P,F]=c(`icon`),I=e=>e==null?void 0:e.includes(`/`),L={dot:Boolean,tag:o(`i`),name:String,size:a,badge:a,color:String,badgeProps:Object,classPrefix:String},R=e({name:P,props:L,setup(e,{slots:i}){let a=r(f,null),o=d(()=>e.classPrefix||(a==null?void 0:a.iconPrefix)||F());return()=>{let{tag:r,dot:a,name:s,size:c,badge:l,color:u}=e,d=I(s);return n(N,t({dot:a,tag:r,class:[o.value,d?``:`${o.value}-${s}`],style:{color:u,fontSize:m(c)},content:l},e.badgeProps),{default:()=>{var e;return[(e=i.default)==null?void 0:e.call(i),d&&n(`img`,{class:F(`image`),src:s},null)]}})}}}),z=u(R),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{E as a,T as c,b as d,_ as f,S as h,D as i,y as l,C as m,z as n,O as o,x as p,N as r,w as s,B as t,v as u}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png differ diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js new file mode 100644 index 000000000..64482f6cc --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js @@ -0,0 +1 @@ +import{a as e,d as t,n,o as r,p as i}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{Ct as a,Et as o,O as s,Q as c,St as l,X as u,Y as d,Z as f,_ as p,et as m,ft as h,g,ht as _,v,wt as y,x as b}from"./index-C_M75pH8.js";function x(e){let t=y();t&&c(t.proxy,e)}var[S,C]=p(`loading`),w=Array(12).fill(null).map((e,t)=>l(`i`,{class:C(`line`,String(t+1))},null)),T=l(`svg`,{class:C(`circular`),viewBox:`25 25 50 50`},[l(`circle`,{cx:`50`,cy:`50`,r:`20`,fill:`none`},null)]),E={size:u,type:d(`circular`),color:String,vertical:Boolean,textSize:u,textColor:String},D=a({name:S,props:E,setup(e,{slots:t}){let n=_(()=>c({color:e.color},b(e.size))),r=()=>{let r=e.type===`spinner`?w:T;return l(`span`,{class:C(`spinner`,e.type),style:n.value},[t.icon?t.icon():r])},i=()=>{var n;if(t.default)return l(`span`,{class:C(`text`),style:{fontSize:v(e.textSize),color:(n=e.textColor)==null?e.color:n}},[t.default()])};return()=>{let{type:t,vertical:n}=e;return l(`div`,{class:C([t,{vertical:n}]),"aria-live":`polite`,"aria-busy":!0},[r(),i()])}}}),O=g(D),[k,A]=p(`button`),j=c({},e,{tag:d(`button`),text:String,icon:String,type:d(`default`),size:d(`normal`),color:String,block:Boolean,plain:Boolean,round:Boolean,square:Boolean,loading:Boolean,hairline:Boolean,disabled:Boolean,iconPrefix:String,nativeType:d(`button`),loadingSize:u,loadingText:String,loadingType:String,iconPosition:d(`left`)}),M=a({name:k,props:j,emits:[`click`],setup(e,{emit:i,slots:a}){let o=r(),c=()=>a.loading?a.loading():l(O,{size:e.loadingSize,type:e.loadingType,class:A(`loading`)},null),u=()=>{if(e.loading)return c();if(a.icon)return l(`div`,{class:A(`icon`)},[a.icon()]);if(e.icon)return l(n,{name:e.icon,class:A(`icon`),classPrefix:e.iconPrefix},null)},d=()=>{let t;if(t=e.loading?e.loadingText:a.default?a.default():e.text,t)return l(`span`,{class:A(`text`)},[t])},f=()=>{let{color:t,plain:n}=e;if(t){let e={color:n?t:`white`};return n||(e.background=t),t.includes(`gradient`)?e.border=0:e.borderColor=t,e}},p=t=>{e.loading?s(t):e.disabled||(i(`click`,t),o())};return()=>{let{tag:n,type:r,size:i,block:a,round:o,plain:s,square:c,loading:m,disabled:h,hairline:g,nativeType:_,iconPosition:v}=e,y=[A([r,i,{plain:s,block:a,round:o,square:c,loading:m,disabled:h,hairline:g}]),{[t]:g}];return l(n,{type:_,class:y,style:f(),disabled:h,onClick:p},{default:()=>[l(`div`,{class:A(`content`)},[v===`left`&&u(),d(),v===`right`&&u()])]})}}}),N=g(M),P=()=>{var e;let{scopeId:t}=((e=y())==null?void 0:e.vnode)||{};return t?{[t]:``}:null},[F,I]=p(`cell`),L={tag:d(`div`),icon:String,size:String,title:u,value:u,label:u,center:Boolean,isLink:Boolean,border:f,iconPrefix:String,valueClass:null,labelClass:null,titleClass:null,titleStyle:null,arrowDirection:String,required:{type:[Boolean,String],default:null},clickable:{type:Boolean,default:null}},R=c({},L,e),z=a({name:F,props:R,setup(e,{slots:t}){let i=r(),a=()=>{if(t.label||m(e.label))return l(`div`,{class:[I(`label`),e.labelClass]},[t.label?t.label():e.label])},o=()=>{var n;if(t.title||m(e.title)){let r=(n=t.title)==null?void 0:n.call(t);return Array.isArray(r)&&r.length===0?void 0:l(`div`,{class:[I(`title`),e.titleClass],style:e.titleStyle},[r||l(`span`,null,[e.title]),a()])}},s=()=>{let n=t.value||t.default;if(n||m(e.value))return l(`div`,{class:[I(`value`),e.valueClass]},[n?n():l(`span`,null,[e.value])])},c=()=>{if(t.icon)return t.icon();if(e.icon)return l(n,{name:e.icon,class:I(`left-icon`),classPrefix:e.iconPrefix},null)},u=()=>{if(t[`right-icon`])return t[`right-icon`]();if(e.isLink){let t=e.arrowDirection&&e.arrowDirection!==`right`?`arrow-${e.arrowDirection}`:`arrow`;return l(n,{name:t,class:I(`right-icon`)},null)}};return()=>{var n,r,a;let{tag:d,size:f,center:p,border:m,isLink:h,required:g}=e,_=!!((a=(r=(n=e.clickable)==null?e.to:n)==null?e.url:r)==null?h:a),v={center:p,required:!!g,clickable:_,borderless:!m};return f&&(v[f]=!!f),l(d,{class:I(v),role:_?`button`:void 0,tabindex:_?0:void 0,onClick:_?i:void 0},{default:()=>{var e;return[c(),o(),s(),u(),(e=t.extra)==null?void 0:e.call(t)]}})}}}),B=g(z),[V,H]=p(`cell-group`),U=a({name:V,inheritAttrs:!1,props:{title:String,inset:Boolean,border:f},setup(e,{slots:t,attrs:n}){let r=()=>{var r;return l(`div`,o({class:[H({inset:e.inset}),{[i]:e.border&&!e.inset}]},n,P()),[(r=t.default)==null?void 0:r.call(t)])},a=()=>l(`div`,{class:H(`title`,{inset:e.inset})},[t.title?t.title():e.title]);return()=>e.title||t.title?l(h,null,[a(),r()]):r()}}),W=g(U);export{N as a,P as i,B as n,O as o,L as r,x as s,W as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js new file mode 100644 index 000000000..5c6c4b23a --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js @@ -0,0 +1 @@ +import{m as e,n as t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n,r,s as i}from"./cell-group-B1mihubH.js";import{A as a,Ct as o,Dt as s,Et as c,Gt as ee,It as l,Kt as u,Mt as d,O as f,P as te,Q as p,St as m,T as h,U as g,V as _,Vt as v,X as y,Y as b,_ as x,b as S,et as C,g as w,ht as T,it as E,k as ne,q as D,rt as O,st as k,tt as A,v as j,wt as M,xt as re,y as ie}from"./index-C_M75pH8.js";var N=0;function P(){let e=M(),{name:t=`unknown`}=(e==null?void 0:e.type)||{};return`${t}-${++N}`}function F(e){return Array.isArray(e)?!e.length:e!==0&&!e}function ae(e,t){if(F(e)){if(t.required)return!1;if(t.validateEmpty===!1)return!0}return!(t.pattern&&!t.pattern.test(String(e)))}function oe(e,t){return new Promise(n=>{let r=t.validator(e,t);if(E(r)){r.then(n);return}n(r)})}function I(e,t){let{message:n}=t;return A(n)?n(e,t):n||``}function se({target:e}){e.composing=!0}function L({target:e}){e.composing&&(e.composing=!1,e.dispatchEvent(new Event(`input`)))}function ce(e,t){let n=h();e.style.height=`auto`;let r=e.scrollHeight;if(O(t)){let{maxHeight:e,minHeight:n}=t;e!==void 0&&(r=Math.min(r,e)),n!==void 0&&(r=Math.max(r,n))}r&&(e.style.height=`${r}px`,a(n))}function le(e,t){return e===`number`&&(e=`text`,t!=null||(t=`decimal`)),e===`digit`&&(e=`tel`,t!=null||(t=`numeric`)),{type:e,inputmode:t}}function R(e){return[...e].length}function z(e,t){return[...e].slice(0,t).join(``)}var[B,V]=x(`field`),H={id:String,name:String,leftIcon:String,rightIcon:String,autofocus:Boolean,clearable:Boolean,maxlength:y,max:Number,min:Number,formatter:Function,clearIcon:b(`clear`),modelValue:D(``),inputAlign:String,placeholder:String,autocomplete:String,autocapitalize:String,autocorrect:String,errorMessage:String,enterkeyhint:String,clearTrigger:b(`focus`),formatTrigger:b(`onChange`),spellcheck:{type:Boolean,default:null},error:{type:Boolean,default:null},disabled:{type:Boolean,default:null},readonly:{type:Boolean,default:null},inputmode:String},U=p({},r,H,{rows:y,type:b(`text`),rules:Array,autosize:[Boolean,Object],labelWidth:y,labelClass:null,labelAlign:String,showWordLimit:Boolean,errorMessageAlign:String,colon:{type:Boolean,default:null}}),W=o({name:B,props:U,emits:[`blur`,`focus`,`clear`,`keypress`,`clickInput`,`endValidate`,`startValidate`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(r,{emit:a,slots:o}){let p=P(),h=ee({status:`unvalidated`,focused:!1,validateMessage:``}),y=u(),b=u(),x=u(),{parent:w}=g(e),E=()=>{var e;return String((e=r.modelValue)==null?``:e)},D=e=>{if(C(r[e]))return r[e];if(w&&C(w.props[e]))return w.props[e]},O=T(()=>{let e=D(`readonly`);if(r.clearable&&!e){let e=E()!==``,t=r.clearTrigger===`always`||r.clearTrigger===`focus`&&h.focused;return e&&t}return!1}),A=T(()=>x.value&&o.input?x.value():r.modelValue),M=T(()=>{var e;let t=D(`required`);return t===`auto`?(e=r.rules)==null?void 0:e.some(e=>e.required):t}),N=e=>e.reduce((e,t)=>e.then(()=>{if(h.status===`failed`)return;let{value:e}=A;if(t.formatter&&(e=t.formatter(e,t)),!ae(e,t)){h.status=`failed`,h.validateMessage=I(e,t);return}if(t.validator)return F(e)&&t.validateEmpty===!1?void 0:oe(e,t).then(n=>{n&&typeof n==`string`?(h.status=`failed`,h.validateMessage=n):n===!1&&(h.status=`failed`,h.validateMessage=I(e,t))})}),Promise.resolve()),B=()=>{h.status=`unvalidated`,h.validateMessage=``},H=()=>a(`endValidate`,{status:h.status,message:h.validateMessage}),U=(e=r.rules)=>new Promise(t=>{B(),e?(a(`startValidate`),N(e).then(()=>{h.status===`failed`?(t({name:r.name,message:h.validateMessage}),H()):(h.status=`passed`,t(),H())})):t()}),W=e=>{if(w&&r.rules){let{validateTrigger:t}=w.props,n=k(t).includes(e),i=r.rules.filter(t=>t.trigger?k(t.trigger).includes(e):n);i.length&&U(i)}},G=e=>{var t;let{maxlength:n}=r;if(C(n)&&R(e)>+n){let r=E();if(r&&R(r)===+n)return r;let i=(t=y.value)==null?void 0:t.selectionEnd;if(h.focused&&i){let t=[...e],r=t.length-+n;return i=R(e.slice(0,i)),t.splice(i-r,r),t.join(``)}return z(e,+n)}return e},K=(e,t=`onChange`)=>{var n,i;let o=e;e=G(e);let s=o.length-e.length;if(r.type===`number`||r.type===`digit`){let a=r.type===`number`;if(e=S(e,a,a),t===`onBlur`&&e!==``&&(r.min!==void 0||r.max!==void 0)){let t=ie(+e,(n=r.min)==null?-1/0:n,(i=r.max)==null?1/0:i);+e!==t&&(e=t.toString())}}let c=0;if(r.formatter&&t===r.formatTrigger){let{formatter:t,maxlength:n}=r;if(e=t(e),C(n)&&R(e)>+n&&(e=z(e,+n)),y.value&&h.focused){let{selectionEnd:e}=y.value,n=z(o,e);c=t(n).length-n.length}}if(y.value&&y.value.value!==e){if(h.focused){let{selectionStart:t,selectionEnd:n}=y.value;if(y.value.value=e,C(t)&&C(n)){let r=e.length;s?(t-=s,n-=s):c&&(t+=c,n+=c),y.value.setSelectionRange(Math.min(t,r),Math.min(n,r))}}else y.value.value=e}e!==r.modelValue&&a(`update:modelValue`,e)},ue=e=>{e.target.composing||K(e.target.value)},q=()=>{var e;return(e=y.value)==null?void 0:e.blur()},J=()=>{var e;return(e=y.value)==null?void 0:e.focus()},Y=()=>{let e=y.value;r.type===`textarea`&&r.autosize&&e&&ce(e,r.autosize)},de=e=>{h.focused=!0,a(`focus`,e),s(Y),D(`readonly`)&&q()},fe=e=>{h.focused=!1,K(E(),`onBlur`),a(`blur`,e),!D(`readonly`)&&(W(`onBlur`),s(Y),ne())},X=e=>a(`clickInput`,e),pe=e=>a(`clickLeftIcon`,e),me=e=>a(`clickRightIcon`,e),he=e=>{f(e),a(`update:modelValue`,``),a(`clear`,e)},Z=T(()=>{if(typeof r.error==`boolean`)return r.error;if(w&&w.props.showError&&h.status===`failed`)return!0}),ge=T(()=>{let e=D(`labelWidth`),t=D(`labelAlign`);if(e&&t!==`top`)return{width:j(e)}}),Q=e=>{e.keyCode===13&&(!(w&&w.props.submitOnEnter)&&r.type!==`textarea`&&f(e),r.type===`search`&&q()),a(`keypress`,e)},$=()=>r.id||`${p}-input`,_e=()=>h.status,ve=()=>{let e=V(`control`,[D(`inputAlign`),{error:Z.value,custom:!!o.input,"min-height":r.type===`textarea`&&!r.autosize}]);if(o.input)return m(`div`,{class:e,onClick:X},[o.input()]);let t={id:$(),ref:y,name:r.name,rows:r.rows===void 0?void 0:+r.rows,class:e,disabled:D(`disabled`),readonly:D(`readonly`),autofocus:r.autofocus,placeholder:r.placeholder,autocomplete:r.autocomplete,autocapitalize:r.autocapitalize,autocorrect:r.autocorrect,enterkeyhint:r.enterkeyhint,spellcheck:r.spellcheck,"aria-labelledby":r.label?`${p}-label`:void 0,"data-allow-mismatch":`attribute`,onBlur:fe,onFocus:de,onInput:ue,onClick:X,onChange:L,onKeypress:Q,onCompositionend:L,onCompositionstart:se};return r.type===`textarea`?m(`textarea`,c(t,{inputmode:r.inputmode}),null):m(`input`,c(le(r.type,r.inputmode),t),null)},ye=()=>{let e=o[`left-icon`];if(r.leftIcon||e)return m(`div`,{class:V(`left-icon`),onClick:pe},[e?e():m(t,{name:r.leftIcon,classPrefix:r.iconPrefix},null)])},be=()=>{let e=o[`right-icon`];if(r.rightIcon||e)return m(`div`,{class:V(`right-icon`),onClick:me},[e?e():m(t,{name:r.rightIcon,classPrefix:r.iconPrefix},null)])},xe=()=>{if(r.showWordLimit&&r.maxlength){let e=R(E());return m(`div`,{class:V(`word-limit`)},[m(`span`,{class:V(`word-num`)},[e]),re(`/`),r.maxlength])}},Se=()=>{if(w&&w.props.showErrorMessage===!1)return;let e=r.errorMessage||h.validateMessage;if(e){let t=o[`error-message`],n=D(`errorMessageAlign`);return m(`div`,{class:V(`error-message`,n)},[t?t({message:e}):e])}},Ce=()=>{let e=D(`labelWidth`),t=D(`labelAlign`),n=D(`colon`)?`:`:``;if(o.label)return[o.label(),n];if(r.label)return m(`label`,{id:`${p}-label`,for:o.input?void 0:$(),"data-allow-mismatch":`attribute`,onClick:e=>{f(e),J()},style:t===`top`&&e?{width:j(e)}:void 0},[r.label+n])},we=()=>[m(`div`,{class:V(`body`)},[ve(),O.value&&m(t,{ref:b,name:r.clearIcon,class:V(`clear`)},null),be(),o.button&&m(`div`,{class:V(`button`)},[o.button()])]),xe(),Se()];return i({blur:q,focus:J,validate:U,formValue:A,resetValidation:B,getValidationStatus:_e,adjustTextareaSize:Y}),l(te,{customValue:x,resetValidation:B,validateWithTrigger:W}),v(()=>r.modelValue,()=>{K(E()),B(),W(`onChange`),s(Y)}),d(()=>{K(E(),r.formatTrigger),s(Y)}),_(`touchstart`,he,{target:T(()=>{var e;return(e=b.value)==null?void 0:e.$el})}),()=>{let e=D(`disabled`),t=D(`labelAlign`),i=ye(),a=()=>{let e=Ce();return t===`top`?[i,e].filter(Boolean):e||[]};return m(n,{size:r.size,class:V({error:Z.value,disabled:e,[`label-${t}`]:t}),center:r.center,border:r.border,isLink:!e&&r.isLink,clickable:!e&&r.clickable,onClick:e?e=>e.stopImmediatePropagation():void 0,titleStyle:ge.value,valueClass:V(`value`),titleClass:[V(`label`,[t,{required:M.value}]),r.labelClass],arrowDirection:r.arrowDirection},{icon:i&&t!==`top`?()=>i:null,title:a,value:we,extra:o.extra})}}}),G=w(W);export{H as n,P as r,G as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js new file mode 100644 index 000000000..563103913 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js @@ -0,0 +1 @@ +import{a as e,f as t,h as n,l as r,n as i,o as a,s as o,u as s}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n as c,t as l}from"./use-placeholder-BHVOzwXa.js";import{a as u,i as d,s as f}from"./cell-group-B1mihubH.js";import{$ as p,B as m,Ct as h,Dt as ee,Et as g,Gt as _,It as te,Kt as v,L as y,Mt as b,O as x,Ot as ne,Q as S,R as C,S as w,St as T,U as E,V as re,Vt as D,Wt as O,X as k,Y as A,Z as j,_ as M,at as N,ct as ie,dt as P,et as ae,ft as oe,g as F,h as se,ht as I,jt as ce,kt as le,lt as ue,mt as de,ot as fe,tt as pe,ut as L,v as me}from"./index-C_M75pH8.js";var[R,z]=M(`action-bar`),B=Symbol(R),he=h({name:R,props:{placeholder:Boolean,safeAreaInsetBottom:j},setup(e,{slots:t}){let n=v(),r=l(n,z),{linkChildren:i}=m(B);i();let a=()=>{var r;return T(`div`,{ref:n,class:[z(),{"van-safe-area-bottom":e.safeAreaInsetBottom}]},[(r=t.default)==null?void 0:r.call(t)])};return()=>e.placeholder?r(a):a()}}),ge=F(he),[_e,ve]=M(`action-bar-button`),ye=S({},e,{type:String,text:String,icon:String,color:String,loading:Boolean,disabled:Boolean}),be=h({name:_e,props:ye,setup(e,{slots:t}){let n=a(),{parent:r,index:i}=E(B),o=I(()=>{if(r){let e=r.children[i.value-1];return!(e&&`isButton`in e)}}),s=I(()=>{if(r){let e=r.children[i.value+1];return!(e&&`isButton`in e)}});return f({isButton:!0}),()=>{let{type:r,icon:i,text:a,color:c,loading:l,disabled:d}=e;return T(u,{class:ve([r,{last:s.value,first:o.value}]),size:`large`,type:r,icon:i,color:c,loading:l,disabled:d,onClick:n},{default:()=>[t.default?t.default():a]})}}}),V=F(be),H={show:Boolean,zIndex:k,overlay:j,duration:k,teleport:[String,Object],lockScroll:j,lazyRender:j,beforeClose:Function,overlayProps:Object,overlayStyle:Object,overlayClass:null,transitionAppear:Boolean,closeOnClickOverlay:j},xe=Object.keys(H);function Se(e,t){return e>t?`horizontal`:t>e?`vertical`:``}function U(){let e=v(0),t=v(0),n=v(0),r=v(0),i=v(0),a=v(0),o=v(``),s=v(!0),c=()=>o.value===`vertical`,l=()=>o.value===`horizontal`,u=()=>{n.value=0,r.value=0,i.value=0,a.value=0,o.value=``,s.value=!0};return{move:(c=>{let l=c.touches[0];n.value=(l.clientX<0?0:l.clientX)-e.value,r.value=l.clientY-t.value,i.value=Math.abs(n.value),a.value=Math.abs(r.value),(!o.value||i.value<10&&a.value<10)&&(o.value=Se(i.value,a.value)),s.value&&(i.value>5||a.value>5)&&(s.value=!1)}),start:(n=>{u(),e.value=n.touches[0].clientX,t.value=n.touches[0].clientY}),reset:u,startX:e,startY:t,deltaX:n,deltaY:r,offsetX:i,offsetY:a,direction:o,isVertical:c,isHorizontal:l,isTap:s}}var W=0,G=`van-overflow-hidden`;function Ce(e,t){let n=U(),r=t=>{n.move(t);let r=n.deltaY.value>0?`10`:`01`,i=y(t.target,e.value);for(;i.scrollHeight<=i.offsetHeight&&i!==e.value&&i.parentElement;)i=y(i.parentElement,e.value);let{scrollHeight:a,offsetHeight:o,scrollTop:s}=i,c=`11`;s===0?c=o>=a?`00`:`01`:s+o>=a&&(c=`10`),c!==`11`&&n.isVertical()&&!(parseInt(c,2)&parseInt(r,2))&&x(t,!0)},i=()=>{document.addEventListener(`touchstart`,n.start),document.addEventListener(`touchmove`,r,{passive:!1}),W||document.body.classList.add(G),W++},a=()=>{W&&(document.removeEventListener(`touchstart`,n.start),document.removeEventListener(`touchmove`,r),W--,W||document.body.classList.remove(G))},o=()=>t()&&i(),s=()=>t()&&a();C(o),ce(s),le(s),D(t,e=>{e?i():a()})}function K(e){let t=v(!1);return D(e,e=>{e&&(t.value=e)},{immediate:!0}),e=>()=>t.value?e():null}var[we,Te]=M(`overlay`),Ee=h({name:we,inheritAttrs:!1,props:{show:Boolean,zIndex:k,duration:k,className:null,lockScroll:j,lazyRender:j,customStyle:Object,teleport:[String,Object]},setup(e,{attrs:t,slots:n}){let r=v(),i=K(()=>e.show||!e.lazyRender),a=t=>{e.lockScroll&&x(t,!0)},o=i(()=>{var i;let a=S(w(e.zIndex),e.customStyle);return ae(e.duration)&&(a.animationDuration=`${e.duration}s`),O(T(`div`,g({ref:r,style:a,class:[Te(),e.className]},t),[(i=n.default)==null?void 0:i.call(n)]),[[L,e.show]])});return re(`touchmove`,a,{target:r}),()=>{let t=T(ie,{name:`van-fade`,appear:!0},{default:o});return e.teleport?T(de,{to:e.teleport},{default:()=>[t]}):t}}}),De=F(Ee),Oe=S({},H,{round:Boolean,position:A(`center`),closeIcon:A(`cross`),closeable:Boolean,transition:String,iconPrefix:String,closeOnPopstate:Boolean,closeIconPosition:A(`top-right`),destroyOnClose:Boolean,safeAreaInsetTop:Boolean,safeAreaInsetBottom:Boolean}),[ke,q]=M(`popup`),Ae=h({name:ke,inheritAttrs:!1,props:Oe,emits:[`open`,`close`,`opened`,`closed`,`keydown`,`update:show`,`clickOverlay`,`clickCloseIcon`],setup(e,{emit:t,attrs:r,slots:a}){let s,l,u=v(),p=v(),m=K(()=>e.show||!e.lazyRender),h=I(()=>{let t={zIndex:u.value};if(ae(e.duration)){let n=e.position===`center`?`animationDuration`:`transitionDuration`;t[n]=`${e.duration}s`}return t}),_=()=>{s||(s=!0,u.value=e.zIndex===void 0?se():+e.zIndex,t(`open`))},y=()=>{s&&c(e.beforeClose,{done(){s=!1,t(`close`),t(`update:show`,!1)}})},x=n=>{t(`clickOverlay`,n),e.closeOnClickOverlay&&y()},C=()=>{if(e.overlay){let t=S({show:e.show,class:e.overlayClass,zIndex:u.value,duration:e.duration,customStyle:e.overlayStyle,role:e.closeOnClickOverlay?`button`:void 0,tabindex:e.closeOnClickOverlay?0:void 0},e.overlayProps);return T(De,g(t,d(),{onClick:x}),{default:a[`overlay-content`]})}},w=e=>{t(`clickCloseIcon`,e),y()},E=()=>{if(e.closeable)return T(i,{role:`button`,tabindex:0,name:e.closeIcon,class:[q(`close-icon`,e.closeIconPosition),n],classPrefix:e.iconPrefix,onClick:w},null)},k,A=()=>{k&&clearTimeout(k),k=setTimeout(()=>{t(`opened`)})},j=()=>t(`closed`),M=e=>t(`keydown`,e),N=m(()=>{var t;let{destroyOnClose:n,round:i,position:o,safeAreaInsetTop:s,safeAreaInsetBottom:c,show:l}=e;if(l||!n)return O(T(`div`,g({ref:p,style:h.value,role:`dialog`,tabindex:0,class:[q({round:i,[o]:o}),{"van-safe-area-top":s,"van-safe-area-bottom":c}],onKeydown:M},r,d()),[(t=a.default)==null?void 0:t.call(a),E()]),[[L,l]])}),P=()=>{let{position:t,transition:n,transitionAppear:r}=e,i=t===`center`?`van-fade`:`van-popup-slide-${t}`;return T(ie,{name:n||i,appear:r,onAfterEnter:A,onAfterLeave:j},{default:N})};return D(()=>e.show,e=>{e&&!s&&(_(),r.tabindex===0&&ee(()=>{var e;(e=p.value)==null||e.focus()})),!e&&s&&(s=!1,t(`close`))}),f({popupRef:p}),Ce(p,()=>e.show&&e.lockScroll),re(`popstate`,()=>{e.closeOnPopstate&&(y(),l=!1)}),b(()=>{e.show&&_()}),ne(()=>{l&&(t(`update:show`,!0),l=!1)}),ce(()=>{e.show&&e.teleport&&(y(),l=!0)}),te(o,()=>e.show),()=>e.teleport?T(de,{to:e.teleport},{default:()=>[C(),P()]}):T(oe,null,[C(),P()])}}),J=F(Ae);function je(){let e=_({show:!1}),t=t=>{e.show=t},n=n=>{S(e,n,{transitionAppear:!0}),t(!0)},r=()=>t(!1);return f({open:n,close:r,toggle:t}),{open:n,close:r,state:e,toggle:t}}function Me(e){let t=ue(e),n=document.createElement(`div`);return document.body.appendChild(n),{instance:t.mount(n),unmount(){t.unmount(),document.body.removeChild(n)}}}var[Ne,Y,X]=M(`dialog`),Pe=S({},H,{title:String,theme:String,width:k,message:[String,Function],callback:Function,allowHtml:Boolean,className:null,transition:A(`van-dialog-bounce`),messageAlign:String,closeOnPopstate:j,showCancelButton:Boolean,cancelButtonText:String,cancelButtonColor:String,cancelButtonDisabled:Boolean,confirmButtonText:String,confirmButtonColor:String,confirmButtonDisabled:Boolean,showConfirmButton:j,closeOnClickOverlay:Boolean,keyboardEnabled:j,destroyOnClose:Boolean}),Fe=[...xe,`transition`,`closeOnPopstate`,`destroyOnClose`],Ie=h({name:Ne,props:Pe,emits:[`confirm`,`cancel`,`keydown`,`update:show`],setup(e,{emit:n,slots:r}){let i=v(),a=_({confirm:!1,cancel:!1}),o=e=>n(`update:show`,e),l=t=>{var n;o(!1),(n=e.callback)==null||n.call(e,t)},d=t=>()=>{e.show&&(n(t),e.beforeClose?(a[t]=!0,c(e.beforeClose,{args:[t],done(){l(t),a[t]=!1},canceled(){a[t]=!1}})):l(t))},f=d(`cancel`),p=d(`confirm`),m=P(t=>{var r,a;e.keyboardEnabled&&t.target===((a=(r=i.value)==null?void 0:r.popupRef)==null?void 0:a.value)&&({Enter:e.showConfirmButton?p:N,Escape:e.showCancelButton?f:N}[t.key](),n(`keydown`,t))},[`enter`,`esc`]),h=()=>{let t=r.title?r.title():e.title;if(t)return T(`div`,{class:Y(`header`,{isolated:!e.message&&!r.default})},[t])},ee=t=>{let{message:n,allowHtml:r,messageAlign:i}=e,a=Y(`message`,{"has-title":t,[i]:i}),o=pe(n)?n():n;return r&&typeof o==`string`?T(`div`,{class:a,innerHTML:o},null):T(`div`,{class:a},[o])},te=()=>{if(r.default)return T(`div`,{class:Y(`content`)},[r.default()]);let{title:t,message:n,allowHtml:i}=e;if(n){let e=!!(t||r.title);return T(`div`,{key:+!!i,class:Y(`content`,{isolated:!e})},[ee(e)])}},y=()=>T(`div`,{class:[t,Y(`footer`)]},[e.showCancelButton&&T(u,{size:`large`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),style:{color:e.cancelButtonColor},loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(u,{size:`large`,text:e.confirmButtonText||X(`confirm`),class:[Y(`confirm`),{[s]:e.showCancelButton}],style:{color:e.confirmButtonColor},loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]),b=()=>T(ge,{class:Y(`footer`)},{default:()=>[e.showCancelButton&&T(V,{type:`warning`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),color:e.cancelButtonColor,loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(V,{type:`danger`,text:e.confirmButtonText||X(`confirm`),class:Y(`confirm`),color:e.confirmButtonColor,loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]}),x=()=>r.footer?r.footer():e.theme===`round-button`?b():y();return()=>{let{width:t,title:n,theme:r,message:a,className:s}=e;return T(J,g({ref:i,role:`dialog`,class:[Y([r]),s],style:{width:me(t)},tabindex:0,"aria-labelledby":n||a,onKeydown:m,"onUpdate:show":o},fe(e,Fe)),{default:()=>[h(),te(),x()]})}}}),Z,Le=S({},{title:``,width:``,theme:null,message:``,overlay:!0,callback:null,teleport:`body`,className:``,allowHtml:!1,lockScroll:!0,transition:void 0,beforeClose:null,overlayClass:``,overlayStyle:void 0,messageAlign:``,cancelButtonText:``,cancelButtonColor:null,cancelButtonDisabled:!1,confirmButtonText:``,confirmButtonColor:null,confirmButtonDisabled:!1,showConfirmButton:!0,showCancelButton:!1,closeOnPopstate:!0,closeOnClickOverlay:!1,destroyOnClose:!1});function Re(){let e={setup(){let{state:e,toggle:t}=je();return()=>T(Ie,g(e,{"onUpdate:show":t}),null)}};({instance:Z}=Me(e))}function ze(e){return p?new Promise((t,n)=>{Z||Re(),Z.open(S({},Le,e,{callback:e=>{(e===`confirm`?t:n)(e)}}))}):Promise.resolve(void 0)}var Be=e=>ze(S({showCancelButton:!0},e)),[Ve,Q]=M(`nav-bar`),He=h({name:Ve,props:{title:String,fixed:Boolean,zIndex:k,border:j,leftText:String,rightText:String,leftDisabled:Boolean,rightDisabled:Boolean,leftArrow:Boolean,placeholder:Boolean,safeAreaInsetTop:Boolean,clickable:j},emits:[`clickLeft`,`clickRight`],setup(e,{emit:t,slots:n}){let a=v(),o=l(a,Q),s=n=>{e.leftDisabled||t(`clickLeft`,n)},c=n=>{e.rightDisabled||t(`clickRight`,n)},u=()=>n.left?n.left():[e.leftArrow&&T(i,{class:Q(`arrow`),name:`arrow-left`},null),e.leftText&&T(`span`,{class:Q(`text`)},[e.leftText])],d=()=>n.right?n.right():T(`span`,{class:Q(`text`)},[e.rightText]),f=()=>{let{title:t,fixed:i,border:o,zIndex:l}=e,f=w(l),p=e.leftArrow||e.leftText||n.left,m=e.rightText||n.right;return T(`div`,{ref:a,style:f,class:[Q({fixed:i}),{[r]:o,"van-safe-area-top":e.safeAreaInsetTop}]},[T(`div`,{class:Q(`content`)},[p&&T(`div`,{class:[Q(`left`,{disabled:e.leftDisabled}),e.clickable&&!e.leftDisabled?`van-haptics-feedback`:``],onClick:s},[u()]),T(`div`,{class:[Q(`title`),`van-ellipsis`]},[n.title?n.title():t]),m&&T(`div`,{class:[Q(`right`,{disabled:e.rightDisabled}),e.clickable&&!e.rightDisabled?`van-haptics-feedback`:``],onClick:c},[d()])])])};return()=>e.fixed&&e.placeholder?o(f):f()}}),Ue=F(He),$=e=>String(e).padStart(2,`0`);function We(e){let t=String(e||``).trim();if(!t)return null;let n=new Date(t);return Number.isNaN(n.getTime())?null:n}function Ge(e,t=new Date){let n=We(e);if(!n)return`—`;let r=Math.round((t.getTime()-n.getTime())/1e3);if(r<45&&r>-60)return`刚刚`;if(r>=45&&r<3600)return`${Math.max(1,Math.round(r/60))} 分钟前`;let i=`${$(n.getHours())}:${$(n.getMinutes())}`,a=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),o=864e5;return n.getTime()>=a&&n.getTime()=a-o&&n.getTime()i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>S(e)===`[object Map]`,p=e=>S(e)===`[object Set]`,m=e=>S(e)===`[object Date]`,h=e=>S(e)===`[object RegExp]`,g=e=>typeof e==`function`,_=e=>typeof e==`string`,v=e=>typeof e==`symbol`,y=e=>typeof e==`object`&&!!e,b=e=>(y(e)||g(e))&&g(e.then)&&g(e.catch),x=Object.prototype.toString,S=e=>x.call(e),C=e=>S(e).slice(8,-1),w=e=>S(e)===`[object Object]`,T=e=>_(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,ee=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),E=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},te=/-\w/g,D=E(e=>e.replace(te,e=>e.slice(1).toUpperCase())),ne=/\B([A-Z])/g,O=E(e=>e.replace(ne,`-$1`).toLowerCase()),re=E(e=>e.charAt(0).toUpperCase()+e.slice(1)),ie=E(e=>e?`on${re(e)}`:``),k=(e,t)=>!Object.is(e,t),A=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},oe=e=>{let t=parseFloat(e);return isNaN(t)?e:t},se=e=>{let t=_(e)?Number(e):NaN;return isNaN(t)?e:t},ce,le=()=>ce||(ce=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{});function ue(e){if(d(e)){let t={};for(let n=0;n{if(e){let n=e.split(fe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function he(e){let t=``;if(_(e))t=e;else if(d(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Se=e=>_(e)?e:e==null?``:d(e)||y(e)&&(e.toString===x||!g(e.toString))?xe(e)?Se(e.value):JSON.stringify(e,Ce,2):String(e),Ce=(e,t)=>xe(t)?Ce(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[we(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>we(e))}:v(t)?we(t):y(t)&&!d(t)&&!w(t)?String(t):t,we=(e,t=``)=>{var n;return v(e)?`Symbol(${(n=e.description)==null?t:n})`:e},j,Te=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&j&&(j.active?(this.parent=j,this.index=(j.scopes||(j.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes){let n=this.scopes.slice();for(e=0,t=n.length;e0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(je){let e=je;for(je=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ae;){let t=Ae;for(Ae=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}function Fe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ie(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),ze(r),Be(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Re(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Re(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ke)||(e.globalVersion=Ke,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Le(e))))return;e.flags|=2;let t=e.dep,n=M,r=Ve;M=e,Ve=!0;try{Fe(e);let n=e.fn(e._value);(t.version===0||k(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{M=n,Ve=r,Ie(e),e.flags&=-3}}function ze(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ze(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Be(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var Ve=!0,He=[];function Ue(){He.push(Ve),Ve=!1}function We(){let e=He.pop();Ve=e===void 0||e}function Ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;try{t()}finally{M=e}}}var Ke=0,qe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Je=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!M||!Ve||M===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==M)t=this.activeLink=new qe(M,this),M.deps?(t.prevDep=M.depsTail,M.depsTail.nextDep=t,M.depsTail=t):M.deps=M.depsTail=t,Ye(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=M.depsTail,t.nextDep=void 0,M.depsTail.nextDep=t,M.depsTail=t,M.deps===t&&(M.deps=e)}return t}trigger(e){this.version++,Ke++,this.notify(e)}notify(e){Ne();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pe()}}};function Ye(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Ye(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var Xe=new WeakMap,Ze=Symbol(``),Qe=Symbol(``),$e=Symbol(``);function N(e,t,n){if(Ve&&M){let t=Xe.get(e);t||Xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Je),r.map=t,r.key=n),r.track()}}function et(e,t,n,r,i,a){let o=Xe.get(e);if(!o){Ke++;return}let s=e=>{e&&e.trigger()};if(Ne(),t===`clear`)o.forEach(s);else{let i=d(e),a=i&&T(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===$e||!v(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get($e)),t){case`add`:i?a&&s(o.get(`length`)):(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`delete`:i||(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`set`:f(e)&&s(o.get(Ze))}}Pe()}function tt(e){let t=F(e);return t===e?t:(N(t,`iterate`,$e),P(e)?t:t.map(Ht))}function nt(e){return N(e=F(e),`iterate`,$e),e}function rt(e,t){return zt(e)?Ut(Rt(e)?Ht(t):t):Ht(t)}var it={__proto__:null,[Symbol.iterator](){return at(this,Symbol.iterator,e=>rt(this,e))},concat(...e){return tt(this).concat(...e.map(e=>d(e)?tt(e):e))},entries(){return at(this,`entries`,e=>(e[1]=rt(this,e[1]),e))},every(e,t){return st(this,`every`,e,t,void 0,arguments)},filter(e,t){return st(this,`filter`,e,t,e=>e.map(e=>rt(this,e)),arguments)},find(e,t){return st(this,`find`,e,t,e=>rt(this,e),arguments)},findIndex(e,t){return st(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return st(this,`findLast`,e,t,e=>rt(this,e),arguments)},findLastIndex(e,t){return st(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return st(this,`forEach`,e,t,void 0,arguments)},includes(...e){return lt(this,`includes`,e)},indexOf(...e){return lt(this,`indexOf`,e)},join(e){return tt(this).join(e)},lastIndexOf(...e){return lt(this,`lastIndexOf`,e)},map(e,t){return st(this,`map`,e,t,void 0,arguments)},pop(){return ut(this,`pop`)},push(...e){return ut(this,`push`,e)},reduce(e,...t){return ct(this,`reduce`,e,t)},reduceRight(e,...t){return ct(this,`reduceRight`,e,t)},shift(){return ut(this,`shift`)},some(e,t){return st(this,`some`,e,t,void 0,arguments)},splice(...e){return ut(this,`splice`,e)},toReversed(){return tt(this).toReversed()},toSorted(e){return tt(this).toSorted(e)},toSpliced(...e){return tt(this).toSpliced(...e)},unshift(...e){return ut(this,`unshift`,e)},values(){return at(this,`values`,e=>rt(this,e))}};function at(e,t,n){let r=nt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var ot=Array.prototype;function st(e,t,n,r,i,a){let o=nt(e),s=o!==e&&!P(e),c=o[t];if(c!==ot[t]){let t=c.apply(e,a);return s?Ht(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,rt(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function ct(e,t,n,r){let i=nt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=rt(e,t)),n.call(this,t,rt(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?rt(e,c):c}function lt(e,t,n){let r=F(e);N(r,`iterate`,$e);let i=r[t](...n);return(i===-1||i===!1)&&Bt(n[0])?(n[0]=F(n[0]),r[t](...n)):i}function ut(e,t,n=[]){Ue(),Ne();let r=F(e)[t].apply(e,n);return Pe(),We(),r}var dt=e(`__proto__,__v_isRef,__isVue`),ft=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(v));function pt(e){v(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.hasOwnProperty(e)}var mt=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?Mt:jt:i?At:kt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=it[t]))return e;if(t===`hasOwnProperty`)return pt}let o=Reflect.get(e,t,I(e)?e:n);if((v(t)?ft.has(t):dt(t))||(r||N(e,`get`,t),i))return o;if(I(o)){let e=a&&T(t)?o:o.value;return r&&y(e)?It(e):e}return y(o)?r?It(o):Pt(o):o}},ht=class extends mt{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&T(t);if(!this._isShallow){let e=zt(i);if(!P(n)&&!zt(n)&&(i=F(i),n=F(n)),!a&&I(i)&&!I(n))return e||(i.value=n),!0}let o=a?Number(t)e,xt=e=>Reflect.getPrototypeOf(e);function St(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a),c=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?bt:t?Ut:Ht;return!t&&N(a,`iterate`,l?Qe:Ze),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Ct(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function wt(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(k(n,a)&&N(i,`get`,n),N(i,`get`,a));let{has:o}=xt(i),s=t?bt:e?Ut:Ht;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(F(t),`iterate`,Ze),t.size},has(t){let n=this.__v_raw,r=F(n),i=F(t);return e||(k(t,i)&&N(r,`has`,t),N(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=F(a),s=t?bt:e?Ut:Ht;return!e&&N(o,`iterate`,Ze),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Ct(`add`),set:Ct(`set`),delete:Ct(`delete`),clear:Ct(`clear`)}:{add(e){let n=F(this),r=xt(n),i=F(e),a=!t&&!P(e)&&!zt(e)?i:e;return r.has.call(n,a)||k(e,a)&&r.has.call(n,e)||k(i,a)&&r.has.call(n,i)||(n.add(a),et(n,`add`,a,a)),this},set(e,n){!t&&!P(n)&&!zt(n)&&(n=F(n));let r=F(this),{has:i,get:a}=xt(r),o=i.call(r,e);o||(e=F(e),o=i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?k(n,s)&&et(r,`set`,e,n,s):et(r,`add`,e,n),this},delete(e){let t=F(this),{has:n,get:r}=xt(t),i=n.call(t,e);i||(e=F(e),i=n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&et(t,`delete`,e,void 0,a),o},clear(){let e=F(this),t=e.size!==0,n=e.clear();return t&&et(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=St(r,e,t)}),n}function Tt(e,t){let n=wt(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var Et={get:Tt(!1,!1)},Dt={get:Tt(!1,!0)},Ot={get:Tt(!0,!1)},kt=new WeakMap,At=new WeakMap,jt=new WeakMap,Mt=new WeakMap;function Nt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function Pt(e){return zt(e)?e:Lt(e,!1,_t,Et,kt)}function Ft(e){return Lt(e,!1,yt,Dt,At)}function It(e){return Lt(e,!0,vt,Ot,jt)}function Lt(e,t,n,r,i){if(!y(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let a=i.get(e);if(a)return a;let o=Nt(C(e));if(o===0)return e;let s=new Proxy(e,o===2?r:n);return i.set(e,s),s}function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function P(e){return!!(e&&e.__v_isShallow)}function Bt(e){return e?!!e.__v_raw:!1}function F(e){let t=e&&e.__v_raw;return t?F(t):e}function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&ae(e,`__v_skip`,!0),e}var Ht=e=>y(e)?Pt(e):e,Ut=e=>y(e)?It(e):e;function I(e){return e?e.__v_isRef===!0:!1}function Wt(e){return Kt(e,!1)}function Gt(e){return Kt(e,!0)}function Kt(e,t){return I(e)?e:new qt(e,t)}var qt=class{constructor(e,t){this.dep=new Je,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:F(e),this._value=t?e:Ht(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?e:F(e),k(e,t)&&(this._rawValue=e,this._value=n?e:Ht(e),this.dep.trigger())}};function L(e){return I(e)?e.value:e}var Jt={get:(e,t,n)=>t===`__v_raw`?e:L(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return I(i)&&!I(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return Rt(e)?e:new Proxy(e,Jt)}var Xt=class{constructor(e){this.__v_isRef=!0,this._value=void 0;let t=this.dep=new Je,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}};function Zt(e){return new Xt(e)}var Qt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Je(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ke-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return Me(this,!0),!0}get value(){let e=this.dep.track();return Re(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function $t(e,t,n=!1){let r,i;return g(e)?r=e:(r=e.get,i=e.set),new Qt(r,i,n)}var en={},tn=new WeakMap,nn=void 0;function rn(e,t=!1,n=nn){if(n){let t=tn.get(n);t||tn.set(n,t=[]),t.push(e)}}function an(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:P(e)||o===!1||o===0?on(e,1):on(e),m,h,_,v,y=!1,b=!1;if(I(e)?(h=()=>e.value,y=P(e)):Rt(e)?(h=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Rt(e)||P(e)),h=()=>e.map(e=>{if(I(e))return e.value;if(Rt(e))return p(e);if(g(e))return f?f(e,2):e()})):h=g(e)?n?f?()=>f(e,2):e:()=>{if(_){Ue();try{_()}finally{We()}}let t=nn;nn=m;try{return f?f(e,3,[v]):e(v)}finally{nn=t}}:r,n&&o){let e=h,t=o===!0?1/0:o;h=()=>on(e(),t)}let x=Ee(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{let n=e(...t);return S(),n}}let C=b?Array(e.length).fill(en):en,w=e=>{if(m.flags&1&&(m.dirty||e)){if(n){let t=m.run();if(e||o||y||(b?t.some((e,t)=>k(e,C[t])):k(t,C))){_&&_();let e=nn;nn=m;try{let e=[t,C===en?void 0:b&&C[0]===en?[]:C,v];C=t,f?f(n,3,e):n(...e)}finally{nn=e}}}else m.run()}};return u&&u(w),m=new Oe(h),m.scheduler=l?()=>l(w,!1):w,v=e=>rn(e,!1,m),_=m.onStop=()=>{let e=tn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();tn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function on(e,t=1/0,n){if(t<=0||!y(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,I(e))on(e.value,t,n);else if(d(e))for(let r=0;r{on(e,t,n)});else if(w(e)){for(let r in e)on(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&on(e[r],t,n)}return e}function sn(e,t,n,r){try{return r?e(...r):e()}catch(e){cn(e,t,n)}}function R(e,t,n,r){if(g(e)){let i=sn(e,t,n,r);return i&&b(i)&&i.catch(e=>{cn(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a>>1,i=z[r],a=Cn(i);a=Cn(n)?z.push(e):z.splice(_n(t),0,e),e.flags|=1,yn()}}function yn(){hn||(hn=mn.then(wn))}function bn(e){if(!d(e))fn&&e.id===-1?fn.splice(pn+1,0,e):e.flags&1||(dn.push(e),e.flags|=1);else for(let t=0;tCn(e)-Cn(t));if(dn.length=0,fn){for(let t=0;te.id==null?e.flags&2?-1:1/0:e.id;function wn(e){try{for(un=0;un{r._d&&pa(-1);let i=En(t),a=la.length,o;try{o=e(...n)}finally{for(let e=la.length;e>a;e--)da();En(i),r._d&&pa(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function On(e,n){if(B===null)return e;let r=qa(B),i=e.dirs||(e.dirs=[]);for(let e=0;e1)return n&&g(t)?t.call(r&&r.proxy):t}}var jn=Symbol.for(`v-scx`),Mn=()=>V(jn);function Nn(e,t){return In(e,null,t)}function Pn(e,t){return In(e,null,{flush:`sync`})}function Fn(e,t,n){return In(e,t,n)}function In(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ba){if(c===`sync`){let e=Mn();f=e.__watcherHandles||(e.__watcherHandles=[])}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=X;u.call=(e,t,n)=>R(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{G(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():vn(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=an(e,n,u);return Ba&&(f?f.push(h):d&&h()),h}function Ln(e,t,n){let r=this.proxy,i=_(e)?e.includes(`.`)?Rn(r,e):()=>r[e]:e.bind(r,r),a;g(t)?a=t:(a=t.handler,n=t);let o=La(this),s=In(i,a.bind(r),n);return o(),s}function Rn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;ee.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===``),Un=e=>e&&(e.defer||e.defer===``),Wn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Gn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Kn=(e,t)=>{let n=e&&e.to;return _(n)?t?t(n):null:n},qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g,parentNode:_}}=l,v=Hn(t.props),{dynamicChildren:y}=t,b=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},x=(e=t)=>{let n=Hn(e.props),r=e.target=Kn(e.props,m),a=Qn(r,e,h,p);r&&(o!==`svg`&&Wn(r)?o=`svg`:o!==`mathml`&&Gn(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(b(e,r,a),Zn(e,!1)))},S=e=>{let t=()=>{if(zn.get(e)===t){if(zn.delete(e),Hn(e.props)){let t=_(e.el)||n;b(e,t,e.anchor),Zn(e,!0)}x(e)}};zn.set(e,t),G(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Un(t.props)||a&&a.pendingBranch){S(t);return}v&&(b(t,n,i),Zn(t,!0)),x()}else{t.el=e.el;let r=t.anchor=e.anchor,u=zn.get(e);if(u){u.flags|=8,zn.delete(e),S(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Hn(e.props),_=g?n:p,b=g?r:h;if(o===`svg`||Wn(p)?o=`svg`:(o===`mathml`||Gn(p))&&(o=`mathml`),y?(f(e.dynamicChildren,y,_,i,a,o,s),$i(e,t,!0)):c||d(e,t,_,b,i,a,o,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Jn(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=Kn(t.props,m);e&&(t.target=e,Jn(t,e,null,l,0))}else g&&Jn(t,p,h,l,1);Zn(t,v)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=Hn(f),m=a||!p,h=zn.get(e);if(h&&(h.flags|=8,zn.delete(e)),d&&(i(l),i(u)),a&&i(c),!h&&(p||d)&&o&16)for(let e=0;e{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}var U=[Function,Array],tr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:U,onEnter:U,onAfterEnter:U,onEnterCancelled:U,onBeforeLeave:U,onLeave:U,onAfterLeave:U,onLeaveCancelled:U,onBeforeAppear:U,onAppear:U,onAfterAppear:U,onAppearCancelled:U},nr=e=>{let t=e.subTree;return t.component?nr(t.component):t},rr={name:`BaseTransition`,props:tr,setup(e,{slots:t}){let n=Pa(),r=er();return()=>{let i=t.default&&dr(t.default(),!0),a=i&&i.length?ir(i):n.subTree?Ea():void 0;if(!a)return;let o=F(e),{mode:s}=o;if(r.isLeaving)return cr(a);let c=lr(a);if(!c)return cr(a);let l=sr(c,o,r,n,e=>l=e);c.type!==K&&ur(c,l);let u=n.subTree&&lr(n.subTree);if(u&&u.type!==K&&!va(u,c)&&nr(n).type!==K){let e=sr(u,o,r,n);if(ur(u,e),s===`out-in`&&c.type!==K)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},cr(a);s===`in-out`&&c.type!==K?e.delayLeave=(e,t,n)=>{let i=or(r,u);i[String(u.key)]=u,e[H]=()=>{t(),e[H]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return a}}};function ir(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==K){t=n;break}}return t}var ar=rr;function or(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function sr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=or(n,e),C=(e,t)=>{e&&R(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted){if(a)r=_||c;else return}t[H]&&t[H](!0);let i=S[x];i&&va(e,i)&&i.el[H]&&i.el[H](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted){if(a)r=v||l,i=y||u,o=b||f;else return}let s=!1;t[$n]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[$n]=void 0)};let c=t[$n].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[$n]&&t[$n](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[H]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[H]=void 0,S[i]===e&&delete S[i])};let o=t[H].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=sr(e,t,n,r,i);return i&&i(a),a}};return T}function cr(e){if(yr(e))return e=wa(e),e.children=null,e}function lr(e){if(!yr(e))return Vn(e.type)&&e.children?ir(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&g(n.default))return n.default()}}function ur(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;let n=e.component.subTree;ur(Vn(n.type)&&lr(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function dr(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;egr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(vr(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&gr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?qa(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,h=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=F(v),b=v===t?i:e=>!mr(h,e)&&u(y,e),x=(e,t)=>!(t&&mr(h,t));if(m!=null&&m!==p){if(_r(n),_(m))h[m]=null,b(m)&&(v[m]=null);else if(I(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(h[e.k]=null)}}if(g(p))sn(p,f,12,[l,h]);else{let t=_(p),n=I(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:h[p]:x(p)||!e.k?p.value:h[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)h[p]=[s],b(p)&&(v[p]=h[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(h[e.k]=t)}}else t?(h[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(h[e.k]=l))};if(l){let t=()=>{i(),hr.delete(e)};t.id=-1,hr.set(e,t),G(t,r)}else _r(e),i()}}}function _r(e){let t=hr.get(e);t&&(t.flags|=8,hr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var vr=e=>!!e.type.__asyncLoader,yr=e=>e.type.__isKeepAlive,br={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=Pa(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),G(()=>{a.isDeactivated=!1,a.a&&A(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Y(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;na(t.m),na(t.a),l(e,f,null,1,s),G(()=>{t.da&&A(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Y(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Er(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=Ja(vr(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!va(t,o))?p(t):o&&Er(o),i.delete(e),a.delete(e)}Fn(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>xr(e,t)),t&&m(e=>!xr(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(ia(n.subTree.type)?G(()=>{i.set(g,Dr(n.subTree))},n.subTree.suspense):i.set(g,Dr(n.subTree)))};return jr(_),Nr(_),Pr(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Dr(t);if(e.type===i.type&&e.key===i.key){Er(i);let e=i.component.da;e&&G(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!_a(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Dr(r);if(s.type===K)return o=null,s;let c=s.type,l=Ja(vr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!xr(u,l))||d&&l&&xr(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=wa(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&ur(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,ia(r.type)?r:s}}};function xr(e,t){return d(e)?e.some(e=>xr(e,t)):_(e)?e.split(`,`).includes(t):h(e)?(e.lastIndex=0,e.test(t)):!1}function Sr(e,t){wr(e,`a`,t)}function Cr(e,t){wr(e,`da`,t)}function wr(e,t,n=X){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Or(t,r,n),n){let e=n.parent;for(;e&&e.parent;)yr(e.parent.vnode)&&Tr(r,t,n,e),e=e.parent}}function Tr(e,t,n,r){let i=Or(t,e,r,!0);Fr(()=>{c(r[t],i)},n)}function Er(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Dr(e){return e.shapeFlag&128?e.ssContent:e}function Or(e,t,n=X,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||(t.__weh=(...r)=>{Ue();let i=La(n),a=R(t,n,e,r);return i(),We(),a});return r?i.unshift(a):i.push(a),a}}var kr=e=>(t,n=X)=>{(!Ba||e===`sp`)&&Or(e,(...e)=>t(...e),n)},Ar=kr(`bm`),jr=kr(`m`),Mr=kr(`bu`),Nr=kr(`u`),Pr=kr(`bum`),Fr=kr(`um`),Ir=kr(`sp`),Lr=kr(`rtg`),Rr=kr(`rtc`);function zr(e,t=X){Or(`ec`,e,t)}var Br=`components`;function Vr(e,t){return Wr(Br,e,!0,t)||e}var Hr=Symbol.for(`v-ndc`);function Ur(e){return _(e)?Wr(Br,e,!1)||e:e||Hr}function Wr(e,t,n=!0,r=!1){let i=B||X;if(i){let n=i.type;if(e===Br){let e=Ja(n,!1);if(e&&(e===t||e===D(t)||e===re(D(t))))return n}let a=Gr(i[e]||n[e],t)||Gr(i.appContext[e],t);return!a&&r?n:a}}function Gr(e,t){return e&&(e[t]||e[D(t)]||e[re(D(t))])}function Kr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||_(e)){let n=o&&Rt(e),r=!1,s=!1;n&&(r=!P(e),s=zt(e),e=nt(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}var Jr=e=>e?za(e)?qa(e):Jr(e.parent):null,Yr=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{vn(e.update)}),$nextTick:e=>e.n||(e.n=gn.bind(e.proxy)),$watch:e=>Ln.bind(e)}),Xr=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),Zr={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(Xr(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else $r&&(s[n]=0)}let d=Yr[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return Xr(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||Xr(n,c)||u(o,c)||u(i,c)||u(Yr,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function Qr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var $r=!0;function ei(e){let t=ii(e),n=e.proxy,i=e.ctx;$r=!1,t.beforeCreate&&ni(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:h,updated:_,activated:v,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:E,errorCaptured:te,serverPrefetch:D,expose:ne,inheritAttrs:O,components:re,directives:ie,filters:k}=t;if(u&&ti(u,i,null),s)for(let e in s){let t=s[e];g(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);y(t)&&(e.data=Pt(t))}if($r=!0,o)for(let e in o){let t=o[e],a=Z({get:g(t)?t.bind(n,n):g(t.get)?t.get.bind(n,n):r,set:!g(t)&&g(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)ri(c[e],i,n,e);if(l){let e=g(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{An(t,e[t])})}f&&ni(f,e,`c`);function A(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(A(Ar,p),A(jr,m),A(Mr,h),A(Nr,_),A(Sr,v),A(Cr,b),A(zr,te),A(Rr,ee),A(Lr,E),A(Pr,S),A(Fr,w),A(Ir,D),d(ne)){if(ne.length){let t=e.exposed||(e.exposed={});ne.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={})}T&&e.render===r&&(e.render=T),O!=null&&(e.inheritAttrs=O),re&&(e.components=re),ie&&(e.directives=ie),D&&pr(e)}function ti(e,t,n=r){d(e)&&(e=li(e));for(let n in e){let r=e[n],i;i=y(r)?`default`in r?V(r.from||n,r.default,!0):V(r.from||n):V(r),I(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function ni(e,t,n){R(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function ri(e,t,n,r){let i=r.includes(`.`)?Rn(n,r):()=>n[r];if(_(e)){let n=t[e];g(n)&&Fn(i,n)}else if(g(e))Fn(i,e.bind(n));else if(y(e)){if(d(e))e.forEach(e=>ri(e,t,n,r));else{let r=g(e.handler)?e.handler.bind(n):t[e.handler];g(r)&&Fn(i,r,e)}}}function ii(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>ai(c,e,o,!0)),ai(c,t,o)),y(t)&&a.set(t,c),c}function ai(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&ai(e,a,n,!0),i&&i.forEach(t=>ai(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=oi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var oi={data:si,props:di,emits:di,methods:ui,computed:ui,beforeCreate:W,created:W,beforeMount:W,mounted:W,beforeUpdate:W,updated:W,beforeDestroy:W,beforeUnmount:W,destroyed:W,unmounted:W,activated:W,deactivated:W,errorCaptured:W,serverPrefetch:W,components:ui,directives:ui,watch:fi,provide:si,inject:ci};function si(e,t){return t?e?function(){return s(g(e)?e.call(this,this):e,g(t)?t.call(this,this):t)}:t:e}function ci(e,t){return ui(li(e),li(t))}function li(e){if(d(e)){let t={};for(let n=0;n{let l,u=t,d;return Pn(()=>{let t=e[a];k(l,t)&&(l=t,c())}),{get(){return s(),r.get?r.get(l):l},set(e){let s=r.set?r.set(e):e;if(!k(s,l)&&!(u!==t&&k(e,u)))return;let f=i.vnode.props,p=!!(f&&(n in f||a in f||o in f)&&(`onUpdate:${n}`in f||`onUpdate:${a}`in f||`onUpdate:${o}`in f));p||(l=e,c()),i.emit(`update:${n}`,s),k(e,u)&&(k(e,s)&&!k(s,d)||p&&u!==t&&!k(s,l))&&c(),u=e,d=s}}});return c[Symbol.iterator]=()=>{let e=0;return{next(){return e<2?{value:e++?s||t:c,done:!1}:{done:!0}}}},c}var vi=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${D(t)}Modifiers`]||e[`${O(t)}Modifiers`];function yi(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&vi(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>_(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(D(n))];!l&&o&&(l=i[c=ie(O(n))]),l&&R(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,R(u,e,6,a)}}var bi=new WeakMap;function xi(e,t,n=!1){let r=n?bi:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!g(e)){let r=e=>{let n=xi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(y(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),y(e)&&r.set(e,o),o)}function Si(e,t){return!e||!a(t)?!1:(t=t.slice(2),t=t===`Once`?t:t.replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,O(t))||u(e,t))}function Ci(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=En(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Da(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Da(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:wi(c)}}catch(t){la.length=0,cn(t,e,1),v=J(K)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ti(y,a)),b=wa(b,y,!1,!0))}return n.dirs&&(b=wa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&ur(Vn(b.type)&&lr(b)||b,n.transition),v=b,En(_),v}var wi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Ei(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Di(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;tObject.create(Ai),Mi=e=>Object.getPrototypeOf(e)===Ai;function Ni(e,t,n,r=!1){let i={},a=ji();e.propsDefaults=Object.create(null),Fi(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);e.props=n?r?i:Ft(i):e.type.props?i:a,e.attrs=a}function Pi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{p=!0;let[t,n]=Ri(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return y(e)&&a.set(e,n),n;if(d(c))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Vi=e=>d(e)?e.map(Da):[Da(e)],Hi=(e,t,n)=>{if(t._n)return t;let r=Dn((...e)=>Vi(t(...e)),n);return r._c=!1,r},Ui=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Bi(n))continue;let i=e[n];if(g(i))t[n]=Hi(n,i,r);else if(i!=null){let e=Vi(i);t[n]=()=>e}}},Wi=(e,t)=>{let n=Vi(t);e.slots.default=()=>n},Gi=(e,t,n)=>{for(let r in t)(n||!Bi(r))&&(e[r]=t[r])},Ki=(e,t,n)=>{let r=e.slots=ji();if(e.vnode.shapeFlag&32){let e=t._;e?(Gi(r,t,n),n&&ae(r,`_`,e,!0)):Ui(t,r)}else t&&Wi(e,t)},qi=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:Gi(a,n,r):(o=!n.$stable,Ui(n,a)),s=n}else n&&(Wi(e,n),s={default:1});if(o)for(let e in a)!Bi(e)&&s[e]==null&&delete a[e]},G=aa;function Ji(e){return Yi(e)}function Yi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!va(e,t)&&(r=ve(e),pe(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case sa:y(e,t,n,r);break;case K:b(e,t,n,r);break;case ca:e==null&&x(t,n,r,o);break;case oa:re(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?ie(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,xe)}u!=null&&i?gr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&gr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)T(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),D(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},T=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&te(e.children,d,null,r,i,Xi(e,a),s,u),_&&kn(e,null,r,`created`),E(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!ee(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Y(f,r,e)}_&&kn(e,null,r,`beforeMount`);let v=Qi(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&G(()=>{try{f&&Y(f,r,e),v&&g.enter(d),_&&kn(e,null,r,`mounted`)}finally{}},i)},E=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t{for(let l=c;l{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&Zi(r,!1),(g=h.onVnodeBeforeUpdate)&&Y(g,r,n,e),f&&kn(n,e,r,`beforeUpdate`),r&&Zi(r,!0),d&&(!e.dynamicChildren||e.dynamicChildren.length!==d.length)&&(u=0,s=!1,d=null),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?ne(e.dynamicChildren,d,l,r,i,Xi(n,a),o):s||ce(e,n,l,null,r,i,Xi(n,a),o,!1),u>0){if(u&16)O(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t{g&&Y(g,r,n,e),f&&kn(n,e,r,`updated`)},i)},ne=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(n!==r){if(n!==t)for(let t in n)!ee(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(ee(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},re=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),te(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(ne(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&$i(e,t,!0)):ce(e,t,n,f,i,a,s,c,l)},ie=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):ae(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Na(e,r,i);if(yr(e)&&(s.ctx.renderer=xe),Va(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,oe,o),!e.el){let r=s.subTree=J(K);b(null,r,t,n),e.placeholder=r.el}}else oe(s,e,t,n,i,a,o)},ae=(e,t,n)=>{let r=t.component=e.component;if(Ei(e,t,n)){if(r.asyncDep&&!r.asyncResolved){se(r,t,n);return}r.next=t,r.update()}else t.el=e.el,r.vnode=t},oe=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=ta(e);if(n){t&&(t.el=c.el,se(e,t,o)),n.asyncDep.then(()=>{G(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;Zi(e,!1),t?(t.el=c.el,se(e,t,o)):t=c,n&&A(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Y(d,s,t,c),Zi(e,!0);let f=Ci(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ve(p),e,i,a),t.el=f.el,u===null&&ki(e,f.el),r&&G(r,i),(d=t.props&&t.props.onVnodeUpdated)&&G(()=>Y(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=vr(t);if(Zi(e,!1),l&&A(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Y(o,d,t),Zi(e,!0),s&&Ce){let t=()=>{e.subTree=Ci(e),Ce(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Ci(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&G(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;G(()=>Y(o,d,e),i)}(t.shapeFlag&256||d&&vr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&G(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Oe(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>vn(u),Zi(e,!0),l()},se=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Pi(e,t.props,r,n),qi(e,t.children,n),Ue(),xn(e),We()},ce=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){de(l,d,n,r,i,a,o,s,c);return}if(f&256){ue(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&_e(l,i,a),d!==l&&p(n,d)):u&16?m&16?de(l,d,n,r,i,a,o,s,c):_e(l,i,a,!0):(u&8&&p(n,``),m&16&&te(d,n,r,i,a,o,s,c))},ue=(e,t,r,i,a,o,s,c,l)=>{e=e||n,t=t||n;let u=e.length,d=t.length,f=Math.min(u,d),p=0;for(;pd?_e(e,a,o,!0,!1,f):te(t,r,i,a,o,s,c,l,f)},de=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?Oa(t[u]):Da(t[u]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?Oa(t[p]):Da(t[p]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=ep)for(;u<=f;)pe(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?Oa(t[u]):Da(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u=b){pe(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&va(n,t[_])){i=_;break}i===void 0?pe(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?ea(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){fe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,xe);return}if(c===oa){o(a,t,n);for(let e=0;el.enter(a),i));else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{let e=a._isLeaving||!!a[H];a._isLeaving&&a[H](!0),l.persisted&&!e?u():r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}}else o(a,t,n)},pe=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ue(),gr(s,null,n,e,!0),We()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!vr(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Y(_,t,e),u&6)ge(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&kn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,xe,r):l&&!l.hasOnce&&(a!==oa||d>0&&d&64)?_e(l,t,n,!1,!0):(a===oa&&d&384||!i&&u&16)&&_e(c,t,n),r&&me(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&G(()=>{_&&Y(_,t,e),h&&kn(e,null,t,`unmounted`),v&&(e.el=null)},n)},me=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===oa){he(n,r);return}if(t===ca){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},he=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},ge=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;na(c),na(l),r&&A(r),i.stop(),a&&(a.flags|=8,pe(o,e,t,n)),s&&G(s,t),G(()=>{e.isUnmounted=!0},t)},_e=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return ve(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Bn];return n?h(n):t},ye=!1,be=(e,t,n)=>{let r;e==null?t._vnode&&(pe(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ye||(ye=!0,xn(r),Sn(),ye=!1)},xe={p:v,um:pe,m:fe,r:me,mt:k,mc:te,pc:ce,pbc:ne,n:ve,o:e},Se,Ce;return i&&([Se,Ce]=i(xe)),{render:be,hydrate:Se,createApp:hi(be,Se)}}function Xi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function Zi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Qi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function $i(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-->0;)n[a]=o,o=t[o];return n}function ta(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ta(t)}function na(e){if(e)for(let t=0;te.__isSuspense;function aa(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):bn(e)}var oa=Symbol.for(`v-fgt`),sa=Symbol.for(`v-txt`),K=Symbol.for(`v-cmt`),ca=Symbol.for(`v-stc`),la=[],q=null;function ua(e=!1){la.push(q=e?null:[])}function da(){la.pop(),q=la[la.length-1]||null}var fa=1;function pa(e,t=!1){fa+=e,e<0&&q&&t&&(q.hasOnce=!0)}function ma(e){return e.dynamicChildren=fa>0?q||n:null,da(),fa>0&&q&&q.push(e),e}function ha(e,t,n,r,i,a){return ma(xa(e,t,n,r,i,a,!0))}function ga(e,t,n,r,i){return ma(J(e,t,n,r,i,!0))}function _a(e){return e?e.__v_isVNode===!0:!1}function va(e,t){return e.type===t.type&&e.key===t.key}var ya=({key:e})=>e==null?null:e,ba=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:_(e)||I(e)||g(e)?{i:B,r:e,k:t,f:!!n}:e);function xa(e,t=null,n=null,r=0,i=null,a=e===oa?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ya(t),ref:t&&ba(t),scopeId:Tn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:B};return s?(ka(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=_(n)?8:16),fa>0&&!o&&q&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&q.push(c),c}var J=Sa;function Sa(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Hr)&&(e=K),_a(e)){let r=wa(e,t,!0);return n&&ka(r,n),fa>0&&!a&&q&&(r.shapeFlag&6?q[q.indexOf(e)]=r:q.push(r)),r.patchFlag=-2,r}if(Ya(e)&&(e=e.__vccOpts),t){t=Ca(t);let{class:e,style:n}=t;e&&!_(e)&&(t.class=he(e)),y(n)&&(Bt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=_(e)?1:ia(e)?128:Vn(e)?64:y(e)?4:g(e)?2:0;return xa(e,t,n,r,i,o,a,!0)}function Ca(e){return e?Bt(e)||Mi(e)?s({},e):e:null}function wa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Aa(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ya(l),ref:t&&t.ref?n&&a?d(a)?a.concat(ba(t)):[a,ba(t)]:ba(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==oa?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&wa(e.ssContent),ssFallback:e.ssFallback&&wa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&ur(u,c.clone(u)),u}function Ta(e=` `,t=0){return J(sa,null,e,t)}function Ea(e=``,t=!1){return t?(ua(),ga(K,null,e)):J(K,null,e)}function Da(e){return e==null||typeof e==`boolean`?J(K):d(e)?J(oa,null,e.slice()):_a(e)?Oa(e):J(sa,null,String(e))}function Oa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:wa(e)}function ka(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`){if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),ka(e,n()),n._c&&(n._d=!0));return}{n=32;let r=t._;!r&&!Mi(t)?t._ctx=B:r===3&&B&&(B.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}}else if(g(t)){if(r&65){ka(e,{default:t});return}t={default:t,_ctx:B},n=32}else t=String(t),r&64?(n=16,t=[Ta(t)]):n=8;e.children=t,e.shapeFlag|=n}function Aa(...e){let t={};for(let n=0;nX||B,Fa,Ia;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Fa=t(`__VUE_INSTANCE_SETTERS__`,e=>X=e),Ia=t(`__VUE_SSR_SETTERS__`,e=>Ba=e)}var La=e=>{let t=X;return Fa(e),e.scope.on(),()=>{e.scope.off(),Fa(t)}},Ra=()=>{X&&X.scope.off(),Fa(null)};function za(e){return e.vnode.shapeFlag&4}var Ba=!1;function Va(e,t=!1,n=!1){t&&Ia(t);let{props:r,children:i}=e.vnode,a=za(e);Ni(e,r,a,t),Ki(e,i,n||t);let o=a?Ha(e,t):void 0;return t&&Ia(!1),o}function Ha(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zr);let{setup:r}=n;if(r){Ue();let n=e.setupContext=r.length>1?Ka(e):null,i=La(e),a=sn(r,e,0,[e.props,n]),o=b(a);if(We(),i(),(o||e.sp)&&!vr(e)&&pr(e),o){if(a.then(Ra,Ra),t)return a.then(n=>{Ia(!0);try{Ua(e,n,t)}finally{Ia(!1)}}).catch(t=>{cn(t,e,0)});e.asyncDep=a}else Ua(e,a,t)}else Wa(e,t)}function Ua(e,t,n){g(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:y(t)&&(e.setupState=Yt(t)),Wa(e,n)}function Wa(e,t,n){let i=e.type;e.render||(e.render=i.render||r);{let t=La(e);Ue();try{ei(e)}finally{We(),t()}}}var Ga={get(e,t){return N(e,`get`,``),e[t]}};function Ka(e){return{attrs:new Proxy(e.attrs,Ga),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function qa(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Vt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yr)return Yr[n](e)},has(e,t){return t in e||t in Yr}})):e.proxy}function Ja(e,t=!0){return g(e)?e.displayName||e.name:e.name||t&&e.__name}function Ya(e){return g(e)&&`__vccOpts`in e}var Z=(e,t)=>$t(e,t,Ba);function Xa(e,t,n){try{pa(-1);let r=arguments.length;return r===2?y(t)&&!d(t)?_a(t)?J(e,null,[t]):J(e,t):J(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_a(n)&&(n=[n]),J(e,t,n))}finally{pa(1)}}var Za=`3.5.41`,Qa=void 0,$a=typeof window<`u`&&window.trustedTypes;if($a)try{Qa=$a.createPolicy(`vue`,{createHTML:e=>e})}catch{}var eo=Qa?e=>Qa.createHTML(e):e=>e,to=`http://www.w3.org/2000/svg`,no=`http://www.w3.org/1998/Math/MathML`,ro=typeof document<`u`?document:null,io=ro&&ro.createElement(`template`),ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i=t===`svg`?ro.createElementNS(to,e):t===`mathml`?ro.createElementNS(no,e):n?ro.createElement(e,{is:n}):ro.createElement(e);return e===`select`&&r&&r.multiple!=null&&i.setAttribute(`multiple`,r.multiple),i},createText:e=>ro.createTextNode(e),createComment:e=>ro.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ro.querySelector(e),setScopeId(e,t){e.setAttribute(t,``)},insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;if(i&&(i===a||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==a&&(i=i.nextSibling););else{io.innerHTML=eo(r===`svg`?`${e}`:r===`mathml`?`${e}`:e);let i=io.content;if(r===`svg`||r===`mathml`){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},oo=`transition`,so=`animation`,co=Symbol(`_vtc`),lo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uo=s({},tr,lo),fo=(e=>(e.displayName=`Transition`,e.props=uo,e))((e,{slots:t})=>Xa(ar,ho(e),t)),po=(e,t=[])=>{d(e)?e.forEach(e=>e(...t)):e&&e(...t)},mo=e=>e?d(e)?e.some(e=>e.length>1):e.length>1:!1;function ho(e){let t={};for(let n in e)n in lo||(t[n]=e[n]);if(e.css===!1)return t;let{name:n=`v`,type:r,duration:i,enterFromClass:a=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=a,appearActiveClass:u=o,appearToClass:d=c,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,h=go(i),g=h&&h[0],_=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:C=v,onAppear:w=y,onAppearCancelled:T=b}=t,ee=(e,t,n,r)=>{e._enterCancelled=r,yo(e,t?d:c),yo(e,t?u:o),n&&n()},E=(e,t)=>{e._isLeaving=!1,yo(e,f),yo(e,m),yo(e,p),t&&t()},te=e=>(t,n)=>{let i=e?w:y,o=()=>ee(t,e,n);po(i,[t,o]),bo(()=>{yo(t,e?l:a),vo(t,e?d:c),mo(i)||So(t,r,g,o)})};return s(t,{onBeforeEnter(e){po(v,[e]),vo(e,a),vo(e,o)},onBeforeAppear(e){po(C,[e]),vo(e,l),vo(e,u)},onEnter:te(!1),onAppear:te(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>E(e,t);vo(e,f),e._enterCancelled?(vo(e,p),Eo(e)):(Eo(e),vo(e,p)),bo(()=>{e._isLeaving&&(yo(e,f),vo(e,m),mo(x)||So(e,r,_,n))}),po(x,[e,n])},onEnterCancelled(e){ee(e,!1,void 0,!0),po(b,[e])},onAppearCancelled(e){ee(e,!0,void 0,!0),po(T,[e])},onLeaveCancelled(e){E(e),po(S,[e])}})}function go(e){if(e==null)return null;if(y(e))return[_o(e.enter),_o(e.leave)];{let t=_o(e);return[t,t]}}function _o(e){return se(e)}function vo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[co]||(e[co]=new Set)).add(t)}function yo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[co];n&&(n.delete(t),n.size||(e[co]=void 0))}function bo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}var xo=0;function So(e,t,n,r){let i=e._endId=++xo,a=()=>{i===e._endId&&r()};if(n!=null)return setTimeout(a,n);let{type:o,timeout:s,propCount:c}=Co(e,t);if(!o)return r();let l=o+`end`,u=0,d=()=>{e.removeEventListener(l,f),a()},f=t=>{t.target===e&&++u>=c&&d()};setTimeout(()=>{u(n[e]||``).split(`, `),i=r(`${oo}Delay`),a=r(`${oo}Duration`),o=wo(i,a),s=r(`${so}Delay`),c=r(`${so}Duration`),l=wo(s,c),u=null,d=0,f=0;t===oo?o>0&&(u=oo,d=o,f=a.length):t===so?l>0&&(u=so,d=l,f=c.length):(d=Math.max(o,l),u=d>0?o>l?oo:so:null,f=u?u===oo?a.length:c.length:0);let p=u===oo&&/\b(?:transform|all)(?:,|$)/.test(r(`${oo}Property`).toString());return{type:u,timeout:d,propCount:f,hasTransform:p}}function wo(e,t){for(;e.lengthTo(t)+To(e[n])))}function To(e){return e===`auto`?0:Number(e.slice(0,-1).replace(`,`,`.`))*1e3}function Eo(e){return(e?e.ownerDocument:document).body.offsetHeight}function Do(e,t,n){let r=e[co];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==null?e.removeAttribute(`class`):n?e.setAttribute(`class`,t):e.className=t}var Oo=Symbol(`_vod`),ko=Symbol(`_vsh`),Ao={name:`show`,beforeMount(e,{value:t},{transition:n}){e[Oo]=e.style.display===`none`?``:e.style.display,n&&t?n.beforeEnter(e):jo(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),jo(e,!0),r.enter(e)):r.leave(e,()=>{jo(e,!1)}):jo(e,t))},beforeUnmount(e,{value:t}){jo(e,t)}};function jo(e,t){e.style.display=t?e[Oo]:`none`,e[ko]=!t}var Mo=Symbol(``),No=/(?:^|;)\s*display\s*:/;function Po(e,t,n){let r=e.style,i=_(n),a=!1;if(n&&!i){if(t){if(_(t))for(let e of t.split(`;`)){let t=e.slice(0,e.indexOf(`:`)).trim();n[t]==null&&Io(r,t,``)}else for(let e in t)n[e]==null&&Io(r,e,``)}for(let i in n){i===`display`&&(a=!0);let o=n[i];o==null?Io(r,i,``):Bo(e,i,!_(t)&&t?t[i]:void 0,o)||Io(r,i,o)}}else if(i){if(t!==n){let e=r[Mo];e&&(n+=`;`+e),r.cssText=n,a=No.test(n)}}else t&&e.removeAttribute(`style`);Oo in e&&(e[Oo]=a?r.display:``,e[ko]&&(r.display=`none`))}var Fo=/\s*!important$/;function Io(e,t,n){if(d(n))n.forEach(n=>Io(e,t,n));else if(n==null&&(n=``),t.startsWith(`--`))e.setProperty(t,n);else{let r=zo(e,t);Fo.test(n)?e.setProperty(O(r),n.replace(Fo,``),`important`):e[r]=n}}var Lo=[`Webkit`,`Moz`,`ms`],Ro={};function zo(e,t){let n=Ro[t];if(n)return n;let r=D(t);if(r!==`filter`&&r in e)return Ro[t]=r;r=re(r);for(let n=0;nZo||(Qo.then(()=>Zo=0),Zo=Date.now());function es(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=n.attached)return;let r=n.value;if(d(r)){let n=e.stopImmediatePropagation;e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0};let i=r.slice(),a=[e];for(let n=0;ne.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ns=(e,t,n,r,i,s)=>{let c=i===`svg`;t===`class`?Do(e,r,c):t===`style`?Po(e,n,r):a(t)?o(t)||qo(e,t,n,r,s):(t[0]===`.`?(t=t.slice(1),1):t[0]===`^`?(t=t.slice(1),0):rs(e,t,r,c))?(Uo(e,t,r),!e.tagName.includes(`-`)&&(t===`value`||t===`checked`||t===`selected`)&&Ho(e,t,r,c,s,t!==`value`)):e._isVueCE&&(is(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!_(r)))?Uo(e,D(t),r,s,t):(t===`true-value`?e._trueValue=r:t===`false-value`&&(e._falseValue=r),Ho(e,t,r,c))};function rs(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t in e&&ts(t)&&g(n));if(t===`spellcheck`||t===`draggable`||t===`translate`||t===`autocorrect`||t===`sandbox`&&e.tagName===`IFRAME`||t===`form`||t===`list`&&e.tagName===`INPUT`||t===`type`&&e.tagName===`TEXTAREA`)return!1;if(t===`width`||t===`height`){let t=e.tagName;if(t===`IMG`||t===`VIDEO`||t===`CANVAS`||t===`SOURCE`)return!1}return ts(t)&&_(n)?!1:t in e}function is(e,t){let n=e._def.props;if(!n)return!1;let r=D(t);return Array.isArray(n)?n.some(e=>D(e)===r):Object.keys(n).some(e=>D(e)===r)}var as={esc:`escape`,space:` `,up:`arrow-up`,left:`arrow-left`,right:`arrow-right`,down:`arrow-down`,delete:`backspace`},os=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(`.`);return n[r]||(n[r]=(n=>{if(!(`key`in n))return;let r=O(n.key);if(t.some(e=>e===r||as[e]===r))return e(n)}))},ss=s({patchProp:ns},ao),cs;function ls(){return cs||(cs=Ji(ss))}var us=((...e)=>{let t=ls().createApp(...e),{mount:n}=t;return t.mount=e=>{let r=fs(e);if(!r)return;let i=t._component;!g(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent=``);let a=n(r,!1,ds(r));return r instanceof Element&&(r.removeAttribute(`v-cloak`),r.setAttribute(`data-v-app`,``)),a},t});function ds(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLElement==`function`&&e instanceof MathMLElement)return`mathml`}function fs(e){return _(e)?document.querySelector(e):e}function ps(){}var ms=Object.assign,hs=typeof window<`u`,gs=e=>typeof e==`object`&&!!e,_s=e=>e!=null,vs=e=>typeof e==`function`,ys=e=>gs(e)&&vs(e.then)&&vs(e.catch),bs=e=>typeof e==`number`||/^\d+(\.\d+)?$/.test(e),xs=()=>hs?/ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()):!1;function Ss(e,t){let n=t.split(`.`),r=e;return n.forEach(e=>{var t;r=gs(r)?(t=r[e])==null?``:t:``}),r}function Cs(e,t,n){return t.reduce((t,r)=>((!n||e[r]!==void 0)&&(t[r]=e[r]),t),{})}var ws=e=>Array.isArray(e)?e:[e],Ts=[Number,String],Es={type:Boolean,default:!0},Ds=e=>({type:e,required:!0}),Os=e=>({type:Number,default:e}),ks=e=>({type:Ts,default:e}),As=e=>({type:String,default:e}),js=typeof window<`u`;function Ms(e){return js?requestAnimationFrame(e):-1}function Ns(e){js&&cancelAnimationFrame(e)}function Ps(e){Ms(()=>Ms(e))}var Fs=e=>e===window,Is=(e,t)=>({top:0,left:0,right:e,bottom:t,width:e,height:t}),Ls=e=>{let t=L(e);if(Fs(t)){let e=t.innerWidth,n=t.innerHeight;return Is(e,n)}return t!=null&&t.getBoundingClientRect?t.getBoundingClientRect():Is(0,0)};function Rs(e){let t=V(e,null);if(t){let e=Pa(),{link:n,unlink:r,internalChildren:i}=t;return n(e),Fr(()=>r(e)),{parent:t,index:Z(()=>i.indexOf(e))}}return{parent:null,index:Wt(-1)}}function zs(e){let t=[],n=e=>{Array.isArray(e)&&e.forEach(e=>{var r;_a(e)&&(t.push(e),(r=e.component)!=null&&r.subTree&&(t.push(e.component.subTree),n(e.component.subTree.children)),e.children&&n(e.children))})};return n(e),t}var Bs=(e,t)=>{let n=e.indexOf(t);return n===-1?e.findIndex(e=>t.key!==void 0&&t.key!==null&&e.type===t.type&&e.key===t.key):n};function Vs(e,t,n){let r=zs(e.subTree.children);n.sort((e,t)=>Bs(r,e.vnode)-Bs(r,t.vnode));let i=n.map(e=>e.proxy);t.sort((e,t)=>i.indexOf(e)-i.indexOf(t))}function Hs(e){let t=Pt([]),n=Pt([]),r=Pa();return{children:t,linkChildren:i=>{An(e,Object.assign({link:e=>{e.proxy&&(n.push(e),t.push(e.proxy),Vs(r,t,n))},unlink:e=>{let r=n.indexOf(e);t.splice(r,1),n.splice(r,1)},children:t,internalChildren:n},i))}}}function Us(e){let t;jr(()=>{e(),gn(()=>{t=!0})}),Sr(()=>{t&&e()})}function Ws(e,t,n={}){if(!js)return;let{target:r=window,passive:i=!1,capture:a=!1}=n,o=!1,s,c=n=>{if(o)return;let r=L(n);r&&!s&&(r.addEventListener(e,t,{capture:a,passive:i}),s=!0)},l=n=>{if(o)return;let r=L(n);r&&s&&(r.removeEventListener(e,t,a),s=!1)};Fr(()=>l(r)),Cr(()=>l(r)),Us(()=>c(r));let u;return I(r)&&(u=Fn(r,(e,t)=>{l(t),c(e)})),()=>{u==null||u(),l(r),o=!0}}var Gs,Ks;function qs(){if(!Gs&&(Gs=Wt(0),Ks=Wt(0),js)){let e=()=>{Gs.value=window.innerWidth,Ks.value=window.innerHeight};e(),window.addEventListener(`resize`,e,{passive:!0}),window.addEventListener(`orientationchange`,e,{passive:!0})}return{width:Gs,height:Ks}}var Js=/scroll|auto|overlay/i,Ys=js?window:void 0;function Xs(e){return e.tagName!==`HTML`&&e.tagName!==`BODY`&&e.nodeType===1}function Zs(e,t=Ys){let n=e;for(;n&&n!==t&&Xs(n);){let{overflowY:e}=window.getComputedStyle(n);if(Js.test(e))return n;n=n.parentNode}return t}function Qs(e,t=Ys){let n=Wt();return jr(()=>{e.value&&(n.value=Zs(e.value,t))}),n}var $s;function ec(){if(!$s&&($s=Wt(`visible`),js)){let e=()=>{$s.value=document.hidden?`hidden`:`visible`};e(),window.addEventListener(`visibilitychange`,e)}return $s}var tc=Symbol(`van-field`);function nc(e){let t=`scrollTop`in e?e.scrollTop:e.pageYOffset;return Math.max(t,0)}function rc(e,t){`scrollTop`in e?e.scrollTop=t:e.scrollTo(e.scrollX,t)}function ic(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}function ac(e){rc(window,e),rc(document.body,e)}function oc(e,t){if(e===window)return 0;let n=t?nc(t):ic();return Ls(e).top+n}var sc=xs();function cc(){sc&&ac(ic())}var lc=e=>e.stopPropagation();function uc(e,t){(typeof e.cancelable!=`boolean`||e.cancelable)&&e.preventDefault(),t&&lc(e)}function dc(e){let t=L(e);if(!t)return!1;let n=window.getComputedStyle(t),r=n.display===`none`,i=t.offsetParent===null&&n.position!==`fixed`;return r||i}var{width:fc,height:pc}=qs();function mc(e){if(_s(e))return bs(e)?`${e}px`:String(e)}function hc(e){if(_s(e)){if(Array.isArray(e))return{width:mc(e[0]),height:mc(e[1])};let t=mc(e);return{width:t,height:t}}}function gc(e){let t={};return e!==void 0&&(t.zIndex=+e),t}var _c;function vc(){if(!_c){let e=document.documentElement,t=e.style.fontSize||window.getComputedStyle(e).fontSize;_c=parseFloat(t)}return _c}function yc(e){return e=e.replace(/rem/g,``),+e*vc()}function bc(e){return e=e.replace(/vw/g,``),+e*fc.value/100}function xc(e){return e=e.replace(/vh/g,``),+e*pc.value/100}function Sc(e){if(typeof e==`number`)return e;if(hs){if(e.includes(`rem`))return yc(e);if(e.includes(`vw`))return bc(e);if(e.includes(`vh`))return xc(e)}return parseFloat(e)}var Cc=/-(\w)/g,wc=e=>e.replace(Cc,(e,t)=>t.toUpperCase()),Tc=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase().replace(/^-/,``),Ec=(e,t,n)=>Math.min(Math.max(e,t),n);function Dc(e,t,n){let r=e.indexOf(t);return r===-1?e:t===`-`&&r!==0?e.slice(0,r):e.slice(0,r+1)+e.slice(r).replace(n,``)}function Oc(e,t=!0,n=!0){e=t?Dc(e,`.`,/\./g):e.split(`.`)[0],e=n?Dc(e,`-`,/-/g):e.replace(/-/,``);let r=t?/[^-0-9.]/g:/[^-0-9]/g;return e.replace(r,``)}var{hasOwnProperty:kc}=Object.prototype;function Ac(e,t,n){let r=t[n];_s(r)&&(e[n]=!kc.call(e,n)||!gs(r)?r:jc(Object(e[n]),r))}function jc(e,t){return Object.keys(t).forEach(n=>{Ac(e,t,n)}),e}var Mc={name:`姓名`,tel:`电话`,save:`保存`,clear:`清空`,undo:`撤销`,cancel:`取消`,confirm:`确认`,delete:`删除`,loading:`加载中...`,noCoupon:`暂无优惠券`,nameEmpty:`请填写姓名`,addContact:`添加联系人`,telInvalid:`请填写正确的电话`,vanCalendar:{end:`结束`,start:`开始`,title:`日期选择`,weekdays:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],monthTitle:(e,t)=>`${e}\u5E74${t}\u6708`,rangePrompt:e=>`\u6700\u591A\u9009\u62E9 ${e} \u5929`},vanCascader:{select:`请选择`},vanPagination:{prev:`上一页`,next:`下一页`},vanPullRefresh:{pulling:`下拉即可刷新...`,loosing:`释放即可刷新...`},vanSubmitBar:{label:`合计:`},vanCoupon:{unlimited:`无门槛`,discount:e=>`${e}\u6298`,condition:e=>`\u6EE1${e}\u5143\u53EF\u7528`},vanCouponCell:{title:`优惠券`,count:e=>`${e}\u5F20\u53EF\u7528`},vanCouponList:{exchange:`兑换`,close:`不使用`,enable:`可用`,disabled:`不可用`,placeholder:`输入优惠码`},vanAddressEdit:{area:`地区`,areaEmpty:`请选择地区`,addressEmpty:`请填写详细地址`,addressDetail:`详细地址`,defaultAddress:`设为默认收货地址`},vanAddressList:{add:`新增地址`}},Nc=Wt(`zh-CN`),Pc=Pt({"zh-CN":Mc}),Fc={messages(){return Pc[Nc.value]},use(e,t){Nc.value=e,this.add({[e]:t})},add(e={}){jc(Pc,e)}};function Ic(e){let t=wc(e)+`.`;return(e,...n)=>{let r=Fc.messages(),i=Ss(r,t+e)||Ss(r,e);return vs(i)?i(...n):i}}function Lc(e,t){return t?typeof t==`string`?` ${e}--${t}`:Array.isArray(t)?t.reduce((t,n)=>t+Lc(e,n),``):Object.keys(t).reduce((n,r)=>n+(t[r]?Lc(e,r):``),``):``}function Rc(e){return(t,n)=>(t&&typeof t!=`string`&&(n=t,t=``),t=t?`${e}__${t}`:e,`${t}${Lc(t,n)}`)}function zc(e){let t=`van-${e}`;return[t,Rc(t),Ic(t)]}function Bc(e){return e.install=t=>{let{name:n}=e;n&&(t.component(n,e),t.component(wc(`-${n}`),e))},e}var Vc=2e3,Hc=()=>++Vc,Uc=e=>{Vc=e},[Wc,Gc]=zc(`config-provider`),Kc=Symbol(Wc),qc={tag:As(`div`),theme:As(`light`),zIndex:Number,themeVars:Object,themeVarsDark:Object,themeVarsLight:Object,themeVarsScope:As(`local`),iconPrefix:String};function Jc(e){return e.replace(/([a-zA-Z])(\d)/g,`$1-$2`)}function Yc(e){let t={};return Object.keys(e).forEach(n=>{let r=Jc(Tc(n));t[`--van-${r}`]=e[n]}),t}function Xc(e={},t={}){Object.keys(e).forEach(n=>{e[n]!==t[n]&&document.documentElement.style.setProperty(n,e[n])}),Object.keys(t).forEach(t=>{e[t]||document.documentElement.style.removeProperty(t)})}var Zc=Bc(fr({name:Wc,props:qc,setup(e,{slots:t}){let n=Z(()=>Yc(ms({},e.themeVars,e.theme===`dark`?e.themeVarsDark:e.themeVarsLight)));if(hs){let t=()=>{document.documentElement.classList.add(`van-theme-${e.theme}`)},r=(t=e.theme)=>{document.documentElement.classList.remove(`van-theme-${t}`)};Fn(()=>e.theme,(e,n)=>{n&&r(n),t()},{immediate:!0}),Sr(t),Cr(r),Pr(r),Fn(n,(t,n)=>{e.themeVarsScope===`global`&&Xc(t,n)}),Fn(()=>e.themeVarsScope,(e,t)=>{t===`global`&&Xc({},n.value),e===`global`&&Xc(n.value,{})}),e.themeVarsScope===`global`&&Xc(n.value,{})}return An(Kc,e),Nn(()=>{e.zIndex!==void 0&&Uc(e.zIndex)}),()=>J(e.tag,{class:Gc(),style:e.themeVarsScope===`local`?n.value:void 0},{default:()=>{var e;return[(e=t.default)==null?void 0:e.call(t)]}})}}));function Qc(e=`(min-width: 768px)`){let t=typeof window<`u`&&window.matchMedia?window.matchMedia(e):null,n=Wt(t?t.matches:!1),r=e=>{n.value=e.matches};return jr(()=>{var e;return t==null||(e=t.addEventListener)==null?void 0:e.call(t,`change`,r)}),Pr(()=>{var e;return t==null||(e=t.removeEventListener)==null?void 0:e.call(t,`change`,r)}),n}function $c(){let e=typeof window<`u`&&window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,t=Wt(e?e.matches:!1),n=e=>{t.value=e.matches};return jr(()=>{var t;return e==null||(t=e.addEventListener)==null?void 0:t.call(e,`change`,n)}),Pr(()=>{var t;return e==null||(t=e.removeEventListener)==null?void 0:t.call(e,`change`,n)}),t}function el(e,t){let n,r=!1,i=()=>{n!==void 0&&window.clearInterval(n),n=void 0},a=()=>{i(),n=window.setInterval(()=>{document.visibilityState===`visible`&&e()},t)},o=()=>{r&&(document.visibilityState===`visible`?(e(),a()):i())};jr(()=>{r=!0,a(),document.addEventListener(`visibilitychange`,o)}),Sr(()=>{r||(r=!0,e(),a())}),Cr(()=>{r=!1,i()}),Pr(()=>{r=!1,i(),document.removeEventListener(`visibilitychange`,o)})}var tl=fr({__name:`App`,setup(e){let t=$c(),n=Z(()=>t.value?`dark`:`light`),r={primaryColor:`#2f6fff`,successColor:`#13b77a`,warningColor:`#e8870c`,dangerColor:`#ff5358`,navBarBackground:`var(--app-bg)`,navBarTitleTextColor:`var(--app-text)`,cellGroupInsetRadius:`14px`,buttonRadius:`12px`,buttonLargeHeight:`48px`},i={primaryColor:`#5b8cff`,successColor:`#2fd19a`,warningColor:`#ffae4a`,dangerColor:`#ff6b70`,background:`#0b1222`,background2:`#131c31`,background3:`#18223b`,textColor:`#e6ecfb`,textColor2:`#8b97b6`,textColor3:`#6c7898`,borderColor:`rgba(140,160,210,.16)`,activeColor:`#1c2743`};return(e,t)=>{let a=Vr(`RouterView`);return ua(),ga(L(Zc),{theme:n.value,"theme-vars-scope":`global`,"theme-vars":r,"theme-vars-dark":i},{default:Dn(()=>[J(a)]),_:1},8,[`theme`])}}});function nl(e){"@babel/helpers - typeof";return nl=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},nl(e)}function rl(e,t){if(nl(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(nl(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function il(e){var t=rl(e,`string`);return nl(t)==`symbol`?t:t+``}function al(e,t,n){return(t=il(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ol(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function sl(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&ol(e.default)}var Q=Object.assign;function cl(e,t){let n={};for(let r in t){let i=t[r];n[r]=ul(i)?i.map(e):e(i)}return n}var ll=()=>{},ul=Array.isArray;function dl(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var fl=Symbol(``);function pl(e,t){return Q(Error(),{type:e,[fl]:!0},t)}function ml(e,t){return e instanceof Error&&fl in e&&(t==null||!!(e.type&t))}var hl=Symbol(``),gl=Symbol(``),_l=Symbol(``),vl=Symbol(``),yl=Symbol(``);function bl(){return V(_l)}function xl(e){return V(vl)}var Sl=typeof document<`u`,Cl=/#/g,wl=/&/g,Tl=/\//g,El=/=/g,Dl=/\?/g,Ol=/\+/g,kl=/%5B/g,Al=/%5D/g,jl=/%5E/g,Ml=/%60/g,Nl=/%7B/g,Pl=/%7C/g,Fl=/%7D/g,Il=/%20/g;function Ll(e){return e==null?``:encodeURI(``+e).replace(Pl,`|`).replace(kl,`[`).replace(Al,`]`)}function Rl(e){return Ll(e).replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function zl(e){return Ll(e).replace(Ol,`%2B`).replace(Il,`+`).replace(Cl,`%23`).replace(wl,`%26`).replace(Ml,"`").replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function Bl(e){return zl(e).replace(El,`%3D`)}function Vl(e){return Ll(e).replace(Cl,`%23`).replace(Dl,`%3F`)}function Hl(e){return Vl(e).replace(Tl,`%2F`)}function Ul(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var Wl=/\/$/,Gl=e=>e.replace(Wl,``);function Kl(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r=r||t.slice(0,s),o=t.slice(s,t.length)),r=eu(r==null?t:r,n),{fullPath:r+a+o,path:r,query:i,hash:Ul(o)}}function ql(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Jl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function Yl(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&Xl(t.matched[r],n.matched[i])&&Zl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Xl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Zl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ql(e[n],t[n]))return!1;return!0}function Ql(e,t){return ul(e)?$l(e,t):ul(t)?$l(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function $l(e,t){return ul(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function eu(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o1&&a--;else break}return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var tu={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0};function nu(e){if(!e){if(Sl){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^/]+/,``)}else e=`/`}return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Gl(e)}var ru=/^[^#]+#/;function iu(e,t){return e.replace(ru,`#`)+t}function au(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var ou=()=>({left:window.scrollX,top:window.scrollY});function su(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=au(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function cu(e,t){return(history.state?history.state.position-t:-1)+e}var lu=new Map;function uu(e,t){lu.set(e,t)}function du(e){let t=lu.get(e);return lu.delete(e),t}function fu(e){return typeof e==`string`||e&&typeof e==`object`}function pu(e){return typeof e==`string`||typeof e==`symbol`}function mu(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;ee&&zl(e)):[r&&zl(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function gu(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=ul(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function _u(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function vu(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(pl(4,{from:n,to:t})):e instanceof Error?c(e):fu(e)?c(pl(2,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function yu(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(t===`beforeRouteEnter`||o.instances[e]){if(ol(s)){let c=(s.__vccOpts||s)[t];c&&a.push(vu(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=sl(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&vu(c,n,r,o,e,i)()}))}}}return a}function bu(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;oXl(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>Xl(e,s))||i.push(s))}return[n,r,i]}var xu=()=>location.protocol+`//`+location.host;function Su(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Jl(n,``)}return Jl(n,e)+r+i}function Cu(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=Su(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:`pop`,direction:u?u>0?`forward`:`back`:``})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(Q({},e.state,{scroll:ou()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function wu(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ou():null}}function Tu(e){let{history:t,location:n}=window,r={value:Su(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:xu()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,Q({},t.state,wu(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=Q({},i.value,t.state,{forward:e,scroll:ou()});a(o.current,o,!0),a(e,Q({},wu(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function Eu(e){e=nu(e);let t=Tu(e),n=Cu(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=Q({location:``,base:e,go:r,createHref:iu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Du(e){return e=location.host?e||location.pathname+location.search:``,e.includes(`#`)||(e+=`#`),Eu(e)}var Ou={type:0,value:``},ku=/[a-zA-Z0-9_]/;function Au(e){if(!e)return[[]];if(e===`/`)return[[Ou]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=0,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&(n===0?a.push({type:0,value:l}):n===1||n===2||n===3?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:1,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),l=``)}function f(){l+=c}for(;st.length?t.length===1&&t[0]===80?1:-1:0}function Iu(e,t){let n=0,r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}var Ru={strict:!1,end:!0,sensitive:!1};function zu(e,t,n){let r=Q(Pu(Au(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Bu(e,t){let n=[],r=new Map;t=dl(Ru,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Hu(e);s.aliasOf=r&&r.record;let l=dl(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Hu(Q({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=zu(t,n,l),r?r.alias.push(d):(f=f||d,f!==d&&f.alias.push(d),i&&e.name&&!Wu(d)&&o(e.name)),Ju(d)&&c(d),s.children){let e=s.children;for(let t=0;t{o(f)}:ll}function o(e){if(pu(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=Ku(e,n);n.splice(t,0,e),e.record.name&&!Wu(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw pl(1,{location:e});s=i.record.name,a=Q(Vu(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&Vu(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name,i.keys.forEach(e=>{e.optional&&!a[e.name]&&delete a[e.name]}));else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw pl(1,{location:e,currentLocation:t});s=i.record.name,a=Q({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Gu(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function Vu(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Hu(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Uu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Uu(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Wu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Gu(e){return e.reduce((e,t)=>Q(e,t.meta),{})}function Ku(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Iu(e,t[i])<0?r=i:n=i+1}let i=qu(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function qu(e){let t=e;for(;t=t.parent;)if(Ju(t)&&Iu(e,t)===0)return t}function Ju({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Yu(e){let t=V(_l),n=V(vl),r=Z(()=>{let n=L(e.to);return t.resolve(n)}),i=Z(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(Xl.bind(null,i));if(o>-1)return o;let s=ed(e[t-2]);return t>1&&ed(i)===s&&a[a.length-1].path!==s?a.findIndex(Xl.bind(null,e[t-2])):o}),a=Z(()=>i.value>-1&&$u(n.params,r.value.params)),o=Z(()=>i.value>-1&&i.value===n.matched.length-1&&Zl(n.params,r.value.params));function s(n={}){if(Qu(n)){let n=t[L(e.replace)?`replace`:`push`](L(e.to)).catch(ll);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:Z(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Xu(e){return e.length===1?e[0]:e}var Zu=fr({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:Yu,setup(e,{slots:t}){let n=Pt(Yu(e)),{options:r}=V(_l),i=Z(()=>({[td(e.activeClass,r.linkActiveClass,`router-link-active`)]:n.isActive,[td(e.exactActiveClass,r.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let r=t.default&&Xu(t.default(n));return e.custom?r:Xa(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}});function Qu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(e.button===void 0||e.button===0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function $u(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!ul(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function ed(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var td=(e,t,n)=>e==null?t==null?n:t:e,nd=fr({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let r=V(yl),i=Z(()=>e.route||r.value),a=V(gl,0),o=Z(()=>{let e=L(a),{matched:t}=i.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),s=Z(()=>i.value.matched[o.value]);An(gl,Z(()=>o.value+1)),An(hl,s),An(yl,i);let c=Wt();return Fn(()=>[c.value,s.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!Xl(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let r=i.value,a=e.name,o=s.value,l=o&&o.components[a];if(!l)return rd(n.default,{Component:l,route:r});let u=o.props[a],d=Xa(l,Q({},u?u===!0?r.params:typeof u==`function`?u(r):u:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(o.instances[a]=null)},ref:c}));return rd(n.default,{Component:d,route:r})||d}}});function rd(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var id=nd;function ad(e){let t=Bu(e.routes,e),n=e.parseQuery||mu,r=e.stringifyQuery||hu,i=e.history,a=_u(),o=_u(),s=_u(),c=Gt(tu),l=tu;Sl&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let u=cl.bind(null,e=>``+e),d=cl.bind(null,Hl),f=cl.bind(null,Ul);function p(e,n){let r,i;return pu(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function m(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function h(){return t.getRoutes().map(e=>e.record)}function g(e){return!!t.getRecordMatcher(e)}function _(e,a){if(a=Q({},a||c.value),typeof e==`string`){let r=Kl(n,e,a.path),o=t.resolve({path:r.path},a),s=i.createHref(r.fullPath);return Q(r,o,{params:f(o.params),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=Q({},e,{path:Kl(n,e.path,a.path).path});else{let t=Q({},e.params);for(let e in t)t[e]==null&&delete t[e];o=Q({},e,{params:d(t)}),a.params=d(a.params)}let s=t.resolve(o,a),l=e.hash||``;s.params=u(f(s.params));let p=ql(r,Q({},e,{hash:Rl(l),path:s.path})),m=i.createHref(p);return Q({fullPath:p,hash:l,query:r===hu?gu(e.query):e.query||{}},s,{redirectedFrom:void 0,href:m})}function v(e){return typeof e==`string`?Kl(n,e,c.value.path):Q({},e)}function y(e,t){if(l!==e)return pl(8,{from:t,to:e})}function b(e){return C(e)}function x(e){return b(Q(v(e),{replace:!0}))}function S(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=v(i):{path:i},i.params={}),Q({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function C(e,t){let n=l=_(e),i=c.value,a=e.state,o=e.force,s=e.replace===!0,u=S(n,i);if(u)return C(Q(v(u),{state:typeof u==`object`?Q({},a,u.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&Yl(r,i,n)&&(f=pl(16,{to:d,from:i}),oe(i,i,!0,!1)),(f?Promise.resolve(f):ee(d,i)).catch(e=>ml(e)?ml(e,2)?e:ae(e):k(e,d,i)).then(e=>{if(e){if(ml(e,2))return C(Q({replace:s},v(e.to),{state:typeof e.to==`object`?Q({},a,e.to.state):a,force:o}),t||d)}else e=te(d,i,!0,s,a);return E(d,i,e),e})}function w(e,t){let n=y(e,t);return n?Promise.reject(n):Promise.resolve()}function T(e){let t=le.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function ee(e,t){let n,[r,i,s]=bu(e,t);n=yu(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(vu(r,e,t))});let c=w.bind(null,e,t);return n.push(c),de(n).then(()=>{n=[];for(let r of a.list())n.push(vu(r,e,t));return n.push(c),de(n)}).then(()=>{n=yu(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(vu(r,e,t))});return n.push(c),de(n)}).then(()=>{n=[];for(let r of s)if(r.beforeEnter){if(ul(r.beforeEnter))for(let i of r.beforeEnter)n.push(vu(i,e,t));else n.push(vu(r.beforeEnter,e,t))}return n.push(c),de(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=yu(s,`beforeRouteEnter`,e,t,T),n.push(c),de(n))).then(()=>{n=[];for(let r of o.list())n.push(vu(r,e,t));return n.push(c),de(n)}).catch(e=>ml(e,8)?e:Promise.reject(e))}function E(e,t,n){s.list().forEach(r=>T(()=>r(e,t,n)))}function te(e,t,n,r,a){let o=y(e,t);if(o)return o;let s=t===tu,l=Sl?history.state:{};n&&(r||s?i.replace(e.fullPath,Q({scroll:s&&l&&l.scroll},a)):i.push(e.fullPath,a)),c.value=e,oe(e,t,n,s),ae()}let D;function ne(){D||(D=i.listen((e,t,n)=>{if(!ue.listening)return;let r=_(e),a=S(r,ue.currentRoute.value);if(a){C(Q(a,{replace:!0,force:!0}),r).catch(ll);return}l=r;let o=c.value;Sl&&uu(cu(o.fullPath,n.delta),ou()),ee(r,o).catch(e=>ml(e,12)?e:ml(e,2)?(C(Q(v(e.to),{force:!0}),r).then(e=>{ml(e,20)&&!n.delta&&n.type===`pop`&&i.go(-1,!1)}).catch(ll),Promise.reject()):(n.delta&&i.go(-n.delta,!1),k(e,r,o))).then(e=>{e=e||te(r,o,!1),e&&(n.delta&&!ml(e,8)?i.go(-n.delta,!1):n.type===`pop`&&ml(e,20)&&i.go(-1,!1)),E(r,o,e)}).catch(ll)}))}let O=_u(),re=_u(),ie;function k(e,t,n){ae(e);let r=re.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function A(){return ie&&c.value!==tu?Promise.resolve():new Promise((e,t)=>{O.add([e,t])})}function ae(e){return ie||(ie=!e,ne(),O.list().forEach(([t,n])=>e?n(e):t()),O.reset()),e}function oe(t,n,r,i){let{scrollBehavior:a}=e;if(!Sl||!a)return Promise.resolve();let o=!r&&du(cu(t.fullPath,0))||(i||!r)&&history.state&&history.state.scroll||null;return gn().then(()=>a(t,n,o)).then(e=>t===c.value&&e&&su(e)).catch(e=>t===c.value&&k(e,t,n))}let se=e=>i.go(e),ce,le=new Set,ue={currentRoute:c,listening:!0,addRoute:p,removeRoute:m,clearRoutes:t.clearRoutes,hasRoute:g,getRoutes:h,resolve:_,options:e,push:b,replace:x,go:se,back:()=>se(-1),forward:()=>se(1),beforeEach:a.add,beforeResolve:o.add,afterEach:s.add,onError:re.add,isReady:A,install(e){e.component(`RouterLink`,Zu),e.component(`RouterView`,id),e.config.globalProperties.$router=ue,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>L(c)}),Sl&&!ce&&c.value===tu&&(ce=!0,b(i.location).catch(e=>{}));let t={};for(let e in tu)Object.defineProperty(t,e,{get:()=>c.value[e],enumerable:!0});e.provide(_l,ue),e.provide(vl,Ft(t)),e.provide(yl,c);let n=e.unmount;le.add(e),e.unmount=function(){le.delete(e),le.size<1&&(l=tu,D&&D(),D=null,c.value=tu,ce=!1,ie=!1),n()}}};function de(e){return e.reduce((e,t)=>e.then(()=>T(t)),Promise.resolve())}return ue}var od=`/api/v2/mobile`,sd=`wecom-mobile-token`,cd=class extends Error{constructor(e,t,n,r=0){super(n),al(this,`status`,void 0),al(this,`code`,void 0),al(this,`retryAfter`,void 0),this.status=e,this.code=t,this.retryAfter=r}},ld=null;function ud(e){ld=e}function dd(){try{return window.localStorage.getItem(sd)||``}catch{return``}}function fd(e){try{e?window.localStorage.setItem(sd,e):window.localStorage.removeItem(sd)}catch{}}var pd=``;function md(){return pd||dd()}function hd(e){pd=e,fd(e)}function gd(e,t){let n=t&&typeof t==`object`&&`detail`in t?t.detail:t;if(n&&typeof n==`object`&&!Array.isArray(n)){let t=n;return new cd(e,String(t.code||`http_${e}`),String(t.message||_d(e)),Number(t.retry_after||0))}return Array.isArray(n)?new cd(e,`validation`,`填写的内容不符合要求,请检查后重试`):typeof n==`string`&&n.trim()?new cd(e,`http_${e}`,n.trim()):new cd(e,`http_${e}`,_d(e))}function _d(e){return e===401?`登录已失效,请重新登录`:e===403?`没有权限执行这个操作`:e===404?`内容不存在或已被删除`:e===429?`操作太频繁,请稍后再试`:e>=500?`服务暂时不可用,请稍后重试`:`请求失败(HTTP ${e})`}async function vd(e,t,n={}){let{body:r,query:i,auth:a=!0,signal:o}=n,s=new URLSearchParams;for(let[e,t]of Object.entries(i||{}))t!=null&&t!==``&&s.set(e,String(t));let c=`${od}${t}${s.toString()?`?${s}`:``}`,l={Accept:`application/json`};r!==void 0&&(l[`Content-Type`]=`application/json`);let u=md();a&&u&&(l.Authorization=`Bearer ${u}`);let d;try{d=await fetch(c,{method:e,headers:l,body:r===void 0?void 0:JSON.stringify(r),signal:o,cache:`no-store`,credentials:`omit`})}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?e:new cd(0,`network`,`网络连接失败,请检查网络后重试`)}let f=null,p=await d.text();if(p)try{f=JSON.parse(p)}catch{f=null}if(!d.ok){let e=gd(d.status,f);throw d.status===401&&a&&(ld==null||ld()),e}return f}var yd=e=>encodeURIComponent(e),bd={login(e,t,n){return vd(`POST`,`/auth/login`,{auth:!1,body:{account:e,password:t,client_name:n}})},logout(){return vd(`POST`,`/auth/logout`)},me(){return vd(`GET`,`/me`)},accounts(e=``){return vd(`GET`,`/accounts`,{query:{q:e}})},desktops(e){return vd(`GET`,`/desktops`,{query:{account_id:e}})},sendCommand(e,t,n){return vd(`POST`,`/desktops/${yd(e)}/commands`,{body:{type:t},query:{account_id:n}})},command(e){return vd(`GET`,`/commands/${yd(e)}`)},conversations(e){return vd(`GET`,`/conversations`,{query:{account_id:e.accountId,q:e.q,scope:e.scope,cursor:e.cursor,limit:e.limit},signal:e.signal})},block(e,t,n){return vd(`PUT`,`/blacklist/${yd(e.account)}/${yd(e.conversation)}`,{body:{name:e.name||``,account_name:e.account_name||``,reason:t},query:{account_id:n}})},unblock(e,t,n){return vd(`DELETE`,`/blacklist/${yd(e)}/${yd(t)}`,{query:{account_id:n}})}},xd=`wecom-mobile-view-account`;function Sd(){try{let e=window.localStorage.getItem(xd),t=e?JSON.parse(e):null;return t&&typeof t.id==`number`?t:null}catch{return null}}var $=Pt({token:md(),me:null,viewed:Sd()}),Cd=Z(()=>{var e;return(e=$.me)==null?void 0:e.permissions}),wd=Z(()=>{let e=$.me,t=$.viewed;if(e&&t&&e.permissions.control_all&&t.id!==e.account.id)return t.id}),Td=Z(()=>{var e,t;return wd.value!==void 0&&$.viewed?$.viewed.nickname||$.viewed.sn||`客服 #${$.viewed.id}`:((e=$.me)==null?void 0:e.account.nickname)||((t=$.me)==null?void 0:t.account.sn)||``});function Ed(e){$.viewed=e?{id:e.id,nickname:e.nickname,sn:e.sn}:null;try{$.viewed?window.localStorage.setItem(xd,JSON.stringify($.viewed)):window.localStorage.removeItem(xd)}catch{}}function Dd(e,t){hd(e),$.token=e,$.me=t,t.permissions.control_all||Ed(null)}function Od(){hd(``),$.token=``,$.me=null,Ed(null)}async function kd(){let e=await bd.me();return $.me=e,e.permissions.control_all||Ed(null),e}async function Ad(){try{await bd.logout()}catch{}Od()}var jd=`modulepreload`,Md=function(e){return`/m/`+e},Nd={},Pd=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute(`nonce`));function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=Md(t,n),t=s(t),t in Nd)return;Nd[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:jd,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Fd=ad({history:Du(),routes:[{path:`/login`,name:`login`,component:()=>Pd(()=>import(`./LoginView-CLW1WjQp.js`),__vite__mapDeps([0,1,2,3,4])),meta:{public:!0}},{path:`/`,component:()=>Pd(()=>import(`./ShellView-DMPqnSgh.js`),__vite__mapDeps([5,1,6,7])),children:[{path:``,redirect:{name:`desktops`}},{path:`desktops`,name:`desktops`,component:()=>Pd(()=>import(`./DevicesView-HbIaJI1u.js`),__vite__mapDeps([8,1,9,6,2,10,3,11,12]))},{path:`conversations`,name:`conversations`,component:()=>Pd(()=>import(`./ConversationsView-BeEp2NbK.js`),__vite__mapDeps([13,1,6,9,2,3,10,11,14]))},{path:`me`,name:`me`,component:()=>Pd(()=>import(`./MeView-DTU4-bV-.js`),__vite__mapDeps([15,1,9,6,2,16]))}]},{path:`/:pathMatch(.*)*`,redirect:{name:`desktops`}}]});Fd.beforeEach(async e=>{if(e.meta.public)return!0;if(!$.token)return{name:`login`,query:{redirect:e.fullPath}};if(!$.me)try{await kd()}catch(t){if(t instanceof cd&&t.status===401)return Od(),{name:`login`,query:{redirect:e.fullPath}}}return!0}),ud(()=>{Od();let e=Fd.currentRoute.value;e.matched.length&&e.name!==`login`&&Fd.replace({name:`login`,query:{redirect:e.fullPath,expired:`1`}})});var Id=`wecom-mobile-chunk-reload`;Fd.onError((e,t)=>{let n=String((e==null?void 0:e.message)||e);if(!/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(n))return;let r=0;try{r=Number(window.sessionStorage.getItem(Id)||0),window.sessionStorage.setItem(Id,String(Date.now()))}catch{}Date.now()-r>1e4&&(window.location.assign(`/m/#${t.fullPath}`),window.location.reload())}),us(tl).use(Fd).mount(`#app`),`serviceWorker`in navigator&&window.isSecureContext&&window.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/m/sw.js`,{scope:`/m/`}).catch(()=>void 0)});export{hs as $,ac as A,Mr as At,Hs as B,_i as Bt,Sc as C,fr as Ct,dc as D,gn as Dt,nc as E,Aa as Et,Ns as F,ua as Ft,Qs as G,Pt as Gt,ec as H,Nn as Ht,Ps as I,An as It,Ds as J,he as Jt,Os as K,Wt as Kt,Zs as L,Kr as Lt,pc as M,jr as Mt,fc as N,Fr as Nt,uc as O,Sr as Ot,tc as P,Nr as Pt,ms as Q,Us as R,Vr as Rt,gc as S,J as St,ic as T,V as Tt,Rs as U,Dn as Ut,Ws as V,Fn as Vt,Ls as W,On as Wt,Ts as X,As as Y,Se as Yt,Es as Z,zc as _,ga as _t,Ed as a,ps as at,Oc as b,qr as bt,cd as c,fo as ct,bl as d,os as dt,_s as et,el as f,oa as ft,Bc as g,xa as gt,Hc as h,Z as ht,Ad as i,ys as it,rc as j,Cr as jt,cc as k,Pr as kt,bd as l,us as lt,Kc as m,Xn as mt,$ as n,bs as nt,wd as o,Cs as ot,Qc as p,br as pt,ks as q,L as qt,Dd as r,gs as rt,Td as s,ws as st,Cd as t,vs as tt,xl as u,Ao as ut,mc as v,Ea as vt,oc as w,Pa as wt,hc as x,Ta as xt,Ec as y,ha as yt,Ms as z,Ur as zt}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css new file mode 100644 index 000000000..da7edc533 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css @@ -0,0 +1 @@ +:root,:host{--van-black:#000;--van-white:#fff;--van-gray-1:#f7f8fa;--van-gray-2:#f2f3f5;--van-gray-3:#ebedf0;--van-gray-4:#dcdee0;--van-gray-5:#c8c9cc;--van-gray-6:#969799;--van-gray-7:#646566;--van-gray-8:#323233;--van-red:#ee0a24;--van-blue:#1989fa;--van-orange:#ff976a;--van-orange-dark:#ed6a0c;--van-orange-light:#fffbe8;--van-green:#07c160;--van-gradient-red:linear-gradient(to right, #ff6034, #ee0a24);--van-gradient-orange:linear-gradient(to right, #ffd01e, #ff8917);--van-primary-color:var(--van-blue);--van-success-color:var(--van-green);--van-danger-color:var(--van-red);--van-warning-color:var(--van-orange);--van-text-color:var(--van-gray-8);--van-text-color-2:var(--van-gray-6);--van-text-color-3:var(--van-gray-5);--van-active-color:var(--van-gray-2);--van-active-opacity:.6;--van-disabled-opacity:.5;--van-background:var(--van-gray-1);--van-background-2:var(--van-white);--van-background-3:var(--van-white);--van-padding-base:4px;--van-padding-xs:8px;--van-padding-sm:12px;--van-padding-md:16px;--van-padding-lg:24px;--van-padding-xl:32px;--van-font-bold:600;--van-font-size-xs:10px;--van-font-size-sm:12px;--van-font-size-md:14px;--van-font-size-lg:16px;--van-line-height-xs:14px;--van-line-height-sm:18px;--van-line-height-md:20px;--van-line-height-lg:22px;--van-base-font:-apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;--van-price-font:avenir-heavy, "PingFang SC", helvetica neue, arial, sans-serif;--van-duration-base:.3s;--van-duration-fast:.2s;--van-ease-out:ease-out;--van-ease-in:ease-in;--van-border-color:var(--van-gray-3);--van-border-width:1px;--van-radius-sm:2px;--van-radius-md:4px;--van-radius-lg:8px;--van-radius-max:999px}.van-theme-dark{--van-text-color:#f5f5f5;--van-text-color-2:#707070;--van-text-color-3:#4d4d4d;--van-border-color:#3a3a3c;--van-active-color:#3a3a3c;--van-background:#000;--van-background-2:#1c1c1e;--van-background-3:#37363b}html{-webkit-tap-highlight-color:transparent}body{font-family:var(--van-base-font);margin:0}a{text-decoration:none}input,button,textarea{color:inherit;font:inherit}a:focus,input:focus,button:focus,textarea:focus,[class*=van-]:focus{outline:none}ol,ul{margin:0;padding:0;list-style:none}@keyframes van-slide-up-enter{0%{transform:translateY(100%)}}@keyframes van-slide-up-leave{to{transform:translateY(100%)}}@keyframes van-slide-down-enter{0%{transform:translateY(-100%)}}@keyframes van-slide-down-leave{to{transform:translateY(-100%)}}@keyframes van-slide-left-enter{0%{transform:translate(-100%)}}@keyframes van-slide-left-leave{to{transform:translate(-100%)}}@keyframes van-slide-right-enter{0%{transform:translate(100%)}}@keyframes van-slide-right-leave{to{transform:translate(100%)}}@keyframes van-fade-in{0%{opacity:0}to{opacity:1}}@keyframes van-fade-out{0%{opacity:1}to{opacity:0}}@keyframes van-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.van-fade-enter-active{animation:var(--van-duration-base) van-fade-in both var(--van-ease-out)}.van-fade-leave-active{animation:var(--van-duration-base) van-fade-out both var(--van-ease-in)}.van-slide-up-enter-active{animation:van-slide-up-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-up-leave-active{animation:van-slide-up-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-down-enter-active{animation:van-slide-down-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-down-leave-active{animation:van-slide-down-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-left-enter-active{animation:van-slide-left-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-left-leave-active{animation:van-slide-left-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-right-enter-active{animation:van-slide-right-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-right-leave-active{animation:van-slide-right-leave var(--van-duration-base) both var(--van-ease-in)}.van-clearfix:after{clear:both;content:"";display:table}.van-ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.van-multi-ellipsis--l2{text-overflow:ellipsis;-webkit-line-clamp:2;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-multi-ellipsis--l3{text-overflow:ellipsis;-webkit-line-clamp:3;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-safe-area-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-safe-area-bottom{padding-bottom:constant(safe-area-inset-bottom);padding-bottom:env(safe-area-inset-bottom)}.van-haptics-feedback{cursor:pointer}.van-haptics-feedback:active{opacity:var(--van-active-opacity)}[class*=van-hairline]:after{box-sizing:border-box;content:" ";pointer-events:none;border:0 solid var(--van-border-color);position:absolute;top:-50%;bottom:-50%;left:-50%;right:-50%;transform:scale(.5)}.van-hairline,.van-hairline--top,.van-hairline--left,.van-hairline--right,.van-hairline--bottom,.van-hairline--surround,.van-hairline--top-bottom{position:relative}.van-hairline--top:after{border-top-width:var(--van-border-width)}.van-hairline--left:after{border-left-width:var(--van-border-width)}.van-hairline--right:after{border-right-width:var(--van-border-width)}.van-hairline--bottom:after{border-bottom-width:var(--van-border-width)}.van-hairline--top-bottom:after,.van-hairline-unset--top-bottom:after{border-width:var(--van-border-width) 0}.van-hairline--surround:after{border-width:var(--van-border-width)}:root,:host{--van-badge-size:16px;--van-badge-color:var(--van-white);--van-badge-padding:0 3px;--van-badge-font-size:var(--van-font-size-sm);--van-badge-font-weight:var(--van-font-bold);--van-badge-border-width:var(--van-border-width);--van-badge-background:var(--van-danger-color);--van-badge-dot-color:var(--van-danger-color);--van-badge-dot-size:8px;--van-badge-font:-apple-system-font, helvetica neue, arial, sans-serif}.van-badge{box-sizing:border-box;min-width:var(--van-badge-size);padding:var(--van-badge-padding);color:var(--van-badge-color);font-weight:var(--van-badge-font-weight);font-size:var(--van-badge-font-size);font-family:var(--van-badge-font);text-align:center;background:var(--van-badge-background);border:var(--van-badge-border-width) solid var(--van-background-2);border-radius:var(--van-radius-max);line-height:1.2;display:inline-block}.van-badge--fixed{transform-origin:100%;position:absolute}.van-badge--top-left{top:0;left:0;transform:translate(-50%,-50%)}.van-badge--top-right{top:0;right:0;transform:translate(50%,-50%)}.van-badge--bottom-left{bottom:0;left:0;transform:translate(-50%,50%)}.van-badge--bottom-right{bottom:0;right:0;transform:translate(50%,50%)}.van-badge--dot{width:var(--van-badge-dot-size);min-width:0;height:var(--van-badge-dot-size);background:var(--van-badge-dot-color);border:none;border-radius:100%;padding:0}.van-badge__wrapper{display:inline-block;position:relative}.van-icon{font:14px/1 vant-icon;font:normal normal normal 14px/1 var(--van-icon-font-family,"vant-icon");font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;display:inline-block;position:relative}.van-icon:before{display:inline-block}.van-icon-arrow-double-left:before{content:""}.van-icon-arrow-double-right:before{content:""}.van-icon-contact:before{content:""}.van-icon-notes:before{content:""}.van-icon-records:before{content:""}.van-icon-cash-back-record:before{content:""}.van-icon-newspaper:before{content:""}.van-icon-discount:before{content:""}.van-icon-completed:before{content:""}.van-icon-user:before{content:""}.van-icon-description:before{content:""}.van-icon-list-switch:before{content:""}.van-icon-list-switching:before{content:""}.van-icon-link-o:before{content:""}.van-icon-miniprogram-o:before{content:""}.van-icon-qq:before{content:""}.van-icon-wechat-moments:before{content:""}.van-icon-weibo:before{content:""}.van-icon-cash-o:before{content:""}.van-icon-guide-o:before{content:""}.van-icon-invitation:before{content:""}.van-icon-shield-o:before{content:""}.van-icon-exchange:before{content:""}.van-icon-eye:before{content:""}.van-icon-enlarge:before{content:""}.van-icon-expand-o:before{content:""}.van-icon-eye-o:before{content:""}.van-icon-expand:before{content:""}.van-icon-filter-o:before{content:""}.van-icon-fire:before{content:""}.van-icon-fail:before{content:""}.van-icon-failure:before{content:""}.van-icon-fire-o:before{content:""}.van-icon-flag-o:before{content:""}.van-icon-font:before{content:""}.van-icon-font-o:before{content:""}.van-icon-gem-o:before{content:""}.van-icon-flower-o:before{content:""}.van-icon-gem:before{content:""}.van-icon-gift-card:before{content:""}.van-icon-friends:before{content:""}.van-icon-friends-o:before{content:""}.van-icon-gold-coin:before{content:""}.van-icon-gold-coin-o:before{content:""}.van-icon-good-job-o:before{content:""}.van-icon-gift:before{content:""}.van-icon-gift-o:before{content:""}.van-icon-gift-card-o:before{content:""}.van-icon-good-job:before{content:""}.van-icon-home-o:before{content:""}.van-icon-goods-collect:before{content:""}.van-icon-graphic:before{content:""}.van-icon-goods-collect-o:before{content:""}.van-icon-hot-o:before{content:""}.van-icon-info:before{content:""}.van-icon-hotel-o:before{content:""}.van-icon-info-o:before{content:""}.van-icon-hot-sale-o:before{content:""}.van-icon-hot:before{content:""}.van-icon-like:before{content:""}.van-icon-idcard:before{content:""}.van-icon-like-o:before{content:""}.van-icon-hot-sale:before{content:""}.van-icon-location-o:before{content:""}.van-icon-location:before{content:""}.van-icon-label:before{content:""}.van-icon-lock:before{content:""}.van-icon-label-o:before{content:""}.van-icon-map-marked:before{content:""}.van-icon-logistics:before{content:""}.van-icon-manager:before{content:""}.van-icon-more:before{content:""}.van-icon-live:before{content:""}.van-icon-manager-o:before{content:""}.van-icon-medal:before{content:""}.van-icon-more-o:before{content:""}.van-icon-music-o:before{content:""}.van-icon-music:before{content:""}.van-icon-new-arrival-o:before{content:""}.van-icon-medal-o:before{content:""}.van-icon-new-o:before{content:""}.van-icon-free-postage:before{content:""}.van-icon-newspaper-o:before{content:""}.van-icon-new-arrival:before{content:""}.van-icon-minus:before{content:""}.van-icon-orders-o:before{content:""}.van-icon-new:before{content:""}.van-icon-paid:before{content:""}.van-icon-notes-o:before{content:""}.van-icon-other-pay:before{content:""}.van-icon-pause-circle:before{content:""}.van-icon-pause:before{content:""}.van-icon-pause-circle-o:before{content:""}.van-icon-peer-pay:before{content:""}.van-icon-pending-payment:before{content:""}.van-icon-passed:before{content:""}.van-icon-plus:before{content:""}.van-icon-phone-circle-o:before{content:""}.van-icon-phone-o:before{content:""}.van-icon-printer:before{content:""}.van-icon-photo-fail:before{content:""}.van-icon-phone:before{content:""}.van-icon-photo-o:before{content:""}.van-icon-play-circle:before{content:""}.van-icon-play:before{content:""}.van-icon-phone-circle:before{content:""}.van-icon-point-gift-o:before{content:""}.van-icon-point-gift:before{content:""}.van-icon-play-circle-o:before{content:""}.van-icon-shrink:before{content:""}.van-icon-photo:before{content:""}.van-icon-qr:before{content:""}.van-icon-qr-invalid:before{content:""}.van-icon-question-o:before{content:""}.van-icon-revoke:before{content:""}.van-icon-replay:before{content:""}.van-icon-service:before{content:""}.van-icon-question:before{content:""}.van-icon-search:before{content:""}.van-icon-refund-o:before{content:""}.van-icon-service-o:before{content:""}.van-icon-scan:before{content:""}.van-icon-share:before{content:""}.van-icon-send-gift-o:before{content:""}.van-icon-share-o:before{content:""}.van-icon-setting:before{content:""}.van-icon-points:before{content:""}.van-icon-photograph:before{content:""}.van-icon-shop:before{content:""}.van-icon-shop-o:before{content:""}.van-icon-shop-collect-o:before{content:""}.van-icon-shop-collect:before{content:""}.van-icon-smile:before{content:""}.van-icon-shopping-cart-o:before{content:""}.van-icon-sign:before{content:""}.van-icon-sort:before{content:""}.van-icon-star-o:before{content:""}.van-icon-smile-comment-o:before{content:""}.van-icon-stop:before{content:""}.van-icon-stop-circle-o:before{content:""}.van-icon-smile-o:before{content:""}.van-icon-star:before{content:""}.van-icon-success:before{content:""}.van-icon-stop-circle:before{content:""}.van-icon-records-o:before{content:""}.van-icon-shopping-cart:before{content:""}.van-icon-tosend:before{content:""}.van-icon-todo-list:before{content:""}.van-icon-thumb-circle-o:before{content:""}.van-icon-thumb-circle:before{content:""}.van-icon-umbrella-circle:before{content:""}.van-icon-underway:before{content:""}.van-icon-upgrade:before{content:""}.van-icon-todo-list-o:before{content:""}.van-icon-tv-o:before{content:""}.van-icon-underway-o:before{content:""}.van-icon-user-o:before{content:""}.van-icon-vip-card-o:before{content:""}.van-icon-vip-card:before{content:""}.van-icon-send-gift:before{content:""}.van-icon-wap-home:before{content:""}.van-icon-wap-nav:before{content:""}.van-icon-volume-o:before{content:""}.van-icon-video:before{content:""}.van-icon-wap-home-o:before{content:""}.van-icon-volume:before{content:""}.van-icon-warning:before{content:""}.van-icon-weapp-nav:before{content:""}.van-icon-wechat-pay:before{content:""}.van-icon-warning-o:before{content:""}.van-icon-wechat:before{content:""}.van-icon-setting-o:before{content:""}.van-icon-youzan-shield:before{content:""}.van-icon-warn-o:before{content:""}.van-icon-smile-comment:before{content:""}.van-icon-user-circle-o:before{content:""}.van-icon-video-o:before{content:""}.van-icon-add-square:before{content:""}.van-icon-add:before{content:""}.van-icon-arrow-down:before{content:""}.van-icon-arrow-up:before{content:""}.van-icon-arrow:before{content:""}.van-icon-after-sale:before{content:""}.van-icon-add-o:before{content:""}.van-icon-alipay:before{content:""}.van-icon-ascending:before{content:""}.van-icon-apps-o:before{content:""}.van-icon-aim:before{content:""}.van-icon-award:before{content:""}.van-icon-arrow-left:before{content:""}.van-icon-award-o:before{content:""}.van-icon-audio:before{content:""}.van-icon-bag-o:before{content:""}.van-icon-balance-list:before{content:""}.van-icon-back-top:before{content:""}.van-icon-bag:before{content:""}.van-icon-balance-pay:before{content:""}.van-icon-balance-o:before{content:""}.van-icon-bar-chart-o:before{content:""}.van-icon-bars:before{content:""}.van-icon-balance-list-o:before{content:""}.van-icon-birthday-cake-o:before{content:""}.van-icon-bookmark:before{content:""}.van-icon-bill:before{content:""}.van-icon-bell:before{content:""}.van-icon-browsing-history-o:before{content:""}.van-icon-browsing-history:before{content:""}.van-icon-bookmark-o:before{content:""}.van-icon-bulb-o:before{content:""}.van-icon-bullhorn-o:before{content:""}.van-icon-bill-o:before{content:""}.van-icon-calendar-o:before{content:""}.van-icon-brush-o:before{content:""}.van-icon-card:before{content:""}.van-icon-cart-o:before{content:""}.van-icon-cart-circle:before{content:""}.van-icon-cart-circle-o:before{content:""}.van-icon-cart:before{content:""}.van-icon-cash-on-deliver:before{content:""}.van-icon-cash-back-record-o:before{content:""}.van-icon-cashier-o:before{content:""}.van-icon-chart-trending-o:before{content:""}.van-icon-certificate:before{content:""}.van-icon-chat:before{content:""}.van-icon-clear:before{content:""}.van-icon-chat-o:before{content:""}.van-icon-checked:before{content:""}.van-icon-clock:before{content:""}.van-icon-clock-o:before{content:""}.van-icon-close:before{content:""}.van-icon-closed-eye:before{content:""}.van-icon-circle:before{content:""}.van-icon-cluster-o:before{content:""}.van-icon-column:before{content:""}.van-icon-comment-circle-o:before{content:""}.van-icon-cluster:before{content:""}.van-icon-comment:before{content:""}.van-icon-comment-o:before{content:""}.van-icon-comment-circle:before{content:""}.van-icon-completed-o:before{content:""}.van-icon-credit-pay:before{content:""}.van-icon-coupon:before{content:""}.van-icon-debit-pay:before{content:""}.van-icon-coupon-o:before{content:""}.van-icon-contact-o:before{content:""}.van-icon-descending:before{content:""}.van-icon-desktop-o:before{content:""}.van-icon-diamond-o:before{content:""}.van-icon-description-o:before{content:""}.van-icon-delete:before{content:""}.van-icon-diamond:before{content:""}.van-icon-delete-o:before{content:""}.van-icon-cross:before{content:""}.van-icon-edit:before{content:""}.van-icon-ellipsis:before{content:""}.van-icon-down:before{content:""}.van-icon-discount-o:before{content:""}.van-icon-ecard-pay:before{content:""}.van-icon-envelop-o:before{content:""}@font-face{font-weight:400;font-family:vant-icon;font-style:normal;font-display:auto;src:url(data:font/woff2;charset=utf-8;base64,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)format("woff2")}.van-icon__image{object-fit:contain;width:1em;height:1em;display:block}:root,:host{--van-nav-bar-height:46px;--van-nav-bar-background:var(--van-background-2);--van-nav-bar-arrow-size:16px;--van-nav-bar-icon-color:var(--van-primary-color);--van-nav-bar-text-color:var(--van-primary-color);--van-nav-bar-title-font-size:var(--van-font-size-lg);--van-nav-bar-title-text-color:var(--van-text-color);--van-nav-bar-z-index:1;--van-nav-bar-disabled-opacity:var(--van-disabled-opacity)}.van-nav-bar{z-index:var(--van-nav-bar-z-index);line-height:var(--van-line-height-lg);text-align:center;background:var(--van-nav-bar-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.van-nav-bar--fixed{width:100%;position:fixed;top:0;left:0}.van-nav-bar--safe-area-inset-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-nav-bar .van-icon{color:var(--van-nav-bar-icon-color)}.van-nav-bar__content{height:var(--van-nav-bar-height);align-items:center;display:flex;position:relative}.van-nav-bar__arrow{margin-right:var(--van-padding-base);font-size:var(--van-nav-bar-arrow-size)}.van-nav-bar__title{max-width:60%;color:var(--van-nav-bar-title-text-color);font-weight:var(--van-font-bold);font-size:var(--van-nav-bar-title-font-size);margin:0 auto}.van-nav-bar__left,.van-nav-bar__right{padding:0 var(--van-padding-md);font-size:var(--van-font-size-md);align-items:center;display:flex;position:absolute;top:0;bottom:0}.van-nav-bar__left--disabled,.van-nav-bar__right--disabled{cursor:not-allowed;opacity:var(--van-nav-bar-disabled-opacity)}.van-nav-bar__left{left:0}.van-nav-bar__right{right:0}.van-nav-bar__text{color:var(--van-nav-bar-text-color)}:root,:host{--van-tabbar-height:50px;--van-tabbar-z-index:1;--van-tabbar-background:var(--van-background-2)}.van-tabbar{z-index:var(--van-tabbar-z-index);box-sizing:content-box;width:100%;height:var(--van-tabbar-height);background:var(--van-tabbar-background);display:flex}.van-tabbar--fixed{position:fixed;bottom:0;left:0}:root,:host{--van-tabbar-item-font-size:var(--van-font-size-sm);--van-tabbar-item-text-color:var(--van-text-color);--van-tabbar-item-active-color:var(--van-primary-color);--van-tabbar-item-active-background:var(--van-background-2);--van-tabbar-item-line-height:1;--van-tabbar-item-icon-size:22px;--van-tabbar-item-icon-margin-bottom:var(--van-padding-base)}.van-tabbar-item{color:var(--van-tabbar-item-text-color);font-size:var(--van-tabbar-item-font-size);line-height:var(--van-tabbar-item-line-height);cursor:pointer;flex-direction:column;flex:1;justify-content:center;align-items:center;display:flex}.van-tabbar-item__icon{margin-bottom:var(--van-tabbar-item-icon-margin-bottom);font-size:var(--van-tabbar-item-icon-size)}.van-tabbar-item__icon .van-icon{display:block}.van-tabbar-item__icon .van-badge{margin-top:var(--van-padding-base)}.van-tabbar-item__icon img{height:20px;display:block}.van-tabbar-item--active{color:var(--van-tabbar-item-active-color);background-color:var(--van-tabbar-item-active-background)}:root,:host{--van-cell-font-size:var(--van-font-size-md);--van-cell-line-height:24px;--van-cell-vertical-padding:10px;--van-cell-horizontal-padding:var(--van-padding-md);--van-cell-text-color:var(--van-text-color);--van-cell-background:var(--van-background-2);--van-cell-border-color:var(--van-border-color);--van-cell-active-color:var(--van-active-color);--van-cell-required-color:var(--van-danger-color);--van-cell-label-color:var(--van-text-color-2);--van-cell-label-font-size:var(--van-font-size-sm);--van-cell-label-line-height:var(--van-line-height-sm);--van-cell-label-margin-top:var(--van-padding-base);--van-cell-value-color:var(--van-text-color-2);--van-cell-value-font-size:inherit;--van-cell-icon-size:16px;--van-cell-right-icon-color:var(--van-gray-6);--van-cell-large-vertical-padding:var(--van-padding-sm);--van-cell-large-title-font-size:var(--van-font-size-lg);--van-cell-large-label-font-size:var(--van-font-size-md);--van-cell-large-value-font-size:inherit}.van-cell{box-sizing:border-box;width:100%;padding:var(--van-cell-vertical-padding) var(--van-cell-horizontal-padding);color:var(--van-cell-text-color);font-size:var(--van-cell-font-size);line-height:var(--van-cell-line-height);background:var(--van-cell-background);display:flex;position:relative;overflow:hidden}.van-cell:after{box-sizing:border-box;content:" ";pointer-events:none;right:var(--van-padding-md);bottom:0;left:var(--van-padding-md);border-bottom:1px solid var(--van-cell-border-color);position:absolute;transform:scaleY(.5)}.van-cell:last-child:after,.van-cell--borderless:after{display:none}.van-cell__label{margin-top:var(--van-cell-label-margin-top);color:var(--van-cell-label-color);font-size:var(--van-cell-label-font-size);line-height:var(--van-cell-label-line-height)}.van-cell__title,.van-cell__value{flex:1}.van-cell__value{color:var(--van-cell-value-color);font-size:var(--van-cell-value-font-size);text-align:right;vertical-align:middle;word-wrap:break-word;position:relative;overflow:hidden}.van-cell__left-icon,.van-cell__right-icon{height:var(--van-cell-line-height);font-size:var(--van-cell-icon-size);line-height:var(--van-cell-line-height)}.van-cell__left-icon{margin-right:var(--van-padding-base)}.van-cell__right-icon{margin-left:var(--van-padding-base);color:var(--van-cell-right-icon-color)}.van-cell--clickable{cursor:pointer}.van-cell--clickable:active{background-color:var(--van-cell-active-color)}.van-cell--required{overflow:visible}.van-cell--required:before{left:var(--van-padding-xs);color:var(--van-cell-required-color);font-size:var(--van-cell-font-size);content:"*";position:absolute}.van-cell--center{align-items:center}.van-cell--large{padding-top:var(--van-cell-large-vertical-padding);padding-bottom:var(--van-cell-large-vertical-padding)}.van-cell--large .van-cell__title{font-size:var(--van-cell-large-title-font-size)}.van-cell--large .van-cell__label{font-size:var(--van-cell-large-label-font-size)}.van-cell--large .van-cell__value{font-size:var(--van-cell-large-value-font-size)}:root,:host{--van-field-label-width:6.2em;--van-field-label-color:var(--van-text-color);--van-field-label-margin-right:var(--van-padding-sm);--van-field-input-text-color:var(--van-text-color);--van-field-input-error-text-color:var(--van-danger-color);--van-field-input-disabled-text-color:var(--van-text-color-3);--van-field-placeholder-text-color:var(--van-text-color-3);--van-field-icon-size:18px;--van-field-clear-icon-size:18px;--van-field-clear-icon-color:var(--van-gray-5);--van-field-right-icon-color:var(--van-gray-6);--van-field-error-message-color:var(--van-danger-color);--van-field-error-message-font-size:12px;--van-field-text-area-min-height:60px;--van-field-word-limit-color:var(--van-gray-7);--van-field-word-limit-font-size:var(--van-font-size-sm);--van-field-word-limit-line-height:16px;--van-field-disabled-text-color:var(--van-text-color-3);--van-field-required-mark-color:var(--van-red)}.van-field{flex-wrap:wrap}.van-field__label{box-sizing:border-box;width:var(--van-field-label-width);margin-right:var(--van-field-label-margin-right);color:var(--van-field-label-color);text-align:left;word-wrap:break-word;flex:none}.van-field__label--center{text-align:center}.van-field__label--right{text-align:right}.van-field__label--top{text-align:left;width:100%;margin-bottom:var(--van-padding-base);overflow-wrap:break-word;display:flex}.van-field__label--required:before{color:var(--van-field-required-mark-color);content:"*";margin-right:2px}.van-field--disabled .van-field__label{color:var(--van-field-disabled-text-color)}.van-field__value{overflow:visible}.van-field__body{align-items:center;display:flex}.van-field__control{box-sizing:border-box;width:100%;min-width:0;color:var(--van-field-input-text-color);line-height:inherit;text-align:left;resize:none;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto;background-color:#0000;border:0;margin:0;padding:0;display:block}.van-field__control::placeholder{color:var(--van-field-placeholder-text-color)}.van-field__control:-moz-read-only{cursor:default}.van-field__control:read-only{cursor:default}.van-field__control:disabled{color:var(--van-field-input-disabled-text-color);cursor:not-allowed;opacity:1;-webkit-text-fill-color:var(--van-field-input-disabled-text-color)}.van-field__control--center{text-align:center;justify-content:center}.van-field__control--right{text-align:right;justify-content:flex-end}.van-field__control--custom{min-height:var(--van-cell-line-height);align-items:center;display:flex}.van-field__control--error::-webkit-input-placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--error,.van-field__control--error::placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--min-height{min-height:var(--van-field-text-area-min-height)}.van-field__control[type=date],.van-field__control[type=time],.van-field__control[type=datetime-local]{min-height:var(--van-cell-line-height)}.van-field__control[type=search]{-webkit-appearance:none}.van-field__clear,.van-field__icon,.van-field__button,.van-field__right-icon{flex-shrink:0}.van-field__clear,.van-field__right-icon{margin-right:calc(var(--van-padding-xs) * -1);padding:0 var(--van-padding-xs);line-height:inherit}.van-field__clear{color:var(--van-field-clear-icon-color);font-size:var(--van-field-clear-icon-size);cursor:pointer}.van-field__left-icon .van-icon,.van-field__right-icon .van-icon{font-size:var(--van-field-icon-size);line-height:inherit;display:block}.van-field__left-icon{margin-right:var(--van-padding-base)}.van-field__right-icon{color:var(--van-field-right-icon-color)}.van-field__button{padding-left:var(--van-padding-xs)}.van-field__error-message{color:var(--van-field-error-message-color);font-size:var(--van-field-error-message-font-size);text-align:left}.van-field__error-message--center{text-align:center}.van-field__error-message--right{text-align:right}.van-field__word-limit{margin-top:var(--van-padding-base);color:var(--van-field-word-limit-color);font-size:var(--van-field-word-limit-font-size);line-height:var(--van-field-word-limit-line-height);text-align:right}:root,:host{--van-cell-group-background:var(--van-background-2);--van-cell-group-title-color:var(--van-text-color-2);--van-cell-group-title-padding:var(--van-padding-md) var(--van-padding-md);--van-cell-group-title-font-size:var(--van-font-size-md);--van-cell-group-title-line-height:16px;--van-cell-group-inset-padding:0 var(--van-padding-md);--van-cell-group-inset-radius:var(--van-radius-lg);--van-cell-group-inset-title-padding:var(--van-padding-md) var(--van-padding-md)}.van-cell-group{background:var(--van-cell-group-background)}.van-cell-group--inset{margin:var(--van-cell-group-inset-padding);border-radius:var(--van-cell-group-inset-radius);overflow:hidden}.van-cell-group__title{padding:var(--van-cell-group-title-padding);color:var(--van-cell-group-title-color);font-size:var(--van-cell-group-title-font-size);line-height:var(--van-cell-group-title-line-height)}.van-cell-group__title--inset{padding:var(--van-cell-group-inset-title-padding)}:root,:host{--van-loading-text-color:var(--van-text-color-2);--van-loading-text-font-size:var(--van-font-size-md);--van-loading-spinner-color:var(--van-gray-5);--van-loading-spinner-size:30px;--van-loading-spinner-duration:.8s}.van-loading{color:var(--van-loading-spinner-color);vertical-align:middle;font-size:0;position:relative}.van-loading__spinner{width:var(--van-loading-spinner-size);max-width:100%;height:var(--van-loading-spinner-size);vertical-align:middle;max-height:100%;animation:van-rotate var(--van-loading-spinner-duration) linear infinite;display:inline-block;position:relative}.van-loading__spinner--spinner{animation-timing-function:steps(12,end)}.van-loading__spinner--circular{animation-duration:2s}.van-loading__line{width:100%;height:100%;position:absolute;top:0;left:0}.van-loading__line:before{content:" ";background-color:currentColor;border-radius:40%;width:2px;height:25%;margin:0 auto;display:block}.van-loading__circular{width:100%;height:100%;display:block}.van-loading__circular circle{stroke:currentColor;stroke-width:3px;stroke-linecap:round;animation:1.5s ease-in-out infinite van-circular}.van-loading__text{margin-left:var(--van-padding-xs);color:var(--van-loading-text-color);font-size:var(--van-loading-text-font-size);vertical-align:middle;display:inline-block}.van-loading--vertical{flex-direction:column;align-items:center;display:flex}.van-loading--vertical .van-loading__text{margin:var(--van-padding-xs) 0 0}@keyframes van-circular{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-40px}to{stroke-dasharray:90 150;stroke-dashoffset:-120px}}.van-loading__line--1{opacity:1;transform:rotate(30deg)}.van-loading__line--2{opacity:.9375;transform:rotate(60deg)}.van-loading__line--3{opacity:.875;transform:rotate(90deg)}.van-loading__line--4{opacity:.8125;transform:rotate(120deg)}.van-loading__line--5{opacity:.75;transform:rotate(150deg)}.van-loading__line--6{opacity:.6875;transform:rotate(180deg)}.van-loading__line--7{opacity:.625;transform:rotate(210deg)}.van-loading__line--8{opacity:.5625;transform:rotate(240deg)}.van-loading__line--9{opacity:.5;transform:rotate(270deg)}.van-loading__line--10{opacity:.4375;transform:rotate(300deg)}.van-loading__line--11{opacity:.375;transform:rotate(330deg)}.van-loading__line--12{opacity:.3125;transform:rotate(360deg)}:root,:host{--van-button-mini-height:24px;--van-button-mini-padding:0 var(--van-padding-base);--van-button-mini-font-size:var(--van-font-size-xs);--van-button-small-height:32px;--van-button-small-padding:0 var(--van-padding-xs);--van-button-small-font-size:var(--van-font-size-sm);--van-button-normal-padding:0 15px;--van-button-normal-font-size:var(--van-font-size-md);--van-button-large-height:50px;--van-button-default-height:44px;--van-button-default-line-height:1.2;--van-button-default-font-size:var(--van-font-size-lg);--van-button-default-color:var(--van-text-color);--van-button-default-background:var(--van-background-2);--van-button-default-border-color:var(--van-gray-4);--van-button-primary-color:var(--van-white);--van-button-primary-background:var(--van-primary-color);--van-button-primary-border-color:var(--van-primary-color);--van-button-success-color:var(--van-white);--van-button-success-background:var(--van-success-color);--van-button-success-border-color:var(--van-success-color);--van-button-danger-color:var(--van-white);--van-button-danger-background:var(--van-danger-color);--van-button-danger-border-color:var(--van-danger-color);--van-button-warning-color:var(--van-white);--van-button-warning-background:var(--van-warning-color);--van-button-warning-border-color:var(--van-warning-color);--van-button-border-width:var(--van-border-width);--van-button-radius:var(--van-radius-md);--van-button-round-radius:var(--van-radius-max);--van-button-plain-background:var(--van-white);--van-button-disabled-opacity:var(--van-disabled-opacity);--van-button-icon-size:1.2em;--van-button-loading-icon-size:20px}.van-theme-dark{--van-button-plain-background:transparent}.van-button{box-sizing:border-box;height:var(--van-button-default-height);font-size:var(--van-button-default-font-size);line-height:var(--van-button-default-line-height);text-align:center;border-radius:var(--van-button-radius);cursor:pointer;transition:opacity var(--van-duration-fast);-webkit-appearance:none;-webkit-font-smoothing:auto;margin:0;padding:0;display:inline-block;position:relative}.van-button:before{background:var(--van-black);border:inherit;border-color:var(--van-black);border-radius:inherit;opacity:0;content:" ";box-sizing:content-box;width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.van-button:active:before{opacity:.1}.van-button--loading:before,.van-button--disabled:before{display:none}.van-button--default{color:var(--van-button-default-color);background:var(--van-button-default-background);border:var(--van-button-border-width) solid var(--van-button-default-border-color)}.van-button--primary{color:var(--van-button-primary-color);background:var(--van-button-primary-background);border:var(--van-button-border-width) solid var(--van-button-primary-border-color)}.van-button--success{color:var(--van-button-success-color);background:var(--van-button-success-background);border:var(--van-button-border-width) solid var(--van-button-success-border-color)}.van-button--danger{color:var(--van-button-danger-color);background:var(--van-button-danger-background);border:var(--van-button-border-width) solid var(--van-button-danger-border-color)}.van-button--warning{color:var(--van-button-warning-color);background:var(--van-button-warning-background);border:var(--van-button-border-width) solid var(--van-button-warning-border-color)}.van-button--plain{background:var(--van-button-plain-background)}.van-button--plain.van-button--primary{color:var(--van-button-primary-background)}.van-button--plain.van-button--success{color:var(--van-button-success-background)}.van-button--plain.van-button--danger{color:var(--van-button-danger-background)}.van-button--plain.van-button--warning{color:var(--van-button-warning-background)}.van-button--large{width:100%;height:var(--van-button-large-height)}.van-button--normal{padding:var(--van-button-normal-padding);font-size:var(--van-button-normal-font-size)}.van-button--small{height:var(--van-button-small-height);padding:var(--van-button-small-padding);font-size:var(--van-button-small-font-size)}.van-button__loading{color:inherit;font-size:inherit}.van-button__loading .van-loading__spinner{color:currentColor;width:var(--van-button-loading-icon-size);height:var(--van-button-loading-icon-size)}.van-button--mini{height:var(--van-button-mini-height);padding:var(--van-button-mini-padding);font-size:var(--van-button-mini-font-size)}.van-button--mini+.van-button--mini{margin-left:var(--van-padding-base)}.van-button--block{width:100%;display:block}.van-button--disabled{cursor:not-allowed;opacity:var(--van-button-disabled-opacity)}.van-button--loading{cursor:default}.van-button--round{border-radius:var(--van-button-round-radius)}.van-button--square{border-radius:0}.van-button__content{justify-content:center;align-items:center;height:100%;display:flex}.van-button__content:before{content:" "}.van-button__icon{font-size:var(--van-button-icon-size);line-height:inherit}.van-button__icon+.van-button__text,.van-button__loading+.van-button__text,.van-button__text+.van-button__icon,.van-button__text+.van-button__loading{margin-left:var(--van-padding-base)}.van-button--hairline{border-width:0}.van-button--hairline:after{border-color:inherit;border-radius:calc(var(--van-button-radius) * 2)}.van-button--hairline.van-button--round:after{border-radius:var(--van-button-round-radius)}.van-button--hairline.van-button--square:after{border-radius:0}:root,:host{--van-search-padding:10px var(--van-padding-sm);--van-search-background:var(--van-background-2);--van-search-content-background:var(--van-background);--van-search-input-height:34px;--van-search-label-padding:0 5px;--van-search-label-color:var(--van-text-color);--van-search-label-font-size:var(--van-font-size-md);--van-search-left-icon-color:var(--van-gray-6);--van-search-action-padding:0 var(--van-padding-xs);--van-search-action-text-color:var(--van-text-color);--van-search-action-font-size:var(--van-font-size-md)}.van-search{box-sizing:border-box;padding:var(--van-search-padding);background:var(--van-search-background);align-items:center;display:flex}.van-search__content{padding-left:var(--van-padding-sm);background:var(--van-search-content-background);border-radius:var(--van-radius-sm);flex:1;display:flex}.van-search__content--round{border-radius:var(--van-radius-max)}.van-search__label{padding:var(--van-search-label-padding);color:var(--van-search-label-color);font-size:var(--van-search-label-font-size);line-height:var(--van-search-input-height)}.van-search__field{padding:0 var(--van-padding-xs) 0 0;height:var(--van-search-input-height);background-color:#0000;flex:1;align-items:center}.van-search__field .van-field__left-icon{color:var(--van-search-left-icon-color)}.van-search__field--with-message{align-items:flex-start;height:auto;padding-top:5px;padding-bottom:5px}.van-search--show-action{padding-right:0}.van-search input::-webkit-search-decoration{display:none}.van-search input::-webkit-search-cancel-button{display:none}.van-search input::-webkit-search-results-button{display:none}.van-search input::-webkit-search-results-decoration{display:none}.van-search__action{padding:var(--van-search-action-padding);color:var(--van-search-action-text-color);font-size:var(--van-search-action-font-size);line-height:var(--van-search-input-height);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.van-search__action:active{background-color:var(--van-active-color)}:root,:host{--van-list-text-color:var(--van-text-color-2);--van-list-text-font-size:var(--van-font-size-md);--van-list-text-line-height:50px;--van-list-loading-icon-size:16px}.van-list__loading,.van-list__finished-text,.van-list__error-text{color:var(--van-list-text-color);font-size:var(--van-list-text-font-size);line-height:var(--van-list-text-line-height);text-align:center}.van-list__placeholder{pointer-events:none;height:0}.van-list__loading-icon .van-loading__spinner{width:var(--van-list-loading-icon-size);height:var(--van-list-loading-icon-size)}:root,:host{--van-pull-refresh-head-height:50px;--van-pull-refresh-head-font-size:var(--van-font-size-md);--van-pull-refresh-head-text-color:var(--van-text-color-2);--van-pull-refresh-loading-icon-size:16px}.van-pull-refresh{overflow:hidden}.van-pull-refresh__track{height:100%;transition-property:transform;position:relative}.van-pull-refresh__head{width:100%;height:var(--van-pull-refresh-head-height);color:var(--van-pull-refresh-head-text-color);font-size:var(--van-pull-refresh-head-font-size);line-height:var(--van-pull-refresh-head-height);text-align:center;position:absolute;left:0;overflow:hidden;transform:translateY(-100%)}.van-pull-refresh__loading .van-loading__spinner{width:var(--van-pull-refresh-loading-icon-size);height:var(--van-pull-refresh-loading-icon-size)}:root,:host{--van-tag-padding:0 var(--van-padding-base);--van-tag-text-color:var(--van-white);--van-tag-font-size:var(--van-font-size-sm);--van-tag-radius:2px;--van-tag-line-height:16px;--van-tag-medium-padding:2px 6px;--van-tag-large-padding:var(--van-padding-base) var(--van-padding-xs);--van-tag-large-radius:var(--van-radius-md);--van-tag-large-font-size:var(--van-font-size-md);--van-tag-round-radius:var(--van-radius-max);--van-tag-danger-color:var(--van-danger-color);--van-tag-primary-color:var(--van-primary-color);--van-tag-success-color:var(--van-success-color);--van-tag-warning-color:var(--van-warning-color);--van-tag-default-color:var(--van-gray-6);--van-tag-plain-background:var(--van-background-2)}.van-tag{padding:var(--van-tag-padding);color:var(--van-tag-text-color);font-size:var(--van-tag-font-size);line-height:var(--van-tag-line-height);border-radius:var(--van-tag-radius);align-items:center;display:inline-flex;position:relative}.van-tag--default{background:var(--van-tag-default-color)}.van-tag--default.van-tag--plain{color:var(--van-tag-default-color)}.van-tag--danger{background:var(--van-tag-danger-color)}.van-tag--danger.van-tag--plain{color:var(--van-tag-danger-color)}.van-tag--primary{background:var(--van-tag-primary-color)}.van-tag--primary.van-tag--plain{color:var(--van-tag-primary-color)}.van-tag--success{background:var(--van-tag-success-color)}.van-tag--success.van-tag--plain{color:var(--van-tag-success-color)}.van-tag--warning{background:var(--van-tag-warning-color)}.van-tag--warning.van-tag--plain{color:var(--van-tag-warning-color)}.van-tag--plain{background:var(--van-tag-plain-background);border-color:currentColor}.van-tag--plain:before{border:1px solid;border-color:inherit;border-radius:inherit;content:"";pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.van-tag--medium{padding:var(--van-tag-medium-padding)}.van-tag--large{padding:var(--van-tag-large-padding);font-size:var(--van-tag-large-font-size);border-radius:var(--van-tag-large-radius)}.van-tag--mark{border-radius:0 var(--van-tag-round-radius) var(--van-tag-round-radius) 0}.van-tag--mark:after{content:"";width:2px;display:block}.van-tag--round{border-radius:var(--van-tag-round-radius)}.van-tag__close{margin-left:2px}:root,:host{--van-sticky-z-index:99}.van-sticky--fixed{z-index:var(--van-sticky-z-index);position:fixed}:root,:host{--van-swipe-indicator-size:6px;--van-swipe-indicator-margin:var(--van-padding-sm);--van-swipe-indicator-active-opacity:1;--van-swipe-indicator-inactive-opacity:.3;--van-swipe-indicator-active-background:var(--van-primary-color);--van-swipe-indicator-inactive-background:var(--van-border-color)}.van-swipe{cursor:-webkit-grab;cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;overflow:hidden;transform:translateZ(0)}.van-swipe__track{height:100%;transition-property:transform;display:flex}.van-swipe__track--vertical{flex-direction:column}.van-swipe__indicators{bottom:var(--van-swipe-indicator-margin);display:flex;position:absolute;left:50%;transform:translate(-50%)}.van-swipe__indicators--vertical{top:50%;bottom:auto;left:var(--van-swipe-indicator-margin);flex-direction:column;transform:translateY(-50%)}.van-swipe__indicators--vertical .van-swipe__indicator:not(:last-child){margin-bottom:var(--van-swipe-indicator-size)}.van-swipe__indicator{width:var(--van-swipe-indicator-size);height:var(--van-swipe-indicator-size);background-color:var(--van-swipe-indicator-inactive-background);opacity:var(--van-swipe-indicator-inactive-opacity);transition:opacity var(--van-duration-fast),background-color var(--van-duration-fast);border-radius:100%}.van-swipe__indicator:not(:last-child){margin-right:var(--van-swipe-indicator-size)}.van-swipe__indicator--active{background-color:var(--van-swipe-indicator-active-background);opacity:var(--van-swipe-indicator-active-opacity)}:root,:host{--van-tab-text-color:var(--van-gray-7);--van-tab-active-text-color:var(--van-text-color);--van-tab-disabled-text-color:var(--van-text-color-3);--van-tab-font-size:var(--van-font-size-md);--van-tab-line-height:var(--van-line-height-md);--van-tabs-default-color:var(--van-primary-color);--van-tabs-line-height:44px;--van-tabs-card-height:30px;--van-tabs-nav-background:var(--van-background-2);--van-tabs-bottom-bar-width:40px;--van-tabs-bottom-bar-height:3px;--van-tabs-bottom-bar-color:var(--van-primary-color)}.van-tab{box-sizing:border-box;padding:0 var(--van-padding-base);color:var(--van-tab-text-color);font-size:var(--van-tab-font-size);line-height:var(--van-tab-line-height);cursor:pointer;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.van-tab--active{color:var(--van-tab-active-text-color);font-weight:var(--van-font-bold)}.van-tab--disabled{color:var(--van-tab-disabled-text-color);cursor:not-allowed}.van-tab--grow{padding:0 var(--van-padding-sm);flex:1 0 auto}.van-tab--shrink{padding:0 var(--van-padding-xs);flex:none}.van-tab--card{color:var(--van-tabs-default-color);border-right:var(--van-border-width) solid var(--van-tabs-default-color)}.van-tab--card:last-child{border-right:none}.van-tab--card.van-tab--active{color:var(--van-white);background-color:var(--van-tabs-default-color)}.van-tab--card.van-tab--disabled{color:var(--van-tab-disabled-text-color)}.van-tab__text--ellipsis{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-tabs{position:relative}.van-tabs__wrap{overflow:hidden}.van-tabs__wrap--page-top{position:fixed}.van-tabs__wrap--content-bottom{top:auto;bottom:0}.van-tabs__nav{background:var(--van-tabs-nav-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;position:relative}.van-tabs__nav--complete{-webkit-overflow-scrolling:touch;overflow-x:auto;overflow-y:hidden}.van-tabs__nav--complete::-webkit-scrollbar{display:none}.van-tabs__nav--line{box-sizing:content-box;height:100%;padding-bottom:15px}.van-tabs__nav--line.van-tabs__nav--shrink,.van-tabs__nav--line.van-tabs__nav--complete{padding-right:var(--van-padding-xs);padding-left:var(--van-padding-xs)}.van-tabs__nav--card{box-sizing:border-box;height:var(--van-tabs-card-height);margin:0 var(--van-padding-md);border:var(--van-border-width) solid var(--van-tabs-default-color);border-radius:var(--van-radius-sm)}.van-tabs__nav--card.van-tabs__nav--shrink{display:inline-flex}.van-tabs__line{z-index:1;width:var(--van-tabs-bottom-bar-width);height:var(--van-tabs-bottom-bar-height);background:var(--van-tabs-bottom-bar-color);border-radius:var(--van-tabs-bottom-bar-height);position:absolute;bottom:15px;left:0}.van-tabs__track{will-change:left;width:100%;height:100%;display:flex;position:relative}.van-tabs__content--animated{overflow:hidden}.van-tabs--line .van-tabs__wrap{height:var(--van-tabs-line-height)}.van-tabs--card>.van-tabs__wrap{height:var(--van-tabs-card-height)}.van-swipe-item{flex-shrink:0;width:100%;height:100%;position:relative}.van-tab__panel,.van-tab__panel-wrapper{box-sizing:border-box;flex-shrink:0;width:100%}.van-tab__panel-wrapper--inactive{height:0;overflow:visible}:root,:host{--van-overlay-z-index:1;--van-overlay-background:#000000b3}.van-overlay{z-index:var(--van-overlay-z-index);background:var(--van-overlay-background);width:100%;height:100%;position:fixed;top:0;left:0}:root,:host{--van-popup-background:var(--van-background-2);--van-popup-transition:transform var(--van-duration-base);--van-popup-round-radius:16px;--van-popup-close-icon-size:22px;--van-popup-close-icon-color:var(--van-gray-5);--van-popup-close-icon-margin:16px;--van-popup-close-icon-z-index:1}.van-overflow-hidden{overflow:hidden!important}.van-popup{box-sizing:border-box;background:var(--van-popup-background);max-height:100%;transition:var(--van-popup-transition);-webkit-overflow-scrolling:touch;position:fixed;overflow-y:auto}.van-popup--center{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100vw - var(--van-padding-md) * 2);margin:0 auto;top:50%;left:0;right:0;transform:translateY(-50%)}.van-popup--center.van-popup--round{border-radius:var(--van-popup-round-radius)}.van-popup--top{width:100%;top:0;left:0}.van-popup--top.van-popup--round{border-radius:0 0 var(--van-popup-round-radius) var(--van-popup-round-radius)}.van-popup--right{top:50%;right:0;transform:translateY(-50%)}.van-popup--right.van-popup--round{border-radius:var(--van-popup-round-radius) 0 0 var(--van-popup-round-radius)}.van-popup--bottom{width:100%;bottom:0;left:0}.van-popup--bottom.van-popup--round{border-radius:var(--van-popup-round-radius) var(--van-popup-round-radius) 0 0}.van-popup--left{top:50%;left:0;transform:translateY(-50%)}.van-popup--left.van-popup--round{border-radius:0 var(--van-popup-round-radius) var(--van-popup-round-radius) 0}.van-popup-slide-top-enter-active,.van-popup-slide-left-enter-active,.van-popup-slide-right-enter-active,.van-popup-slide-bottom-enter-active{transition-timing-function:var(--van-ease-out)}.van-popup-slide-top-leave-active,.van-popup-slide-left-leave-active,.van-popup-slide-right-leave-active,.van-popup-slide-bottom-leave-active{transition-timing-function:var(--van-ease-in)}.van-popup-slide-top-enter-from,.van-popup-slide-top-leave-active{transform:translateY(-100%)}.van-popup-slide-right-enter-from,.van-popup-slide-right-leave-active{transform:translate(100%,-50%)}.van-popup-slide-bottom-enter-from,.van-popup-slide-bottom-leave-active{transform:translateY(100%)}.van-popup-slide-left-enter-from,.van-popup-slide-left-leave-active{transform:translate(-100%,-50%)}.van-popup__close-icon{z-index:var(--van-popup-close-icon-z-index);color:var(--van-popup-close-icon-color);font-size:var(--van-popup-close-icon-size);position:absolute}.van-popup__close-icon--top-left{top:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--top-right{top:var(--van-popup-close-icon-margin);right:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-left{bottom:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-right{right:var(--van-popup-close-icon-margin);bottom:var(--van-popup-close-icon-margin)}:root,:host{--van-empty-padding:var(--van-padding-xl) 0;--van-empty-image-size:160px;--van-empty-description-margin-top:var(--van-padding-md);--van-empty-description-padding:0 60px;--van-empty-description-color:var(--van-text-color-2);--van-empty-description-font-size:var(--van-font-size-md);--van-empty-description-line-height:var(--van-line-height-md);--van-empty-bottom-margin-top:24px}.van-empty{box-sizing:border-box;padding:var(--van-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-empty__image{width:var(--van-empty-image-size);height:var(--van-empty-image-size)}.van-empty__image img{width:100%;height:100%}.van-empty__description{margin-top:var(--van-empty-description-margin-top);padding:var(--van-empty-description-padding);color:var(--van-empty-description-color);font-size:var(--van-empty-description-font-size);line-height:var(--van-empty-description-line-height)}.van-empty__bottom{margin-top:var(--van-empty-bottom-margin-top)}.van-theme-dark .van-empty{opacity:.5}:root,:host{--van-notice-bar-height:40px;--van-notice-bar-padding:0 var(--van-padding-md);--van-notice-bar-wrapable-padding:var(--van-padding-xs) var(--van-padding-md);--van-notice-bar-text-color:var(--van-orange-dark);--van-notice-bar-font-size:var(--van-font-size-md);--van-notice-bar-line-height:24px;--van-notice-bar-background:var(--van-orange-light);--van-notice-bar-icon-size:16px;--van-notice-bar-icon-min-width:24px}.van-notice-bar{height:var(--van-notice-bar-height);padding:var(--van-notice-bar-padding);color:var(--van-notice-bar-text-color);font-size:var(--van-notice-bar-font-size);line-height:var(--van-notice-bar-line-height);background:var(--van-notice-bar-background);align-items:center;display:flex;position:relative}.van-notice-bar__left-icon,.van-notice-bar__right-icon{min-width:var(--van-notice-bar-icon-min-width);font-size:var(--van-notice-bar-icon-size)}.van-notice-bar__right-icon{text-align:right;cursor:pointer}.van-notice-bar__wrap{flex:1;align-items:center;height:100%;display:flex;position:relative;overflow:hidden}.van-notice-bar__content{white-space:nowrap;transition-timing-function:linear;position:absolute}.van-notice-bar__content.van-ellipsis{max-width:100%}.van-notice-bar--wrapable{height:auto;padding:var(--van-notice-bar-wrapable-padding)}.van-notice-bar--wrapable .van-notice-bar__wrap{height:auto}.van-notice-bar--wrapable .van-notice-bar__content{white-space:normal;word-wrap:break-word;position:relative}:root,:host{--van-skeleton-title-width:40%}.van-skeleton-title{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-title--round{border-radius:var(--van-radius-max)}.van-skeleton-title{width:var(--van-skeleton-title-width);margin:0}.van-skeleton-title+.van-skeleton-paragraph{margin-top:20px}:root,:host{--van-skeleton-avatar-size:32px;--van-skeleton-avatar-background:var(--van-active-color)}.van-skeleton-avatar{width:var(--van-skeleton-avatar-size);height:var(--van-skeleton-avatar-size);margin-right:var(--van-padding-md);background:var(--van-skeleton-avatar-background);flex-shrink:0}.van-skeleton-avatar--round{border-radius:var(--van-radius-max)}.van-skeleton-avatar+.van-skeleton__content{padding-top:var(--van-padding-xs)}:root,:host{--van-skeleton-paragraph-height:16px;--van-skeleton-paragraph-background:var(--van-active-color);--van-skeleton-paragraph-margin-top:var(--van-padding-sm)}.van-skeleton-paragraph{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-paragraph--round{border-radius:var(--van-radius-max)}.van-skeleton-paragraph:not(:first-child){margin-top:var(--van-skeleton-paragraph-margin-top)}:root,:host{--van-skeleton-duration:1.2s}.van-skeleton{padding:0 var(--van-padding-md);display:flex}.van-skeleton__content{width:100%}.van-skeleton--animate{animation:van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite}@keyframes van-skeleton-blink{50%{opacity:.6}}:root,:host{--van-action-bar-background:var(--van-background-2);--van-action-bar-height:50px}.van-action-bar{box-sizing:content-box;height:var(--van-action-bar-height);background:var(--van-action-bar-background);align-items:center;display:flex;position:fixed;bottom:0;left:0;right:0}:root,:host{--van-action-bar-button-height:40px;--van-action-bar-button-warning-color:var(--van-gradient-orange);--van-action-bar-button-danger-color:var(--van-gradient-red)}.van-action-bar-button{height:var(--van-action-bar-button-height);font-weight:var(--van-font-bold);font-size:var(--van-font-size-md);border:none;border-radius:0;flex:1}.van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max);margin-left:5px}.van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max);margin-right:5px}.van-action-bar-button--warning{background:var(--van-action-bar-button-warning-color)}.van-action-bar-button--danger{background:var(--van-action-bar-button-danger-color)}@media (max-width:321px){.van-action-bar-button{font-size:13px}}:root,:host{--van-dialog-width:320px;--van-dialog-small-screen-width:90%;--van-dialog-font-size:var(--van-font-size-lg);--van-dialog-transition:var(--van-duration-base);--van-dialog-radius:16px;--van-dialog-background:var(--van-background-2);--van-dialog-header-font-weight:var(--van-font-bold);--van-dialog-header-line-height:24px;--van-dialog-header-padding-top:26px;--van-dialog-header-isolated-padding:var(--van-padding-lg) 0;--van-dialog-message-padding:var(--van-padding-lg);--van-dialog-message-font-size:var(--van-font-size-md);--van-dialog-message-line-height:var(--van-line-height-md);--van-dialog-message-max-height:60vh;--van-dialog-has-title-message-text-color:var(--van-gray-7);--van-dialog-has-title-message-padding-top:var(--van-padding-xs);--van-dialog-button-height:48px;--van-dialog-round-button-height:36px;--van-dialog-confirm-button-text-color:var(--van-primary-color)}.van-dialog{width:var(--van-dialog-width);font-size:var(--van-dialog-font-size);background:var(--van-dialog-background);border-radius:var(--van-dialog-radius);-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:var(--van-dialog-transition);transition-property:transform,opacity;top:45%;overflow:hidden}@media (max-width:321px){.van-dialog{width:var(--van-dialog-small-screen-width)}}.van-dialog__header{color:var(--van-text-color);padding-top:var(--van-dialog-header-padding-top);font-weight:var(--van-dialog-header-font-weight);line-height:var(--van-dialog-header-line-height);text-align:center}.van-dialog__header--isolated{padding:var(--van-dialog-header-isolated-padding)}.van-dialog__content--isolated{align-items:center;min-height:104px;display:flex}.van-dialog__message{color:var(--van-text-color);max-height:var(--van-dialog-message-max-height);padding:26px var(--van-dialog-message-padding);font-size:var(--van-dialog-message-font-size);line-height:var(--van-dialog-message-line-height);white-space:pre-wrap;text-align:center;word-wrap:break-word;-webkit-overflow-scrolling:touch;flex:1;overflow-y:auto}.van-dialog__message--has-title{padding-top:var(--van-dialog-has-title-message-padding-top);color:var(--van-dialog-has-title-message-text-color)}.van-dialog__message--left{text-align:left}.van-dialog__message--right{text-align:right}.van-dialog__message--justify{text-align:justify}.van-dialog__footer{-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;overflow:hidden}.van-dialog__confirm,.van-dialog__cancel{height:var(--van-dialog-button-height);border:0;border-radius:0;flex:1;margin:0}.van-dialog__confirm,.van-dialog__confirm:active{color:var(--van-dialog-confirm-button-text-color)}.van-dialog--round-button .van-dialog__footer{height:auto;padding:var(--van-padding-xs) var(--van-padding-lg) var(--van-padding-md);position:relative}.van-dialog--round-button .van-dialog__message{padding-bottom:var(--van-padding-md);color:var(--van-text-color)}.van-dialog--round-button .van-dialog__confirm,.van-dialog--round-button .van-dialog__cancel{height:var(--van-dialog-round-button-height)}.van-dialog--round-button .van-dialog__confirm{color:var(--van-white)}.van-dialog--round-button .van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max)}.van-dialog--round-button .van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max)}.van-dialog-bounce-enter-from{opacity:0;transform:translateY(-50%)scale(.7)}.van-dialog-bounce-leave-active{opacity:0;transform:translateY(-50%)scale(.9)}:root,:host{--van-toast-max-width:70%;--van-toast-font-size:var(--van-font-size-md);--van-toast-text-color:var(--van-white);--van-toast-loading-icon-color:var(--van-white);--van-toast-line-height:var(--van-line-height-md);--van-toast-radius:var(--van-radius-lg);--van-toast-background:#000000b3;--van-toast-icon-size:36px;--van-toast-text-min-width:96px;--van-toast-text-padding:var(--van-padding-xs) var(--van-padding-sm);--van-toast-default-padding:var(--van-padding-md);--van-toast-default-width:88px;--van-toast-default-min-height:88px;--van-toast-position-top-distance:20%;--van-toast-position-bottom-distance:20%}.van-toast{box-sizing:content-box;transition:all var(--van-duration-fast);width:var(--van-toast-default-width);max-width:var(--van-toast-max-width);min-height:var(--van-toast-default-min-height);padding:var(--van-toast-default-padding);color:var(--van-toast-text-color);font-size:var(--van-toast-font-size);line-height:var(--van-toast-line-height);white-space:pre-wrap;word-break:break-all;text-align:center;background:var(--van-toast-background);border-radius:var(--van-toast-radius);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-toast--break-normal{word-break:normal;word-wrap:normal}.van-toast--break-word{word-break:normal;word-wrap:break-word}.van-toast--unclickable{cursor:not-allowed;overflow:hidden}.van-toast--unclickable *{pointer-events:none}.van-toast--text,.van-toast--html{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;min-width:var(--van-toast-text-min-width);min-height:0;padding:var(--van-toast-text-padding)}.van-toast--text .van-toast__text,.van-toast--html .van-toast__text{margin-top:0}.van-toast--top{top:var(--van-toast-position-top-distance)}.van-toast--bottom{top:auto;bottom:var(--van-toast-position-bottom-distance)}.van-toast__icon{font-size:var(--van-toast-icon-size)}.van-toast__loading{padding:var(--van-padding-base);color:var(--van-toast-loading-icon-color)}.van-toast__text{margin-top:var(--van-padding-xs)}:root{--app-bg:#f4f7fd;--app-surface:#fff;--app-surface-2:#f6f8fe;--app-border:#6e89c92e;--app-text:#1d315f;--app-text-2:#3f527d;--app-muted:#7b89ab;--app-primary:#2f6fff;--app-primary-soft:#2f6fff1a;--app-success:#0f9e69;--app-success-soft:#13b77a1f;--app-warning:#c46f00;--app-warning-soft:#ff981d24;--app-danger:#e5383e;--app-danger-soft:#ff53581f;--app-muted-soft:#7b89ab24;--app-shadow:0 10px 28px #4e63a214;--app-radius:16px;--app-font:"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;--app-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root{--app-bg:#0b1222;--app-surface:#131c31;--app-surface-2:#18223b;--app-border:#8ca0d229;--app-text:#e6ecfb;--app-text-2:#bcc6e0;--app-muted:#8b97b6;--app-primary:#6f9bff;--app-primary-soft:#5b8cff29;--app-success:#2fd19a;--app-success-soft:#2fd19a24;--app-warning:#ffb45a;--app-warning-soft:#ffae4a24;--app-danger:#ff7a7f;--app-danger-soft:#ff6b7026;--app-muted-soft:#8390b029;--app-shadow:0 10px 28px #00000047;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*,:before,:after{box-sizing:border-box}html,body{background:var(--app-bg);color:var(--app-text);font-family:var(--app-font);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body,#app{min-height:100vh;min-height:100dvh}button,input,textarea{font-family:inherit}.app-card{background:var(--app-surface);border:1px solid var(--app-border);border-radius:var(--app-radius);box-shadow:var(--app-shadow)}.app-muted{color:var(--app-muted)}.app-mono{font-family:var(--app-mono);word-break:break-all;font-size:12px}.tone-success{color:var(--app-success)}.tone-warning{color:var(--app-warning)}.tone-danger{color:var(--app-danger)}.tone-muted{color:var(--app-muted)}.tone-primary{color:var(--app-primary)}.van-nav-bar .van-nav-bar__title{font-weight:600}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js new file mode 100644 index 000000000..6b213d033 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js @@ -0,0 +1 @@ +import{c as e}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{Dt as t,Kt as n,M as r,Mt as i,N as a,St as o,Vt as s,W as c,at as l,it as u}from"./index-C_M75pH8.js";function d(e,{args:t=[],done:n,canceled:r,error:i}){if(e){let a=e.apply(null,t);u(a)?a.then(e=>{e?n():r&&r()}).catch(i||l):a?n():r&&r()}else n()}var f=(o,l)=>{let u=n(),d=()=>{u.value=c(o).height};return i(()=>{if(t(d),l)for(let e=1;e<=3;e++)setTimeout(d,100*e)}),e(()=>t(d)),s([a,r],d),u};function p(e,t){let n=f(e,!0);return e=>o(`div`,{class:t(`placeholder`),style:{height:n.value?`${n.value}px`:void 0}},[e()])}export{d as n,p as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png differ diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png differ diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png differ diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png differ diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png differ diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/index.html b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/index.html new file mode 100644 index 000000000..48cbab769 --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + + + +
+ + + diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest new file mode 100644 index 000000000..bc53828cc --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话忽略。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/sw.js b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/deploy/mobile-ignore-20260929/payload/admin-web/apps/mobile-h5/dist/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/deploy/mobile-ignore-20260929/prepare_bundle.py b/deploy/mobile-ignore-20260929/prepare_bundle.py new file mode 100644 index 000000000..acdf6b26b --- /dev/null +++ b/deploy/mobile-ignore-20260929/prepare_bundle.py @@ -0,0 +1,97 @@ +"""Assemble the server bundle locally: payload + manifest + tar.gz. No upload, no deployment. + + python prepare_bundle.py + +Sources: the rebuilt C:/kefu/admin-web/apps/mobile-h5/dist. +Production /m/ is exactly the mobile-http-20260928 payload, so the replaced file is +diffed against that payload and the diff is written to payload-diff.txt for review. +""" +from pathlib import Path +import difflib, hashlib, json, shutil, tarfile + +folder = Path(__file__).resolve().parent +kefu = folder.parent.parent +previous = folder.parent / 'mobile-http-20260928' / 'payload' +plan = json.loads((folder / 'release-plan.json').read_text(encoding='utf-8')) +base = json.loads((folder / 'server-baseline.json').read_text(encoding='utf-8')) +sha = lambda data: hashlib.sha256(data).hexdigest() +lf = lambda data: data.replace(b'\r\n', b'\n') +frontend = plan['frontendDir'] +BANNER = '黑名单'.encode() + +# 1. The recorded baseline is what the previous release put on the server. +for name, wanted in base['expected'].items(): + assert sha((previous / name).read_bytes()) == wanted, 'Baseline differs from the deployed payload: ' + name + +# 2. Fresh payload from the working trees; record the backend diff. +payload = folder / 'payload' +if payload.exists(): + shutil.rmtree(payload) +diff_lines = [] +for name in plan['backendFiles'] + plan['newBackendFiles'] + plan['deployFiles']: + data = lf((kefu / name).read_bytes()) + assert b'\r' not in data, 'Stray carriage return: ' + name + if name.endswith('.py'): + compile(data, name, 'exec') + before = previous / name + if before.exists(): + diff_lines += difflib.unified_diff(before.read_text(encoding='utf-8').splitlines(), + data.decode('utf-8').splitlines(), + 'production/' + name, 'release/' + name, lineterm='') + target = payload / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) +(folder / 'payload-diff.txt').write_text('\n'.join(diff_lines) + '\n', encoding='utf-8') +dist = kefu / frontend +assert (dist / 'index.html').is_file(), 'Build the phone console first: cd admin-web/apps/mobile-h5 && pnpm build' +for text in dist.rglob('*'): + if text.is_file(): + data = text.read_bytes() + if text.suffix in {'.html', '.js', '.css', '.json', '.webmanifest'}: + assert b'\r\n' not in data, 'CRLF in build output: ' + str(text) + assert BANNER not in data, 'Old wording 黑名单 still in build: ' + str(text) +shutil.copytree(dist, payload / frontend) + +tests = folder / 'tests' +if tests.exists(): + shutil.rmtree(tests) +tests.mkdir() +for module in plan['serverTestModules']: + data = lf((kefu / 'wechat_rpa' / f'{module}.py').read_bytes()) + compile(data, module, 'exec') + (tests / f'{module}.py').write_bytes(data) + +scripts = ['release-plan.json', 'server-baseline.json', 'Dockerfile.overlay', 'remote_server_guard.py', + '01-preflight.sh', '02-build.sh', '03-deploy.sh', '04-verify.sh', 'rollback.sh'] + +# 3. Manifest over everything the server will verify. +manifest = [] +for path in sorted(p for p in list(payload.rglob('*')) + list(tests.rglob('*')) if p.is_file()): + data = path.read_bytes() + manifest.append({'path': path.relative_to(folder).as_posix(), 'sha256': sha(data), 'bytes': len(data)}) +(folder / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8') + +# 4. Server bundle (LF line endings for shell scripts) and archive. +bundle = folder / 'server-bundle' +if bundle.exists(): + shutil.rmtree(bundle) +bundle.mkdir() +shutil.copytree(payload, bundle / 'payload') +shutil.copytree(tests, bundle / 'tests') +for name in scripts + ['manifest.json']: + (bundle / name).write_bytes(lf((folder / name).read_bytes())) +archive = folder / 'mobile-ignore-server-bundle.tar.gz' +if archive.exists(): + archive.unlink() +with tarfile.open(archive, 'x:gz') as tf: + for p in sorted(bundle.rglob('*')): + if p.is_file(): + arc = p.relative_to(bundle).as_posix(); assert not arc.startswith(('/', '..')) + info = tf.gettarinfo(str(p), arcname=arc) + info.mode = 0o755 if arc.endswith('.sh') else 0o644 + info.uid = info.gid = 0 + info.uname = info.gname = '' + with p.open('rb') as handle: + tf.addfile(info, handle) +print(json.dumps({'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes()), + 'files': len(manifest), 'diff_lines': len(diff_lines), 'uploaded': False}, ensure_ascii=False, indent=1)) diff --git a/deploy/mobile-ignore-20260929/release-plan.json b/deploy/mobile-ignore-20260929/release-plan.json new file mode 100644 index 000000000..678223af1 --- /dev/null +++ b/deploy/mobile-ignore-20260929/release-plan.json @@ -0,0 +1,21 @@ +{ + "release": "mobile-ignore-20260929", + "candidateRoot": "C:/kefu", + "backendFiles": [], + "newBackendFiles": [], + "deployFiles": [], + "frontendDir": "admin-web/apps/mobile-h5/dist", + "serverTestModules": [ + "test_remote_api", + "test_zyt_auth", + "test_backend" + ], + "imageTestModules": [ + "test_archive_api" + ], + "schemaMigration": "none", + "restartServices": [ + "api" + ], + "notes": "User request 2026-09-29: rename the UI wording 黑名单 to 忽略 in the phone console (/m/). Text only; rules, APIs and permission codes unchanged. No backend code or database change." +} \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/remote_server_guard.py b/deploy/mobile-ignore-20260929/remote_server_guard.py new file mode 100644 index 000000000..5f02cc66d --- /dev/null +++ b/deploy/mobile-ignore-20260929/remote_server_guard.py @@ -0,0 +1,138 @@ +"""Read-only checks for mobile-ignore-20260929: baseline / candidate / deployed. + +Prints hashes and status only; never reads chat content, patients or credentials. +""" +from pathlib import Path +import argparse, hashlib, json, subprocess, urllib.error, urllib.request + +parser = argparse.ArgumentParser() +parser.add_argument('mode', choices=['baseline', 'candidate', 'deployed']) +args = parser.parse_args() +RELEASE = 'mobile-ignore-20260929' +BANNER = '黑名单' # 本次把界面文字“黑名单”改成“忽略”,新版 /m/ 里不应再出现 +folder = Path(__file__).resolve().parent +project = Path('/opt/im-admin') +base = json.loads((folder / 'server-baseline.json').read_text()) +plan = json.loads((folder / 'release-plan.json').read_text()) +manifest = json.loads((folder / 'manifest.json').read_text()) +payload_rows = [row for row in manifest if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +sha = lambda data: hashlib.sha256(data).hexdigest() +image = lambda ref: subprocess.check_output(['docker', 'image', 'inspect', ref, '--format', '{{.Id}}'], text=True).strip() + +# The unpacked bundle itself must be intact. +for row in manifest: + path = (folder / row['path']).resolve() + assert path.is_relative_to(folder), row['path'] + assert path.stat().st_size == row['bytes'] and sha(path.read_bytes()) == row['sha256'], row['path'] + +# project-relative path -> expected sha256 on the host +if args.mode == 'baseline': + expected_host = dict(base['expected']) +else: + expected_host = {row['path'][len('payload/'):]: row['sha256'] for row in payload_rows} +container_code = {k: v for k, v in expected_host.items() if k.startswith(('wechat_rpa/', 'admin-web/'))} +frontend_files = sorted(k[len(frontend) + 1:] for k in expected_host if k.startswith(frontend + '/')) + + +def in_container(code, target, ref=None): + command = (['docker', 'run', '--rm', '-i', '--network=none', '--entrypoint', 'python', ref, '-c', code] + if ref else ['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code]) + return json.loads(subprocess.check_output(command, input=json.dumps(target).encode())) + + +HASHES = ('from pathlib import Path;import hashlib,json,sys;names=json.load(sys.stdin);' + 'print(json.dumps({n:(hashlib.sha256((Path("/app")/n).read_bytes()).hexdigest() ' + 'if (Path("/app")/n).is_file() else None) for n in names}))') +# /m/ 目录里的完整文件清单(不能残留旧版带横幅的页面文件)和横幅文字检查。 +FRONTEND = ('from pathlib import Path;import json,sys;q=json.load(sys.stdin);root=Path("/app")/q["root"];' + 'files=sorted(p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file());' + 'print(json.dumps({"files":files,"banner":[f for f in files if q["banner"] in ' + '(root/f).read_bytes().decode("utf-8","ignore")]}))') +FRONTEND_QUERY = {'root': frontend, 'banner': BANNER} + +current = image('zyt/wecom-admin:current') +result = {'mode': args.mode, 'release': RELEASE, 'image': current} +# 只重建 api:网关和知识库 worker 继续跑它们自己的镜像,部署后核对它们没有被动过。 +OTHERS = ['im-admin-gateway-1', 'im-admin-knowledge-worker-1'] + + +def inspect(container): + raw = json.loads(subprocess.check_output(['docker', 'inspect', container]))[0] + return {'id': raw['Id'], 'image': raw['Image'], 'running': raw['State']['Running'], + 'health': raw['State'].get('Health', {}).get('Status', 'not_configured')} + + +if args.mode == 'baseline': + api = inspect('im-admin-api-1') + assert api['image'] == current, 'API container is not on zyt/wecom-admin:current' + assert api['running'] and api['health'] == 'healthy', 'API container is not healthy' + others = {name: inspect(name) for name in OTHERS} + for name, state in others.items(): + assert state['running'], 'Not running: ' + name + result['others'] = others + +if args.mode in ('baseline', 'deployed'): + for name, wanted in expected_host.items(): + assert sha((project / name).read_bytes()) == wanted, 'Host drift: ' + name + actual = in_container(HASHES, sorted(container_code)) + assert actual == container_code, 'Running api container differs from expected code' + result['files'] = len(expected_host) + +if args.mode == 'candidate': + candidate = 'zyt/wecom-admin:' + RELEASE + actual = in_container(HASHES, sorted(container_code), candidate) + assert actual == container_code, 'Candidate image differs from manifest' + listing = in_container(FRONTEND, FRONTEND_QUERY, candidate) + assert listing['files'] == frontend_files, 'Candidate /m/ has extra or missing files' + assert not listing['banner'], listing['banner'] + result['candidate'] = image(candidate) + result['files'] = len(container_code) + +if args.mode == 'deployed': + wanted = image('zyt/wecom-admin:' + RELEASE) + assert current == wanted, 'current tag does not point at the release image' + raw = json.loads(subprocess.check_output(['docker', 'inspect', 'im-admin-api-1']))[0] + assert raw['Image'] == wanted and raw['State']['Running'] and raw['RestartCount'] == 0 + assert raw['State'].get('Health', {}).get('Status') == 'healthy' + recorded = folder / 'baseline-check.json' + if recorded.exists(): + before = json.loads(recorded.read_text())['others'] + for name in OTHERS: + now = inspect(name) + assert now['id'] == before[name]['id'] and now['running'], 'Unrelated container changed: ' + name + result['others_unchanged'] = OTHERS + host_files = sorted(p.relative_to(project / frontend).as_posix() for p in (project / frontend).rglob('*') if p.is_file()) + assert host_files == frontend_files, 'Host /m/ has extra or missing files' + listing = in_container(FRONTEND, FRONTEND_QUERY) + assert listing['files'] == frontend_files and not listing['banner'], listing + for url in ['http://127.0.0.1:18766/api/v2/health', 'http://127.0.0.1:18770/health']: + with urllib.request.urlopen(url, timeout=15) as response: + assert response.status == 200, url + # New routes still refuse anonymous callers. No login is attempted. + for method, path in [('GET', '/api/v2/mobile/me'), ('GET', '/api/v2/mobile/desktops'), + ('POST', '/api/v2/desktop/commands/poll'), ('GET', '/api/v2/desktop/blacklist')]: + request = urllib.request.Request('http://127.0.0.1:18766' + path, method=method, + data=b'{}' if method == 'POST' else None, + headers={'Content-Type': 'application/json'}) + try: + urllib.request.urlopen(request, timeout=15) + except urllib.error.HTTPError as exc: + assert exc.code == 401, (path, exc.code) + else: + raise AssertionError('Accessible without authentication: ' + path) + with urllib.request.urlopen('http://127.0.0.1:18766/m/', timeout=15) as response: + body = response.read() + assert response.status == 200 and '企微客服助手'.encode() in body, 'mobile console entry missing' + # 本次目的:公网来源的明文 HTTP 登录不再被拒绝(在运行中的容器里、用它的真实环境变量判断)。 + code = ("import remote_api,json;from starlette.requests import Request;" + "r=Request({'type':'http','method':'POST','path':'/api/v2/mobile/auth/login','root_path':''," + "'headers':[(b'x-real-ip',b'114.114.114.114')],'scheme':'http','query_string':b''," + "'client':('172.18.0.1',40000),'server':('127.0.0.1',8766)});" + "print(json.dumps({'public_http_login_refused':remote_api.https_required(r)}))") + policy = json.loads(subprocess.check_output(['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code])) + assert policy == {'public_http_login_refused': False}, policy + result['policy'] = policy + +result['status'] = 'passed' +print(json.dumps(result, ensure_ascii=False)) diff --git a/deploy/mobile-ignore-20260929/rollback.sh b/deploy/mobile-ignore-20260929/rollback.sh new file mode 100644 index 000000000..e692a290b --- /dev/null +++ b/deploy/mobile-ignore-20260929/rollback.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# 恢复原 /m/ 目录,切回旧镜像(mobile-http-20260928),只重建 api。不涉及数据库。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd /opt/im-admin/deploy/im-admin +test -d "$release/source-before" +test "$(docker image inspect zyt/wecom-admin:before-mobile-ignore-20260929 --format '{{.Id}}')" = "$(cat "$release/image-before.txt")" +python3 - <<'PY' +from pathlib import Path +import hashlib, json, os, shutil +r = Path('/home/ps/mobile-ignore-20260929'); project = Path('/opt/im-admin') +base = json.loads((r / 'server-baseline.json').read_text()) +plan = json.loads((r / 'release-plan.json').read_text()) +sha = lambda data: hashlib.sha256(data).hexdigest() +for name in plan['backendFiles'] + plan['deployFiles']: + data = (r / 'source-before' / name).read_bytes(); assert sha(data) == base['expected'][name] + target = project / name; tmp = target.with_name(target.name + '.rollback-ignore.tmp') + if tmp.exists(): + tmp.unlink() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, target.stat().st_mode & 0o777); os.replace(tmp, target) +frontend = plan['frontendDir'] +saved = r / 'source-before' / frontend +for name, wanted in base['expected'].items(): + if name.startswith(frontend + '/'): + assert sha((saved / name[len(frontend) + 1:]).read_bytes()) == wanted, name +restore = project / (frontend + '.rollback-ignore.tmp') +if restore.exists(): + shutil.rmtree(restore) +shutil.copytree(saved, restore) +for leftover in [project / frontend, project / (frontend + '.mobile-ignore.tmp')]: + if leftover.exists(): + shutil.rmtree(leftover) +old = project / (frontend + '.mobile-ignore.old') +if old.exists(): + shutil.rmtree(old) +os.replace(restore, project / frontend) +PY +docker tag zyt/wecom-admin:before-mobile-ignore-20260929 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +curl -fsS -o /dev/null http://127.0.0.1:18766/api/v2/health +curl -fsS -o /dev/null http://127.0.0.1:18770/health +printf '%s\n' 'Previous image and /m/ restored.' diff --git a/deploy/mobile-ignore-20260929/server-baseline.json b/deploy/mobile-ignore-20260929/server-baseline.json new file mode 100644 index 000000000..46b6c7629 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-baseline.json @@ -0,0 +1,38 @@ +{ + "host": "192.168.110.111", + "user": "ps", + "project": "/opt/im-admin", + "compose": "/opt/im-admin/deploy/im-admin", + "checked_at": "2026-09-29", + "source": "Production /m/ as deployed by mobile-http-20260928 (28 files from that release payload).", + "expected": { + "admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js": "8d6186020c3d8562f581ea579742dd497649203a880b9c13cc904462b134bc60", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js": "226d4cde03c5434f20aa96c84cfb33538af084d79805348a089e343c5056281f", + "admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js": "b0d4b420bc5197a5df6d918e740e2b90b108c6e06ba930c3fd8e97fabe010556", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js": "75cd3809eb0d7db6c994302dd296674eadd366a53a4feb97a759173d70546e80", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js": "237de4f76004cbf16e5928060cf009fbbb3beec826006a876ddbede0ee9512f7", + "admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js": "da55fbacb606cdb51f6eb09fb8bf4963fe249e869d653f0fb71d3c0da9ed7b4c", + "admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js": "a98c816d31f9a937c5418dd48d25bc5b7b96fb3084d58571565d0810f420ced9", + "admin-web/apps/mobile-h5/dist/assets/index-C_mEe-tZ.js": "6c6620136850dbdfa6ea22a9fa6c320f3f5d33150b83f1d1b9597e0894f5d0de", + "admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js": "29bdcbe8030ef2e169b2ea453cef160f2b3c86b9a83718971023a9862e10bd45", + "admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css": "15ecdd5cb294bb51eef05e4843ac3a65f6e1718cbf16206547a876c52c5e8a2d", + "admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js": "9e5ff0c332dfae54d9f8e1c70b0a896e48aef80e8b45101092c08a59dfddf521", + "admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js": "13f70e99d3eccfa014f14dc313a195b4f24200929bf60142f6fd190ef33f2f3d", + "admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js": "530e97b69f045bf3c2798b79c1f49bd3ee69f53a657bad39825cb69d68552c9c", + "admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "admin-web/apps/mobile-h5/dist/icons/favicon-64.png": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "admin-web/apps/mobile-h5/dist/icons/icon-192.png": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "admin-web/apps/mobile-h5/dist/icons/icon-512.png": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "admin-web/apps/mobile-h5/dist/index.html": "6f335e7804fe9a1ce7bb5df388d2af81c7d919ddebb3beaee9533553a36adf04", + "admin-web/apps/mobile-h5/dist/manifest.webmanifest": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "admin-web/apps/mobile-h5/dist/sw.js": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69" + } +} \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/01-preflight.sh b/deploy/mobile-ignore-20260929/server-bundle/01-preflight.sh new file mode 100644 index 000000000..eeeabef60 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/01-preflight.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# 基线核对、备份将被替换的源文件和 /m/ 目录、给当前镜像打回滚标签。只读线上 + 写本目录。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +test ! -e baseline-check.json +python3 remote_server_guard.py baseline > baseline-check.json.tmp +mv baseline-check.json.tmp baseline-check.json +cat baseline-check.json +test ! -e source-before +test ! -e preflight-complete +if docker image inspect zyt/wecom-admin:before-mobile-ignore-20260929 >/dev/null 2>&1; then + echo 'Refuse to replace existing rollback tag' >&2; exit 1 +fi +mkdir -m 700 source-before +python3 - <<'PY' +from pathlib import Path +import json, shutil +r = Path('/home/ps/mobile-ignore-20260929'); project = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + target = r / 'source-before' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(project / name, target) +shutil.copytree(project / plan['frontendDir'], r / 'source-before' / plan['frontendDir']) +PY +docker tag zyt/wecom-admin:current zyt/wecom-admin:before-mobile-ignore-20260929 +docker image inspect zyt/wecom-admin:current --format '{{.Id}}' > image-before.txt +printf '%s\n' ready > preflight-complete +echo "preflight complete; rollback image tagged before-mobile-ignore-20260929 ($(cat image-before.txt))" diff --git a/deploy/mobile-ignore-20260929/server-bundle/02-build.sh b/deploy/mobile-ignore-20260929/server-bundle/02-build.sh new file mode 100644 index 000000000..55dbb84bb --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/02-build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# 构建候选镜像 + 离线跑测试 + 核对候选镜像文件。线上服务不受影响。 +# 本次不改数据库(只改 /m/ 静态文件里的文字),不做整库备份;回滚靠旧镜像和 source-before。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +test -f preflight-complete +test ! -e candidate-passed +python3 remote_server_guard.py baseline +test "$(docker image inspect zyt/wecom-admin:before-mobile-ignore-20260929 --format '{{.Id}}')" = "$(cat image-before.txt)" +docker build --network=none --pull=false -f Dockerfile.overlay -t zyt/wecom-admin:mobile-ignore-20260929 . > candidate-build.log 2>&1 +# serverTestModules 随包放在 tests/;imageTestModules 用镜像自带的测试文件(PYTHONPATH 后一项)。 +# -P:不把工作目录 /app/wechat_rpa 放到 sys.path 最前面,否则镜像里同名的旧测试会盖住随包的新测试。 +mapfile -t modules < <(python3 -c 'import json;p=json.load(open("release-plan.json"));print("\n".join(p["serverTestModules"]+p.get("imageTestModules",[])))') +test "${#modules[@]}" -gt 0 +docker run --rm --network=none --read-only --tmpfs /tmp:size=512m,mode=1777 --tmpfs /data:size=256m,mode=1777 \ + -v "$release/tests:/release-tests:ro" -e PYTHONPATH=/release-tests:/app/wechat_rpa \ + zyt/wecom-admin:mobile-ignore-20260929 python -X utf8 -B -P -m unittest "${modules[@]}" -q > candidate-tests.log 2>&1 +tail -n 3 candidate-tests.log +python3 remote_server_guard.py candidate +printf '%s\n' passed > candidate-passed diff --git a/deploy/mobile-ignore-20260929/server-bundle/03-deploy.sh b/deploy/mobile-ignore-20260929/server-bundle/03-deploy.sh new file mode 100644 index 000000000..c81d9f613 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/03-deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# 只替换整个 /m/ 目录(后端代码不变);切换镜像,只重建 api 容器。任何一步失败自动调用 rollback.sh。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +test -f candidate-passed +python3 remote_server_guard.py baseline +cd /opt/im-admin/deploy/im-admin +docker compose config --quiet +rollback_on_error() { rc=$?; trap - ERR; bash "$release/rollback.sh"; exit "$rc"; } +trap rollback_on_error ERR +python3 - <<'PY' +from pathlib import Path +import json, hashlib, os, shutil +r = Path('/home/ps/mobile-ignore-20260929'); target = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +rows = [row for row in json.loads((r / 'manifest.json').read_text()) if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +# 容器从镜像里读 /m/,宿主上的目录只给以后整包重建用;仍然整体换名,避免留下半套文件。 +staging = target / (frontend + '.mobile-ignore.tmp'); assert not staging.exists() +old = target / (frontend + '.mobile-ignore.old'); assert not old.exists() +for row in rows: + rel = row['path'][len('payload/'):] + data = (r / row['path']).read_bytes(); assert hashlib.sha256(data).hexdigest() == row['sha256'], rel + if rel.startswith(frontend + '/'): + dest = staging / rel[len(frontend) + 1:] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data); os.chmod(dest, 0o644) + continue + dest = (target / rel).resolve(); assert dest.is_relative_to(target) and dest.exists() + tmp = dest.with_name(dest.name + '.mobile-ignore.tmp'); assert not tmp.exists() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, dest.stat().st_mode & 0o777); os.replace(tmp, dest) +for directory in [staging] + [p for p in staging.rglob('*') if p.is_dir()]: + os.chmod(directory, 0o755) +final = target / frontend +os.replace(final, old) +os.replace(staging, final) +shutil.rmtree(old) # 原目录已在 source-before 里 +PY +docker tag zyt/wecom-admin:mobile-ignore-20260929 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +bash "$release/04-verify.sh" +trap - ERR +printf '%s\n' 'Phone console wording 黑名单 -> 忽略 deployed and verified.' diff --git a/deploy/mobile-ignore-20260929/server-bundle/04-verify.sh b/deploy/mobile-ignore-20260929/server-bundle/04-verify.sh new file mode 100644 index 000000000..fb71671b4 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/04-verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd "$release" +python3 remote_server_guard.py deployed | tee deployed-verification.json diff --git a/deploy/mobile-ignore-20260929/server-bundle/Dockerfile.overlay b/deploy/mobile-ignore-20260929/server-bundle/Dockerfile.overlay new file mode 100644 index 000000000..e31a68c10 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/Dockerfile.overlay @@ -0,0 +1,8 @@ +ARG BASE_IMAGE=zyt/wecom-admin:before-mobile-ignore-20260929 +FROM ${BASE_IMAGE} +# 先删掉旧版 /m/ 目录再放新版,镜像里不残留旧的(带“黑名单”文字的)页面文件。 +USER root +RUN rm -rf /app/admin-web/apps/mobile-h5/dist +USER app +COPY --chown=app:app payload/admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ +WORKDIR /app/wechat_rpa diff --git a/deploy/mobile-ignore-20260929/server-bundle/manifest.json b/deploy/mobile-ignore-20260929/server-bundle/manifest.json new file mode 100644 index 000000000..e646a4f90 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/manifest.json @@ -0,0 +1,157 @@ +[ + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js", + "sha256": "2903b693612f3c118ad2142aadbd72b7df39f7e9264c2699d3a184118edf79e9", + "bytes": 2308 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css", + "sha256": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "bytes": 359 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js", + "sha256": "ef16f6b813d27b9dd615960fdef1e6e9a371483c611957b727f3eef766729db4", + "bytes": 14528 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png", + "sha256": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "bytes": 16743 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js", + "sha256": "889a5877c28e5d0cd1d3abec0e86ad6ca6615140a2270e977f58d6398de13fec", + "bytes": 4490 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js", + "sha256": "ecb3f12c2b4c6a2e11cf0300824237c237e5e08e3479445c942382fd9c5f5d96", + "bytes": 28481 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css", + "sha256": "b30e00c1e3c5d454be03e7a36622130f5dd14fe81f196cd6e226dd083d040617", + "bytes": 4220 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css", + "sha256": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "bytes": 3983 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js", + "sha256": "46136ea68c275d388a61345624b3511a7d3857d3b6591247602e89d99f5cecc8", + "bytes": 10460 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js", + "sha256": "67e7a6e35dd7abcaa8f61a02b448e24abe94362151fcd042b8706e4aebf7248c", + "bytes": 8422 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js", + "sha256": "5718f9373cb4af20835d41ecf49df053e85f29e835ec7857f806d453f2be7227", + "bytes": 14363 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-C_M75pH8.js", + "sha256": "fbedf701fea8bb30607032576c6afaa4de6a02308be22346010e43c8fa7b6ccb", + "bytes": 117757 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css", + "sha256": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "bytes": 97051 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js", + "sha256": "07735a478c62e626d73cb49b675182ef12683917b8d9ab6c18ad604a5ed051f6", + "bytes": 6336 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css", + "sha256": "15ecdd5cb294bb51eef05e4843ac3a65f6e1718cbf16206547a876c52c5e8a2d", + "bytes": 1107 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css", + "sha256": "08a41cb29e760c2040c22007c7fc5dd5669d84e7515f3c671072bf2241dae2bb", + "bytes": 927 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js", + "sha256": "3e2a4ec3c8ac8fc804c1c08dd3a913ec2fd298006f03643488dec37f08f74d0f", + "bytes": 3459 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css", + "sha256": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "bytes": 130 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js", + "sha256": "256c84cc64e04be449d156d1704efb4575d4e5d137af99f9a83397b2e768317c", + "bytes": 2939 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js", + "sha256": "0ed96441a0fb80d8e585d63838b200ea6435f8deb2641f1effee136f96ef2e76", + "bytes": 607 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png", + "sha256": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "bytes": 24681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png", + "sha256": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "bytes": 5439 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png", + "sha256": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "bytes": 26433 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png", + "sha256": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "bytes": 144310 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png", + "sha256": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "bytes": 95961 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/index.html", + "sha256": "d69ec239352bbbfcd48f6243ab704424ed9708c2dd332581a974fe3b4a3855fe", + "bytes": 1357 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest", + "sha256": "36885be5f98b3bba7e79780a32ab0bbbbe06ed1fc26a14950abc1bb6ffe6a697", + "bytes": 681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/sw.js", + "sha256": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "bytes": 339 + }, + { + "path": "tests/test_backend.py", + "sha256": "7473ec5583c97941e132ade7a1f66abcf54fa6108ec6bb2a182f0152964fff61", + "bytes": 66279 + }, + { + "path": "tests/test_remote_api.py", + "sha256": "479cb9b042dc14707af718734c33688013203d4154f36199b7b0ced656653c39", + "bytes": 38523 + }, + { + "path": "tests/test_zyt_auth.py", + "sha256": "40ab2d37f51780af26497e22948ef665c92b7f3707f6ba30a6d711bf5b15dfa6", + "bytes": 4907 + } +] \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css new file mode 100644 index 000000000..03b9e2d0d --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css @@ -0,0 +1 @@ +.switcher[data-v-91cf283c]{max-height:80vh}.switcher-body[data-v-91cf283c]{padding:16px 0 12px}.switcher-body h3[data-v-91cf283c]{margin:0 16px 8px;font-size:17px}.switcher-state[data-v-91cf283c]{place-items:center;padding:32px 0;display:grid}.switcher-list[data-v-91cf283c]{margin-top:12px}.current[data-v-91cf283c]{color:var(--app-primary);margin-left:6px} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js new file mode 100644 index 000000000..8b8a72d4a --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DmKdvr8J.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{c as n,d as r,l as i,u as a}from"./format-Dt9-L1ne.js";import{n as o,o as s,s as c,t as l}from"./cell-group-B1mihubH.js";import{n as u,r as d,t as f}from"./field-BLYigdmn.js";import{$ as p,Bt as m,Ct as h,Dt as g,E as _,Et as v,Ft as y,G as b,Gt as x,Jt as S,K as C,Kt as w,Lt as T,Mt as E,Nt as ee,O as D,Q as O,St as k,Ut as A,V as te,Vt as j,X as M,Y as N,Yt as ne,Z as re,_ as P,_t as F,a as ie,c as ae,et as oe,ft as se,g as I,gt as L,ht as ce,l as le,n as ue,o as R,ot as z,q as B,qt as V,rt as de,vt as fe,wt as pe,x as me,yt as H}from"./index-C_M75pH8.js";var U=0;function he(e){e?(U||document.body.classList.add(`van-toast--unclickable`),U++):U&&(U--,U||document.body.classList.remove(`van-toast--unclickable`))}var[ge,W]=P(`toast`),_e=[`show`,`overlay`,`teleport`,`transition`,`overlayClass`,`overlayStyle`,`closeOnClickOverlay`,`zIndex`],ve={icon:String,show:Boolean,type:N(`text`),overlay:Boolean,message:M,iconSize:M,duration:C(2e3),position:N(`middle`),teleport:[String,Object],wordBreak:String,className:null,iconPrefix:String,transition:N(`van-fade`),loadingType:String,forbidClick:Boolean,overlayClass:null,overlayStyle:Object,closeOnClick:Boolean,closeOnClickOverlay:Boolean,zIndex:M},ye=h({name:ge,props:ve,emits:[`update:show`],setup(t,{emit:n,slots:r}){let i,o=!1,c=()=>{let e=t.show&&t.forbidClick;o!==e&&(o=e,he(o))},l=e=>n(`update:show`,e),u=()=>{t.closeOnClick&&l(!1)},d=()=>clearTimeout(i),f=()=>{let{icon:n,type:r,iconSize:i,iconPrefix:a,loadingType:o}=t;if(n||r===`success`||r===`fail`)return k(e,{name:n||r,size:i,class:W(`icon`),classPrefix:a},null);if(r===`loading`)return k(s,{class:W(`loading`),size:i,type:o},null)},p=()=>{let{type:e,message:n}=t;if(r.message)return k(`div`,{class:W(`text`)},[r.message()]);if(oe(n)&&n!==``)return e===`html`?k(`div`,{key:0,class:W(`text`),innerHTML:String(n)},null):k(`div`,{class:W(`text`)},[n])};return j(()=>[t.show,t.forbidClick],c),j(()=>[t.show,t.type,t.message,t.duration],()=>{d(),t.show&&t.duration>0&&(i=setTimeout(()=>{l(!1)},t.duration))}),E(c),ee(c),()=>k(a,v({class:[W([t.position,t.wordBreak===`normal`?`break-normal`:t.wordBreak,{[t.type]:!t.icon}]),t.className],lockScroll:!1,onClick:u,onClosed:d,"onUpdate:show":l},z(t,_e)),{default:()=>[f(),p()]})}}),be={icon:``,type:`text`,message:``,className:``,overlay:!1,onClose:void 0,onOpened:void 0,duration:2e3,teleport:`body`,iconSize:void 0,iconPrefix:void 0,position:`middle`,transition:`van-fade`,forbidClick:!1,loadingType:void 0,overlayClass:``,overlayStyle:void 0,closeOnClick:!1,closeOnClickOverlay:!1},G=[],K=O({},be),xe=new Map;function Se(e){return de(e)?e:{message:e}}function Ce(){let{instance:e,unmount:t}=n({setup(){let e=w(``),{open:t,state:n,close:r,toggle:a}=i(),o=()=>{},s=()=>k(ye,v(n,{onClosed:o,"onUpdate:show":a}),null);return j(e,e=>{n.message=e}),pe().render=s,{open:t,close:r,message:e}}});return e}function we(){if(!G.length){let e=Ce();G.push(e)}return G[G.length-1]}function Te(e={}){if(!p)return{};let t=we(),n=Se(e);return t.open(O({},K,xe.get(n.type||K.type),n)),t}var[Ee,q]=P(`empty`),De={image:N(`default`),imageSize:[Number,String,Array],description:String},Oe=h({name:Ee,props:De,setup(e,{slots:t}){let n=()=>{let n=t.description?t.description():e.description;if(n)return k(`p`,{class:q(`description`)},[n])},r=()=>{if(t.default)return k(`div`,{class:q(`bottom`)},[t.default()])},i=d(),a=e=>`${i}-${e}`,o=e=>`url(#${a(e)})`,s=(e,t,n)=>k(`stop`,{"stop-color":e,offset:`${t}%`,"stop-opacity":n},null),c=(e,t)=>[s(e,0),s(t,100)],l=e=>[k(`defs`,null,[k(`radialGradient`,{id:a(e),cx:`50%`,cy:`54%`,fx:`50%`,fy:`54%`,r:`297%`,gradientTransform:`matrix(-.16 0 0 -.33 .58 .72)`,"data-allow-mismatch":`attribute`},[s(`#EBEDF0`,0),s(`#F2F3F5`,100,.3)])]),k(`ellipse`,{fill:o(e),opacity:`.8`,cx:`80`,cy:`140`,rx:`46`,ry:`8`,"data-allow-mismatch":`attribute`},null)],u=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`a`),x1:`64%`,y1:`100%`,x2:`64%`,"data-allow-mismatch":`attribute`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M36 131V53H16v20H2v58h34z`,fill:o(`a`)},null),k(`path`,{d:`M123 15h22v14h9v77h-31V15z`,fill:o(`a`)},null)])],f=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`b`),x1:`64%`,y1:`97%`,x2:`64%`,y2:`0%`,"data-allow-mismatch":`attribute`},[s(`#F2F3F5`,0,.3),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M87 6c3 0 7 3 8 6a8 8 0 1 1-1 16H80a7 7 0 0 1-8-6c0-4 3-7 6-7 0-5 4-9 9-9Z`,fill:o(`b`)},null),k(`path`,{d:`M19 23c2 0 3 1 4 3 2 0 4 2 4 4a4 4 0 0 1-4 3v1h-7v-1l-1 1c-2 0-3-2-3-4 0-1 1-3 3-3 0-2 2-4 4-4Z`,fill:o(`b`)},null)])],p=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{id:a(1),x1:`64%`,y1:`100%`,x2:`64%`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)]),k(`linearGradient`,{id:a(2),x1:`50%`,x2:`50%`,y2:`84%`},[s(`#EBEDF0`,0),s(`#DCDEE0`,100,0)]),k(`linearGradient`,{id:a(3),x1:`100%`,x2:`100%`,y2:`100%`},[c(`#EAEDF0`,`#DCDEE0`)]),k(`radialGradient`,{id:a(4),cx:`50%`,cy:`0%`,fx:`50%`,fy:`0%`,r:`100%`,gradientTransform:`matrix(0 1 -.54 0 .5 -.5)`},[s(`#EBEDF0`,0),s(`#FFF`,100,0)])]),k(`g`,{fill:`none`},[u(),k(`path`,{fill:o(4),d:`M0 139h160v21H0z`,"data-allow-mismatch":`attribute`},null),k(`path`,{d:`M80 54a7 7 0 0 1 3 13v27l-2 2h-2a2 2 0 0 1-2-2V67a7 7 0 0 1 3-13z`,fill:o(2),"data-allow-mismatch":`attribute`},null),k(`g`,{opacity:`.6`,"stroke-linecap":`round`,"stroke-width":`7`,"data-allow-mismatch":`children`},[k(`path`,{d:`M64 47a19 19 0 0 0-5 13c0 5 2 10 5 13`,stroke:o(3)},null),k(`path`,{d:`M53 36a34 34 0 0 0 0 48`,stroke:o(3)},null),k(`path`,{d:`M95 73a19 19 0 0 0 6-13c0-5-2-9-6-13`,stroke:o(3)},null),k(`path`,{d:`M106 84a34 34 0 0 0 0-48`,stroke:o(3)},null)]),k(`g`,{transform:`translate(31 105)`},[k(`rect`,{fill:`#EBEDF0`,width:`98`,height:`34`,rx:`2`},null),k(`rect`,{fill:`#FFF`,x:`9`,y:`8`,width:`80`,height:`18`,rx:`1.1`},null),k(`rect`,{fill:`#EBEDF0`,x:`15`,y:`12`,width:`18`,height:`6`,rx:`1.1`},null)])])]),m=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(5)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`95%`,y1:`48%`,x2:`5.5%`,y2:`51%`,id:a(6)},[c(`#EAEDF1`,`#DCDEE0`)]),k(`linearGradient`,{y1:`45%`,x2:`100%`,y2:`54%`,id:a(7)},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),k(`g`,{transform:`translate(36 50)`,fill:`none`},[k(`g`,{transform:`translate(8)`},[k(`rect`,{fill:`#EBEDF0`,opacity:`.6`,x:`38`,y:`13`,width:`36`,height:`53`,rx:`2`},null),k(`rect`,{fill:o(5),width:`64`,height:`66`,rx:`2`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#FFF`,x:`6`,y:`6`,width:`52`,height:`55`,rx:`1`},null),k(`g`,{transform:`translate(15 17)`,fill:o(6),"data-allow-mismatch":`attribute`},[k(`rect`,{width:`34`,height:`6`,rx:`1`},null),k(`path`,{d:`M0 14h34v6H0z`},null),k(`rect`,{y:`28`,width:`34`,height:`6`,rx:`1`},null)])]),k(`rect`,{fill:o(7),y:`61`,width:`88`,height:`28`,rx:`1`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#F7F8FA`,x:`29`,y:`72`,width:`30`,height:`6`,rx:`1`},null)])]),h=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,null,[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(8),"data-allow-mismatch":`attribute`},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),l(`c`),k(`path`,{d:`m59 60 21 21 21-21h3l9 9v3L92 93l21 21v3l-9 9h-3l-21-21-21 21h-3l-9-9v-3l21-21-21-21v-3l9-9h3Z`,fill:o(8),"data-allow-mismatch":`attribute`},null)]),g=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,y1:`100%`,x2:`50%`,id:a(9)},[c(`#EEE`,`#D8D8D8`)]),k(`linearGradient`,{x1:`100%`,y1:`50%`,y2:`50%`,id:a(10)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(11)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(12)},[c(`#FFF`,`#F7F8FA`)])]),u(),f(),l(`d`),k(`g`,{transform:`rotate(-45 113 -4)`,fill:`none`,"data-allow-mismatch":`children`},[k(`rect`,{fill:o(9),x:`24`,y:`52.8`,width:`5.8`,height:`19`,rx:`1`},null),k(`rect`,{fill:o(10),x:`22.1`,y:`67.3`,width:`9.9`,height:`28`,rx:`1`},null),k(`circle`,{stroke:o(11),"stroke-width":`8`,cx:`27`,cy:`27`,r:`27`},null),k(`circle`,{fill:o(12),cx:`27`,cy:`27`,r:`16`},null),k(`path`,{d:`M37 7c-8 0-15 5-16 12`,stroke:o(11),"stroke-width":`3`,opacity:`.5`,"stroke-linecap":`round`,transform:`rotate(45 29 13)`},null)])]),_=()=>{var n;if(t.image)return t.image();let r={error:h,search:g,network:p,default:m};return((n=r[e.image])==null?void 0:n.call(r))||k(`img`,{src:e.image},null)};return()=>k(`div`,{class:q()},[k(`div`,{class:q(`image`),style:me(e.imageSize)},[_()]),n(),r()])}}),J=I(Oe),[ke,Y,Ae]=P(`pull-refresh`),X=50,je=[`pulling`,`loosing`,`success`],Me={disabled:Boolean,modelValue:Boolean,headHeight:B(X),successText:String,pullingText:String,loosingText:String,loadingText:String,pullDistance:M,successDuration:B(500),animationDuration:B(300)},Ne=h({name:ke,props:Me,emits:[`change`,`refresh`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i,a=w(),o=w(),c=b(a),l=x({status:`normal`,distance:0,duration:0}),u=r(),d=()=>{if(e.headHeight!==X)return{height:`${e.headHeight}px`}},f=()=>l.status!==`loading`&&l.status!==`success`&&!e.disabled,p=t=>{let n=+(e.pullDistance||e.headHeight);return t>n&&(t=t{let i=+(e.pullDistance||e.headHeight);l.distance=n,r?l.status=`loading`:n===0?l.status=`normal`:n{let{status:t}=l;return t===`normal`?``:e[`${t}Text`]||Ae(t)},v=()=>{let{status:e,distance:t}=l;if(n[e])return n[e]({distance:t});let r=[];return je.includes(e)&&r.push(k(`div`,{class:Y(`text`)},[h()])),e===`loading`&&r.push(k(s,{class:Y(`loading`)},{default:h})),r},y=()=>{l.status=`success`,setTimeout(()=>{m(0)},+e.successDuration)},S=e=>{i=_(c.value)===0,i&&(l.duration=0,u.start(e))},C=e=>{f()&&S(e)},T=e=>{if(f()){i||S(e);let{deltaY:t}=u;u.move(e),i&&t.value>=0&&u.isVertical()&&(D(e),m(p(t.value)))}},E=()=>{i&&u.deltaY.value&&f()&&(l.duration=+e.animationDuration,l.status===`loosing`?(m(+e.headHeight,!0),t(`update:modelValue`,!0),g(()=>t(`refresh`))):m(0))};return j(()=>e.modelValue,t=>{l.duration=+e.animationDuration,t?m(+e.headHeight,!0):n.success||e.successText?y():m(0,!1)}),te(`touchmove`,T,{target:o}),()=>{var e;let t={transitionDuration:`${l.duration}ms`,transform:l.distance?`translate3d(0,${l.distance}px, 0)`:``};return k(`div`,{ref:a,class:Y()},[k(`div`,{ref:o,class:Y(`track`),style:t,onTouchstartPassive:C,onTouchend:E,onTouchcancel:E},[k(`div`,{class:Y(`head`),style:d()},[v()]),(e=n.default)==null?void 0:e.call(n)])])}}}),Pe=I(Ne),[Fe,Z,Q]=P(`search`),Ie=O({},u,{label:String,shape:N(`square`),leftIcon:N(`search`),clearable:re,actionText:String,background:String,showAction:Boolean}),Le=h({name:Fe,props:Ie,emits:[`blur`,`focus`,`clear`,`search`,`cancel`,`clickInput`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(e,{emit:t,slots:n,attrs:r}){let i=d(),a=w(),o=()=>{n.action||(t(`update:modelValue`,``),t(`cancel`))},s=n=>{n.keyCode===13&&(D(n),t(`search`,e.modelValue))},l=()=>e.id||`${i}-input`,p=()=>{if(n.label||e.label)return k(`label`,{class:Z(`label`),for:l(),"data-allow-mismatch":`attribute`},[n.label?n.label():e.label])},m=()=>{if(e.showAction){let t=e.actionText||Q(`cancel`);return k(`div`,{class:Z(`action`),role:`button`,tabindex:0,onClick:o},[n.action?n.action():t])}},h=()=>{var e;return(e=a.value)==null?void 0:e.blur()},g=()=>{var e;return(e=a.value)==null?void 0:e.focus()},_=e=>t(`blur`,e),y=e=>t(`focus`,e),b=e=>t(`clear`,e),x=e=>t(`clickInput`,e),S=e=>t(`clickLeftIcon`,e),C=e=>t(`clickRightIcon`,e),T=Object.keys(u),E=()=>{let i=O({},r,z(e,T),{id:l()});return k(f,v({ref:a,type:`search`,class:Z(`field`,{"with-message":i.errorMessage}),border:!1,labelAlign:`left`,onBlur:_,onFocus:y,onClear:b,onKeypress:s,onClickInput:x,onClickLeftIcon:S,onClickRightIcon:C,"onUpdate:modelValue":e=>t(`update:modelValue`,e)},i),z(n,[`left-icon`,`right-icon`]))};return c({focus:g,blur:h}),()=>{var t;return k(`div`,{class:Z({"show-action":e.showAction}),style:{background:e.background}},[(t=n.left)==null?void 0:t.call(n),k(`div`,{class:Z(`content`,e.shape)},[p(),E()]),m()])}}}),$=I(Le),Re={class:`switcher-body`},ze={key:0,class:`switcher-state`},Be={key:0,class:`current`},Ve=t(h({__name:`AccountSwitcher`,props:{show:{type:Boolean,default:!1},showModifiers:{}},emits:[`update:show`],setup(e){let t=m(e,`show`),n=w(``),r=w([]),i=w(!1),c=w(``),u=0,d,f=ce(()=>{var e;return(e=ue.me)==null?void 0:e.account});async function p(){let e=++u;i.value=!0,c.value=``;try{let{items:t}=await le.accounts(n.value.trim());e===u&&(r.value=t.filter(e=>{var t;return e.id!==((t=f.value)==null?void 0:t.id)}))}catch(t){e===u&&(c.value=t instanceof ae?t.message:`加载失败`)}finally{e===u&&(i.value=!1)}}j(t,e=>{e&&p()}),j(n,()=>{window.clearTimeout(d),d=window.setTimeout(p,300)});function h(e){ie(e),t.value=!1}function g(e){return e.device_count?e.online?e.listening?`监听中`:`已停止`:`离线`:`没有电脑`}function _(e){return e.online?e.listening?`tone-success`:`tone-warning`:`tone-muted`}return(e,u)=>(y(),F(V(a),{show:t.value,"onUpdate:show":u[2]||(u[2]=e=>t.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`switcher`},{default:A(()=>[L(`div`,Re,[u[3]||(u[3]=L(`h3`,null,`切换客服`,-1)),k(V($),{modelValue:n.value,"onUpdate:modelValue":u[0]||(u[0]=e=>n.value=e),shape:`round`,placeholder:`搜索客服姓名或账号`},null,8,[`modelValue`]),k(V(l),{inset:``},{default:A(()=>[k(V(o),{title:`我自己`,label:f.value?`${f.value.nickname||f.value.sn}`:``,icon:`user-o`,clickable:``,value:V(R)===void 0?`当前`:``,onClick:u[1]||(u[1]=e=>h(null))},null,8,[`label`,`value`])]),_:1}),i.value&&!r.value.length?(y(),H(`div`,ze,[k(V(s),{size:`20`})])):c.value?(y(),F(V(J),{key:1,image:`network`,description:c.value},null,8,[`description`])):r.value.length?(y(),F(V(l),{key:3,inset:``,class:`switcher-list`},{default:A(()=>[(y(!0),H(se,null,T(r.value,e=>(y(),F(V(o),{key:e.id,title:e.nickname||e.sn||`客服 #${e.id}`,label:e.sn,clickable:``,onClick:t=>h(e)},{value:A(()=>[L(`span`,{class:S(_(e))},ne(g(e)),3),V(R)===e.id?(y(),H(`span`,Be,`当前`)):fe(``,!0)]),_:2},1032,[`title`,`label`,`onClick`]))),128))]),_:1})):(y(),F(V(J),{key:2,image:`search`,description:`没有找到其他客服`}))])]),_:1},8,[`show`]))}}),[[`__scopeId`,`data-v-91cf283c`]]);export{Te as a,J as i,$ as n,Pe as r,Ve as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css new file mode 100644 index 000000000..323e8d6a5 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BJrOxtrJ.css @@ -0,0 +1 @@ +.detail[data-v-bdbcc19d]{padding-bottom:16px}.detail-head[data-v-bdbcc19d]{align-items:center;gap:12px;padding:16px;display:flex}.avatar[data-v-bdbcc19d]{width:52px;height:52px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:14px;flex:none;place-items:center;font-size:22px;font-weight:600;display:grid}.detail-title[data-v-bdbcc19d]{flex:1;min-width:0}.detail-title h2[data-v-bdbcc19d]{text-overflow:ellipsis;white-space:nowrap;margin:0 0 4px;font-size:18px;overflow:hidden}.detail-title p[data-v-bdbcc19d]{margin:0;font-size:13px}.badge[data-v-bdbcc19d]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.group[data-v-bdbcc19d]{margin-bottom:12px}.state[data-v-bdbcc19d]{background:var(--app-surface);border:1px solid var(--app-border);border-radius:14px;margin:0 16px;padding:16px}.state--blocked[data-v-bdbcc19d]{border-color:var(--app-danger-soft);background:linear-gradient(0deg, var(--app-danger-soft), var(--app-danger-soft)), var(--app-surface)}.state h3[data-v-bdbcc19d]{align-items:center;gap:6px;margin:0 0 6px;font-size:15px;display:flex}.state--blocked h3[data-v-bdbcc19d]{color:var(--app-danger)}.state>p[data-v-bdbcc19d]{color:var(--app-text-2);margin:0 0 12px;font-size:13px;line-height:1.6}.state dl[data-v-bdbcc19d]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin:0 0 14px;display:grid}.state dt[data-v-bdbcc19d]{color:var(--app-muted);font-size:12px}.state dd[data-v-bdbcc19d]{word-break:break-all;margin:2px 0 0;font-size:14px}.reason[data-v-bdbcc19d]{margin:0 0 12px}.reason[data-v-bdbcc19d] .van-cell-group--inset{margin:0}.state[data-v-bdbcc19d] .van-cell-group--inset{margin:0 0 12px}.note[data-v-bdbcc19d]{color:var(--app-muted);margin:0;font-size:13px;line-height:1.6}.row[data-v-645c253b]{border:0;border-bottom:1px solid var(--app-border);background:var(--app-surface);width:100%;min-height:64px;color:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;padding:10px 16px;display:flex}.row[data-v-645c253b]:active,.row.active[data-v-645c253b]{background:var(--app-primary-soft)}.avatar[data-v-645c253b]{border-radius:12px;flex:none;place-items:center;width:42px;height:42px;font-size:17px;font-weight:600;display:grid}.avatar--wecom[data-v-645c253b]{color:#2f6fff;background:#2f6fff1f}.avatar--wechat[data-v-645c253b]{color:#0f9e69;background:#13b77a24}.avatar--group[data-v-645c253b]{color:#7a5cff;background:#8a6bff24}.body[data-v-645c253b]{flex:1;gap:4px;min-width:0;display:grid}.line[data-v-645c253b]{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.name[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.time[data-v-645c253b]{color:var(--app-muted);flex:none;font-size:12px}.sub[data-v-645c253b]{color:var(--app-muted);font-size:12px}.meta[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.blocked[data-v-645c253b]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;padding:1px 8px;font-weight:600}.page[data-v-4433756d]{min-height:100%}.nav-action[data-v-4433756d]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.toolbar[data-v-4433756d]{top:calc(46px + env(safe-area-inset-top));z-index:2;background:var(--app-bg);position:-webkit-sticky;position:sticky}.toolbar[data-v-4433756d] .van-search{background:0 0;padding-bottom:4px}.tabs[data-v-4433756d]{--van-tabs-nav-background:transparent;padding:0 4px}.list-body[data-v-4433756d]{min-height:60vh}.single .list-pane[data-v-4433756d]{max-width:720px;margin:0 auto}.split[data-v-4433756d]{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;max-width:1180px;margin:0 auto;padding:0 24px 24px;display:grid}.split .list-body[data-v-4433756d]{border-radius:16px;overflow:clip}.detail-pane[data-v-4433756d]{top:calc(46px + env(safe-area-inset-top) + 12px);min-height:320px;margin-top:12px;position:-webkit-sticky;position:sticky}.sheet[data-v-4433756d]{max-height:88vh;padding-top:8px} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js new file mode 100644 index 000000000..dc33e629c --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-BeEp2NbK.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,n as r,p as i,r as a,t as o}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n as s}from"./use-placeholder-BHVOzwXa.js";import{a as c,d as l,i as u,o as d,s as f,u as p}from"./format-Dt9-L1ne.js";import{a as m,n as h,o as g,s as _,t as v}from"./cell-group-B1mihubH.js";import{r as y,t as b}from"./field-BLYigdmn.js";import{a as x,i as S,n as ee,r as C,t as w}from"./AccountSwitcher-DmKdvr8J.js";import{$ as te,A as ne,At as T,B as E,C as D,Ct as O,D as k,Dt as A,E as j,Et as M,F as N,Ft as P,G as re,Gt as ie,H as ae,Ht as F,I,It as oe,J as se,Jt as ce,Kt as L,Lt as le,M as ue,Mt as de,N as fe,O as pe,Ot as me,Pt as he,Q as ge,R as _e,S as ve,St as R,Tt as ye,U as be,Ut as z,V as xe,Vt as B,W as V,Wt as Se,X as H,Y as Ce,Yt as U,Z as W,_ as G,_t as K,bt as we,c as Te,et as Ee,ft as De,g as Oe,gt as q,ht as J,j as ke,jt as Ae,kt as je,l as Me,n as Ne,o as Pe,ot as Fe,p as Ie,q as Y,qt as X,s as Le,t as Re,ut as ze,v as Be,vt as Z,w as Ve,wt as He,xt as Q,y as Ue,yt as $,z as We}from"./index-C_M75pH8.js";var Ge=Array.isArray,Ke=e=>typeof e==`string`,qe=e=>typeof e==`object`&&!!e,Je=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ye=/\B([A-Z])/g,Xe=Je(e=>e.replace(Ye,`-$1`).toLowerCase());function Ze(e){if(Ge(e)){let t={};for(let n=0;ne.startsWith(`/*`)?``:e).split(Qe).forEach(e=>{if(e){let n=e.split($e);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function nt(e){if(!e)return``;if(Ke(e))return e;let t=``;for(let n in e){let r=e[n];if(Ke(r)||typeof r==`number`){let e=n.startsWith(`--`)?n:Xe(n);t+=`${e}:${r};`}}return t}function rt(e){let t=``;if(Ke(e))t=e;else if(Ge(e))for(let n=0;nt||!o&&at?i=We(u):r&&(i=We(r))}return u(),l}function ot(){let e=L([]),t=[];return T(()=>{e.value=[]}),[e,n=>(t[n]||(t[n]=t=>{e.value[n]=t}),t[n])]}function st(e,t){if(!te||!window.IntersectionObserver)return;let n=new IntersectionObserver(e=>{t(e[0].intersectionRatio>0)},{root:document.body}),r=()=>{e.value&&n.observe(e.value)},i=()=>{e.value&&n.unobserve(e.value)};Ae(i),je(i),_e(r)}var[ct,lt]=G(`sticky`),ut={zIndex:H,position:Ce(`top`),container:Object,offsetTop:Y(0),offsetBottom:Y(0)},dt=O({name:ct,props:ut,emits:[`scroll`,`change`],setup(e,{emit:t,slots:n}){let r=L(),i=re(r),a=ie({fixed:!1,width:0,height:0,transform:0}),o=L(!1),s=J(()=>D(e.position===`top`?e.offsetTop:e.offsetBottom)),c=J(()=>{if(o.value)return;let{fixed:e,height:t,width:n}=a;if(e)return{width:`${n}px`,height:`${t}px`}}),l=J(()=>{if(!a.fixed||o.value)return;let t=ge(ve(e.zIndex),{width:`${a.width}px`,height:`${a.height}px`,[e.position]:`${s.value}px`});return a.transform&&(t.transform=`translate3d(0, ${a.transform}px, 0)`),t}),u=e=>t(`scroll`,{scrollTop:e,isFixed:a.fixed}),d=()=>{if(!r.value||k(r))return;let{container:t,position:n}=e,i=V(r),o=j(window);if(a.width=i.width,a.height=i.height,n===`top`){if(t){let e=V(t),n=e.bottom-s.value-a.height;a.fixed=s.value>i.top&&e.bottom>0,a.transform=n<0?n:0}else a.fixed=s.value>i.top}else{let{clientHeight:e}=document.documentElement;if(t){let n=V(t),r=e-n.top-s.value-a.height;a.fixed=e-s.valuen.top,a.transform=r<0?-r:0}else a.fixed=e-s.valuea.fixed,e=>t(`change`,e)),xe(`scroll`,d,{target:i,passive:!0}),st(r,d),B([fe,ue],()=>{r.value&&!k(r)&&a.fixed&&(o.value=!0,A(()=>{let e=V(r);a.width=e.width,a.height=e.height,o.value=!1}))}),()=>{var e;return R(`div`,{ref:r,style:c.value},[R(`div`,{class:lt({fixed:a.fixed&&!o.value}),style:l.value},[(e=n.default)==null?void 0:e.call(n)])])}}}),ft=Oe(dt),[pt,mt]=G(`swipe`),ht={loop:W,width:H,height:H,vertical:Boolean,autoplay:Y(0),duration:Y(500),touchable:W,lazyRender:Boolean,initialSwipe:Y(0),indicatorColor:String,showIndicators:W,stopPropagation:W},gt=Symbol(pt),_t=O({name:pt,props:ht,emits:[`change`,`dragStart`,`dragEnd`],setup(e,{emit:n,slots:r}){let i=L(),a=L(),o=ie({rect:null,width:0,height:0,offset:0,active:0,swiping:!1}),s=!1,c=l(),{children:u,linkChildren:d}=E(gt),f=J(()=>u.length),p=J(()=>o[e.vertical?`height`:`width`]),m=J(()=>e.vertical?c.deltaY.value:c.deltaX.value),h=J(()=>o.rect?(e.vertical?o.rect.height:o.rect.width)-p.value*f.value:0),g=J(()=>p.value?Math.ceil(Math.abs(h.value)/p.value):f.value),v=J(()=>f.value*p.value),y=J(()=>(o.active+f.value)%f.value),b=J(()=>{let t=e.vertical?`vertical`:`horizontal`;return c.direction.value===t}),x=J(()=>{let t={transitionDuration:`${o.swiping?0:e.duration}ms`,transform:`translate${e.vertical?`Y`:`X`}(${+o.offset.toFixed(2)}px)`};if(p.value){let n=e.vertical?`height`:`width`,r=e.vertical?`width`:`height`;t[n]=`${v.value}px`,t[r]=e[r]?`${e[r]}px`:``}return t}),S=t=>{let{active:n}=o;return t?e.loop?Ue(n+t,-1,f.value):Ue(n+t,0,g.value):n},ee=(t,n=0)=>{let r=t*p.value;e.loop||(r=Math.min(r,-h.value));let i=n-r;return e.loop||(i=Ue(i,h.value,0)),i},C=({pace:t=0,offset:r=0,emitChange:i})=>{if(f.value<=1)return;let{active:a}=o,s=S(t),c=ee(s,r);if(e.loop){if(u[0]&&c!==h.value){let e=c0;u[f.value-1].setOffset(e?-v.value:0)}}o.active=s,o.offset=c,i&&s!==a&&n(`change`,y.value)},w=()=>{o.swiping=!0,o.active<=-1?C({pace:f.value}):o.active>=f.value&&C({pace:-f.value})},te=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:-1,emitChange:!0})})},ne=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:1,emitChange:!0})})},T,D=()=>clearTimeout(T),O=()=>{D(),+e.autoplay>0&&f.value>1&&(T=setTimeout(()=>{ne(),O()},+e.autoplay))},j=(t=+e.initialSwipe)=>{if(!i.value)return;let n=()=>{var n,r;if(!k(i)){let t={width:i.value.offsetWidth,height:i.value.offsetHeight};o.rect=t,o.width=+((n=e.width)==null?t.width:n),o.height=+((r=e.height)==null?t.height:r)}f.value&&(t=Math.min(f.value-1,t),t===-1&&(t=f.value-1)),o.active=t,o.swiping=!0,o.offset=ee(t),u.forEach(e=>{e.setOffset(0)}),O()};k(i)?A().then(n):n()},M=()=>j(o.active),N,P=t=>{!e.touchable||t.touches.length>1||(c.start(t),s=!1,N=Date.now(),D(),w())},re=t=>{e.touchable&&o.swiping&&(c.move(t),b.value&&(!e.loop&&(o.active===0&&m.value>0||o.active===f.value-1&&m.value<0)||(pe(t,e.stopPropagation),C({offset:m.value}),s||(n(`dragStart`,{index:y.value}),s=!0))))},F=()=>{if(!e.touchable||!o.swiping)return;let t=Date.now()-N,r=m.value/t;if((Math.abs(r)>.25||Math.abs(m.value)>p.value/2)&&b.value){let t=e.vertical?c.offsetY.value:c.offsetX.value,n=0;n=e.loop?t>0?m.value>0?-1:1:0:-Math[m.value>0?`ceil`:`floor`](m.value/p.value),C({pace:n,emitChange:!0})}else m.value&&C({pace:0});s=!1,o.swiping=!1,n(`dragEnd`,{index:y.value}),O()},oe=(t,n={})=>{w(),c.reset(),I(()=>{let r;r=e.loop&&t===f.value?o.active===0?0:t:t%f.value,n.immediate?I(()=>{o.swiping=!1}):o.swiping=!1,C({pace:r-o.active,emitChange:!0})})},se=(t,n)=>{let r=n===y.value,i=r?{backgroundColor:e.indicatorColor}:void 0;return R(`i`,{style:i,class:mt(`indicator`,{active:r})},null)},ce=()=>{if(r.indicator)return r.indicator({active:y.value,total:f.value});if(e.showIndicators&&f.value>1)return R(`div`,{class:mt(`indicators`,{vertical:e.vertical})},[Array(f.value).fill(``).map(se)])};return _({prev:te,next:ne,state:o,resize:M,swipeTo:oe}),d({size:p,props:e,count:f,activeIndicator:y}),B(()=>e.initialSwipe,e=>j(+e)),B(f,()=>j(o.active)),B(()=>e.autoplay,O),B([fe,ue,()=>e.width,()=>e.height],M),B(ae(),e=>{e===`visible`?O():D()}),de(j),me(()=>j(o.active)),t(()=>j(o.active)),Ae(D),je(D),xe(`touchmove`,re,{target:a}),()=>{var t;return R(`div`,{ref:i,class:mt()},[R(`div`,{ref:a,style:x.value,class:mt(`track`,{vertical:e.vertical}),onTouchstartPassive:P,onTouchend:F,onTouchcancel:F},[(t=r.default)==null?void 0:t.call(r)]),ce()])}}}),vt=Oe(_t),[yt,bt]=G(`tabs`),xt=O({name:yt,props:{count:se(Number),inited:Boolean,animated:Boolean,duration:se(H),swipeable:Boolean,lazyRender:Boolean,currentIndex:se(Number)},emits:[`change`],setup(e,{emit:t,slots:n}){let r=L(),i=e=>t(`change`,e),a=()=>{var t;let a=(t=n.default)==null?void 0:t.call(n);return e.animated||e.swipeable?R(vt,{ref:r,loop:!1,class:bt(`track`),duration:e.duration*1e3,touchable:e.swipeable,lazyRender:e.lazyRender,showIndicators:!1,onChange:i},{default:()=>[a]}):a},o=t=>{let n=r.value;n&&n.state.active!==t&&n.swipeTo(t,{immediate:!e.inited})};return B(()=>e.currentIndex,o),de(()=>{o(e.currentIndex)}),_({swipeRef:r}),()=>R(`div`,{class:bt(`content`,{animated:e.animated||e.swipeable})},[a()])}}),[St,Ct]=G(`tabs`),wt={type:Ce(`line`),color:String,border:Boolean,sticky:Boolean,shrink:Boolean,active:Y(0),duration:Y(.3),animated:Boolean,ellipsis:W,swipeable:Boolean,scrollspy:Boolean,offsetTop:Y(0),background:String,lazyRender:W,showHeader:W,lineWidth:H,lineHeight:H,beforeChange:Function,swipeThreshold:Y(5),titleActiveColor:String,titleInactiveColor:String},Tt=Symbol(St),Et=O({name:St,props:wt,emits:[`change`,`scroll`,`rendered`,`clickTab`,`update:active`],setup(e,{emit:r,slots:a}){let o,c,l,u,d,f=L(),p=L(),m=L(),h=L(),g=y(),v=re(f),[b,x]=ot(),{children:S,linkChildren:ee}=E(Tt),C=ie({inited:!1,position:``,lineStyle:{},currentIndex:-1}),w=J(()=>S.length>+e.swipeThreshold||!e.ellipsis||e.shrink),te=J(()=>({borderColor:e.color,background:e.background})),T=(e,t)=>{var n;return(n=e.name)==null?t:n},O=J(()=>{let e=S[C.currentIndex];if(e)return T(e,C.currentIndex)}),j=J(()=>D(e.offsetTop)),M=J(()=>e.sticky?j.value+o:0),N=t=>{let n=p.value,r=b.value;if(!w.value||!n||!r||!r[C.currentIndex])return;let i=r[C.currentIndex].$el,a=i.offsetLeft-(n.offsetWidth-i.offsetWidth)/2;u&&u(),u=it(n,a,t?0:+e.duration)},P=()=>{let t=C.inited;A(()=>{let n=b.value;if(!n||!n[C.currentIndex]||e.type!==`line`||k(f.value))return;let r=n[C.currentIndex].$el,{lineWidth:i,lineHeight:a}=e,o=r.offsetLeft+r.offsetWidth/2,s={width:Be(i),backgroundColor:e.color,transform:`translateX(${o}px) translateX(-50%)`};if(t&&(s.transitionDuration=`${e.duration}s`),Ee(a)){let e=Be(a);s.height=e,s.borderRadius=e}C.lineStyle=s})},ae=e=>{let t=e=0&&e{let i=ae(t);if(!Ee(i))return;let a=S[i],o=T(a,i),s=C.currentIndex!==null;C.currentIndex!==i&&(C.currentIndex=i,n||N(),P()),o!==e.active&&(r(`update:active`,o),s&&r(`change`,o,a.title)),l&&!e.scrollspy&&ne(Math.ceil(Ve(f.value)-j.value))},I=(e,t)=>{let n=S.findIndex((t,n)=>T(t,n)===e);F(n===-1?0:n,t)},oe=(t=!1)=>{if(e.scrollspy){let n=S[C.currentIndex].$el;if(n&&v.value){let r=Ve(n,v.value)-M.value;c=!0,d&&d(),d=at(v.value,r,t?0:+e.duration,()=>{c=!1})}}},se=(t,i,a)=>{let{title:o,disabled:c}=S[i],l=T(S[i],i);c||(s(e.beforeChange,{args:[l],done:()=>{F(i),oe()}}),n(t)),r(`clickTab`,{name:l,title:o,event:a,disabled:c})},ce=e=>{l=e.isFixed,r(`scroll`,e)},le=e=>{A(()=>{I(e),oe(!0)})},ue=()=>{for(let e=0;eM.value)return e===0?0:e-1}return S.length-1},de=()=>{if(e.scrollspy&&!c){let e=ue();F(e)}},pe=()=>{if(e.type===`line`&&S.length)return R(`div`,{class:Ct(`line`),style:C.lineStyle},null)},he=()=>{var t,n,r;let{type:o,border:s,sticky:c}=e,l=[R(`div`,{ref:c?void 0:m,class:[Ct(`wrap`),{[i]:o===`line`&&s}]},[R(`div`,{ref:p,role:`tablist`,class:Ct(`nav`,[o,{shrink:e.shrink,complete:w.value}]),style:te.value,"aria-orientation":`horizontal`},[(t=a[`nav-left`])==null?void 0:t.call(a),S.map(e=>e.renderTitle(se)),pe(),(n=a[`nav-right`])==null?void 0:n.call(a)])]),(r=a[`nav-bottom`])==null?void 0:r.call(a)];return c?R(`div`,{ref:m},[l]):l},ge=()=>{P(),A(()=>{var e,t;N(!0),(t=(e=h.value)==null?void 0:e.swipeRef.value)==null||t.resize()})};return B(()=>[e.color,e.duration,e.lineWidth,e.lineHeight],P),B(fe,ge),B(()=>e.active,e=>{e!==O.value&&I(e)}),B(()=>S.length,()=>{C.inited&&(I(e.active),P(),A(()=>{N(!0)}))}),_({resize:ge,scrollTo:le}),me(P),t(P),_e(()=>{I(e.active,!0),A(()=>{C.inited=!0,m.value&&(o=V(m.value).height),N(!0)})}),st(f,P),xe(`scroll`,de,{target:v,passive:!0}),ee({id:g,props:e,setLine:P,scrollable:w,onRendered:(e,t)=>r(`rendered`,e,t),currentName:O,setTitleRefs:x,scrollIntoView:N}),()=>R(`div`,{ref:f,class:Ct([e.type])},[e.showHeader?e.sticky?R(ft,{container:f.value,offsetTop:j.value,onScroll:ce},{default:()=>[he()]}):he():null,R(xt,{ref:h,count:S.length,inited:C.inited,animated:e.animated,duration:e.duration,swipeable:e.swipeable,lazyRender:e.lazyRender,currentIndex:C.currentIndex,onChange:F},{default:()=>{var e;return[(e=a.default)==null?void 0:e.call(a)]}})])}}),Dt=Symbol(),Ot=Symbol(),kt=()=>ye(Ot,null),At=e=>{let t=kt();oe(Dt,e),oe(Ot,J(()=>(t==null||t.value)&&e.value))},[jt,Mt]=G(`tab`),Nt=O({name:jt,props:{id:String,dot:Boolean,type:String,color:String,title:String,badge:H,shrink:Boolean,isActive:Boolean,disabled:Boolean,controls:String,scrollable:Boolean,activeColor:String,inactiveColor:String,showZeroBadge:W},setup(e,{slots:t}){let n=J(()=>{let t={},{type:n,color:r,disabled:i,isActive:a,activeColor:o,inactiveColor:s}=e;r&&n===`card`&&(t.borderColor=r,i||(a?t.backgroundColor=r:t.color=r));let c=a?o:s;return c&&(t.color=c),t}),r=()=>{let n=R(`span`,{class:Mt(`text`,{ellipsis:!e.scrollable})},[t.title?t.title():e.title]);return e.dot||Ee(e.badge)&&e.badge!==``?R(a,{dot:e.dot,content:e.badge,showZero:e.showZeroBadge},{default:()=>[n]}):n};return()=>R(`div`,{id:e.id,role:`tab`,class:[Mt([e.type,{grow:e.scrollable&&!e.shrink,shrink:e.shrink,active:e.isActive,disabled:e.disabled}])],style:n.value,tabindex:e.disabled?void 0:e.isActive?0:-1,"aria-selected":e.isActive,"aria-disabled":e.disabled||void 0,"aria-controls":e.controls,"data-allow-mismatch":`attribute`},[r()])}}),[Pt,Ft]=G(`swipe-item`),It=O({name:Pt,setup(e,{slots:t}){let n,r=ie({offset:0,inited:!1,mounted:!1}),{parent:i,index:a}=be(gt);if(!i)return;let o=J(()=>{let e={},{vertical:t}=i.props;return i.size.value&&(e[t?`height`:`width`]=`${i.size.value}px`),r.offset&&(e.transform=`translate${t?`Y`:`X`}(${r.offset}px)`),e}),s=J(()=>{let{loop:e,lazyRender:t}=i.props;if(!t||n)return!0;if(!r.mounted)return!1;let o=i.activeIndicator.value,s=i.count.value-1,c=o===0&&e?s:o-1,l=o===s&&e?0:o+1;return n=a.value===o||a.value===c||a.value===l,n});return de(()=>{A(()=>{r.mounted=!0})}),_({setOffset:e=>{r.offset=e}}),()=>{var e;return R(`div`,{class:Ft(),style:o.value},[s.value?(e=t.default)==null?void 0:e.call(t):null])}}}),Lt=Oe(It),[Rt,zt]=G(`tab`),Bt=ge({},e,{dot:Boolean,name:H,badge:H,title:String,disabled:Boolean,titleClass:null,titleStyle:[String,Object],showZeroBadge:W}),Vt=O({name:Rt,props:Bt,setup(e,{slots:t}){let n=y(),r=L(!1),i=He(),{parent:a,index:o}=be(Tt);if(!a)return;let s=()=>{var t;return(t=e.name)==null?o.value:t},c=()=>{r.value=!0,a.props.lazyRender&&A(()=>{a.onRendered(s(),e.title)})},l=J(()=>{let e=s()===a.currentName.value;return e&&!r.value&&c(),e}),u=L(``),d=L(``);F(()=>{let{titleClass:t,titleStyle:n}=e;u.value=t?rt(t):``,d.value=n&&typeof n!=`string`?nt(Ze(n)):n});let f=r=>R(Nt,M({key:n,id:`${a.id}-${o.value}`,ref:a.setTitleRefs(o.value),style:d.value,class:u.value,isActive:l.value,controls:n,scrollable:a.scrollable.value,activeColor:a.props.titleActiveColor,inactiveColor:a.props.titleInactiveColor,onClick:e=>r(i.proxy,o.value,e)},Fe(a.props,[`type`,`color`,`shrink`]),Fe(e,[`dot`,`badge`,`title`,`disabled`,`showZeroBadge`])),{title:t.title}),p=L(!l.value);return B(l,e=>{e?p.value=!1:I(()=>{p.value=!0})}),B(()=>e.title,()=>{a.setLine(),a.scrollIntoView()}),At(l),_({id:n,renderTitle:f}),()=>{var e;let i=`${a.id}-${o.value}`,{animated:s,swipeable:c,scrollspy:u,lazyRender:d}=a.props;if(!t.default&&!s)return;let f=u||l.value;if(s||c)return R(Lt,{id:n,role:`tabpanel`,class:zt(`panel-wrapper`,{inactive:p.value}),tabindex:l.value?0:-1,"aria-hidden":!l.value,"aria-labelledby":i,"data-allow-mismatch":`attribute`},{default:()=>{var e;return[R(`div`,{class:zt(`panel`)},[(e=t.default)==null?void 0:e.call(t)])]}});let m=r.value||u||!d?(e=t.default)==null?void 0:e.call(t):null;return Se(R(`div`,{id:n,role:`tabpanel`,class:zt(`panel`),tabindex:f?0:-1,"aria-labelledby":i,"data-allow-mismatch":`attribute`},[m]),[[ze,f]])}}}),Ht=Oe(Vt),Ut=Oe(Et),[Wt,Gt,Kt]=G(`list`),qt={error:Boolean,offset:Y(300),loading:Boolean,disabled:Boolean,finished:Boolean,scroller:Object,errorText:String,direction:Ce(`down`),loadingText:{type:String,default:``},finishedText:String,immediateCheck:W},Jt=O({name:Wt,props:qt,emits:[`load`,`update:error`,`update:loading`],setup(e,{emit:t,slots:n}){let r=L(e.loading),i=L(),a=L(),o=kt(),s=re(i),c=J(()=>e.scroller||s.value),l=()=>{A(()=>{if(r.value||e.finished||e.disabled||e.error||(o==null?void 0:o.value)===!1)return;let{direction:n}=e,s=+e.offset,l=V(c);if(!l.height||k(i))return;let u=!1,d=V(a);u=n===`up`?l.top-d.top<=s:d.bottom-l.bottom<=s,u&&(r.value=!0,t(`update:loading`,!0),t(`load`))})},u=()=>{if(e.finished){let t=n.finished?n.finished():e.finishedText;if(t)return R(`div`,{class:Gt(`finished-text`)},[t])}},d=()=>{t(`update:error`,!1),l()},f=()=>{if(e.error){let t=n.error?n.error():e.errorText;if(t)return R(`div`,{role:`button`,class:Gt(`error-text`),tabindex:0,onClick:d},[t])}},p=()=>{if(r.value&&!e.finished&&!e.disabled)return R(`div`,{class:Gt(`loading`)},[n.loading?n.loading():e.loadingText!=null&&R(g,{class:Gt(`loading-icon`)},{default:()=>[e.loadingText||Kt(`loading`)]})])};return B(()=>[e.loading,e.finished,e.error],l),o&&B(o,e=>{e&&l()}),he(()=>{r.value=e.loading}),de(()=>{e.immediateCheck&&l()}),_({check:l}),xe(`scroll`,l,{target:c,passive:!0}),()=>{var t;let o=(t=n.default)==null?void 0:t.call(n),s=R(`div`,{ref:a,class:Gt(`placeholder`)},null);return R(`div`,{ref:i,role:`feed`,class:Gt(),"aria-busy":r.value},[e.direction===`down`?o:s,p(),u(),f(),e.direction===`up`?o:s])}}}),Yt=Oe(Jt),Xt={class:`detail`},Zt={class:`detail-head`},Qt={class:`avatar`,"aria-hidden":`true`},$t={class:`detail-title`},en={class:`app-muted`},tn={key:0,class:`badge`},nn={class:`app-mono`},rn={key:0,class:`state state--blocked`},an={key:1,class:`note`},on={key:1,class:`state`},sn={key:1,class:`note`},cn=o(O({__name:`ConversationDetail`,props:{conversation:{},accountId:{},canAdd:{type:Boolean},canRemove:{type:Boolean},addCode:{},removeCode:{}},emits:[`updated`],setup(e,{emit:t}){let n=e,i=t,a=L(``),o=L(!1);B(()=>`${n.conversation.account}|${n.conversation.conversation}`,()=>{a.value=``});let s=J(()=>n.conversation.blacklist),l=J(()=>{var e;let t=(e=s.value)==null?void 0:e.source;return t===`mobile`?`手机控制台`:t===`desktop`?`客服电脑`:t||`—`});async function d(){if(!o.value){try{await f({title:`加入忽略?`,message:`加入后,系统不再向「${n.conversation.name}」自动发送任何消息,只能由客服手动处理。`,confirmButtonText:`加入忽略`,confirmButtonColor:`var(--app-danger)`})}catch{return}o.value=!0;try{let{entry:e}=await Me.block(n.conversation,a.value.trim(),n.accountId);i(`updated`,{...n.conversation,blocked:!0,blacklist:e}),x({message:`已加入忽略,电脑同步后立即生效`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}async function p(){if(!o.value){try{await f({title:`移出忽略?`,message:`移出后,系统会恢复向「${n.conversation.name}」自动回复。`,confirmButtonText:`移出`})}catch{return}o.value=!0;try{await Me.unblock(n.conversation.account,n.conversation.conversation,n.accountId),i(`updated`,{...n.conversation,blocked:!1,blacklist:null}),x({message:`已移出忽略`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}return(t,n)=>(P(),$(`section`,Xt,[q(`header`,Zt,[q(`span`,Qt,U(X(u)(e.conversation.name)),1),q(`div`,$t,[q(`h2`,null,U(e.conversation.name),1),q(`p`,en,U(e.conversation.type_label),1)]),e.conversation.blocked?(P(),$(`span`,tn,[R(X(r),{name:`shield-o`}),n[1]||(n[1]=Q(` 忽略`,-1))])):Z(``,!0)]),R(X(v),{inset:``,class:`group`},{default:z(()=>[R(X(h),{title:`企业微信账号`,value:e.conversation.account_name||`—`,label:e.conversation.account},null,8,[`value`,`label`]),R(X(h),{title:`最近消息`,value:e.conversation.last_message_at?X(c)(e.conversation.last_message_at):`—`},null,8,[`value`]),R(X(h),{title:`会话标识`},{label:z(()=>[q(`span`,nn,U(e.conversation.conversation),1)]),_:1})]),_:1}),e.conversation.blocked&&s.value?(P(),$(`div`,rn,[q(`h3`,null,[R(X(r),{name:`shield-o`}),n[2]||(n[2]=Q(` 已加入忽略`,-1))]),n[8]||(n[8]=q(`p`,null,`系统不会向这个会话自动发送消息;电脑离线时,下次联网自动同步。`,-1)),q(`dl`,null,[q(`div`,null,[n[3]||(n[3]=q(`dt`,null,`原因`,-1)),q(`dd`,null,U(s.value.reason||`未填写`),1)]),q(`div`,null,[n[4]||(n[4]=q(`dt`,null,`操作人`,-1)),q(`dd`,null,U(s.value.created_by||`—`),1)]),q(`div`,null,[n[5]||(n[5]=q(`dt`,null,`时间`,-1)),q(`dd`,null,U(X(c)(s.value.created_at)),1)]),q(`div`,null,[n[6]||(n[6]=q(`dt`,null,`来源`,-1)),q(`dd`,null,U(l.value),1)])]),e.canRemove?(P(),K(X(m),{key:0,block:``,round:``,plain:``,type:`primary`,icon:`revoke`,loading:o.value,onClick:p},{default:z(()=>[...n[7]||(n[7]=[Q(` 移出忽略 `,-1)])]),_:1},8,[`loading`])):(P(),$(`p`,an,`移出忽略需要主管权限(`+U(e.removeCode)+`),请联系主管处理。`,1))])):(P(),$(`div`,on,[q(`h3`,null,[R(X(r),{name:`info-o`}),n[9]||(n[9]=Q(` 正常接收自动回复`,-1))]),n[11]||(n[11]=q(`p`,null,`客户投诉、需要人工跟进或不适合机器人回复的会话,可以加入忽略。`,-1)),e.canAdd?(P(),$(De,{key:0},[R(X(v),{inset:``,class:`reason`},{default:z(()=>[R(X(b),{modelValue:a.value,"onUpdate:modelValue":n[0]||(n[0]=e=>a.value=e),label:`原因`,placeholder:`选填,例如:客户要求人工服务`,maxlength:`200`,"show-word-limit":``,rows:`2`,autosize:``,type:`textarea`},null,8,[`modelValue`])]),_:1}),R(X(m),{block:``,round:``,type:`danger`,icon:`shield-o`,loading:o.value,onClick:d},{default:z(()=>[...n[10]||(n[10]=[Q(` 加入忽略 `,-1)])]),_:1},8,[`loading`])],64)):(P(),$(`p`,sn,`加入忽略需要 ZYT 权限(`+U(e.addCode)+`),请联系管理员开通。`,1))]))]))}}),[[`__scopeId`,`data-v-bdbcc19d`]]),ln={class:`body`},un={class:`line`},dn={class:`name`},fn={class:`time`},pn={class:`line sub`},mn={class:`meta`},hn={key:0,class:`blocked`},gn=o(O({__name:`ConversationRow`,props:{item:{},active:{type:Boolean}},emits:[`select`],setup(e){let t=e,n=J(()=>t.item.type===`group`?`group`:t.item.type===`direct_wechat`?`wechat`:`wecom`);return(t,r)=>(P(),$(`button`,{type:`button`,class:ce([`row`,{active:e.active}]),onClick:r[0]||(r[0]=n=>t.$emit(`select`,e.item))},[q(`span`,{class:ce([`avatar`,`avatar--${n.value}`]),"aria-hidden":`true`},U(X(u)(e.item.name)),3),q(`span`,ln,[q(`span`,un,[q(`strong`,dn,U(e.item.name),1),q(`span`,fn,U(e.item.last_message_at?X(c)(e.item.last_message_at):``),1)]),q(`span`,pn,[q(`span`,mn,[Q(U(e.item.type_label),1),e.item.account_name?(P(),$(De,{key:0},[Q(` · `+U(e.item.account_name),1)],64)):Z(``,!0)]),e.item.blocked?(P(),$(`span`,hn,`已拉黑`)):Z(``,!0)])])],2))}}),[[`__scopeId`,`data-v-645c253b`]]),_n={class:`page`},vn={class:`list-pane`},yn={class:`toolbar`},bn={key:0,class:`detail-pane app-card`},xn=o(O({name:`ConversationsView`,__name:`ConversationsView`,setup(e){let t=Ie(),n=L(``),i=L(`all`),a=L([]),o=L(``),s=L(!1),c=L(!1),l=L(!1),u=L(``),f=L(!1),h=L(0),g=L(null),_=L(!1),v=L(!1),y=0,b=null,x,te=J(()=>{var e;return(e=Ne.me)==null?void 0:e.permission_codes}),ne=J(()=>Pe.value===void 0?`会话`:`${Le.value} 的会话`),T=e=>`${e.account}|${e.conversation}`;function E(){y+=1,b==null||b.abort(),b=null,a.value=[],o.value=``,s.value=!1,l.value=!1,u.value=``,c.value=!1,D()}async function D(){if(s.value)return;let e=y;b=new AbortController,c.value=!0;try{let r=await Me.conversations({accountId:Pe.value,q:n.value.trim(),scope:i.value,cursor:o.value,limit:30,signal:b.signal});if(e!==y)return;let c=new Set(a.value.map(T));a.value=a.value.concat(r.items.filter(e=>!c.has(T(e)))),o.value=r.next_cursor,s.value=!r.has_more,h.value=r.blocked_total,l.value=!1,t.value&&!g.value&&a.value.length&&(g.value=a.value[0])}catch(t){if(e!==y||t instanceof DOMException&&t.name===`AbortError`)return;l.value=!0,u.value=t instanceof Te?t.message:`加载失败`}finally{e===y&&(c.value=!1,f.value=!1)}}function O(){f.value=!0,E()}function k(e){g.value=e,t.value||(_.value=!0)}function A(e){var t;let n=a.value.findIndex(t=>T(t)===T(e)),r=n>=0?a.value[n].blocked:(t=g.value)==null?void 0:t.blocked;n>=0&&(i.value===`blocked`&&!e.blocked?a.value.splice(n,1):a.value.splice(n,1,e)),r!==e.blocked&&(h.value+=e.blocked?1:-1),g.value=e}return B(n,()=>{window.clearTimeout(x),x=window.setTimeout(E,350)}),B(i,E),B(Pe,()=>{g.value=null,_.value=!1,E()}),E(),(e,o)=>{var y,b,x,E,j,M,N,re;return P(),$(`div`,_n,[R(X(d),{title:ne.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},we({_:2},[(y=X(Re))!=null&&y.control_all?{name:`right`,fn:z(()=>[q(`button`,{type:`button`,class:`nav-action`,onClick:o[0]||(o[0]=e=>v.value=!0)},[R(X(r),{name:`friends-o`}),o[9]||(o[9]=Q(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),q(`div`,{class:ce(X(t)?`split`:`single`)},[q(`section`,vn,[q(`div`,yn,[R(X(ee),{modelValue:n.value,"onUpdate:modelValue":o[1]||(o[1]=e=>n.value=e),shape:`round`,placeholder:`搜索会话名称`,clearable:!0},null,8,[`modelValue`]),R(X(Ut),{active:i.value,"onUpdate:active":o[2]||(o[2]=e=>i.value=e),shrink:``,border:!1,class:`tabs`},{default:z(()=>[R(X(Ht),{name:`all`,title:`全部会话`}),R(X(Ht),{name:`blocked`,title:h.value?`忽略 ${h.value}`:`忽略`},null,8,[`title`])]),_:1},8,[`active`])]),R(X(C),{modelValue:f.value,"onUpdate:modelValue":o[6]||(o[6]=e=>f.value=e),class:`list-body`,onRefresh:O},{default:z(()=>[R(X(Yt),{loading:c.value,"onUpdate:loading":o[3]||(o[3]=e=>c.value=e),error:l.value,"onUpdate:error":o[4]||(o[4]=e=>l.value=e),finished:s.value,"error-text":`${u.value},点击重试`,"finished-text":a.value.length?`没有更多了`:``,"immediate-check":!1,onLoad:D},{default:z(()=>[(P(!0),$(De,null,le(a.value,e=>(P(),K(gn,{key:T(e),item:e,active:X(t)&&g.value!==null&&T(g.value)===T(e),onSelect:k},null,8,[`item`,`active`]))),128))]),_:1},8,[`loading`,`error`,`finished`,`error-text`,`finished-text`]),s.value&&!a.value.length&&!l.value?(P(),K(X(S),{key:0,image:n.value?`search`:`default`,description:n.value?`没有找到匹配的会话`:i.value===`blocked`?`忽略是空的`:`还没有归档的会话:电脑开启聊天备份后会自动出现`},{default:z(()=>[n.value?(P(),K(X(m),{key:0,round:``,size:`small`,onClick:o[5]||(o[5]=e=>n.value=``)},{default:z(()=>[...o[10]||(o[10]=[Q(`清除搜索`,-1)])]),_:1})):Z(``,!0)]),_:1},8,[`image`,`description`])):Z(``,!0)]),_:1},8,[`modelValue`])]),X(t)?(P(),$(`section`,bn,[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((b=X(Re))!=null&&b.blacklist_add),"can-remove":!!((x=X(Re))!=null&&x.blacklist_remove),"add-code":(E=(j=te.value)==null?void 0:j.blacklist_add)==null?``:E,"remove-code":(M=(N=te.value)==null?void 0:N.blacklist_remove)==null?``:M,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):(P(),K(X(S),{key:1,image:`default`,description:`选择左侧的会话查看详情`}))])):Z(``,!0)],2),X(t)?Z(``,!0):(P(),K(X(p),{key:0,show:_.value,"onUpdate:show":o[7]||(o[7]=e=>_.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`sheet`},{default:z(()=>{var e,t,n,r,i,a;return[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((e=X(Re))!=null&&e.blacklist_add),"can-remove":!!((t=X(Re))!=null&&t.blacklist_remove),"add-code":(n=(r=te.value)==null?void 0:r.blacklist_add)==null?``:n,"remove-code":(i=(a=te.value)==null?void 0:a.blacklist_remove)==null?``:i,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):Z(``,!0)]}),_:1},8,[`show`])),(re=X(Re))!=null&&re.control_all?(P(),K(w,{key:1,show:v.value,"onUpdate:show":o[8]||(o[8]=e=>v.value=e)},null,8,[`show`])):Z(``,!0)])}}}),[[`__scopeId`,`data-v-4433756d`]]);export{xn as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css new file mode 100644 index 000000000..617ba43f7 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css @@ -0,0 +1 @@ +.panel[data-v-bcd7f731]{padding:16px}.panel-head[data-v-bcd7f731]{align-items:flex-start;gap:12px;display:flex}.panel-icon[data-v-bcd7f731]{width:44px;height:44px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:12px;flex:none;place-items:center;font-size:22px;display:grid}.panel-title[data-v-bcd7f731]{flex:1;min-width:0}.panel-title h2[data-v-bcd7f731]{text-overflow:ellipsis;white-space:nowrap;margin:2px 0 4px;font-size:17px;font-weight:600;overflow:hidden}.panel-title p[data-v-bcd7f731]{margin:0;font-size:13px;line-height:1.45}.pill[data-v-bcd7f731]{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.pill .dot[data-v-bcd7f731]{background:currentColor;border-radius:50%;width:7px;height:7px}.pill--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.pill--success .dot[data-v-bcd7f731]{box-shadow:0 0 0 3px var(--app-success-soft);animation:2.2s ease-in-out infinite breathe-bcd7f731}.pill--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.pill--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.pill--muted[data-v-bcd7f731],.pill--primary[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}@keyframes breathe-bcd7f731{50%{opacity:.45}}.stats[data-v-bcd7f731]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0 0;display:grid}.stat[data-v-bcd7f731]{background:var(--app-surface-2);border:1px solid var(--app-border);border-radius:12px;padding:10px 12px}.stat dt[data-v-bcd7f731]{color:var(--app-muted);font-size:12px}.stat dd[data-v-bcd7f731]{margin:4px 0 0;font-size:15px;font-weight:600}.meta[data-v-bcd7f731]{margin:12px 0 0;font-size:12px}.progress[data-v-bcd7f731]{border-radius:12px;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45;display:flex}.progress--primary[data-v-bcd7f731]{color:var(--app-primary);background:var(--app-primary-soft)}.progress--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.progress--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.progress--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.progress--muted[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}.actions[data-v-bcd7f731]{margin-top:16px}.blocked[data-v-bcd7f731]{margin:10px 4px 0;font-size:13px;line-height:1.5}.page[data-v-8ead1f63]{min-height:100%}.page-body[data-v-8ead1f63]{min-height:calc(100vh - 96px);min-height:calc(100dvh - 96px)}.content[data-v-8ead1f63]{max-width:720px;margin:0 auto;padding:12px 16px 24px}.stack[data-v-8ead1f63]{gap:12px;display:grid}.skeleton-card[data-v-8ead1f63]{padding:16px 0}.empty-hint[data-v-8ead1f63]{max-width:280px;color:var(--app-muted);text-align:center;margin:0 auto;font-size:13px;line-height:1.6}.nav-action[data-v-8ead1f63]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.stale[data-v-8ead1f63]{margin:0 4px;font-size:12px}.split[data-v-8ead1f63]{grid-template-columns:minmax(240px,320px) minmax(0,1fr);align-items:start;gap:16px;max-width:1080px;margin:0 auto;padding:16px 24px 24px;display:grid}.split-list[data-v-8ead1f63]{gap:10px;display:grid}.device-row[data-v-8ead1f63]{text-align:left;width:100%;color:inherit;cursor:pointer;align-items:center;gap:12px;padding:12px 14px;display:flex}.device-row.active[data-v-8ead1f63]{border-color:var(--app-primary);box-shadow:0 0 0 2px var(--app-primary-soft)}.device-row-icon[data-v-8ead1f63]{color:var(--app-primary);font-size:20px}.device-row-text[data-v-8ead1f63]{gap:2px;min-width:0;display:grid}.device-row-text strong[data-v-8ead1f63]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;overflow:hidden}.device-row-text small[data-v-8ead1f63]{font-size:12px} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js new file mode 100644 index 000000000..fd0676b92 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-HbIaJI1u.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n,o as r,r as i,s as a,t as o}from"./format-Dt9-L1ne.js";import{a as s,o as c}from"./cell-group-B1mihubH.js";import{a as l,i as u,r as d,t as f}from"./AccountSwitcher-DmKdvr8J.js";import{Ct as p,Et as m,Ft as h,Jt as g,Kt as _,Lt as v,St as y,Ut as b,Vt as x,X as S,Y as C,Yt as w,Z as T,_ as E,_t as D,bt as O,c as k,f as A,ft as j,g as M,gt as N,ht as P,kt as F,l as I,n as L,o as R,p as z,q as B,qt as V,s as ee,t as H,v as U,vt as W,x as te,xt as G,yt as K}from"./index-C_M75pH8.js";var[ne,re]=E(`skeleton-title`),ie=p({name:ne,props:{round:Boolean,titleWidth:S},setup(e){return()=>y(`h3`,{class:re([{round:e.round}]),style:{width:U(e.titleWidth)}},null)}}),q=M(ie),[J,Y]=E(`skeleton-avatar`),ae={avatarSize:S,avatarShape:C(`round`)},oe=p({name:J,props:ae,setup(e){return()=>y(`div`,{class:Y([e.avatarShape]),style:te(e.avatarSize)},null)}}),se=M(oe),X=`100%`,ce={round:Boolean,rowWidth:{type:S,default:X}},[le,ue]=E(`skeleton-paragraph`),de=p({name:le,props:ce,setup(e){return()=>y(`div`,{class:ue([{round:e.round}]),style:{width:e.rowWidth}},null)}}),fe=M(de),[pe,Z]=E(`skeleton`),me=`60%`,he={row:B(0),round:Boolean,title:Boolean,titleWidth:S,avatar:Boolean,avatarSize:S,avatarShape:C(`round`),loading:T,animate:T,rowWidth:{type:[Number,String,Array],default:X}},ge=p({name:pe,inheritAttrs:!1,props:he,setup(e,{slots:t,attrs:n}){let r=()=>{if(e.avatar)return y(se,{avatarShape:e.avatarShape,avatarSize:e.avatarSize},null)},i=()=>{if(e.title)return y(q,{round:e.round,titleWidth:e.titleWidth},null)},a=t=>{let{rowWidth:n}=e;return n===`100%`&&t===e.row-1?me:Array.isArray(n)?n[t]:n},o=()=>Array(+e.row).fill(``).map((t,n)=>y(fe,{key:n,round:e.round,rowWidth:U(a(n))},null)),s=()=>t.template?t.template():y(j,null,[r(),y(`div`,{class:Z(`content`)},[i(),o()])]);return()=>{var r;return e.loading?y(`div`,m({class:Z({animate:e.animate,round:e.round})},n),[s()]):(r=t.default)==null?void 0:r.call(t)}}}),_e=M(ge),ve={class:`panel-head`},ye={class:`panel-icon`,"aria-hidden":`true`},be={class:`panel-title`},xe={class:`app-muted`},Se={class:`stats`},Ce={class:`stat`},we={class:`stat`},Te={class:`stat`},Ee={class:`stat`},De={class:`meta app-muted`},Oe={key:0},ke={key:1},Ae={class:`actions`},je={key:0,class:`blocked app-muted`},Q=t(p({__name:`DevicePanel`,props:{device:{},accountId:{},canControl:{type:Boolean},controlCode:{}},emits:[`changed`],setup(t,{emit:r}){let u=t,d=r,f=_(!1),p=_(null),m,v=0,S=P(()=>n(u.device)),C=P(()=>u.device.status),T=P(()=>{var e;return!!((e=C.value)!=null&&e.listening)}),E=P(()=>T.value?`stop_listening`:`start_listening`),O=P(()=>p.value?o(p.value):null),A=P(()=>{var e;return f.value||!!((e=O.value)!=null&&e.busy)}),j=P(()=>u.device.online?u.device.remote_capable?u.canControl?``:`你的 ZYT 账号没有远程启停权限(${u.controlCode}),请联系管理员开通。`:`电脑上的客服软件版本过低,升级到最新版后才能远程控制。`:`电脑离线:客服软件没有运行,无法远程拉起。请到电脑上打开软件。`);function M(){m!==void 0&&window.clearTimeout(m),m=void 0}async function L(){let e=p.value;if(e&&!e.final){try{let{command:t}=await I.command(e.id);if(p.value=t,t.final){d(`changed`);let e=o(t).text;l({message:e,type:t.status===`succeeded`?`success`:`fail`});return}}catch(e){if(e instanceof k&&e.status===404)return}Date.now()u.device.last_command,e=>{if(!e)return;if(p.value&&p.value.id===e.id){!p.value.final&&e.final&&(p.value=e);return}let t=e.expires_at*1e3>Date.now();!e.final&&t?R(e):!p.value&&e.final&&t&&(p.value=e)},{immediate:!0}),F(M);async function z(){if(A.value||j.value)return;let e=E.value===`stop_listening`;try{await a({title:e?`停止自动回复?`:`启动自动回复?`,message:e?`「${u.device.device_name||`这台电脑`}」将停止监听,新消息不再自动回复。`:`「${u.device.device_name||`这台电脑`}」将开始监听,并按电脑上的设置自动回复新消息。`,confirmButtonText:e?`停止`:`启动`,confirmButtonColor:e?`var(--app-danger)`:`var(--app-primary)`})}catch{return}f.value=!0;try{let{command:e}=await I.sendCommand(u.device.device_id,E.value,u.accountId);R(e)}catch(e){l({message:e instanceof k?e.message:`指令发送失败,请重试`,type:`fail`}),d(`changed`)}finally{f.value=!1}}return(n,r)=>{var a,o,l,u,d;return h(),K(`article`,{class:g([`panel app-card`,`panel--${S.value.tone}`])},[N(`header`,ve,[N(`div`,ye,[y(V(e),{name:`desktop-o`})]),N(`div`,be,[N(`h2`,null,w(t.device.device_name||`未命名电脑`),1),N(`p`,xe,w(S.value.hint),1)]),N(`span`,{class:g([`pill`,`pill--${S.value.tone}`])},[r[0]||(r[0]=N(`span`,{class:`dot`,"aria-hidden":`true`},null,-1)),G(` `+w(S.value.label),1)],2)]),N(`dl`,Se,[N(`div`,Ce,[r[1]||(r[1]=N(`dt`,null,`自动回复`,-1)),N(`dd`,{class:g(T.value?`tone-success`:`tone-muted`)},w(C.value?T.value?`监听中`:`已停止`:`—`),3)]),N(`div`,we,[r[2]||(r[2]=N(`dt`,null,`企业微信`,-1)),N(`dd`,{class:g((a=C.value)!=null&&a.wecom_connected?`tone-success`:`tone-muted`)},w(C.value?C.value.wecom_connected?`已连接`:`未连接`:`—`),3)]),N(`div`,Te,[r[3]||(r[3]=N(`dt`,null,`回复通道`,-1)),N(`dd`,null,w(((o=C.value)==null?void 0:o.transport_label)||`—`),1)]),N(`div`,Ee,[r[4]||(r[4]=N(`dt`,null,`待处理`,-1)),N(`dd`,{class:g((((l=C.value)==null?void 0:l.pending_count)||0)>0?`tone-warning`:``)},w(C.value?`${C.value.pending_count} 条`:`—`),3)])]),N(`p`,De,[N(`span`,null,`版本 `+w(t.device.app_version||`未知`),1),t.device.online&&((u=C.value)==null?void 0:u.reported_seconds)!==null&&((d=C.value)==null?void 0:d.reported_seconds)!==void 0?(h(),K(`span`,Oe,` · 状态 `+w(V(i)(C.value.reported_seconds))+`前上报 `,1)):!t.device.online&&t.device.last_seen_seconds!==null?(h(),K(`span`,ke,` · `+w(V(i)(t.device.last_seen_seconds))+`前在线 `,1)):W(``,!0)]),O.value?(h(),K(`div`,{key:0,class:g([`progress`,`progress--${O.value.tone}`]),role:`status`,"aria-live":`polite`},[O.value.busy?(h(),D(V(c),{key:0,size:`16`})):(h(),D(V(e),{key:1,name:O.value.tone===`success`?`passed`:`info-o`},null,8,[`name`])),N(`span`,null,w(O.value.text),1)],2)):W(``,!0),N(`div`,Ae,[y(V(s),{block:``,round:``,type:T.value?`danger`:`primary`,plain:T.value,icon:T.value?`pause-circle-o`:`play-circle-o`,disabled:!!j.value,loading:A.value,"loading-text":f.value?`正在下发…`:`等待电脑执行…`,onClick:z},{default:b(()=>[G(w(T.value?`停止自动回复`:`启动自动回复`),1)]),_:1},8,[`type`,`plain`,`icon`,`disabled`,`loading`,`loading-text`]),j.value?(h(),K(`p`,je,w(j.value),1)):W(``,!0)])],2)}}}),[[`__scopeId`,`data-v-bcd7f731`]]),Me={class:`page`},Ne={key:0,class:`content`},Pe={class:`app-card skeleton-card`},Fe={key:1,class:`content`},Ie={key:2,class:`content`},Le={class:`empty-hint`},$={key:3,class:`split`},Re={class:`split-list`,"aria-label":`电脑列表`},ze=[`onClick`],Be={class:`device-row-text`},Ve={key:0,class:`stale app-muted`},He={class:`split-detail`},Ue={key:4,class:`content stack`},We={key:0,class:`stale app-muted`},Ge=t(p({name:`DevicesView`,__name:`DevicesView`,setup(t){let i=z(),a=_(null),o=_(!1),c=_(!1),l=_(``),p=_(``),m=_(!1),S=0,C=P(()=>{var e,t;return(e=(t=a.value)==null?void 0:t.items)==null?[]:e}),T=P(()=>{var e,t;return(e=(t=C.value.find(e=>e.device_id===p.value))==null?C.value[0]:t)==null?null:e}),E=P(()=>{var e;return!!((e=H.value)!=null&&e.control)}),M=P(()=>{var e,t;return(e=(t=L.me)==null?void 0:t.permission_codes.control)==null?`wecom_rpa.remote/control`:e}),F=P(()=>R.value===void 0?`我的电脑`:`${ee.value} 的电脑`);async function B(){let e=++S;a.value||(o.value=!0);try{let t=await I.desktops(R.value);if(e!==S)return;a.value=t,l.value=``}catch(t){if(e!==S)return;l.value=t instanceof k?t.message:`加载失败,请重试`}finally{e===S&&(o.value=!1,c.value=!1)}}async function U(){c.value=!0,await B()}return x(R,()=>{a.value=null,p.value=``,B()},{immediate:!0}),A(B,5e3),(t,_)=>{var x,S;return h(),K(`div`,Me,[y(V(r),{title:F.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},O({_:2},[(x=V(H))!=null&&x.control_all?{name:`right`,fn:b(()=>[N(`button`,{type:`button`,class:`nav-action`,onClick:_[0]||(_[0]=e=>m.value=!0)},[y(V(e),{name:`friends-o`}),_[3]||(_[3]=G(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),y(V(d),{modelValue:c.value,"onUpdate:modelValue":_[1]||(_[1]=e=>c.value=e),class:`page-body`,onRefresh:U},{default:b(()=>[o.value&&!a.value?(h(),K(`div`,Ne,[N(`div`,Pe,[y(V(_e),{title:``,row:4})])])):l.value&&!a.value?(h(),K(`div`,Fe,[y(V(u),{image:`network`,description:l.value},{default:b(()=>[y(V(s),{round:``,type:`primary`,size:`small`,onClick:B},{default:b(()=>[..._[4]||(_[4]=[G(`重新加载`,-1)])]),_:1})]),_:1},8,[`description`])])):C.value.length?V(i)?(h(),K(`div`,$,[N(`aside`,Re,[(h(!0),K(j,null,v(C.value,t=>{var r;return h(),K(`button`,{key:t.device_id,type:`button`,class:g([`device-row app-card`,{active:((r=T.value)==null?void 0:r.device_id)===t.device_id}]),onClick:e=>p.value=t.device_id},[y(V(e),{name:`desktop-o`,class:`device-row-icon`}),N(`span`,Be,[N(`strong`,null,w(t.device_name||`未命名电脑`),1),N(`small`,{class:g(`tone-${V(n)(t).tone}`)},w(V(n)(t).label),3)])],10,ze)}),128)),l.value?(h(),K(`p`,Ve,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0)]),N(`section`,He,[T.value?(h(),D(Q,{key:T.value.device_id,device:T.value,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`])):W(``,!0)])])):(h(),K(`div`,Ue,[l.value?(h(),K(`p`,We,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0),(h(!0),K(j,null,v(C.value,e=>(h(),D(Q,{key:e.device_id,device:e,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`]))),128))])):(h(),K(`div`,Ie,[y(V(u),{image:`default`,description:`还没有登录过企微客服助手的电脑`},{default:b(()=>[N(`p`,Le,` 在电脑上打开企微客服助手,用`+w(V(R)===void 0?`同一个`:`该客服的`)+` ZYT 账号登录后,这里会显示电脑状态。 `,1)]),_:1})]))]),_:1},8,[`modelValue`]),(S=V(H))!=null&&S.control_all?(h(),D(f,{key:0,show:m.value,"onUpdate:show":_[2]||(_[2]=e=>m.value=e)},null,8,[`show`])):W(``,!0)])}}}),[[`__scopeId`,`data-v-8ead1f63`]]);export{Ge as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js new file mode 100644 index 000000000..531d04c7d --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-CLW1WjQp.js @@ -0,0 +1 @@ +import{c as e,m as t,n,t as r}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{a as i,s as a,t as o}from"./cell-group-B1mihubH.js";import{t as s}from"./field-BLYigdmn.js";import{B as c,Ct as l,Ft as u,Gt as d,I as f,Kt as p,O as m,R as h,St as g,Ut as _,V as v,Vt as y,W as b,Wt as x,X as S,Yt as C,Z as w,_ as T,_t as E,c as D,d as O,et as k,g as A,gt as j,ht as M,l as N,q as P,qt as F,r as I,u as L,ut as R,vt as z,xt as B,yt as V,z as H}from"./index-C_M75pH8.js";var[U,W]=T(`form`),G=l({name:U,props:{colon:Boolean,disabled:Boolean,readonly:Boolean,required:[Boolean,String],showError:Boolean,labelWidth:S,labelAlign:String,inputAlign:String,scrollToError:Boolean,scrollToErrorPosition:String,validateFirst:Boolean,submitOnEnter:w,showErrorMessage:w,errorMessageAlign:String,validateTrigger:{type:[String,Array],default:`onBlur`}},emits:[`submit`,`failed`],setup(e,{emit:n,slots:r}){let{children:i,linkChildren:o}=c(t),s=e=>e?i.filter(t=>e.includes(t.name)):i,l=e=>new Promise((t,n)=>{let r=[];s(e).reduce((e,t)=>e.then(()=>{if(!r.length)return t.validate().then(e=>{e&&r.push(e)})}),Promise.resolve()).then(()=>{r.length?n(r):t()})}),u=e=>new Promise((t,n)=>{let r=s(e);Promise.all(r.map(e=>e.validate())).then(e=>{e=e.filter(Boolean),e.length?n(e):t()})}),d=e=>{let t=i.find(t=>t.name===e);return t?new Promise((e,n)=>{t.validate().then(t=>{t?n(t):e()})}):Promise.reject()},f=t=>typeof t==`string`?d(t):e.validateFirst?l(t):u(t),p=e=>{typeof e==`string`&&(e=[e]),s(e).forEach(e=>{e.resetValidation()})},h=()=>i.reduce((e,t)=>(e[t.name]=t.getValidationStatus(),e),{}),_=(e,t)=>{i.some(n=>n.name===e&&(n.$el.scrollIntoView(t),!0))},v=()=>i.reduce((e,t)=>(t.name!==void 0&&(e[t.name]=t.formValue.value),e),{}),y=()=>{let t=v();f().then(()=>n(`submit`,t)).catch(r=>{n(`failed`,{values:t,errors:r});let{scrollToError:i,scrollToErrorPosition:a}=e;i&&r[0].name&&_(r[0].name,a?{block:a}:void 0)})},b=e=>{m(e),y()};return o({props:e}),a({submit:y,validate:f,getValues:v,scrollToField:_,resetValidation:p,getValidationStatus:h}),()=>{var e;return g(`form`,{class:W(),onSubmit:b},[(e=r.default)==null?void 0:e.call(r)])}}}),K=A(G),[q,J]=T(`notice-bar`),Y={text:String,mode:String,color:String,delay:P(1),speed:P(60),leftIcon:String,wrapable:Boolean,background:String,scrollable:{type:Boolean,default:null}},X=l({name:q,props:Y,emits:[`close`,`replay`],setup(t,{emit:r,slots:i}){let o=0,s=0,c,l=p(),u=p(),m=d({show:!0,offset:0,duration:0}),_=()=>{if(i[`left-icon`])return i[`left-icon`]();if(t.leftIcon)return g(n,{class:J(`left-icon`),name:t.leftIcon},null)},S=()=>{if(t.mode===`closeable`)return`cross`;if(t.mode===`link`)return`arrow`},C=e=>{t.mode===`closeable`&&(m.show=!1,r(`close`,e))},w=()=>{if(i[`right-icon`])return i[`right-icon`]();let e=S();if(e)return g(n,{name:e,class:J(`right-icon`),onClick:C},null)},T=()=>{m.offset=o,m.duration=0,H(()=>{f(()=>{m.offset=-s,m.duration=(s+o)/+t.speed,r(`replay`)})})},E=()=>{let e=t.scrollable===!1&&!t.wrapable,n={transform:m.offset?`translateX(${m.offset}px)`:``,transitionDuration:`${m.duration}s`};return g(`div`,{ref:l,role:`marquee`,class:J(`wrap`)},[g(`div`,{ref:u,style:n,class:[J(`content`),{"van-ellipsis":e}],onTransitionend:T},[i.default?i.default():t.text])])},D=()=>{let{delay:e,speed:n,scrollable:r}=t,i=k(e)?e*1e3:0;o=0,s=0,m.offset=0,m.duration=0,clearTimeout(c),c=setTimeout(()=>{if(!l.value||!u.value||r===!1)return;let e=b(l).width,t=b(u).width;(r||t>e)&&f(()=>{o=e,s=t,m.offset=-s,m.duration=s/+n})},i)};return e(D),h(D),v(`pageshow`,D),a({reset:D}),y(()=>[t.text,t.scrollable],D),()=>{let{color:e,wrapable:n,background:r}=t;return x(g(`div`,{role:`alert`,class:J({wrapable:n}),style:{color:e,background:r}},[_(),E(),w()]),[[R,m.show]])}}}),Z=A(X),Q=`/m/assets/brand-mark-CPUQS9Am.png`,$={class:`login`},ee={class:`login-card`},te={class:`login-head`},ne=[`src`],re={key:0,class:`login-error`,role:`alert`},ie={class:`login-actions`},ae=r(l({__name:`LoginView`,setup(e){let t=L(),n=O(),r=p(``),a=p(``),c=p(!1),l=p(``),d=M(()=>t.query.expired===`1`);function f(){let e=navigator.userAgent,t=/iPad/.test(e)?`iPad`:/iPhone/.test(e)?`iPhone`:/HarmonyOS/i.test(e)?`鸿蒙`:/Android/.test(e)?`Android`:/Windows/.test(e)?`Windows`:/Mac OS/.test(e)?`Mac`:`浏览器`,n=/wxwork/i.test(e)?`企业微信`:/MicroMessenger/i.test(e)?`微信`:``;return n?`${t} · ${n}`:t}function m(){let e=t.query.redirect;return typeof e==`string`&&e.startsWith(`/`)&&!e.startsWith(`//`)&&!e.startsWith(`/login`)?e:`/desktops`}async function h(){if(!c.value){l.value=``,c.value=!0;try{let e=await N.login(r.value.trim(),a.value,f());a.value=``,I(e.access_token,e.me),await n.replace(m())}catch(e){e instanceof D?(l.value=e.message,e.code===`invalid_credentials`&&(a.value=``)):l.value=`登录失败,请稍后重试`}finally{c.value=!1}}}return(e,t)=>(u(),V(`main`,$,[j(`section`,ee,[j(`header`,te,[j(`img`,{class:`login-mark`,src:F(Q),alt:`甄养堂`,width:`72`,height:`72`},null,8,ne),t[2]||(t[2]=j(`h1`,null,`企微客服助手`,-1)),t[3]||(t[3]=j(`p`,null,`手机控制台 · 使用 ZYT 账号登录`,-1))]),d.value?(u(),E(F(Z),{key:0,class:`login-notice`,"left-icon":`info-o`,scrollable:!1,wrapable:``,text:`登录已失效或已在管理端被下线,请重新登录。`})):z(``,!0),g(F(K),{class:`login-form`,onSubmit:h},{default:_(()=>[g(F(o),{inset:``},{default:_(()=>[g(F(s),{modelValue:r.value,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),name:`account`,label:`账号`,placeholder:`ZYT 登录账号`,autocomplete:`username`,autocapitalize:`off`,spellcheck:!1,enterkeyhint:`next`,clearable:``,rules:[{required:!0,message:`请输入账号`}]},null,8,[`modelValue`]),g(F(s),{modelValue:a.value,"onUpdate:modelValue":t[1]||(t[1]=e=>a.value=e),type:`password`,name:`password`,label:`密码`,placeholder:`ZYT 登录密码`,autocomplete:`current-password`,enterkeyhint:`go`,rules:[{required:!0,message:`请输入密码`}]},null,8,[`modelValue`])]),_:1}),l.value?(u(),V(`p`,re,C(l.value),1)):z(``,!0),j(`div`,ie,[g(F(i),{round:``,block:``,type:`primary`,"native-type":`submit`,loading:c.value,"loading-text":`正在登录…`},{default:_(()=>[...t[4]||(t[4]=[B(` 登录 `,-1)])]),_:1},8,[`loading`])])]),_:1}),t[5]||(t[5]=j(`p`,{class:`login-foot`},`密码只用于向 ZYT 验证身份,不会保存在手机上。`,-1))])]))}}),[[`__scopeId`,`data-v-004201f6`]]);export{ae as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css new file mode 100644 index 000000000..54f80a155 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css @@ -0,0 +1 @@ +.login[data-v-004201f6]{min-height:100vh;min-height:100dvh;padding:calc(env(safe-area-inset-top) + 24px) 16px calc(env(safe-area-inset-bottom) + 24px);background:radial-gradient(120% 60% at 50% 0%, var(--app-primary-soft), transparent 70%), var(--app-bg);justify-content:center;align-items:center;display:flex}.login-card[data-v-004201f6]{width:100%;max-width:420px}.login-head[data-v-004201f6]{text-align:center;margin-bottom:24px}.login-mark[data-v-004201f6]{box-shadow:var(--app-shadow);border-radius:20px}.login-head h1[data-v-004201f6]{letter-spacing:.5px;margin:16px 0 6px;font-size:24px;font-weight:700}.login-head p[data-v-004201f6]{color:var(--app-muted);margin:0;font-size:14px}.login-notice[data-v-004201f6]{border-radius:12px;margin:0 16px 12px}.login-form[data-v-004201f6] .van-cell-group--inset,.login-form[data-v-004201f6]{margin:0}.login-error[data-v-004201f6]{color:var(--app-danger);margin:12px 16px 0;font-size:14px;line-height:1.5}.login-actions[data-v-004201f6]{margin:20px 16px 0}.login-foot[data-v-004201f6]{text-align:center;color:var(--app-muted);margin:16px 16px 0;font-size:12px} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css new file mode 100644 index 000000000..c9fbeae2d --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-CpoVHD8m.css @@ -0,0 +1 @@ +.content[data-v-a968ee67]{max-width:720px;margin:0 auto;padding:12px 0 32px}.profile[data-v-a968ee67]{align-items:center;gap:14px;margin:0 16px;padding:16px;display:flex}.avatar[data-v-a968ee67]{color:#fff;background:linear-gradient(135deg,#2f6fff,#5a65ff);border-radius:50%;flex:none;place-items:center;width:54px;height:54px;font-size:22px;font-weight:700;display:grid}.profile-text[data-v-a968ee67]{flex:1;min-width:0}.profile-text h2[data-v-a968ee67]{margin:0 0 4px;font-size:18px}.profile-text p[data-v-a968ee67]{margin:0;font-size:13px}.root[data-v-a968ee67]{color:var(--app-warning);background:var(--app-warning-soft);border-radius:999px;flex:none;padding:4px 10px;font-size:12px;font-weight:600}.section-title[data-v-a968ee67]{color:var(--app-muted);margin:24px 32px 8px;font-size:13px;font-weight:600}.hint[data-v-a968ee67]{margin:8px 32px 0;font-size:12px;line-height:1.6}.logout[data-v-a968ee67]{margin:32px 16px 0} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js new file mode 100644 index 000000000..adf43b057 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-DTU4-bV-.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{a as n,i as r,o as i,s as a}from"./format-Dt9-L1ne.js";import{a as o,n as s,t as c}from"./cell-group-B1mihubH.js";import{Ct as l,Ft as u,Jt as d,Kt as f,Lt as p,St as m,Ut as h,Yt as g,_t as _,d as v,ft as y,gt as b,ht as x,i as S,n as C,qt as w,vt as T,xt as E,yt as D}from"./index-C_M75pH8.js";var O={class:`page`},k={key:0,class:`content`},A={class:`profile app-card`},j={class:`avatar`,"aria-hidden":`true`},M={class:`profile-text`},N={class:`app-muted`},P={key:0,class:`root`},F={class:`hint app-muted`},I={class:`logout`},L=t(l({__name:`MeView`,setup(t){let l=v(),L=f(!1),R=x(()=>C.me),z=x(()=>{let e=R.value;if(!e)return[];let{permissions:t,permission_codes:n}=e;return[{title:`查看本人电脑与会话`,code:`登录即可`,granted:!0},{title:`远程启停本人电脑`,code:n.control,granted:t.control},{title:`查看并控制所有客服电脑`,code:n.control_all,granted:t.control_all},{title:`会话加入忽略`,code:n.blacklist_add,granted:t.blacklist_add},{title:`会话移出忽略`,code:n.blacklist_remove,granted:t.blacklist_remove}]}),B=x(()=>{var e;let t=(e=R.value)==null?void 0:e.session.expires_at;return t?new Date(t*1e3).toISOString():``}),V=/iPhone|iPad|iPod/.test(navigator.userAgent);async function H(){try{await a({title:`退出登录?`,message:`退出后需要重新输入 ZYT 账号密码。`,confirmButtonText:`退出`})}catch{return}L.value=!0,await S(),await l.replace({name:`login`})}return(t,a)=>(u(),D(`div`,O,[m(w(i),{title:`我的`,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1}),R.value?(u(),D(`div`,k,[b(`section`,A,[b(`span`,j,g(w(r)(R.value.account.nickname||R.value.account.sn)),1),b(`div`,M,[b(`h2`,null,g(R.value.account.nickname||R.value.account.sn),1),b(`p`,N,`ZYT 账号 `+g(R.value.account.sn||`—`),1)]),R.value.permissions.root?(u(),D(`span`,P,`超级管理员`)):T(``,!0)]),a[1]||(a[1]=b(`h3`,{class:`section-title`},`手机端权限`,-1)),m(w(c),{inset:``},{default:h(()=>[(u(!0),D(y,null,p(z.value,t=>(u(),_(w(s),{key:t.title,title:t.title,label:t.code,center:``},{value:h(()=>[b(`span`,{class:d(t.granted?`tone-success`:`tone-muted`)},[m(w(e),{name:t.granted?`passed`:`close`},null,8,[`name`]),E(` `+g(t.granted?`已开通`:`未开通`),1)],2)]),_:2},1032,[`title`,`label`]))),128))]),_:1}),a[2]||(a[2]=b(`p`,{class:`hint app-muted`},` 权限在 ZYT「权限管理 → 角色管理」里勾选「企微客服助手」下的项目,调整后 10 分钟内自动生效,或退出重新登录。 `,-1)),a[3]||(a[3]=b(`h3`,{class:`section-title`},`登录信息`,-1)),m(w(c),{inset:``},{default:h(()=>[m(w(s),{title:`登录时间`,value:w(n)(R.value.session.created_at)},null,8,[`value`]),m(w(s),{title:`最长有效至`,value:B.value?w(n)(B.value):`—`,label:`ZYT 登录失效后会提前退出`},null,8,[`value`])]),_:1}),a[4]||(a[4]=b(`h3`,{class:`section-title`},`添加到主屏幕`,-1)),b(`p`,F,[w(V)?(u(),D(y,{key:0},[E(`在 Safari 中点底部「分享」按钮,选择「添加到主屏幕」,之后像 App 一样打开。`)],64)):(u(),D(y,{key:1},[E(`在浏览器菜单中选择「添加到主屏幕」或「安装应用」,之后像 App 一样打开。`)],64))]),b(`div`,I,[m(w(o),{block:``,round:``,type:`danger`,plain:``,loading:L.value,onClick:H},{default:h(()=>[...a[0]||(a[0]=[E(`退出登录`,-1)])]),_:1},8,[`loading`])])])):T(``,!0)]))}}),[[`__scopeId`,`data-v-a968ee67`]]);export{L as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css new file mode 100644 index 000000000..a8ec81749 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css @@ -0,0 +1 @@ +.shell[data-v-5972669c]{min-height:100vh;min-height:100dvh}.shell-tabbar[data-v-5972669c]{border-top:1px solid var(--app-border)} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js new file mode 100644 index 000000000..50a6278ca --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-DMPqnSgh.js @@ -0,0 +1 @@ +import{a as e,n as t,o as n,p as r,r as i,t as a}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n as o,t as s}from"./use-placeholder-BHVOzwXa.js";import{B as c,Ct as l,Et as u,Ft as d,Kt as f,Q as p,Rt as m,S as h,St as g,U as _,Ut as v,X as y,Z as b,_ as x,_t as S,g as C,ht as w,pt as T,q as E,qt as D,rt as O,wt as k,xt as A,yt as j,zt as M}from"./index-C_M75pH8.js";var[N,P]=x(`tabbar`),F={route:Boolean,fixed:b,border:b,zIndex:y,placeholder:Boolean,activeColor:String,beforeChange:Function,inactiveColor:String,modelValue:E(0),safeAreaInsetBottom:{type:Boolean,default:null}},I=Symbol(N),L=l({name:N,props:F,emits:[`change`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i=f(),{linkChildren:a}=c(I),l=s(i,P),u=()=>{var t;return(t=e.safeAreaInsetBottom)==null?e.fixed:t},d=()=>{var t;let{fixed:a,zIndex:o,border:s}=e;return g(`div`,{ref:i,role:`tablist`,style:h(o),class:[P({fixed:a}),{[r]:s,"van-safe-area-bottom":u()}]},[(t=n.default)==null?void 0:t.call(n)])};return a({props:e,setActive:(n,r)=>{o(e.beforeChange,{args:[n],done(){t(`update:modelValue`,n),t(`change`,n),r()}})}}),()=>e.fixed&&e.placeholder?l(d):d()}}),R=C(L),[z,B]=x(`tabbar-item`),V=p({},e,{dot:Boolean,icon:String,name:y,badge:y,badgeProps:Object,iconPrefix:String}),H=l({name:z,props:V,emits:[`click`],setup(e,{emit:r,slots:a}){let o=n(),s=k().proxy,{parent:c,index:l}=_(I);if(!c)return;let d=w(()=>{var t;let{route:n,modelValue:r}=c.props;if(n&&`$route`in s){let{$route:t}=s,{to:n}=e,r=O(n)?n:{path:n};return t.matched.some(e=>{let t=`path`in r&&r.path===e.path,n=`name`in r&&r.name===e.name;return t||n})}return((t=e.name)==null?l.value:t)===r}),f=t=>{var n;d.value||c.setActive((n=e.name)==null?l.value:n,o),r(`click`,t)},p=()=>{if(a.icon)return a.icon({active:d.value});if(e.icon)return g(t,{name:e.icon,classPrefix:e.iconPrefix},null)};return()=>{var t;let{dot:n,badge:r}=e,{activeColor:o,inactiveColor:s}=c.props,l=d.value?o:s;return g(`div`,{role:`tab`,class:B({active:d.value}),style:{color:l},tabindex:0,"aria-selected":d.value,onClick:f},[g(i,u({dot:n,class:B(`icon`),content:r},e.badgeProps),{default:p}),g(`div`,{class:B(`text`)},[(t=a.default)==null?void 0:t.call(a,{active:d.value})])])}}}),U=C(H),W={class:`shell`},G=a(l({__name:`ShellView`,setup(e){return(e,t)=>{let n=m(`RouterView`);return d(),j(`div`,W,[g(n,null,{default:v(({Component:e})=>[(d(),S(T,{include:[`DevicesView`,`ConversationsView`]},[(d(),S(M(e)))],1024))]),_:1}),g(D(R),{route:``,placeholder:``,"safe-area-inset-bottom":``,class:`shell-tabbar`},{default:v(()=>[g(D(U),{replace:``,to:{name:`desktops`},icon:`desktop-o`},{default:v(()=>[...t[0]||(t[0]=[A(`电脑`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`conversations`},icon:`chat-o`},{default:v(()=>[...t[1]||(t[1]=[A(`会话`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`me`},icon:`user-o`},{default:v(()=>[...t[2]||(t[2]=[A(`我的`,-1)])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-5972669c`]]);export{G as default}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js new file mode 100644 index 000000000..21f72e323 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-D4BeZ2vp.js @@ -0,0 +1 @@ +import{Ct as e,Et as t,St as n,Tt as r,Vt as i,X as a,Y as o,Z as s,_ as c,et as l,g as u,ht as d,m as f,nt as p,v as m,wt as h}from"./index-C_M75pH8.js";var g=`van-hairline`,_=`${g}--top`,v=`${g}--left`;`${g}`;var y=`${g}--bottom`,b=`${g}--surround`,x=`${g}--top-bottom`;`${g}`;var S=`van-haptics-feedback`,C=Symbol(`van-form`),w=Symbol();function T(e){let t=r(w,null);t&&i(t,t=>{t&&e()})}var E={to:[String,Object],url:String,replace:Boolean};function D({to:e,url:t,replace:n,$router:r}){e&&r?r[n?`replace`:`push`](e):t&&(n?location.replace(t):location.href=t)}function O(){let e=h().proxy;return()=>D(e)}var[k,A]=c(`badge`),j={dot:Boolean,max:a,tag:o(`div`),color:String,offset:Array,content:a,showZero:s,position:o(`top-right`)},M=e({name:k,props:j,setup(e,{slots:t}){let r=()=>{if(t.content)return!0;let{content:n,showZero:r}=e;return l(n)&&n!==``&&(r||n!==0&&n!==`0`)},i=()=>{let{dot:n,max:i,content:a}=e;if(!n&&r())return t.content?t.content():l(i)&&p(a)&&+a>+i?`${i}+`:a},a=e=>e.startsWith(`-`)?e.replace(`-`,``):`-${e}`,o=d(()=>{let n={background:e.color};if(e.offset){let[r,i]=e.offset,{position:o}=e,[s,c]=o.split(`-`);t.default?(n[s]=typeof i==`number`?m(s===`top`?i:-i):s===`top`?m(i):a(i),n[c]=typeof r==`number`?m(c===`left`?r:-r):c===`left`?m(r):a(r)):(n.marginTop=m(i),n.marginLeft=m(r))}return n}),s=()=>{if(r()||e.dot)return n(`div`,{class:A([e.position,{dot:e.dot,fixed:!!t.default}]),style:o.value},[i()])};return()=>{if(t.default){let{tag:r}=e;return n(r,{class:A(`wrapper`)},{default:()=>[t.default(),s()]})}return s()}}}),N=u(M),[P,F]=c(`icon`),I=e=>e==null?void 0:e.includes(`/`),L={dot:Boolean,tag:o(`i`),name:String,size:a,badge:a,color:String,badgeProps:Object,classPrefix:String},R=e({name:P,props:L,setup(e,{slots:i}){let a=r(f,null),o=d(()=>e.classPrefix||(a==null?void 0:a.iconPrefix)||F());return()=>{let{tag:r,dot:a,name:s,size:c,badge:l,color:u}=e,d=I(s);return n(N,t({dot:a,tag:r,class:[o.value,d?``:`${o.value}-${s}`],style:{color:u,fontSize:m(c)},content:l},e.badgeProps),{default:()=>{var e;return[(e=i.default)==null?void 0:e.call(i),d&&n(`img`,{class:F(`image`),src:s},null)]}})}}}),z=u(R),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{E as a,T as c,b as d,_ as f,S as h,D as i,y as l,C as m,z as n,O as o,x as p,N as r,w as s,B as t,v as u}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png differ diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js new file mode 100644 index 000000000..64482f6cc --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1mihubH.js @@ -0,0 +1 @@ +import{a as e,d as t,n,o as r,p as i}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{Ct as a,Et as o,O as s,Q as c,St as l,X as u,Y as d,Z as f,_ as p,et as m,ft as h,g,ht as _,v,wt as y,x as b}from"./index-C_M75pH8.js";function x(e){let t=y();t&&c(t.proxy,e)}var[S,C]=p(`loading`),w=Array(12).fill(null).map((e,t)=>l(`i`,{class:C(`line`,String(t+1))},null)),T=l(`svg`,{class:C(`circular`),viewBox:`25 25 50 50`},[l(`circle`,{cx:`50`,cy:`50`,r:`20`,fill:`none`},null)]),E={size:u,type:d(`circular`),color:String,vertical:Boolean,textSize:u,textColor:String},D=a({name:S,props:E,setup(e,{slots:t}){let n=_(()=>c({color:e.color},b(e.size))),r=()=>{let r=e.type===`spinner`?w:T;return l(`span`,{class:C(`spinner`,e.type),style:n.value},[t.icon?t.icon():r])},i=()=>{var n;if(t.default)return l(`span`,{class:C(`text`),style:{fontSize:v(e.textSize),color:(n=e.textColor)==null?e.color:n}},[t.default()])};return()=>{let{type:t,vertical:n}=e;return l(`div`,{class:C([t,{vertical:n}]),"aria-live":`polite`,"aria-busy":!0},[r(),i()])}}}),O=g(D),[k,A]=p(`button`),j=c({},e,{tag:d(`button`),text:String,icon:String,type:d(`default`),size:d(`normal`),color:String,block:Boolean,plain:Boolean,round:Boolean,square:Boolean,loading:Boolean,hairline:Boolean,disabled:Boolean,iconPrefix:String,nativeType:d(`button`),loadingSize:u,loadingText:String,loadingType:String,iconPosition:d(`left`)}),M=a({name:k,props:j,emits:[`click`],setup(e,{emit:i,slots:a}){let o=r(),c=()=>a.loading?a.loading():l(O,{size:e.loadingSize,type:e.loadingType,class:A(`loading`)},null),u=()=>{if(e.loading)return c();if(a.icon)return l(`div`,{class:A(`icon`)},[a.icon()]);if(e.icon)return l(n,{name:e.icon,class:A(`icon`),classPrefix:e.iconPrefix},null)},d=()=>{let t;if(t=e.loading?e.loadingText:a.default?a.default():e.text,t)return l(`span`,{class:A(`text`)},[t])},f=()=>{let{color:t,plain:n}=e;if(t){let e={color:n?t:`white`};return n||(e.background=t),t.includes(`gradient`)?e.border=0:e.borderColor=t,e}},p=t=>{e.loading?s(t):e.disabled||(i(`click`,t),o())};return()=>{let{tag:n,type:r,size:i,block:a,round:o,plain:s,square:c,loading:m,disabled:h,hairline:g,nativeType:_,iconPosition:v}=e,y=[A([r,i,{plain:s,block:a,round:o,square:c,loading:m,disabled:h,hairline:g}]),{[t]:g}];return l(n,{type:_,class:y,style:f(),disabled:h,onClick:p},{default:()=>[l(`div`,{class:A(`content`)},[v===`left`&&u(),d(),v===`right`&&u()])]})}}}),N=g(M),P=()=>{var e;let{scopeId:t}=((e=y())==null?void 0:e.vnode)||{};return t?{[t]:``}:null},[F,I]=p(`cell`),L={tag:d(`div`),icon:String,size:String,title:u,value:u,label:u,center:Boolean,isLink:Boolean,border:f,iconPrefix:String,valueClass:null,labelClass:null,titleClass:null,titleStyle:null,arrowDirection:String,required:{type:[Boolean,String],default:null},clickable:{type:Boolean,default:null}},R=c({},L,e),z=a({name:F,props:R,setup(e,{slots:t}){let i=r(),a=()=>{if(t.label||m(e.label))return l(`div`,{class:[I(`label`),e.labelClass]},[t.label?t.label():e.label])},o=()=>{var n;if(t.title||m(e.title)){let r=(n=t.title)==null?void 0:n.call(t);return Array.isArray(r)&&r.length===0?void 0:l(`div`,{class:[I(`title`),e.titleClass],style:e.titleStyle},[r||l(`span`,null,[e.title]),a()])}},s=()=>{let n=t.value||t.default;if(n||m(e.value))return l(`div`,{class:[I(`value`),e.valueClass]},[n?n():l(`span`,null,[e.value])])},c=()=>{if(t.icon)return t.icon();if(e.icon)return l(n,{name:e.icon,class:I(`left-icon`),classPrefix:e.iconPrefix},null)},u=()=>{if(t[`right-icon`])return t[`right-icon`]();if(e.isLink){let t=e.arrowDirection&&e.arrowDirection!==`right`?`arrow-${e.arrowDirection}`:`arrow`;return l(n,{name:t,class:I(`right-icon`)},null)}};return()=>{var n,r,a;let{tag:d,size:f,center:p,border:m,isLink:h,required:g}=e,_=!!((a=(r=(n=e.clickable)==null?e.to:n)==null?e.url:r)==null?h:a),v={center:p,required:!!g,clickable:_,borderless:!m};return f&&(v[f]=!!f),l(d,{class:I(v),role:_?`button`:void 0,tabindex:_?0:void 0,onClick:_?i:void 0},{default:()=>{var e;return[c(),o(),s(),u(),(e=t.extra)==null?void 0:e.call(t)]}})}}}),B=g(z),[V,H]=p(`cell-group`),U=a({name:V,inheritAttrs:!1,props:{title:String,inset:Boolean,border:f},setup(e,{slots:t,attrs:n}){let r=()=>{var r;return l(`div`,o({class:[H({inset:e.inset}),{[i]:e.border&&!e.inset}]},n,P()),[(r=t.default)==null?void 0:r.call(t)])},a=()=>l(`div`,{class:H(`title`,{inset:e.inset})},[t.title?t.title():e.title]);return()=>e.title||t.title?l(h,null,[a(),r()]):r()}}),W=g(U);export{N as a,P as i,B as n,O as o,L as r,x as s,W as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js new file mode 100644 index 000000000..5c6c4b23a --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-BLYigdmn.js @@ -0,0 +1 @@ +import{m as e,n as t}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n,r,s as i}from"./cell-group-B1mihubH.js";import{A as a,Ct as o,Dt as s,Et as c,Gt as ee,It as l,Kt as u,Mt as d,O as f,P as te,Q as p,St as m,T as h,U as g,V as _,Vt as v,X as y,Y as b,_ as x,b as S,et as C,g as w,ht as T,it as E,k as ne,q as D,rt as O,st as k,tt as A,v as j,wt as M,xt as re,y as ie}from"./index-C_M75pH8.js";var N=0;function P(){let e=M(),{name:t=`unknown`}=(e==null?void 0:e.type)||{};return`${t}-${++N}`}function F(e){return Array.isArray(e)?!e.length:e!==0&&!e}function ae(e,t){if(F(e)){if(t.required)return!1;if(t.validateEmpty===!1)return!0}return!(t.pattern&&!t.pattern.test(String(e)))}function oe(e,t){return new Promise(n=>{let r=t.validator(e,t);if(E(r)){r.then(n);return}n(r)})}function I(e,t){let{message:n}=t;return A(n)?n(e,t):n||``}function se({target:e}){e.composing=!0}function L({target:e}){e.composing&&(e.composing=!1,e.dispatchEvent(new Event(`input`)))}function ce(e,t){let n=h();e.style.height=`auto`;let r=e.scrollHeight;if(O(t)){let{maxHeight:e,minHeight:n}=t;e!==void 0&&(r=Math.min(r,e)),n!==void 0&&(r=Math.max(r,n))}r&&(e.style.height=`${r}px`,a(n))}function le(e,t){return e===`number`&&(e=`text`,t!=null||(t=`decimal`)),e===`digit`&&(e=`tel`,t!=null||(t=`numeric`)),{type:e,inputmode:t}}function R(e){return[...e].length}function z(e,t){return[...e].slice(0,t).join(``)}var[B,V]=x(`field`),H={id:String,name:String,leftIcon:String,rightIcon:String,autofocus:Boolean,clearable:Boolean,maxlength:y,max:Number,min:Number,formatter:Function,clearIcon:b(`clear`),modelValue:D(``),inputAlign:String,placeholder:String,autocomplete:String,autocapitalize:String,autocorrect:String,errorMessage:String,enterkeyhint:String,clearTrigger:b(`focus`),formatTrigger:b(`onChange`),spellcheck:{type:Boolean,default:null},error:{type:Boolean,default:null},disabled:{type:Boolean,default:null},readonly:{type:Boolean,default:null},inputmode:String},U=p({},r,H,{rows:y,type:b(`text`),rules:Array,autosize:[Boolean,Object],labelWidth:y,labelClass:null,labelAlign:String,showWordLimit:Boolean,errorMessageAlign:String,colon:{type:Boolean,default:null}}),W=o({name:B,props:U,emits:[`blur`,`focus`,`clear`,`keypress`,`clickInput`,`endValidate`,`startValidate`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(r,{emit:a,slots:o}){let p=P(),h=ee({status:`unvalidated`,focused:!1,validateMessage:``}),y=u(),b=u(),x=u(),{parent:w}=g(e),E=()=>{var e;return String((e=r.modelValue)==null?``:e)},D=e=>{if(C(r[e]))return r[e];if(w&&C(w.props[e]))return w.props[e]},O=T(()=>{let e=D(`readonly`);if(r.clearable&&!e){let e=E()!==``,t=r.clearTrigger===`always`||r.clearTrigger===`focus`&&h.focused;return e&&t}return!1}),A=T(()=>x.value&&o.input?x.value():r.modelValue),M=T(()=>{var e;let t=D(`required`);return t===`auto`?(e=r.rules)==null?void 0:e.some(e=>e.required):t}),N=e=>e.reduce((e,t)=>e.then(()=>{if(h.status===`failed`)return;let{value:e}=A;if(t.formatter&&(e=t.formatter(e,t)),!ae(e,t)){h.status=`failed`,h.validateMessage=I(e,t);return}if(t.validator)return F(e)&&t.validateEmpty===!1?void 0:oe(e,t).then(n=>{n&&typeof n==`string`?(h.status=`failed`,h.validateMessage=n):n===!1&&(h.status=`failed`,h.validateMessage=I(e,t))})}),Promise.resolve()),B=()=>{h.status=`unvalidated`,h.validateMessage=``},H=()=>a(`endValidate`,{status:h.status,message:h.validateMessage}),U=(e=r.rules)=>new Promise(t=>{B(),e?(a(`startValidate`),N(e).then(()=>{h.status===`failed`?(t({name:r.name,message:h.validateMessage}),H()):(h.status=`passed`,t(),H())})):t()}),W=e=>{if(w&&r.rules){let{validateTrigger:t}=w.props,n=k(t).includes(e),i=r.rules.filter(t=>t.trigger?k(t.trigger).includes(e):n);i.length&&U(i)}},G=e=>{var t;let{maxlength:n}=r;if(C(n)&&R(e)>+n){let r=E();if(r&&R(r)===+n)return r;let i=(t=y.value)==null?void 0:t.selectionEnd;if(h.focused&&i){let t=[...e],r=t.length-+n;return i=R(e.slice(0,i)),t.splice(i-r,r),t.join(``)}return z(e,+n)}return e},K=(e,t=`onChange`)=>{var n,i;let o=e;e=G(e);let s=o.length-e.length;if(r.type===`number`||r.type===`digit`){let a=r.type===`number`;if(e=S(e,a,a),t===`onBlur`&&e!==``&&(r.min!==void 0||r.max!==void 0)){let t=ie(+e,(n=r.min)==null?-1/0:n,(i=r.max)==null?1/0:i);+e!==t&&(e=t.toString())}}let c=0;if(r.formatter&&t===r.formatTrigger){let{formatter:t,maxlength:n}=r;if(e=t(e),C(n)&&R(e)>+n&&(e=z(e,+n)),y.value&&h.focused){let{selectionEnd:e}=y.value,n=z(o,e);c=t(n).length-n.length}}if(y.value&&y.value.value!==e){if(h.focused){let{selectionStart:t,selectionEnd:n}=y.value;if(y.value.value=e,C(t)&&C(n)){let r=e.length;s?(t-=s,n-=s):c&&(t+=c,n+=c),y.value.setSelectionRange(Math.min(t,r),Math.min(n,r))}}else y.value.value=e}e!==r.modelValue&&a(`update:modelValue`,e)},ue=e=>{e.target.composing||K(e.target.value)},q=()=>{var e;return(e=y.value)==null?void 0:e.blur()},J=()=>{var e;return(e=y.value)==null?void 0:e.focus()},Y=()=>{let e=y.value;r.type===`textarea`&&r.autosize&&e&&ce(e,r.autosize)},de=e=>{h.focused=!0,a(`focus`,e),s(Y),D(`readonly`)&&q()},fe=e=>{h.focused=!1,K(E(),`onBlur`),a(`blur`,e),!D(`readonly`)&&(W(`onBlur`),s(Y),ne())},X=e=>a(`clickInput`,e),pe=e=>a(`clickLeftIcon`,e),me=e=>a(`clickRightIcon`,e),he=e=>{f(e),a(`update:modelValue`,``),a(`clear`,e)},Z=T(()=>{if(typeof r.error==`boolean`)return r.error;if(w&&w.props.showError&&h.status===`failed`)return!0}),ge=T(()=>{let e=D(`labelWidth`),t=D(`labelAlign`);if(e&&t!==`top`)return{width:j(e)}}),Q=e=>{e.keyCode===13&&(!(w&&w.props.submitOnEnter)&&r.type!==`textarea`&&f(e),r.type===`search`&&q()),a(`keypress`,e)},$=()=>r.id||`${p}-input`,_e=()=>h.status,ve=()=>{let e=V(`control`,[D(`inputAlign`),{error:Z.value,custom:!!o.input,"min-height":r.type===`textarea`&&!r.autosize}]);if(o.input)return m(`div`,{class:e,onClick:X},[o.input()]);let t={id:$(),ref:y,name:r.name,rows:r.rows===void 0?void 0:+r.rows,class:e,disabled:D(`disabled`),readonly:D(`readonly`),autofocus:r.autofocus,placeholder:r.placeholder,autocomplete:r.autocomplete,autocapitalize:r.autocapitalize,autocorrect:r.autocorrect,enterkeyhint:r.enterkeyhint,spellcheck:r.spellcheck,"aria-labelledby":r.label?`${p}-label`:void 0,"data-allow-mismatch":`attribute`,onBlur:fe,onFocus:de,onInput:ue,onClick:X,onChange:L,onKeypress:Q,onCompositionend:L,onCompositionstart:se};return r.type===`textarea`?m(`textarea`,c(t,{inputmode:r.inputmode}),null):m(`input`,c(le(r.type,r.inputmode),t),null)},ye=()=>{let e=o[`left-icon`];if(r.leftIcon||e)return m(`div`,{class:V(`left-icon`),onClick:pe},[e?e():m(t,{name:r.leftIcon,classPrefix:r.iconPrefix},null)])},be=()=>{let e=o[`right-icon`];if(r.rightIcon||e)return m(`div`,{class:V(`right-icon`),onClick:me},[e?e():m(t,{name:r.rightIcon,classPrefix:r.iconPrefix},null)])},xe=()=>{if(r.showWordLimit&&r.maxlength){let e=R(E());return m(`div`,{class:V(`word-limit`)},[m(`span`,{class:V(`word-num`)},[e]),re(`/`),r.maxlength])}},Se=()=>{if(w&&w.props.showErrorMessage===!1)return;let e=r.errorMessage||h.validateMessage;if(e){let t=o[`error-message`],n=D(`errorMessageAlign`);return m(`div`,{class:V(`error-message`,n)},[t?t({message:e}):e])}},Ce=()=>{let e=D(`labelWidth`),t=D(`labelAlign`),n=D(`colon`)?`:`:``;if(o.label)return[o.label(),n];if(r.label)return m(`label`,{id:`${p}-label`,for:o.input?void 0:$(),"data-allow-mismatch":`attribute`,onClick:e=>{f(e),J()},style:t===`top`&&e?{width:j(e)}:void 0},[r.label+n])},we=()=>[m(`div`,{class:V(`body`)},[ve(),O.value&&m(t,{ref:b,name:r.clearIcon,class:V(`clear`)},null),be(),o.button&&m(`div`,{class:V(`button`)},[o.button()])]),xe(),Se()];return i({blur:q,focus:J,validate:U,formValue:A,resetValidation:B,getValidationStatus:_e,adjustTextareaSize:Y}),l(te,{customValue:x,resetValidation:B,validateWithTrigger:W}),v(()=>r.modelValue,()=>{K(E()),B(),W(`onChange`),s(Y)}),d(()=>{K(E(),r.formatTrigger),s(Y)}),_(`touchstart`,he,{target:T(()=>{var e;return(e=b.value)==null?void 0:e.$el})}),()=>{let e=D(`disabled`),t=D(`labelAlign`),i=ye(),a=()=>{let e=Ce();return t===`top`?[i,e].filter(Boolean):e||[]};return m(n,{size:r.size,class:V({error:Z.value,disabled:e,[`label-${t}`]:t}),center:r.center,border:r.border,isLink:!e&&r.isLink,clickable:!e&&r.clickable,onClick:e?e=>e.stopImmediatePropagation():void 0,titleStyle:ge.value,valueClass:V(`value`),titleClass:[V(`label`,[t,{required:M.value}]),r.labelClass],arrowDirection:r.arrowDirection},{icon:i&&t!==`top`?()=>i:null,title:a,value:we,extra:o.extra})}}}),G=w(W);export{H as n,P as r,G as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js new file mode 100644 index 000000000..563103913 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-Dt9-L1ne.js @@ -0,0 +1 @@ +import{a as e,f as t,h as n,l as r,n as i,o as a,s as o,u as s}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{n as c,t as l}from"./use-placeholder-BHVOzwXa.js";import{a as u,i as d,s as f}from"./cell-group-B1mihubH.js";import{$ as p,B as m,Ct as h,Dt as ee,Et as g,Gt as _,It as te,Kt as v,L as y,Mt as b,O as x,Ot as ne,Q as S,R as C,S as w,St as T,U as E,V as re,Vt as D,Wt as O,X as k,Y as A,Z as j,_ as M,at as N,ct as ie,dt as P,et as ae,ft as oe,g as F,h as se,ht as I,jt as ce,kt as le,lt as ue,mt as de,ot as fe,tt as pe,ut as L,v as me}from"./index-C_M75pH8.js";var[R,z]=M(`action-bar`),B=Symbol(R),he=h({name:R,props:{placeholder:Boolean,safeAreaInsetBottom:j},setup(e,{slots:t}){let n=v(),r=l(n,z),{linkChildren:i}=m(B);i();let a=()=>{var r;return T(`div`,{ref:n,class:[z(),{"van-safe-area-bottom":e.safeAreaInsetBottom}]},[(r=t.default)==null?void 0:r.call(t)])};return()=>e.placeholder?r(a):a()}}),ge=F(he),[_e,ve]=M(`action-bar-button`),ye=S({},e,{type:String,text:String,icon:String,color:String,loading:Boolean,disabled:Boolean}),be=h({name:_e,props:ye,setup(e,{slots:t}){let n=a(),{parent:r,index:i}=E(B),o=I(()=>{if(r){let e=r.children[i.value-1];return!(e&&`isButton`in e)}}),s=I(()=>{if(r){let e=r.children[i.value+1];return!(e&&`isButton`in e)}});return f({isButton:!0}),()=>{let{type:r,icon:i,text:a,color:c,loading:l,disabled:d}=e;return T(u,{class:ve([r,{last:s.value,first:o.value}]),size:`large`,type:r,icon:i,color:c,loading:l,disabled:d,onClick:n},{default:()=>[t.default?t.default():a]})}}}),V=F(be),H={show:Boolean,zIndex:k,overlay:j,duration:k,teleport:[String,Object],lockScroll:j,lazyRender:j,beforeClose:Function,overlayProps:Object,overlayStyle:Object,overlayClass:null,transitionAppear:Boolean,closeOnClickOverlay:j},xe=Object.keys(H);function Se(e,t){return e>t?`horizontal`:t>e?`vertical`:``}function U(){let e=v(0),t=v(0),n=v(0),r=v(0),i=v(0),a=v(0),o=v(``),s=v(!0),c=()=>o.value===`vertical`,l=()=>o.value===`horizontal`,u=()=>{n.value=0,r.value=0,i.value=0,a.value=0,o.value=``,s.value=!0};return{move:(c=>{let l=c.touches[0];n.value=(l.clientX<0?0:l.clientX)-e.value,r.value=l.clientY-t.value,i.value=Math.abs(n.value),a.value=Math.abs(r.value),(!o.value||i.value<10&&a.value<10)&&(o.value=Se(i.value,a.value)),s.value&&(i.value>5||a.value>5)&&(s.value=!1)}),start:(n=>{u(),e.value=n.touches[0].clientX,t.value=n.touches[0].clientY}),reset:u,startX:e,startY:t,deltaX:n,deltaY:r,offsetX:i,offsetY:a,direction:o,isVertical:c,isHorizontal:l,isTap:s}}var W=0,G=`van-overflow-hidden`;function Ce(e,t){let n=U(),r=t=>{n.move(t);let r=n.deltaY.value>0?`10`:`01`,i=y(t.target,e.value);for(;i.scrollHeight<=i.offsetHeight&&i!==e.value&&i.parentElement;)i=y(i.parentElement,e.value);let{scrollHeight:a,offsetHeight:o,scrollTop:s}=i,c=`11`;s===0?c=o>=a?`00`:`01`:s+o>=a&&(c=`10`),c!==`11`&&n.isVertical()&&!(parseInt(c,2)&parseInt(r,2))&&x(t,!0)},i=()=>{document.addEventListener(`touchstart`,n.start),document.addEventListener(`touchmove`,r,{passive:!1}),W||document.body.classList.add(G),W++},a=()=>{W&&(document.removeEventListener(`touchstart`,n.start),document.removeEventListener(`touchmove`,r),W--,W||document.body.classList.remove(G))},o=()=>t()&&i(),s=()=>t()&&a();C(o),ce(s),le(s),D(t,e=>{e?i():a()})}function K(e){let t=v(!1);return D(e,e=>{e&&(t.value=e)},{immediate:!0}),e=>()=>t.value?e():null}var[we,Te]=M(`overlay`),Ee=h({name:we,inheritAttrs:!1,props:{show:Boolean,zIndex:k,duration:k,className:null,lockScroll:j,lazyRender:j,customStyle:Object,teleport:[String,Object]},setup(e,{attrs:t,slots:n}){let r=v(),i=K(()=>e.show||!e.lazyRender),a=t=>{e.lockScroll&&x(t,!0)},o=i(()=>{var i;let a=S(w(e.zIndex),e.customStyle);return ae(e.duration)&&(a.animationDuration=`${e.duration}s`),O(T(`div`,g({ref:r,style:a,class:[Te(),e.className]},t),[(i=n.default)==null?void 0:i.call(n)]),[[L,e.show]])});return re(`touchmove`,a,{target:r}),()=>{let t=T(ie,{name:`van-fade`,appear:!0},{default:o});return e.teleport?T(de,{to:e.teleport},{default:()=>[t]}):t}}}),De=F(Ee),Oe=S({},H,{round:Boolean,position:A(`center`),closeIcon:A(`cross`),closeable:Boolean,transition:String,iconPrefix:String,closeOnPopstate:Boolean,closeIconPosition:A(`top-right`),destroyOnClose:Boolean,safeAreaInsetTop:Boolean,safeAreaInsetBottom:Boolean}),[ke,q]=M(`popup`),Ae=h({name:ke,inheritAttrs:!1,props:Oe,emits:[`open`,`close`,`opened`,`closed`,`keydown`,`update:show`,`clickOverlay`,`clickCloseIcon`],setup(e,{emit:t,attrs:r,slots:a}){let s,l,u=v(),p=v(),m=K(()=>e.show||!e.lazyRender),h=I(()=>{let t={zIndex:u.value};if(ae(e.duration)){let n=e.position===`center`?`animationDuration`:`transitionDuration`;t[n]=`${e.duration}s`}return t}),_=()=>{s||(s=!0,u.value=e.zIndex===void 0?se():+e.zIndex,t(`open`))},y=()=>{s&&c(e.beforeClose,{done(){s=!1,t(`close`),t(`update:show`,!1)}})},x=n=>{t(`clickOverlay`,n),e.closeOnClickOverlay&&y()},C=()=>{if(e.overlay){let t=S({show:e.show,class:e.overlayClass,zIndex:u.value,duration:e.duration,customStyle:e.overlayStyle,role:e.closeOnClickOverlay?`button`:void 0,tabindex:e.closeOnClickOverlay?0:void 0},e.overlayProps);return T(De,g(t,d(),{onClick:x}),{default:a[`overlay-content`]})}},w=e=>{t(`clickCloseIcon`,e),y()},E=()=>{if(e.closeable)return T(i,{role:`button`,tabindex:0,name:e.closeIcon,class:[q(`close-icon`,e.closeIconPosition),n],classPrefix:e.iconPrefix,onClick:w},null)},k,A=()=>{k&&clearTimeout(k),k=setTimeout(()=>{t(`opened`)})},j=()=>t(`closed`),M=e=>t(`keydown`,e),N=m(()=>{var t;let{destroyOnClose:n,round:i,position:o,safeAreaInsetTop:s,safeAreaInsetBottom:c,show:l}=e;if(l||!n)return O(T(`div`,g({ref:p,style:h.value,role:`dialog`,tabindex:0,class:[q({round:i,[o]:o}),{"van-safe-area-top":s,"van-safe-area-bottom":c}],onKeydown:M},r,d()),[(t=a.default)==null?void 0:t.call(a),E()]),[[L,l]])}),P=()=>{let{position:t,transition:n,transitionAppear:r}=e,i=t===`center`?`van-fade`:`van-popup-slide-${t}`;return T(ie,{name:n||i,appear:r,onAfterEnter:A,onAfterLeave:j},{default:N})};return D(()=>e.show,e=>{e&&!s&&(_(),r.tabindex===0&&ee(()=>{var e;(e=p.value)==null||e.focus()})),!e&&s&&(s=!1,t(`close`))}),f({popupRef:p}),Ce(p,()=>e.show&&e.lockScroll),re(`popstate`,()=>{e.closeOnPopstate&&(y(),l=!1)}),b(()=>{e.show&&_()}),ne(()=>{l&&(t(`update:show`,!0),l=!1)}),ce(()=>{e.show&&e.teleport&&(y(),l=!0)}),te(o,()=>e.show),()=>e.teleport?T(de,{to:e.teleport},{default:()=>[C(),P()]}):T(oe,null,[C(),P()])}}),J=F(Ae);function je(){let e=_({show:!1}),t=t=>{e.show=t},n=n=>{S(e,n,{transitionAppear:!0}),t(!0)},r=()=>t(!1);return f({open:n,close:r,toggle:t}),{open:n,close:r,state:e,toggle:t}}function Me(e){let t=ue(e),n=document.createElement(`div`);return document.body.appendChild(n),{instance:t.mount(n),unmount(){t.unmount(),document.body.removeChild(n)}}}var[Ne,Y,X]=M(`dialog`),Pe=S({},H,{title:String,theme:String,width:k,message:[String,Function],callback:Function,allowHtml:Boolean,className:null,transition:A(`van-dialog-bounce`),messageAlign:String,closeOnPopstate:j,showCancelButton:Boolean,cancelButtonText:String,cancelButtonColor:String,cancelButtonDisabled:Boolean,confirmButtonText:String,confirmButtonColor:String,confirmButtonDisabled:Boolean,showConfirmButton:j,closeOnClickOverlay:Boolean,keyboardEnabled:j,destroyOnClose:Boolean}),Fe=[...xe,`transition`,`closeOnPopstate`,`destroyOnClose`],Ie=h({name:Ne,props:Pe,emits:[`confirm`,`cancel`,`keydown`,`update:show`],setup(e,{emit:n,slots:r}){let i=v(),a=_({confirm:!1,cancel:!1}),o=e=>n(`update:show`,e),l=t=>{var n;o(!1),(n=e.callback)==null||n.call(e,t)},d=t=>()=>{e.show&&(n(t),e.beforeClose?(a[t]=!0,c(e.beforeClose,{args:[t],done(){l(t),a[t]=!1},canceled(){a[t]=!1}})):l(t))},f=d(`cancel`),p=d(`confirm`),m=P(t=>{var r,a;e.keyboardEnabled&&t.target===((a=(r=i.value)==null?void 0:r.popupRef)==null?void 0:a.value)&&({Enter:e.showConfirmButton?p:N,Escape:e.showCancelButton?f:N}[t.key](),n(`keydown`,t))},[`enter`,`esc`]),h=()=>{let t=r.title?r.title():e.title;if(t)return T(`div`,{class:Y(`header`,{isolated:!e.message&&!r.default})},[t])},ee=t=>{let{message:n,allowHtml:r,messageAlign:i}=e,a=Y(`message`,{"has-title":t,[i]:i}),o=pe(n)?n():n;return r&&typeof o==`string`?T(`div`,{class:a,innerHTML:o},null):T(`div`,{class:a},[o])},te=()=>{if(r.default)return T(`div`,{class:Y(`content`)},[r.default()]);let{title:t,message:n,allowHtml:i}=e;if(n){let e=!!(t||r.title);return T(`div`,{key:+!!i,class:Y(`content`,{isolated:!e})},[ee(e)])}},y=()=>T(`div`,{class:[t,Y(`footer`)]},[e.showCancelButton&&T(u,{size:`large`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),style:{color:e.cancelButtonColor},loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(u,{size:`large`,text:e.confirmButtonText||X(`confirm`),class:[Y(`confirm`),{[s]:e.showCancelButton}],style:{color:e.confirmButtonColor},loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]),b=()=>T(ge,{class:Y(`footer`)},{default:()=>[e.showCancelButton&&T(V,{type:`warning`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),color:e.cancelButtonColor,loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(V,{type:`danger`,text:e.confirmButtonText||X(`confirm`),class:Y(`confirm`),color:e.confirmButtonColor,loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]}),x=()=>r.footer?r.footer():e.theme===`round-button`?b():y();return()=>{let{width:t,title:n,theme:r,message:a,className:s}=e;return T(J,g({ref:i,role:`dialog`,class:[Y([r]),s],style:{width:me(t)},tabindex:0,"aria-labelledby":n||a,onKeydown:m,"onUpdate:show":o},fe(e,Fe)),{default:()=>[h(),te(),x()]})}}}),Z,Le=S({},{title:``,width:``,theme:null,message:``,overlay:!0,callback:null,teleport:`body`,className:``,allowHtml:!1,lockScroll:!0,transition:void 0,beforeClose:null,overlayClass:``,overlayStyle:void 0,messageAlign:``,cancelButtonText:``,cancelButtonColor:null,cancelButtonDisabled:!1,confirmButtonText:``,confirmButtonColor:null,confirmButtonDisabled:!1,showConfirmButton:!0,showCancelButton:!1,closeOnPopstate:!0,closeOnClickOverlay:!1,destroyOnClose:!1});function Re(){let e={setup(){let{state:e,toggle:t}=je();return()=>T(Ie,g(e,{"onUpdate:show":t}),null)}};({instance:Z}=Me(e))}function ze(e){return p?new Promise((t,n)=>{Z||Re(),Z.open(S({},Le,e,{callback:e=>{(e===`confirm`?t:n)(e)}}))}):Promise.resolve(void 0)}var Be=e=>ze(S({showCancelButton:!0},e)),[Ve,Q]=M(`nav-bar`),He=h({name:Ve,props:{title:String,fixed:Boolean,zIndex:k,border:j,leftText:String,rightText:String,leftDisabled:Boolean,rightDisabled:Boolean,leftArrow:Boolean,placeholder:Boolean,safeAreaInsetTop:Boolean,clickable:j},emits:[`clickLeft`,`clickRight`],setup(e,{emit:t,slots:n}){let a=v(),o=l(a,Q),s=n=>{e.leftDisabled||t(`clickLeft`,n)},c=n=>{e.rightDisabled||t(`clickRight`,n)},u=()=>n.left?n.left():[e.leftArrow&&T(i,{class:Q(`arrow`),name:`arrow-left`},null),e.leftText&&T(`span`,{class:Q(`text`)},[e.leftText])],d=()=>n.right?n.right():T(`span`,{class:Q(`text`)},[e.rightText]),f=()=>{let{title:t,fixed:i,border:o,zIndex:l}=e,f=w(l),p=e.leftArrow||e.leftText||n.left,m=e.rightText||n.right;return T(`div`,{ref:a,style:f,class:[Q({fixed:i}),{[r]:o,"van-safe-area-top":e.safeAreaInsetTop}]},[T(`div`,{class:Q(`content`)},[p&&T(`div`,{class:[Q(`left`,{disabled:e.leftDisabled}),e.clickable&&!e.leftDisabled?`van-haptics-feedback`:``],onClick:s},[u()]),T(`div`,{class:[Q(`title`),`van-ellipsis`]},[n.title?n.title():t]),m&&T(`div`,{class:[Q(`right`,{disabled:e.rightDisabled}),e.clickable&&!e.rightDisabled?`van-haptics-feedback`:``],onClick:c},[d()])])])};return()=>e.fixed&&e.placeholder?o(f):f()}}),Ue=F(He),$=e=>String(e).padStart(2,`0`);function We(e){let t=String(e||``).trim();if(!t)return null;let n=new Date(t);return Number.isNaN(n.getTime())?null:n}function Ge(e,t=new Date){let n=We(e);if(!n)return`—`;let r=Math.round((t.getTime()-n.getTime())/1e3);if(r<45&&r>-60)return`刚刚`;if(r>=45&&r<3600)return`${Math.max(1,Math.round(r/60))} 分钟前`;let i=`${$(n.getHours())}:${$(n.getMinutes())}`,a=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),o=864e5;return n.getTime()>=a&&n.getTime()=a-o&&n.getTime()i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>S(e)===`[object Map]`,p=e=>S(e)===`[object Set]`,m=e=>S(e)===`[object Date]`,h=e=>S(e)===`[object RegExp]`,g=e=>typeof e==`function`,_=e=>typeof e==`string`,v=e=>typeof e==`symbol`,y=e=>typeof e==`object`&&!!e,b=e=>(y(e)||g(e))&&g(e.then)&&g(e.catch),x=Object.prototype.toString,S=e=>x.call(e),C=e=>S(e).slice(8,-1),w=e=>S(e)===`[object Object]`,T=e=>_(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,ee=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),E=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},te=/-\w/g,D=E(e=>e.replace(te,e=>e.slice(1).toUpperCase())),ne=/\B([A-Z])/g,O=E(e=>e.replace(ne,`-$1`).toLowerCase()),re=E(e=>e.charAt(0).toUpperCase()+e.slice(1)),ie=E(e=>e?`on${re(e)}`:``),k=(e,t)=>!Object.is(e,t),A=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},oe=e=>{let t=parseFloat(e);return isNaN(t)?e:t},se=e=>{let t=_(e)?Number(e):NaN;return isNaN(t)?e:t},ce,le=()=>ce||(ce=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{});function ue(e){if(d(e)){let t={};for(let n=0;n{if(e){let n=e.split(fe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function he(e){let t=``;if(_(e))t=e;else if(d(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Se=e=>_(e)?e:e==null?``:d(e)||y(e)&&(e.toString===x||!g(e.toString))?xe(e)?Se(e.value):JSON.stringify(e,Ce,2):String(e),Ce=(e,t)=>xe(t)?Ce(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[we(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>we(e))}:v(t)?we(t):y(t)&&!d(t)&&!w(t)?String(t):t,we=(e,t=``)=>{var n;return v(e)?`Symbol(${(n=e.description)==null?t:n})`:e},j,Te=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&j&&(j.active?(this.parent=j,this.index=(j.scopes||(j.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes){let n=this.scopes.slice();for(e=0,t=n.length;e0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(je){let e=je;for(je=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ae;){let t=Ae;for(Ae=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}function Fe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ie(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),ze(r),Be(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Re(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Re(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ke)||(e.globalVersion=Ke,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Le(e))))return;e.flags|=2;let t=e.dep,n=M,r=Ve;M=e,Ve=!0;try{Fe(e);let n=e.fn(e._value);(t.version===0||k(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{M=n,Ve=r,Ie(e),e.flags&=-3}}function ze(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ze(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Be(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var Ve=!0,He=[];function Ue(){He.push(Ve),Ve=!1}function We(){let e=He.pop();Ve=e===void 0||e}function Ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;try{t()}finally{M=e}}}var Ke=0,qe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Je=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!M||!Ve||M===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==M)t=this.activeLink=new qe(M,this),M.deps?(t.prevDep=M.depsTail,M.depsTail.nextDep=t,M.depsTail=t):M.deps=M.depsTail=t,Ye(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=M.depsTail,t.nextDep=void 0,M.depsTail.nextDep=t,M.depsTail=t,M.deps===t&&(M.deps=e)}return t}trigger(e){this.version++,Ke++,this.notify(e)}notify(e){Ne();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pe()}}};function Ye(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Ye(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var Xe=new WeakMap,Ze=Symbol(``),Qe=Symbol(``),$e=Symbol(``);function N(e,t,n){if(Ve&&M){let t=Xe.get(e);t||Xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Je),r.map=t,r.key=n),r.track()}}function et(e,t,n,r,i,a){let o=Xe.get(e);if(!o){Ke++;return}let s=e=>{e&&e.trigger()};if(Ne(),t===`clear`)o.forEach(s);else{let i=d(e),a=i&&T(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===$e||!v(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get($e)),t){case`add`:i?a&&s(o.get(`length`)):(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`delete`:i||(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`set`:f(e)&&s(o.get(Ze))}}Pe()}function tt(e){let t=F(e);return t===e?t:(N(t,`iterate`,$e),P(e)?t:t.map(Ht))}function nt(e){return N(e=F(e),`iterate`,$e),e}function rt(e,t){return zt(e)?Ut(Rt(e)?Ht(t):t):Ht(t)}var it={__proto__:null,[Symbol.iterator](){return at(this,Symbol.iterator,e=>rt(this,e))},concat(...e){return tt(this).concat(...e.map(e=>d(e)?tt(e):e))},entries(){return at(this,`entries`,e=>(e[1]=rt(this,e[1]),e))},every(e,t){return st(this,`every`,e,t,void 0,arguments)},filter(e,t){return st(this,`filter`,e,t,e=>e.map(e=>rt(this,e)),arguments)},find(e,t){return st(this,`find`,e,t,e=>rt(this,e),arguments)},findIndex(e,t){return st(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return st(this,`findLast`,e,t,e=>rt(this,e),arguments)},findLastIndex(e,t){return st(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return st(this,`forEach`,e,t,void 0,arguments)},includes(...e){return lt(this,`includes`,e)},indexOf(...e){return lt(this,`indexOf`,e)},join(e){return tt(this).join(e)},lastIndexOf(...e){return lt(this,`lastIndexOf`,e)},map(e,t){return st(this,`map`,e,t,void 0,arguments)},pop(){return ut(this,`pop`)},push(...e){return ut(this,`push`,e)},reduce(e,...t){return ct(this,`reduce`,e,t)},reduceRight(e,...t){return ct(this,`reduceRight`,e,t)},shift(){return ut(this,`shift`)},some(e,t){return st(this,`some`,e,t,void 0,arguments)},splice(...e){return ut(this,`splice`,e)},toReversed(){return tt(this).toReversed()},toSorted(e){return tt(this).toSorted(e)},toSpliced(...e){return tt(this).toSpliced(...e)},unshift(...e){return ut(this,`unshift`,e)},values(){return at(this,`values`,e=>rt(this,e))}};function at(e,t,n){let r=nt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var ot=Array.prototype;function st(e,t,n,r,i,a){let o=nt(e),s=o!==e&&!P(e),c=o[t];if(c!==ot[t]){let t=c.apply(e,a);return s?Ht(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,rt(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function ct(e,t,n,r){let i=nt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=rt(e,t)),n.call(this,t,rt(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?rt(e,c):c}function lt(e,t,n){let r=F(e);N(r,`iterate`,$e);let i=r[t](...n);return(i===-1||i===!1)&&Bt(n[0])?(n[0]=F(n[0]),r[t](...n)):i}function ut(e,t,n=[]){Ue(),Ne();let r=F(e)[t].apply(e,n);return Pe(),We(),r}var dt=e(`__proto__,__v_isRef,__isVue`),ft=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(v));function pt(e){v(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.hasOwnProperty(e)}var mt=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?Mt:jt:i?At:kt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=it[t]))return e;if(t===`hasOwnProperty`)return pt}let o=Reflect.get(e,t,I(e)?e:n);if((v(t)?ft.has(t):dt(t))||(r||N(e,`get`,t),i))return o;if(I(o)){let e=a&&T(t)?o:o.value;return r&&y(e)?It(e):e}return y(o)?r?It(o):Pt(o):o}},ht=class extends mt{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&T(t);if(!this._isShallow){let e=zt(i);if(!P(n)&&!zt(n)&&(i=F(i),n=F(n)),!a&&I(i)&&!I(n))return e||(i.value=n),!0}let o=a?Number(t)e,xt=e=>Reflect.getPrototypeOf(e);function St(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a),c=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?bt:t?Ut:Ht;return!t&&N(a,`iterate`,l?Qe:Ze),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Ct(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function wt(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(k(n,a)&&N(i,`get`,n),N(i,`get`,a));let{has:o}=xt(i),s=t?bt:e?Ut:Ht;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(F(t),`iterate`,Ze),t.size},has(t){let n=this.__v_raw,r=F(n),i=F(t);return e||(k(t,i)&&N(r,`has`,t),N(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=F(a),s=t?bt:e?Ut:Ht;return!e&&N(o,`iterate`,Ze),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Ct(`add`),set:Ct(`set`),delete:Ct(`delete`),clear:Ct(`clear`)}:{add(e){let n=F(this),r=xt(n),i=F(e),a=!t&&!P(e)&&!zt(e)?i:e;return r.has.call(n,a)||k(e,a)&&r.has.call(n,e)||k(i,a)&&r.has.call(n,i)||(n.add(a),et(n,`add`,a,a)),this},set(e,n){!t&&!P(n)&&!zt(n)&&(n=F(n));let r=F(this),{has:i,get:a}=xt(r),o=i.call(r,e);o||(e=F(e),o=i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?k(n,s)&&et(r,`set`,e,n,s):et(r,`add`,e,n),this},delete(e){let t=F(this),{has:n,get:r}=xt(t),i=n.call(t,e);i||(e=F(e),i=n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&et(t,`delete`,e,void 0,a),o},clear(){let e=F(this),t=e.size!==0,n=e.clear();return t&&et(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=St(r,e,t)}),n}function Tt(e,t){let n=wt(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var Et={get:Tt(!1,!1)},Dt={get:Tt(!1,!0)},Ot={get:Tt(!0,!1)},kt=new WeakMap,At=new WeakMap,jt=new WeakMap,Mt=new WeakMap;function Nt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function Pt(e){return zt(e)?e:Lt(e,!1,_t,Et,kt)}function Ft(e){return Lt(e,!1,yt,Dt,At)}function It(e){return Lt(e,!0,vt,Ot,jt)}function Lt(e,t,n,r,i){if(!y(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let a=i.get(e);if(a)return a;let o=Nt(C(e));if(o===0)return e;let s=new Proxy(e,o===2?r:n);return i.set(e,s),s}function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function P(e){return!!(e&&e.__v_isShallow)}function Bt(e){return e?!!e.__v_raw:!1}function F(e){let t=e&&e.__v_raw;return t?F(t):e}function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&ae(e,`__v_skip`,!0),e}var Ht=e=>y(e)?Pt(e):e,Ut=e=>y(e)?It(e):e;function I(e){return e?e.__v_isRef===!0:!1}function Wt(e){return Kt(e,!1)}function Gt(e){return Kt(e,!0)}function Kt(e,t){return I(e)?e:new qt(e,t)}var qt=class{constructor(e,t){this.dep=new Je,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:F(e),this._value=t?e:Ht(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?e:F(e),k(e,t)&&(this._rawValue=e,this._value=n?e:Ht(e),this.dep.trigger())}};function L(e){return I(e)?e.value:e}var Jt={get:(e,t,n)=>t===`__v_raw`?e:L(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return I(i)&&!I(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return Rt(e)?e:new Proxy(e,Jt)}var Xt=class{constructor(e){this.__v_isRef=!0,this._value=void 0;let t=this.dep=new Je,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}};function Zt(e){return new Xt(e)}var Qt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Je(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ke-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return Me(this,!0),!0}get value(){let e=this.dep.track();return Re(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function $t(e,t,n=!1){let r,i;return g(e)?r=e:(r=e.get,i=e.set),new Qt(r,i,n)}var en={},tn=new WeakMap,nn=void 0;function rn(e,t=!1,n=nn){if(n){let t=tn.get(n);t||tn.set(n,t=[]),t.push(e)}}function an(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:P(e)||o===!1||o===0?on(e,1):on(e),m,h,_,v,y=!1,b=!1;if(I(e)?(h=()=>e.value,y=P(e)):Rt(e)?(h=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Rt(e)||P(e)),h=()=>e.map(e=>{if(I(e))return e.value;if(Rt(e))return p(e);if(g(e))return f?f(e,2):e()})):h=g(e)?n?f?()=>f(e,2):e:()=>{if(_){Ue();try{_()}finally{We()}}let t=nn;nn=m;try{return f?f(e,3,[v]):e(v)}finally{nn=t}}:r,n&&o){let e=h,t=o===!0?1/0:o;h=()=>on(e(),t)}let x=Ee(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{let n=e(...t);return S(),n}}let C=b?Array(e.length).fill(en):en,w=e=>{if(m.flags&1&&(m.dirty||e)){if(n){let t=m.run();if(e||o||y||(b?t.some((e,t)=>k(e,C[t])):k(t,C))){_&&_();let e=nn;nn=m;try{let e=[t,C===en?void 0:b&&C[0]===en?[]:C,v];C=t,f?f(n,3,e):n(...e)}finally{nn=e}}}else m.run()}};return u&&u(w),m=new Oe(h),m.scheduler=l?()=>l(w,!1):w,v=e=>rn(e,!1,m),_=m.onStop=()=>{let e=tn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();tn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function on(e,t=1/0,n){if(t<=0||!y(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,I(e))on(e.value,t,n);else if(d(e))for(let r=0;r{on(e,t,n)});else if(w(e)){for(let r in e)on(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&on(e[r],t,n)}return e}function sn(e,t,n,r){try{return r?e(...r):e()}catch(e){cn(e,t,n)}}function R(e,t,n,r){if(g(e)){let i=sn(e,t,n,r);return i&&b(i)&&i.catch(e=>{cn(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a>>1,i=z[r],a=Cn(i);a=Cn(n)?z.push(e):z.splice(_n(t),0,e),e.flags|=1,yn()}}function yn(){hn||(hn=mn.then(wn))}function bn(e){if(!d(e))fn&&e.id===-1?fn.splice(pn+1,0,e):e.flags&1||(dn.push(e),e.flags|=1);else for(let t=0;tCn(e)-Cn(t));if(dn.length=0,fn){for(let t=0;te.id==null?e.flags&2?-1:1/0:e.id;function wn(e){try{for(un=0;un{r._d&&pa(-1);let i=En(t),a=la.length,o;try{o=e(...n)}finally{for(let e=la.length;e>a;e--)da();En(i),r._d&&pa(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function On(e,n){if(B===null)return e;let r=qa(B),i=e.dirs||(e.dirs=[]);for(let e=0;e1)return n&&g(t)?t.call(r&&r.proxy):t}}var jn=Symbol.for(`v-scx`),Mn=()=>V(jn);function Nn(e,t){return In(e,null,t)}function Pn(e,t){return In(e,null,{flush:`sync`})}function Fn(e,t,n){return In(e,t,n)}function In(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ba){if(c===`sync`){let e=Mn();f=e.__watcherHandles||(e.__watcherHandles=[])}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=X;u.call=(e,t,n)=>R(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{G(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():vn(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=an(e,n,u);return Ba&&(f?f.push(h):d&&h()),h}function Ln(e,t,n){let r=this.proxy,i=_(e)?e.includes(`.`)?Rn(r,e):()=>r[e]:e.bind(r,r),a;g(t)?a=t:(a=t.handler,n=t);let o=La(this),s=In(i,a.bind(r),n);return o(),s}function Rn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;ee.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===``),Un=e=>e&&(e.defer||e.defer===``),Wn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Gn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Kn=(e,t)=>{let n=e&&e.to;return _(n)?t?t(n):null:n},qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g,parentNode:_}}=l,v=Hn(t.props),{dynamicChildren:y}=t,b=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},x=(e=t)=>{let n=Hn(e.props),r=e.target=Kn(e.props,m),a=Qn(r,e,h,p);r&&(o!==`svg`&&Wn(r)?o=`svg`:o!==`mathml`&&Gn(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(b(e,r,a),Zn(e,!1)))},S=e=>{let t=()=>{if(zn.get(e)===t){if(zn.delete(e),Hn(e.props)){let t=_(e.el)||n;b(e,t,e.anchor),Zn(e,!0)}x(e)}};zn.set(e,t),G(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Un(t.props)||a&&a.pendingBranch){S(t);return}v&&(b(t,n,i),Zn(t,!0)),x()}else{t.el=e.el;let r=t.anchor=e.anchor,u=zn.get(e);if(u){u.flags|=8,zn.delete(e),S(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Hn(e.props),_=g?n:p,b=g?r:h;if(o===`svg`||Wn(p)?o=`svg`:(o===`mathml`||Gn(p))&&(o=`mathml`),y?(f(e.dynamicChildren,y,_,i,a,o,s),$i(e,t,!0)):c||d(e,t,_,b,i,a,o,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Jn(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=Kn(t.props,m);e&&(t.target=e,Jn(t,e,null,l,0))}else g&&Jn(t,p,h,l,1);Zn(t,v)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=Hn(f),m=a||!p,h=zn.get(e);if(h&&(h.flags|=8,zn.delete(e)),d&&(i(l),i(u)),a&&i(c),!h&&(p||d)&&o&16)for(let e=0;e{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}var U=[Function,Array],tr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:U,onEnter:U,onAfterEnter:U,onEnterCancelled:U,onBeforeLeave:U,onLeave:U,onAfterLeave:U,onLeaveCancelled:U,onBeforeAppear:U,onAppear:U,onAfterAppear:U,onAppearCancelled:U},nr=e=>{let t=e.subTree;return t.component?nr(t.component):t},rr={name:`BaseTransition`,props:tr,setup(e,{slots:t}){let n=Pa(),r=er();return()=>{let i=t.default&&dr(t.default(),!0),a=i&&i.length?ir(i):n.subTree?Ea():void 0;if(!a)return;let o=F(e),{mode:s}=o;if(r.isLeaving)return cr(a);let c=lr(a);if(!c)return cr(a);let l=sr(c,o,r,n,e=>l=e);c.type!==K&&ur(c,l);let u=n.subTree&&lr(n.subTree);if(u&&u.type!==K&&!va(u,c)&&nr(n).type!==K){let e=sr(u,o,r,n);if(ur(u,e),s===`out-in`&&c.type!==K)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},cr(a);s===`in-out`&&c.type!==K?e.delayLeave=(e,t,n)=>{let i=or(r,u);i[String(u.key)]=u,e[H]=()=>{t(),e[H]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return a}}};function ir(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==K){t=n;break}}return t}var ar=rr;function or(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function sr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=or(n,e),C=(e,t)=>{e&&R(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted){if(a)r=_||c;else return}t[H]&&t[H](!0);let i=S[x];i&&va(e,i)&&i.el[H]&&i.el[H](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted){if(a)r=v||l,i=y||u,o=b||f;else return}let s=!1;t[$n]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[$n]=void 0)};let c=t[$n].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[$n]&&t[$n](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[H]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[H]=void 0,S[i]===e&&delete S[i])};let o=t[H].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=sr(e,t,n,r,i);return i&&i(a),a}};return T}function cr(e){if(yr(e))return e=wa(e),e.children=null,e}function lr(e){if(!yr(e))return Vn(e.type)&&e.children?ir(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&g(n.default))return n.default()}}function ur(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;let n=e.component.subTree;ur(Vn(n.type)&&lr(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function dr(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;egr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(vr(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&gr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?qa(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,h=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=F(v),b=v===t?i:e=>!mr(h,e)&&u(y,e),x=(e,t)=>!(t&&mr(h,t));if(m!=null&&m!==p){if(_r(n),_(m))h[m]=null,b(m)&&(v[m]=null);else if(I(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(h[e.k]=null)}}if(g(p))sn(p,f,12,[l,h]);else{let t=_(p),n=I(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:h[p]:x(p)||!e.k?p.value:h[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)h[p]=[s],b(p)&&(v[p]=h[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(h[e.k]=t)}}else t?(h[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(h[e.k]=l))};if(l){let t=()=>{i(),hr.delete(e)};t.id=-1,hr.set(e,t),G(t,r)}else _r(e),i()}}}function _r(e){let t=hr.get(e);t&&(t.flags|=8,hr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var vr=e=>!!e.type.__asyncLoader,yr=e=>e.type.__isKeepAlive,br={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=Pa(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),G(()=>{a.isDeactivated=!1,a.a&&A(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Y(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;na(t.m),na(t.a),l(e,f,null,1,s),G(()=>{t.da&&A(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Y(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Er(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=Ja(vr(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!va(t,o))?p(t):o&&Er(o),i.delete(e),a.delete(e)}Fn(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>xr(e,t)),t&&m(e=>!xr(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(ia(n.subTree.type)?G(()=>{i.set(g,Dr(n.subTree))},n.subTree.suspense):i.set(g,Dr(n.subTree)))};return jr(_),Nr(_),Pr(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Dr(t);if(e.type===i.type&&e.key===i.key){Er(i);let e=i.component.da;e&&G(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!_a(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Dr(r);if(s.type===K)return o=null,s;let c=s.type,l=Ja(vr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!xr(u,l))||d&&l&&xr(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=wa(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&ur(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,ia(r.type)?r:s}}};function xr(e,t){return d(e)?e.some(e=>xr(e,t)):_(e)?e.split(`,`).includes(t):h(e)?(e.lastIndex=0,e.test(t)):!1}function Sr(e,t){wr(e,`a`,t)}function Cr(e,t){wr(e,`da`,t)}function wr(e,t,n=X){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Or(t,r,n),n){let e=n.parent;for(;e&&e.parent;)yr(e.parent.vnode)&&Tr(r,t,n,e),e=e.parent}}function Tr(e,t,n,r){let i=Or(t,e,r,!0);Fr(()=>{c(r[t],i)},n)}function Er(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Dr(e){return e.shapeFlag&128?e.ssContent:e}function Or(e,t,n=X,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||(t.__weh=(...r)=>{Ue();let i=La(n),a=R(t,n,e,r);return i(),We(),a});return r?i.unshift(a):i.push(a),a}}var kr=e=>(t,n=X)=>{(!Ba||e===`sp`)&&Or(e,(...e)=>t(...e),n)},Ar=kr(`bm`),jr=kr(`m`),Mr=kr(`bu`),Nr=kr(`u`),Pr=kr(`bum`),Fr=kr(`um`),Ir=kr(`sp`),Lr=kr(`rtg`),Rr=kr(`rtc`);function zr(e,t=X){Or(`ec`,e,t)}var Br=`components`;function Vr(e,t){return Wr(Br,e,!0,t)||e}var Hr=Symbol.for(`v-ndc`);function Ur(e){return _(e)?Wr(Br,e,!1)||e:e||Hr}function Wr(e,t,n=!0,r=!1){let i=B||X;if(i){let n=i.type;if(e===Br){let e=Ja(n,!1);if(e&&(e===t||e===D(t)||e===re(D(t))))return n}let a=Gr(i[e]||n[e],t)||Gr(i.appContext[e],t);return!a&&r?n:a}}function Gr(e,t){return e&&(e[t]||e[D(t)]||e[re(D(t))])}function Kr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||_(e)){let n=o&&Rt(e),r=!1,s=!1;n&&(r=!P(e),s=zt(e),e=nt(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}var Jr=e=>e?za(e)?qa(e):Jr(e.parent):null,Yr=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{vn(e.update)}),$nextTick:e=>e.n||(e.n=gn.bind(e.proxy)),$watch:e=>Ln.bind(e)}),Xr=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),Zr={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(Xr(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else $r&&(s[n]=0)}let d=Yr[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return Xr(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||Xr(n,c)||u(o,c)||u(i,c)||u(Yr,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function Qr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var $r=!0;function ei(e){let t=ii(e),n=e.proxy,i=e.ctx;$r=!1,t.beforeCreate&&ni(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:h,updated:_,activated:v,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:E,errorCaptured:te,serverPrefetch:D,expose:ne,inheritAttrs:O,components:re,directives:ie,filters:k}=t;if(u&&ti(u,i,null),s)for(let e in s){let t=s[e];g(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);y(t)&&(e.data=Pt(t))}if($r=!0,o)for(let e in o){let t=o[e],a=Z({get:g(t)?t.bind(n,n):g(t.get)?t.get.bind(n,n):r,set:!g(t)&&g(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)ri(c[e],i,n,e);if(l){let e=g(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{An(t,e[t])})}f&&ni(f,e,`c`);function A(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(A(Ar,p),A(jr,m),A(Mr,h),A(Nr,_),A(Sr,v),A(Cr,b),A(zr,te),A(Rr,ee),A(Lr,E),A(Pr,S),A(Fr,w),A(Ir,D),d(ne)){if(ne.length){let t=e.exposed||(e.exposed={});ne.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={})}T&&e.render===r&&(e.render=T),O!=null&&(e.inheritAttrs=O),re&&(e.components=re),ie&&(e.directives=ie),D&&pr(e)}function ti(e,t,n=r){d(e)&&(e=li(e));for(let n in e){let r=e[n],i;i=y(r)?`default`in r?V(r.from||n,r.default,!0):V(r.from||n):V(r),I(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function ni(e,t,n){R(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function ri(e,t,n,r){let i=r.includes(`.`)?Rn(n,r):()=>n[r];if(_(e)){let n=t[e];g(n)&&Fn(i,n)}else if(g(e))Fn(i,e.bind(n));else if(y(e)){if(d(e))e.forEach(e=>ri(e,t,n,r));else{let r=g(e.handler)?e.handler.bind(n):t[e.handler];g(r)&&Fn(i,r,e)}}}function ii(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>ai(c,e,o,!0)),ai(c,t,o)),y(t)&&a.set(t,c),c}function ai(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&ai(e,a,n,!0),i&&i.forEach(t=>ai(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=oi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var oi={data:si,props:di,emits:di,methods:ui,computed:ui,beforeCreate:W,created:W,beforeMount:W,mounted:W,beforeUpdate:W,updated:W,beforeDestroy:W,beforeUnmount:W,destroyed:W,unmounted:W,activated:W,deactivated:W,errorCaptured:W,serverPrefetch:W,components:ui,directives:ui,watch:fi,provide:si,inject:ci};function si(e,t){return t?e?function(){return s(g(e)?e.call(this,this):e,g(t)?t.call(this,this):t)}:t:e}function ci(e,t){return ui(li(e),li(t))}function li(e){if(d(e)){let t={};for(let n=0;n{let l,u=t,d;return Pn(()=>{let t=e[a];k(l,t)&&(l=t,c())}),{get(){return s(),r.get?r.get(l):l},set(e){let s=r.set?r.set(e):e;if(!k(s,l)&&!(u!==t&&k(e,u)))return;let f=i.vnode.props,p=!!(f&&(n in f||a in f||o in f)&&(`onUpdate:${n}`in f||`onUpdate:${a}`in f||`onUpdate:${o}`in f));p||(l=e,c()),i.emit(`update:${n}`,s),k(e,u)&&(k(e,s)&&!k(s,d)||p&&u!==t&&!k(s,l))&&c(),u=e,d=s}}});return c[Symbol.iterator]=()=>{let e=0;return{next(){return e<2?{value:e++?s||t:c,done:!1}:{done:!0}}}},c}var vi=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${D(t)}Modifiers`]||e[`${O(t)}Modifiers`];function yi(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&vi(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>_(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(D(n))];!l&&o&&(l=i[c=ie(O(n))]),l&&R(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,R(u,e,6,a)}}var bi=new WeakMap;function xi(e,t,n=!1){let r=n?bi:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!g(e)){let r=e=>{let n=xi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(y(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),y(e)&&r.set(e,o),o)}function Si(e,t){return!e||!a(t)?!1:(t=t.slice(2),t=t===`Once`?t:t.replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,O(t))||u(e,t))}function Ci(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=En(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Da(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Da(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:wi(c)}}catch(t){la.length=0,cn(t,e,1),v=J(K)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ti(y,a)),b=wa(b,y,!1,!0))}return n.dirs&&(b=wa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&ur(Vn(b.type)&&lr(b)||b,n.transition),v=b,En(_),v}var wi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Ei(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Di(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;tObject.create(Ai),Mi=e=>Object.getPrototypeOf(e)===Ai;function Ni(e,t,n,r=!1){let i={},a=ji();e.propsDefaults=Object.create(null),Fi(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);e.props=n?r?i:Ft(i):e.type.props?i:a,e.attrs=a}function Pi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{p=!0;let[t,n]=Ri(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return y(e)&&a.set(e,n),n;if(d(c))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Vi=e=>d(e)?e.map(Da):[Da(e)],Hi=(e,t,n)=>{if(t._n)return t;let r=Dn((...e)=>Vi(t(...e)),n);return r._c=!1,r},Ui=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Bi(n))continue;let i=e[n];if(g(i))t[n]=Hi(n,i,r);else if(i!=null){let e=Vi(i);t[n]=()=>e}}},Wi=(e,t)=>{let n=Vi(t);e.slots.default=()=>n},Gi=(e,t,n)=>{for(let r in t)(n||!Bi(r))&&(e[r]=t[r])},Ki=(e,t,n)=>{let r=e.slots=ji();if(e.vnode.shapeFlag&32){let e=t._;e?(Gi(r,t,n),n&&ae(r,`_`,e,!0)):Ui(t,r)}else t&&Wi(e,t)},qi=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:Gi(a,n,r):(o=!n.$stable,Ui(n,a)),s=n}else n&&(Wi(e,n),s={default:1});if(o)for(let e in a)!Bi(e)&&s[e]==null&&delete a[e]},G=aa;function Ji(e){return Yi(e)}function Yi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!va(e,t)&&(r=ve(e),pe(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case sa:y(e,t,n,r);break;case K:b(e,t,n,r);break;case ca:e==null&&x(t,n,r,o);break;case oa:re(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?ie(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,xe)}u!=null&&i?gr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&gr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)T(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),D(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},T=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&te(e.children,d,null,r,i,Xi(e,a),s,u),_&&kn(e,null,r,`created`),E(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!ee(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Y(f,r,e)}_&&kn(e,null,r,`beforeMount`);let v=Qi(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&G(()=>{try{f&&Y(f,r,e),v&&g.enter(d),_&&kn(e,null,r,`mounted`)}finally{}},i)},E=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t{for(let l=c;l{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&Zi(r,!1),(g=h.onVnodeBeforeUpdate)&&Y(g,r,n,e),f&&kn(n,e,r,`beforeUpdate`),r&&Zi(r,!0),d&&(!e.dynamicChildren||e.dynamicChildren.length!==d.length)&&(u=0,s=!1,d=null),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?ne(e.dynamicChildren,d,l,r,i,Xi(n,a),o):s||ce(e,n,l,null,r,i,Xi(n,a),o,!1),u>0){if(u&16)O(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t{g&&Y(g,r,n,e),f&&kn(n,e,r,`updated`)},i)},ne=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(n!==r){if(n!==t)for(let t in n)!ee(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(ee(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},re=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),te(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(ne(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&$i(e,t,!0)):ce(e,t,n,f,i,a,s,c,l)},ie=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):ae(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Na(e,r,i);if(yr(e)&&(s.ctx.renderer=xe),Va(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,oe,o),!e.el){let r=s.subTree=J(K);b(null,r,t,n),e.placeholder=r.el}}else oe(s,e,t,n,i,a,o)},ae=(e,t,n)=>{let r=t.component=e.component;if(Ei(e,t,n)){if(r.asyncDep&&!r.asyncResolved){se(r,t,n);return}r.next=t,r.update()}else t.el=e.el,r.vnode=t},oe=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=ta(e);if(n){t&&(t.el=c.el,se(e,t,o)),n.asyncDep.then(()=>{G(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;Zi(e,!1),t?(t.el=c.el,se(e,t,o)):t=c,n&&A(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Y(d,s,t,c),Zi(e,!0);let f=Ci(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ve(p),e,i,a),t.el=f.el,u===null&&ki(e,f.el),r&&G(r,i),(d=t.props&&t.props.onVnodeUpdated)&&G(()=>Y(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=vr(t);if(Zi(e,!1),l&&A(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Y(o,d,t),Zi(e,!0),s&&Ce){let t=()=>{e.subTree=Ci(e),Ce(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Ci(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&G(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;G(()=>Y(o,d,e),i)}(t.shapeFlag&256||d&&vr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&G(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Oe(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>vn(u),Zi(e,!0),l()},se=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Pi(e,t.props,r,n),qi(e,t.children,n),Ue(),xn(e),We()},ce=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){de(l,d,n,r,i,a,o,s,c);return}if(f&256){ue(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&_e(l,i,a),d!==l&&p(n,d)):u&16?m&16?de(l,d,n,r,i,a,o,s,c):_e(l,i,a,!0):(u&8&&p(n,``),m&16&&te(d,n,r,i,a,o,s,c))},ue=(e,t,r,i,a,o,s,c,l)=>{e=e||n,t=t||n;let u=e.length,d=t.length,f=Math.min(u,d),p=0;for(;pd?_e(e,a,o,!0,!1,f):te(t,r,i,a,o,s,c,l,f)},de=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?Oa(t[u]):Da(t[u]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?Oa(t[p]):Da(t[p]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=ep)for(;u<=f;)pe(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?Oa(t[u]):Da(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u=b){pe(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&va(n,t[_])){i=_;break}i===void 0?pe(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?ea(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){fe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,xe);return}if(c===oa){o(a,t,n);for(let e=0;el.enter(a),i));else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{let e=a._isLeaving||!!a[H];a._isLeaving&&a[H](!0),l.persisted&&!e?u():r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}}else o(a,t,n)},pe=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ue(),gr(s,null,n,e,!0),We()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!vr(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Y(_,t,e),u&6)ge(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&kn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,xe,r):l&&!l.hasOnce&&(a!==oa||d>0&&d&64)?_e(l,t,n,!1,!0):(a===oa&&d&384||!i&&u&16)&&_e(c,t,n),r&&me(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&G(()=>{_&&Y(_,t,e),h&&kn(e,null,t,`unmounted`),v&&(e.el=null)},n)},me=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===oa){he(n,r);return}if(t===ca){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},he=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},ge=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;na(c),na(l),r&&A(r),i.stop(),a&&(a.flags|=8,pe(o,e,t,n)),s&&G(s,t),G(()=>{e.isUnmounted=!0},t)},_e=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return ve(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Bn];return n?h(n):t},ye=!1,be=(e,t,n)=>{let r;e==null?t._vnode&&(pe(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ye||(ye=!0,xn(r),Sn(),ye=!1)},xe={p:v,um:pe,m:fe,r:me,mt:k,mc:te,pc:ce,pbc:ne,n:ve,o:e},Se,Ce;return i&&([Se,Ce]=i(xe)),{render:be,hydrate:Se,createApp:hi(be,Se)}}function Xi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function Zi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Qi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function $i(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-->0;)n[a]=o,o=t[o];return n}function ta(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ta(t)}function na(e){if(e)for(let t=0;te.__isSuspense;function aa(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):bn(e)}var oa=Symbol.for(`v-fgt`),sa=Symbol.for(`v-txt`),K=Symbol.for(`v-cmt`),ca=Symbol.for(`v-stc`),la=[],q=null;function ua(e=!1){la.push(q=e?null:[])}function da(){la.pop(),q=la[la.length-1]||null}var fa=1;function pa(e,t=!1){fa+=e,e<0&&q&&t&&(q.hasOnce=!0)}function ma(e){return e.dynamicChildren=fa>0?q||n:null,da(),fa>0&&q&&q.push(e),e}function ha(e,t,n,r,i,a){return ma(xa(e,t,n,r,i,a,!0))}function ga(e,t,n,r,i){return ma(J(e,t,n,r,i,!0))}function _a(e){return e?e.__v_isVNode===!0:!1}function va(e,t){return e.type===t.type&&e.key===t.key}var ya=({key:e})=>e==null?null:e,ba=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:_(e)||I(e)||g(e)?{i:B,r:e,k:t,f:!!n}:e);function xa(e,t=null,n=null,r=0,i=null,a=e===oa?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ya(t),ref:t&&ba(t),scopeId:Tn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:B};return s?(ka(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=_(n)?8:16),fa>0&&!o&&q&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&q.push(c),c}var J=Sa;function Sa(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Hr)&&(e=K),_a(e)){let r=wa(e,t,!0);return n&&ka(r,n),fa>0&&!a&&q&&(r.shapeFlag&6?q[q.indexOf(e)]=r:q.push(r)),r.patchFlag=-2,r}if(Ya(e)&&(e=e.__vccOpts),t){t=Ca(t);let{class:e,style:n}=t;e&&!_(e)&&(t.class=he(e)),y(n)&&(Bt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=_(e)?1:ia(e)?128:Vn(e)?64:y(e)?4:g(e)?2:0;return xa(e,t,n,r,i,o,a,!0)}function Ca(e){return e?Bt(e)||Mi(e)?s({},e):e:null}function wa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Aa(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ya(l),ref:t&&t.ref?n&&a?d(a)?a.concat(ba(t)):[a,ba(t)]:ba(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==oa?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&wa(e.ssContent),ssFallback:e.ssFallback&&wa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&ur(u,c.clone(u)),u}function Ta(e=` `,t=0){return J(sa,null,e,t)}function Ea(e=``,t=!1){return t?(ua(),ga(K,null,e)):J(K,null,e)}function Da(e){return e==null||typeof e==`boolean`?J(K):d(e)?J(oa,null,e.slice()):_a(e)?Oa(e):J(sa,null,String(e))}function Oa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:wa(e)}function ka(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`){if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),ka(e,n()),n._c&&(n._d=!0));return}{n=32;let r=t._;!r&&!Mi(t)?t._ctx=B:r===3&&B&&(B.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}}else if(g(t)){if(r&65){ka(e,{default:t});return}t={default:t,_ctx:B},n=32}else t=String(t),r&64?(n=16,t=[Ta(t)]):n=8;e.children=t,e.shapeFlag|=n}function Aa(...e){let t={};for(let n=0;nX||B,Fa,Ia;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Fa=t(`__VUE_INSTANCE_SETTERS__`,e=>X=e),Ia=t(`__VUE_SSR_SETTERS__`,e=>Ba=e)}var La=e=>{let t=X;return Fa(e),e.scope.on(),()=>{e.scope.off(),Fa(t)}},Ra=()=>{X&&X.scope.off(),Fa(null)};function za(e){return e.vnode.shapeFlag&4}var Ba=!1;function Va(e,t=!1,n=!1){t&&Ia(t);let{props:r,children:i}=e.vnode,a=za(e);Ni(e,r,a,t),Ki(e,i,n||t);let o=a?Ha(e,t):void 0;return t&&Ia(!1),o}function Ha(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zr);let{setup:r}=n;if(r){Ue();let n=e.setupContext=r.length>1?Ka(e):null,i=La(e),a=sn(r,e,0,[e.props,n]),o=b(a);if(We(),i(),(o||e.sp)&&!vr(e)&&pr(e),o){if(a.then(Ra,Ra),t)return a.then(n=>{Ia(!0);try{Ua(e,n,t)}finally{Ia(!1)}}).catch(t=>{cn(t,e,0)});e.asyncDep=a}else Ua(e,a,t)}else Wa(e,t)}function Ua(e,t,n){g(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:y(t)&&(e.setupState=Yt(t)),Wa(e,n)}function Wa(e,t,n){let i=e.type;e.render||(e.render=i.render||r);{let t=La(e);Ue();try{ei(e)}finally{We(),t()}}}var Ga={get(e,t){return N(e,`get`,``),e[t]}};function Ka(e){return{attrs:new Proxy(e.attrs,Ga),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function qa(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Vt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yr)return Yr[n](e)},has(e,t){return t in e||t in Yr}})):e.proxy}function Ja(e,t=!0){return g(e)?e.displayName||e.name:e.name||t&&e.__name}function Ya(e){return g(e)&&`__vccOpts`in e}var Z=(e,t)=>$t(e,t,Ba);function Xa(e,t,n){try{pa(-1);let r=arguments.length;return r===2?y(t)&&!d(t)?_a(t)?J(e,null,[t]):J(e,t):J(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_a(n)&&(n=[n]),J(e,t,n))}finally{pa(1)}}var Za=`3.5.41`,Qa=void 0,$a=typeof window<`u`&&window.trustedTypes;if($a)try{Qa=$a.createPolicy(`vue`,{createHTML:e=>e})}catch{}var eo=Qa?e=>Qa.createHTML(e):e=>e,to=`http://www.w3.org/2000/svg`,no=`http://www.w3.org/1998/Math/MathML`,ro=typeof document<`u`?document:null,io=ro&&ro.createElement(`template`),ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i=t===`svg`?ro.createElementNS(to,e):t===`mathml`?ro.createElementNS(no,e):n?ro.createElement(e,{is:n}):ro.createElement(e);return e===`select`&&r&&r.multiple!=null&&i.setAttribute(`multiple`,r.multiple),i},createText:e=>ro.createTextNode(e),createComment:e=>ro.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ro.querySelector(e),setScopeId(e,t){e.setAttribute(t,``)},insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;if(i&&(i===a||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==a&&(i=i.nextSibling););else{io.innerHTML=eo(r===`svg`?`${e}`:r===`mathml`?`${e}`:e);let i=io.content;if(r===`svg`||r===`mathml`){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},oo=`transition`,so=`animation`,co=Symbol(`_vtc`),lo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uo=s({},tr,lo),fo=(e=>(e.displayName=`Transition`,e.props=uo,e))((e,{slots:t})=>Xa(ar,ho(e),t)),po=(e,t=[])=>{d(e)?e.forEach(e=>e(...t)):e&&e(...t)},mo=e=>e?d(e)?e.some(e=>e.length>1):e.length>1:!1;function ho(e){let t={};for(let n in e)n in lo||(t[n]=e[n]);if(e.css===!1)return t;let{name:n=`v`,type:r,duration:i,enterFromClass:a=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=a,appearActiveClass:u=o,appearToClass:d=c,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,h=go(i),g=h&&h[0],_=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:C=v,onAppear:w=y,onAppearCancelled:T=b}=t,ee=(e,t,n,r)=>{e._enterCancelled=r,yo(e,t?d:c),yo(e,t?u:o),n&&n()},E=(e,t)=>{e._isLeaving=!1,yo(e,f),yo(e,m),yo(e,p),t&&t()},te=e=>(t,n)=>{let i=e?w:y,o=()=>ee(t,e,n);po(i,[t,o]),bo(()=>{yo(t,e?l:a),vo(t,e?d:c),mo(i)||So(t,r,g,o)})};return s(t,{onBeforeEnter(e){po(v,[e]),vo(e,a),vo(e,o)},onBeforeAppear(e){po(C,[e]),vo(e,l),vo(e,u)},onEnter:te(!1),onAppear:te(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>E(e,t);vo(e,f),e._enterCancelled?(vo(e,p),Eo(e)):(Eo(e),vo(e,p)),bo(()=>{e._isLeaving&&(yo(e,f),vo(e,m),mo(x)||So(e,r,_,n))}),po(x,[e,n])},onEnterCancelled(e){ee(e,!1,void 0,!0),po(b,[e])},onAppearCancelled(e){ee(e,!0,void 0,!0),po(T,[e])},onLeaveCancelled(e){E(e),po(S,[e])}})}function go(e){if(e==null)return null;if(y(e))return[_o(e.enter),_o(e.leave)];{let t=_o(e);return[t,t]}}function _o(e){return se(e)}function vo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[co]||(e[co]=new Set)).add(t)}function yo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[co];n&&(n.delete(t),n.size||(e[co]=void 0))}function bo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}var xo=0;function So(e,t,n,r){let i=e._endId=++xo,a=()=>{i===e._endId&&r()};if(n!=null)return setTimeout(a,n);let{type:o,timeout:s,propCount:c}=Co(e,t);if(!o)return r();let l=o+`end`,u=0,d=()=>{e.removeEventListener(l,f),a()},f=t=>{t.target===e&&++u>=c&&d()};setTimeout(()=>{u(n[e]||``).split(`, `),i=r(`${oo}Delay`),a=r(`${oo}Duration`),o=wo(i,a),s=r(`${so}Delay`),c=r(`${so}Duration`),l=wo(s,c),u=null,d=0,f=0;t===oo?o>0&&(u=oo,d=o,f=a.length):t===so?l>0&&(u=so,d=l,f=c.length):(d=Math.max(o,l),u=d>0?o>l?oo:so:null,f=u?u===oo?a.length:c.length:0);let p=u===oo&&/\b(?:transform|all)(?:,|$)/.test(r(`${oo}Property`).toString());return{type:u,timeout:d,propCount:f,hasTransform:p}}function wo(e,t){for(;e.lengthTo(t)+To(e[n])))}function To(e){return e===`auto`?0:Number(e.slice(0,-1).replace(`,`,`.`))*1e3}function Eo(e){return(e?e.ownerDocument:document).body.offsetHeight}function Do(e,t,n){let r=e[co];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==null?e.removeAttribute(`class`):n?e.setAttribute(`class`,t):e.className=t}var Oo=Symbol(`_vod`),ko=Symbol(`_vsh`),Ao={name:`show`,beforeMount(e,{value:t},{transition:n}){e[Oo]=e.style.display===`none`?``:e.style.display,n&&t?n.beforeEnter(e):jo(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),jo(e,!0),r.enter(e)):r.leave(e,()=>{jo(e,!1)}):jo(e,t))},beforeUnmount(e,{value:t}){jo(e,t)}};function jo(e,t){e.style.display=t?e[Oo]:`none`,e[ko]=!t}var Mo=Symbol(``),No=/(?:^|;)\s*display\s*:/;function Po(e,t,n){let r=e.style,i=_(n),a=!1;if(n&&!i){if(t){if(_(t))for(let e of t.split(`;`)){let t=e.slice(0,e.indexOf(`:`)).trim();n[t]==null&&Io(r,t,``)}else for(let e in t)n[e]==null&&Io(r,e,``)}for(let i in n){i===`display`&&(a=!0);let o=n[i];o==null?Io(r,i,``):Bo(e,i,!_(t)&&t?t[i]:void 0,o)||Io(r,i,o)}}else if(i){if(t!==n){let e=r[Mo];e&&(n+=`;`+e),r.cssText=n,a=No.test(n)}}else t&&e.removeAttribute(`style`);Oo in e&&(e[Oo]=a?r.display:``,e[ko]&&(r.display=`none`))}var Fo=/\s*!important$/;function Io(e,t,n){if(d(n))n.forEach(n=>Io(e,t,n));else if(n==null&&(n=``),t.startsWith(`--`))e.setProperty(t,n);else{let r=zo(e,t);Fo.test(n)?e.setProperty(O(r),n.replace(Fo,``),`important`):e[r]=n}}var Lo=[`Webkit`,`Moz`,`ms`],Ro={};function zo(e,t){let n=Ro[t];if(n)return n;let r=D(t);if(r!==`filter`&&r in e)return Ro[t]=r;r=re(r);for(let n=0;nZo||(Qo.then(()=>Zo=0),Zo=Date.now());function es(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=n.attached)return;let r=n.value;if(d(r)){let n=e.stopImmediatePropagation;e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0};let i=r.slice(),a=[e];for(let n=0;ne.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ns=(e,t,n,r,i,s)=>{let c=i===`svg`;t===`class`?Do(e,r,c):t===`style`?Po(e,n,r):a(t)?o(t)||qo(e,t,n,r,s):(t[0]===`.`?(t=t.slice(1),1):t[0]===`^`?(t=t.slice(1),0):rs(e,t,r,c))?(Uo(e,t,r),!e.tagName.includes(`-`)&&(t===`value`||t===`checked`||t===`selected`)&&Ho(e,t,r,c,s,t!==`value`)):e._isVueCE&&(is(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!_(r)))?Uo(e,D(t),r,s,t):(t===`true-value`?e._trueValue=r:t===`false-value`&&(e._falseValue=r),Ho(e,t,r,c))};function rs(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t in e&&ts(t)&&g(n));if(t===`spellcheck`||t===`draggable`||t===`translate`||t===`autocorrect`||t===`sandbox`&&e.tagName===`IFRAME`||t===`form`||t===`list`&&e.tagName===`INPUT`||t===`type`&&e.tagName===`TEXTAREA`)return!1;if(t===`width`||t===`height`){let t=e.tagName;if(t===`IMG`||t===`VIDEO`||t===`CANVAS`||t===`SOURCE`)return!1}return ts(t)&&_(n)?!1:t in e}function is(e,t){let n=e._def.props;if(!n)return!1;let r=D(t);return Array.isArray(n)?n.some(e=>D(e)===r):Object.keys(n).some(e=>D(e)===r)}var as={esc:`escape`,space:` `,up:`arrow-up`,left:`arrow-left`,right:`arrow-right`,down:`arrow-down`,delete:`backspace`},os=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(`.`);return n[r]||(n[r]=(n=>{if(!(`key`in n))return;let r=O(n.key);if(t.some(e=>e===r||as[e]===r))return e(n)}))},ss=s({patchProp:ns},ao),cs;function ls(){return cs||(cs=Ji(ss))}var us=((...e)=>{let t=ls().createApp(...e),{mount:n}=t;return t.mount=e=>{let r=fs(e);if(!r)return;let i=t._component;!g(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent=``);let a=n(r,!1,ds(r));return r instanceof Element&&(r.removeAttribute(`v-cloak`),r.setAttribute(`data-v-app`,``)),a},t});function ds(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLElement==`function`&&e instanceof MathMLElement)return`mathml`}function fs(e){return _(e)?document.querySelector(e):e}function ps(){}var ms=Object.assign,hs=typeof window<`u`,gs=e=>typeof e==`object`&&!!e,_s=e=>e!=null,vs=e=>typeof e==`function`,ys=e=>gs(e)&&vs(e.then)&&vs(e.catch),bs=e=>typeof e==`number`||/^\d+(\.\d+)?$/.test(e),xs=()=>hs?/ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()):!1;function Ss(e,t){let n=t.split(`.`),r=e;return n.forEach(e=>{var t;r=gs(r)?(t=r[e])==null?``:t:``}),r}function Cs(e,t,n){return t.reduce((t,r)=>((!n||e[r]!==void 0)&&(t[r]=e[r]),t),{})}var ws=e=>Array.isArray(e)?e:[e],Ts=[Number,String],Es={type:Boolean,default:!0},Ds=e=>({type:e,required:!0}),Os=e=>({type:Number,default:e}),ks=e=>({type:Ts,default:e}),As=e=>({type:String,default:e}),js=typeof window<`u`;function Ms(e){return js?requestAnimationFrame(e):-1}function Ns(e){js&&cancelAnimationFrame(e)}function Ps(e){Ms(()=>Ms(e))}var Fs=e=>e===window,Is=(e,t)=>({top:0,left:0,right:e,bottom:t,width:e,height:t}),Ls=e=>{let t=L(e);if(Fs(t)){let e=t.innerWidth,n=t.innerHeight;return Is(e,n)}return t!=null&&t.getBoundingClientRect?t.getBoundingClientRect():Is(0,0)};function Rs(e){let t=V(e,null);if(t){let e=Pa(),{link:n,unlink:r,internalChildren:i}=t;return n(e),Fr(()=>r(e)),{parent:t,index:Z(()=>i.indexOf(e))}}return{parent:null,index:Wt(-1)}}function zs(e){let t=[],n=e=>{Array.isArray(e)&&e.forEach(e=>{var r;_a(e)&&(t.push(e),(r=e.component)!=null&&r.subTree&&(t.push(e.component.subTree),n(e.component.subTree.children)),e.children&&n(e.children))})};return n(e),t}var Bs=(e,t)=>{let n=e.indexOf(t);return n===-1?e.findIndex(e=>t.key!==void 0&&t.key!==null&&e.type===t.type&&e.key===t.key):n};function Vs(e,t,n){let r=zs(e.subTree.children);n.sort((e,t)=>Bs(r,e.vnode)-Bs(r,t.vnode));let i=n.map(e=>e.proxy);t.sort((e,t)=>i.indexOf(e)-i.indexOf(t))}function Hs(e){let t=Pt([]),n=Pt([]),r=Pa();return{children:t,linkChildren:i=>{An(e,Object.assign({link:e=>{e.proxy&&(n.push(e),t.push(e.proxy),Vs(r,t,n))},unlink:e=>{let r=n.indexOf(e);t.splice(r,1),n.splice(r,1)},children:t,internalChildren:n},i))}}}function Us(e){let t;jr(()=>{e(),gn(()=>{t=!0})}),Sr(()=>{t&&e()})}function Ws(e,t,n={}){if(!js)return;let{target:r=window,passive:i=!1,capture:a=!1}=n,o=!1,s,c=n=>{if(o)return;let r=L(n);r&&!s&&(r.addEventListener(e,t,{capture:a,passive:i}),s=!0)},l=n=>{if(o)return;let r=L(n);r&&s&&(r.removeEventListener(e,t,a),s=!1)};Fr(()=>l(r)),Cr(()=>l(r)),Us(()=>c(r));let u;return I(r)&&(u=Fn(r,(e,t)=>{l(t),c(e)})),()=>{u==null||u(),l(r),o=!0}}var Gs,Ks;function qs(){if(!Gs&&(Gs=Wt(0),Ks=Wt(0),js)){let e=()=>{Gs.value=window.innerWidth,Ks.value=window.innerHeight};e(),window.addEventListener(`resize`,e,{passive:!0}),window.addEventListener(`orientationchange`,e,{passive:!0})}return{width:Gs,height:Ks}}var Js=/scroll|auto|overlay/i,Ys=js?window:void 0;function Xs(e){return e.tagName!==`HTML`&&e.tagName!==`BODY`&&e.nodeType===1}function Zs(e,t=Ys){let n=e;for(;n&&n!==t&&Xs(n);){let{overflowY:e}=window.getComputedStyle(n);if(Js.test(e))return n;n=n.parentNode}return t}function Qs(e,t=Ys){let n=Wt();return jr(()=>{e.value&&(n.value=Zs(e.value,t))}),n}var $s;function ec(){if(!$s&&($s=Wt(`visible`),js)){let e=()=>{$s.value=document.hidden?`hidden`:`visible`};e(),window.addEventListener(`visibilitychange`,e)}return $s}var tc=Symbol(`van-field`);function nc(e){let t=`scrollTop`in e?e.scrollTop:e.pageYOffset;return Math.max(t,0)}function rc(e,t){`scrollTop`in e?e.scrollTop=t:e.scrollTo(e.scrollX,t)}function ic(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}function ac(e){rc(window,e),rc(document.body,e)}function oc(e,t){if(e===window)return 0;let n=t?nc(t):ic();return Ls(e).top+n}var sc=xs();function cc(){sc&&ac(ic())}var lc=e=>e.stopPropagation();function uc(e,t){(typeof e.cancelable!=`boolean`||e.cancelable)&&e.preventDefault(),t&&lc(e)}function dc(e){let t=L(e);if(!t)return!1;let n=window.getComputedStyle(t),r=n.display===`none`,i=t.offsetParent===null&&n.position!==`fixed`;return r||i}var{width:fc,height:pc}=qs();function mc(e){if(_s(e))return bs(e)?`${e}px`:String(e)}function hc(e){if(_s(e)){if(Array.isArray(e))return{width:mc(e[0]),height:mc(e[1])};let t=mc(e);return{width:t,height:t}}}function gc(e){let t={};return e!==void 0&&(t.zIndex=+e),t}var _c;function vc(){if(!_c){let e=document.documentElement,t=e.style.fontSize||window.getComputedStyle(e).fontSize;_c=parseFloat(t)}return _c}function yc(e){return e=e.replace(/rem/g,``),+e*vc()}function bc(e){return e=e.replace(/vw/g,``),+e*fc.value/100}function xc(e){return e=e.replace(/vh/g,``),+e*pc.value/100}function Sc(e){if(typeof e==`number`)return e;if(hs){if(e.includes(`rem`))return yc(e);if(e.includes(`vw`))return bc(e);if(e.includes(`vh`))return xc(e)}return parseFloat(e)}var Cc=/-(\w)/g,wc=e=>e.replace(Cc,(e,t)=>t.toUpperCase()),Tc=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase().replace(/^-/,``),Ec=(e,t,n)=>Math.min(Math.max(e,t),n);function Dc(e,t,n){let r=e.indexOf(t);return r===-1?e:t===`-`&&r!==0?e.slice(0,r):e.slice(0,r+1)+e.slice(r).replace(n,``)}function Oc(e,t=!0,n=!0){e=t?Dc(e,`.`,/\./g):e.split(`.`)[0],e=n?Dc(e,`-`,/-/g):e.replace(/-/,``);let r=t?/[^-0-9.]/g:/[^-0-9]/g;return e.replace(r,``)}var{hasOwnProperty:kc}=Object.prototype;function Ac(e,t,n){let r=t[n];_s(r)&&(e[n]=!kc.call(e,n)||!gs(r)?r:jc(Object(e[n]),r))}function jc(e,t){return Object.keys(t).forEach(n=>{Ac(e,t,n)}),e}var Mc={name:`姓名`,tel:`电话`,save:`保存`,clear:`清空`,undo:`撤销`,cancel:`取消`,confirm:`确认`,delete:`删除`,loading:`加载中...`,noCoupon:`暂无优惠券`,nameEmpty:`请填写姓名`,addContact:`添加联系人`,telInvalid:`请填写正确的电话`,vanCalendar:{end:`结束`,start:`开始`,title:`日期选择`,weekdays:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],monthTitle:(e,t)=>`${e}\u5E74${t}\u6708`,rangePrompt:e=>`\u6700\u591A\u9009\u62E9 ${e} \u5929`},vanCascader:{select:`请选择`},vanPagination:{prev:`上一页`,next:`下一页`},vanPullRefresh:{pulling:`下拉即可刷新...`,loosing:`释放即可刷新...`},vanSubmitBar:{label:`合计:`},vanCoupon:{unlimited:`无门槛`,discount:e=>`${e}\u6298`,condition:e=>`\u6EE1${e}\u5143\u53EF\u7528`},vanCouponCell:{title:`优惠券`,count:e=>`${e}\u5F20\u53EF\u7528`},vanCouponList:{exchange:`兑换`,close:`不使用`,enable:`可用`,disabled:`不可用`,placeholder:`输入优惠码`},vanAddressEdit:{area:`地区`,areaEmpty:`请选择地区`,addressEmpty:`请填写详细地址`,addressDetail:`详细地址`,defaultAddress:`设为默认收货地址`},vanAddressList:{add:`新增地址`}},Nc=Wt(`zh-CN`),Pc=Pt({"zh-CN":Mc}),Fc={messages(){return Pc[Nc.value]},use(e,t){Nc.value=e,this.add({[e]:t})},add(e={}){jc(Pc,e)}};function Ic(e){let t=wc(e)+`.`;return(e,...n)=>{let r=Fc.messages(),i=Ss(r,t+e)||Ss(r,e);return vs(i)?i(...n):i}}function Lc(e,t){return t?typeof t==`string`?` ${e}--${t}`:Array.isArray(t)?t.reduce((t,n)=>t+Lc(e,n),``):Object.keys(t).reduce((n,r)=>n+(t[r]?Lc(e,r):``),``):``}function Rc(e){return(t,n)=>(t&&typeof t!=`string`&&(n=t,t=``),t=t?`${e}__${t}`:e,`${t}${Lc(t,n)}`)}function zc(e){let t=`van-${e}`;return[t,Rc(t),Ic(t)]}function Bc(e){return e.install=t=>{let{name:n}=e;n&&(t.component(n,e),t.component(wc(`-${n}`),e))},e}var Vc=2e3,Hc=()=>++Vc,Uc=e=>{Vc=e},[Wc,Gc]=zc(`config-provider`),Kc=Symbol(Wc),qc={tag:As(`div`),theme:As(`light`),zIndex:Number,themeVars:Object,themeVarsDark:Object,themeVarsLight:Object,themeVarsScope:As(`local`),iconPrefix:String};function Jc(e){return e.replace(/([a-zA-Z])(\d)/g,`$1-$2`)}function Yc(e){let t={};return Object.keys(e).forEach(n=>{let r=Jc(Tc(n));t[`--van-${r}`]=e[n]}),t}function Xc(e={},t={}){Object.keys(e).forEach(n=>{e[n]!==t[n]&&document.documentElement.style.setProperty(n,e[n])}),Object.keys(t).forEach(t=>{e[t]||document.documentElement.style.removeProperty(t)})}var Zc=Bc(fr({name:Wc,props:qc,setup(e,{slots:t}){let n=Z(()=>Yc(ms({},e.themeVars,e.theme===`dark`?e.themeVarsDark:e.themeVarsLight)));if(hs){let t=()=>{document.documentElement.classList.add(`van-theme-${e.theme}`)},r=(t=e.theme)=>{document.documentElement.classList.remove(`van-theme-${t}`)};Fn(()=>e.theme,(e,n)=>{n&&r(n),t()},{immediate:!0}),Sr(t),Cr(r),Pr(r),Fn(n,(t,n)=>{e.themeVarsScope===`global`&&Xc(t,n)}),Fn(()=>e.themeVarsScope,(e,t)=>{t===`global`&&Xc({},n.value),e===`global`&&Xc(n.value,{})}),e.themeVarsScope===`global`&&Xc(n.value,{})}return An(Kc,e),Nn(()=>{e.zIndex!==void 0&&Uc(e.zIndex)}),()=>J(e.tag,{class:Gc(),style:e.themeVarsScope===`local`?n.value:void 0},{default:()=>{var e;return[(e=t.default)==null?void 0:e.call(t)]}})}}));function Qc(e=`(min-width: 768px)`){let t=typeof window<`u`&&window.matchMedia?window.matchMedia(e):null,n=Wt(t?t.matches:!1),r=e=>{n.value=e.matches};return jr(()=>{var e;return t==null||(e=t.addEventListener)==null?void 0:e.call(t,`change`,r)}),Pr(()=>{var e;return t==null||(e=t.removeEventListener)==null?void 0:e.call(t,`change`,r)}),n}function $c(){let e=typeof window<`u`&&window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,t=Wt(e?e.matches:!1),n=e=>{t.value=e.matches};return jr(()=>{var t;return e==null||(t=e.addEventListener)==null?void 0:t.call(e,`change`,n)}),Pr(()=>{var t;return e==null||(t=e.removeEventListener)==null?void 0:t.call(e,`change`,n)}),t}function el(e,t){let n,r=!1,i=()=>{n!==void 0&&window.clearInterval(n),n=void 0},a=()=>{i(),n=window.setInterval(()=>{document.visibilityState===`visible`&&e()},t)},o=()=>{r&&(document.visibilityState===`visible`?(e(),a()):i())};jr(()=>{r=!0,a(),document.addEventListener(`visibilitychange`,o)}),Sr(()=>{r||(r=!0,e(),a())}),Cr(()=>{r=!1,i()}),Pr(()=>{r=!1,i(),document.removeEventListener(`visibilitychange`,o)})}var tl=fr({__name:`App`,setup(e){let t=$c(),n=Z(()=>t.value?`dark`:`light`),r={primaryColor:`#2f6fff`,successColor:`#13b77a`,warningColor:`#e8870c`,dangerColor:`#ff5358`,navBarBackground:`var(--app-bg)`,navBarTitleTextColor:`var(--app-text)`,cellGroupInsetRadius:`14px`,buttonRadius:`12px`,buttonLargeHeight:`48px`},i={primaryColor:`#5b8cff`,successColor:`#2fd19a`,warningColor:`#ffae4a`,dangerColor:`#ff6b70`,background:`#0b1222`,background2:`#131c31`,background3:`#18223b`,textColor:`#e6ecfb`,textColor2:`#8b97b6`,textColor3:`#6c7898`,borderColor:`rgba(140,160,210,.16)`,activeColor:`#1c2743`};return(e,t)=>{let a=Vr(`RouterView`);return ua(),ga(L(Zc),{theme:n.value,"theme-vars-scope":`global`,"theme-vars":r,"theme-vars-dark":i},{default:Dn(()=>[J(a)]),_:1},8,[`theme`])}}});function nl(e){"@babel/helpers - typeof";return nl=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},nl(e)}function rl(e,t){if(nl(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(nl(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function il(e){var t=rl(e,`string`);return nl(t)==`symbol`?t:t+``}function al(e,t,n){return(t=il(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ol(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function sl(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&ol(e.default)}var Q=Object.assign;function cl(e,t){let n={};for(let r in t){let i=t[r];n[r]=ul(i)?i.map(e):e(i)}return n}var ll=()=>{},ul=Array.isArray;function dl(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var fl=Symbol(``);function pl(e,t){return Q(Error(),{type:e,[fl]:!0},t)}function ml(e,t){return e instanceof Error&&fl in e&&(t==null||!!(e.type&t))}var hl=Symbol(``),gl=Symbol(``),_l=Symbol(``),vl=Symbol(``),yl=Symbol(``);function bl(){return V(_l)}function xl(e){return V(vl)}var Sl=typeof document<`u`,Cl=/#/g,wl=/&/g,Tl=/\//g,El=/=/g,Dl=/\?/g,Ol=/\+/g,kl=/%5B/g,Al=/%5D/g,jl=/%5E/g,Ml=/%60/g,Nl=/%7B/g,Pl=/%7C/g,Fl=/%7D/g,Il=/%20/g;function Ll(e){return e==null?``:encodeURI(``+e).replace(Pl,`|`).replace(kl,`[`).replace(Al,`]`)}function Rl(e){return Ll(e).replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function zl(e){return Ll(e).replace(Ol,`%2B`).replace(Il,`+`).replace(Cl,`%23`).replace(wl,`%26`).replace(Ml,"`").replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function Bl(e){return zl(e).replace(El,`%3D`)}function Vl(e){return Ll(e).replace(Cl,`%23`).replace(Dl,`%3F`)}function Hl(e){return Vl(e).replace(Tl,`%2F`)}function Ul(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var Wl=/\/$/,Gl=e=>e.replace(Wl,``);function Kl(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r=r||t.slice(0,s),o=t.slice(s,t.length)),r=eu(r==null?t:r,n),{fullPath:r+a+o,path:r,query:i,hash:Ul(o)}}function ql(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Jl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function Yl(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&Xl(t.matched[r],n.matched[i])&&Zl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Xl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Zl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ql(e[n],t[n]))return!1;return!0}function Ql(e,t){return ul(e)?$l(e,t):ul(t)?$l(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function $l(e,t){return ul(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function eu(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o1&&a--;else break}return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var tu={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0};function nu(e){if(!e){if(Sl){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^/]+/,``)}else e=`/`}return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Gl(e)}var ru=/^[^#]+#/;function iu(e,t){return e.replace(ru,`#`)+t}function au(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var ou=()=>({left:window.scrollX,top:window.scrollY});function su(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=au(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function cu(e,t){return(history.state?history.state.position-t:-1)+e}var lu=new Map;function uu(e,t){lu.set(e,t)}function du(e){let t=lu.get(e);return lu.delete(e),t}function fu(e){return typeof e==`string`||e&&typeof e==`object`}function pu(e){return typeof e==`string`||typeof e==`symbol`}function mu(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;ee&&zl(e)):[r&&zl(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function gu(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=ul(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function _u(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function vu(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(pl(4,{from:n,to:t})):e instanceof Error?c(e):fu(e)?c(pl(2,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function yu(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(t===`beforeRouteEnter`||o.instances[e]){if(ol(s)){let c=(s.__vccOpts||s)[t];c&&a.push(vu(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=sl(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&vu(c,n,r,o,e,i)()}))}}}return a}function bu(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;oXl(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>Xl(e,s))||i.push(s))}return[n,r,i]}var xu=()=>location.protocol+`//`+location.host;function Su(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Jl(n,``)}return Jl(n,e)+r+i}function Cu(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=Su(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:`pop`,direction:u?u>0?`forward`:`back`:``})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(Q({},e.state,{scroll:ou()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function wu(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ou():null}}function Tu(e){let{history:t,location:n}=window,r={value:Su(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:xu()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,Q({},t.state,wu(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=Q({},i.value,t.state,{forward:e,scroll:ou()});a(o.current,o,!0),a(e,Q({},wu(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function Eu(e){e=nu(e);let t=Tu(e),n=Cu(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=Q({location:``,base:e,go:r,createHref:iu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Du(e){return e=location.host?e||location.pathname+location.search:``,e.includes(`#`)||(e+=`#`),Eu(e)}var Ou={type:0,value:``},ku=/[a-zA-Z0-9_]/;function Au(e){if(!e)return[[]];if(e===`/`)return[[Ou]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=0,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&(n===0?a.push({type:0,value:l}):n===1||n===2||n===3?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:1,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),l=``)}function f(){l+=c}for(;st.length?t.length===1&&t[0]===80?1:-1:0}function Iu(e,t){let n=0,r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}var Ru={strict:!1,end:!0,sensitive:!1};function zu(e,t,n){let r=Q(Pu(Au(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Bu(e,t){let n=[],r=new Map;t=dl(Ru,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Hu(e);s.aliasOf=r&&r.record;let l=dl(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Hu(Q({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=zu(t,n,l),r?r.alias.push(d):(f=f||d,f!==d&&f.alias.push(d),i&&e.name&&!Wu(d)&&o(e.name)),Ju(d)&&c(d),s.children){let e=s.children;for(let t=0;t{o(f)}:ll}function o(e){if(pu(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=Ku(e,n);n.splice(t,0,e),e.record.name&&!Wu(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw pl(1,{location:e});s=i.record.name,a=Q(Vu(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&Vu(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name,i.keys.forEach(e=>{e.optional&&!a[e.name]&&delete a[e.name]}));else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw pl(1,{location:e,currentLocation:t});s=i.record.name,a=Q({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Gu(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function Vu(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Hu(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Uu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Uu(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Wu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Gu(e){return e.reduce((e,t)=>Q(e,t.meta),{})}function Ku(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Iu(e,t[i])<0?r=i:n=i+1}let i=qu(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function qu(e){let t=e;for(;t=t.parent;)if(Ju(t)&&Iu(e,t)===0)return t}function Ju({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Yu(e){let t=V(_l),n=V(vl),r=Z(()=>{let n=L(e.to);return t.resolve(n)}),i=Z(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(Xl.bind(null,i));if(o>-1)return o;let s=ed(e[t-2]);return t>1&&ed(i)===s&&a[a.length-1].path!==s?a.findIndex(Xl.bind(null,e[t-2])):o}),a=Z(()=>i.value>-1&&$u(n.params,r.value.params)),o=Z(()=>i.value>-1&&i.value===n.matched.length-1&&Zl(n.params,r.value.params));function s(n={}){if(Qu(n)){let n=t[L(e.replace)?`replace`:`push`](L(e.to)).catch(ll);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:Z(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Xu(e){return e.length===1?e[0]:e}var Zu=fr({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:Yu,setup(e,{slots:t}){let n=Pt(Yu(e)),{options:r}=V(_l),i=Z(()=>({[td(e.activeClass,r.linkActiveClass,`router-link-active`)]:n.isActive,[td(e.exactActiveClass,r.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let r=t.default&&Xu(t.default(n));return e.custom?r:Xa(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}});function Qu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(e.button===void 0||e.button===0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function $u(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!ul(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function ed(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var td=(e,t,n)=>e==null?t==null?n:t:e,nd=fr({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let r=V(yl),i=Z(()=>e.route||r.value),a=V(gl,0),o=Z(()=>{let e=L(a),{matched:t}=i.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),s=Z(()=>i.value.matched[o.value]);An(gl,Z(()=>o.value+1)),An(hl,s),An(yl,i);let c=Wt();return Fn(()=>[c.value,s.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!Xl(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let r=i.value,a=e.name,o=s.value,l=o&&o.components[a];if(!l)return rd(n.default,{Component:l,route:r});let u=o.props[a],d=Xa(l,Q({},u?u===!0?r.params:typeof u==`function`?u(r):u:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(o.instances[a]=null)},ref:c}));return rd(n.default,{Component:d,route:r})||d}}});function rd(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var id=nd;function ad(e){let t=Bu(e.routes,e),n=e.parseQuery||mu,r=e.stringifyQuery||hu,i=e.history,a=_u(),o=_u(),s=_u(),c=Gt(tu),l=tu;Sl&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let u=cl.bind(null,e=>``+e),d=cl.bind(null,Hl),f=cl.bind(null,Ul);function p(e,n){let r,i;return pu(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function m(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function h(){return t.getRoutes().map(e=>e.record)}function g(e){return!!t.getRecordMatcher(e)}function _(e,a){if(a=Q({},a||c.value),typeof e==`string`){let r=Kl(n,e,a.path),o=t.resolve({path:r.path},a),s=i.createHref(r.fullPath);return Q(r,o,{params:f(o.params),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=Q({},e,{path:Kl(n,e.path,a.path).path});else{let t=Q({},e.params);for(let e in t)t[e]==null&&delete t[e];o=Q({},e,{params:d(t)}),a.params=d(a.params)}let s=t.resolve(o,a),l=e.hash||``;s.params=u(f(s.params));let p=ql(r,Q({},e,{hash:Rl(l),path:s.path})),m=i.createHref(p);return Q({fullPath:p,hash:l,query:r===hu?gu(e.query):e.query||{}},s,{redirectedFrom:void 0,href:m})}function v(e){return typeof e==`string`?Kl(n,e,c.value.path):Q({},e)}function y(e,t){if(l!==e)return pl(8,{from:t,to:e})}function b(e){return C(e)}function x(e){return b(Q(v(e),{replace:!0}))}function S(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=v(i):{path:i},i.params={}),Q({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function C(e,t){let n=l=_(e),i=c.value,a=e.state,o=e.force,s=e.replace===!0,u=S(n,i);if(u)return C(Q(v(u),{state:typeof u==`object`?Q({},a,u.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&Yl(r,i,n)&&(f=pl(16,{to:d,from:i}),oe(i,i,!0,!1)),(f?Promise.resolve(f):ee(d,i)).catch(e=>ml(e)?ml(e,2)?e:ae(e):k(e,d,i)).then(e=>{if(e){if(ml(e,2))return C(Q({replace:s},v(e.to),{state:typeof e.to==`object`?Q({},a,e.to.state):a,force:o}),t||d)}else e=te(d,i,!0,s,a);return E(d,i,e),e})}function w(e,t){let n=y(e,t);return n?Promise.reject(n):Promise.resolve()}function T(e){let t=le.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function ee(e,t){let n,[r,i,s]=bu(e,t);n=yu(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(vu(r,e,t))});let c=w.bind(null,e,t);return n.push(c),de(n).then(()=>{n=[];for(let r of a.list())n.push(vu(r,e,t));return n.push(c),de(n)}).then(()=>{n=yu(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(vu(r,e,t))});return n.push(c),de(n)}).then(()=>{n=[];for(let r of s)if(r.beforeEnter){if(ul(r.beforeEnter))for(let i of r.beforeEnter)n.push(vu(i,e,t));else n.push(vu(r.beforeEnter,e,t))}return n.push(c),de(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=yu(s,`beforeRouteEnter`,e,t,T),n.push(c),de(n))).then(()=>{n=[];for(let r of o.list())n.push(vu(r,e,t));return n.push(c),de(n)}).catch(e=>ml(e,8)?e:Promise.reject(e))}function E(e,t,n){s.list().forEach(r=>T(()=>r(e,t,n)))}function te(e,t,n,r,a){let o=y(e,t);if(o)return o;let s=t===tu,l=Sl?history.state:{};n&&(r||s?i.replace(e.fullPath,Q({scroll:s&&l&&l.scroll},a)):i.push(e.fullPath,a)),c.value=e,oe(e,t,n,s),ae()}let D;function ne(){D||(D=i.listen((e,t,n)=>{if(!ue.listening)return;let r=_(e),a=S(r,ue.currentRoute.value);if(a){C(Q(a,{replace:!0,force:!0}),r).catch(ll);return}l=r;let o=c.value;Sl&&uu(cu(o.fullPath,n.delta),ou()),ee(r,o).catch(e=>ml(e,12)?e:ml(e,2)?(C(Q(v(e.to),{force:!0}),r).then(e=>{ml(e,20)&&!n.delta&&n.type===`pop`&&i.go(-1,!1)}).catch(ll),Promise.reject()):(n.delta&&i.go(-n.delta,!1),k(e,r,o))).then(e=>{e=e||te(r,o,!1),e&&(n.delta&&!ml(e,8)?i.go(-n.delta,!1):n.type===`pop`&&ml(e,20)&&i.go(-1,!1)),E(r,o,e)}).catch(ll)}))}let O=_u(),re=_u(),ie;function k(e,t,n){ae(e);let r=re.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function A(){return ie&&c.value!==tu?Promise.resolve():new Promise((e,t)=>{O.add([e,t])})}function ae(e){return ie||(ie=!e,ne(),O.list().forEach(([t,n])=>e?n(e):t()),O.reset()),e}function oe(t,n,r,i){let{scrollBehavior:a}=e;if(!Sl||!a)return Promise.resolve();let o=!r&&du(cu(t.fullPath,0))||(i||!r)&&history.state&&history.state.scroll||null;return gn().then(()=>a(t,n,o)).then(e=>t===c.value&&e&&su(e)).catch(e=>t===c.value&&k(e,t,n))}let se=e=>i.go(e),ce,le=new Set,ue={currentRoute:c,listening:!0,addRoute:p,removeRoute:m,clearRoutes:t.clearRoutes,hasRoute:g,getRoutes:h,resolve:_,options:e,push:b,replace:x,go:se,back:()=>se(-1),forward:()=>se(1),beforeEach:a.add,beforeResolve:o.add,afterEach:s.add,onError:re.add,isReady:A,install(e){e.component(`RouterLink`,Zu),e.component(`RouterView`,id),e.config.globalProperties.$router=ue,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>L(c)}),Sl&&!ce&&c.value===tu&&(ce=!0,b(i.location).catch(e=>{}));let t={};for(let e in tu)Object.defineProperty(t,e,{get:()=>c.value[e],enumerable:!0});e.provide(_l,ue),e.provide(vl,Ft(t)),e.provide(yl,c);let n=e.unmount;le.add(e),e.unmount=function(){le.delete(e),le.size<1&&(l=tu,D&&D(),D=null,c.value=tu,ce=!1,ie=!1),n()}}};function de(e){return e.reduce((e,t)=>e.then(()=>T(t)),Promise.resolve())}return ue}var od=`/api/v2/mobile`,sd=`wecom-mobile-token`,cd=class extends Error{constructor(e,t,n,r=0){super(n),al(this,`status`,void 0),al(this,`code`,void 0),al(this,`retryAfter`,void 0),this.status=e,this.code=t,this.retryAfter=r}},ld=null;function ud(e){ld=e}function dd(){try{return window.localStorage.getItem(sd)||``}catch{return``}}function fd(e){try{e?window.localStorage.setItem(sd,e):window.localStorage.removeItem(sd)}catch{}}var pd=``;function md(){return pd||dd()}function hd(e){pd=e,fd(e)}function gd(e,t){let n=t&&typeof t==`object`&&`detail`in t?t.detail:t;if(n&&typeof n==`object`&&!Array.isArray(n)){let t=n;return new cd(e,String(t.code||`http_${e}`),String(t.message||_d(e)),Number(t.retry_after||0))}return Array.isArray(n)?new cd(e,`validation`,`填写的内容不符合要求,请检查后重试`):typeof n==`string`&&n.trim()?new cd(e,`http_${e}`,n.trim()):new cd(e,`http_${e}`,_d(e))}function _d(e){return e===401?`登录已失效,请重新登录`:e===403?`没有权限执行这个操作`:e===404?`内容不存在或已被删除`:e===429?`操作太频繁,请稍后再试`:e>=500?`服务暂时不可用,请稍后重试`:`请求失败(HTTP ${e})`}async function vd(e,t,n={}){let{body:r,query:i,auth:a=!0,signal:o}=n,s=new URLSearchParams;for(let[e,t]of Object.entries(i||{}))t!=null&&t!==``&&s.set(e,String(t));let c=`${od}${t}${s.toString()?`?${s}`:``}`,l={Accept:`application/json`};r!==void 0&&(l[`Content-Type`]=`application/json`);let u=md();a&&u&&(l.Authorization=`Bearer ${u}`);let d;try{d=await fetch(c,{method:e,headers:l,body:r===void 0?void 0:JSON.stringify(r),signal:o,cache:`no-store`,credentials:`omit`})}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?e:new cd(0,`network`,`网络连接失败,请检查网络后重试`)}let f=null,p=await d.text();if(p)try{f=JSON.parse(p)}catch{f=null}if(!d.ok){let e=gd(d.status,f);throw d.status===401&&a&&(ld==null||ld()),e}return f}var yd=e=>encodeURIComponent(e),bd={login(e,t,n){return vd(`POST`,`/auth/login`,{auth:!1,body:{account:e,password:t,client_name:n}})},logout(){return vd(`POST`,`/auth/logout`)},me(){return vd(`GET`,`/me`)},accounts(e=``){return vd(`GET`,`/accounts`,{query:{q:e}})},desktops(e){return vd(`GET`,`/desktops`,{query:{account_id:e}})},sendCommand(e,t,n){return vd(`POST`,`/desktops/${yd(e)}/commands`,{body:{type:t},query:{account_id:n}})},command(e){return vd(`GET`,`/commands/${yd(e)}`)},conversations(e){return vd(`GET`,`/conversations`,{query:{account_id:e.accountId,q:e.q,scope:e.scope,cursor:e.cursor,limit:e.limit},signal:e.signal})},block(e,t,n){return vd(`PUT`,`/blacklist/${yd(e.account)}/${yd(e.conversation)}`,{body:{name:e.name||``,account_name:e.account_name||``,reason:t},query:{account_id:n}})},unblock(e,t,n){return vd(`DELETE`,`/blacklist/${yd(e)}/${yd(t)}`,{query:{account_id:n}})}},xd=`wecom-mobile-view-account`;function Sd(){try{let e=window.localStorage.getItem(xd),t=e?JSON.parse(e):null;return t&&typeof t.id==`number`?t:null}catch{return null}}var $=Pt({token:md(),me:null,viewed:Sd()}),Cd=Z(()=>{var e;return(e=$.me)==null?void 0:e.permissions}),wd=Z(()=>{let e=$.me,t=$.viewed;if(e&&t&&e.permissions.control_all&&t.id!==e.account.id)return t.id}),Td=Z(()=>{var e,t;return wd.value!==void 0&&$.viewed?$.viewed.nickname||$.viewed.sn||`客服 #${$.viewed.id}`:((e=$.me)==null?void 0:e.account.nickname)||((t=$.me)==null?void 0:t.account.sn)||``});function Ed(e){$.viewed=e?{id:e.id,nickname:e.nickname,sn:e.sn}:null;try{$.viewed?window.localStorage.setItem(xd,JSON.stringify($.viewed)):window.localStorage.removeItem(xd)}catch{}}function Dd(e,t){hd(e),$.token=e,$.me=t,t.permissions.control_all||Ed(null)}function Od(){hd(``),$.token=``,$.me=null,Ed(null)}async function kd(){let e=await bd.me();return $.me=e,e.permissions.control_all||Ed(null),e}async function Ad(){try{await bd.logout()}catch{}Od()}var jd=`modulepreload`,Md=function(e){return`/m/`+e},Nd={},Pd=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute(`nonce`));function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=Md(t,n),t=s(t),t in Nd)return;Nd[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:jd,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Fd=ad({history:Du(),routes:[{path:`/login`,name:`login`,component:()=>Pd(()=>import(`./LoginView-CLW1WjQp.js`),__vite__mapDeps([0,1,2,3,4])),meta:{public:!0}},{path:`/`,component:()=>Pd(()=>import(`./ShellView-DMPqnSgh.js`),__vite__mapDeps([5,1,6,7])),children:[{path:``,redirect:{name:`desktops`}},{path:`desktops`,name:`desktops`,component:()=>Pd(()=>import(`./DevicesView-HbIaJI1u.js`),__vite__mapDeps([8,1,9,6,2,10,3,11,12]))},{path:`conversations`,name:`conversations`,component:()=>Pd(()=>import(`./ConversationsView-BeEp2NbK.js`),__vite__mapDeps([13,1,6,9,2,3,10,11,14]))},{path:`me`,name:`me`,component:()=>Pd(()=>import(`./MeView-DTU4-bV-.js`),__vite__mapDeps([15,1,9,6,2,16]))}]},{path:`/:pathMatch(.*)*`,redirect:{name:`desktops`}}]});Fd.beforeEach(async e=>{if(e.meta.public)return!0;if(!$.token)return{name:`login`,query:{redirect:e.fullPath}};if(!$.me)try{await kd()}catch(t){if(t instanceof cd&&t.status===401)return Od(),{name:`login`,query:{redirect:e.fullPath}}}return!0}),ud(()=>{Od();let e=Fd.currentRoute.value;e.matched.length&&e.name!==`login`&&Fd.replace({name:`login`,query:{redirect:e.fullPath,expired:`1`}})});var Id=`wecom-mobile-chunk-reload`;Fd.onError((e,t)=>{let n=String((e==null?void 0:e.message)||e);if(!/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(n))return;let r=0;try{r=Number(window.sessionStorage.getItem(Id)||0),window.sessionStorage.setItem(Id,String(Date.now()))}catch{}Date.now()-r>1e4&&(window.location.assign(`/m/#${t.fullPath}`),window.location.reload())}),us(tl).use(Fd).mount(`#app`),`serviceWorker`in navigator&&window.isSecureContext&&window.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/m/sw.js`,{scope:`/m/`}).catch(()=>void 0)});export{hs as $,ac as A,Mr as At,Hs as B,_i as Bt,Sc as C,fr as Ct,dc as D,gn as Dt,nc as E,Aa as Et,Ns as F,ua as Ft,Qs as G,Pt as Gt,ec as H,Nn as Ht,Ps as I,An as It,Ds as J,he as Jt,Os as K,Wt as Kt,Zs as L,Kr as Lt,pc as M,jr as Mt,fc as N,Fr as Nt,uc as O,Sr as Ot,tc as P,Nr as Pt,ms as Q,Us as R,Vr as Rt,gc as S,J as St,ic as T,V as Tt,Rs as U,Dn as Ut,Ws as V,Fn as Vt,Ls as W,On as Wt,Ts as X,As as Y,Se as Yt,Es as Z,zc as _,ga as _t,Ed as a,ps as at,Oc as b,qr as bt,cd as c,fo as ct,bl as d,os as dt,_s as et,el as f,oa as ft,Bc as g,xa as gt,Hc as h,Z as ht,Ad as i,ys as it,rc as j,Cr as jt,cc as k,Pr as kt,bd as l,us as lt,Kc as m,Xn as mt,$ as n,bs as nt,wd as o,Cs as ot,Qc as p,br as pt,ks as q,L as qt,Dd as r,gs as rt,Td as s,ws as st,Cd as t,vs as tt,xl as u,Ao as ut,mc as v,Ea as vt,oc as w,Pa as wt,hc as x,Ta as xt,Ec as y,ha as yt,Ms as z,Ur as zt}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css new file mode 100644 index 000000000..da7edc533 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css @@ -0,0 +1 @@ +:root,:host{--van-black:#000;--van-white:#fff;--van-gray-1:#f7f8fa;--van-gray-2:#f2f3f5;--van-gray-3:#ebedf0;--van-gray-4:#dcdee0;--van-gray-5:#c8c9cc;--van-gray-6:#969799;--van-gray-7:#646566;--van-gray-8:#323233;--van-red:#ee0a24;--van-blue:#1989fa;--van-orange:#ff976a;--van-orange-dark:#ed6a0c;--van-orange-light:#fffbe8;--van-green:#07c160;--van-gradient-red:linear-gradient(to right, #ff6034, #ee0a24);--van-gradient-orange:linear-gradient(to right, #ffd01e, #ff8917);--van-primary-color:var(--van-blue);--van-success-color:var(--van-green);--van-danger-color:var(--van-red);--van-warning-color:var(--van-orange);--van-text-color:var(--van-gray-8);--van-text-color-2:var(--van-gray-6);--van-text-color-3:var(--van-gray-5);--van-active-color:var(--van-gray-2);--van-active-opacity:.6;--van-disabled-opacity:.5;--van-background:var(--van-gray-1);--van-background-2:var(--van-white);--van-background-3:var(--van-white);--van-padding-base:4px;--van-padding-xs:8px;--van-padding-sm:12px;--van-padding-md:16px;--van-padding-lg:24px;--van-padding-xl:32px;--van-font-bold:600;--van-font-size-xs:10px;--van-font-size-sm:12px;--van-font-size-md:14px;--van-font-size-lg:16px;--van-line-height-xs:14px;--van-line-height-sm:18px;--van-line-height-md:20px;--van-line-height-lg:22px;--van-base-font:-apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;--van-price-font:avenir-heavy, "PingFang SC", helvetica neue, arial, sans-serif;--van-duration-base:.3s;--van-duration-fast:.2s;--van-ease-out:ease-out;--van-ease-in:ease-in;--van-border-color:var(--van-gray-3);--van-border-width:1px;--van-radius-sm:2px;--van-radius-md:4px;--van-radius-lg:8px;--van-radius-max:999px}.van-theme-dark{--van-text-color:#f5f5f5;--van-text-color-2:#707070;--van-text-color-3:#4d4d4d;--van-border-color:#3a3a3c;--van-active-color:#3a3a3c;--van-background:#000;--van-background-2:#1c1c1e;--van-background-3:#37363b}html{-webkit-tap-highlight-color:transparent}body{font-family:var(--van-base-font);margin:0}a{text-decoration:none}input,button,textarea{color:inherit;font:inherit}a:focus,input:focus,button:focus,textarea:focus,[class*=van-]:focus{outline:none}ol,ul{margin:0;padding:0;list-style:none}@keyframes van-slide-up-enter{0%{transform:translateY(100%)}}@keyframes van-slide-up-leave{to{transform:translateY(100%)}}@keyframes van-slide-down-enter{0%{transform:translateY(-100%)}}@keyframes van-slide-down-leave{to{transform:translateY(-100%)}}@keyframes van-slide-left-enter{0%{transform:translate(-100%)}}@keyframes van-slide-left-leave{to{transform:translate(-100%)}}@keyframes van-slide-right-enter{0%{transform:translate(100%)}}@keyframes van-slide-right-leave{to{transform:translate(100%)}}@keyframes van-fade-in{0%{opacity:0}to{opacity:1}}@keyframes van-fade-out{0%{opacity:1}to{opacity:0}}@keyframes van-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.van-fade-enter-active{animation:var(--van-duration-base) van-fade-in both var(--van-ease-out)}.van-fade-leave-active{animation:var(--van-duration-base) van-fade-out both var(--van-ease-in)}.van-slide-up-enter-active{animation:van-slide-up-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-up-leave-active{animation:van-slide-up-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-down-enter-active{animation:van-slide-down-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-down-leave-active{animation:van-slide-down-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-left-enter-active{animation:van-slide-left-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-left-leave-active{animation:van-slide-left-leave var(--van-duration-base) both var(--van-ease-in)}.van-slide-right-enter-active{animation:van-slide-right-enter var(--van-duration-base) both var(--van-ease-out)}.van-slide-right-leave-active{animation:van-slide-right-leave var(--van-duration-base) both var(--van-ease-in)}.van-clearfix:after{clear:both;content:"";display:table}.van-ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.van-multi-ellipsis--l2{text-overflow:ellipsis;-webkit-line-clamp:2;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-multi-ellipsis--l3{text-overflow:ellipsis;-webkit-line-clamp:3;line-break:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-safe-area-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-safe-area-bottom{padding-bottom:constant(safe-area-inset-bottom);padding-bottom:env(safe-area-inset-bottom)}.van-haptics-feedback{cursor:pointer}.van-haptics-feedback:active{opacity:var(--van-active-opacity)}[class*=van-hairline]:after{box-sizing:border-box;content:" ";pointer-events:none;border:0 solid var(--van-border-color);position:absolute;top:-50%;bottom:-50%;left:-50%;right:-50%;transform:scale(.5)}.van-hairline,.van-hairline--top,.van-hairline--left,.van-hairline--right,.van-hairline--bottom,.van-hairline--surround,.van-hairline--top-bottom{position:relative}.van-hairline--top:after{border-top-width:var(--van-border-width)}.van-hairline--left:after{border-left-width:var(--van-border-width)}.van-hairline--right:after{border-right-width:var(--van-border-width)}.van-hairline--bottom:after{border-bottom-width:var(--van-border-width)}.van-hairline--top-bottom:after,.van-hairline-unset--top-bottom:after{border-width:var(--van-border-width) 0}.van-hairline--surround:after{border-width:var(--van-border-width)}:root,:host{--van-badge-size:16px;--van-badge-color:var(--van-white);--van-badge-padding:0 3px;--van-badge-font-size:var(--van-font-size-sm);--van-badge-font-weight:var(--van-font-bold);--van-badge-border-width:var(--van-border-width);--van-badge-background:var(--van-danger-color);--van-badge-dot-color:var(--van-danger-color);--van-badge-dot-size:8px;--van-badge-font:-apple-system-font, helvetica neue, arial, sans-serif}.van-badge{box-sizing:border-box;min-width:var(--van-badge-size);padding:var(--van-badge-padding);color:var(--van-badge-color);font-weight:var(--van-badge-font-weight);font-size:var(--van-badge-font-size);font-family:var(--van-badge-font);text-align:center;background:var(--van-badge-background);border:var(--van-badge-border-width) solid var(--van-background-2);border-radius:var(--van-radius-max);line-height:1.2;display:inline-block}.van-badge--fixed{transform-origin:100%;position:absolute}.van-badge--top-left{top:0;left:0;transform:translate(-50%,-50%)}.van-badge--top-right{top:0;right:0;transform:translate(50%,-50%)}.van-badge--bottom-left{bottom:0;left:0;transform:translate(-50%,50%)}.van-badge--bottom-right{bottom:0;right:0;transform:translate(50%,50%)}.van-badge--dot{width:var(--van-badge-dot-size);min-width:0;height:var(--van-badge-dot-size);background:var(--van-badge-dot-color);border:none;border-radius:100%;padding:0}.van-badge__wrapper{display:inline-block;position:relative}.van-icon{font:14px/1 vant-icon;font:normal normal normal 14px/1 var(--van-icon-font-family,"vant-icon");font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;display:inline-block;position:relative}.van-icon:before{display:inline-block}.van-icon-arrow-double-left:before{content:""}.van-icon-arrow-double-right:before{content:""}.van-icon-contact:before{content:""}.van-icon-notes:before{content:""}.van-icon-records:before{content:""}.van-icon-cash-back-record:before{content:""}.van-icon-newspaper:before{content:""}.van-icon-discount:before{content:""}.van-icon-completed:before{content:""}.van-icon-user:before{content:""}.van-icon-description:before{content:""}.van-icon-list-switch:before{content:""}.van-icon-list-switching:before{content:""}.van-icon-link-o:before{content:""}.van-icon-miniprogram-o:before{content:""}.van-icon-qq:before{content:""}.van-icon-wechat-moments:before{content:""}.van-icon-weibo:before{content:""}.van-icon-cash-o:before{content:""}.van-icon-guide-o:before{content:""}.van-icon-invitation:before{content:""}.van-icon-shield-o:before{content:""}.van-icon-exchange:before{content:""}.van-icon-eye:before{content:""}.van-icon-enlarge:before{content:""}.van-icon-expand-o:before{content:""}.van-icon-eye-o:before{content:""}.van-icon-expand:before{content:""}.van-icon-filter-o:before{content:""}.van-icon-fire:before{content:""}.van-icon-fail:before{content:""}.van-icon-failure:before{content:""}.van-icon-fire-o:before{content:""}.van-icon-flag-o:before{content:""}.van-icon-font:before{content:""}.van-icon-font-o:before{content:""}.van-icon-gem-o:before{content:""}.van-icon-flower-o:before{content:""}.van-icon-gem:before{content:""}.van-icon-gift-card:before{content:""}.van-icon-friends:before{content:""}.van-icon-friends-o:before{content:""}.van-icon-gold-coin:before{content:""}.van-icon-gold-coin-o:before{content:""}.van-icon-good-job-o:before{content:""}.van-icon-gift:before{content:""}.van-icon-gift-o:before{content:""}.van-icon-gift-card-o:before{content:""}.van-icon-good-job:before{content:""}.van-icon-home-o:before{content:""}.van-icon-goods-collect:before{content:""}.van-icon-graphic:before{content:""}.van-icon-goods-collect-o:before{content:""}.van-icon-hot-o:before{content:""}.van-icon-info:before{content:""}.van-icon-hotel-o:before{content:""}.van-icon-info-o:before{content:""}.van-icon-hot-sale-o:before{content:""}.van-icon-hot:before{content:""}.van-icon-like:before{content:""}.van-icon-idcard:before{content:""}.van-icon-like-o:before{content:""}.van-icon-hot-sale:before{content:""}.van-icon-location-o:before{content:""}.van-icon-location:before{content:""}.van-icon-label:before{content:""}.van-icon-lock:before{content:""}.van-icon-label-o:before{content:""}.van-icon-map-marked:before{content:""}.van-icon-logistics:before{content:""}.van-icon-manager:before{content:""}.van-icon-more:before{content:""}.van-icon-live:before{content:""}.van-icon-manager-o:before{content:""}.van-icon-medal:before{content:""}.van-icon-more-o:before{content:""}.van-icon-music-o:before{content:""}.van-icon-music:before{content:""}.van-icon-new-arrival-o:before{content:""}.van-icon-medal-o:before{content:""}.van-icon-new-o:before{content:""}.van-icon-free-postage:before{content:""}.van-icon-newspaper-o:before{content:""}.van-icon-new-arrival:before{content:""}.van-icon-minus:before{content:""}.van-icon-orders-o:before{content:""}.van-icon-new:before{content:""}.van-icon-paid:before{content:""}.van-icon-notes-o:before{content:""}.van-icon-other-pay:before{content:""}.van-icon-pause-circle:before{content:""}.van-icon-pause:before{content:""}.van-icon-pause-circle-o:before{content:""}.van-icon-peer-pay:before{content:""}.van-icon-pending-payment:before{content:""}.van-icon-passed:before{content:""}.van-icon-plus:before{content:""}.van-icon-phone-circle-o:before{content:""}.van-icon-phone-o:before{content:""}.van-icon-printer:before{content:""}.van-icon-photo-fail:before{content:""}.van-icon-phone:before{content:""}.van-icon-photo-o:before{content:""}.van-icon-play-circle:before{content:""}.van-icon-play:before{content:""}.van-icon-phone-circle:before{content:""}.van-icon-point-gift-o:before{content:""}.van-icon-point-gift:before{content:""}.van-icon-play-circle-o:before{content:""}.van-icon-shrink:before{content:""}.van-icon-photo:before{content:""}.van-icon-qr:before{content:""}.van-icon-qr-invalid:before{content:""}.van-icon-question-o:before{content:""}.van-icon-revoke:before{content:""}.van-icon-replay:before{content:""}.van-icon-service:before{content:""}.van-icon-question:before{content:""}.van-icon-search:before{content:""}.van-icon-refund-o:before{content:""}.van-icon-service-o:before{content:""}.van-icon-scan:before{content:""}.van-icon-share:before{content:""}.van-icon-send-gift-o:before{content:""}.van-icon-share-o:before{content:""}.van-icon-setting:before{content:""}.van-icon-points:before{content:""}.van-icon-photograph:before{content:""}.van-icon-shop:before{content:""}.van-icon-shop-o:before{content:""}.van-icon-shop-collect-o:before{content:""}.van-icon-shop-collect:before{content:""}.van-icon-smile:before{content:""}.van-icon-shopping-cart-o:before{content:""}.van-icon-sign:before{content:""}.van-icon-sort:before{content:""}.van-icon-star-o:before{content:""}.van-icon-smile-comment-o:before{content:""}.van-icon-stop:before{content:""}.van-icon-stop-circle-o:before{content:""}.van-icon-smile-o:before{content:""}.van-icon-star:before{content:""}.van-icon-success:before{content:""}.van-icon-stop-circle:before{content:""}.van-icon-records-o:before{content:""}.van-icon-shopping-cart:before{content:""}.van-icon-tosend:before{content:""}.van-icon-todo-list:before{content:""}.van-icon-thumb-circle-o:before{content:""}.van-icon-thumb-circle:before{content:""}.van-icon-umbrella-circle:before{content:""}.van-icon-underway:before{content:""}.van-icon-upgrade:before{content:""}.van-icon-todo-list-o:before{content:""}.van-icon-tv-o:before{content:""}.van-icon-underway-o:before{content:""}.van-icon-user-o:before{content:""}.van-icon-vip-card-o:before{content:""}.van-icon-vip-card:before{content:""}.van-icon-send-gift:before{content:""}.van-icon-wap-home:before{content:""}.van-icon-wap-nav:before{content:""}.van-icon-volume-o:before{content:""}.van-icon-video:before{content:""}.van-icon-wap-home-o:before{content:""}.van-icon-volume:before{content:""}.van-icon-warning:before{content:""}.van-icon-weapp-nav:before{content:""}.van-icon-wechat-pay:before{content:""}.van-icon-warning-o:before{content:""}.van-icon-wechat:before{content:""}.van-icon-setting-o:before{content:""}.van-icon-youzan-shield:before{content:""}.van-icon-warn-o:before{content:""}.van-icon-smile-comment:before{content:""}.van-icon-user-circle-o:before{content:""}.van-icon-video-o:before{content:""}.van-icon-add-square:before{content:""}.van-icon-add:before{content:""}.van-icon-arrow-down:before{content:""}.van-icon-arrow-up:before{content:""}.van-icon-arrow:before{content:""}.van-icon-after-sale:before{content:""}.van-icon-add-o:before{content:""}.van-icon-alipay:before{content:""}.van-icon-ascending:before{content:""}.van-icon-apps-o:before{content:""}.van-icon-aim:before{content:""}.van-icon-award:before{content:""}.van-icon-arrow-left:before{content:""}.van-icon-award-o:before{content:""}.van-icon-audio:before{content:""}.van-icon-bag-o:before{content:""}.van-icon-balance-list:before{content:""}.van-icon-back-top:before{content:""}.van-icon-bag:before{content:""}.van-icon-balance-pay:before{content:""}.van-icon-balance-o:before{content:""}.van-icon-bar-chart-o:before{content:""}.van-icon-bars:before{content:""}.van-icon-balance-list-o:before{content:""}.van-icon-birthday-cake-o:before{content:""}.van-icon-bookmark:before{content:""}.van-icon-bill:before{content:""}.van-icon-bell:before{content:""}.van-icon-browsing-history-o:before{content:""}.van-icon-browsing-history:before{content:""}.van-icon-bookmark-o:before{content:""}.van-icon-bulb-o:before{content:""}.van-icon-bullhorn-o:before{content:""}.van-icon-bill-o:before{content:""}.van-icon-calendar-o:before{content:""}.van-icon-brush-o:before{content:""}.van-icon-card:before{content:""}.van-icon-cart-o:before{content:""}.van-icon-cart-circle:before{content:""}.van-icon-cart-circle-o:before{content:""}.van-icon-cart:before{content:""}.van-icon-cash-on-deliver:before{content:""}.van-icon-cash-back-record-o:before{content:""}.van-icon-cashier-o:before{content:""}.van-icon-chart-trending-o:before{content:""}.van-icon-certificate:before{content:""}.van-icon-chat:before{content:""}.van-icon-clear:before{content:""}.van-icon-chat-o:before{content:""}.van-icon-checked:before{content:""}.van-icon-clock:before{content:""}.van-icon-clock-o:before{content:""}.van-icon-close:before{content:""}.van-icon-closed-eye:before{content:""}.van-icon-circle:before{content:""}.van-icon-cluster-o:before{content:""}.van-icon-column:before{content:""}.van-icon-comment-circle-o:before{content:""}.van-icon-cluster:before{content:""}.van-icon-comment:before{content:""}.van-icon-comment-o:before{content:""}.van-icon-comment-circle:before{content:""}.van-icon-completed-o:before{content:""}.van-icon-credit-pay:before{content:""}.van-icon-coupon:before{content:""}.van-icon-debit-pay:before{content:""}.van-icon-coupon-o:before{content:""}.van-icon-contact-o:before{content:""}.van-icon-descending:before{content:""}.van-icon-desktop-o:before{content:""}.van-icon-diamond-o:before{content:""}.van-icon-description-o:before{content:""}.van-icon-delete:before{content:""}.van-icon-diamond:before{content:""}.van-icon-delete-o:before{content:""}.van-icon-cross:before{content:""}.van-icon-edit:before{content:""}.van-icon-ellipsis:before{content:""}.van-icon-down:before{content:""}.van-icon-discount-o:before{content:""}.van-icon-ecard-pay:before{content:""}.van-icon-envelop-o:before{content:""}@font-face{font-weight:400;font-family:vant-icon;font-style:normal;font-display:auto;src:url(data:font/woff2;charset=utf-8;base64,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)format("woff2")}.van-icon__image{object-fit:contain;width:1em;height:1em;display:block}:root,:host{--van-nav-bar-height:46px;--van-nav-bar-background:var(--van-background-2);--van-nav-bar-arrow-size:16px;--van-nav-bar-icon-color:var(--van-primary-color);--van-nav-bar-text-color:var(--van-primary-color);--van-nav-bar-title-font-size:var(--van-font-size-lg);--van-nav-bar-title-text-color:var(--van-text-color);--van-nav-bar-z-index:1;--van-nav-bar-disabled-opacity:var(--van-disabled-opacity)}.van-nav-bar{z-index:var(--van-nav-bar-z-index);line-height:var(--van-line-height-lg);text-align:center;background:var(--van-nav-bar-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.van-nav-bar--fixed{width:100%;position:fixed;top:0;left:0}.van-nav-bar--safe-area-inset-top{padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top)}.van-nav-bar .van-icon{color:var(--van-nav-bar-icon-color)}.van-nav-bar__content{height:var(--van-nav-bar-height);align-items:center;display:flex;position:relative}.van-nav-bar__arrow{margin-right:var(--van-padding-base);font-size:var(--van-nav-bar-arrow-size)}.van-nav-bar__title{max-width:60%;color:var(--van-nav-bar-title-text-color);font-weight:var(--van-font-bold);font-size:var(--van-nav-bar-title-font-size);margin:0 auto}.van-nav-bar__left,.van-nav-bar__right{padding:0 var(--van-padding-md);font-size:var(--van-font-size-md);align-items:center;display:flex;position:absolute;top:0;bottom:0}.van-nav-bar__left--disabled,.van-nav-bar__right--disabled{cursor:not-allowed;opacity:var(--van-nav-bar-disabled-opacity)}.van-nav-bar__left{left:0}.van-nav-bar__right{right:0}.van-nav-bar__text{color:var(--van-nav-bar-text-color)}:root,:host{--van-tabbar-height:50px;--van-tabbar-z-index:1;--van-tabbar-background:var(--van-background-2)}.van-tabbar{z-index:var(--van-tabbar-z-index);box-sizing:content-box;width:100%;height:var(--van-tabbar-height);background:var(--van-tabbar-background);display:flex}.van-tabbar--fixed{position:fixed;bottom:0;left:0}:root,:host{--van-tabbar-item-font-size:var(--van-font-size-sm);--van-tabbar-item-text-color:var(--van-text-color);--van-tabbar-item-active-color:var(--van-primary-color);--van-tabbar-item-active-background:var(--van-background-2);--van-tabbar-item-line-height:1;--van-tabbar-item-icon-size:22px;--van-tabbar-item-icon-margin-bottom:var(--van-padding-base)}.van-tabbar-item{color:var(--van-tabbar-item-text-color);font-size:var(--van-tabbar-item-font-size);line-height:var(--van-tabbar-item-line-height);cursor:pointer;flex-direction:column;flex:1;justify-content:center;align-items:center;display:flex}.van-tabbar-item__icon{margin-bottom:var(--van-tabbar-item-icon-margin-bottom);font-size:var(--van-tabbar-item-icon-size)}.van-tabbar-item__icon .van-icon{display:block}.van-tabbar-item__icon .van-badge{margin-top:var(--van-padding-base)}.van-tabbar-item__icon img{height:20px;display:block}.van-tabbar-item--active{color:var(--van-tabbar-item-active-color);background-color:var(--van-tabbar-item-active-background)}:root,:host{--van-cell-font-size:var(--van-font-size-md);--van-cell-line-height:24px;--van-cell-vertical-padding:10px;--van-cell-horizontal-padding:var(--van-padding-md);--van-cell-text-color:var(--van-text-color);--van-cell-background:var(--van-background-2);--van-cell-border-color:var(--van-border-color);--van-cell-active-color:var(--van-active-color);--van-cell-required-color:var(--van-danger-color);--van-cell-label-color:var(--van-text-color-2);--van-cell-label-font-size:var(--van-font-size-sm);--van-cell-label-line-height:var(--van-line-height-sm);--van-cell-label-margin-top:var(--van-padding-base);--van-cell-value-color:var(--van-text-color-2);--van-cell-value-font-size:inherit;--van-cell-icon-size:16px;--van-cell-right-icon-color:var(--van-gray-6);--van-cell-large-vertical-padding:var(--van-padding-sm);--van-cell-large-title-font-size:var(--van-font-size-lg);--van-cell-large-label-font-size:var(--van-font-size-md);--van-cell-large-value-font-size:inherit}.van-cell{box-sizing:border-box;width:100%;padding:var(--van-cell-vertical-padding) var(--van-cell-horizontal-padding);color:var(--van-cell-text-color);font-size:var(--van-cell-font-size);line-height:var(--van-cell-line-height);background:var(--van-cell-background);display:flex;position:relative;overflow:hidden}.van-cell:after{box-sizing:border-box;content:" ";pointer-events:none;right:var(--van-padding-md);bottom:0;left:var(--van-padding-md);border-bottom:1px solid var(--van-cell-border-color);position:absolute;transform:scaleY(.5)}.van-cell:last-child:after,.van-cell--borderless:after{display:none}.van-cell__label{margin-top:var(--van-cell-label-margin-top);color:var(--van-cell-label-color);font-size:var(--van-cell-label-font-size);line-height:var(--van-cell-label-line-height)}.van-cell__title,.van-cell__value{flex:1}.van-cell__value{color:var(--van-cell-value-color);font-size:var(--van-cell-value-font-size);text-align:right;vertical-align:middle;word-wrap:break-word;position:relative;overflow:hidden}.van-cell__left-icon,.van-cell__right-icon{height:var(--van-cell-line-height);font-size:var(--van-cell-icon-size);line-height:var(--van-cell-line-height)}.van-cell__left-icon{margin-right:var(--van-padding-base)}.van-cell__right-icon{margin-left:var(--van-padding-base);color:var(--van-cell-right-icon-color)}.van-cell--clickable{cursor:pointer}.van-cell--clickable:active{background-color:var(--van-cell-active-color)}.van-cell--required{overflow:visible}.van-cell--required:before{left:var(--van-padding-xs);color:var(--van-cell-required-color);font-size:var(--van-cell-font-size);content:"*";position:absolute}.van-cell--center{align-items:center}.van-cell--large{padding-top:var(--van-cell-large-vertical-padding);padding-bottom:var(--van-cell-large-vertical-padding)}.van-cell--large .van-cell__title{font-size:var(--van-cell-large-title-font-size)}.van-cell--large .van-cell__label{font-size:var(--van-cell-large-label-font-size)}.van-cell--large .van-cell__value{font-size:var(--van-cell-large-value-font-size)}:root,:host{--van-field-label-width:6.2em;--van-field-label-color:var(--van-text-color);--van-field-label-margin-right:var(--van-padding-sm);--van-field-input-text-color:var(--van-text-color);--van-field-input-error-text-color:var(--van-danger-color);--van-field-input-disabled-text-color:var(--van-text-color-3);--van-field-placeholder-text-color:var(--van-text-color-3);--van-field-icon-size:18px;--van-field-clear-icon-size:18px;--van-field-clear-icon-color:var(--van-gray-5);--van-field-right-icon-color:var(--van-gray-6);--van-field-error-message-color:var(--van-danger-color);--van-field-error-message-font-size:12px;--van-field-text-area-min-height:60px;--van-field-word-limit-color:var(--van-gray-7);--van-field-word-limit-font-size:var(--van-font-size-sm);--van-field-word-limit-line-height:16px;--van-field-disabled-text-color:var(--van-text-color-3);--van-field-required-mark-color:var(--van-red)}.van-field{flex-wrap:wrap}.van-field__label{box-sizing:border-box;width:var(--van-field-label-width);margin-right:var(--van-field-label-margin-right);color:var(--van-field-label-color);text-align:left;word-wrap:break-word;flex:none}.van-field__label--center{text-align:center}.van-field__label--right{text-align:right}.van-field__label--top{text-align:left;width:100%;margin-bottom:var(--van-padding-base);overflow-wrap:break-word;display:flex}.van-field__label--required:before{color:var(--van-field-required-mark-color);content:"*";margin-right:2px}.van-field--disabled .van-field__label{color:var(--van-field-disabled-text-color)}.van-field__value{overflow:visible}.van-field__body{align-items:center;display:flex}.van-field__control{box-sizing:border-box;width:100%;min-width:0;color:var(--van-field-input-text-color);line-height:inherit;text-align:left;resize:none;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto;background-color:#0000;border:0;margin:0;padding:0;display:block}.van-field__control::placeholder{color:var(--van-field-placeholder-text-color)}.van-field__control:-moz-read-only{cursor:default}.van-field__control:read-only{cursor:default}.van-field__control:disabled{color:var(--van-field-input-disabled-text-color);cursor:not-allowed;opacity:1;-webkit-text-fill-color:var(--van-field-input-disabled-text-color)}.van-field__control--center{text-align:center;justify-content:center}.van-field__control--right{text-align:right;justify-content:flex-end}.van-field__control--custom{min-height:var(--van-cell-line-height);align-items:center;display:flex}.van-field__control--error::-webkit-input-placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--error,.van-field__control--error::placeholder{color:var(--van-field-input-error-text-color);-webkit-text-fill-color:currentColor}.van-field__control--min-height{min-height:var(--van-field-text-area-min-height)}.van-field__control[type=date],.van-field__control[type=time],.van-field__control[type=datetime-local]{min-height:var(--van-cell-line-height)}.van-field__control[type=search]{-webkit-appearance:none}.van-field__clear,.van-field__icon,.van-field__button,.van-field__right-icon{flex-shrink:0}.van-field__clear,.van-field__right-icon{margin-right:calc(var(--van-padding-xs) * -1);padding:0 var(--van-padding-xs);line-height:inherit}.van-field__clear{color:var(--van-field-clear-icon-color);font-size:var(--van-field-clear-icon-size);cursor:pointer}.van-field__left-icon .van-icon,.van-field__right-icon .van-icon{font-size:var(--van-field-icon-size);line-height:inherit;display:block}.van-field__left-icon{margin-right:var(--van-padding-base)}.van-field__right-icon{color:var(--van-field-right-icon-color)}.van-field__button{padding-left:var(--van-padding-xs)}.van-field__error-message{color:var(--van-field-error-message-color);font-size:var(--van-field-error-message-font-size);text-align:left}.van-field__error-message--center{text-align:center}.van-field__error-message--right{text-align:right}.van-field__word-limit{margin-top:var(--van-padding-base);color:var(--van-field-word-limit-color);font-size:var(--van-field-word-limit-font-size);line-height:var(--van-field-word-limit-line-height);text-align:right}:root,:host{--van-cell-group-background:var(--van-background-2);--van-cell-group-title-color:var(--van-text-color-2);--van-cell-group-title-padding:var(--van-padding-md) var(--van-padding-md);--van-cell-group-title-font-size:var(--van-font-size-md);--van-cell-group-title-line-height:16px;--van-cell-group-inset-padding:0 var(--van-padding-md);--van-cell-group-inset-radius:var(--van-radius-lg);--van-cell-group-inset-title-padding:var(--van-padding-md) var(--van-padding-md)}.van-cell-group{background:var(--van-cell-group-background)}.van-cell-group--inset{margin:var(--van-cell-group-inset-padding);border-radius:var(--van-cell-group-inset-radius);overflow:hidden}.van-cell-group__title{padding:var(--van-cell-group-title-padding);color:var(--van-cell-group-title-color);font-size:var(--van-cell-group-title-font-size);line-height:var(--van-cell-group-title-line-height)}.van-cell-group__title--inset{padding:var(--van-cell-group-inset-title-padding)}:root,:host{--van-loading-text-color:var(--van-text-color-2);--van-loading-text-font-size:var(--van-font-size-md);--van-loading-spinner-color:var(--van-gray-5);--van-loading-spinner-size:30px;--van-loading-spinner-duration:.8s}.van-loading{color:var(--van-loading-spinner-color);vertical-align:middle;font-size:0;position:relative}.van-loading__spinner{width:var(--van-loading-spinner-size);max-width:100%;height:var(--van-loading-spinner-size);vertical-align:middle;max-height:100%;animation:van-rotate var(--van-loading-spinner-duration) linear infinite;display:inline-block;position:relative}.van-loading__spinner--spinner{animation-timing-function:steps(12,end)}.van-loading__spinner--circular{animation-duration:2s}.van-loading__line{width:100%;height:100%;position:absolute;top:0;left:0}.van-loading__line:before{content:" ";background-color:currentColor;border-radius:40%;width:2px;height:25%;margin:0 auto;display:block}.van-loading__circular{width:100%;height:100%;display:block}.van-loading__circular circle{stroke:currentColor;stroke-width:3px;stroke-linecap:round;animation:1.5s ease-in-out infinite van-circular}.van-loading__text{margin-left:var(--van-padding-xs);color:var(--van-loading-text-color);font-size:var(--van-loading-text-font-size);vertical-align:middle;display:inline-block}.van-loading--vertical{flex-direction:column;align-items:center;display:flex}.van-loading--vertical .van-loading__text{margin:var(--van-padding-xs) 0 0}@keyframes van-circular{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-40px}to{stroke-dasharray:90 150;stroke-dashoffset:-120px}}.van-loading__line--1{opacity:1;transform:rotate(30deg)}.van-loading__line--2{opacity:.9375;transform:rotate(60deg)}.van-loading__line--3{opacity:.875;transform:rotate(90deg)}.van-loading__line--4{opacity:.8125;transform:rotate(120deg)}.van-loading__line--5{opacity:.75;transform:rotate(150deg)}.van-loading__line--6{opacity:.6875;transform:rotate(180deg)}.van-loading__line--7{opacity:.625;transform:rotate(210deg)}.van-loading__line--8{opacity:.5625;transform:rotate(240deg)}.van-loading__line--9{opacity:.5;transform:rotate(270deg)}.van-loading__line--10{opacity:.4375;transform:rotate(300deg)}.van-loading__line--11{opacity:.375;transform:rotate(330deg)}.van-loading__line--12{opacity:.3125;transform:rotate(360deg)}:root,:host{--van-button-mini-height:24px;--van-button-mini-padding:0 var(--van-padding-base);--van-button-mini-font-size:var(--van-font-size-xs);--van-button-small-height:32px;--van-button-small-padding:0 var(--van-padding-xs);--van-button-small-font-size:var(--van-font-size-sm);--van-button-normal-padding:0 15px;--van-button-normal-font-size:var(--van-font-size-md);--van-button-large-height:50px;--van-button-default-height:44px;--van-button-default-line-height:1.2;--van-button-default-font-size:var(--van-font-size-lg);--van-button-default-color:var(--van-text-color);--van-button-default-background:var(--van-background-2);--van-button-default-border-color:var(--van-gray-4);--van-button-primary-color:var(--van-white);--van-button-primary-background:var(--van-primary-color);--van-button-primary-border-color:var(--van-primary-color);--van-button-success-color:var(--van-white);--van-button-success-background:var(--van-success-color);--van-button-success-border-color:var(--van-success-color);--van-button-danger-color:var(--van-white);--van-button-danger-background:var(--van-danger-color);--van-button-danger-border-color:var(--van-danger-color);--van-button-warning-color:var(--van-white);--van-button-warning-background:var(--van-warning-color);--van-button-warning-border-color:var(--van-warning-color);--van-button-border-width:var(--van-border-width);--van-button-radius:var(--van-radius-md);--van-button-round-radius:var(--van-radius-max);--van-button-plain-background:var(--van-white);--van-button-disabled-opacity:var(--van-disabled-opacity);--van-button-icon-size:1.2em;--van-button-loading-icon-size:20px}.van-theme-dark{--van-button-plain-background:transparent}.van-button{box-sizing:border-box;height:var(--van-button-default-height);font-size:var(--van-button-default-font-size);line-height:var(--van-button-default-line-height);text-align:center;border-radius:var(--van-button-radius);cursor:pointer;transition:opacity var(--van-duration-fast);-webkit-appearance:none;-webkit-font-smoothing:auto;margin:0;padding:0;display:inline-block;position:relative}.van-button:before{background:var(--van-black);border:inherit;border-color:var(--van-black);border-radius:inherit;opacity:0;content:" ";box-sizing:content-box;width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.van-button:active:before{opacity:.1}.van-button--loading:before,.van-button--disabled:before{display:none}.van-button--default{color:var(--van-button-default-color);background:var(--van-button-default-background);border:var(--van-button-border-width) solid var(--van-button-default-border-color)}.van-button--primary{color:var(--van-button-primary-color);background:var(--van-button-primary-background);border:var(--van-button-border-width) solid var(--van-button-primary-border-color)}.van-button--success{color:var(--van-button-success-color);background:var(--van-button-success-background);border:var(--van-button-border-width) solid var(--van-button-success-border-color)}.van-button--danger{color:var(--van-button-danger-color);background:var(--van-button-danger-background);border:var(--van-button-border-width) solid var(--van-button-danger-border-color)}.van-button--warning{color:var(--van-button-warning-color);background:var(--van-button-warning-background);border:var(--van-button-border-width) solid var(--van-button-warning-border-color)}.van-button--plain{background:var(--van-button-plain-background)}.van-button--plain.van-button--primary{color:var(--van-button-primary-background)}.van-button--plain.van-button--success{color:var(--van-button-success-background)}.van-button--plain.van-button--danger{color:var(--van-button-danger-background)}.van-button--plain.van-button--warning{color:var(--van-button-warning-background)}.van-button--large{width:100%;height:var(--van-button-large-height)}.van-button--normal{padding:var(--van-button-normal-padding);font-size:var(--van-button-normal-font-size)}.van-button--small{height:var(--van-button-small-height);padding:var(--van-button-small-padding);font-size:var(--van-button-small-font-size)}.van-button__loading{color:inherit;font-size:inherit}.van-button__loading .van-loading__spinner{color:currentColor;width:var(--van-button-loading-icon-size);height:var(--van-button-loading-icon-size)}.van-button--mini{height:var(--van-button-mini-height);padding:var(--van-button-mini-padding);font-size:var(--van-button-mini-font-size)}.van-button--mini+.van-button--mini{margin-left:var(--van-padding-base)}.van-button--block{width:100%;display:block}.van-button--disabled{cursor:not-allowed;opacity:var(--van-button-disabled-opacity)}.van-button--loading{cursor:default}.van-button--round{border-radius:var(--van-button-round-radius)}.van-button--square{border-radius:0}.van-button__content{justify-content:center;align-items:center;height:100%;display:flex}.van-button__content:before{content:" "}.van-button__icon{font-size:var(--van-button-icon-size);line-height:inherit}.van-button__icon+.van-button__text,.van-button__loading+.van-button__text,.van-button__text+.van-button__icon,.van-button__text+.van-button__loading{margin-left:var(--van-padding-base)}.van-button--hairline{border-width:0}.van-button--hairline:after{border-color:inherit;border-radius:calc(var(--van-button-radius) * 2)}.van-button--hairline.van-button--round:after{border-radius:var(--van-button-round-radius)}.van-button--hairline.van-button--square:after{border-radius:0}:root,:host{--van-search-padding:10px var(--van-padding-sm);--van-search-background:var(--van-background-2);--van-search-content-background:var(--van-background);--van-search-input-height:34px;--van-search-label-padding:0 5px;--van-search-label-color:var(--van-text-color);--van-search-label-font-size:var(--van-font-size-md);--van-search-left-icon-color:var(--van-gray-6);--van-search-action-padding:0 var(--van-padding-xs);--van-search-action-text-color:var(--van-text-color);--van-search-action-font-size:var(--van-font-size-md)}.van-search{box-sizing:border-box;padding:var(--van-search-padding);background:var(--van-search-background);align-items:center;display:flex}.van-search__content{padding-left:var(--van-padding-sm);background:var(--van-search-content-background);border-radius:var(--van-radius-sm);flex:1;display:flex}.van-search__content--round{border-radius:var(--van-radius-max)}.van-search__label{padding:var(--van-search-label-padding);color:var(--van-search-label-color);font-size:var(--van-search-label-font-size);line-height:var(--van-search-input-height)}.van-search__field{padding:0 var(--van-padding-xs) 0 0;height:var(--van-search-input-height);background-color:#0000;flex:1;align-items:center}.van-search__field .van-field__left-icon{color:var(--van-search-left-icon-color)}.van-search__field--with-message{align-items:flex-start;height:auto;padding-top:5px;padding-bottom:5px}.van-search--show-action{padding-right:0}.van-search input::-webkit-search-decoration{display:none}.van-search input::-webkit-search-cancel-button{display:none}.van-search input::-webkit-search-results-button{display:none}.van-search input::-webkit-search-results-decoration{display:none}.van-search__action{padding:var(--van-search-action-padding);color:var(--van-search-action-text-color);font-size:var(--van-search-action-font-size);line-height:var(--van-search-input-height);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.van-search__action:active{background-color:var(--van-active-color)}:root,:host{--van-list-text-color:var(--van-text-color-2);--van-list-text-font-size:var(--van-font-size-md);--van-list-text-line-height:50px;--van-list-loading-icon-size:16px}.van-list__loading,.van-list__finished-text,.van-list__error-text{color:var(--van-list-text-color);font-size:var(--van-list-text-font-size);line-height:var(--van-list-text-line-height);text-align:center}.van-list__placeholder{pointer-events:none;height:0}.van-list__loading-icon .van-loading__spinner{width:var(--van-list-loading-icon-size);height:var(--van-list-loading-icon-size)}:root,:host{--van-pull-refresh-head-height:50px;--van-pull-refresh-head-font-size:var(--van-font-size-md);--van-pull-refresh-head-text-color:var(--van-text-color-2);--van-pull-refresh-loading-icon-size:16px}.van-pull-refresh{overflow:hidden}.van-pull-refresh__track{height:100%;transition-property:transform;position:relative}.van-pull-refresh__head{width:100%;height:var(--van-pull-refresh-head-height);color:var(--van-pull-refresh-head-text-color);font-size:var(--van-pull-refresh-head-font-size);line-height:var(--van-pull-refresh-head-height);text-align:center;position:absolute;left:0;overflow:hidden;transform:translateY(-100%)}.van-pull-refresh__loading .van-loading__spinner{width:var(--van-pull-refresh-loading-icon-size);height:var(--van-pull-refresh-loading-icon-size)}:root,:host{--van-tag-padding:0 var(--van-padding-base);--van-tag-text-color:var(--van-white);--van-tag-font-size:var(--van-font-size-sm);--van-tag-radius:2px;--van-tag-line-height:16px;--van-tag-medium-padding:2px 6px;--van-tag-large-padding:var(--van-padding-base) var(--van-padding-xs);--van-tag-large-radius:var(--van-radius-md);--van-tag-large-font-size:var(--van-font-size-md);--van-tag-round-radius:var(--van-radius-max);--van-tag-danger-color:var(--van-danger-color);--van-tag-primary-color:var(--van-primary-color);--van-tag-success-color:var(--van-success-color);--van-tag-warning-color:var(--van-warning-color);--van-tag-default-color:var(--van-gray-6);--van-tag-plain-background:var(--van-background-2)}.van-tag{padding:var(--van-tag-padding);color:var(--van-tag-text-color);font-size:var(--van-tag-font-size);line-height:var(--van-tag-line-height);border-radius:var(--van-tag-radius);align-items:center;display:inline-flex;position:relative}.van-tag--default{background:var(--van-tag-default-color)}.van-tag--default.van-tag--plain{color:var(--van-tag-default-color)}.van-tag--danger{background:var(--van-tag-danger-color)}.van-tag--danger.van-tag--plain{color:var(--van-tag-danger-color)}.van-tag--primary{background:var(--van-tag-primary-color)}.van-tag--primary.van-tag--plain{color:var(--van-tag-primary-color)}.van-tag--success{background:var(--van-tag-success-color)}.van-tag--success.van-tag--plain{color:var(--van-tag-success-color)}.van-tag--warning{background:var(--van-tag-warning-color)}.van-tag--warning.van-tag--plain{color:var(--van-tag-warning-color)}.van-tag--plain{background:var(--van-tag-plain-background);border-color:currentColor}.van-tag--plain:before{border:1px solid;border-color:inherit;border-radius:inherit;content:"";pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.van-tag--medium{padding:var(--van-tag-medium-padding)}.van-tag--large{padding:var(--van-tag-large-padding);font-size:var(--van-tag-large-font-size);border-radius:var(--van-tag-large-radius)}.van-tag--mark{border-radius:0 var(--van-tag-round-radius) var(--van-tag-round-radius) 0}.van-tag--mark:after{content:"";width:2px;display:block}.van-tag--round{border-radius:var(--van-tag-round-radius)}.van-tag__close{margin-left:2px}:root,:host{--van-sticky-z-index:99}.van-sticky--fixed{z-index:var(--van-sticky-z-index);position:fixed}:root,:host{--van-swipe-indicator-size:6px;--van-swipe-indicator-margin:var(--van-padding-sm);--van-swipe-indicator-active-opacity:1;--van-swipe-indicator-inactive-opacity:.3;--van-swipe-indicator-active-background:var(--van-primary-color);--van-swipe-indicator-inactive-background:var(--van-border-color)}.van-swipe{cursor:-webkit-grab;cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;overflow:hidden;transform:translateZ(0)}.van-swipe__track{height:100%;transition-property:transform;display:flex}.van-swipe__track--vertical{flex-direction:column}.van-swipe__indicators{bottom:var(--van-swipe-indicator-margin);display:flex;position:absolute;left:50%;transform:translate(-50%)}.van-swipe__indicators--vertical{top:50%;bottom:auto;left:var(--van-swipe-indicator-margin);flex-direction:column;transform:translateY(-50%)}.van-swipe__indicators--vertical .van-swipe__indicator:not(:last-child){margin-bottom:var(--van-swipe-indicator-size)}.van-swipe__indicator{width:var(--van-swipe-indicator-size);height:var(--van-swipe-indicator-size);background-color:var(--van-swipe-indicator-inactive-background);opacity:var(--van-swipe-indicator-inactive-opacity);transition:opacity var(--van-duration-fast),background-color var(--van-duration-fast);border-radius:100%}.van-swipe__indicator:not(:last-child){margin-right:var(--van-swipe-indicator-size)}.van-swipe__indicator--active{background-color:var(--van-swipe-indicator-active-background);opacity:var(--van-swipe-indicator-active-opacity)}:root,:host{--van-tab-text-color:var(--van-gray-7);--van-tab-active-text-color:var(--van-text-color);--van-tab-disabled-text-color:var(--van-text-color-3);--van-tab-font-size:var(--van-font-size-md);--van-tab-line-height:var(--van-line-height-md);--van-tabs-default-color:var(--van-primary-color);--van-tabs-line-height:44px;--van-tabs-card-height:30px;--van-tabs-nav-background:var(--van-background-2);--van-tabs-bottom-bar-width:40px;--van-tabs-bottom-bar-height:3px;--van-tabs-bottom-bar-color:var(--van-primary-color)}.van-tab{box-sizing:border-box;padding:0 var(--van-padding-base);color:var(--van-tab-text-color);font-size:var(--van-tab-font-size);line-height:var(--van-tab-line-height);cursor:pointer;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.van-tab--active{color:var(--van-tab-active-text-color);font-weight:var(--van-font-bold)}.van-tab--disabled{color:var(--van-tab-disabled-text-color);cursor:not-allowed}.van-tab--grow{padding:0 var(--van-padding-sm);flex:1 0 auto}.van-tab--shrink{padding:0 var(--van-padding-xs);flex:none}.van-tab--card{color:var(--van-tabs-default-color);border-right:var(--van-border-width) solid var(--van-tabs-default-color)}.van-tab--card:last-child{border-right:none}.van-tab--card.van-tab--active{color:var(--van-white);background-color:var(--van-tabs-default-color)}.van-tab--card.van-tab--disabled{color:var(--van-tab-disabled-text-color)}.van-tab__text--ellipsis{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.van-tabs{position:relative}.van-tabs__wrap{overflow:hidden}.van-tabs__wrap--page-top{position:fixed}.van-tabs__wrap--content-bottom{top:auto;bottom:0}.van-tabs__nav{background:var(--van-tabs-nav-background);-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;position:relative}.van-tabs__nav--complete{-webkit-overflow-scrolling:touch;overflow-x:auto;overflow-y:hidden}.van-tabs__nav--complete::-webkit-scrollbar{display:none}.van-tabs__nav--line{box-sizing:content-box;height:100%;padding-bottom:15px}.van-tabs__nav--line.van-tabs__nav--shrink,.van-tabs__nav--line.van-tabs__nav--complete{padding-right:var(--van-padding-xs);padding-left:var(--van-padding-xs)}.van-tabs__nav--card{box-sizing:border-box;height:var(--van-tabs-card-height);margin:0 var(--van-padding-md);border:var(--van-border-width) solid var(--van-tabs-default-color);border-radius:var(--van-radius-sm)}.van-tabs__nav--card.van-tabs__nav--shrink{display:inline-flex}.van-tabs__line{z-index:1;width:var(--van-tabs-bottom-bar-width);height:var(--van-tabs-bottom-bar-height);background:var(--van-tabs-bottom-bar-color);border-radius:var(--van-tabs-bottom-bar-height);position:absolute;bottom:15px;left:0}.van-tabs__track{will-change:left;width:100%;height:100%;display:flex;position:relative}.van-tabs__content--animated{overflow:hidden}.van-tabs--line .van-tabs__wrap{height:var(--van-tabs-line-height)}.van-tabs--card>.van-tabs__wrap{height:var(--van-tabs-card-height)}.van-swipe-item{flex-shrink:0;width:100%;height:100%;position:relative}.van-tab__panel,.van-tab__panel-wrapper{box-sizing:border-box;flex-shrink:0;width:100%}.van-tab__panel-wrapper--inactive{height:0;overflow:visible}:root,:host{--van-overlay-z-index:1;--van-overlay-background:#000000b3}.van-overlay{z-index:var(--van-overlay-z-index);background:var(--van-overlay-background);width:100%;height:100%;position:fixed;top:0;left:0}:root,:host{--van-popup-background:var(--van-background-2);--van-popup-transition:transform var(--van-duration-base);--van-popup-round-radius:16px;--van-popup-close-icon-size:22px;--van-popup-close-icon-color:var(--van-gray-5);--van-popup-close-icon-margin:16px;--van-popup-close-icon-z-index:1}.van-overflow-hidden{overflow:hidden!important}.van-popup{box-sizing:border-box;background:var(--van-popup-background);max-height:100%;transition:var(--van-popup-transition);-webkit-overflow-scrolling:touch;position:fixed;overflow-y:auto}.van-popup--center{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100vw - var(--van-padding-md) * 2);margin:0 auto;top:50%;left:0;right:0;transform:translateY(-50%)}.van-popup--center.van-popup--round{border-radius:var(--van-popup-round-radius)}.van-popup--top{width:100%;top:0;left:0}.van-popup--top.van-popup--round{border-radius:0 0 var(--van-popup-round-radius) var(--van-popup-round-radius)}.van-popup--right{top:50%;right:0;transform:translateY(-50%)}.van-popup--right.van-popup--round{border-radius:var(--van-popup-round-radius) 0 0 var(--van-popup-round-radius)}.van-popup--bottom{width:100%;bottom:0;left:0}.van-popup--bottom.van-popup--round{border-radius:var(--van-popup-round-radius) var(--van-popup-round-radius) 0 0}.van-popup--left{top:50%;left:0;transform:translateY(-50%)}.van-popup--left.van-popup--round{border-radius:0 var(--van-popup-round-radius) var(--van-popup-round-radius) 0}.van-popup-slide-top-enter-active,.van-popup-slide-left-enter-active,.van-popup-slide-right-enter-active,.van-popup-slide-bottom-enter-active{transition-timing-function:var(--van-ease-out)}.van-popup-slide-top-leave-active,.van-popup-slide-left-leave-active,.van-popup-slide-right-leave-active,.van-popup-slide-bottom-leave-active{transition-timing-function:var(--van-ease-in)}.van-popup-slide-top-enter-from,.van-popup-slide-top-leave-active{transform:translateY(-100%)}.van-popup-slide-right-enter-from,.van-popup-slide-right-leave-active{transform:translate(100%,-50%)}.van-popup-slide-bottom-enter-from,.van-popup-slide-bottom-leave-active{transform:translateY(100%)}.van-popup-slide-left-enter-from,.van-popup-slide-left-leave-active{transform:translate(-100%,-50%)}.van-popup__close-icon{z-index:var(--van-popup-close-icon-z-index);color:var(--van-popup-close-icon-color);font-size:var(--van-popup-close-icon-size);position:absolute}.van-popup__close-icon--top-left{top:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--top-right{top:var(--van-popup-close-icon-margin);right:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-left{bottom:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-right{right:var(--van-popup-close-icon-margin);bottom:var(--van-popup-close-icon-margin)}:root,:host{--van-empty-padding:var(--van-padding-xl) 0;--van-empty-image-size:160px;--van-empty-description-margin-top:var(--van-padding-md);--van-empty-description-padding:0 60px;--van-empty-description-color:var(--van-text-color-2);--van-empty-description-font-size:var(--van-font-size-md);--van-empty-description-line-height:var(--van-line-height-md);--van-empty-bottom-margin-top:24px}.van-empty{box-sizing:border-box;padding:var(--van-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-empty__image{width:var(--van-empty-image-size);height:var(--van-empty-image-size)}.van-empty__image img{width:100%;height:100%}.van-empty__description{margin-top:var(--van-empty-description-margin-top);padding:var(--van-empty-description-padding);color:var(--van-empty-description-color);font-size:var(--van-empty-description-font-size);line-height:var(--van-empty-description-line-height)}.van-empty__bottom{margin-top:var(--van-empty-bottom-margin-top)}.van-theme-dark .van-empty{opacity:.5}:root,:host{--van-notice-bar-height:40px;--van-notice-bar-padding:0 var(--van-padding-md);--van-notice-bar-wrapable-padding:var(--van-padding-xs) var(--van-padding-md);--van-notice-bar-text-color:var(--van-orange-dark);--van-notice-bar-font-size:var(--van-font-size-md);--van-notice-bar-line-height:24px;--van-notice-bar-background:var(--van-orange-light);--van-notice-bar-icon-size:16px;--van-notice-bar-icon-min-width:24px}.van-notice-bar{height:var(--van-notice-bar-height);padding:var(--van-notice-bar-padding);color:var(--van-notice-bar-text-color);font-size:var(--van-notice-bar-font-size);line-height:var(--van-notice-bar-line-height);background:var(--van-notice-bar-background);align-items:center;display:flex;position:relative}.van-notice-bar__left-icon,.van-notice-bar__right-icon{min-width:var(--van-notice-bar-icon-min-width);font-size:var(--van-notice-bar-icon-size)}.van-notice-bar__right-icon{text-align:right;cursor:pointer}.van-notice-bar__wrap{flex:1;align-items:center;height:100%;display:flex;position:relative;overflow:hidden}.van-notice-bar__content{white-space:nowrap;transition-timing-function:linear;position:absolute}.van-notice-bar__content.van-ellipsis{max-width:100%}.van-notice-bar--wrapable{height:auto;padding:var(--van-notice-bar-wrapable-padding)}.van-notice-bar--wrapable .van-notice-bar__wrap{height:auto}.van-notice-bar--wrapable .van-notice-bar__content{white-space:normal;word-wrap:break-word;position:relative}:root,:host{--van-skeleton-title-width:40%}.van-skeleton-title{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-title--round{border-radius:var(--van-radius-max)}.van-skeleton-title{width:var(--van-skeleton-title-width);margin:0}.van-skeleton-title+.van-skeleton-paragraph{margin-top:20px}:root,:host{--van-skeleton-avatar-size:32px;--van-skeleton-avatar-background:var(--van-active-color)}.van-skeleton-avatar{width:var(--van-skeleton-avatar-size);height:var(--van-skeleton-avatar-size);margin-right:var(--van-padding-md);background:var(--van-skeleton-avatar-background);flex-shrink:0}.van-skeleton-avatar--round{border-radius:var(--van-radius-max)}.van-skeleton-avatar+.van-skeleton__content{padding-top:var(--van-padding-xs)}:root,:host{--van-skeleton-paragraph-height:16px;--van-skeleton-paragraph-background:var(--van-active-color);--van-skeleton-paragraph-margin-top:var(--van-padding-sm)}.van-skeleton-paragraph{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-paragraph--round{border-radius:var(--van-radius-max)}.van-skeleton-paragraph:not(:first-child){margin-top:var(--van-skeleton-paragraph-margin-top)}:root,:host{--van-skeleton-duration:1.2s}.van-skeleton{padding:0 var(--van-padding-md);display:flex}.van-skeleton__content{width:100%}.van-skeleton--animate{animation:van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite}@keyframes van-skeleton-blink{50%{opacity:.6}}:root,:host{--van-action-bar-background:var(--van-background-2);--van-action-bar-height:50px}.van-action-bar{box-sizing:content-box;height:var(--van-action-bar-height);background:var(--van-action-bar-background);align-items:center;display:flex;position:fixed;bottom:0;left:0;right:0}:root,:host{--van-action-bar-button-height:40px;--van-action-bar-button-warning-color:var(--van-gradient-orange);--van-action-bar-button-danger-color:var(--van-gradient-red)}.van-action-bar-button{height:var(--van-action-bar-button-height);font-weight:var(--van-font-bold);font-size:var(--van-font-size-md);border:none;border-radius:0;flex:1}.van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max);margin-left:5px}.van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max);margin-right:5px}.van-action-bar-button--warning{background:var(--van-action-bar-button-warning-color)}.van-action-bar-button--danger{background:var(--van-action-bar-button-danger-color)}@media (max-width:321px){.van-action-bar-button{font-size:13px}}:root,:host{--van-dialog-width:320px;--van-dialog-small-screen-width:90%;--van-dialog-font-size:var(--van-font-size-lg);--van-dialog-transition:var(--van-duration-base);--van-dialog-radius:16px;--van-dialog-background:var(--van-background-2);--van-dialog-header-font-weight:var(--van-font-bold);--van-dialog-header-line-height:24px;--van-dialog-header-padding-top:26px;--van-dialog-header-isolated-padding:var(--van-padding-lg) 0;--van-dialog-message-padding:var(--van-padding-lg);--van-dialog-message-font-size:var(--van-font-size-md);--van-dialog-message-line-height:var(--van-line-height-md);--van-dialog-message-max-height:60vh;--van-dialog-has-title-message-text-color:var(--van-gray-7);--van-dialog-has-title-message-padding-top:var(--van-padding-xs);--van-dialog-button-height:48px;--van-dialog-round-button-height:36px;--van-dialog-confirm-button-text-color:var(--van-primary-color)}.van-dialog{width:var(--van-dialog-width);font-size:var(--van-dialog-font-size);background:var(--van-dialog-background);border-radius:var(--van-dialog-radius);-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:var(--van-dialog-transition);transition-property:transform,opacity;top:45%;overflow:hidden}@media (max-width:321px){.van-dialog{width:var(--van-dialog-small-screen-width)}}.van-dialog__header{color:var(--van-text-color);padding-top:var(--van-dialog-header-padding-top);font-weight:var(--van-dialog-header-font-weight);line-height:var(--van-dialog-header-line-height);text-align:center}.van-dialog__header--isolated{padding:var(--van-dialog-header-isolated-padding)}.van-dialog__content--isolated{align-items:center;min-height:104px;display:flex}.van-dialog__message{color:var(--van-text-color);max-height:var(--van-dialog-message-max-height);padding:26px var(--van-dialog-message-padding);font-size:var(--van-dialog-message-font-size);line-height:var(--van-dialog-message-line-height);white-space:pre-wrap;text-align:center;word-wrap:break-word;-webkit-overflow-scrolling:touch;flex:1;overflow-y:auto}.van-dialog__message--has-title{padding-top:var(--van-dialog-has-title-message-padding-top);color:var(--van-dialog-has-title-message-text-color)}.van-dialog__message--left{text-align:left}.van-dialog__message--right{text-align:right}.van-dialog__message--justify{text-align:justify}.van-dialog__footer{-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;overflow:hidden}.van-dialog__confirm,.van-dialog__cancel{height:var(--van-dialog-button-height);border:0;border-radius:0;flex:1;margin:0}.van-dialog__confirm,.van-dialog__confirm:active{color:var(--van-dialog-confirm-button-text-color)}.van-dialog--round-button .van-dialog__footer{height:auto;padding:var(--van-padding-xs) var(--van-padding-lg) var(--van-padding-md);position:relative}.van-dialog--round-button .van-dialog__message{padding-bottom:var(--van-padding-md);color:var(--van-text-color)}.van-dialog--round-button .van-dialog__confirm,.van-dialog--round-button .van-dialog__cancel{height:var(--van-dialog-round-button-height)}.van-dialog--round-button .van-dialog__confirm{color:var(--van-white)}.van-dialog--round-button .van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max)}.van-dialog--round-button .van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max)}.van-dialog-bounce-enter-from{opacity:0;transform:translateY(-50%)scale(.7)}.van-dialog-bounce-leave-active{opacity:0;transform:translateY(-50%)scale(.9)}:root,:host{--van-toast-max-width:70%;--van-toast-font-size:var(--van-font-size-md);--van-toast-text-color:var(--van-white);--van-toast-loading-icon-color:var(--van-white);--van-toast-line-height:var(--van-line-height-md);--van-toast-radius:var(--van-radius-lg);--van-toast-background:#000000b3;--van-toast-icon-size:36px;--van-toast-text-min-width:96px;--van-toast-text-padding:var(--van-padding-xs) var(--van-padding-sm);--van-toast-default-padding:var(--van-padding-md);--van-toast-default-width:88px;--van-toast-default-min-height:88px;--van-toast-position-top-distance:20%;--van-toast-position-bottom-distance:20%}.van-toast{box-sizing:content-box;transition:all var(--van-duration-fast);width:var(--van-toast-default-width);max-width:var(--van-toast-max-width);min-height:var(--van-toast-default-min-height);padding:var(--van-toast-default-padding);color:var(--van-toast-text-color);font-size:var(--van-toast-font-size);line-height:var(--van-toast-line-height);white-space:pre-wrap;word-break:break-all;text-align:center;background:var(--van-toast-background);border-radius:var(--van-toast-radius);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-toast--break-normal{word-break:normal;word-wrap:normal}.van-toast--break-word{word-break:normal;word-wrap:break-word}.van-toast--unclickable{cursor:not-allowed;overflow:hidden}.van-toast--unclickable *{pointer-events:none}.van-toast--text,.van-toast--html{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;min-width:var(--van-toast-text-min-width);min-height:0;padding:var(--van-toast-text-padding)}.van-toast--text .van-toast__text,.van-toast--html .van-toast__text{margin-top:0}.van-toast--top{top:var(--van-toast-position-top-distance)}.van-toast--bottom{top:auto;bottom:var(--van-toast-position-bottom-distance)}.van-toast__icon{font-size:var(--van-toast-icon-size)}.van-toast__loading{padding:var(--van-padding-base);color:var(--van-toast-loading-icon-color)}.van-toast__text{margin-top:var(--van-padding-xs)}:root{--app-bg:#f4f7fd;--app-surface:#fff;--app-surface-2:#f6f8fe;--app-border:#6e89c92e;--app-text:#1d315f;--app-text-2:#3f527d;--app-muted:#7b89ab;--app-primary:#2f6fff;--app-primary-soft:#2f6fff1a;--app-success:#0f9e69;--app-success-soft:#13b77a1f;--app-warning:#c46f00;--app-warning-soft:#ff981d24;--app-danger:#e5383e;--app-danger-soft:#ff53581f;--app-muted-soft:#7b89ab24;--app-shadow:0 10px 28px #4e63a214;--app-radius:16px;--app-font:"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;--app-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root{--app-bg:#0b1222;--app-surface:#131c31;--app-surface-2:#18223b;--app-border:#8ca0d229;--app-text:#e6ecfb;--app-text-2:#bcc6e0;--app-muted:#8b97b6;--app-primary:#6f9bff;--app-primary-soft:#5b8cff29;--app-success:#2fd19a;--app-success-soft:#2fd19a24;--app-warning:#ffb45a;--app-warning-soft:#ffae4a24;--app-danger:#ff7a7f;--app-danger-soft:#ff6b7026;--app-muted-soft:#8390b029;--app-shadow:0 10px 28px #00000047;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*,:before,:after{box-sizing:border-box}html,body{background:var(--app-bg);color:var(--app-text);font-family:var(--app-font);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body,#app{min-height:100vh;min-height:100dvh}button,input,textarea{font-family:inherit}.app-card{background:var(--app-surface);border:1px solid var(--app-border);border-radius:var(--app-radius);box-shadow:var(--app-shadow)}.app-muted{color:var(--app-muted)}.app-mono{font-family:var(--app-mono);word-break:break-all;font-size:12px}.tone-success{color:var(--app-success)}.tone-warning{color:var(--app-warning)}.tone-danger{color:var(--app-danger)}.tone-muted{color:var(--app-muted)}.tone-primary{color:var(--app-primary)}.van-nav-bar .van-nav-bar__title{font-weight:600}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js new file mode 100644 index 000000000..6b213d033 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-BHVOzwXa.js @@ -0,0 +1 @@ +import{c as e}from"./_plugin-vue_export-helper-D4BeZ2vp.js";import{Dt as t,Kt as n,M as r,Mt as i,N as a,St as o,Vt as s,W as c,at as l,it as u}from"./index-C_M75pH8.js";function d(e,{args:t=[],done:n,canceled:r,error:i}){if(e){let a=e.apply(null,t);u(a)?a.then(e=>{e?n():r&&r()}).catch(i||l):a?n():r&&r()}else n()}var f=(o,l)=>{let u=n(),d=()=>{u.value=c(o).height};return i(()=>{if(t(d),l)for(let e=1;e<=3;e++)setTimeout(d,100*e)}),e(()=>t(d)),s([a,r],d),u};function p(e,t){let n=f(e,!0);return e=>o(`div`,{class:t(`placeholder`),style:{height:n.value?`${n.value}px`:void 0}},[e()])}export{d as n,p as t}; \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png differ diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png differ diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png differ diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png differ diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png differ diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html new file mode 100644 index 000000000..48cbab769 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + + + +
+ + + diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest new file mode 100644 index 000000000..bc53828cc --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话忽略。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/deploy/mobile-ignore-20260929/server-bundle/release-plan.json b/deploy/mobile-ignore-20260929/server-bundle/release-plan.json new file mode 100644 index 000000000..678223af1 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/release-plan.json @@ -0,0 +1,21 @@ +{ + "release": "mobile-ignore-20260929", + "candidateRoot": "C:/kefu", + "backendFiles": [], + "newBackendFiles": [], + "deployFiles": [], + "frontendDir": "admin-web/apps/mobile-h5/dist", + "serverTestModules": [ + "test_remote_api", + "test_zyt_auth", + "test_backend" + ], + "imageTestModules": [ + "test_archive_api" + ], + "schemaMigration": "none", + "restartServices": [ + "api" + ], + "notes": "User request 2026-09-29: rename the UI wording 黑名单 to 忽略 in the phone console (/m/). Text only; rules, APIs and permission codes unchanged. No backend code or database change." +} \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/remote_server_guard.py b/deploy/mobile-ignore-20260929/server-bundle/remote_server_guard.py new file mode 100644 index 000000000..5f02cc66d --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/remote_server_guard.py @@ -0,0 +1,138 @@ +"""Read-only checks for mobile-ignore-20260929: baseline / candidate / deployed. + +Prints hashes and status only; never reads chat content, patients or credentials. +""" +from pathlib import Path +import argparse, hashlib, json, subprocess, urllib.error, urllib.request + +parser = argparse.ArgumentParser() +parser.add_argument('mode', choices=['baseline', 'candidate', 'deployed']) +args = parser.parse_args() +RELEASE = 'mobile-ignore-20260929' +BANNER = '黑名单' # 本次把界面文字“黑名单”改成“忽略”,新版 /m/ 里不应再出现 +folder = Path(__file__).resolve().parent +project = Path('/opt/im-admin') +base = json.loads((folder / 'server-baseline.json').read_text()) +plan = json.loads((folder / 'release-plan.json').read_text()) +manifest = json.loads((folder / 'manifest.json').read_text()) +payload_rows = [row for row in manifest if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +sha = lambda data: hashlib.sha256(data).hexdigest() +image = lambda ref: subprocess.check_output(['docker', 'image', 'inspect', ref, '--format', '{{.Id}}'], text=True).strip() + +# The unpacked bundle itself must be intact. +for row in manifest: + path = (folder / row['path']).resolve() + assert path.is_relative_to(folder), row['path'] + assert path.stat().st_size == row['bytes'] and sha(path.read_bytes()) == row['sha256'], row['path'] + +# project-relative path -> expected sha256 on the host +if args.mode == 'baseline': + expected_host = dict(base['expected']) +else: + expected_host = {row['path'][len('payload/'):]: row['sha256'] for row in payload_rows} +container_code = {k: v for k, v in expected_host.items() if k.startswith(('wechat_rpa/', 'admin-web/'))} +frontend_files = sorted(k[len(frontend) + 1:] for k in expected_host if k.startswith(frontend + '/')) + + +def in_container(code, target, ref=None): + command = (['docker', 'run', '--rm', '-i', '--network=none', '--entrypoint', 'python', ref, '-c', code] + if ref else ['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code]) + return json.loads(subprocess.check_output(command, input=json.dumps(target).encode())) + + +HASHES = ('from pathlib import Path;import hashlib,json,sys;names=json.load(sys.stdin);' + 'print(json.dumps({n:(hashlib.sha256((Path("/app")/n).read_bytes()).hexdigest() ' + 'if (Path("/app")/n).is_file() else None) for n in names}))') +# /m/ 目录里的完整文件清单(不能残留旧版带横幅的页面文件)和横幅文字检查。 +FRONTEND = ('from pathlib import Path;import json,sys;q=json.load(sys.stdin);root=Path("/app")/q["root"];' + 'files=sorted(p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file());' + 'print(json.dumps({"files":files,"banner":[f for f in files if q["banner"] in ' + '(root/f).read_bytes().decode("utf-8","ignore")]}))') +FRONTEND_QUERY = {'root': frontend, 'banner': BANNER} + +current = image('zyt/wecom-admin:current') +result = {'mode': args.mode, 'release': RELEASE, 'image': current} +# 只重建 api:网关和知识库 worker 继续跑它们自己的镜像,部署后核对它们没有被动过。 +OTHERS = ['im-admin-gateway-1', 'im-admin-knowledge-worker-1'] + + +def inspect(container): + raw = json.loads(subprocess.check_output(['docker', 'inspect', container]))[0] + return {'id': raw['Id'], 'image': raw['Image'], 'running': raw['State']['Running'], + 'health': raw['State'].get('Health', {}).get('Status', 'not_configured')} + + +if args.mode == 'baseline': + api = inspect('im-admin-api-1') + assert api['image'] == current, 'API container is not on zyt/wecom-admin:current' + assert api['running'] and api['health'] == 'healthy', 'API container is not healthy' + others = {name: inspect(name) for name in OTHERS} + for name, state in others.items(): + assert state['running'], 'Not running: ' + name + result['others'] = others + +if args.mode in ('baseline', 'deployed'): + for name, wanted in expected_host.items(): + assert sha((project / name).read_bytes()) == wanted, 'Host drift: ' + name + actual = in_container(HASHES, sorted(container_code)) + assert actual == container_code, 'Running api container differs from expected code' + result['files'] = len(expected_host) + +if args.mode == 'candidate': + candidate = 'zyt/wecom-admin:' + RELEASE + actual = in_container(HASHES, sorted(container_code), candidate) + assert actual == container_code, 'Candidate image differs from manifest' + listing = in_container(FRONTEND, FRONTEND_QUERY, candidate) + assert listing['files'] == frontend_files, 'Candidate /m/ has extra or missing files' + assert not listing['banner'], listing['banner'] + result['candidate'] = image(candidate) + result['files'] = len(container_code) + +if args.mode == 'deployed': + wanted = image('zyt/wecom-admin:' + RELEASE) + assert current == wanted, 'current tag does not point at the release image' + raw = json.loads(subprocess.check_output(['docker', 'inspect', 'im-admin-api-1']))[0] + assert raw['Image'] == wanted and raw['State']['Running'] and raw['RestartCount'] == 0 + assert raw['State'].get('Health', {}).get('Status') == 'healthy' + recorded = folder / 'baseline-check.json' + if recorded.exists(): + before = json.loads(recorded.read_text())['others'] + for name in OTHERS: + now = inspect(name) + assert now['id'] == before[name]['id'] and now['running'], 'Unrelated container changed: ' + name + result['others_unchanged'] = OTHERS + host_files = sorted(p.relative_to(project / frontend).as_posix() for p in (project / frontend).rglob('*') if p.is_file()) + assert host_files == frontend_files, 'Host /m/ has extra or missing files' + listing = in_container(FRONTEND, FRONTEND_QUERY) + assert listing['files'] == frontend_files and not listing['banner'], listing + for url in ['http://127.0.0.1:18766/api/v2/health', 'http://127.0.0.1:18770/health']: + with urllib.request.urlopen(url, timeout=15) as response: + assert response.status == 200, url + # New routes still refuse anonymous callers. No login is attempted. + for method, path in [('GET', '/api/v2/mobile/me'), ('GET', '/api/v2/mobile/desktops'), + ('POST', '/api/v2/desktop/commands/poll'), ('GET', '/api/v2/desktop/blacklist')]: + request = urllib.request.Request('http://127.0.0.1:18766' + path, method=method, + data=b'{}' if method == 'POST' else None, + headers={'Content-Type': 'application/json'}) + try: + urllib.request.urlopen(request, timeout=15) + except urllib.error.HTTPError as exc: + assert exc.code == 401, (path, exc.code) + else: + raise AssertionError('Accessible without authentication: ' + path) + with urllib.request.urlopen('http://127.0.0.1:18766/m/', timeout=15) as response: + body = response.read() + assert response.status == 200 and '企微客服助手'.encode() in body, 'mobile console entry missing' + # 本次目的:公网来源的明文 HTTP 登录不再被拒绝(在运行中的容器里、用它的真实环境变量判断)。 + code = ("import remote_api,json;from starlette.requests import Request;" + "r=Request({'type':'http','method':'POST','path':'/api/v2/mobile/auth/login','root_path':''," + "'headers':[(b'x-real-ip',b'114.114.114.114')],'scheme':'http','query_string':b''," + "'client':('172.18.0.1',40000),'server':('127.0.0.1',8766)});" + "print(json.dumps({'public_http_login_refused':remote_api.https_required(r)}))") + policy = json.loads(subprocess.check_output(['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code])) + assert policy == {'public_http_login_refused': False}, policy + result['policy'] = policy + +result['status'] = 'passed' +print(json.dumps(result, ensure_ascii=False)) diff --git a/deploy/mobile-ignore-20260929/server-bundle/rollback.sh b/deploy/mobile-ignore-20260929/server-bundle/rollback.sh new file mode 100644 index 000000000..e692a290b --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/rollback.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# 恢复原 /m/ 目录,切回旧镜像(mobile-http-20260928),只重建 api。不涉及数据库。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-ignore-20260929 +release=/home/ps/mobile-ignore-20260929 +cd /opt/im-admin/deploy/im-admin +test -d "$release/source-before" +test "$(docker image inspect zyt/wecom-admin:before-mobile-ignore-20260929 --format '{{.Id}}')" = "$(cat "$release/image-before.txt")" +python3 - <<'PY' +from pathlib import Path +import hashlib, json, os, shutil +r = Path('/home/ps/mobile-ignore-20260929'); project = Path('/opt/im-admin') +base = json.loads((r / 'server-baseline.json').read_text()) +plan = json.loads((r / 'release-plan.json').read_text()) +sha = lambda data: hashlib.sha256(data).hexdigest() +for name in plan['backendFiles'] + plan['deployFiles']: + data = (r / 'source-before' / name).read_bytes(); assert sha(data) == base['expected'][name] + target = project / name; tmp = target.with_name(target.name + '.rollback-ignore.tmp') + if tmp.exists(): + tmp.unlink() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, target.stat().st_mode & 0o777); os.replace(tmp, target) +frontend = plan['frontendDir'] +saved = r / 'source-before' / frontend +for name, wanted in base['expected'].items(): + if name.startswith(frontend + '/'): + assert sha((saved / name[len(frontend) + 1:]).read_bytes()) == wanted, name +restore = project / (frontend + '.rollback-ignore.tmp') +if restore.exists(): + shutil.rmtree(restore) +shutil.copytree(saved, restore) +for leftover in [project / frontend, project / (frontend + '.mobile-ignore.tmp')]: + if leftover.exists(): + shutil.rmtree(leftover) +old = project / (frontend + '.mobile-ignore.old') +if old.exists(): + shutil.rmtree(old) +os.replace(restore, project / frontend) +PY +docker tag zyt/wecom-admin:before-mobile-ignore-20260929 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +curl -fsS -o /dev/null http://127.0.0.1:18766/api/v2/health +curl -fsS -o /dev/null http://127.0.0.1:18770/health +printf '%s\n' 'Previous image and /m/ restored.' diff --git a/deploy/mobile-ignore-20260929/server-bundle/server-baseline.json b/deploy/mobile-ignore-20260929/server-bundle/server-baseline.json new file mode 100644 index 000000000..46b6c7629 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/server-baseline.json @@ -0,0 +1,38 @@ +{ + "host": "192.168.110.111", + "user": "ps", + "project": "/opt/im-admin", + "compose": "/opt/im-admin/deploy/im-admin", + "checked_at": "2026-09-29", + "source": "Production /m/ as deployed by mobile-http-20260928 (28 files from that release payload).", + "expected": { + "admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-4o9rbdkf.js": "8d6186020c3d8562f581ea579742dd497649203a880b9c13cc904462b134bc60", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-DHX0QDeT.js": "226d4cde03c5434f20aa96c84cfb33538af084d79805348a089e343c5056281f", + "admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "admin-web/apps/mobile-h5/dist/assets/cell-group-CEKvg8Wf.js": "b0d4b420bc5197a5df6d918e740e2b90b108c6e06ba930c3fd8e97fabe010556", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-BMjeHKhk.js": "75cd3809eb0d7db6c994302dd296674eadd366a53a4feb97a759173d70546e80", + "admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "admin-web/apps/mobile-h5/dist/assets/DevicesView-DVXAQfpT.js": "237de4f76004cbf16e5928060cf009fbbb3beec826006a876ddbede0ee9512f7", + "admin-web/apps/mobile-h5/dist/assets/field-Bs9V8Rb1.js": "da55fbacb606cdb51f6eb09fb8bf4963fe249e869d653f0fb71d3c0da9ed7b4c", + "admin-web/apps/mobile-h5/dist/assets/format-2xch3ZbY.js": "a98c816d31f9a937c5418dd48d25bc5b7b96fb3084d58571565d0810f420ced9", + "admin-web/apps/mobile-h5/dist/assets/index-C_mEe-tZ.js": "6c6620136850dbdfa6ea22a9fa6c320f3f5d33150b83f1d1b9597e0894f5d0de", + "admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "admin-web/apps/mobile-h5/dist/assets/LoginView-Br8nsdQM.js": "29bdcbe8030ef2e169b2ea453cef160f2b3c86b9a83718971023a9862e10bd45", + "admin-web/apps/mobile-h5/dist/assets/LoginView-J4b4q6ai.css": "15ecdd5cb294bb51eef05e4843ac3a65f6e1718cbf16206547a876c52c5e8a2d", + "admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "admin-web/apps/mobile-h5/dist/assets/MeView-BOWGunAt.js": "9e5ff0c332dfae54d9f8e1c70b0a896e48aef80e8b45101092c08a59dfddf521", + "admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "admin-web/apps/mobile-h5/dist/assets/ShellView-DcQ5QK1V.js": "13f70e99d3eccfa014f14dc313a195b4f24200929bf60142f6fd190ef33f2f3d", + "admin-web/apps/mobile-h5/dist/assets/use-placeholder-D9ZemjXL.js": "530e97b69f045bf3c2798b79c1f49bd3ee69f53a657bad39825cb69d68552c9c", + "admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "admin-web/apps/mobile-h5/dist/icons/favicon-64.png": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "admin-web/apps/mobile-h5/dist/icons/icon-192.png": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "admin-web/apps/mobile-h5/dist/icons/icon-512.png": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "admin-web/apps/mobile-h5/dist/index.html": "6f335e7804fe9a1ce7bb5df388d2af81c7d919ddebb3beaee9533553a36adf04", + "admin-web/apps/mobile-h5/dist/manifest.webmanifest": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "admin-web/apps/mobile-h5/dist/sw.js": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69" + } +} \ No newline at end of file diff --git a/deploy/mobile-ignore-20260929/server-bundle/tests/test_backend.py b/deploy/mobile-ignore-20260929/server-bundle/tests/test_backend.py new file mode 100644 index 000000000..6a460176b --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/tests/test_backend.py @@ -0,0 +1,1527 @@ +# -*- coding: utf-8 -*- +"""配置后台与桌面端同步的本地闭环测试。""" + +from __future__ import annotations + +import json +import hashlib +import shutil +import model_protocol +import socket +import sqlite3 +import tempfile +import threading +import unittest +import urllib.error +import urllib.request +from unittest import mock +from pathlib import Path + +from test_support import local_state_redirect +import admin_backend +import app_version +import backend_client +import zyt_auth + + +class BackendIntegrationTest(unittest.TestCase): + + + def test_development_diagnostics_are_cloud_controlled_and_redacted(self) -> None: + config = { + "AI_DEVELOPMENT_MODE": True, + "AI_PROVIDER_TYPE": "openai", + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_MCP_SERVERS": [ + { + "headers": {"Authorization": "Bearer hidden-auth"}, + "env": {"ACCESS_TOKEN": "hidden-token"}, + "args": [ + "--token", + "hidden-argument", + "--password=hidden-inline", + "Bearer hidden-bearer", + ], + } + ], + } + response = {"version": 8, "updated_at": "now"} + diagnostics = backend_client._config_diagnostics( + config, + response, + "https://cloud.example/api/v1/desktop/config?api_key=query-secret", + ) + rendered = "\n".join(diagnostics) + self.assertIn("https://cloud.example/api/v1/desktop/config", rendered) + self.assertIn("云端配置版本: v8", rendered) + self.assertIn('"AI_MODEL": "test-model"', rendered) + for secret in ( + "top-secret-key", + "hidden-auth", + "hidden-token", + "hidden-argument", + "hidden-inline", + "hidden-bearer", + "query-secret", + ): + self.assertNotIn(secret, rendered) + self.assertEqual( + backend_client._config_diagnostics( + {**config, "AI_DEVELOPMENT_MODE": False}, response, "https://cloud.example/config" + ), + [], + ) + + def test_model_request_diagnostics_never_print_the_api_key(self) -> None: + import ai_chat + import ai_config + + previous = { + "AI_DEVELOPMENT_MODE": getattr(ai_config, "AI_DEVELOPMENT_MODE", False), + "AI_API_BASE": ai_config.AI_API_BASE, + "AI_API_KEY": ai_config.AI_API_KEY, + "AI_MODEL": ai_config.AI_MODEL, + } + try: + ai_config.AI_DEVELOPMENT_MODE = True + ai_config.AI_API_BASE = "https://api.example/v1" + ai_config.AI_API_KEY = "never-print-this-key" + ai_config.AI_MODEL = "diagnostic-model" + with mock.patch("builtins.print") as printer: + ai_chat._log_request_diagnostics( + "https://api.example/v1/chat/completions", "OpenAI 兼容" + ) + rendered = "\n".join( + " ".join(str(item) for item in call.args) + for call in printer.call_args_list + ) + self.assertIn("https://api.example/v1/chat/completions", rendered) + self.assertIn("diagnostic-model", rendered) + self.assertNotIn("never-print-this-key", rendered) + finally: + for key, value in previous.items(): + setattr(ai_config, key, value) + + def test_model_connection_uses_unsaved_values_and_saved_key(self) -> None: + current = { + "AI_API_BASE": "https://saved.example/v1", + "AI_API_KEY": "saved-secret", + "AI_MODEL": "saved-model", + "AI_TIMEOUT": 120, + } + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://new.example/v1", + "AI_API_KEY": "", + "AI_MODEL": "new-model", + "AI_TIMEOUT": "180", + }, + current, + ) + self.assertEqual(config["endpoint"], "https://new.example/v1/chat/completions") + self.assertEqual(config["api_key"], "saved-secret") + self.assertEqual(config["model"], "new-model") + self.assertEqual(config["timeout"], 60) + + def test_model_connection_success_does_not_expose_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"choices": [{"message": {"content": "OK"}}]} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(200, response) + ) as call: + result = admin_backend.test_model_connection(config) + self.assertTrue(result["ok"]) + self.assertEqual(result["http_status"], 200) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + self.assertEqual(call.call_args.args[0], config["endpoint"]) + self.assertEqual( + call.call_args.kwargs["headers"]["Authorization"], "Bearer top-secret-key" + ) + payload = call.call_args.kwargs["payload"] + self.assertEqual(payload["model"], "test-model") + + def test_model_connection_error_redacts_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"error": {"message": "invalid top-secret-key"}} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(401, response) + ): + result = admin_backend.test_model_connection(config) + self.assertFalse(result["ok"]) + self.assertEqual(result["http_status"], 401) + self.assertIn("API Key 无效", result["message"]) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + + def test_dify_and_comfyui_use_provider_specific_endpoints(self) -> None: + dify = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "dify", + "AI_API_BASE": "https://dify.example/v1", + "AI_API_KEY": "app-secret", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(dify["endpoint"], "https://dify.example/v1/chat-messages") + self.assertEqual(dify["provider_type"], "dify") + + comfyui = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "comfyui", + "AI_API_BASE": "http://127.0.0.1:8188", + "AI_API_KEY": "", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(comfyui["endpoint"], "http://127.0.0.1:8188/system_stats") + with mock.patch( + "admin_backend._perform_http_request", + return_value=(200, b'{"system": {"os": "windows"}, "devices": []}'), + ) as call: + result = admin_backend.test_model_connection(comfyui) + self.assertTrue(result["ok"]) + self.assertEqual(call.call_args.kwargs["method"], "GET") + self.assertIsNone(call.call_args.kwargs["payload"]) + + def test_desktop_ai_respects_explicit_dify_provider(self) -> None: + import ai_chat + import ai_config + + old_provider = ai_config.AI_PROVIDER_TYPE + old_base = ai_config.AI_API_BASE + try: + ai_config.AI_PROVIDER_TYPE = "dify" + ai_config.AI_API_BASE = "https://dify.example/v1" + self.assertTrue(ai_chat._is_dify_endpoint()) + self.assertEqual( + ai_chat._completions_url(), + "https://dify.example/v1/chat-messages", + ) + ai_config.AI_PROVIDER_TYPE = "openai" + self.assertFalse(ai_chat._is_dify_endpoint()) + finally: + ai_config.AI_PROVIDER_TYPE = old_provider + ai_config.AI_API_BASE = old_base + + + def test_release_status_detects_optional_and_forced_updates(self) -> None: + optional = app_version.release_status( + {"latest_version": "99.0.0", "force_upgrade": False} + ) + self.assertTrue(optional["update_available"]) + self.assertFalse(optional["force_upgrade"]) + + forced = app_version.release_status( + {"latest_version": "2.0.0", "force_upgrade": True} + ) + self.assertTrue(forced["force_upgrade"]) + + current = app_version.release_status( + {"latest_version": app_version.APP_VERSION, "force_upgrade": True} + ) + self.assertFalse(current["update_available"]) + self.assertFalse(current["force_upgrade"]) + + downgrade = app_version.release_status( + {"latest_version": "1.0.1", "force_upgrade": True} + ) + self.assertFalse(downgrade["update_available"]) + self.assertFalse(downgrade["force_upgrade"]) + + def test_release_form_validation(self) -> None: + release = admin_backend.validate_release_form( + { + "latest_version": "v1.2.3", + "download_url": "https://example.com/client.exe", + "release_notes": "修复已知问题", + "force_upgrade": "1", + } + ) + self.assertEqual(release["latest_version"], "1.2.3") + self.assertTrue(release["force_upgrade"]) + automatic = admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "package_sha256": "a" * 64, + "package_size": "1024", + "auto_install": "1", + "release_channel": "stable", + "rollout_percent": "25", + "status": "published", + } + ) + self.assertTrue(automatic["auto_install"]) + self.assertEqual(automatic["rollout_percent"], 25) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2", "force_upgrade": "1"} + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "auto_install": "1", + } + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2.3", "force_upgrade": "1"} + ) + + def test_pbkdf2_fallback_matches_standard_library(self) -> None: + password = b"FallbackPassword123" + salt = b"0123456789abcdef" + expected = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000) + native_pbkdf2 = admin_backend.hashlib.pbkdf2_hmac + try: + admin_backend.hashlib.pbkdf2_hmac = None + actual = admin_backend.pbkdf2_sha256(password, salt, 1_000) + finally: + admin_backend.hashlib.pbkdf2_hmac = native_pbkdf2 + self.assertEqual(actual, expected) + + def test_stale_runtime_file_is_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + runtime_file = root / "backend_runtime.json" + runtime_file.write_text( + json.dumps( + { + "pid": 0, + "host": "127.0.0.1", + "port": 8765, + "server_url": "http://127.0.0.1:8765", + "local_sync_token": "stale-token", + } + ), + encoding="utf-8", + ) + old_runtime_file = backend_client.RUNTIME_FILE + try: + backend_client.RUNTIME_FILE = runtime_file + self.assertEqual(backend_client.discover_local_runtime(), {}) + self.assertEqual(backend_client.discover_local_sync_token(), "") + finally: + backend_client.RUNTIME_FILE = old_runtime_file + + def test_local_agent_name_overrides_cloud_and_blank_follows_cloud(self) -> None: + import ai_config + + old_agent = ai_config.AI_AGENT_NAME + old_cloud = ai_config.AI_CLOUD_AGENT_NAME + old_local = ai_config.AI_LOCAL_AGENT_NAME + old_prompt = ai_config.AI_SYSTEM_PROMPT + try: + ai_config.AI_CLOUD_AGENT_NAME = "旧云端客服" + ai_config.AI_LOCAL_AGENT_NAME = "" + ai_config.AI_AGENT_NAME = "旧云端客服" + ai_config.set_local_agent_name_override("本机客服", persist=False) + ai_config.apply_settings({"AI_AGENT_NAME": "新云端客服"}, persist=False) + self.assertEqual(ai_config.AI_CLOUD_AGENT_NAME, "新云端客服") + self.assertEqual(ai_config.AI_AGENT_NAME, "本机客服") + ai_config.set_local_agent_name_override("", persist=False) + self.assertEqual(ai_config.AI_AGENT_NAME, "新云端客服") + finally: + ai_config.AI_AGENT_NAME = old_agent + ai_config.AI_CLOUD_AGENT_NAME = old_cloud + ai_config.AI_LOCAL_AGENT_NAME = old_local + ai_config.AI_SYSTEM_PROMPT = old_prompt + + +class DesktopConfigFallbackTest(unittest.TestCase): + """桌面端拉配置:新接口优先,老接口兜底。 + + 客户端和服务端是分批更新的,两边版本对不齐是常态而不是意外。直接把路径改 + 成 v2,那些服务端还没升级的部署会当场同步失败——而且失败得很安静,界面上 + 只是配置一直显示旧的,没人会立刻发现。 + """ + + def _fail(self, message: str): + return backend_client.BackendError(message) + + def test_startup_auth_config_is_read_from_management_and_cached(self) -> None: + calls = [] + saved = {} + + def fake(method, server, path, **kwargs): + calls.append((method, server, path)) + return 200, { + "zyt_api_url": "https://account.example.com/zyt/", + "terminal": 7, + "configured": True, + } + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", lambda: {}), + mock.patch.object(backend_client, "save_settings", saved.update), + ): + config = backend_client.fetch_desktop_auth_config("https://admin.example.com/") + + self.assertEqual( + calls, + [("GET", "https://admin.example.com", "/api/v2/desktop/auth/config")], + ) + self.assertEqual(config["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["server_url"], "https://admin.example.com") + + def test_startup_auth_config_rejects_an_incomplete_response(self) -> None: + with mock.patch.object( + backend_client, "_request", return_value=(200, {"terminal": 7}) + ): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client.fetch_desktop_auth_config("https://admin.example.com") + self.assertIn("登录 API", str(caught.exception)) + + def test_zyt_admin_login_uses_admin_endpoint_and_desktop_terminal(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append((method, server, path, kwargs.get("payload"))) + return 200, { + "code": 1, + "msg": "登录成功", + "data": {"token": "admin-desktop-token", "name": "管理员"}, + } + + with mock.patch.object(backend_client, "_request", fake): + token, user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(token, "admin-desktop-token") + self.assertEqual(user["name"], "管理员") + self.assertEqual(calls[0][2], "/adminapi/login/account") + self.assertEqual(calls[0][3]["terminal"], 7) + self.assertEqual(calls[0][3]["account"], "admin") + + def test_zyt_admin_login_falls_back_while_server_has_no_terminal_7(self) -> None: + terminals = [] + + def fake(method, server, path, **kwargs): + terminal = kwargs["payload"]["terminal"] + terminals.append(terminal) + if terminal == 7: + return 200, {"code": 0, "msg": "terminal必须在 1,2 范围内"} + return 200, {"code": 1, "data": {"token": "legacy-admin-token"}} + + with mock.patch.object(backend_client, "_request", fake): + token, _user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(terminals, [7, 1]) + self.assertEqual(token, "legacy-admin-token") + + def test_desktop_login_can_remember_password_with_dpapi_protection(self) -> None: + settings = backend_client.default_settings() + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, + "_protect_secret", + side_effect=lambda value: f"protected:{value}", + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=True, + ) + + self.assertTrue(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["zyt_account"], "admin") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:secret" + ) + self.assertNotIn("secret", json.dumps(saved[-1], ensure_ascii=False).replace( + "protected:secret", "" + )) + + def test_desktop_login_unchecked_forgets_the_saved_password(self) -> None: + settings = backend_client.default_settings() + settings["remembered_password_protected"] = "protected:old-password" + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected:token" + ), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=False, + ) + + self.assertFalse(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["remembered_password_protected"], "") + + def test_forced_logout_keeps_explicitly_remembered_password(self) -> None: + settings = backend_client.default_settings() + settings.update( + { + "zyt_account": "admin", + "remember_login": True, + "remembered_password_protected": "protected:password", + "zyt_token_protected": "protected:zyt-token", + "desktop_token_protected": "protected:desktop-token", + } + ) + saved = [] + with ( + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + ): + backend_client._clear_desktop_credentials(settings) + + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:password" + ) + + def test_session_heartbeat_never_silently_reauthenticates(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + } + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object(backend_client, "save_settings"), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object( + backend_client, + "_request", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + with self.assertRaises(backend_client.AuthenticationError): + backend_client.validate_desktop_session() + exchange.assert_not_called() + + def test_session_heartbeat_persists_the_renewed_expiration(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "desktop_expires_at": 1200, + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_request", + return_value=( + 200, + { + "id": 1, + "sn": "admin", + "expires_at": 50_000, + }, + ), + ), + ): + account = backend_client.validate_desktop_session() + + self.assertEqual(account["id"], 1) + self.assertEqual(saved[-1]["desktop_expires_at"], 50_000) + + def test_revoked_unexpired_session_requires_password_again(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 2000, + "zyt_account": "admin", + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "validate_desktop_session", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + self.assertIsNone(backend_client.restore_desktop_session()) + + exchange.assert_not_called() + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["zyt_account"], "admin") + + def test_naturally_expired_session_can_refresh_from_zyt(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-old", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 999, + } + saved = [] + + def unprotect(value): + return { + "protected-old": "old-token", + "protected-zyt": "zyt-token", + }.get(value, "") + + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object(backend_client, "_unprotect_secret", side_effect=unprotect), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected-new" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "new-token", + "expires_in": 1800, + "account": {"id": 1, "sn": "admin"}, + }, + ), + ): + account = backend_client.restore_desktop_session() + + self.assertEqual(account, {"id": 1, "sn": "admin"}) + self.assertEqual(saved[-1]["desktop_token_protected"], "protected-new") + self.assertEqual(saved[-1]["desktop_expires_at"], 2800) + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"config": {}, "version": 3} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + self.assertEqual(used, "/api/v2/desktop/config") + self.assertEqual(response["version"], 3) + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """服务端还是老版本时,必须照常同步,不能报错。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("HTTP 404", status=404) + return 200, {"config": {}, "version": 7} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config", "/api/v1/desktop/config"]) + self.assertEqual(used, "/api/v1/desktop/config") + self.assertEqual(response["version"], 7) + + def test_the_fallback_is_decided_by_status_not_by_wording(self) -> None: + """老后台对未知 GET 路径回的是"页面不存在",未知 POST 才回"接口不存在"。 + + 一个字之差。曾经这里靠文案匹配,结果对着真的老后台跑就整个失效了—— + 判据必须是状态码。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("页面不存在", status=404) + return 200, {"config": {}} + + with mock.patch.object(backend_client, "_request", fake): + _, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(used, "/api/v1/desktop/config") + + def test_a_connection_failure_is_not_retried_on_the_old_path(self) -> None: + """连不上就是连不上,换个路径同样连不上。 + + 真正的危害不是多发一个请求,而是把"服务器宕了"这个明确故障,伪装成 + "接口不存在"往下走——最后一层抛出来的是老接口的错误,人会去查一个根本 + 没坏的地方。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + # 连不上根本没拿到响应,所以没有状态码 + raise backend_client.BackendError("无法连接后台:timed out") + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"], "不该退回去重试") + self.assertIn("无法连接", str(caught.exception)) + + def test_a_bad_sync_key_surfaces_instead_of_falling_back(self) -> None: + """401/403 说明服务在、只是凭证不对。这是要人去改配置的错误,不能吞。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.AuthenticationError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + + def test_a_404_on_both_paths_reports_the_last_error(self) -> None: + def fake(method, server, path, **kwargs): + raise backend_client.BackendError("HTTP 404", status=404) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + def test_the_legacy_constant_still_points_at_a_real_path(self) -> None: + """DESKTOP_CONFIG_PATH 可能被别处引用,不能变成空字符串。""" + self.assertIn(backend_client.DESKTOP_CONFIG_PATH, backend_client.DESKTOP_CONFIG_PATHS) + self.assertTrue(backend_client.DESKTOP_CONFIG_PATH.startswith("/api/")) + + +class ZytAdminTokenVerificationTest(unittest.TestCase): + def test_backend_verifies_the_admin_desktop_session_endpoint(self) -> None: + session_response = mock.MagicMock() + session_response.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user_id": "admin:1", + "sn": "admin", + "nickname": "管理员", + "mobile": "", + "avatar": "", + "terminal": 7, + "status": "active", + "root": False, + "role_ids": [2], + "permissions": ["tcm.diagnosis/lists"], + }, + } + ).encode("utf-8") + with mock.patch.object( + zyt_auth.urllib.request, + "urlopen", + return_value=session_response, + ) as opened: + identity = zyt_auth.verify_desktop_token( + "admin-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 1) + request = opened.call_args_list[0].args[0] + self.assertEqual( + request.full_url, + "https://admin.example.com/adminapi/desktop/session", + ) + self.assertEqual(request.get_header("Token"), "admin-token") + self.assertEqual(identity["user_id"], "admin:1") + self.assertTrue(identity["permissions_known"]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + def test_old_server_falls_back_to_current_admin_profile(self) -> None: + profile = mock.MagicMock() + profile.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user": { + "id": 9, + "account": "doctor9", + "name": "九号医助", + "phone": "13800138000", + "avatar": "https://img.example/a.png", + "disable": 0, + "root": 0, + "role_ids": [2], + } + ,"permissions": ["tcm.diagnosis/lists"] + }, + } + ).encode("utf-8") + missing = urllib.error.HTTPError( + "https://admin.example.com/adminapi/desktop/session", + 404, + "Not Found", + None, + None, + ) + + with mock.patch.object( + zyt_auth.urllib.request, "urlopen", side_effect=[missing, profile] + ) as opened: + identity = zyt_auth.verify_desktop_token( + "legacy-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 2) + fallback_request = opened.call_args_list[1].args[0] + self.assertEqual( + fallback_request.full_url, + "https://admin.example.com/adminapi/auth.admin/mySelf", + ) + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(identity["nickname"], "九号医助") + self.assertEqual(identity["terminal"], 7) + self.assertEqual(identity["role_ids"], [2]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + +class ModelCallReportFallbackTest(unittest.TestCase): + """调用留痕上报:和拉配置同一套 v2 → v1 顺序。 + + 补这组测试是因为老后台(8765)退役前卡在这一条上:配置同步早有 v2, + 上报却只有 v1。桌面端只要改指到 8766,配置照常同步、回复照常发,唯独 + 调用记录一条都不进库——静悄悄地丢掉的正是出事后用来解释"这句话怎么来的" + 那份证据。 + """ + + def _settings(self): + return {"server_url": "http://x"} + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls"], "新接口在就不该再打老的") + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """还指着老后台(8765)的客户端必须照常上报,不能悄悄丢数据。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("接口不存在", status=404) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls", "/api/v1/model/calls"]) + + def test_a_401_does_not_fall_back(self) -> None: + """凭证不对换个路径重试同样不对,只会多打一个请求。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertFalse(ok) + self.assertEqual(calls, ["/api/v2/model/calls"]) + + def test_a_failure_never_raises_into_the_reply_path(self) -> None: + """上报是观测。它出问题绝不能把已经生成好的回复流程带下水。""" + def boom(*_args, **_kwargs): + raise RuntimeError("断网") + + with ( + mock.patch.object(backend_client, "_request", boom), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + self.assertFalse(backend_client.report_model_call({"task_id": "t1"})) + + +class GatewayUrlDerivationTest(unittest.TestCase): + """网关地址由后台算出来告诉客户端。 + + 这是"只改一个域名"的关键。桌面端配置里只有后台地址一项,网关在哪、模型怎么 + 编排全部由后台决定——运维换网关位置只动后台一处,不用挨个改客户端。 + """ + + def test_direct_port_access_points_at_the_local_gateway_port(self) -> None: + """本机开发:三个服务各占一个端口,网关就在同机的 8770。""" + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "127.0.0.1:8765"), + "http://127.0.0.1:8770/v1/answer", + ) + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "192.168.1.20:8766"), + "http://192.168.1.20:8770/v1/answer", + ) + + def test_a_proxied_domain_gets_a_same_origin_path(self) -> None: + """反代后面只有 80/443 对外,公网根本连不到 8770。 + + 这两种推法必须分开。用一套规则套两种部署,总有一边是错的——而且错得很 + 安静:客户端拿到一个连不上的地址,表现成"模型不回复"。 + """ + self.assertEqual( + admin_backend.derive_gateway_url("", "https", "xchat.example.com"), + "https://xchat.example.com/gateway/v1/answer", + ) + + def test_an_absolute_override_is_used_as_is(self) -> None: + """网关在别的域名或别的机器上时用这个。""" + self.assertEqual( + admin_backend.derive_gateway_url( + "https://gw.example.com/v1/answer", "https", "xchat.example.com" + ), + "https://gw.example.com/v1/answer", + ) + + def test_a_path_override_hangs_off_the_backend_domain(self) -> None: + self.assertEqual( + admin_backend.derive_gateway_url( + "/model-gw/v1/answer", "https", "xchat.example.com" + ), + "https://xchat.example.com/model-gw/v1/answer", + ) + + def test_a_missing_host_still_yields_something_usable(self) -> None: + """拿不到 Host 时给本机地址,而不是空串。 + + 空串会让客户端把 gateway 当成"没配",静默回落到本机单模型——而本机 + 没有密钥,最后报的是一个看不懂的 401。 + """ + self.assertTrue( + admin_backend.derive_gateway_url("", "http", "").startswith("http://") + ) + + def test_an_override_without_a_scheme_is_refused_on_save(self) -> None: + """漏了 http:// 会被拼成 https://你的域名/gw.example.com/v1/answer。 + + 那是个没意义的地址,而且失败要等到桌面端下次发模型请求才暴露。 + """ + base = { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + } + with self.assertRaises(ValueError) as caught: + admin_backend.validate_config_form( + {**base, "AI_GATEWAY_URL": "gw.example.com/v1/answer"}, {} + ) + self.assertIn("http", str(caught.exception)) + + def test_an_empty_override_is_accepted_and_means_auto(self) -> None: + config = admin_backend.validate_config_form( + { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + "AI_GATEWAY_URL": "", + }, + {}, + ) + self.assertEqual(config["AI_GATEWAY_URL"], "") + + +class GatewayArrivesBySyncTest(unittest.TestCase): + """桌面端不用手工配网关——同步时后台会把地址带过来。""" + + def _settings(self, response, existing=None): + """跑一遍 _apply_config_response,返回它写下的设置。""" + saved = {} + base = backend_client.default_settings() + if existing: + base.update(existing) + + with ( + mock.patch.object(backend_client, "load_settings", lambda: dict(base)), + mock.patch.object(backend_client, "save_settings", saved.update), + mock.patch("ai_config.apply_settings", lambda cfg, persist=True: cfg), + ): + backend_client._apply_config_response(response, dict(base)) + return saved + + def test_the_synced_gateway_address_lands_in_the_settings(self) -> None: + saved = self._settings( + { + "version": 3, + "config": {}, + "gateway": {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + } + ) + self.assertEqual( + saved["gateway"], + {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + ) + + def test_an_old_backend_without_a_gateway_block_keeps_the_last_one(self) -> None: + """老版本后端不返回这一段。清空等于把已经能用的客户端打回"没有网关"。""" + existing = {"gateway": {"enabled": True, "url": "https://old.example.com/v1/answer"}} + saved = self._settings({"version": 4, "config": {}}, existing=existing) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + def test_a_blank_url_from_the_backend_is_ignored(self) -> None: + """后台配错回了空地址时,宁可保持原样也不要把客户端弄成没网关。""" + existing = {"gateway": {"enabled": True, "url": "https://good.example.com/v1/answer"}} + saved = self._settings( + {"version": 5, "config": {}, "gateway": {"enabled": True, "url": " "}}, + existing=existing, + ) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + +class EndpointModeTest(unittest.TestCase): + """接口地址:自动补全 vs 原样使用。 + + 加这个开关是因为 auto 那套拼接规则只覆盖得了"服务商标准形状"。自建服务的路径 + 常常不按套路,比如 `https://api.example.com/custom/llm/invoke`——auto 会把它 + 拼成 `.../invoke/chat/completions`,请求发到一个不存在的地址,报 404,而排查的 + 人会去怀疑密钥和网络。 + + 为什么用显式开关而不是"更聪明的猜测":`https://api.example.com/openai` 到底是 + 前缀还是完整端点,光看地址分不出来。猜错的两种方向都会静默地把请求发歪。 + """ + + def test_auto_completes_the_path_for_each_kind(self) -> None: + cases = [ + ("openai", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"), + ("openai", "https://api.deepseek.com", "https://api.deepseek.com/chat/completions"), + ("claude", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"), + ("dify", "https://api.dify.ai/v1", "https://api.dify.ai/v1/chat-messages"), + ] + for kind, base, expected in cases: + with self.subTest(kind=kind, base=base): + self.assertEqual(model_protocol.endpoint_url(kind, base), expected) + + def test_auto_does_not_double_append_a_complete_url(self) -> None: + """已经写全的标准地址不该再被拼一次。""" + full = "https://api.openai.com/v1/chat/completions" + self.assertEqual(model_protocol.endpoint_url("openai", full), full) + + def test_exact_uses_the_address_verbatim(self) -> None: + odd = "https://api.example.com/custom/llm/invoke" + self.assertEqual(model_protocol.endpoint_url("openai", odd), f"{odd}/chat/completions") + self.assertEqual(model_protocol.endpoint_url("openai", odd, "exact"), odd) + + def test_exact_wins_for_every_kind(self) -> None: + """exact 是"别动我的地址",不该被接口类型的规则翻掉。""" + odd = "https://gw.internal/llm" + for kind in ("openai", "claude", "dify", "comfyui"): + with self.subTest(kind=kind): + self.assertEqual(model_protocol.endpoint_url(kind, odd, "exact"), odd) + + def test_a_trailing_slash_is_trimmed_in_both_modes(self) -> None: + self.assertEqual( + model_protocol.endpoint_url("openai", "https://x.com/llm/", "exact"), + "https://x.com/llm", + ) + + def test_the_connectivity_test_uses_the_same_rule_as_the_real_call(self) -> None: + """两边规则若不一致,测试会去戳一个和实际调用不同的地址。 + + 那是最没用的一种测试:测通了照样用不了,或者反过来。 + """ + odd = "https://api.example.com/custom/llm/invoke" + for mode in ("auto", "exact"): + with self.subTest(mode=mode): + self.assertEqual( + admin_backend._model_endpoint(odd, "openai", mode), + model_protocol.endpoint_url("openai", odd, mode), + ) + + def test_dify_upload_root_follows_the_mode(self) -> None: + """Dify 传图要拼 /files/upload,根地址得跟着 exact 走。""" + self.assertEqual( + model_protocol.dify_api_root("https://dify.internal/app/chat", "exact"), + "https://dify.internal/app/chat", + ) + + +class EndpointModeStorageTest(unittest.TestCase): + """存和读。老库要能平滑升级,老记录的行为必须一个字都不变。""" + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + self.uid = self.db.authenticate("admin", "Admin@123456")["id"] + + def _save(self, **extra): + item = { + "id": "p1", "name": "出口", "kind": "openai", + "base_url": "https://api.example.com/custom/llm/invoke", + "api_key": "sk-x", "model": "m", + } + item.update(extra) + return self.db.save_model_provider(item, self.uid, "1.1.1.1") + + def test_the_mode_round_trips(self) -> None: + self._save(endpoint_mode="exact") + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "exact") + self.assertEqual(item["endpoint"], "https://api.example.com/custom/llm/invoke") + + def test_the_default_is_auto(self) -> None: + """不填就是老行为。这个默认值是整件事零回归的前提。""" + self._save() + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "auto") + self.assertTrue(item["endpoint"].endswith("/chat/completions")) + + def test_an_unknown_mode_is_refused(self) -> None: + with self.assertRaises(ValueError) as caught: + self._save(endpoint_mode="随便写") + self.assertIn("地址模式", str(caught.exception)) + + def test_an_old_database_without_the_column_is_upgraded_in_place(self) -> None: + """已经在用的库不能因为加一列就要重建表——重建的每一步都可能丢数据。""" + path = self.root / "old.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_providers ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, + base_url TEXT NOT NULL, api_key_enc TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', capabilities TEXT NOT NULL DEFAULT 'text', + max_tokens INTEGER NOT NULL DEFAULT 500, temperature REAL NOT NULL DEFAULT 0.35, + timeout_ms INTEGER NOT NULL DEFAULT 30000, max_inflight INTEGER NOT NULL DEFAULT 32, + rpm_limit INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, + health TEXT NOT NULL DEFAULT 'unknown', health_checked_at TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by INTEGER); + INSERT INTO model_providers (id,name,kind,base_url,created_at,updated_at) + VALUES ('old-1','老出口','openai','https://api.openai.com/v1','x','x'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + item = next( + x for x in admin_backend.Database(path).model_providers() if x["id"] == "old-1" + ) + self.assertEqual(item["endpoint_mode"], "auto", "老记录必须还是老行为") + self.assertEqual(item["endpoint"], "https://api.openai.com/v1/chat/completions") + + def test_running_the_column_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.assertTrue(self.db.model_providers() is not None) + + +class ModelCallTraceabilityTest(unittest.TestCase): + """`model_calls` 存客户原话、模型原话、审核原因——用来"跟踪问题"。 + + 网关和桌面端各自都会往这张表报一次同一次调用(网关看得到候选和裁判, + 桌面端才知道审核规则命中原因)。两边共用 task_id,谁先落盘谁建行,后到 + 的一方只补自己独有的字段——这里测的就是这道合并逻辑不多写也不少写。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _record(self, **extra) -> dict: + record = { + "device_id": "dev-1", + "task_id": "task-1", + "roles_version": 1, + "judge_mode": "arbitrate", + "chosen": "openai-primary", + "judge": {"winner": "A", "score": 0.8, "risk": "low"}, + "candidates": [{"provider": "openai-primary", "text": "建议您注意休息"}], + "total_ms": 900, + "customer_text": "我这几天总是失眠", + "reply_text": "建议您注意休息", + "review_reason": "", + } + record.update(extra) + return record + + def _row(self, task_id: str) -> sqlite3.Row: + with self.db.connect() as con: + return con.execute( + "SELECT * FROM model_calls WHERE task_id = ?", (task_id,) + ).fetchone() + + def test_a_call_round_trips_customer_and_reply_text(self) -> None: + self.db.log_model_call(self._record()) + row = self._row("task-1") + self.assertEqual(row["customer_text"], "我这几天总是失眠") + self.assertEqual(row["reply_text"], "建议您注意休息") + self.assertEqual(row["review_reason"], "") + + def test_a_second_report_with_the_same_task_id_only_merges_review_reason(self) -> None: + """网关先落盘(候选、裁判都是权威数据),桌面端后到——不能把网关的数据覆盖掉。""" + self.db.log_model_call(self._record()) + self.db.log_model_call(self._record( + chosen="不该生效的值", + candidates=[{"provider": "不该生效的值", "text": "x"}], + total_ms=1, + review_reason="命中审核规则「诊断」", + )) + with self.db.connect() as con: + rows = con.execute( + "SELECT * FROM model_calls WHERE task_id='task-1'" + ).fetchall() + self.assertEqual(len(rows), 1, "同一个 task_id 只能落一行,不是两行") + row = self._row("task-1") + self.assertEqual(row["chosen"], "openai-primary", "第二次上报不该覆盖已有的候选数据") + self.assertEqual(row["review_reason"], "命中审核规则「诊断」", "审核原因只有桌面端知道,必须生效") + + def test_blank_task_ids_never_collide(self) -> None: + """本地非网关路径目前还给不出 task_id,留空时按老行为各插一行。""" + self.db.log_model_call(self._record(task_id="", customer_text="第一条")) + self.db.log_model_call(self._record(task_id="", customer_text="第二条")) + with self.db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id=''" + ).fetchone()["n"] + self.assertEqual(n, 2) + + def test_list_model_calls_finds_a_keyword_in_either_side_of_the_exchange(self) -> None: + self.db.log_model_call(self._record(task_id="t1", customer_text="想咨询糖尿病用药")) + self.db.log_model_call(self._record(task_id="t2", customer_text="今天天气不错", reply_text="是呀")) + found = self.db.list_model_calls(days=7, q="糖尿病") + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "t1") + + def test_list_model_calls_parses_candidates_back_into_a_list(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + found = self.db.list_model_calls(days=7) + self.assertEqual(found["items"][0]["candidates"][0]["provider"], "openai-primary") + + def test_list_model_calls_respects_the_days_window(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + with self.db.connect() as con: + con.execute( + "UPDATE model_calls SET created_at = '2000-01-01 00:00:00' WHERE task_id='t1'" + ) + con.commit() + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 0) + + def test_an_old_database_with_duplicate_task_ids_is_deduped_before_the_index_goes_on(self) -> None: + """老版本拿会话指纹当 task_id,同一个会话十几轮全共享一个值——建唯一索引前必须先清干净。""" + path = self.root / "old_calls.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p1','2026-01-01 00:00:01'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p2','2026-01-01 00:00:02'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p3','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + rows = con.execute( + "SELECT task_id, chosen FROM model_calls ORDER BY id" + ).fetchall() + self.assertEqual([r["task_id"] for r in rows], ["", "", "same-session"], "只留最新一行的 task_id") + self.assertEqual(rows[-1]["chosen"], "p3", "保留的必须是最新那一行") + # 索引确实建起来了:迁移后再报一次同名 task_id 应该走合并而不是新插一行 + db.log_model_call({"task_id": "same-session", "review_reason": "新原因"}) + with db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id='same-session'" + ).fetchone()["n"] + merged = con.execute( + "SELECT chosen, review_reason FROM model_calls WHERE task_id='same-session'" + ).fetchone() + self.assertEqual(n, 1, "唯一索引必须已经生效,不能再插出第二行") + self.assertEqual(merged["chosen"], "p3", "合并不该覆盖已有数据") + self.assertEqual(merged["review_reason"], "新原因") + + def test_running_the_model_calls_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.db.log_model_call(self._record(task_id="tx")) + self.assertEqual(self._row("tx")["chosen"], "openai-primary") + + +class GuardCallsStayOutOfTheCustomerLogTest(unittest.TestCase): + """界面识别调用不能混进"客服对话"的调用日志和调用统计。 + + 这是用户真实报上来的 bug:调用记录点开一看,客户消息和模型回复全是「(空)」, + 候选里全是 `{"state":"unknown","navigation_right_ratio":0,...}`——那是机器人 + 自己在看企业微信窗口,不是在回客户。守卫每轮轮询都要问一次模型,条数远多于 + 真实对话,不分开的话这张表根本没法用来查问题,裁判分布也在给错误的东西打分。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _log(self, task_id: str, purpose: str, **extra) -> None: + record = { + "device_id": "d1", "task_id": task_id, "chosen": "p1", + "judge": {"winner": "A", "score": 0.9, "risk": "low"}, + "candidates": [{"provider": "p1", "text": "文本"}], "total_ms": 100, + "customer_text": "客户说的", "reply_text": "模型答的", + "purpose": purpose, + } + record.update(extra) + self.db.log_model_call(record) + + def test_the_default_log_shows_only_customer_chats(self) -> None: + self._log("c1", "chat") + self._log("g1", "guard") + self._log("g2", "guard") + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "c1") + + def test_guard_calls_can_still_be_listed_on_purpose(self) -> None: + """内部调用照样在花钱,需要的时候得能翻出来看,不能查无此物。""" + self._log("c1", "chat") + self._log("g1", "guard") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + self.assertEqual(self.db.list_model_calls(days=7, purpose="")["total"], 2) + + def test_stats_ignore_guard_calls(self) -> None: + """裁判分布要量的是"发给客户的回复够不够好",不是"布局认得准不准"。""" + self._log("c1", "chat", judge={"winner": "A", "score": 0.9, "risk": "low"}) + for index in range(5): + self._log( + f"g{index}", "guard", + judge={"winner": "B", "score": 0.1, "risk": "high"}, + ) + stats = self.db.model_call_stats(7) + self.assertEqual(stats["total"], 1, "5 条界面识别不该被算成客服调用") + self.assertEqual(stats["judged"], 1) + self.assertAlmostEqual(stats["avg_score"], 0.9, places=3) + self.assertNotIn("high", stats["risk"], "守卫的低分不该污染风险占比") + + def test_a_call_without_an_explicit_purpose_counts_as_a_customer_chat(self) -> None: + """桌面端补报的那条路不带 purpose——默认必须是 chat,不能凭空消失。""" + self.db.log_model_call({ + "task_id": "c9", "chosen": "p1", "judge": {}, "candidates": [], + "customer_text": "在吗", "reply_text": "在的", + }) + self.assertEqual(self.db.list_model_calls(days=7)["total"], 1) + + def test_legacy_guard_rows_are_reclassified_by_their_fingerprint(self) -> None: + """老库里的守卫调用落的是默认 chat,要按候选内容的指纹认出来并改判。 + + 指纹用的是分类器专用的 JSON 字段名(布局守卫的 navigation_right_ratio、 + 会话行分类器的 reply_capable)——正常客服回复里不可能出现这些词。 + """ + path = self.root / "legacy.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (task_id,candidates_json,created_at) VALUES + ('L1','[{"provider":"a","text":"{\\"state\\":\\"unknown\\",\\"navigation_right_ratio\\":0}"}]','2026-01-01 00:00:01'), + ('L2','[{"provider":"a","text":"{\\"kind\\":\\"customer_chat\\",\\"reply_capable\\":true}"}]','2026-01-01 00:00:02'), + ('L3','[{"provider":"a","text":"忙完了,歇会儿吧,最近还好吗"}]','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + marks = { + row["task_id"]: row["purpose"] + for row in con.execute("SELECT task_id, purpose FROM model_calls") + } + self.assertEqual(marks["L1"], "guard", "布局守卫的记录要改判") + self.assertEqual(marks["L2"], "guard", "会话行分类器的记录也要改判") + self.assertEqual(marks["L3"], "chat", "真实客服回复绝不能被误判成内部调用") + + def test_the_reclassification_never_deletes_anything(self) -> None: + self._log("g1", "chat", candidates=[ + {"provider": "a", "text": '{"navigation_right_ratio":0.1}'} + ]) + before = self.db.list_model_calls(days=7, purpose="")["total"] + with self.db.connect() as con: + admin_backend.Database._backfill_guard_purpose(con) + con.commit() + after = self.db.list_model_calls(days=7, purpose="")["total"] + self.assertEqual(before, after, "只改分类标记,一行都不能少") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + + +def setUpModule(): + # 别让测试读到开发机上的真实桌面端配置——配过模型网关的机器会让 + # `ai_chat.current_provider()` 整体改走网关分支,一大片无关测试跟着变行为。 + local_state_redirect.start() + + +def tearDownModule(): + local_state_redirect.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-ignore-20260929/server-bundle/tests/test_remote_api.py b/deploy/mobile-ignore-20260929/server-bundle/tests/test_remote_api.py new file mode 100644 index 000000000..120d628b0 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/tests/test_remote_api.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_plain_http_login_is_allowed_by_default(self): + with mock.patch.dict(os.environ): + os.environ.pop("MOBILE_REQUIRE_HTTPS") + self.mobile_login(headers={"X-Real-IP": "114.114.114.114"}) + self.mobile_login("kefu2", headers={}) + self.assertEqual(self.zyt.login_calls, 2) + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-ignore-20260929/server-bundle/tests/test_zyt_auth.py b/deploy/mobile-ignore-20260929/server-bundle/tests/test_zyt_auth.py new file mode 100644 index 000000000..a5af82759 --- /dev/null +++ b/deploy/mobile-ignore-20260929/server-bundle/tests/test_zyt_auth.py @@ -0,0 +1,100 @@ +"""跨服务器桌面认证:只在路由缺失时兼容,拒绝结果不能降级。""" +import io +import json +import unittest +import urllib.error +from unittest import mock + +import zyt_auth + + +class DesktopVerificationRegressionTest(unittest.TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def session(self, **updates): + data = {"user_id": "admin:9", "terminal": 7, "status": "active", + "root": False, "role_ids": [2], "permissions": ["tcm.diagnosis/lists"]} + data.update(updates) + return {"code": 1, "data": data} + + def verify(self): + return zyt_auth.verify_desktop_token("test-token", api_url="https://identity.example.test") + + def missing(self, status=404): + return urllib.error.HTTPError("https://identity.example.test", status, "test", None, None) + + def test_success_never_requests_ip_bound_web_profile(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(self.session(root=True, permissions=["*"])), + self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertTrue(identity["root"]) + self.assertEqual(identity["role_ids"], [2]) + self.assertEqual(identity["permissions"], ["*"]) + self.assertTrue(identity["permissions_known"]) + + def test_missing_permissions_do_not_trigger_profile_request(self): + payload = self.session() + del payload["data"]["permissions"] + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertEqual(identity["permissions"], []) + self.assertFalse(identity["permissions_known"]) + + def test_empty_permissions_are_known(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(self.session(permissions=[]))): + identity = self.verify() + self.assertTrue(identity["permissions_known"]) + self.assertEqual(identity["permissions"], []) + + def test_only_missing_routes_allow_legacy_verification(self): + for status in (404, 405): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(status), self.response({"code": 1, "data": { + "user": {"id": 9, "disable": 0, "role_ids": [2]}, "permissions": []}}), + ]) as opened: + identity = self.verify() + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(opened.call_count, 2) + + def test_http_rejections_do_not_fall_back(self): + for status in (401, 403, 500, 502): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=self.missing(status)) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_invalid_identity_cannot_be_accepted_through_web_fallback(self): + failures = [{"code": -1, "msg": "登录已失效"}, {"code": "bad"}, [], + {"code": 1, "data": []}, self.session(user_id=""), + self.session(terminal=1), self.session(terminal="bad"), + self.session(status="disabled")] + for payload in failures: + with self.subTest(payload=payload), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(payload), self.response({"code": 1, "data": {"user": {"id": 9}}}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_bad_json_and_network_failure_do_not_fall_back(self): + for result in (io.BytesIO(b"error"), urllib.error.URLError("offline")): + with self.subTest(result=result), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[result]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_legacy_ip_error_identifies_missing_server_deployment(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(), self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]): + with self.assertRaisesRegex(zyt_auth.ZytAuthError, "/adminapi/desktop/session"): + self.verify() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-ignore-20260929/tests/test_backend.py b/deploy/mobile-ignore-20260929/tests/test_backend.py new file mode 100644 index 000000000..6a460176b --- /dev/null +++ b/deploy/mobile-ignore-20260929/tests/test_backend.py @@ -0,0 +1,1527 @@ +# -*- coding: utf-8 -*- +"""配置后台与桌面端同步的本地闭环测试。""" + +from __future__ import annotations + +import json +import hashlib +import shutil +import model_protocol +import socket +import sqlite3 +import tempfile +import threading +import unittest +import urllib.error +import urllib.request +from unittest import mock +from pathlib import Path + +from test_support import local_state_redirect +import admin_backend +import app_version +import backend_client +import zyt_auth + + +class BackendIntegrationTest(unittest.TestCase): + + + def test_development_diagnostics_are_cloud_controlled_and_redacted(self) -> None: + config = { + "AI_DEVELOPMENT_MODE": True, + "AI_PROVIDER_TYPE": "openai", + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_MCP_SERVERS": [ + { + "headers": {"Authorization": "Bearer hidden-auth"}, + "env": {"ACCESS_TOKEN": "hidden-token"}, + "args": [ + "--token", + "hidden-argument", + "--password=hidden-inline", + "Bearer hidden-bearer", + ], + } + ], + } + response = {"version": 8, "updated_at": "now"} + diagnostics = backend_client._config_diagnostics( + config, + response, + "https://cloud.example/api/v1/desktop/config?api_key=query-secret", + ) + rendered = "\n".join(diagnostics) + self.assertIn("https://cloud.example/api/v1/desktop/config", rendered) + self.assertIn("云端配置版本: v8", rendered) + self.assertIn('"AI_MODEL": "test-model"', rendered) + for secret in ( + "top-secret-key", + "hidden-auth", + "hidden-token", + "hidden-argument", + "hidden-inline", + "hidden-bearer", + "query-secret", + ): + self.assertNotIn(secret, rendered) + self.assertEqual( + backend_client._config_diagnostics( + {**config, "AI_DEVELOPMENT_MODE": False}, response, "https://cloud.example/config" + ), + [], + ) + + def test_model_request_diagnostics_never_print_the_api_key(self) -> None: + import ai_chat + import ai_config + + previous = { + "AI_DEVELOPMENT_MODE": getattr(ai_config, "AI_DEVELOPMENT_MODE", False), + "AI_API_BASE": ai_config.AI_API_BASE, + "AI_API_KEY": ai_config.AI_API_KEY, + "AI_MODEL": ai_config.AI_MODEL, + } + try: + ai_config.AI_DEVELOPMENT_MODE = True + ai_config.AI_API_BASE = "https://api.example/v1" + ai_config.AI_API_KEY = "never-print-this-key" + ai_config.AI_MODEL = "diagnostic-model" + with mock.patch("builtins.print") as printer: + ai_chat._log_request_diagnostics( + "https://api.example/v1/chat/completions", "OpenAI 兼容" + ) + rendered = "\n".join( + " ".join(str(item) for item in call.args) + for call in printer.call_args_list + ) + self.assertIn("https://api.example/v1/chat/completions", rendered) + self.assertIn("diagnostic-model", rendered) + self.assertNotIn("never-print-this-key", rendered) + finally: + for key, value in previous.items(): + setattr(ai_config, key, value) + + def test_model_connection_uses_unsaved_values_and_saved_key(self) -> None: + current = { + "AI_API_BASE": "https://saved.example/v1", + "AI_API_KEY": "saved-secret", + "AI_MODEL": "saved-model", + "AI_TIMEOUT": 120, + } + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://new.example/v1", + "AI_API_KEY": "", + "AI_MODEL": "new-model", + "AI_TIMEOUT": "180", + }, + current, + ) + self.assertEqual(config["endpoint"], "https://new.example/v1/chat/completions") + self.assertEqual(config["api_key"], "saved-secret") + self.assertEqual(config["model"], "new-model") + self.assertEqual(config["timeout"], 60) + + def test_model_connection_success_does_not_expose_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"choices": [{"message": {"content": "OK"}}]} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(200, response) + ) as call: + result = admin_backend.test_model_connection(config) + self.assertTrue(result["ok"]) + self.assertEqual(result["http_status"], 200) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + self.assertEqual(call.call_args.args[0], config["endpoint"]) + self.assertEqual( + call.call_args.kwargs["headers"]["Authorization"], "Bearer top-secret-key" + ) + payload = call.call_args.kwargs["payload"] + self.assertEqual(payload["model"], "test-model") + + def test_model_connection_error_redacts_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"error": {"message": "invalid top-secret-key"}} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(401, response) + ): + result = admin_backend.test_model_connection(config) + self.assertFalse(result["ok"]) + self.assertEqual(result["http_status"], 401) + self.assertIn("API Key 无效", result["message"]) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + + def test_dify_and_comfyui_use_provider_specific_endpoints(self) -> None: + dify = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "dify", + "AI_API_BASE": "https://dify.example/v1", + "AI_API_KEY": "app-secret", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(dify["endpoint"], "https://dify.example/v1/chat-messages") + self.assertEqual(dify["provider_type"], "dify") + + comfyui = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "comfyui", + "AI_API_BASE": "http://127.0.0.1:8188", + "AI_API_KEY": "", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(comfyui["endpoint"], "http://127.0.0.1:8188/system_stats") + with mock.patch( + "admin_backend._perform_http_request", + return_value=(200, b'{"system": {"os": "windows"}, "devices": []}'), + ) as call: + result = admin_backend.test_model_connection(comfyui) + self.assertTrue(result["ok"]) + self.assertEqual(call.call_args.kwargs["method"], "GET") + self.assertIsNone(call.call_args.kwargs["payload"]) + + def test_desktop_ai_respects_explicit_dify_provider(self) -> None: + import ai_chat + import ai_config + + old_provider = ai_config.AI_PROVIDER_TYPE + old_base = ai_config.AI_API_BASE + try: + ai_config.AI_PROVIDER_TYPE = "dify" + ai_config.AI_API_BASE = "https://dify.example/v1" + self.assertTrue(ai_chat._is_dify_endpoint()) + self.assertEqual( + ai_chat._completions_url(), + "https://dify.example/v1/chat-messages", + ) + ai_config.AI_PROVIDER_TYPE = "openai" + self.assertFalse(ai_chat._is_dify_endpoint()) + finally: + ai_config.AI_PROVIDER_TYPE = old_provider + ai_config.AI_API_BASE = old_base + + + def test_release_status_detects_optional_and_forced_updates(self) -> None: + optional = app_version.release_status( + {"latest_version": "99.0.0", "force_upgrade": False} + ) + self.assertTrue(optional["update_available"]) + self.assertFalse(optional["force_upgrade"]) + + forced = app_version.release_status( + {"latest_version": "2.0.0", "force_upgrade": True} + ) + self.assertTrue(forced["force_upgrade"]) + + current = app_version.release_status( + {"latest_version": app_version.APP_VERSION, "force_upgrade": True} + ) + self.assertFalse(current["update_available"]) + self.assertFalse(current["force_upgrade"]) + + downgrade = app_version.release_status( + {"latest_version": "1.0.1", "force_upgrade": True} + ) + self.assertFalse(downgrade["update_available"]) + self.assertFalse(downgrade["force_upgrade"]) + + def test_release_form_validation(self) -> None: + release = admin_backend.validate_release_form( + { + "latest_version": "v1.2.3", + "download_url": "https://example.com/client.exe", + "release_notes": "修复已知问题", + "force_upgrade": "1", + } + ) + self.assertEqual(release["latest_version"], "1.2.3") + self.assertTrue(release["force_upgrade"]) + automatic = admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "package_sha256": "a" * 64, + "package_size": "1024", + "auto_install": "1", + "release_channel": "stable", + "rollout_percent": "25", + "status": "published", + } + ) + self.assertTrue(automatic["auto_install"]) + self.assertEqual(automatic["rollout_percent"], 25) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2", "force_upgrade": "1"} + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "auto_install": "1", + } + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2.3", "force_upgrade": "1"} + ) + + def test_pbkdf2_fallback_matches_standard_library(self) -> None: + password = b"FallbackPassword123" + salt = b"0123456789abcdef" + expected = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000) + native_pbkdf2 = admin_backend.hashlib.pbkdf2_hmac + try: + admin_backend.hashlib.pbkdf2_hmac = None + actual = admin_backend.pbkdf2_sha256(password, salt, 1_000) + finally: + admin_backend.hashlib.pbkdf2_hmac = native_pbkdf2 + self.assertEqual(actual, expected) + + def test_stale_runtime_file_is_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + runtime_file = root / "backend_runtime.json" + runtime_file.write_text( + json.dumps( + { + "pid": 0, + "host": "127.0.0.1", + "port": 8765, + "server_url": "http://127.0.0.1:8765", + "local_sync_token": "stale-token", + } + ), + encoding="utf-8", + ) + old_runtime_file = backend_client.RUNTIME_FILE + try: + backend_client.RUNTIME_FILE = runtime_file + self.assertEqual(backend_client.discover_local_runtime(), {}) + self.assertEqual(backend_client.discover_local_sync_token(), "") + finally: + backend_client.RUNTIME_FILE = old_runtime_file + + def test_local_agent_name_overrides_cloud_and_blank_follows_cloud(self) -> None: + import ai_config + + old_agent = ai_config.AI_AGENT_NAME + old_cloud = ai_config.AI_CLOUD_AGENT_NAME + old_local = ai_config.AI_LOCAL_AGENT_NAME + old_prompt = ai_config.AI_SYSTEM_PROMPT + try: + ai_config.AI_CLOUD_AGENT_NAME = "旧云端客服" + ai_config.AI_LOCAL_AGENT_NAME = "" + ai_config.AI_AGENT_NAME = "旧云端客服" + ai_config.set_local_agent_name_override("本机客服", persist=False) + ai_config.apply_settings({"AI_AGENT_NAME": "新云端客服"}, persist=False) + self.assertEqual(ai_config.AI_CLOUD_AGENT_NAME, "新云端客服") + self.assertEqual(ai_config.AI_AGENT_NAME, "本机客服") + ai_config.set_local_agent_name_override("", persist=False) + self.assertEqual(ai_config.AI_AGENT_NAME, "新云端客服") + finally: + ai_config.AI_AGENT_NAME = old_agent + ai_config.AI_CLOUD_AGENT_NAME = old_cloud + ai_config.AI_LOCAL_AGENT_NAME = old_local + ai_config.AI_SYSTEM_PROMPT = old_prompt + + +class DesktopConfigFallbackTest(unittest.TestCase): + """桌面端拉配置:新接口优先,老接口兜底。 + + 客户端和服务端是分批更新的,两边版本对不齐是常态而不是意外。直接把路径改 + 成 v2,那些服务端还没升级的部署会当场同步失败——而且失败得很安静,界面上 + 只是配置一直显示旧的,没人会立刻发现。 + """ + + def _fail(self, message: str): + return backend_client.BackendError(message) + + def test_startup_auth_config_is_read_from_management_and_cached(self) -> None: + calls = [] + saved = {} + + def fake(method, server, path, **kwargs): + calls.append((method, server, path)) + return 200, { + "zyt_api_url": "https://account.example.com/zyt/", + "terminal": 7, + "configured": True, + } + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", lambda: {}), + mock.patch.object(backend_client, "save_settings", saved.update), + ): + config = backend_client.fetch_desktop_auth_config("https://admin.example.com/") + + self.assertEqual( + calls, + [("GET", "https://admin.example.com", "/api/v2/desktop/auth/config")], + ) + self.assertEqual(config["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["server_url"], "https://admin.example.com") + + def test_startup_auth_config_rejects_an_incomplete_response(self) -> None: + with mock.patch.object( + backend_client, "_request", return_value=(200, {"terminal": 7}) + ): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client.fetch_desktop_auth_config("https://admin.example.com") + self.assertIn("登录 API", str(caught.exception)) + + def test_zyt_admin_login_uses_admin_endpoint_and_desktop_terminal(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append((method, server, path, kwargs.get("payload"))) + return 200, { + "code": 1, + "msg": "登录成功", + "data": {"token": "admin-desktop-token", "name": "管理员"}, + } + + with mock.patch.object(backend_client, "_request", fake): + token, user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(token, "admin-desktop-token") + self.assertEqual(user["name"], "管理员") + self.assertEqual(calls[0][2], "/adminapi/login/account") + self.assertEqual(calls[0][3]["terminal"], 7) + self.assertEqual(calls[0][3]["account"], "admin") + + def test_zyt_admin_login_falls_back_while_server_has_no_terminal_7(self) -> None: + terminals = [] + + def fake(method, server, path, **kwargs): + terminal = kwargs["payload"]["terminal"] + terminals.append(terminal) + if terminal == 7: + return 200, {"code": 0, "msg": "terminal必须在 1,2 范围内"} + return 200, {"code": 1, "data": {"token": "legacy-admin-token"}} + + with mock.patch.object(backend_client, "_request", fake): + token, _user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(terminals, [7, 1]) + self.assertEqual(token, "legacy-admin-token") + + def test_desktop_login_can_remember_password_with_dpapi_protection(self) -> None: + settings = backend_client.default_settings() + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, + "_protect_secret", + side_effect=lambda value: f"protected:{value}", + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=True, + ) + + self.assertTrue(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["zyt_account"], "admin") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:secret" + ) + self.assertNotIn("secret", json.dumps(saved[-1], ensure_ascii=False).replace( + "protected:secret", "" + )) + + def test_desktop_login_unchecked_forgets_the_saved_password(self) -> None: + settings = backend_client.default_settings() + settings["remembered_password_protected"] = "protected:old-password" + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected:token" + ), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=False, + ) + + self.assertFalse(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["remembered_password_protected"], "") + + def test_forced_logout_keeps_explicitly_remembered_password(self) -> None: + settings = backend_client.default_settings() + settings.update( + { + "zyt_account": "admin", + "remember_login": True, + "remembered_password_protected": "protected:password", + "zyt_token_protected": "protected:zyt-token", + "desktop_token_protected": "protected:desktop-token", + } + ) + saved = [] + with ( + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + ): + backend_client._clear_desktop_credentials(settings) + + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:password" + ) + + def test_session_heartbeat_never_silently_reauthenticates(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + } + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object(backend_client, "save_settings"), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object( + backend_client, + "_request", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + with self.assertRaises(backend_client.AuthenticationError): + backend_client.validate_desktop_session() + exchange.assert_not_called() + + def test_session_heartbeat_persists_the_renewed_expiration(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "desktop_expires_at": 1200, + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_request", + return_value=( + 200, + { + "id": 1, + "sn": "admin", + "expires_at": 50_000, + }, + ), + ), + ): + account = backend_client.validate_desktop_session() + + self.assertEqual(account["id"], 1) + self.assertEqual(saved[-1]["desktop_expires_at"], 50_000) + + def test_revoked_unexpired_session_requires_password_again(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 2000, + "zyt_account": "admin", + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "validate_desktop_session", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + self.assertIsNone(backend_client.restore_desktop_session()) + + exchange.assert_not_called() + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["zyt_account"], "admin") + + def test_naturally_expired_session_can_refresh_from_zyt(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-old", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 999, + } + saved = [] + + def unprotect(value): + return { + "protected-old": "old-token", + "protected-zyt": "zyt-token", + }.get(value, "") + + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object(backend_client, "_unprotect_secret", side_effect=unprotect), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected-new" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "new-token", + "expires_in": 1800, + "account": {"id": 1, "sn": "admin"}, + }, + ), + ): + account = backend_client.restore_desktop_session() + + self.assertEqual(account, {"id": 1, "sn": "admin"}) + self.assertEqual(saved[-1]["desktop_token_protected"], "protected-new") + self.assertEqual(saved[-1]["desktop_expires_at"], 2800) + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"config": {}, "version": 3} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + self.assertEqual(used, "/api/v2/desktop/config") + self.assertEqual(response["version"], 3) + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """服务端还是老版本时,必须照常同步,不能报错。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("HTTP 404", status=404) + return 200, {"config": {}, "version": 7} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config", "/api/v1/desktop/config"]) + self.assertEqual(used, "/api/v1/desktop/config") + self.assertEqual(response["version"], 7) + + def test_the_fallback_is_decided_by_status_not_by_wording(self) -> None: + """老后台对未知 GET 路径回的是"页面不存在",未知 POST 才回"接口不存在"。 + + 一个字之差。曾经这里靠文案匹配,结果对着真的老后台跑就整个失效了—— + 判据必须是状态码。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("页面不存在", status=404) + return 200, {"config": {}} + + with mock.patch.object(backend_client, "_request", fake): + _, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(used, "/api/v1/desktop/config") + + def test_a_connection_failure_is_not_retried_on_the_old_path(self) -> None: + """连不上就是连不上,换个路径同样连不上。 + + 真正的危害不是多发一个请求,而是把"服务器宕了"这个明确故障,伪装成 + "接口不存在"往下走——最后一层抛出来的是老接口的错误,人会去查一个根本 + 没坏的地方。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + # 连不上根本没拿到响应,所以没有状态码 + raise backend_client.BackendError("无法连接后台:timed out") + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"], "不该退回去重试") + self.assertIn("无法连接", str(caught.exception)) + + def test_a_bad_sync_key_surfaces_instead_of_falling_back(self) -> None: + """401/403 说明服务在、只是凭证不对。这是要人去改配置的错误,不能吞。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.AuthenticationError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + + def test_a_404_on_both_paths_reports_the_last_error(self) -> None: + def fake(method, server, path, **kwargs): + raise backend_client.BackendError("HTTP 404", status=404) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + def test_the_legacy_constant_still_points_at_a_real_path(self) -> None: + """DESKTOP_CONFIG_PATH 可能被别处引用,不能变成空字符串。""" + self.assertIn(backend_client.DESKTOP_CONFIG_PATH, backend_client.DESKTOP_CONFIG_PATHS) + self.assertTrue(backend_client.DESKTOP_CONFIG_PATH.startswith("/api/")) + + +class ZytAdminTokenVerificationTest(unittest.TestCase): + def test_backend_verifies_the_admin_desktop_session_endpoint(self) -> None: + session_response = mock.MagicMock() + session_response.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user_id": "admin:1", + "sn": "admin", + "nickname": "管理员", + "mobile": "", + "avatar": "", + "terminal": 7, + "status": "active", + "root": False, + "role_ids": [2], + "permissions": ["tcm.diagnosis/lists"], + }, + } + ).encode("utf-8") + with mock.patch.object( + zyt_auth.urllib.request, + "urlopen", + return_value=session_response, + ) as opened: + identity = zyt_auth.verify_desktop_token( + "admin-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 1) + request = opened.call_args_list[0].args[0] + self.assertEqual( + request.full_url, + "https://admin.example.com/adminapi/desktop/session", + ) + self.assertEqual(request.get_header("Token"), "admin-token") + self.assertEqual(identity["user_id"], "admin:1") + self.assertTrue(identity["permissions_known"]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + def test_old_server_falls_back_to_current_admin_profile(self) -> None: + profile = mock.MagicMock() + profile.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user": { + "id": 9, + "account": "doctor9", + "name": "九号医助", + "phone": "13800138000", + "avatar": "https://img.example/a.png", + "disable": 0, + "root": 0, + "role_ids": [2], + } + ,"permissions": ["tcm.diagnosis/lists"] + }, + } + ).encode("utf-8") + missing = urllib.error.HTTPError( + "https://admin.example.com/adminapi/desktop/session", + 404, + "Not Found", + None, + None, + ) + + with mock.patch.object( + zyt_auth.urllib.request, "urlopen", side_effect=[missing, profile] + ) as opened: + identity = zyt_auth.verify_desktop_token( + "legacy-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 2) + fallback_request = opened.call_args_list[1].args[0] + self.assertEqual( + fallback_request.full_url, + "https://admin.example.com/adminapi/auth.admin/mySelf", + ) + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(identity["nickname"], "九号医助") + self.assertEqual(identity["terminal"], 7) + self.assertEqual(identity["role_ids"], [2]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + +class ModelCallReportFallbackTest(unittest.TestCase): + """调用留痕上报:和拉配置同一套 v2 → v1 顺序。 + + 补这组测试是因为老后台(8765)退役前卡在这一条上:配置同步早有 v2, + 上报却只有 v1。桌面端只要改指到 8766,配置照常同步、回复照常发,唯独 + 调用记录一条都不进库——静悄悄地丢掉的正是出事后用来解释"这句话怎么来的" + 那份证据。 + """ + + def _settings(self): + return {"server_url": "http://x"} + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls"], "新接口在就不该再打老的") + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """还指着老后台(8765)的客户端必须照常上报,不能悄悄丢数据。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("接口不存在", status=404) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls", "/api/v1/model/calls"]) + + def test_a_401_does_not_fall_back(self) -> None: + """凭证不对换个路径重试同样不对,只会多打一个请求。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertFalse(ok) + self.assertEqual(calls, ["/api/v2/model/calls"]) + + def test_a_failure_never_raises_into_the_reply_path(self) -> None: + """上报是观测。它出问题绝不能把已经生成好的回复流程带下水。""" + def boom(*_args, **_kwargs): + raise RuntimeError("断网") + + with ( + mock.patch.object(backend_client, "_request", boom), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + self.assertFalse(backend_client.report_model_call({"task_id": "t1"})) + + +class GatewayUrlDerivationTest(unittest.TestCase): + """网关地址由后台算出来告诉客户端。 + + 这是"只改一个域名"的关键。桌面端配置里只有后台地址一项,网关在哪、模型怎么 + 编排全部由后台决定——运维换网关位置只动后台一处,不用挨个改客户端。 + """ + + def test_direct_port_access_points_at_the_local_gateway_port(self) -> None: + """本机开发:三个服务各占一个端口,网关就在同机的 8770。""" + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "127.0.0.1:8765"), + "http://127.0.0.1:8770/v1/answer", + ) + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "192.168.1.20:8766"), + "http://192.168.1.20:8770/v1/answer", + ) + + def test_a_proxied_domain_gets_a_same_origin_path(self) -> None: + """反代后面只有 80/443 对外,公网根本连不到 8770。 + + 这两种推法必须分开。用一套规则套两种部署,总有一边是错的——而且错得很 + 安静:客户端拿到一个连不上的地址,表现成"模型不回复"。 + """ + self.assertEqual( + admin_backend.derive_gateway_url("", "https", "xchat.example.com"), + "https://xchat.example.com/gateway/v1/answer", + ) + + def test_an_absolute_override_is_used_as_is(self) -> None: + """网关在别的域名或别的机器上时用这个。""" + self.assertEqual( + admin_backend.derive_gateway_url( + "https://gw.example.com/v1/answer", "https", "xchat.example.com" + ), + "https://gw.example.com/v1/answer", + ) + + def test_a_path_override_hangs_off_the_backend_domain(self) -> None: + self.assertEqual( + admin_backend.derive_gateway_url( + "/model-gw/v1/answer", "https", "xchat.example.com" + ), + "https://xchat.example.com/model-gw/v1/answer", + ) + + def test_a_missing_host_still_yields_something_usable(self) -> None: + """拿不到 Host 时给本机地址,而不是空串。 + + 空串会让客户端把 gateway 当成"没配",静默回落到本机单模型——而本机 + 没有密钥,最后报的是一个看不懂的 401。 + """ + self.assertTrue( + admin_backend.derive_gateway_url("", "http", "").startswith("http://") + ) + + def test_an_override_without_a_scheme_is_refused_on_save(self) -> None: + """漏了 http:// 会被拼成 https://你的域名/gw.example.com/v1/answer。 + + 那是个没意义的地址,而且失败要等到桌面端下次发模型请求才暴露。 + """ + base = { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + } + with self.assertRaises(ValueError) as caught: + admin_backend.validate_config_form( + {**base, "AI_GATEWAY_URL": "gw.example.com/v1/answer"}, {} + ) + self.assertIn("http", str(caught.exception)) + + def test_an_empty_override_is_accepted_and_means_auto(self) -> None: + config = admin_backend.validate_config_form( + { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + "AI_GATEWAY_URL": "", + }, + {}, + ) + self.assertEqual(config["AI_GATEWAY_URL"], "") + + +class GatewayArrivesBySyncTest(unittest.TestCase): + """桌面端不用手工配网关——同步时后台会把地址带过来。""" + + def _settings(self, response, existing=None): + """跑一遍 _apply_config_response,返回它写下的设置。""" + saved = {} + base = backend_client.default_settings() + if existing: + base.update(existing) + + with ( + mock.patch.object(backend_client, "load_settings", lambda: dict(base)), + mock.patch.object(backend_client, "save_settings", saved.update), + mock.patch("ai_config.apply_settings", lambda cfg, persist=True: cfg), + ): + backend_client._apply_config_response(response, dict(base)) + return saved + + def test_the_synced_gateway_address_lands_in_the_settings(self) -> None: + saved = self._settings( + { + "version": 3, + "config": {}, + "gateway": {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + } + ) + self.assertEqual( + saved["gateway"], + {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + ) + + def test_an_old_backend_without_a_gateway_block_keeps_the_last_one(self) -> None: + """老版本后端不返回这一段。清空等于把已经能用的客户端打回"没有网关"。""" + existing = {"gateway": {"enabled": True, "url": "https://old.example.com/v1/answer"}} + saved = self._settings({"version": 4, "config": {}}, existing=existing) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + def test_a_blank_url_from_the_backend_is_ignored(self) -> None: + """后台配错回了空地址时,宁可保持原样也不要把客户端弄成没网关。""" + existing = {"gateway": {"enabled": True, "url": "https://good.example.com/v1/answer"}} + saved = self._settings( + {"version": 5, "config": {}, "gateway": {"enabled": True, "url": " "}}, + existing=existing, + ) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + +class EndpointModeTest(unittest.TestCase): + """接口地址:自动补全 vs 原样使用。 + + 加这个开关是因为 auto 那套拼接规则只覆盖得了"服务商标准形状"。自建服务的路径 + 常常不按套路,比如 `https://api.example.com/custom/llm/invoke`——auto 会把它 + 拼成 `.../invoke/chat/completions`,请求发到一个不存在的地址,报 404,而排查的 + 人会去怀疑密钥和网络。 + + 为什么用显式开关而不是"更聪明的猜测":`https://api.example.com/openai` 到底是 + 前缀还是完整端点,光看地址分不出来。猜错的两种方向都会静默地把请求发歪。 + """ + + def test_auto_completes_the_path_for_each_kind(self) -> None: + cases = [ + ("openai", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"), + ("openai", "https://api.deepseek.com", "https://api.deepseek.com/chat/completions"), + ("claude", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"), + ("dify", "https://api.dify.ai/v1", "https://api.dify.ai/v1/chat-messages"), + ] + for kind, base, expected in cases: + with self.subTest(kind=kind, base=base): + self.assertEqual(model_protocol.endpoint_url(kind, base), expected) + + def test_auto_does_not_double_append_a_complete_url(self) -> None: + """已经写全的标准地址不该再被拼一次。""" + full = "https://api.openai.com/v1/chat/completions" + self.assertEqual(model_protocol.endpoint_url("openai", full), full) + + def test_exact_uses_the_address_verbatim(self) -> None: + odd = "https://api.example.com/custom/llm/invoke" + self.assertEqual(model_protocol.endpoint_url("openai", odd), f"{odd}/chat/completions") + self.assertEqual(model_protocol.endpoint_url("openai", odd, "exact"), odd) + + def test_exact_wins_for_every_kind(self) -> None: + """exact 是"别动我的地址",不该被接口类型的规则翻掉。""" + odd = "https://gw.internal/llm" + for kind in ("openai", "claude", "dify", "comfyui"): + with self.subTest(kind=kind): + self.assertEqual(model_protocol.endpoint_url(kind, odd, "exact"), odd) + + def test_a_trailing_slash_is_trimmed_in_both_modes(self) -> None: + self.assertEqual( + model_protocol.endpoint_url("openai", "https://x.com/llm/", "exact"), + "https://x.com/llm", + ) + + def test_the_connectivity_test_uses_the_same_rule_as_the_real_call(self) -> None: + """两边规则若不一致,测试会去戳一个和实际调用不同的地址。 + + 那是最没用的一种测试:测通了照样用不了,或者反过来。 + """ + odd = "https://api.example.com/custom/llm/invoke" + for mode in ("auto", "exact"): + with self.subTest(mode=mode): + self.assertEqual( + admin_backend._model_endpoint(odd, "openai", mode), + model_protocol.endpoint_url("openai", odd, mode), + ) + + def test_dify_upload_root_follows_the_mode(self) -> None: + """Dify 传图要拼 /files/upload,根地址得跟着 exact 走。""" + self.assertEqual( + model_protocol.dify_api_root("https://dify.internal/app/chat", "exact"), + "https://dify.internal/app/chat", + ) + + +class EndpointModeStorageTest(unittest.TestCase): + """存和读。老库要能平滑升级,老记录的行为必须一个字都不变。""" + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + self.uid = self.db.authenticate("admin", "Admin@123456")["id"] + + def _save(self, **extra): + item = { + "id": "p1", "name": "出口", "kind": "openai", + "base_url": "https://api.example.com/custom/llm/invoke", + "api_key": "sk-x", "model": "m", + } + item.update(extra) + return self.db.save_model_provider(item, self.uid, "1.1.1.1") + + def test_the_mode_round_trips(self) -> None: + self._save(endpoint_mode="exact") + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "exact") + self.assertEqual(item["endpoint"], "https://api.example.com/custom/llm/invoke") + + def test_the_default_is_auto(self) -> None: + """不填就是老行为。这个默认值是整件事零回归的前提。""" + self._save() + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "auto") + self.assertTrue(item["endpoint"].endswith("/chat/completions")) + + def test_an_unknown_mode_is_refused(self) -> None: + with self.assertRaises(ValueError) as caught: + self._save(endpoint_mode="随便写") + self.assertIn("地址模式", str(caught.exception)) + + def test_an_old_database_without_the_column_is_upgraded_in_place(self) -> None: + """已经在用的库不能因为加一列就要重建表——重建的每一步都可能丢数据。""" + path = self.root / "old.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_providers ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, + base_url TEXT NOT NULL, api_key_enc TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', capabilities TEXT NOT NULL DEFAULT 'text', + max_tokens INTEGER NOT NULL DEFAULT 500, temperature REAL NOT NULL DEFAULT 0.35, + timeout_ms INTEGER NOT NULL DEFAULT 30000, max_inflight INTEGER NOT NULL DEFAULT 32, + rpm_limit INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, + health TEXT NOT NULL DEFAULT 'unknown', health_checked_at TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by INTEGER); + INSERT INTO model_providers (id,name,kind,base_url,created_at,updated_at) + VALUES ('old-1','老出口','openai','https://api.openai.com/v1','x','x'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + item = next( + x for x in admin_backend.Database(path).model_providers() if x["id"] == "old-1" + ) + self.assertEqual(item["endpoint_mode"], "auto", "老记录必须还是老行为") + self.assertEqual(item["endpoint"], "https://api.openai.com/v1/chat/completions") + + def test_running_the_column_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.assertTrue(self.db.model_providers() is not None) + + +class ModelCallTraceabilityTest(unittest.TestCase): + """`model_calls` 存客户原话、模型原话、审核原因——用来"跟踪问题"。 + + 网关和桌面端各自都会往这张表报一次同一次调用(网关看得到候选和裁判, + 桌面端才知道审核规则命中原因)。两边共用 task_id,谁先落盘谁建行,后到 + 的一方只补自己独有的字段——这里测的就是这道合并逻辑不多写也不少写。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _record(self, **extra) -> dict: + record = { + "device_id": "dev-1", + "task_id": "task-1", + "roles_version": 1, + "judge_mode": "arbitrate", + "chosen": "openai-primary", + "judge": {"winner": "A", "score": 0.8, "risk": "low"}, + "candidates": [{"provider": "openai-primary", "text": "建议您注意休息"}], + "total_ms": 900, + "customer_text": "我这几天总是失眠", + "reply_text": "建议您注意休息", + "review_reason": "", + } + record.update(extra) + return record + + def _row(self, task_id: str) -> sqlite3.Row: + with self.db.connect() as con: + return con.execute( + "SELECT * FROM model_calls WHERE task_id = ?", (task_id,) + ).fetchone() + + def test_a_call_round_trips_customer_and_reply_text(self) -> None: + self.db.log_model_call(self._record()) + row = self._row("task-1") + self.assertEqual(row["customer_text"], "我这几天总是失眠") + self.assertEqual(row["reply_text"], "建议您注意休息") + self.assertEqual(row["review_reason"], "") + + def test_a_second_report_with_the_same_task_id_only_merges_review_reason(self) -> None: + """网关先落盘(候选、裁判都是权威数据),桌面端后到——不能把网关的数据覆盖掉。""" + self.db.log_model_call(self._record()) + self.db.log_model_call(self._record( + chosen="不该生效的值", + candidates=[{"provider": "不该生效的值", "text": "x"}], + total_ms=1, + review_reason="命中审核规则「诊断」", + )) + with self.db.connect() as con: + rows = con.execute( + "SELECT * FROM model_calls WHERE task_id='task-1'" + ).fetchall() + self.assertEqual(len(rows), 1, "同一个 task_id 只能落一行,不是两行") + row = self._row("task-1") + self.assertEqual(row["chosen"], "openai-primary", "第二次上报不该覆盖已有的候选数据") + self.assertEqual(row["review_reason"], "命中审核规则「诊断」", "审核原因只有桌面端知道,必须生效") + + def test_blank_task_ids_never_collide(self) -> None: + """本地非网关路径目前还给不出 task_id,留空时按老行为各插一行。""" + self.db.log_model_call(self._record(task_id="", customer_text="第一条")) + self.db.log_model_call(self._record(task_id="", customer_text="第二条")) + with self.db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id=''" + ).fetchone()["n"] + self.assertEqual(n, 2) + + def test_list_model_calls_finds_a_keyword_in_either_side_of_the_exchange(self) -> None: + self.db.log_model_call(self._record(task_id="t1", customer_text="想咨询糖尿病用药")) + self.db.log_model_call(self._record(task_id="t2", customer_text="今天天气不错", reply_text="是呀")) + found = self.db.list_model_calls(days=7, q="糖尿病") + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "t1") + + def test_list_model_calls_parses_candidates_back_into_a_list(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + found = self.db.list_model_calls(days=7) + self.assertEqual(found["items"][0]["candidates"][0]["provider"], "openai-primary") + + def test_list_model_calls_respects_the_days_window(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + with self.db.connect() as con: + con.execute( + "UPDATE model_calls SET created_at = '2000-01-01 00:00:00' WHERE task_id='t1'" + ) + con.commit() + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 0) + + def test_an_old_database_with_duplicate_task_ids_is_deduped_before_the_index_goes_on(self) -> None: + """老版本拿会话指纹当 task_id,同一个会话十几轮全共享一个值——建唯一索引前必须先清干净。""" + path = self.root / "old_calls.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p1','2026-01-01 00:00:01'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p2','2026-01-01 00:00:02'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p3','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + rows = con.execute( + "SELECT task_id, chosen FROM model_calls ORDER BY id" + ).fetchall() + self.assertEqual([r["task_id"] for r in rows], ["", "", "same-session"], "只留最新一行的 task_id") + self.assertEqual(rows[-1]["chosen"], "p3", "保留的必须是最新那一行") + # 索引确实建起来了:迁移后再报一次同名 task_id 应该走合并而不是新插一行 + db.log_model_call({"task_id": "same-session", "review_reason": "新原因"}) + with db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id='same-session'" + ).fetchone()["n"] + merged = con.execute( + "SELECT chosen, review_reason FROM model_calls WHERE task_id='same-session'" + ).fetchone() + self.assertEqual(n, 1, "唯一索引必须已经生效,不能再插出第二行") + self.assertEqual(merged["chosen"], "p3", "合并不该覆盖已有数据") + self.assertEqual(merged["review_reason"], "新原因") + + def test_running_the_model_calls_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.db.log_model_call(self._record(task_id="tx")) + self.assertEqual(self._row("tx")["chosen"], "openai-primary") + + +class GuardCallsStayOutOfTheCustomerLogTest(unittest.TestCase): + """界面识别调用不能混进"客服对话"的调用日志和调用统计。 + + 这是用户真实报上来的 bug:调用记录点开一看,客户消息和模型回复全是「(空)」, + 候选里全是 `{"state":"unknown","navigation_right_ratio":0,...}`——那是机器人 + 自己在看企业微信窗口,不是在回客户。守卫每轮轮询都要问一次模型,条数远多于 + 真实对话,不分开的话这张表根本没法用来查问题,裁判分布也在给错误的东西打分。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _log(self, task_id: str, purpose: str, **extra) -> None: + record = { + "device_id": "d1", "task_id": task_id, "chosen": "p1", + "judge": {"winner": "A", "score": 0.9, "risk": "low"}, + "candidates": [{"provider": "p1", "text": "文本"}], "total_ms": 100, + "customer_text": "客户说的", "reply_text": "模型答的", + "purpose": purpose, + } + record.update(extra) + self.db.log_model_call(record) + + def test_the_default_log_shows_only_customer_chats(self) -> None: + self._log("c1", "chat") + self._log("g1", "guard") + self._log("g2", "guard") + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "c1") + + def test_guard_calls_can_still_be_listed_on_purpose(self) -> None: + """内部调用照样在花钱,需要的时候得能翻出来看,不能查无此物。""" + self._log("c1", "chat") + self._log("g1", "guard") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + self.assertEqual(self.db.list_model_calls(days=7, purpose="")["total"], 2) + + def test_stats_ignore_guard_calls(self) -> None: + """裁判分布要量的是"发给客户的回复够不够好",不是"布局认得准不准"。""" + self._log("c1", "chat", judge={"winner": "A", "score": 0.9, "risk": "low"}) + for index in range(5): + self._log( + f"g{index}", "guard", + judge={"winner": "B", "score": 0.1, "risk": "high"}, + ) + stats = self.db.model_call_stats(7) + self.assertEqual(stats["total"], 1, "5 条界面识别不该被算成客服调用") + self.assertEqual(stats["judged"], 1) + self.assertAlmostEqual(stats["avg_score"], 0.9, places=3) + self.assertNotIn("high", stats["risk"], "守卫的低分不该污染风险占比") + + def test_a_call_without_an_explicit_purpose_counts_as_a_customer_chat(self) -> None: + """桌面端补报的那条路不带 purpose——默认必须是 chat,不能凭空消失。""" + self.db.log_model_call({ + "task_id": "c9", "chosen": "p1", "judge": {}, "candidates": [], + "customer_text": "在吗", "reply_text": "在的", + }) + self.assertEqual(self.db.list_model_calls(days=7)["total"], 1) + + def test_legacy_guard_rows_are_reclassified_by_their_fingerprint(self) -> None: + """老库里的守卫调用落的是默认 chat,要按候选内容的指纹认出来并改判。 + + 指纹用的是分类器专用的 JSON 字段名(布局守卫的 navigation_right_ratio、 + 会话行分类器的 reply_capable)——正常客服回复里不可能出现这些词。 + """ + path = self.root / "legacy.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (task_id,candidates_json,created_at) VALUES + ('L1','[{"provider":"a","text":"{\\"state\\":\\"unknown\\",\\"navigation_right_ratio\\":0}"}]','2026-01-01 00:00:01'), + ('L2','[{"provider":"a","text":"{\\"kind\\":\\"customer_chat\\",\\"reply_capable\\":true}"}]','2026-01-01 00:00:02'), + ('L3','[{"provider":"a","text":"忙完了,歇会儿吧,最近还好吗"}]','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + marks = { + row["task_id"]: row["purpose"] + for row in con.execute("SELECT task_id, purpose FROM model_calls") + } + self.assertEqual(marks["L1"], "guard", "布局守卫的记录要改判") + self.assertEqual(marks["L2"], "guard", "会话行分类器的记录也要改判") + self.assertEqual(marks["L3"], "chat", "真实客服回复绝不能被误判成内部调用") + + def test_the_reclassification_never_deletes_anything(self) -> None: + self._log("g1", "chat", candidates=[ + {"provider": "a", "text": '{"navigation_right_ratio":0.1}'} + ]) + before = self.db.list_model_calls(days=7, purpose="")["total"] + with self.db.connect() as con: + admin_backend.Database._backfill_guard_purpose(con) + con.commit() + after = self.db.list_model_calls(days=7, purpose="")["total"] + self.assertEqual(before, after, "只改分类标记,一行都不能少") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + + +def setUpModule(): + # 别让测试读到开发机上的真实桌面端配置——配过模型网关的机器会让 + # `ai_chat.current_provider()` 整体改走网关分支,一大片无关测试跟着变行为。 + local_state_redirect.start() + + +def tearDownModule(): + local_state_redirect.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-ignore-20260929/tests/test_remote_api.py b/deploy/mobile-ignore-20260929/tests/test_remote_api.py new file mode 100644 index 000000000..120d628b0 --- /dev/null +++ b/deploy/mobile-ignore-20260929/tests/test_remote_api.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_plain_http_login_is_allowed_by_default(self): + with mock.patch.dict(os.environ): + os.environ.pop("MOBILE_REQUIRE_HTTPS") + self.mobile_login(headers={"X-Real-IP": "114.114.114.114"}) + self.mobile_login("kefu2", headers={}) + self.assertEqual(self.zyt.login_calls, 2) + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-ignore-20260929/tests/test_zyt_auth.py b/deploy/mobile-ignore-20260929/tests/test_zyt_auth.py new file mode 100644 index 000000000..a5af82759 --- /dev/null +++ b/deploy/mobile-ignore-20260929/tests/test_zyt_auth.py @@ -0,0 +1,100 @@ +"""跨服务器桌面认证:只在路由缺失时兼容,拒绝结果不能降级。""" +import io +import json +import unittest +import urllib.error +from unittest import mock + +import zyt_auth + + +class DesktopVerificationRegressionTest(unittest.TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def session(self, **updates): + data = {"user_id": "admin:9", "terminal": 7, "status": "active", + "root": False, "role_ids": [2], "permissions": ["tcm.diagnosis/lists"]} + data.update(updates) + return {"code": 1, "data": data} + + def verify(self): + return zyt_auth.verify_desktop_token("test-token", api_url="https://identity.example.test") + + def missing(self, status=404): + return urllib.error.HTTPError("https://identity.example.test", status, "test", None, None) + + def test_success_never_requests_ip_bound_web_profile(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(self.session(root=True, permissions=["*"])), + self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertTrue(identity["root"]) + self.assertEqual(identity["role_ids"], [2]) + self.assertEqual(identity["permissions"], ["*"]) + self.assertTrue(identity["permissions_known"]) + + def test_missing_permissions_do_not_trigger_profile_request(self): + payload = self.session() + del payload["data"]["permissions"] + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertEqual(identity["permissions"], []) + self.assertFalse(identity["permissions_known"]) + + def test_empty_permissions_are_known(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(self.session(permissions=[]))): + identity = self.verify() + self.assertTrue(identity["permissions_known"]) + self.assertEqual(identity["permissions"], []) + + def test_only_missing_routes_allow_legacy_verification(self): + for status in (404, 405): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(status), self.response({"code": 1, "data": { + "user": {"id": 9, "disable": 0, "role_ids": [2]}, "permissions": []}}), + ]) as opened: + identity = self.verify() + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(opened.call_count, 2) + + def test_http_rejections_do_not_fall_back(self): + for status in (401, 403, 500, 502): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=self.missing(status)) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_invalid_identity_cannot_be_accepted_through_web_fallback(self): + failures = [{"code": -1, "msg": "登录已失效"}, {"code": "bad"}, [], + {"code": 1, "data": []}, self.session(user_id=""), + self.session(terminal=1), self.session(terminal="bad"), + self.session(status="disabled")] + for payload in failures: + with self.subTest(payload=payload), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(payload), self.response({"code": 1, "data": {"user": {"id": 9}}}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_bad_json_and_network_failure_do_not_fall_back(self): + for result in (io.BytesIO(b"error"), urllib.error.URLError("offline")): + with self.subTest(result=result), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[result]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_legacy_ip_error_identifies_missing_server_deployment(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(), self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]): + with self.assertRaisesRegex(zyt_auth.ZytAuthError, "/adminapi/desktop/session"): + self.verify() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-remote-20260928/01-preflight.sh b/deploy/mobile-remote-20260928/01-preflight.sh new file mode 100644 index 000000000..2cf39bd53 --- /dev/null +++ b/deploy/mobile-remote-20260928/01-preflight.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# 基线核对、备份将被替换的源文件、给当前镜像打回滚标签。只读线上 + 写本目录。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +test ! -e baseline-check.json +python3 remote_server_guard.py baseline > baseline-check.json.tmp +mv baseline-check.json.tmp baseline-check.json +cat baseline-check.json +test ! -e source-before +test ! -e preflight-complete +if docker image inspect zyt/wecom-admin:before-mobile-remote-20260928 >/dev/null 2>&1; then + echo 'Refuse to replace existing rollback tag' >&2; exit 1 +fi +mkdir -m 700 source-before +python3 - <<'PY' +from pathlib import Path +import json, shutil +r = Path('/home/ps/mobile-remote-20260928'); project = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + target = r / 'source-before' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(project / name, target) +PY +docker tag zyt/wecom-admin:current zyt/wecom-admin:before-mobile-remote-20260928 +docker image inspect zyt/wecom-admin:current --format '{{.Id}}' > image-before.txt +printf '%s\n' ready > preflight-complete +echo "preflight complete; rollback image tagged before-mobile-remote-20260928 ($(cat image-before.txt))" diff --git a/deploy/mobile-remote-20260928/02-backup-build.sh b/deploy/mobile-remote-20260928/02-backup-build.sh new file mode 100644 index 000000000..8cbfa6c94 --- /dev/null +++ b/deploy/mobile-remote-20260928/02-backup-build.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# 数据库一致性备份 + 构建候选镜像 + 离线跑测试 + 核对候选镜像文件哈希。线上服务不受影响。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +test -f preflight-complete +test ! -e candidate-passed +python3 remote_server_guard.py baseline +test "$(docker image inspect zyt/wecom-admin:before-mobile-remote-20260928 --format '{{.Id}}')" = "$(cat image-before.txt)" +# 新表都是 CREATE TABLE IF NOT EXISTS 的纯新增;备份仍然先做,作为安全网。 +# backup.json 只在备份和 quick_check 都成功后才出现;本脚本在测试阶段停下后重跑时沿用它,不重复拷整库。 +if [ -f backup.json ]; then + echo "reusing backup: $(cat backup.json)" + docker exec -i im-admin-api-1 python - "$(cat backup.json)" <<'PY' +from pathlib import Path +import json, sys +info = json.loads(sys.argv[1]); folder = Path(info['backup']) +assert info['quick_check'] == 'ok' and (folder / 'backend.db').stat().st_size == info['bytes'], 'Recorded backup changed' +assert len(list(folder.glob('*.key'))) == info['keyCount'], 'Key backup missing' +PY +else +docker exec -i im-admin-api-1 python - <<'PY' > backup.json.tmp +from pathlib import Path +import sqlite3, shutil, os, json +folder = Path('/data/backups/mobile-remote-20260928'); folder.mkdir(mode=0o700) +source = sqlite3.connect('file:/data/backend.db?mode=ro', uri=True, timeout=30) +backup = sqlite3.connect(folder / 'backend.db') +try: + source.backup(backup, pages=0) +finally: + backup.close(); source.close() +with sqlite3.connect('file:' + str(folder / 'backend.db') + '?mode=ro', uri=True) as check: + assert check.execute('PRAGMA quick_check').fetchone()[0] == 'ok' +os.chmod(folder / 'backend.db', 0o600) +keys = list(Path('/data').glob('*.key')); assert keys, 'Encryption key backup missing' +for p in keys: + shutil.copy2(p, folder / p.name); os.chmod(folder / p.name, 0o600) +print(json.dumps({'backup': str(folder), 'quick_check': 'ok', 'bytes': (folder / 'backend.db').stat().st_size, 'keyCount': len(keys)})) +PY +mv backup.json.tmp backup.json +cat backup.json +fi +docker build --network=none --pull=false -f Dockerfile.overlay -t zyt/wecom-admin:mobile-remote-20260928 . > candidate-build.log 2>&1 +# serverTestModules 随包放在 tests/;imageTestModules 用镜像自带的测试文件(PYTHONPATH 后一项)。 +# -P:不把工作目录 /app/wechat_rpa 放到 sys.path 最前面,否则镜像里同名的旧测试会盖住随包的新测试。 +mapfile -t modules < <(python3 -c 'import json;p=json.load(open("release-plan.json"));print("\n".join(p["serverTestModules"]+p.get("imageTestModules",[])))') +test "${#modules[@]}" -gt 0 +docker run --rm --network=none --read-only --tmpfs /tmp:size=512m,mode=1777 --tmpfs /data:size=256m,mode=1777 \ + -v "$release/tests:/release-tests:ro" -e PYTHONPATH=/release-tests:/app/wechat_rpa \ + zyt/wecom-admin:mobile-remote-20260928 python -X utf8 -B -P -m unittest "${modules[@]}" -q > candidate-tests.log 2>&1 +tail -n 3 candidate-tests.log +python3 remote_server_guard.py candidate +printf '%s\n' passed > candidate-passed diff --git a/deploy/mobile-remote-20260928/03-deploy.sh b/deploy/mobile-remote-20260928/03-deploy.sh new file mode 100644 index 000000000..ee376a9d1 --- /dev/null +++ b/deploy/mobile-remote-20260928/03-deploy.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# 替换 2 个后端文件、新增 2 个后端文件和 /m/ 前端、更新 Dockerfile;切换镜像,只重建 api 容器。 +# 任何一步失败自动调用 rollback.sh。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +test -f candidate-passed +test -f backup.json +python3 remote_server_guard.py baseline +cd /opt/im-admin/deploy/im-admin +docker compose config --quiet +rollback_on_error() { rc=$?; trap - ERR; bash "$release/rollback.sh"; exit "$rc"; } +trap rollback_on_error ERR +python3 - <<'PY' +from pathlib import Path +import json, hashlib, os +r = Path('/home/ps/mobile-remote-20260928'); target = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +reference = (target / 'wechat_rpa/admin_api.py').stat().st_mode & 0o777 +rows = [row for row in json.loads((r / 'manifest.json').read_text()) if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +# 前端整体先放临时目录,写完再一次性改名,外面看不到半套静态文件。 +staging = target / (frontend + '.mobile-remote.tmp'); assert not staging.exists() +for row in rows: + rel = row['path'][len('payload/'):] + data = (r / row['path']).read_bytes(); assert hashlib.sha256(data).hexdigest() == row['sha256'], rel + if rel.startswith(frontend + '/'): + dest = staging / rel[len(frontend) + 1:] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data); os.chmod(dest, 0o644) + continue + dest = (target / rel).resolve(); assert dest.is_relative_to(target) + tmp = dest.with_name(dest.name + '.mobile-remote.tmp'); assert not tmp.exists() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, dest.stat().st_mode & 0o777 if dest.exists() else reference); os.replace(tmp, dest) +for directory in [staging] + [p for p in staging.rglob('*') if p.is_dir()]: + os.chmod(directory, 0o755) +final = target / frontend; assert not final.exists() +final.parent.mkdir(parents=True, exist_ok=True) +os.replace(staging, final) +PY +docker tag zyt/wecom-admin:mobile-remote-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +bash "$release/04-verify.sh" +trap - ERR +printf '%s\n' 'Mobile remote console deployed and verified.' diff --git a/deploy/mobile-remote-20260928/04-verify.sh b/deploy/mobile-remote-20260928/04-verify.sh new file mode 100644 index 000000000..8ada5445d --- /dev/null +++ b/deploy/mobile-remote-20260928/04-verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +python3 remote_server_guard.py deployed | tee deployed-verification.json diff --git a/deploy/mobile-remote-20260928/Dockerfile.overlay b/deploy/mobile-remote-20260928/Dockerfile.overlay new file mode 100644 index 000000000..2ca792b68 --- /dev/null +++ b/deploy/mobile-remote-20260928/Dockerfile.overlay @@ -0,0 +1,8 @@ +ARG BASE_IMAGE=zyt/wecom-admin:before-mobile-remote-20260928 +FROM ${BASE_IMAGE} +COPY --chown=app:app payload/wechat_rpa/admin_api.py /app/wechat_rpa/admin_api.py +COPY --chown=app:app payload/wechat_rpa/zyt_auth.py /app/wechat_rpa/zyt_auth.py +COPY --chown=app:app payload/wechat_rpa/remote_api.py /app/wechat_rpa/remote_api.py +COPY --chown=app:app payload/wechat_rpa/remote_store.py /app/wechat_rpa/remote_store.py +COPY --chown=app:app payload/admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ +WORKDIR /app/wechat_rpa diff --git a/deploy/mobile-remote-20260928/RELEASE.md b/deploy/mobile-remote-20260928/RELEASE.md new file mode 100644 index 000000000..462c19614 --- /dev/null +++ b/deploy/mobile-remote-20260928/RELEASE.md @@ -0,0 +1,132 @@ +# 手机远程控制台上线包(mobile-remote-20260928) + +状态:**管理端已于 2026-09-28 15:21 部署到生产并验证通过(记录:`C:/kefu/deploy/mobile-remote-deployed.md`)。ZYT 由 ZYT 团队自行发布;桌面端 1.5.1 由管理员在「版本升级」发布。** + +## 本包内容(管理端服务器 `/opt/im-admin`) + +| 类型 | 文件 | 说明 | +|---|---|---| +| 替换 | `wechat_rpa/admin_api.py` | 注册手机 / 命令通道路由;`/m/` 静态托管;强制下线、停用账号时同时收回手机会话;客户端账号列表多返回 `mobile_session_count` | +| 替换 | `wechat_rpa/zyt_auth.py` | 新增终端 9 的代登(`login_admin`)、身份复核(`verify_admin_session`)、注销;桌面端原有校验不变 | +| 新增 | `wechat_rpa/remote_api.py` | `/api/v2/mobile/*`、`/api/v2/desktop/commands/*`、`/api/v2/desktop/status`、`/api/v2/desktop/blacklist*` | +| 新增 | `wechat_rpa/remote_store.py` | 4 张新表的数据层 | +| 新增 | `admin-web/apps/mobile-h5/dist/` | 手机 / 平板 H5(`/m/`) | +| 替换 | `deploy/im-admin/Dockerfile` | 以后整包重建时把 mobile-h5 一起打进镜像 | + +数据库:启动时 `CREATE TABLE IF NOT EXISTS` 新增 `mobile_sessions`、`desktop_commands`、`desktop_status`、 +`conversation_blacklist`,不改任何已有表。回滚保留这些表(旧代码不会读写)。 + +发布包:`mobile-remote-server-bundle.tar.gz`,522,202 字节,SHA-256 +`dc59f2a6278ae04da1f8a4c6193373317126a2fda184cdf3335004cfd06a751e`(由 `prepare_bundle.py` 生成;源码再改动要重新生成并更新这里)。 +服务器上保存为 `mobile-remote-server-bundle-r2.tar.gz`。 + +**生产基线不是 `C:/kefu` 的 HEAD**:2026-09-23 的 COS 直传发布从 `C:/wechat_rpa/tmp/cos-direct-server-payload` +替换了生产的 `admin_api.py`、`archive_store.py`(CRLF 换行)。`baseline-snapshot/` 保存了生产上被替换 3 个文件的原样字节, +`server-baseline.json` 记录其哈希,预检脚本在宿主和容器内再核对一遍,任何漂移即停止。本包的 `admin_api.py` += 生产版 + 本次改动(三方合并无冲突;`prepare_bundle.py` 每次打包都重做"HEAD→生产"合并并要求结果不变, +防止落后的本地副本把生产改动冲掉)。`C:/kefu/wechat_rpa` 的 `admin_api.py`、`archive_store.py`、`archive_api.py` +已同步到"生产 + 本次改动"(后两个文件本地原本只是落后于生产,同步没有丢任何本地内容)。 + +## 上线顺序 + +1. **ZYT(`C:/zyt`,单独仓库,ZYT 团队自行发布)**:已提交到 `master`(`694c83dc7`、`3cdc812f2`),参考副本见 + `zyt/`:新增终端 9、`LoginValidate` 接受 9、`/adminapi/desktop/session` 同时接受 7 和 9 并返回真实终端; + `add_wecom_rpa_remote_menu.sql`(隐藏目录"企微客服助手"下 4 个按钮权限,默认不授予任何角色)。 + 终端 8 已被 ZYT 的 AI 浏览器占用,所以用 9。ZYT 上线前手机登录会提示 + "ZYT 尚未开通企微客服手机端登录(终端 9)",其余功能不受影响(管理端可以先于 ZYT 上线)。 + **SQL 必须和终端 9 代码同一次上线(或更早)**:ZYT 对"拥有全部菜单"的角色返回 `*`,新菜单项不存在时 + 这类角色会被客服后台当成全权限(可控制所有电脑、移出黑名单);SQL 执行后它们就不再是"全部菜单"。 +2. **本包**(下文步骤)。 +3. **HTTPS 入口**:见 `deploy/im-admin/im.zhenyangtang.cn.https.conf.example`(证书 + 公网端口转发 + Nginx)。 + 配好之前只能在内网用 HTTP 登录手机端,公网明文登录会被后台拒绝。上线后用手机 4G(不连公司 Wi-Fi) + 打开 `http://im.zhenyangtang.cn:8369/m/` 试登录,应提示改用 HTTPS:如果能登录,说明路由器端口转发改写了 + 来源地址(公网客户端看起来像内网),需要调整转发或把 `MOBILE_REQUIRE_HTTPS` 设为 `always`。 +4. **桌面端 1.5.1**(`C:/wechat_rpa`):`build_windows_exe.ps1` → `build_installer.ps1`,在管理端 + 「版本升级」先按小比例灰度发布。旧版桌面端在手机上显示"版本过低",不会收到指令。 +5. **灰度**:在 ZYT 角色管理里先给 1~2 位客服所在角色勾选 `wecom_rpa.remote/control`、 + `wecom_rpa.blacklist/add`,主管角色加 `wecom_rpa.blacklist/remove`(需要跨账号控制再加 + `wecom_rpa.remote/all`);试用无问题再全员放开。 + +## 上传(内网模式:SSH 绑定有线网卡) + +```bash +ssh -b 192.168.123.43 ps@192.168.110.111 'mkdir -m 700 /home/ps/mobile-remote-20260928' +scp -o BindAddress=192.168.123.43 C:/kefu/deploy/mobile-remote-20260928/mobile-remote-server-bundle.tar.gz ps@192.168.110.111:/home/ps/mobile-remote-20260928/ +ssh -b 192.168.123.43 ps@192.168.110.111 'cd /home/ps/mobile-remote-20260928 && echo "dc59f2a6278ae04da1f8a4c6193373317126a2fda184cdf3335004cfd06a751e mobile-remote-server-bundle.tar.gz" | sha256sum -c - && tar -xzf mobile-remote-server-bundle.tar.gz' +``` + +## 部署(在服务器上依次执行) + +```bash +export REMOTE_RELEASE_APPROVED=mobile-remote-20260928 +bash /home/ps/mobile-remote-20260928/01-preflight.sh # 基线核对、备份源文件、打回滚镜像标签 +bash /home/ps/mobile-remote-20260928/02-backup-build.sh # 数据库一致性备份 + 候选镜像 + 离线测试(test_remote_api、test_zyt_auth、test_backend、test_archive_api) +bash /home/ps/mobile-remote-20260928/03-deploy.sh # 放文件、切镜像、只重建 api 容器、04 验证;失败自动回滚 +``` + +`04-verify.sh` 核对:宿主与容器内文件哈希、api 容器健康且无重启、网关健康、4 个新接口匿名访问返回 401、 +`/m/` 返回入口页、4 张新表存在且 `quick_check` 为 ok。不登录、不读取任何聊天或患者数据。 + +手动回滚: + +```bash +REMOTE_RELEASE_APPROVED=mobile-remote-20260928 bash /home/ps/mobile-remote-20260928/rollback.sh +``` + +## 本地验证记录(2026-09-28) + +- 管理端后端:`test_admin_api`、`test_archive_api`、`test_zyt_order_api`、`test_zyt_patient_client`、 + `test_zyt_auth`、`test_knowledge`、`test_remote_api` 共 224 项通过。新增的 31 项覆盖:登录限流(含 12 个并发 + 错误密码最多 4 个到达 ZYT)、"账号不存在"不耗预算、成功登录清零预算、初始密码拒绝、公网 HTTPS 与缺失 + X-Real-IP 时拒绝明文、ZYT 锁定短路、令牌互不通用、跨账号越权、命令去重 / 取代 / 过期 / 长轮询唤醒、 + 黑名单权限与同步、`/m/` 托管(缺失资源 404、防嵌入头)。 +- 代码审查:另起两个独立审查(后台 + H5、桌面端 + ZYT),确认的问题均已修复并补了测试。 +- ZYT:`AdminDesktopAuthContractTest`、`AdminDesktopSessionBehaviorTest` 通过(PHP 8.2);SQL 在 MySQL 5.7 + 临时库连续执行两次,结果一致(幂等)。 +- 桌面端:新增 30 项测试(黑名单缓存合并、账号切换改绑、服务端拒收条目、以服务端为准的移出、路由缺失的 404 + 不删本机拦截、首个网络失败即停止批量移出、命令通道去重 / 过期 / 服务端时钟 / 同步冷却 / 不空转 / 网络异常 + 不退出、超时指令作废,主窗口执行指令时的协议检测等待、先等旧线程退出再启动)。另用真实桌面端代码对接真实 + 后台:启动指令 0.3 秒内执行并回执,手机端加入 / 移出黑名单几秒内同步到电脑本地缓存,无权限的电脑端移出被拒绝。 +- H5:`vue-tsc`、`vitest` 6 项、生产构建通过;浏览器端到端(假 ZYT + 真实后台 + 模拟电脑)走通登录、 + 错误密码提示、远程停止 / 启动、主管切换客服、黑名单加入 / 移出、平板两栏、深色模式、CSP 下图标正常、 + 发版后缺失页面代码时自动刷新一次且不循环。 + +- **按生产基线重建后的候选(2026-09-28)**:生产 104 个后端文件 + 本包 4 个文件,本地跑 `test_remote_api`、 + `test_zyt_auth`、`test_admin_api`、`test_archive_api`、`test_backend` 共 241 项,2 项失败——两项在**未改动的生产代码上 + 同样失败**(生产镜像里的 `test_admin_api` 早于 9-23 COS 下载地址改动;`test_backend` 仍按旧的两次请求模拟 ZYT + 会话接口),与本次无关;`C:/kefu` 较新的 `test_backend` 对同一候选 72 项全部通过,随包带上服务器跑。 + +## 行为变化与风险 + +- **移出黑名单以服务端为准**,需要 `wecom_rpa.blacklist/remove`:没有这个权限的客服不能再在电脑上移出已同步的 + 黑名单(本机刚加、尚未同步的条目仍可直接移出)。加入黑名单在电脑上不需要 ZYT 权限(拉黑是安全方向), + 在手机上需要 `wecom_rpa.blacklist/add`。 +- **ZYT 按来源 IP 锁定登录**(默认 30 分钟错 5 次锁 30 分钟),手机登录全部从服务器出口 IP 发出。后台限流: + 每账号 5 次 / 10 分钟、每客户端 IP 10 次 / 10 分钟、全局"真正发到 ZYT 的密码错误"4 次 / 30 分钟 + (`MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET`,0 为关闭),并记住 ZYT 返回的锁定时长不再重试。正在进行中的尝试 + 也占名额,并发请求无法一起穿过限流;"账号不存在"不消耗全局预算(ZYT 也不计数),任一次密码正确的登录会 + 清零预算(与 ZYT 清零计数一致)。如果服务器和办公电脑共用出口 IP,ZYT 一旦锁定也会影响电脑端登录—— + 建议 ZYT 侧评估对该出口 IP 放宽阈值或改为按账号计数。 +- **仍在使用初始密码的 ZYT 账号不能登录手机端**(ZYT 返回 `is_paw=0`),提示先到 ZYT 网页后台改密码。 +- **`/m/` 带 CSP、`X-Frame-Options: DENY`**,不能被其他网站嵌入诱导点击;发版后旧页面请求已不存在的 + 资源文件会得到 404,前端自动整页刷新一次加载新版本。 +- **(已被 mobile-http-20260928 取代:16:26 起默认 `never`,内外网都可用 HTTP 登录)** ~~公网明文 HTTP 的手机登录被拒绝~~(`MOBILE_REQUIRE_HTTPS=auto`;`always` / `never` 可覆盖)。判断用 Nginx 写入的 + `X-Real-IP`,不信任客户端自带的 `X-Forwarded-For`。 +- **手机会话**最长 30 天,每 10 分钟向 ZYT 复核一次身份与权限;ZYT 终端 9 令牌默认 8 小时无操作过期,过期后需重新 + 登录。ZYT 暂时不可达时沿用上次权限、不强制下线。 +- **只能控制正在运行的程序**(包括收在托盘里);程序没开时手机端显示离线,无法远程拉起。 +- **同一台电脑换了 ZYT 账号登录**:本机黑名单缓存改绑新账号,原账号已同步的条目转为"待上传"继续拦截,并上传到 + 新账号名下(主管可在手机端查看、移出),不会因为换号而静默放开。 +- 桌面端通道:长轮询 15 秒(短于容器 20 秒的停机宽限);任何网络异常只退避不退出;指令有效期按服务端时钟判断; + 界面线程 45 秒内没执行的指令会作废,不会在回执"超时"之后又被执行。 + +## 与方案的差异 + +- 手机会话单独建表 `mobile_sessions`,没有在 `desktop_devices` 上加 `client_kind`:`desktop_sessions` 有"每账号只有 + 一个有效会话"的唯一索引,手机会话写进去会顶掉值班电脑;分表后两种令牌也天然不能互相冒用。 +- 桌面端长轮询是 `POST /api/v2/desktop/commands/poll`(请求体带状态,最长挂起 15 秒,方案中为 25 秒: + 需要短于容器停机宽限,指令本身靠进程内通知即时送达),另加 `POST /api/v2/desktop/status` 在监听状态变化时立即上报。 +- 黑名单变化不走命令,而是长轮询返回的 `blacklist_version`;桌面端版本不一致时调用 + `POST /api/v2/desktop/blacklist/sync`(补传本机新增 + 拉全量)。 +- ZYT 新权限默认不授予任何角色(与此前新增菜单的做法一致),按"灰度"一步在角色里勾选。 +- vben 管理端页面本次未改动:强制下线按钮已同时收回手机会话,接口多返回的 `mobile_session_count` 可在后续加列展示。 diff --git a/deploy/mobile-remote-20260928/baseline-snapshot/deploy/im-admin/Dockerfile b/deploy/mobile-remote-20260928/baseline-snapshot/deploy/im-admin/Dockerfile new file mode 100644 index 000000000..8c3423b23 --- /dev/null +++ b/deploy/mobile-remote-20260928/baseline-snapshot/deploy/im-admin/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +COPY deploy/im-admin/requirements.txt /tmp/requirements.txt +RUN pip install --disable-pip-version-check -r /tmp/requirements.txt \ + && useradd --create-home --uid 10001 --shell /usr/sbin/nologin app + +COPY --chown=app:app wechat_rpa/*.py /app/wechat_rpa/ +COPY --chown=app:app admin-web/apps/web-antd/dist/ /app/admin-web/apps/web-antd/dist/ + +USER app +WORKDIR /app/wechat_rpa + +EXPOSE 8766 8770 + +CMD ["python", "admin_api.py", "--db", "/data/backend.db", "--host", "0.0.0.0", "--port", "8766"] diff --git a/deploy/mobile-remote-20260928/baseline-snapshot/wechat_rpa/admin_api.py b/deploy/mobile-remote-20260928/baseline-snapshot/wechat_rpa/admin_api.py new file mode 100644 index 000000000..940cc2d35 --- /dev/null +++ b/deploy/mobile-remote-20260928/baseline-snapshot/wechat_rpa/admin_api.py @@ -0,0 +1,1560 @@ +# -*- coding: utf-8 -*- +"""管理后台 JSON API(FastAPI)。 + +当初另起一个,是因为老的 `admin_backend.py` 是服务端渲染的 HTML 后台——表单 POST +加整页刷新。Vue 前端要的是 JSON API + 明确的权限清单,两种形态没法在一个处理器 +里长期共存。 + +绞杀者模式已经走完:老网页后台(8765)整体退役,`admin_backend.py` 只剩数据层 +(Database / 配置校验 / 模型测试),这个模块是唯一的 HTTP 入口。 + + admin_backend.py 数据层,被本模块和 model_gateway.py 共用 + admin_api.py JSON API + Vue 前端静态托管 + 桌面端同步,全在这一个 + +权限的判法只有一种:`Depends(require("model:write"))`。代码里**永远不判角色名** +——判角色名就等于把运营策略焊死在代码里,加个角色都得发版。 + +跑起来: + uvicorn admin_api:create_app --factory --host 127.0.0.1 --port 8766 + # 或 + python admin_api.py --db backend.db --port 8766 +""" + +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import hmac +import json +import os +import re +import time +import urllib.error +import urllib.parse +from pathlib import Path +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, RedirectResponse +from pydantic import BaseModel, Field, field_validator + +import admin_backend as backend +import app_version +import archive_api +import knowledge_api +import zyt_auth + +# 账号下拉框里「全部」的取值。用 -1 而不是 0:0 早就有含义了(旧版未归属数据), +# 而"不传"在 FastAPI 里就是 None,同样已经被占用。 +ALL_ACCOUNTS = -1 + +# Vue 开发服务器的默认端口。生产环境前端和 API 同源,这些只在开发时用得上。 +DEV_ORIGINS = [ + "http://localhost:5173", "http://127.0.0.1:5173", + "http://localhost:3000", "http://127.0.0.1:3000", +] + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class LoginBody(BaseModel): + username: str + password: str + device_name: str = "web" + + +class PasswordBody(BaseModel): + current_password: str + new_password: str + + +class RoleBody(BaseModel): + code: str + name: str = "" + permissions: list[str] = Field(default_factory=list) + + +class UserBody(BaseModel): + username: str = "" + password: str = "" + role: str = "viewer" + active: bool = True + + +class DesktopExchangeBody(BaseModel): + device_id: str = Field(min_length=8, max_length=128) + device_name: str = Field(default="", max_length=120) + app_version: str = Field(default="", max_length=64) + interactive_login: bool = False + + +class DesktopZytSessionBody(BaseModel): + zyt_token: str = Field(min_length=1, max_length=8192, repr=False) + + +class DesktopAccountBody(BaseModel): + status: str = "active" + backup_enabled: bool | None = None + backup_interval_minutes: int | None = Field(default=None, ge=1, le=10080) + + +class DesktopAccountScopeBody(BaseModel): + admin_user_ids: list[int] = Field(default_factory=list) + + +class ProviderBody(BaseModel): + id: str + name: str = "" + kind: str + base_url: str + # auto:按接口类型补全路径;exact:地址原样使用,一个字符不加 + endpoint_mode: str = "auto" + api_key: str = "" # 留空 = 保持原密钥不动 + model: str = "" + capabilities: str = "text" + max_tokens: int = 500 + temperature: float = 0.35 + timeout_ms: int = 30000 + max_inflight: int = 32 + rpm_limit: int = 0 + enabled: bool = True + + +class RolesPlanBody(BaseModel): + answer_ids: str + judge_id: str = "" + vision_id: str = "" + fallback_ids: str = "" + judge_mode: str = "shadow" + + +class AgentRuleBody(BaseModel): + """一条角色规则。 + + 三种类型的必填项不同,逐条校验在 `admin_backend.validate_agent_rules` 里—— + 这里只保证形状是个对象、字段名对得上。把"guide 必须有 instruction"这种规则 + 写进 Pydantic,等于同一套约束维护两份,迟早对不上。 + """ + + id: str = "" + label: str = "" + # guide 补充指令 / forbid 禁止措辞 / reply 固定口径 + type: str = "guide" + keywords: list[str] = Field(default_factory=list) + instruction: str = "" + reply: str = "" + enabled: bool = True + + +class AgentBody(BaseModel): + """一个智能体 = 人设 + 一组规则。""" + + id: str = "" # 留空 = 按名称自动生成 + name: str + role: str = "" + description: str = "" + persona: str = "" + keywords: list[str] = Field(default_factory=list) + rules: list[AgentRuleBody] = Field(default_factory=list) + enabled: bool = True + priority: int = 100 + + +class AgentPlanBody(BaseModel): + """启用与协作方案。single 就是"切换到某一个",collaborate 是"多个同时在场"。""" + + mode: str = "single" + primary_id: str = "" + active_ids: list[str] = Field(default_factory=list) + + +class ConfigBody(BaseModel): + """桌面端下发配置。 + + 字段名保持和老后台的表单 `name` 完全一致(大写下划线),这样两边共用同一个 + `validate_config_form`——校验规则只有一份,不会出现"网页后台拦得住、新后台 + 放得过"这种两套标准。 + + **这里没有模型连接参数**。服务类型 / API 地址 / API Key / 模型名称 / 温度 / + max_tokens / 超时全部搬到了「模型清单 + 角色编排」,理由见 + `admin_backend.CONFIG_KEYS` 上面那段。剩下的都是客户端行为。 + """ + + AI_ENABLED: bool = False + AI_DEVELOPMENT_MODE: bool = False + AI_USE_VISION: bool = False + AI_UI_GUARD_ENABLED: bool = False + AI_CONTEXT_ENABLED: bool = False + AI_CONTEXT_MAX_ROUNDS: int = 8 + AI_COUNTER_INSULT_ENABLED: bool = False + AI_AGENT_NAME: str = "" + AI_HOSPITAL_NAME: str = "" + AI_MCP_ENABLED: bool = False + AI_MCP_MAX_ROUNDS: int = 5 + AI_MCP_SERVERS: list[Any] = Field(default_factory=list) + # 留空 = 按后台自己的地址推算。桌面端不配这个,由后台同步时下发。 + AI_GATEWAY_URL: str = "" + # 桌面软件启动时先向本管理端读取该地址,再把账号密码发给 ZYT。 + ZYT_LOGIN_API_URL: str = "" + ARCHIVE_AUTO_BACKUP_ENABLED: bool = True + ARCHIVE_AUTO_BACKUP_INTERVAL_MINUTES: int = 60 + # 选择性审核:命中才转人工,其余自动发送。逐条形状由 + # `admin_backend.validate_review_rules` 校验,这里只接住一个数组。 + AI_REVIEW_RULES: list[Any] = Field(default_factory=list) + + @field_validator("*", mode="before") + @classmethod + def _null_means_unset(cls, value: Any, info: Any) -> Any: + """把请求体里的 `null` 当成"没填",换成这一项的出厂默认值。 + + 正常情况下 GET /config 现在不会再回 null,前端也就没有 null 可发。这里 + 挡的是另一种情况:浏览器里开着这次修复之前拉取的旧页面,表单里还揣着 + 当时的 null;用户点保存,POST 出去的照样是 null。没有这道,Pydantic 的 + 严格类型校验会直接拒收,报一句"Input should be a valid boolean"—— + 对着这句话,人只会怀疑是不是自己填错了什么,看不出问题出在缓存的旧页面。 + """ + if value is None: + return backend.CONFIG_DEFAULTS.get(info.field_name) + return value + + +class ReleaseBody(BaseModel): + package_media_id: str = "" + latest_version: str + download_url: str = "" + release_notes: str = "" + force_upgrade: bool = False + auto_install: bool = False + package_sha256: str = "" + package_size: int = Field(default=0, ge=0) + signature_required: bool = False + release_channel: str = "stable" + rollout_percent: int = Field(default=100, ge=1, le=100) + status: str = "published" + + +class ReleasePackagePrepareBody(BaseModel): + file_name: str = Field(min_length=1, max_length=512) + package_sha256: str = Field(pattern=r"^[0-9a-fA-F]{64}$") + package_size: int = Field(gt=0, le=1024 * 1024 * 1024) + + +class ReleasePackagePartBody(BaseModel): + part_number: int = Field(ge=1, le=10000) + etag: str = Field(min_length=1, max_length=512) + + +class ReleasePackageMultipartCompleteBody(BaseModel): + upload_id: str = Field(min_length=1, max_length=2048) + parts: list[ReleasePackagePartBody] = Field(min_length=1, max_length=10000) + + +class UpdateReportBody(BaseModel): + current_version: str = Field(default="", max_length=64) + target_version: str = Field(min_length=1, max_length=64) + status: str = Field(min_length=1, max_length=32) + progress: int = Field(default=0, ge=0, le=100) + error: str = Field(default="", max_length=1000) + + +class ModelTestBody(BaseModel): + """连通性测试。 + + 两种用法:给 `provider_id` 就测模型清单里那一条(密钥从库里解出来,永远不 + 经过前端);不给就用页面上临时填的值测——正在新建一个模型、还没保存的时候 + 需要这个。 + """ + + provider_id: str = "" + kind: str = "" + base_url: str = "" + endpoint_mode: str = "" + api_key: str = "" # 留空且指定了 provider_id = 用库里存的密钥 + model: str = "" + timeout_seconds: int = 20 + + +def _public_users(database) -> list[dict[str, Any]]: + """用户列表的对外投影。密码盐和摘要一个字节都不能出接口。""" + return [ + { + "id": row["id"], + "username": row["username"], + "role": row["role"], + "active": bool(row["active"]), + "must_change_password": bool(row["must_change_password"]), + "created_at": row["created_at"], + "updated_at": row["updated_at"], + } + for row in database.list_users() + ] + + +class Principal: + """当前请求的调用者:用户行 + 他实际拥有的权限码。""" + + def __init__(self, user, permissions: set[str]): + self.user = user + self.permissions = permissions + + @property + def id(self) -> int: + return int(self.user["id"]) + + def payload(self) -> dict[str, Any]: + return { + "id": self.id, + "username": self.user["username"], + "role": self.user["role"], + "must_change_password": bool(self.user["must_change_password"]), + "permissions": sorted(self.permissions), + } + + +def _frontend_dist() -> Path: + """Vue 前端的构建产物目录。 + + 单独抽出来是为了能在测试里替换掉——不然测试结果取决于本机有没有跑过 + `vite build`,同一份代码在开发机和 CI 上表现不一样。 + """ + return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "web-antd" / "dist" + + +def create_app( + db_path: Path | str = "backend.db", + zyt_token_verifier: Callable[[str], dict[str, Any]] | None = None, + zyt_patient_searcher: Callable[[str, str, int, int], dict[str, Any]] | None = None, +) -> FastAPI: + database = backend.Database(Path(db_path).resolve()) + # 每个碰这个库的服务启动时都要把结构升到当前版本。只起 API、不起网页后台的 + # 部署,以前会撞上 `no such column`——而报错里看不出是漏了迁移。 + database.migrate() + release_directory = database.path.parent / "releases" + release_directory.mkdir(parents=True, exist_ok=True) + app = FastAPI(title="企微客服助手管理 API", version="2.0") + app.add_middleware( + CORSMiddleware, + allow_origins=DEV_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + def client_ip(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + return forwarded or (request.client.host if request.client else "") + + def current(request: Request) -> Principal: + raw = request.headers.get("authorization", "") + token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + user = database.session(token) if token else None + if user is not None and str(user["token_kind"]) != "api": + user = None + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已失效,请重新登录" + ) + return Principal(user, database.permissions_for_user(int(user["id"]))) + + def current_desktop(request: Request): + raw = request.headers.get("authorization", "") + token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + account = database.desktop_session(token) + if account is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="桌面登录已失效,请重新登录", + ) + return account + + def require(*codes: str): + """权限依赖。只认权限码,不认角色名。""" + + async def guard(principal: Principal = Depends(current)) -> Principal: + missing = [code for code in codes if code not in principal.permissions] + if missing: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"缺少权限:{'、'.join(missing)}", + ) + return principal + + return guard + + def resolve_admin_tenant( + principal: Principal, account_id: int | None + ) -> str | list[str]: + """把界面上选的客户端账号翻成这次查询的租户范围。 + + 下拉框有三档,这里是它们的含义: + + ALL_ACCOUNTS(-1) 全部。有 `desktop-account:all` 的返回 `"*"`(不限 + 租户);只被授权到若干账号的返回那几个账号的租户, + 「全部」对他就是「我能看的全部」,不是所有人的全部 + 0 / 不传 旧版未归属数据(tenant_id 为空或 default 的老记录) + 具体账号 id 那个账号的租户,且必须先过授权检查 + + 返回值有两种形状(一个租户 / 一组租户),下游统一交给 + `archive_store.tenant_scope` 和 `admin_backend.tenant_filter` 去翻译成 + SQL 条件——范围判断只在这一处做,散到各个接口里迟早会漏一个。 + """ + if account_id == ALL_ACCOUNTS: + if "desktop-account:all" in principal.permissions: + return backend.ALL_TENANTS + scoped = database.desktop_account_ids_for_admin(principal.id) + if not scoped: + # 没有被限制到任何账号 = 不受限,和上面一样能看全部。 + return backend.ALL_TENANTS + tenants = [] + for item in scoped: + account = database.desktop_account(int(item)) + if account is not None: + tenants.append(str(account["tenant_id"])) + if not tenants: + raise HTTPException(status_code=403, detail="没有已授权的客户端账号") + return tenants + if not account_id: + if ( + "desktop-account:all" not in principal.permissions + and database.desktop_account_ids_for_admin(principal.id) + ): + raise HTTPException(status_code=403, detail="请选择已授权的客户端账号") + return "default" + account = database.desktop_account(int(account_id)) + if account is None: + raise HTTPException(status_code=404, detail="客户端账号不存在") + if ( + "desktop-account:all" not in principal.permissions + and not database.admin_can_access_desktop_account(principal.id, int(account_id)) + ): + raise HTTPException(status_code=403, detail="无权查看该客户端账号的数据") + return str(account["tenant_id"]) + + def desktop_login_api_url() -> tuple[str, bool]: + """返回桌面登录服务地址,以及它是否来自管理端持久化配置。""" + + stored = json.loads(database.config()["config_json"]) + configured = str( + backend.effective_config(stored).get("ZYT_LOGIN_API_URL") or "" + ).strip().rstrip("/") + return (configured or zyt_auth.api_base_url(), bool(configured)) + + def release_payload(row: Any, *, direct_download: bool = False) -> dict[str, Any]: + keys = set(row.keys()) if hasattr(row, "keys") else set() + + def value(name: str, default: Any = "") -> Any: + return row[name] if name in keys else default + + result = { + "latest_version": str(value("latest_version")), + "download_url": str(value("download_url")), + "release_notes": str(value("release_notes")), + "force_upgrade": bool(value("force_upgrade", 0)), + "auto_install": bool(value("auto_install", 0)), + "package_sha256": str(value("package_sha256")), + "package_size": int(value("package_size", 0) or 0), + "signature_required": bool(value("signature_required", 0)), + "release_channel": str(value("release_channel", "stable")), + "rollout_percent": int(value("rollout_percent", 100) or 100), + "status": str(value("status", "published")), + "updated_at": str(value("updated_at", value("created_at", ""))), + "updated_by": str( + value("updated_by_name", value("created_by_name", "system")) + or "system" + ), + } + + identity = app.state.archive_store.release_package_identity(result['download_url']) + result['package_media_id'] = identity[0] if identity else '' + if direct_download and identity: + try: + result['download_url'] = app.state.archive_store.release_package_download_url(*identity) + except (KeyError, ValueError, RuntimeError) as exc: + raise HTTPException(status_code=409, detail=f"COS 安装包暂不可用:{exc}") from exc + except Exception as exc: + raise HTTPException(status_code=502, detail="COS 下载地址生成失败,请重试") from exc + return result + + # ── 桌面账号认证 ───────────────────────────────────────────────────── + @app.get("/api/v2/desktop/auth/config") + async def desktop_auth_config() -> dict: + login_api_url, configured = desktop_login_api_url() + return { + "zyt_api_url": login_api_url, + "terminal": zyt_auth.WECOM_RPA_TERMINAL, + "configured": configured, + } + + @app.post("/api/v2/desktop/auth/exchange") + async def desktop_auth_exchange(body: DesktopExchangeBody, request: Request) -> dict: + raw = request.headers.get("authorization", "") + zyt_token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + if not zyt_token: + raise HTTPException(status_code=401, detail="缺少 ZYT 登录令牌") + if zyt_token_verifier is not None: + verifier = zyt_token_verifier + else: + login_api_url, _configured = desktop_login_api_url() + verifier = lambda token: zyt_auth.verify_desktop_token( + token, api_url=login_api_url + ) + try: + identity = await asyncio.to_thread(verifier, zyt_token) + token, account = database.create_desktop_session( + identity, + body.device_id, + body.device_name, + body.app_version, + client_ip(request), + interactive_login=body.interactive_login, + ) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.save_zyt_session( + str(account["tenant_id"]), + zyt_token, + str(identity.get("user_id") or ""), + ) + except (zyt_auth.ZytAuthError, ValueError) as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + return { + "access_token": token, + "expires_in": max(0, int(account["expires_at"]) - int(time.time())), + "account": { + "id": int(account["id"]), + "sn": str(account["zyt_sn"]), + "nickname": str(account["nickname"]), + "tenant_id": str(account["tenant_id"]), + "device_id": str(account["device_id"]), + }, + } + + @app.post("/api/v2/desktop/auth/zyt-session") + async def desktop_refresh_zyt_session( + body: DesktopZytSessionBody, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + raw = request.headers.get("authorization", "") + desktop_token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + if zyt_token_verifier is not None: + verifier = zyt_token_verifier + else: + login_api_url, _configured = desktop_login_api_url() + verifier = lambda token: zyt_auth.verify_desktop_token(token, api_url=login_api_url) + try: + identity = await asyncio.to_thread(verifier, body.zyt_token.strip()) + if (not isinstance(identity, dict) or not identity.get("user_id") + or str(identity.get("terminal")) != str(zyt_auth.WECOM_RPA_TERMINAL) + or str(identity.get("status") or "") != "active"): + raise zyt_auth.ZytAuthError("ZYT 桌面身份验证失败,请重新验证账号") + except zyt_auth.ZytAuthError as exc: + # The identity adapter preserves transport/JSON causes. An outage must + # not send an otherwise logged-in user into a reauthentication loop. + cause = exc.__cause__ + http_code = cause.code if isinstance(cause, urllib.error.HTTPError) else None + if http_code == 403 or any(word in str(exc) for word in ("权限", "无权")): + raise HTTPException(status_code=403, detail="ZYT 账号无权验证患者查询身份,请联系管理员。") from exc + unavailable = ( + (http_code is not None and http_code != 401) + or (http_code is None and isinstance(cause, (urllib.error.URLError, OSError, ValueError, TypeError))) + or any(word in str(exc) for word in ("无法连接", "无效数据", "身份信息不完整", "配置不正确", "尚未部署")) + ) + if unavailable: + raise HTTPException(status_code=502, detail={ + "code": "zyt_unavailable", + "message": "ZYT 身份服务暂不可用,请稍后重试。", + }) from exc + raise HTTPException(status_code=409, detail={ + "code": "zyt_session_expired", + "message": "ZYT 患者查询登录已过期,请重新验证账号。", + }) from exc + if str(identity["user_id"]).strip() != str(account["zyt_user_id"]): + raise HTTPException(status_code=403, detail="请使用当前桌面账号重新验证,不能切换到其他账号") + archive_store = getattr(app.state, "archive_store", None) + if archive_store is None: + raise HTTPException(status_code=503, detail="患者查询服务暂不可用") + try: + await asyncio.to_thread( + archive_store.refresh_zyt_session, desktop_token, int(account["id"]), + str(account["tenant_id"]), body.zyt_token, identity, + ) + except PermissionError as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + return {"ok": True} + + @app.get("/api/v2/desktop/me") + async def desktop_me(account: Any = Depends(current_desktop)) -> dict: + return { + "id": int(account["id"]), + "sn": str(account["zyt_sn"]), + "nickname": str(account["nickname"]), + "tenant_id": str(account["tenant_id"]), + "device_id": str(account["device_id"]), + "expires_at": int(account["expires_at"]), + } + + @app.post("/api/v2/desktop/auth/logout") + async def desktop_auth_logout( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + raw = request.headers.get("authorization", "") + database.revoke_desktop_session(raw[7:].strip()) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(str(account["tenant_id"])) + database.audit( + None, + "desktop.logout", + f"account={account['id']} device={account['device_id']}", + client_ip(request), + ) + return {"ok": True} + + # ── 管理端认证 ─────────────────────────────────────────────────────── + @app.post("/api/v2/auth/login") + async def login(body: LoginBody, request: Request) -> dict: + ip = client_ip(request) + key = f"{ip}:{body.username.lower()}" + if not backend.LOGIN_LIMITER.allowed(key): + raise HTTPException(status_code=429, detail="登录失败次数过多,请稍后再试") + user = database.authenticate(body.username, body.password) + if not user: + backend.LOGIN_LIMITER.failure(key) + database.audit(None, "login.failed", f"username={body.username}", ip) + raise HTTPException(status_code=401, detail="用户名或密码不正确") + backend.LOGIN_LIMITER.success(key) + token, _ = database.create_token( + int(user["id"]), "api", body.device_name, 30 * 86400 + ) + database.audit(int(user["id"]), "login.api", body.device_name, ip) + principal = Principal(user, database.permissions_for_user(int(user["id"]))) + # 首次登录不拦 API:改密页面本身也要调接口。前端凭 + # must_change_password 把用户锁在改密页上,比后端一刀切 403 好用得多 + # ——老实现那样连改密接口都调不通,只能去网页后台改。 + return { + "access_token": token, + "expires_in": 30 * 86400, + "user": principal.payload(), + } + + @app.post("/api/v2/auth/logout") + async def logout(request: Request, principal: Principal = Depends(current)) -> dict: + raw = request.headers.get("authorization", "") + database.revoke(raw[7:].strip() if raw.lower().startswith("bearer ") else "") + database.audit(principal.id, "logout.api", "", client_ip(request)) + return {"ok": True} + + @app.get("/api/v2/me") + async def me(principal: Principal = Depends(current)) -> dict: + """前端路由守卫和按钮显隐的唯一数据源。""" + return principal.payload() + + @app.post("/api/v2/me/password") + async def change_password( + body: PasswordBody, request: Request, principal: Principal = Depends(current) + ) -> dict: + if not backend.valid_password(body.new_password): + raise HTTPException(status_code=400, detail="密码至少 10 位,且需同时含字母和数字") + try: + database.change_password( + principal.id, body.current_password, body.new_password, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"ok": True} + + # ── 角色与权限 ─────────────────────────────────────────────────────── + @app.get("/api/v2/permissions") + async def permissions(_: Principal = Depends(require("role:write"))) -> dict: + return {"permissions": database.permission_catalog()} + + @app.get("/api/v2/roles") + async def list_roles(_: Principal = Depends(require("user:read"))) -> dict: + return {"roles": database.roles()} + + @app.post("/api/v2/roles") + async def save_role( + body: RoleBody, request: Request, principal: Principal = Depends(require("role:write")) + ) -> dict: + try: + saved = database.save_role( + body.code, body.name, body.permissions, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"role": saved, "roles": database.roles()} + + @app.delete("/api/v2/roles/{code}") + async def delete_role( + code: str, request: Request, principal: Principal = Depends(require("role:write")) + ) -> dict: + try: + database.delete_role(code, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"roles": database.roles()} + + # ── 用户 ───────────────────────────────────────────────────────────── + @app.get("/api/v2/users") + async def list_users(_: Principal = Depends(require("user:read"))) -> dict: + return {"users": _public_users(database)} + + @app.post("/api/v2/users") + async def create_user( + body: UserBody, request: Request, principal: Principal = Depends(require("user:write")) + ) -> dict: + if not backend.valid_password(body.password): + raise HTTPException(status_code=400, detail="密码至少 10 位,且需同时含字母和数字") + known = {item["code"] for item in database.roles()} + if body.role not in known: + raise HTTPException(status_code=400, detail=f"角色不存在:{body.role}") + try: + database.create_user( + body.username, body.password, body.role, principal.id, client_ip(request) + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"users": _public_users(database)} + + @app.put("/api/v2/users/{user_id}") + async def update_user( + user_id: int, + body: UserBody, + request: Request, + principal: Principal = Depends(require("user:write")), + ) -> dict: + known = {item["code"] for item in database.roles()} + if body.role not in known: + raise HTTPException(status_code=400, detail=f"角色不存在:{body.role}") + try: + database.update_user( + user_id, body.role, body.active, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"users": _public_users(database)} + + # ── 客户端账号与设备 ───────────────────────────────────────────────── + @app.get("/api/v2/desktop-accounts") + def list_desktop_accounts( + include_counts: bool = True, + principal: Principal = Depends(current), + ) -> dict: + allowed = ( + None + if "desktop-account:all" in principal.permissions + else database.desktop_account_ids_for_admin(principal.id) + ) + return { + "accounts": database.desktop_accounts(allowed, include_counts=include_counts), + "can_view_all": "desktop-account:all" in principal.permissions, + # 尚未分配任何账号的旧角色仍能查看升级前的未归属数据;一旦分配账号, + # 就必须明确选择授权账号,不能再借默认值绕过范围限制。 + "can_view_legacy": ( + "desktop-account:all" in principal.permissions or not allowed + ), + } + + @app.put("/api/v2/desktop-accounts/{account_id}") + async def update_desktop_account( + account_id: int, + body: DesktopAccountBody, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + try: + account = database.update_desktop_account( + account_id, + status=body.status, + backup_enabled=body.backup_enabled, + backup_interval_minutes=body.backup_interval_minutes, + actor_id=principal.id, + ip=client_ip(request), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if str(account.get("status") or "") == "disabled": + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(str(account["tenant_id"])) + return {"account": account} + + @app.post("/api/v2/desktop-accounts/{account_id}/revoke-devices") + async def revoke_desktop_devices( + account_id: int, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + tenant = resolve_admin_tenant(principal, account_id) + database.revoke_desktop_devices(account_id, principal.id, client_ip(request)) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(tenant) + return {"ok": True} + + @app.get("/api/v2/desktop-accounts/{account_id}/admins") + async def desktop_account_admins( + account_id: int, + principal: Principal = Depends(require("desktop-account:read")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + return {"admin_user_ids": database.desktop_account_admin_ids(account_id)} + + @app.put("/api/v2/desktop-accounts/{account_id}/admins") + async def update_desktop_account_admins( + account_id: int, + body: DesktopAccountScopeBody, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + try: + database.set_desktop_account_admins( + account_id, body.admin_user_ids, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"admin_user_ids": database.desktop_account_admin_ids(account_id)} + + # ── 模型清单与编排 ─────────────────────────────────────────────────── + @app.get("/api/v2/models") + async def list_models(_: Principal = Depends(require("model:read"))) -> dict: + return { + "models": database.model_providers(), + "roles": database.model_roles(), + "kinds": ["dify", "openai", "claude", "comfyui"], + "judge_modes": ["shadow", "score_only", "arbitrate"], + } + + @app.post("/api/v2/models") + async def save_model( + body: ProviderBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + database.save_model_provider( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"models": database.model_providers()} + + @app.delete("/api/v2/models/{provider_id}") + async def delete_model( + provider_id: str, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + database.delete_model_provider(provider_id, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"models": database.model_providers()} + + @app.post("/api/v2/models/plan") + async def save_plan( + body: RolesPlanBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + version = database.save_model_roles( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"version": version, "roles": database.model_roles()} + + # ── 智能体 ─────────────────────────────────────────────────────────── + # 模型清单管的是"用哪个模型",这里管的是"用什么身份、按什么规矩说话"。分开 + # 是因为这两件事的变更频率差一个数量级:模型半年动一次,话术一周动三次。 + @app.get("/api/v2/agents") + async def list_agents(_: Principal = Depends(require("agent:read"))) -> dict: + return { + "agents": database.agents(), + "plan": database.agent_plan(), + "rule_types": list(backend.AGENT_RULE_TYPES), + "plan_modes": list(backend.AGENT_PLAN_MODES), + } + + @app.post("/api/v2/agents") + async def save_agent( + body: AgentBody, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + database.save_agent(body.model_dump(), principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"agents": database.agents(), "plan": database.agent_plan()} + + @app.delete("/api/v2/agents/{agent_id}") + async def delete_agent( + agent_id: str, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + database.delete_agent(agent_id, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"agents": database.agents(), "plan": database.agent_plan()} + + @app.post("/api/v2/agents/plan") + async def save_agent_plan( + body: AgentPlanBody, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + version = database.save_agent_plan( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"version": version, "plan": database.agent_plan()} + + # ── 运营 ───────────────────────────────────────────────────────────── + @app.get("/api/v2/stats/model-calls") + async def model_call_stats( + days: int = 7, + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return { + "days": days, + **database.model_call_stats( + days, resolve_admin_tenant(principal, account_id) + ), + } + + @app.get("/api/v2/stats/model-calls/log") + async def model_call_log( + days: int = 7, + limit: int = 50, + offset: int = 0, + q: str = "", + # chat = 真实对话(默认);guard = 界面识别;留空 = 全都要 + purpose: str = "chat", + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.list_model_calls( + days, + limit, + offset, + q, + purpose, + resolve_admin_tenant(principal, account_id), + ) + + @app.get("/api/v2/stats/model-usage") + async def model_usage_stats( + days: int = Query(default=7, ge=1, le=90), + purpose: str = Query(default="", pattern="^(chat|guard|knowledge)?$"), + provider_id: str = Query(default="", max_length=256), + model: str = Query(default="", max_length=512), + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.model_usage_stats( + days, resolve_admin_tenant(principal, account_id), purpose, provider_id, model) + + @app.get("/api/v2/stats/model-usage/log") + async def model_usage_log( + days: int = Query(default=7, ge=1, le=90), + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + purpose: str = Query(default="", pattern="^(chat|guard|knowledge)?$"), + provider_id: str = Query(default="", max_length=256), + model: str = Query(default="", max_length=512), + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.list_model_usage( + days, limit, offset, resolve_admin_tenant(principal, account_id), purpose, provider_id, model) + + # ── 桌面端配置 ─────────────────────────────────────────────────────── + # 这一组是从老网页后台整体搬过来的("能力开关 / 模型与身份 / MCP 服务器" + # 三个区块)。桌面端启动和定时同步都拉这份配置,改错一个字段全线客户端 + # 立刻受影响——所以校验函数直接复用老后台那一份,不另写一套。 + @app.get("/api/v2/config") + async def get_config( + request: Request, _: Principal = Depends(require("config:read")) + ) -> dict: + row = database.config() + stored = json.loads(row["config_json"]) + # 只回当前在用的字段。库里老记录还带着已退休的模型参数(不删用户 + # 数据),但把它们发给前端只会让人以为那里还能改。 + config = backend.effective_config(stored) + return { + "config": config, + "version": int(row["version"]), + "updated_at": row["updated_at"], + "updated_by": row["updated_by_name"] or "system", + "bool_keys": sorted(backend.BOOL_KEYS), + # 已经搬走的字段,连同它们现在的去处。前端拿它渲染一条指路说明, + # 而不是让人对着一个"以前在这儿的东西不见了"的页面发愣。 + "retired_keys": list(backend.RETIRED_CONFIG_KEYS), + "model_settings_moved_to": "AI 模型 → 模型清单 / 角色编排", + # 留空时实际会下发什么。不显示出来的话,"自动推算"对使用的人就是 + # 一个黑盒——出问题时没法判断是推错了还是网关没起。 + "gateway_url_effective": backend.derive_gateway_url( + str(config.get("AI_GATEWAY_URL") or ""), + request.headers.get("x-forwarded-proto", request.url.scheme), + request.headers.get("host", ""), + ), + # 登录窗口在用户输入账号前会读取公开配置接口。这里显示它最终会 + # 拿到哪个地址,便于管理员保存后当场核对。 + "login_api_url_effective": desktop_login_api_url()[0], + } + + @app.post("/api/v2/config") + async def save_config( + body: ConfigBody, + request: Request, + principal: Principal = Depends(require("config:write")), + ) -> dict: + current = json.loads(database.config()["config_json"]) + # validate_config_form 收的是 HTML 表单那种"全是字符串"的字典,布尔用 + # "1"/"" 表示。这里把 JSON 转成它认识的形状,而不是把它改成认 JSON—— + # 老后台还在跑,改它等于同时动两个正在服务的系统。 + form: dict[str, str] = {} + for key, value in body.model_dump().items(): + if key in backend.BOOL_KEYS: + form[key] = "1" if value else "" + elif key in ("AI_MCP_SERVERS", "AI_REVIEW_RULES"): + form[key] = json.dumps(value, ensure_ascii=False) + else: + form[key] = str(value) + try: + config = backend.validate_config_form(form, current) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + version = database.save_config(config, principal.id, client_ip(request)) + return {"ok": True, "version": version} + + # ── 桌面端版本升级 ─────────────────────────────────────────────────── + @app.get("/api/v2/release") + async def get_release(_: Principal = Depends(require("config:read"))) -> dict: + return release_payload(database.release(), direct_download=True) + + @app.post("/api/v2/release") + async def save_release( + body: ReleaseBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + download_url = app.state.archive_store.normalize_release_download( + body.download_url, body.package_sha256, body.package_size, body.package_media_id + ) + release = backend.validate_release_form( + { + "latest_version": body.latest_version, + "download_url": download_url, + "release_notes": body.release_notes, + "force_upgrade": "1" if body.force_upgrade else "", + "auto_install": "1" if body.auto_install else "", + "package_sha256": body.package_sha256, + "package_size": str(body.package_size), + "signature_required": "1" if body.signature_required else "", + "release_channel": body.release_channel, + "rollout_percent": str(body.rollout_percent), + "status": body.status, + } + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + database.save_release( + release["latest_version"], + release["download_url"], + release["release_notes"], + release["force_upgrade"], + principal.id, + client_ip(request), + auto_install=release["auto_install"], + package_sha256=release["package_sha256"], + package_size=release["package_size"], + signature_required=release["signature_required"], + release_channel=release["release_channel"], + rollout_percent=release["rollout_percent"], + status=release["status"], + ) + return {"ok": True, **release_payload(database.release(), direct_download=True)} + + @app.post("/api/v2/release/package/prepare") + async def prepare_release_package( + body: ReleasePackagePrepareBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + """签发 COS 直传地址;EXE 内容不会进入管理端进程或本机磁盘。""" + + try: + store = app.state.archive_store + prepared = store.prepare_release_package( + { + "original_filename": body.file_name, + "sha256": body.package_sha256.lower(), + "size_bytes": body.package_size, + } + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException( + status_code=502, detail=f"COS 预上传失败:{exc}" + ) from exc + database.audit( + principal.id, + "release.package.prepare", + f"file={prepared['file_name']} size={prepared['package_size']}", + client_ip(request), + ) + return prepared + + @app.post("/api/v2/release/package/{media_id}/complete") + async def complete_release_package( + media_id: str, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + package = app.state.archive_store.complete_release_package(media_id) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 文件校验失败:{exc}") from exc + database.audit( + principal.id, + "release.package.complete", + f"media={media_id} size={package['package_size']}", + client_ip(request), + ) + return package + + @app.post("/api/v2/release/package/{media_id}/multipart-complete") + async def complete_multipart_release_package( + media_id: str, + body: ReleasePackageMultipartCompleteBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + package = app.state.archive_store.complete_multipart_release_package( + media_id, + body.upload_id, + [part.model_dump() for part in body.parts], + ) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 分块合并失败:{exc}") from exc + database.audit( + principal.id, + "release.package.complete", + f"media={media_id} multipart=1 size={package['package_size']}", + client_ip(request), + ) + return package + + @app.get("/api/v2/releases") + async def release_history( + _: Principal = Depends(require("config:read")), + ) -> dict: + current = release_payload(database.release()) + return { + "history": [release_payload(row) for row in database.release_history()], + "reports": database.update_report_summary(current["latest_version"]), + } + + @app.get("/api/v2/desktop/update/packages/{filename}") + async def desktop_update_package(filename: str) -> FileResponse: + if Path(filename).name != filename or not filename.lower().endswith(".exe"): + raise HTTPException(status_code=404, detail="升级包不存在") + target = (release_directory / filename).resolve() + if target.parent != release_directory.resolve() or not target.is_file(): + raise HTTPException(status_code=404, detail="升级包不存在") + return FileResponse( + target, + media_type="application/vnd.microsoft.portable-executable", + filename=filename, + ) + + @app.get("/api/v2/desktop/update/cos-packages/{media_id}/{filename}") + async def desktop_update_cos_package(media_id: str, filename: str) -> RedirectResponse: + try: + url = app.state.archive_store.release_package_download_url( + media_id, filename + ) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 下载签名失败:{exc}") from exc + return RedirectResponse(url=url, status_code=307) + + @app.get("/api/v2/desktop/update/manifest") + async def desktop_update_manifest( + request: Request, + current_version: str = "", + device_id: str = "", + channel: str = "stable", + ) -> dict: + release = release_payload(database.release()) + latest = release["latest_version"] + eligible = release["status"] == "published" and release["release_channel"] == channel + if eligible and release["rollout_percent"] < 100: + bucket = int( + hashlib.sha256(f"{device_id}:{latest}".encode("utf-8")).hexdigest()[:8], + 16, + ) % 100 + 1 + eligible = bucket <= release["rollout_percent"] + update_available = bool( + eligible and app_version.is_newer_version(latest, current_version) + ) + download_url = release["download_url"] + identity = app.state.archive_store.release_package_identity(download_url) + if identity: + # Each check gets a fresh COS HTTPS URL, including old release rows. + download_url = release_payload(release, direct_download=True)["download_url"] if update_available else "" + elif download_url.startswith("/"): + forwarded_scheme = str( + request.headers.get("x-forwarded-proto") or request.url.scheme + ).split(",", 1)[0].strip() + forwarded_host = str( + request.headers.get("x-forwarded-host") + or request.headers.get("host") + or request.url.netloc + ).split(",", 1)[0].strip() + public_base = ( + f"{forwarded_scheme}://{forwarded_host}/" + if forwarded_scheme and forwarded_host + else str(request.base_url) + ) + download_url = urllib.parse.urljoin( + public_base, download_url.lstrip("/") + ) + return { + **release, + "download_url": download_url, + "download_url_expires_in": 900 if identity and update_available else 0, + "update_available": update_available, + "eligible": eligible, + } + + @app.post("/api/v2/desktop/update/report") + async def desktop_update_report( + body: UpdateReportBody, + account: Any = Depends(current_desktop), + ) -> dict: + allowed = { + "available", + "downloading", + "downloaded", + "installing", + "installed", + "failed", + "deferred", + } + if body.status not in allowed: + raise HTTPException(status_code=400, detail="升级状态不正确") + if not app_version.normalize_version(body.target_version): + raise HTTPException(status_code=400, detail="目标版本号不正确") + database.record_update_report( + int(account["id"]), + str(account["device_id"]), + body.current_version, + body.target_version, + body.status, + body.progress, + body.error, + ) + return {"ok": True} + + # ── 模型连通性测试 ─────────────────────────────────────────────────── + @app.post("/api/v2/models/test") + async def test_model( + body: ModelTestBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + """真的向模型发一次最小请求,返回不含密钥的诊断结果。 + + 要 `model:write` 而不是 `model:read`:这个动作会拿着真实密钥向外网发请 + 求,还可能产生费用。只读用户能看模型清单,但不该能替公司花钱。 + + 密钥来源有两处,都不经过前端:指定了 `provider_id` 就从库里解密取;否则 + 用请求里临时填的(新建模型还没保存时要用)。 + """ + kind = str(body.kind or "").strip().lower() + base_url = str(body.base_url or "").strip() + model = str(body.model or "").strip() + api_key = str(body.api_key or "").strip() + endpoint_mode = str(body.endpoint_mode or "").strip().lower() + label = "" + + if body.provider_id: + match = next( + ( + item + for item in database.model_providers(include_secrets=True) + if str(item.get("id")) == body.provider_id + ), + None, + ) + if match is None: + raise HTTPException(status_code=404, detail="模型不存在") + label = str(match.get("name") or match.get("id") or "") + kind = kind or str(match.get("kind") or "") + base_url = base_url or str(match.get("base_url") or "") + model = model or str(match.get("model") or "") + # 前端提交空密钥 = 用库里存的那把。这样"测一下现有配置通不通" + # 不需要把密钥再发一遍,也就没有它在网络上多走一趟的机会。 + api_key = api_key or str(match.get("api_key") or "") + endpoint_mode = endpoint_mode or str(match.get("endpoint_mode") or "auto") + + if kind == "claude": + # 老后台的测试器只认 openai / dify / comfyui 三种。Claude 是新增 + # 的出口类型,端点和鉴权头都不一样,硬套 openai 只会得到 404。 + raise HTTPException( + status_code=400, + detail="Claude 出口暂不支持一键连通性测试,请在模型网关的调用统计里确认", + ) + if kind not in backend.PROVIDER_TYPES: + raise HTTPException( + status_code=400, + detail=f"不支持的接口类型:{kind or '(空)'}", + ) + if not base_url: + raise HTTPException(status_code=400, detail="接口地址不能为空") + + try: + config = backend.model_test_config( + { + "AI_PROVIDER_TYPE": kind, + "AI_API_BASE": base_url, + "AI_MODEL": model, + "AI_API_KEY": api_key, + "AI_TIMEOUT": body.timeout_seconds, + "AI_ENDPOINT_MODE": endpoint_mode or "auto", + }, + {}, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + result = backend.test_model_connection(config) + database.audit( + principal.id, + "model.test", + f"provider={label or body.provider_id or '临时'}, ok={int(bool(result['ok']))}, " + f"http={result.get('http_status')}, endpoint={str(result.get('endpoint') or '')[:200]}", + client_ip(request), + ) + # 故意返回 200 而不是 502:测试本身跑成功了,"连不上"是它的结论, + # 不是这个接口的失败。返 5xx 会让前端的错误拦截器弹一个通用报错, + # 把真正有用的诊断信息盖掉。 + return {"result": result, "label": label} + + # ── 桌面端同步 ─────────────────────────────────────────────────────── + # 桌面客户端拉的就是这一个接口。老后台(8765)已退役,这里是唯一来源; + # 客户端仍保留 v2→v1 回退,是为了兼容还没升级到新服务端的部署。 + @app.get("/api/v2/desktop/config") + async def desktop_config( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + # 这一份的结构由 `admin_backend.desktop_config_payload` 统一给出。 + # 两处各写一份的话迟早漂移,表现成"某台客户端少了个字段",极难查。 + payload = backend.desktop_config_payload( + database, + scheme=request.headers.get("x-forwarded-proto", request.url.scheme), + host=request.headers.get("host", ""), + desktop_account=account, + ) + knowledge = app.state.knowledge_store + tenant = str(account["tenant_id"]) + counts = await asyncio.to_thread(knowledge.stats, tenant) + settings = await asyncio.to_thread(knowledge.settings, tenant) + payload["knowledge"] = {"published": counts.get("published", 0), + "enabled": bool(settings["enabled"]), "updated_at": settings["updated_at"]} + return payload + + # 桌面端回流一次编排调用。补上这条之前,客户端改指 8766 会丢掉全部调用留痕 + # ——配置照常同步、回复照常发,唯独调用记录一条不进库,而那恰恰是出事后用来 + # 解释"这句话怎么来的"的东西。它是老后台退役的最后一块拼图。 + @app.post("/api/v2/model/calls") + async def log_model_call( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + try: + record = await request.json() + except Exception: + record = {} + # 落库失败也返回 200:这是观测数据,不能因为它出问题就让桌面端以为 + # 回复流程失败了。`log_model_call` 内部已经吞掉异常并打日志。 + if isinstance(record, dict): + record["desktop_account_id"] = int(account["id"]) + record["tenant_id"] = str(account["tenant_id"]) + database.log_model_call(record) + return {"ok": True} + + @app.get("/api/v2/audit") + async def audit_log( + limit: int = 200, _: Principal = Depends(require("audit:read")) + ) -> dict: + return {"entries": database.audit_entries(limit)} + + @app.get("/api/v2/health") + async def health() -> dict: + return {"status": "ok", "time": time.strftime("%Y-%m-%d %H:%M:%S")} + + # 聊天归档是独立新模块,只复用当前登录、权限和审计能力。 + # 必须在 SPA 通配路由之前注册,避免 /api 请求被静态页入口截住。 + archive_api.register_archive_routes( + app, + database, + current, + require, + client_ip, + current_desktop=current_desktop, + resolve_admin_tenant=resolve_admin_tenant, + zyt_api_url=lambda: desktop_login_api_url()[0], + zyt_patient_searcher=zyt_patient_searcher, + ) + + knowledge_api.register_knowledge_routes( + app, database, require, client_ip, resolve_admin_tenant, current_desktop, + ) + + # ── 前端静态资源 ───────────────────────────────────────────────────── + # 构建产物存在时由本服务一起托管,前后端同源——省掉生产环境的 CORS 配置, + # 也省掉一个 Nginx location。开发时前端跑 Vite dev server,走它自己的代理。 + dist = _frontend_dist() + if (dist / "index.html").is_file(): + from fastapi.staticfiles import StaticFiles + from starlette.responses import FileResponse + + # 判据用 index.html 而不是目录本身:构建中途、或者只剩一个空 dist 目录 + # 的时候,目录是在的但没有可服务的东西,挂上去只会让 SPA 兜底路由把 + # 每个请求都回一个不存在的文件。 + # + # 静态子目录也必须逐个确认存在——StaticFiles 的构造函数遇到不存在的 + # 目录**直接抛异常**,整个服务起不来。vite 默认输出的是 js/ 和 css/, + # 不是 assets/;写死 assets 的话,一旦真的有了构建产物,API 就崩在启动。 + for name in ("assets", "js", "css", "static"): + sub = dist / name + if sub.is_dir(): + app.mount(f"/{name}", StaticFiles(directory=sub), name=name) + + @app.get("/{full_path:path}") + async def spa(full_path: str): + """SPA 的 history 路由:非 /api 的路径一律回 index.html。 + + 少了这一条,用户在 /system/roles 上按 F5 会拿到 404——前端路由是 + 浏览器端的,服务器上并不存在那个文件。 + """ + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="接口不存在") + candidate = dist / full_path + if full_path and candidate.is_file(): + return FileResponse(candidate) + return FileResponse(dist / "index.html") + + return app + + +def main() -> None: + parser = argparse.ArgumentParser(description="管理后台 JSON API") + parser.add_argument("--db", default="backend.db") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8766) + parser.add_argument( + "--zyt-api-url", + default=os.environ.get("ZYT_API_BASE_URL", zyt_auth.DEFAULT_ZYT_API_BASE_URL), + help="ZYT 用户 API 根地址,例如 https://example.com", + ) + parser.add_argument( + "--runtime-file", + default=str(backend.DEFAULT_RUNTIME_FILE), + help="发布实际监听地址,供同机桌面端自动发现", + ) + parser.add_argument( + "--initial-admin-password", + default=os.environ.get( + "WECOM_ADMIN_INITIAL_PASSWORD", backend.DEFAULT_ADMIN_PASSWORD + ), + help="首次创建数据库时的 admin 密码", + ) + parser.add_argument( + "--reset-admin-password", + action="store_true", + help="交互式重置 admin 密码后退出", + ) + args = parser.parse_args() + zyt_auth.DEFAULT_ZYT_API_BASE_URL = str(args.zyt_api_url).strip().rstrip("/") + + # 建库并按需播种 admin。这两件事以前只有老网页后台的 main() 做——不搬过来的 + # 话,全新部署起来之后库里一个用户都没有,Vue 管理端登录页永远进不去,而且 + # 报的是"用户名或密码不正确",完全看不出是根本没建过账号。 + database = backend.Database(Path(args.db).resolve()) + created = database.initialize(args.initial_admin_password) + + if args.reset_admin_password: + import getpass + + first = getpass.getpass("新的 admin 密码:") + if first != getpass.getpass("再次输入:"): + raise SystemExit("两次输入不一致") + if not backend.valid_password(first): + raise SystemExit("密码至少 10 位,并同时包含字母和数字") + database.reset_admin_password(first) + print("admin 密码已重置,下次登录时必须再次修改。") + return + + if created: + print("首次登录账号:admin") + print(f"首次登录密码:{args.initial_admin_password}") + print("登录后必须立即修改初始密码。") + if args.host not in ("127.0.0.1", "localhost", "::1"): + print("警告:当前监听非本机地址;生产环境请通过 HTTPS 反向代理访问。") + + import uvicorn + + # 发布本机地址,桌面端靠它自动发现后台。 + # + # 这件事以前只有老网页后台(8765)做。不搬过来的话,老后台一停,桌面端的 + # 自动发现就永远指向一个没人监听的 8765,报出来的是"无法连接后台"——而真正 + # 在跑的 8766 从来没被试过。这是老后台能退役的前提之一。 + runtime_path = Path(args.runtime_file) + backend.write_runtime_info(runtime_path, args.host, args.port) + try: + uvicorn.run( + create_app(args.db), host=args.host, port=args.port, log_level="info" + ) + finally: + backend.clear_runtime_info(runtime_path, args.port) + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-remote-20260928/baseline-snapshot/wechat_rpa/zyt_auth.py b/deploy/mobile-remote-20260928/baseline-snapshot/wechat_rpa/zyt_auth.py new file mode 100644 index 000000000..fdfeb0f4d --- /dev/null +++ b/deploy/mobile-remote-20260928/baseline-snapshot/wechat_rpa/zyt_auth.py @@ -0,0 +1,162 @@ +"""ZYT 管理员身份服务适配器。 + +桌面软件的密码只发送给 ZYT;客服后台拿到管理员 token 后通过最小会话接口验证, +再签发仅能访问客服数据的短期 token。 +""" + +from __future__ import annotations + +import json +import os +import socket +import urllib.error +import urllib.parse +import urllib.request +from typing import Any + + +WECOM_RPA_TERMINAL = 7 +DEFAULT_ZYT_API_BASE_URL = os.environ.get( + "ZYT_API_BASE_URL", "http://127.0.0.1" +).strip().rstrip("/") + + +class ZytAuthError(RuntimeError): + pass + + +def api_base_url() -> str: + value = DEFAULT_ZYT_API_BASE_URL + parsed = urllib.parse.urlparse(value) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytAuthError("ZYT_API_BASE_URL 配置不正确") + return value.rstrip("/") + + +def verify_desktop_token( + token: str, *, api_url: str | None = None, timeout: float = 8.0 +) -> dict[str, Any]: + token = str(token or "").strip() + if not token: + raise ZytAuthError("缺少 ZYT 登录令牌") + base_url = str(api_url or api_base_url()).strip().rstrip("/") + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytAuthError("ZYT 登录 API 域名配置不正确") + last_error = "ZYT 登录已失效" + # 第一条是新版专用接口;第二条兼容尚未发布桌面终端的线上旧版。 + endpoints = ( + ("/adminapi/desktop/session", False), + ("/adminapi/auth.admin/mySelf", True), + ) + for path, legacy in endpoints: + request = urllib.request.Request( + f"{base_url}{path}", + headers={ + "Accept": "application/json", + "User-Agent": "WeCom-RPA-Backend/1.0", + # ZYT 管理端登录中间件读取 token 头,不是 Bearer。 + "token": token, + }, + method="GET", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + payload = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + last_error = f"ZYT 身份服务拒绝请求(HTTP {exc.code})" + # 只兼容尚未部署的路由;鉴权拒绝不能降级为网页身份。 + if not legacy and exc.code in {404, 405}: + continue + raise ZytAuthError(last_error) from exc + except (urllib.error.URLError, TimeoutError, socket.timeout) as exc: + raise ZytAuthError( + f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytAuthError("ZYT 身份服务返回了无效数据") from exc + + if not isinstance(payload, dict): + raise ZytAuthError("ZYT 身份服务返回了无效数据") + if str(payload.get("code")) != "1": + last_error = str( + payload.get("msg") if isinstance(payload, dict) else "" + ) or "ZYT 登录已失效" + if legacy and "ip" in last_error.lower() and "变化" in last_error: + raise ZytAuthError( + "ZYT 桌面认证接口尚未部署,旧版接口拒绝跨服务器验证。" + "请管理员部署 /adminapi/desktop/session 后重新登录" + ) + raise ZytAuthError(last_error) + data = payload.get("data") + if not isinstance(data, dict): + raise ZytAuthError("ZYT 身份信息不完整") + + if legacy: + admin = data.get("user") + if not isinstance(admin, dict) or not admin.get("id"): + raise ZytAuthError("ZYT 管理员身份信息不完整") + if int(admin.get("disable") or 0) != 0: + raise ZytAuthError("ZYT 管理员账号已停用") + permissions_value = data.get("permissions") + permissions = ( + sorted( + { + str(item).strip() + for item in permissions_value + if str(item).strip() + } + ) + if isinstance(permissions_value, list) + else [] + ) + role_ids_value = admin.get("role_ids") + role_ids: list[int] = [] + if isinstance(role_ids_value, list): + for item in role_ids_value: + try: + role_ids.append(int(item)) + except (TypeError, ValueError): + continue + profile_identity = { + "user_id": f"admin:{admin['id']}", + "sn": str(admin.get("account") or ""), + "nickname": str(admin.get("name") or admin.get("account") or ""), + "mobile": str(admin.get("phone") or ""), + "avatar": str(admin.get("avatar") or ""), + "terminal": WECOM_RPA_TERMINAL, + "status": "active", + "account_type": "admin", + "root": bool(int(admin.get("root") or 0)), + "role_ids": sorted(set(role_ids)), + "permissions": permissions, + "permissions_known": isinstance(permissions_value, list), + } + return profile_identity + + identity = dict(data) + if not identity.get("user_id"): + raise ZytAuthError("ZYT 身份信息不完整") + if str(identity.get("terminal")) != str(WECOM_RPA_TERMINAL): + raise ZytAuthError("当前令牌不是企业微信客服桌面端令牌") + if str(identity.get("status") or "") != "active": + raise ZytAuthError("ZYT 管理员账号已停用") + permissions_value = identity.get("permissions") + identity["permissions"] = ( + sorted( + { + str(item).strip() + for item in permissions_value + if str(item).strip() + } + ) + if isinstance(permissions_value, list) + else [] + ) + identity["permissions_known"] = isinstance(permissions_value, list) + identity["root"] = bool(int(identity.get("root") or 0)) + # 新版专用接口完整返回权限;旧版缺权限时保留未知状态,患者功能 + # 按最小权限关闭,不再跨 IP 请求网页 mySelf 补权限。 + return identity + + raise ZytAuthError(last_error) diff --git a/deploy/mobile-remote-20260928/manifest.json b/deploy/mobile-remote-20260928/manifest.json new file mode 100644 index 000000000..d7086302a --- /dev/null +++ b/deploy/mobile-remote-20260928/manifest.json @@ -0,0 +1,182 @@ +[ + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js", + "sha256": "97abcbc7c960add5c448947dad40ea8ca6ca2ae18069a632b62b61723e43f24f", + "bytes": 2308 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js", + "sha256": "8c06092db48aca97b0d3fb35871315e4434b5ad5ccd9c5d44a7cfc3ba343db22", + "bytes": 14528 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css", + "sha256": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "bytes": 359 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png", + "sha256": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "bytes": 16743 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js", + "sha256": "7a648b566b9c00c1c8d1b6d2ca9737ca237e521f0e25065cedf86e82fa2471d8", + "bytes": 4490 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js", + "sha256": "c145f55146ff5373b293e2922b021f55c6851fa34db65fdacb64139fb88c4fa8", + "bytes": 28526 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css", + "sha256": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "bytes": 4220 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js", + "sha256": "ddb9c705e5cc4822e9166686a7eab319c772540981134f30a9f850fc85ac8692", + "bytes": 10460 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css", + "sha256": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "bytes": 3983 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js", + "sha256": "393394f3545f3224264be9d1eb87c72b6ce474651b3794b3839f43bfbd69aa86", + "bytes": 8422 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js", + "sha256": "d6a7fc2e067a2adcf92b498812422755d5ac8f00acfcf07fd935201eee35c056", + "bytes": 14363 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css", + "sha256": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "bytes": 97051 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js", + "sha256": "63a10401ab035b495ba24c55f9b1bd7ef54c76c6d6bc549f4b0eda62dbd915ac", + "bytes": 117757 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css", + "sha256": "b561a20e6d76366db7634b028098ed9f848f665b95f0ffe538f02abce87e8991", + "bytes": 1242 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js", + "sha256": "ded80bbda052a9da5f7beaab6af536258b706f2bf0688331fa8bccd9e23355c6", + "bytes": 6668 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css", + "sha256": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "bytes": 927 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js", + "sha256": "711a690039d1fe0d648c285da7bd1ef5b62afe8249d10ad7e76208318fc3094d", + "bytes": 3465 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js", + "sha256": "8a2afb226d06ece5a929c2c85c4c73bbf68977b4ccde960aa7f6a5ae4614af62", + "bytes": 2939 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css", + "sha256": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "bytes": 130 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js", + "sha256": "168066192a95a203466245545388b756dfd035c466b007e65ccadc1c94f3fd80", + "bytes": 607 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png", + "sha256": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "bytes": 24681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png", + "sha256": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "bytes": 5439 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png", + "sha256": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "bytes": 26433 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png", + "sha256": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "bytes": 144310 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png", + "sha256": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "bytes": 95961 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/index.html", + "sha256": "d11023af89db5c20bde0c4eaaa35fd9ad192a85353165c0a675e139ae5d94f13", + "bytes": 1357 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest", + "sha256": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "bytes": 684 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/sw.js", + "sha256": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "bytes": 339 + }, + { + "path": "payload/deploy/im-admin/Dockerfile", + "sha256": "d52249fe81d3537742ecd500f3b1b4c6f64d5048f74177d188946cbf2ac4224b", + "bytes": 802 + }, + { + "path": "payload/wechat_rpa/admin_api.py", + "sha256": "109919284e4d9877a1f9699a27a8fdba11db8be104b99379862232c880368485", + "bytes": 72796 + }, + { + "path": "payload/wechat_rpa/remote_api.py", + "sha256": "843f0a814a0de4f8078f7f9f23a4ac92388d7556c9fe378987f67eda46c55c5b", + "bytes": 37534 + }, + { + "path": "payload/wechat_rpa/remote_store.py", + "sha256": "38304e831a7085808e7395dada5cdb3ecad2335fe49ba3dab29a6267d1a95006", + "bytes": 53499 + }, + { + "path": "payload/wechat_rpa/zyt_auth.py", + "sha256": "f5fd01f08668fa223cf9b303df3d9b82758575b0ae17db447c7bf70d14be63e8", + "bytes": 15750 + }, + { + "path": "tests/test_backend.py", + "sha256": "7473ec5583c97941e132ade7a1f66abcf54fa6108ec6bb2a182f0152964fff61", + "bytes": 66279 + }, + { + "path": "tests/test_remote_api.py", + "sha256": "d3d4eef5dc7a6774da1f6fc30fe5aa65e9e11eb49b0ace8a72883544f2cbb09a", + "bytes": 38197 + }, + { + "path": "tests/test_zyt_auth.py", + "sha256": "40ab2d37f51780af26497e22948ef665c92b7f3707f6ba30a6d711bf5b15dfa6", + "bytes": 4907 + } +] \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/mobile-remote-server-bundle.tar.gz b/deploy/mobile-remote-20260928/mobile-remote-server-bundle.tar.gz new file mode 100644 index 000000000..8483cfa45 Binary files /dev/null and b/deploy/mobile-remote-20260928/mobile-remote-server-bundle.tar.gz differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js new file mode 100644 index 000000000..9d43b6f76 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{c as n,d as r,l as i,u as a}from"./format-DEXJ_MrY.js";import{n as o,o as s,s as c,t as l}from"./cell-group-B1PakqJq.js";import{n as u,r as d,t as f}from"./field-CRX3eS7h.js";import{$ as p,Bt as m,Ct as h,Dt as g,E as _,Et as v,Ft as y,G as b,Gt as x,Jt as S,K as C,Kt as w,Lt as T,Mt as E,Nt as ee,O as D,Q as O,St as k,Ut as A,V as te,Vt as j,X as M,Y as N,Yt as ne,Z as re,_ as P,_t as F,a as ie,c as ae,et as oe,ft as se,g as I,gt as L,ht as ce,l as le,n as ue,o as R,ot as z,q as B,qt as V,rt as de,vt as fe,wt as pe,x as me,yt as H}from"./index-Gkjcdjlo.js";var U=0;function he(e){e?(U||document.body.classList.add(`van-toast--unclickable`),U++):U&&(U--,U||document.body.classList.remove(`van-toast--unclickable`))}var[ge,W]=P(`toast`),_e=[`show`,`overlay`,`teleport`,`transition`,`overlayClass`,`overlayStyle`,`closeOnClickOverlay`,`zIndex`],ve={icon:String,show:Boolean,type:N(`text`),overlay:Boolean,message:M,iconSize:M,duration:C(2e3),position:N(`middle`),teleport:[String,Object],wordBreak:String,className:null,iconPrefix:String,transition:N(`van-fade`),loadingType:String,forbidClick:Boolean,overlayClass:null,overlayStyle:Object,closeOnClick:Boolean,closeOnClickOverlay:Boolean,zIndex:M},ye=h({name:ge,props:ve,emits:[`update:show`],setup(t,{emit:n,slots:r}){let i,o=!1,c=()=>{let e=t.show&&t.forbidClick;o!==e&&(o=e,he(o))},l=e=>n(`update:show`,e),u=()=>{t.closeOnClick&&l(!1)},d=()=>clearTimeout(i),f=()=>{let{icon:n,type:r,iconSize:i,iconPrefix:a,loadingType:o}=t;if(n||r===`success`||r===`fail`)return k(e,{name:n||r,size:i,class:W(`icon`),classPrefix:a},null);if(r===`loading`)return k(s,{class:W(`loading`),size:i,type:o},null)},p=()=>{let{type:e,message:n}=t;if(r.message)return k(`div`,{class:W(`text`)},[r.message()]);if(oe(n)&&n!==``)return e===`html`?k(`div`,{key:0,class:W(`text`),innerHTML:String(n)},null):k(`div`,{class:W(`text`)},[n])};return j(()=>[t.show,t.forbidClick],c),j(()=>[t.show,t.type,t.message,t.duration],()=>{d(),t.show&&t.duration>0&&(i=setTimeout(()=>{l(!1)},t.duration))}),E(c),ee(c),()=>k(a,v({class:[W([t.position,t.wordBreak===`normal`?`break-normal`:t.wordBreak,{[t.type]:!t.icon}]),t.className],lockScroll:!1,onClick:u,onClosed:d,"onUpdate:show":l},z(t,_e)),{default:()=>[f(),p()]})}}),be={icon:``,type:`text`,message:``,className:``,overlay:!1,onClose:void 0,onOpened:void 0,duration:2e3,teleport:`body`,iconSize:void 0,iconPrefix:void 0,position:`middle`,transition:`van-fade`,forbidClick:!1,loadingType:void 0,overlayClass:``,overlayStyle:void 0,closeOnClick:!1,closeOnClickOverlay:!1},G=[],K=O({},be),xe=new Map;function Se(e){return de(e)?e:{message:e}}function Ce(){let{instance:e,unmount:t}=n({setup(){let e=w(``),{open:t,state:n,close:r,toggle:a}=i(),o=()=>{},s=()=>k(ye,v(n,{onClosed:o,"onUpdate:show":a}),null);return j(e,e=>{n.message=e}),pe().render=s,{open:t,close:r,message:e}}});return e}function we(){if(!G.length){let e=Ce();G.push(e)}return G[G.length-1]}function Te(e={}){if(!p)return{};let t=we(),n=Se(e);return t.open(O({},K,xe.get(n.type||K.type),n)),t}var[Ee,q]=P(`empty`),De={image:N(`default`),imageSize:[Number,String,Array],description:String},Oe=h({name:Ee,props:De,setup(e,{slots:t}){let n=()=>{let n=t.description?t.description():e.description;if(n)return k(`p`,{class:q(`description`)},[n])},r=()=>{if(t.default)return k(`div`,{class:q(`bottom`)},[t.default()])},i=d(),a=e=>`${i}-${e}`,o=e=>`url(#${a(e)})`,s=(e,t,n)=>k(`stop`,{"stop-color":e,offset:`${t}%`,"stop-opacity":n},null),c=(e,t)=>[s(e,0),s(t,100)],l=e=>[k(`defs`,null,[k(`radialGradient`,{id:a(e),cx:`50%`,cy:`54%`,fx:`50%`,fy:`54%`,r:`297%`,gradientTransform:`matrix(-.16 0 0 -.33 .58 .72)`,"data-allow-mismatch":`attribute`},[s(`#EBEDF0`,0),s(`#F2F3F5`,100,.3)])]),k(`ellipse`,{fill:o(e),opacity:`.8`,cx:`80`,cy:`140`,rx:`46`,ry:`8`,"data-allow-mismatch":`attribute`},null)],u=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`a`),x1:`64%`,y1:`100%`,x2:`64%`,"data-allow-mismatch":`attribute`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M36 131V53H16v20H2v58h34z`,fill:o(`a`)},null),k(`path`,{d:`M123 15h22v14h9v77h-31V15z`,fill:o(`a`)},null)])],f=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`b`),x1:`64%`,y1:`97%`,x2:`64%`,y2:`0%`,"data-allow-mismatch":`attribute`},[s(`#F2F3F5`,0,.3),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M87 6c3 0 7 3 8 6a8 8 0 1 1-1 16H80a7 7 0 0 1-8-6c0-4 3-7 6-7 0-5 4-9 9-9Z`,fill:o(`b`)},null),k(`path`,{d:`M19 23c2 0 3 1 4 3 2 0 4 2 4 4a4 4 0 0 1-4 3v1h-7v-1l-1 1c-2 0-3-2-3-4 0-1 1-3 3-3 0-2 2-4 4-4Z`,fill:o(`b`)},null)])],p=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{id:a(1),x1:`64%`,y1:`100%`,x2:`64%`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)]),k(`linearGradient`,{id:a(2),x1:`50%`,x2:`50%`,y2:`84%`},[s(`#EBEDF0`,0),s(`#DCDEE0`,100,0)]),k(`linearGradient`,{id:a(3),x1:`100%`,x2:`100%`,y2:`100%`},[c(`#EAEDF0`,`#DCDEE0`)]),k(`radialGradient`,{id:a(4),cx:`50%`,cy:`0%`,fx:`50%`,fy:`0%`,r:`100%`,gradientTransform:`matrix(0 1 -.54 0 .5 -.5)`},[s(`#EBEDF0`,0),s(`#FFF`,100,0)])]),k(`g`,{fill:`none`},[u(),k(`path`,{fill:o(4),d:`M0 139h160v21H0z`,"data-allow-mismatch":`attribute`},null),k(`path`,{d:`M80 54a7 7 0 0 1 3 13v27l-2 2h-2a2 2 0 0 1-2-2V67a7 7 0 0 1 3-13z`,fill:o(2),"data-allow-mismatch":`attribute`},null),k(`g`,{opacity:`.6`,"stroke-linecap":`round`,"stroke-width":`7`,"data-allow-mismatch":`children`},[k(`path`,{d:`M64 47a19 19 0 0 0-5 13c0 5 2 10 5 13`,stroke:o(3)},null),k(`path`,{d:`M53 36a34 34 0 0 0 0 48`,stroke:o(3)},null),k(`path`,{d:`M95 73a19 19 0 0 0 6-13c0-5-2-9-6-13`,stroke:o(3)},null),k(`path`,{d:`M106 84a34 34 0 0 0 0-48`,stroke:o(3)},null)]),k(`g`,{transform:`translate(31 105)`},[k(`rect`,{fill:`#EBEDF0`,width:`98`,height:`34`,rx:`2`},null),k(`rect`,{fill:`#FFF`,x:`9`,y:`8`,width:`80`,height:`18`,rx:`1.1`},null),k(`rect`,{fill:`#EBEDF0`,x:`15`,y:`12`,width:`18`,height:`6`,rx:`1.1`},null)])])]),m=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(5)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`95%`,y1:`48%`,x2:`5.5%`,y2:`51%`,id:a(6)},[c(`#EAEDF1`,`#DCDEE0`)]),k(`linearGradient`,{y1:`45%`,x2:`100%`,y2:`54%`,id:a(7)},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),k(`g`,{transform:`translate(36 50)`,fill:`none`},[k(`g`,{transform:`translate(8)`},[k(`rect`,{fill:`#EBEDF0`,opacity:`.6`,x:`38`,y:`13`,width:`36`,height:`53`,rx:`2`},null),k(`rect`,{fill:o(5),width:`64`,height:`66`,rx:`2`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#FFF`,x:`6`,y:`6`,width:`52`,height:`55`,rx:`1`},null),k(`g`,{transform:`translate(15 17)`,fill:o(6),"data-allow-mismatch":`attribute`},[k(`rect`,{width:`34`,height:`6`,rx:`1`},null),k(`path`,{d:`M0 14h34v6H0z`},null),k(`rect`,{y:`28`,width:`34`,height:`6`,rx:`1`},null)])]),k(`rect`,{fill:o(7),y:`61`,width:`88`,height:`28`,rx:`1`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#F7F8FA`,x:`29`,y:`72`,width:`30`,height:`6`,rx:`1`},null)])]),h=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,null,[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(8),"data-allow-mismatch":`attribute`},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),l(`c`),k(`path`,{d:`m59 60 21 21 21-21h3l9 9v3L92 93l21 21v3l-9 9h-3l-21-21-21 21h-3l-9-9v-3l21-21-21-21v-3l9-9h3Z`,fill:o(8),"data-allow-mismatch":`attribute`},null)]),g=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,y1:`100%`,x2:`50%`,id:a(9)},[c(`#EEE`,`#D8D8D8`)]),k(`linearGradient`,{x1:`100%`,y1:`50%`,y2:`50%`,id:a(10)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(11)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(12)},[c(`#FFF`,`#F7F8FA`)])]),u(),f(),l(`d`),k(`g`,{transform:`rotate(-45 113 -4)`,fill:`none`,"data-allow-mismatch":`children`},[k(`rect`,{fill:o(9),x:`24`,y:`52.8`,width:`5.8`,height:`19`,rx:`1`},null),k(`rect`,{fill:o(10),x:`22.1`,y:`67.3`,width:`9.9`,height:`28`,rx:`1`},null),k(`circle`,{stroke:o(11),"stroke-width":`8`,cx:`27`,cy:`27`,r:`27`},null),k(`circle`,{fill:o(12),cx:`27`,cy:`27`,r:`16`},null),k(`path`,{d:`M37 7c-8 0-15 5-16 12`,stroke:o(11),"stroke-width":`3`,opacity:`.5`,"stroke-linecap":`round`,transform:`rotate(45 29 13)`},null)])]),_=()=>{var n;if(t.image)return t.image();let r={error:h,search:g,network:p,default:m};return((n=r[e.image])==null?void 0:n.call(r))||k(`img`,{src:e.image},null)};return()=>k(`div`,{class:q()},[k(`div`,{class:q(`image`),style:me(e.imageSize)},[_()]),n(),r()])}}),J=I(Oe),[ke,Y,Ae]=P(`pull-refresh`),X=50,je=[`pulling`,`loosing`,`success`],Me={disabled:Boolean,modelValue:Boolean,headHeight:B(X),successText:String,pullingText:String,loosingText:String,loadingText:String,pullDistance:M,successDuration:B(500),animationDuration:B(300)},Ne=h({name:ke,props:Me,emits:[`change`,`refresh`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i,a=w(),o=w(),c=b(a),l=x({status:`normal`,distance:0,duration:0}),u=r(),d=()=>{if(e.headHeight!==X)return{height:`${e.headHeight}px`}},f=()=>l.status!==`loading`&&l.status!==`success`&&!e.disabled,p=t=>{let n=+(e.pullDistance||e.headHeight);return t>n&&(t=t{let i=+(e.pullDistance||e.headHeight);l.distance=n,r?l.status=`loading`:n===0?l.status=`normal`:n{let{status:t}=l;return t===`normal`?``:e[`${t}Text`]||Ae(t)},v=()=>{let{status:e,distance:t}=l;if(n[e])return n[e]({distance:t});let r=[];return je.includes(e)&&r.push(k(`div`,{class:Y(`text`)},[h()])),e===`loading`&&r.push(k(s,{class:Y(`loading`)},{default:h})),r},y=()=>{l.status=`success`,setTimeout(()=>{m(0)},+e.successDuration)},S=e=>{i=_(c.value)===0,i&&(l.duration=0,u.start(e))},C=e=>{f()&&S(e)},T=e=>{if(f()){i||S(e);let{deltaY:t}=u;u.move(e),i&&t.value>=0&&u.isVertical()&&(D(e),m(p(t.value)))}},E=()=>{i&&u.deltaY.value&&f()&&(l.duration=+e.animationDuration,l.status===`loosing`?(m(+e.headHeight,!0),t(`update:modelValue`,!0),g(()=>t(`refresh`))):m(0))};return j(()=>e.modelValue,t=>{l.duration=+e.animationDuration,t?m(+e.headHeight,!0):n.success||e.successText?y():m(0,!1)}),te(`touchmove`,T,{target:o}),()=>{var e;let t={transitionDuration:`${l.duration}ms`,transform:l.distance?`translate3d(0,${l.distance}px, 0)`:``};return k(`div`,{ref:a,class:Y()},[k(`div`,{ref:o,class:Y(`track`),style:t,onTouchstartPassive:C,onTouchend:E,onTouchcancel:E},[k(`div`,{class:Y(`head`),style:d()},[v()]),(e=n.default)==null?void 0:e.call(n)])])}}}),Pe=I(Ne),[Fe,Z,Q]=P(`search`),Ie=O({},u,{label:String,shape:N(`square`),leftIcon:N(`search`),clearable:re,actionText:String,background:String,showAction:Boolean}),Le=h({name:Fe,props:Ie,emits:[`blur`,`focus`,`clear`,`search`,`cancel`,`clickInput`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(e,{emit:t,slots:n,attrs:r}){let i=d(),a=w(),o=()=>{n.action||(t(`update:modelValue`,``),t(`cancel`))},s=n=>{n.keyCode===13&&(D(n),t(`search`,e.modelValue))},l=()=>e.id||`${i}-input`,p=()=>{if(n.label||e.label)return k(`label`,{class:Z(`label`),for:l(),"data-allow-mismatch":`attribute`},[n.label?n.label():e.label])},m=()=>{if(e.showAction){let t=e.actionText||Q(`cancel`);return k(`div`,{class:Z(`action`),role:`button`,tabindex:0,onClick:o},[n.action?n.action():t])}},h=()=>{var e;return(e=a.value)==null?void 0:e.blur()},g=()=>{var e;return(e=a.value)==null?void 0:e.focus()},_=e=>t(`blur`,e),y=e=>t(`focus`,e),b=e=>t(`clear`,e),x=e=>t(`clickInput`,e),S=e=>t(`clickLeftIcon`,e),C=e=>t(`clickRightIcon`,e),T=Object.keys(u),E=()=>{let i=O({},r,z(e,T),{id:l()});return k(f,v({ref:a,type:`search`,class:Z(`field`,{"with-message":i.errorMessage}),border:!1,labelAlign:`left`,onBlur:_,onFocus:y,onClear:b,onKeypress:s,onClickInput:x,onClickLeftIcon:S,onClickRightIcon:C,"onUpdate:modelValue":e=>t(`update:modelValue`,e)},i),z(n,[`left-icon`,`right-icon`]))};return c({focus:g,blur:h}),()=>{var t;return k(`div`,{class:Z({"show-action":e.showAction}),style:{background:e.background}},[(t=n.left)==null?void 0:t.call(n),k(`div`,{class:Z(`content`,e.shape)},[p(),E()]),m()])}}}),$=I(Le),Re={class:`switcher-body`},ze={key:0,class:`switcher-state`},Be={key:0,class:`current`},Ve=t(h({__name:`AccountSwitcher`,props:{show:{type:Boolean,default:!1},showModifiers:{}},emits:[`update:show`],setup(e){let t=m(e,`show`),n=w(``),r=w([]),i=w(!1),c=w(``),u=0,d,f=ce(()=>{var e;return(e=ue.me)==null?void 0:e.account});async function p(){let e=++u;i.value=!0,c.value=``;try{let{items:t}=await le.accounts(n.value.trim());e===u&&(r.value=t.filter(e=>{var t;return e.id!==((t=f.value)==null?void 0:t.id)}))}catch(t){e===u&&(c.value=t instanceof ae?t.message:`加载失败`)}finally{e===u&&(i.value=!1)}}j(t,e=>{e&&p()}),j(n,()=>{window.clearTimeout(d),d=window.setTimeout(p,300)});function h(e){ie(e),t.value=!1}function g(e){return e.device_count?e.online?e.listening?`监听中`:`已停止`:`离线`:`没有电脑`}function _(e){return e.online?e.listening?`tone-success`:`tone-warning`:`tone-muted`}return(e,u)=>(y(),F(V(a),{show:t.value,"onUpdate:show":u[2]||(u[2]=e=>t.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`switcher`},{default:A(()=>[L(`div`,Re,[u[3]||(u[3]=L(`h3`,null,`切换客服`,-1)),k(V($),{modelValue:n.value,"onUpdate:modelValue":u[0]||(u[0]=e=>n.value=e),shape:`round`,placeholder:`搜索客服姓名或账号`},null,8,[`modelValue`]),k(V(l),{inset:``},{default:A(()=>[k(V(o),{title:`我自己`,label:f.value?`${f.value.nickname||f.value.sn}`:``,icon:`user-o`,clickable:``,value:V(R)===void 0?`当前`:``,onClick:u[1]||(u[1]=e=>h(null))},null,8,[`label`,`value`])]),_:1}),i.value&&!r.value.length?(y(),H(`div`,ze,[k(V(s),{size:`20`})])):c.value?(y(),F(V(J),{key:1,image:`network`,description:c.value},null,8,[`description`])):r.value.length?(y(),F(V(l),{key:3,inset:``,class:`switcher-list`},{default:A(()=>[(y(!0),H(se,null,T(r.value,e=>(y(),F(V(o),{key:e.id,title:e.nickname||e.sn||`客服 #${e.id}`,label:e.sn,clickable:``,onClick:t=>h(e)},{value:A(()=>[L(`span`,{class:S(_(e))},ne(g(e)),3),V(R)===e.id?(y(),H(`span`,Be,`当前`)):fe(``,!0)]),_:2},1032,[`title`,`label`,`onClick`]))),128))]),_:1})):(y(),F(V(J),{key:2,image:`search`,description:`没有找到其他客服`}))])]),_:1},8,[`show`]))}}),[[`__scopeId`,`data-v-91cf283c`]]);export{Te as a,J as i,$ as n,Pe as r,Ve as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css new file mode 100644 index 000000000..03b9e2d0d --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css @@ -0,0 +1 @@ +.switcher[data-v-91cf283c]{max-height:80vh}.switcher-body[data-v-91cf283c]{padding:16px 0 12px}.switcher-body h3[data-v-91cf283c]{margin:0 16px 8px;font-size:17px}.switcher-state[data-v-91cf283c]{place-items:center;padding:32px 0;display:grid}.switcher-list[data-v-91cf283c]{margin-top:12px}.current[data-v-91cf283c]{color:var(--app-primary);margin-left:6px} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js new file mode 100644 index 000000000..a58d214a6 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,n as r,p as i,r as a,t as o}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n as s}from"./use-placeholder-CcVnlsb4.js";import{a as c,d as l,i as u,o as d,s as f,u as p}from"./format-DEXJ_MrY.js";import{a as m,n as h,o as g,s as _,t as v}from"./cell-group-B1PakqJq.js";import{r as y,t as b}from"./field-CRX3eS7h.js";import{a as x,i as S,n as ee,r as C,t as w}from"./AccountSwitcher-5FuHazzf.js";import{$ as te,A as ne,At as T,B as E,C as D,Ct as O,D as k,Dt as A,E as j,Et as M,F as N,Ft as P,G as re,Gt as ie,H as ae,Ht as F,I,It as oe,J as se,Jt as ce,Kt as L,Lt as le,M as ue,Mt as de,N as fe,O as pe,Ot as me,Pt as he,Q as ge,R as _e,S as ve,St as R,Tt as ye,U as be,Ut as z,V as xe,Vt as B,W as V,Wt as Se,X as H,Y as Ce,Yt as U,Z as W,_ as G,_t as K,bt as we,c as Te,et as Ee,ft as De,g as Oe,gt as q,ht as J,j as ke,jt as Ae,kt as je,l as Me,n as Ne,o as Pe,ot as Fe,p as Ie,q as Y,qt as X,s as Le,t as Re,ut as ze,v as Be,vt as Z,w as Ve,wt as He,xt as Q,y as Ue,yt as $,z as We}from"./index-Gkjcdjlo.js";var Ge=Array.isArray,Ke=e=>typeof e==`string`,qe=e=>typeof e==`object`&&!!e,Je=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ye=/\B([A-Z])/g,Xe=Je(e=>e.replace(Ye,`-$1`).toLowerCase());function Ze(e){if(Ge(e)){let t={};for(let n=0;ne.startsWith(`/*`)?``:e).split(Qe).forEach(e=>{if(e){let n=e.split($e);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function nt(e){if(!e)return``;if(Ke(e))return e;let t=``;for(let n in e){let r=e[n];if(Ke(r)||typeof r==`number`){let e=n.startsWith(`--`)?n:Xe(n);t+=`${e}:${r};`}}return t}function rt(e){let t=``;if(Ke(e))t=e;else if(Ge(e))for(let n=0;nt||!o&&at?i=We(u):r&&(i=We(r))}return u(),l}function ot(){let e=L([]),t=[];return T(()=>{e.value=[]}),[e,n=>(t[n]||(t[n]=t=>{e.value[n]=t}),t[n])]}function st(e,t){if(!te||!window.IntersectionObserver)return;let n=new IntersectionObserver(e=>{t(e[0].intersectionRatio>0)},{root:document.body}),r=()=>{e.value&&n.observe(e.value)},i=()=>{e.value&&n.unobserve(e.value)};Ae(i),je(i),_e(r)}var[ct,lt]=G(`sticky`),ut={zIndex:H,position:Ce(`top`),container:Object,offsetTop:Y(0),offsetBottom:Y(0)},dt=O({name:ct,props:ut,emits:[`scroll`,`change`],setup(e,{emit:t,slots:n}){let r=L(),i=re(r),a=ie({fixed:!1,width:0,height:0,transform:0}),o=L(!1),s=J(()=>D(e.position===`top`?e.offsetTop:e.offsetBottom)),c=J(()=>{if(o.value)return;let{fixed:e,height:t,width:n}=a;if(e)return{width:`${n}px`,height:`${t}px`}}),l=J(()=>{if(!a.fixed||o.value)return;let t=ge(ve(e.zIndex),{width:`${a.width}px`,height:`${a.height}px`,[e.position]:`${s.value}px`});return a.transform&&(t.transform=`translate3d(0, ${a.transform}px, 0)`),t}),u=e=>t(`scroll`,{scrollTop:e,isFixed:a.fixed}),d=()=>{if(!r.value||k(r))return;let{container:t,position:n}=e,i=V(r),o=j(window);if(a.width=i.width,a.height=i.height,n===`top`){if(t){let e=V(t),n=e.bottom-s.value-a.height;a.fixed=s.value>i.top&&e.bottom>0,a.transform=n<0?n:0}else a.fixed=s.value>i.top}else{let{clientHeight:e}=document.documentElement;if(t){let n=V(t),r=e-n.top-s.value-a.height;a.fixed=e-s.valuen.top,a.transform=r<0?-r:0}else a.fixed=e-s.valuea.fixed,e=>t(`change`,e)),xe(`scroll`,d,{target:i,passive:!0}),st(r,d),B([fe,ue],()=>{r.value&&!k(r)&&a.fixed&&(o.value=!0,A(()=>{let e=V(r);a.width=e.width,a.height=e.height,o.value=!1}))}),()=>{var e;return R(`div`,{ref:r,style:c.value},[R(`div`,{class:lt({fixed:a.fixed&&!o.value}),style:l.value},[(e=n.default)==null?void 0:e.call(n)])])}}}),ft=Oe(dt),[pt,mt]=G(`swipe`),ht={loop:W,width:H,height:H,vertical:Boolean,autoplay:Y(0),duration:Y(500),touchable:W,lazyRender:Boolean,initialSwipe:Y(0),indicatorColor:String,showIndicators:W,stopPropagation:W},gt=Symbol(pt),_t=O({name:pt,props:ht,emits:[`change`,`dragStart`,`dragEnd`],setup(e,{emit:n,slots:r}){let i=L(),a=L(),o=ie({rect:null,width:0,height:0,offset:0,active:0,swiping:!1}),s=!1,c=l(),{children:u,linkChildren:d}=E(gt),f=J(()=>u.length),p=J(()=>o[e.vertical?`height`:`width`]),m=J(()=>e.vertical?c.deltaY.value:c.deltaX.value),h=J(()=>o.rect?(e.vertical?o.rect.height:o.rect.width)-p.value*f.value:0),g=J(()=>p.value?Math.ceil(Math.abs(h.value)/p.value):f.value),v=J(()=>f.value*p.value),y=J(()=>(o.active+f.value)%f.value),b=J(()=>{let t=e.vertical?`vertical`:`horizontal`;return c.direction.value===t}),x=J(()=>{let t={transitionDuration:`${o.swiping?0:e.duration}ms`,transform:`translate${e.vertical?`Y`:`X`}(${+o.offset.toFixed(2)}px)`};if(p.value){let n=e.vertical?`height`:`width`,r=e.vertical?`width`:`height`;t[n]=`${v.value}px`,t[r]=e[r]?`${e[r]}px`:``}return t}),S=t=>{let{active:n}=o;return t?e.loop?Ue(n+t,-1,f.value):Ue(n+t,0,g.value):n},ee=(t,n=0)=>{let r=t*p.value;e.loop||(r=Math.min(r,-h.value));let i=n-r;return e.loop||(i=Ue(i,h.value,0)),i},C=({pace:t=0,offset:r=0,emitChange:i})=>{if(f.value<=1)return;let{active:a}=o,s=S(t),c=ee(s,r);if(e.loop){if(u[0]&&c!==h.value){let e=c0;u[f.value-1].setOffset(e?-v.value:0)}}o.active=s,o.offset=c,i&&s!==a&&n(`change`,y.value)},w=()=>{o.swiping=!0,o.active<=-1?C({pace:f.value}):o.active>=f.value&&C({pace:-f.value})},te=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:-1,emitChange:!0})})},ne=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:1,emitChange:!0})})},T,D=()=>clearTimeout(T),O=()=>{D(),+e.autoplay>0&&f.value>1&&(T=setTimeout(()=>{ne(),O()},+e.autoplay))},j=(t=+e.initialSwipe)=>{if(!i.value)return;let n=()=>{var n,r;if(!k(i)){let t={width:i.value.offsetWidth,height:i.value.offsetHeight};o.rect=t,o.width=+((n=e.width)==null?t.width:n),o.height=+((r=e.height)==null?t.height:r)}f.value&&(t=Math.min(f.value-1,t),t===-1&&(t=f.value-1)),o.active=t,o.swiping=!0,o.offset=ee(t),u.forEach(e=>{e.setOffset(0)}),O()};k(i)?A().then(n):n()},M=()=>j(o.active),N,P=t=>{!e.touchable||t.touches.length>1||(c.start(t),s=!1,N=Date.now(),D(),w())},re=t=>{e.touchable&&o.swiping&&(c.move(t),b.value&&(!e.loop&&(o.active===0&&m.value>0||o.active===f.value-1&&m.value<0)||(pe(t,e.stopPropagation),C({offset:m.value}),s||(n(`dragStart`,{index:y.value}),s=!0))))},F=()=>{if(!e.touchable||!o.swiping)return;let t=Date.now()-N,r=m.value/t;if((Math.abs(r)>.25||Math.abs(m.value)>p.value/2)&&b.value){let t=e.vertical?c.offsetY.value:c.offsetX.value,n=0;n=e.loop?t>0?m.value>0?-1:1:0:-Math[m.value>0?`ceil`:`floor`](m.value/p.value),C({pace:n,emitChange:!0})}else m.value&&C({pace:0});s=!1,o.swiping=!1,n(`dragEnd`,{index:y.value}),O()},oe=(t,n={})=>{w(),c.reset(),I(()=>{let r;r=e.loop&&t===f.value?o.active===0?0:t:t%f.value,n.immediate?I(()=>{o.swiping=!1}):o.swiping=!1,C({pace:r-o.active,emitChange:!0})})},se=(t,n)=>{let r=n===y.value,i=r?{backgroundColor:e.indicatorColor}:void 0;return R(`i`,{style:i,class:mt(`indicator`,{active:r})},null)},ce=()=>{if(r.indicator)return r.indicator({active:y.value,total:f.value});if(e.showIndicators&&f.value>1)return R(`div`,{class:mt(`indicators`,{vertical:e.vertical})},[Array(f.value).fill(``).map(se)])};return _({prev:te,next:ne,state:o,resize:M,swipeTo:oe}),d({size:p,props:e,count:f,activeIndicator:y}),B(()=>e.initialSwipe,e=>j(+e)),B(f,()=>j(o.active)),B(()=>e.autoplay,O),B([fe,ue,()=>e.width,()=>e.height],M),B(ae(),e=>{e===`visible`?O():D()}),de(j),me(()=>j(o.active)),t(()=>j(o.active)),Ae(D),je(D),xe(`touchmove`,re,{target:a}),()=>{var t;return R(`div`,{ref:i,class:mt()},[R(`div`,{ref:a,style:x.value,class:mt(`track`,{vertical:e.vertical}),onTouchstartPassive:P,onTouchend:F,onTouchcancel:F},[(t=r.default)==null?void 0:t.call(r)]),ce()])}}}),vt=Oe(_t),[yt,bt]=G(`tabs`),xt=O({name:yt,props:{count:se(Number),inited:Boolean,animated:Boolean,duration:se(H),swipeable:Boolean,lazyRender:Boolean,currentIndex:se(Number)},emits:[`change`],setup(e,{emit:t,slots:n}){let r=L(),i=e=>t(`change`,e),a=()=>{var t;let a=(t=n.default)==null?void 0:t.call(n);return e.animated||e.swipeable?R(vt,{ref:r,loop:!1,class:bt(`track`),duration:e.duration*1e3,touchable:e.swipeable,lazyRender:e.lazyRender,showIndicators:!1,onChange:i},{default:()=>[a]}):a},o=t=>{let n=r.value;n&&n.state.active!==t&&n.swipeTo(t,{immediate:!e.inited})};return B(()=>e.currentIndex,o),de(()=>{o(e.currentIndex)}),_({swipeRef:r}),()=>R(`div`,{class:bt(`content`,{animated:e.animated||e.swipeable})},[a()])}}),[St,Ct]=G(`tabs`),wt={type:Ce(`line`),color:String,border:Boolean,sticky:Boolean,shrink:Boolean,active:Y(0),duration:Y(.3),animated:Boolean,ellipsis:W,swipeable:Boolean,scrollspy:Boolean,offsetTop:Y(0),background:String,lazyRender:W,showHeader:W,lineWidth:H,lineHeight:H,beforeChange:Function,swipeThreshold:Y(5),titleActiveColor:String,titleInactiveColor:String},Tt=Symbol(St),Et=O({name:St,props:wt,emits:[`change`,`scroll`,`rendered`,`clickTab`,`update:active`],setup(e,{emit:r,slots:a}){let o,c,l,u,d,f=L(),p=L(),m=L(),h=L(),g=y(),v=re(f),[b,x]=ot(),{children:S,linkChildren:ee}=E(Tt),C=ie({inited:!1,position:``,lineStyle:{},currentIndex:-1}),w=J(()=>S.length>+e.swipeThreshold||!e.ellipsis||e.shrink),te=J(()=>({borderColor:e.color,background:e.background})),T=(e,t)=>{var n;return(n=e.name)==null?t:n},O=J(()=>{let e=S[C.currentIndex];if(e)return T(e,C.currentIndex)}),j=J(()=>D(e.offsetTop)),M=J(()=>e.sticky?j.value+o:0),N=t=>{let n=p.value,r=b.value;if(!w.value||!n||!r||!r[C.currentIndex])return;let i=r[C.currentIndex].$el,a=i.offsetLeft-(n.offsetWidth-i.offsetWidth)/2;u&&u(),u=it(n,a,t?0:+e.duration)},P=()=>{let t=C.inited;A(()=>{let n=b.value;if(!n||!n[C.currentIndex]||e.type!==`line`||k(f.value))return;let r=n[C.currentIndex].$el,{lineWidth:i,lineHeight:a}=e,o=r.offsetLeft+r.offsetWidth/2,s={width:Be(i),backgroundColor:e.color,transform:`translateX(${o}px) translateX(-50%)`};if(t&&(s.transitionDuration=`${e.duration}s`),Ee(a)){let e=Be(a);s.height=e,s.borderRadius=e}C.lineStyle=s})},ae=e=>{let t=e=0&&e{let i=ae(t);if(!Ee(i))return;let a=S[i],o=T(a,i),s=C.currentIndex!==null;C.currentIndex!==i&&(C.currentIndex=i,n||N(),P()),o!==e.active&&(r(`update:active`,o),s&&r(`change`,o,a.title)),l&&!e.scrollspy&&ne(Math.ceil(Ve(f.value)-j.value))},I=(e,t)=>{let n=S.findIndex((t,n)=>T(t,n)===e);F(n===-1?0:n,t)},oe=(t=!1)=>{if(e.scrollspy){let n=S[C.currentIndex].$el;if(n&&v.value){let r=Ve(n,v.value)-M.value;c=!0,d&&d(),d=at(v.value,r,t?0:+e.duration,()=>{c=!1})}}},se=(t,i,a)=>{let{title:o,disabled:c}=S[i],l=T(S[i],i);c||(s(e.beforeChange,{args:[l],done:()=>{F(i),oe()}}),n(t)),r(`clickTab`,{name:l,title:o,event:a,disabled:c})},ce=e=>{l=e.isFixed,r(`scroll`,e)},le=e=>{A(()=>{I(e),oe(!0)})},ue=()=>{for(let e=0;eM.value)return e===0?0:e-1}return S.length-1},de=()=>{if(e.scrollspy&&!c){let e=ue();F(e)}},pe=()=>{if(e.type===`line`&&S.length)return R(`div`,{class:Ct(`line`),style:C.lineStyle},null)},he=()=>{var t,n,r;let{type:o,border:s,sticky:c}=e,l=[R(`div`,{ref:c?void 0:m,class:[Ct(`wrap`),{[i]:o===`line`&&s}]},[R(`div`,{ref:p,role:`tablist`,class:Ct(`nav`,[o,{shrink:e.shrink,complete:w.value}]),style:te.value,"aria-orientation":`horizontal`},[(t=a[`nav-left`])==null?void 0:t.call(a),S.map(e=>e.renderTitle(se)),pe(),(n=a[`nav-right`])==null?void 0:n.call(a)])]),(r=a[`nav-bottom`])==null?void 0:r.call(a)];return c?R(`div`,{ref:m},[l]):l},ge=()=>{P(),A(()=>{var e,t;N(!0),(t=(e=h.value)==null?void 0:e.swipeRef.value)==null||t.resize()})};return B(()=>[e.color,e.duration,e.lineWidth,e.lineHeight],P),B(fe,ge),B(()=>e.active,e=>{e!==O.value&&I(e)}),B(()=>S.length,()=>{C.inited&&(I(e.active),P(),A(()=>{N(!0)}))}),_({resize:ge,scrollTo:le}),me(P),t(P),_e(()=>{I(e.active,!0),A(()=>{C.inited=!0,m.value&&(o=V(m.value).height),N(!0)})}),st(f,P),xe(`scroll`,de,{target:v,passive:!0}),ee({id:g,props:e,setLine:P,scrollable:w,onRendered:(e,t)=>r(`rendered`,e,t),currentName:O,setTitleRefs:x,scrollIntoView:N}),()=>R(`div`,{ref:f,class:Ct([e.type])},[e.showHeader?e.sticky?R(ft,{container:f.value,offsetTop:j.value,onScroll:ce},{default:()=>[he()]}):he():null,R(xt,{ref:h,count:S.length,inited:C.inited,animated:e.animated,duration:e.duration,swipeable:e.swipeable,lazyRender:e.lazyRender,currentIndex:C.currentIndex,onChange:F},{default:()=>{var e;return[(e=a.default)==null?void 0:e.call(a)]}})])}}),Dt=Symbol(),Ot=Symbol(),kt=()=>ye(Ot,null),At=e=>{let t=kt();oe(Dt,e),oe(Ot,J(()=>(t==null||t.value)&&e.value))},[jt,Mt]=G(`tab`),Nt=O({name:jt,props:{id:String,dot:Boolean,type:String,color:String,title:String,badge:H,shrink:Boolean,isActive:Boolean,disabled:Boolean,controls:String,scrollable:Boolean,activeColor:String,inactiveColor:String,showZeroBadge:W},setup(e,{slots:t}){let n=J(()=>{let t={},{type:n,color:r,disabled:i,isActive:a,activeColor:o,inactiveColor:s}=e;r&&n===`card`&&(t.borderColor=r,i||(a?t.backgroundColor=r:t.color=r));let c=a?o:s;return c&&(t.color=c),t}),r=()=>{let n=R(`span`,{class:Mt(`text`,{ellipsis:!e.scrollable})},[t.title?t.title():e.title]);return e.dot||Ee(e.badge)&&e.badge!==``?R(a,{dot:e.dot,content:e.badge,showZero:e.showZeroBadge},{default:()=>[n]}):n};return()=>R(`div`,{id:e.id,role:`tab`,class:[Mt([e.type,{grow:e.scrollable&&!e.shrink,shrink:e.shrink,active:e.isActive,disabled:e.disabled}])],style:n.value,tabindex:e.disabled?void 0:e.isActive?0:-1,"aria-selected":e.isActive,"aria-disabled":e.disabled||void 0,"aria-controls":e.controls,"data-allow-mismatch":`attribute`},[r()])}}),[Pt,Ft]=G(`swipe-item`),It=O({name:Pt,setup(e,{slots:t}){let n,r=ie({offset:0,inited:!1,mounted:!1}),{parent:i,index:a}=be(gt);if(!i)return;let o=J(()=>{let e={},{vertical:t}=i.props;return i.size.value&&(e[t?`height`:`width`]=`${i.size.value}px`),r.offset&&(e.transform=`translate${t?`Y`:`X`}(${r.offset}px)`),e}),s=J(()=>{let{loop:e,lazyRender:t}=i.props;if(!t||n)return!0;if(!r.mounted)return!1;let o=i.activeIndicator.value,s=i.count.value-1,c=o===0&&e?s:o-1,l=o===s&&e?0:o+1;return n=a.value===o||a.value===c||a.value===l,n});return de(()=>{A(()=>{r.mounted=!0})}),_({setOffset:e=>{r.offset=e}}),()=>{var e;return R(`div`,{class:Ft(),style:o.value},[s.value?(e=t.default)==null?void 0:e.call(t):null])}}}),Lt=Oe(It),[Rt,zt]=G(`tab`),Bt=ge({},e,{dot:Boolean,name:H,badge:H,title:String,disabled:Boolean,titleClass:null,titleStyle:[String,Object],showZeroBadge:W}),Vt=O({name:Rt,props:Bt,setup(e,{slots:t}){let n=y(),r=L(!1),i=He(),{parent:a,index:o}=be(Tt);if(!a)return;let s=()=>{var t;return(t=e.name)==null?o.value:t},c=()=>{r.value=!0,a.props.lazyRender&&A(()=>{a.onRendered(s(),e.title)})},l=J(()=>{let e=s()===a.currentName.value;return e&&!r.value&&c(),e}),u=L(``),d=L(``);F(()=>{let{titleClass:t,titleStyle:n}=e;u.value=t?rt(t):``,d.value=n&&typeof n!=`string`?nt(Ze(n)):n});let f=r=>R(Nt,M({key:n,id:`${a.id}-${o.value}`,ref:a.setTitleRefs(o.value),style:d.value,class:u.value,isActive:l.value,controls:n,scrollable:a.scrollable.value,activeColor:a.props.titleActiveColor,inactiveColor:a.props.titleInactiveColor,onClick:e=>r(i.proxy,o.value,e)},Fe(a.props,[`type`,`color`,`shrink`]),Fe(e,[`dot`,`badge`,`title`,`disabled`,`showZeroBadge`])),{title:t.title}),p=L(!l.value);return B(l,e=>{e?p.value=!1:I(()=>{p.value=!0})}),B(()=>e.title,()=>{a.setLine(),a.scrollIntoView()}),At(l),_({id:n,renderTitle:f}),()=>{var e;let i=`${a.id}-${o.value}`,{animated:s,swipeable:c,scrollspy:u,lazyRender:d}=a.props;if(!t.default&&!s)return;let f=u||l.value;if(s||c)return R(Lt,{id:n,role:`tabpanel`,class:zt(`panel-wrapper`,{inactive:p.value}),tabindex:l.value?0:-1,"aria-hidden":!l.value,"aria-labelledby":i,"data-allow-mismatch":`attribute`},{default:()=>{var e;return[R(`div`,{class:zt(`panel`)},[(e=t.default)==null?void 0:e.call(t)])]}});let m=r.value||u||!d?(e=t.default)==null?void 0:e.call(t):null;return Se(R(`div`,{id:n,role:`tabpanel`,class:zt(`panel`),tabindex:f?0:-1,"aria-labelledby":i,"data-allow-mismatch":`attribute`},[m]),[[ze,f]])}}}),Ht=Oe(Vt),Ut=Oe(Et),[Wt,Gt,Kt]=G(`list`),qt={error:Boolean,offset:Y(300),loading:Boolean,disabled:Boolean,finished:Boolean,scroller:Object,errorText:String,direction:Ce(`down`),loadingText:{type:String,default:``},finishedText:String,immediateCheck:W},Jt=O({name:Wt,props:qt,emits:[`load`,`update:error`,`update:loading`],setup(e,{emit:t,slots:n}){let r=L(e.loading),i=L(),a=L(),o=kt(),s=re(i),c=J(()=>e.scroller||s.value),l=()=>{A(()=>{if(r.value||e.finished||e.disabled||e.error||(o==null?void 0:o.value)===!1)return;let{direction:n}=e,s=+e.offset,l=V(c);if(!l.height||k(i))return;let u=!1,d=V(a);u=n===`up`?l.top-d.top<=s:d.bottom-l.bottom<=s,u&&(r.value=!0,t(`update:loading`,!0),t(`load`))})},u=()=>{if(e.finished){let t=n.finished?n.finished():e.finishedText;if(t)return R(`div`,{class:Gt(`finished-text`)},[t])}},d=()=>{t(`update:error`,!1),l()},f=()=>{if(e.error){let t=n.error?n.error():e.errorText;if(t)return R(`div`,{role:`button`,class:Gt(`error-text`),tabindex:0,onClick:d},[t])}},p=()=>{if(r.value&&!e.finished&&!e.disabled)return R(`div`,{class:Gt(`loading`)},[n.loading?n.loading():e.loadingText!=null&&R(g,{class:Gt(`loading-icon`)},{default:()=>[e.loadingText||Kt(`loading`)]})])};return B(()=>[e.loading,e.finished,e.error],l),o&&B(o,e=>{e&&l()}),he(()=>{r.value=e.loading}),de(()=>{e.immediateCheck&&l()}),_({check:l}),xe(`scroll`,l,{target:c,passive:!0}),()=>{var t;let o=(t=n.default)==null?void 0:t.call(n),s=R(`div`,{ref:a,class:Gt(`placeholder`)},null);return R(`div`,{ref:i,role:`feed`,class:Gt(),"aria-busy":r.value},[e.direction===`down`?o:s,p(),u(),f(),e.direction===`up`?o:s])}}}),Yt=Oe(Jt),Xt={class:`detail`},Zt={class:`detail-head`},Qt={class:`avatar`,"aria-hidden":`true`},$t={class:`detail-title`},en={class:`app-muted`},tn={key:0,class:`badge`},nn={class:`app-mono`},rn={key:0,class:`state state--blocked`},an={key:1,class:`note`},on={key:1,class:`state`},sn={key:1,class:`note`},cn=o(O({__name:`ConversationDetail`,props:{conversation:{},accountId:{},canAdd:{type:Boolean},canRemove:{type:Boolean},addCode:{},removeCode:{}},emits:[`updated`],setup(e,{emit:t}){let n=e,i=t,a=L(``),o=L(!1);B(()=>`${n.conversation.account}|${n.conversation.conversation}`,()=>{a.value=``});let s=J(()=>n.conversation.blacklist),l=J(()=>{var e;let t=(e=s.value)==null?void 0:e.source;return t===`mobile`?`手机控制台`:t===`desktop`?`客服电脑`:t||`—`});async function d(){if(!o.value){try{await f({title:`加入黑名单?`,message:`加入后,系统不再向「${n.conversation.name}」自动发送任何消息,只能由客服手动处理。`,confirmButtonText:`加入黑名单`,confirmButtonColor:`var(--app-danger)`})}catch{return}o.value=!0;try{let{entry:e}=await Me.block(n.conversation,a.value.trim(),n.accountId);i(`updated`,{...n.conversation,blocked:!0,blacklist:e}),x({message:`已加入黑名单,电脑同步后立即生效`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}async function p(){if(!o.value){try{await f({title:`移出黑名单?`,message:`移出后,系统会恢复向「${n.conversation.name}」自动回复。`,confirmButtonText:`移出`})}catch{return}o.value=!0;try{await Me.unblock(n.conversation.account,n.conversation.conversation,n.accountId),i(`updated`,{...n.conversation,blocked:!1,blacklist:null}),x({message:`已移出黑名单`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}return(t,n)=>(P(),$(`section`,Xt,[q(`header`,Zt,[q(`span`,Qt,U(X(u)(e.conversation.name)),1),q(`div`,$t,[q(`h2`,null,U(e.conversation.name),1),q(`p`,en,U(e.conversation.type_label),1)]),e.conversation.blocked?(P(),$(`span`,tn,[R(X(r),{name:`shield-o`}),n[1]||(n[1]=Q(` 黑名单`,-1))])):Z(``,!0)]),R(X(v),{inset:``,class:`group`},{default:z(()=>[R(X(h),{title:`企业微信账号`,value:e.conversation.account_name||`—`,label:e.conversation.account},null,8,[`value`,`label`]),R(X(h),{title:`最近消息`,value:e.conversation.last_message_at?X(c)(e.conversation.last_message_at):`—`},null,8,[`value`]),R(X(h),{title:`会话标识`},{label:z(()=>[q(`span`,nn,U(e.conversation.conversation),1)]),_:1})]),_:1}),e.conversation.blocked&&s.value?(P(),$(`div`,rn,[q(`h3`,null,[R(X(r),{name:`shield-o`}),n[2]||(n[2]=Q(` 已加入黑名单`,-1))]),n[8]||(n[8]=q(`p`,null,`系统不会向这个会话自动发送消息;电脑离线时,下次联网自动同步。`,-1)),q(`dl`,null,[q(`div`,null,[n[3]||(n[3]=q(`dt`,null,`原因`,-1)),q(`dd`,null,U(s.value.reason||`未填写`),1)]),q(`div`,null,[n[4]||(n[4]=q(`dt`,null,`操作人`,-1)),q(`dd`,null,U(s.value.created_by||`—`),1)]),q(`div`,null,[n[5]||(n[5]=q(`dt`,null,`时间`,-1)),q(`dd`,null,U(X(c)(s.value.created_at)),1)]),q(`div`,null,[n[6]||(n[6]=q(`dt`,null,`来源`,-1)),q(`dd`,null,U(l.value),1)])]),e.canRemove?(P(),K(X(m),{key:0,block:``,round:``,plain:``,type:`primary`,icon:`revoke`,loading:o.value,onClick:p},{default:z(()=>[...n[7]||(n[7]=[Q(` 移出黑名单 `,-1)])]),_:1},8,[`loading`])):(P(),$(`p`,an,`移出黑名单需要主管权限(`+U(e.removeCode)+`),请联系主管处理。`,1))])):(P(),$(`div`,on,[q(`h3`,null,[R(X(r),{name:`info-o`}),n[9]||(n[9]=Q(` 正常接收自动回复`,-1))]),n[11]||(n[11]=q(`p`,null,`客户投诉、需要人工跟进或不适合机器人回复的会话,可以加入黑名单。`,-1)),e.canAdd?(P(),$(De,{key:0},[R(X(v),{inset:``,class:`reason`},{default:z(()=>[R(X(b),{modelValue:a.value,"onUpdate:modelValue":n[0]||(n[0]=e=>a.value=e),label:`原因`,placeholder:`选填,例如:客户要求人工服务`,maxlength:`200`,"show-word-limit":``,rows:`2`,autosize:``,type:`textarea`},null,8,[`modelValue`])]),_:1}),R(X(m),{block:``,round:``,type:`danger`,icon:`shield-o`,loading:o.value,onClick:d},{default:z(()=>[...n[10]||(n[10]=[Q(` 加入黑名单 `,-1)])]),_:1},8,[`loading`])],64)):(P(),$(`p`,sn,`加入黑名单需要 ZYT 权限(`+U(e.addCode)+`),请联系管理员开通。`,1))]))]))}}),[[`__scopeId`,`data-v-70465908`]]),ln={class:`body`},un={class:`line`},dn={class:`name`},fn={class:`time`},pn={class:`line sub`},mn={class:`meta`},hn={key:0,class:`blocked`},gn=o(O({__name:`ConversationRow`,props:{item:{},active:{type:Boolean}},emits:[`select`],setup(e){let t=e,n=J(()=>t.item.type===`group`?`group`:t.item.type===`direct_wechat`?`wechat`:`wecom`);return(t,r)=>(P(),$(`button`,{type:`button`,class:ce([`row`,{active:e.active}]),onClick:r[0]||(r[0]=n=>t.$emit(`select`,e.item))},[q(`span`,{class:ce([`avatar`,`avatar--${n.value}`]),"aria-hidden":`true`},U(X(u)(e.item.name)),3),q(`span`,ln,[q(`span`,un,[q(`strong`,dn,U(e.item.name),1),q(`span`,fn,U(e.item.last_message_at?X(c)(e.item.last_message_at):``),1)]),q(`span`,pn,[q(`span`,mn,[Q(U(e.item.type_label),1),e.item.account_name?(P(),$(De,{key:0},[Q(` · `+U(e.item.account_name),1)],64)):Z(``,!0)]),e.item.blocked?(P(),$(`span`,hn,`已拉黑`)):Z(``,!0)])])],2))}}),[[`__scopeId`,`data-v-645c253b`]]),_n={class:`page`},vn={class:`list-pane`},yn={class:`toolbar`},bn={key:0,class:`detail-pane app-card`},xn=o(O({name:`ConversationsView`,__name:`ConversationsView`,setup(e){let t=Ie(),n=L(``),i=L(`all`),a=L([]),o=L(``),s=L(!1),c=L(!1),l=L(!1),u=L(``),f=L(!1),h=L(0),g=L(null),_=L(!1),v=L(!1),y=0,b=null,x,te=J(()=>{var e;return(e=Ne.me)==null?void 0:e.permission_codes}),ne=J(()=>Pe.value===void 0?`会话`:`${Le.value} 的会话`),T=e=>`${e.account}|${e.conversation}`;function E(){y+=1,b==null||b.abort(),b=null,a.value=[],o.value=``,s.value=!1,l.value=!1,u.value=``,c.value=!1,D()}async function D(){if(s.value)return;let e=y;b=new AbortController,c.value=!0;try{let r=await Me.conversations({accountId:Pe.value,q:n.value.trim(),scope:i.value,cursor:o.value,limit:30,signal:b.signal});if(e!==y)return;let c=new Set(a.value.map(T));a.value=a.value.concat(r.items.filter(e=>!c.has(T(e)))),o.value=r.next_cursor,s.value=!r.has_more,h.value=r.blocked_total,l.value=!1,t.value&&!g.value&&a.value.length&&(g.value=a.value[0])}catch(t){if(e!==y||t instanceof DOMException&&t.name===`AbortError`)return;l.value=!0,u.value=t instanceof Te?t.message:`加载失败`}finally{e===y&&(c.value=!1,f.value=!1)}}function O(){f.value=!0,E()}function k(e){g.value=e,t.value||(_.value=!0)}function A(e){var t;let n=a.value.findIndex(t=>T(t)===T(e)),r=n>=0?a.value[n].blocked:(t=g.value)==null?void 0:t.blocked;n>=0&&(i.value===`blocked`&&!e.blocked?a.value.splice(n,1):a.value.splice(n,1,e)),r!==e.blocked&&(h.value+=e.blocked?1:-1),g.value=e}return B(n,()=>{window.clearTimeout(x),x=window.setTimeout(E,350)}),B(i,E),B(Pe,()=>{g.value=null,_.value=!1,E()}),E(),(e,o)=>{var y,b,x,E,j,M,N,re;return P(),$(`div`,_n,[R(X(d),{title:ne.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},we({_:2},[(y=X(Re))!=null&&y.control_all?{name:`right`,fn:z(()=>[q(`button`,{type:`button`,class:`nav-action`,onClick:o[0]||(o[0]=e=>v.value=!0)},[R(X(r),{name:`friends-o`}),o[9]||(o[9]=Q(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),q(`div`,{class:ce(X(t)?`split`:`single`)},[q(`section`,vn,[q(`div`,yn,[R(X(ee),{modelValue:n.value,"onUpdate:modelValue":o[1]||(o[1]=e=>n.value=e),shape:`round`,placeholder:`搜索会话名称`,clearable:!0},null,8,[`modelValue`]),R(X(Ut),{active:i.value,"onUpdate:active":o[2]||(o[2]=e=>i.value=e),shrink:``,border:!1,class:`tabs`},{default:z(()=>[R(X(Ht),{name:`all`,title:`全部会话`}),R(X(Ht),{name:`blocked`,title:h.value?`黑名单 ${h.value}`:`黑名单`},null,8,[`title`])]),_:1},8,[`active`])]),R(X(C),{modelValue:f.value,"onUpdate:modelValue":o[6]||(o[6]=e=>f.value=e),class:`list-body`,onRefresh:O},{default:z(()=>[R(X(Yt),{loading:c.value,"onUpdate:loading":o[3]||(o[3]=e=>c.value=e),error:l.value,"onUpdate:error":o[4]||(o[4]=e=>l.value=e),finished:s.value,"error-text":`${u.value},点击重试`,"finished-text":a.value.length?`没有更多了`:``,"immediate-check":!1,onLoad:D},{default:z(()=>[(P(!0),$(De,null,le(a.value,e=>(P(),K(gn,{key:T(e),item:e,active:X(t)&&g.value!==null&&T(g.value)===T(e),onSelect:k},null,8,[`item`,`active`]))),128))]),_:1},8,[`loading`,`error`,`finished`,`error-text`,`finished-text`]),s.value&&!a.value.length&&!l.value?(P(),K(X(S),{key:0,image:n.value?`search`:`default`,description:n.value?`没有找到匹配的会话`:i.value===`blocked`?`黑名单是空的`:`还没有归档的会话:电脑开启聊天备份后会自动出现`},{default:z(()=>[n.value?(P(),K(X(m),{key:0,round:``,size:`small`,onClick:o[5]||(o[5]=e=>n.value=``)},{default:z(()=>[...o[10]||(o[10]=[Q(`清除搜索`,-1)])]),_:1})):Z(``,!0)]),_:1},8,[`image`,`description`])):Z(``,!0)]),_:1},8,[`modelValue`])]),X(t)?(P(),$(`section`,bn,[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((b=X(Re))!=null&&b.blacklist_add),"can-remove":!!((x=X(Re))!=null&&x.blacklist_remove),"add-code":(E=(j=te.value)==null?void 0:j.blacklist_add)==null?``:E,"remove-code":(M=(N=te.value)==null?void 0:N.blacklist_remove)==null?``:M,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):(P(),K(X(S),{key:1,image:`default`,description:`选择左侧的会话查看详情`}))])):Z(``,!0)],2),X(t)?Z(``,!0):(P(),K(X(p),{key:0,show:_.value,"onUpdate:show":o[7]||(o[7]=e=>_.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`sheet`},{default:z(()=>{var e,t,n,r,i,a;return[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((e=X(Re))!=null&&e.blacklist_add),"can-remove":!!((t=X(Re))!=null&&t.blacklist_remove),"add-code":(n=(r=te.value)==null?void 0:r.blacklist_add)==null?``:n,"remove-code":(i=(a=te.value)==null?void 0:a.blacklist_remove)==null?``:i,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):Z(``,!0)]}),_:1},8,[`show`])),(re=X(Re))!=null&&re.control_all?(P(),K(w,{key:1,show:v.value,"onUpdate:show":o[8]||(o[8]=e=>v.value=e)},null,8,[`show`])):Z(``,!0)])}}}),[[`__scopeId`,`data-v-cc7f80e6`]]);export{xn as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css new file mode 100644 index 000000000..464684d56 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css @@ -0,0 +1 @@ +.detail[data-v-70465908]{padding-bottom:16px}.detail-head[data-v-70465908]{align-items:center;gap:12px;padding:16px;display:flex}.avatar[data-v-70465908]{width:52px;height:52px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:14px;flex:none;place-items:center;font-size:22px;font-weight:600;display:grid}.detail-title[data-v-70465908]{flex:1;min-width:0}.detail-title h2[data-v-70465908]{text-overflow:ellipsis;white-space:nowrap;margin:0 0 4px;font-size:18px;overflow:hidden}.detail-title p[data-v-70465908]{margin:0;font-size:13px}.badge[data-v-70465908]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.group[data-v-70465908]{margin-bottom:12px}.state[data-v-70465908]{background:var(--app-surface);border:1px solid var(--app-border);border-radius:14px;margin:0 16px;padding:16px}.state--blocked[data-v-70465908]{border-color:var(--app-danger-soft);background:linear-gradient(0deg, var(--app-danger-soft), var(--app-danger-soft)), var(--app-surface)}.state h3[data-v-70465908]{align-items:center;gap:6px;margin:0 0 6px;font-size:15px;display:flex}.state--blocked h3[data-v-70465908]{color:var(--app-danger)}.state>p[data-v-70465908]{color:var(--app-text-2);margin:0 0 12px;font-size:13px;line-height:1.6}.state dl[data-v-70465908]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin:0 0 14px;display:grid}.state dt[data-v-70465908]{color:var(--app-muted);font-size:12px}.state dd[data-v-70465908]{word-break:break-all;margin:2px 0 0;font-size:14px}.reason[data-v-70465908]{margin:0 0 12px}.reason[data-v-70465908] .van-cell-group--inset{margin:0}.state[data-v-70465908] .van-cell-group--inset{margin:0 0 12px}.note[data-v-70465908]{color:var(--app-muted);margin:0;font-size:13px;line-height:1.6}.row[data-v-645c253b]{border:0;border-bottom:1px solid var(--app-border);background:var(--app-surface);width:100%;min-height:64px;color:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;padding:10px 16px;display:flex}.row[data-v-645c253b]:active,.row.active[data-v-645c253b]{background:var(--app-primary-soft)}.avatar[data-v-645c253b]{border-radius:12px;flex:none;place-items:center;width:42px;height:42px;font-size:17px;font-weight:600;display:grid}.avatar--wecom[data-v-645c253b]{color:#2f6fff;background:#2f6fff1f}.avatar--wechat[data-v-645c253b]{color:#0f9e69;background:#13b77a24}.avatar--group[data-v-645c253b]{color:#7a5cff;background:#8a6bff24}.body[data-v-645c253b]{flex:1;gap:4px;min-width:0;display:grid}.line[data-v-645c253b]{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.name[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.time[data-v-645c253b]{color:var(--app-muted);flex:none;font-size:12px}.sub[data-v-645c253b]{color:var(--app-muted);font-size:12px}.meta[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.blocked[data-v-645c253b]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;padding:1px 8px;font-weight:600}.page[data-v-cc7f80e6]{min-height:100%}.nav-action[data-v-cc7f80e6]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.toolbar[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top));z-index:2;background:var(--app-bg);position:-webkit-sticky;position:sticky}.toolbar[data-v-cc7f80e6] .van-search{background:0 0;padding-bottom:4px}.tabs[data-v-cc7f80e6]{--van-tabs-nav-background:transparent;padding:0 4px}.list-body[data-v-cc7f80e6]{min-height:60vh}.single .list-pane[data-v-cc7f80e6]{max-width:720px;margin:0 auto}.split[data-v-cc7f80e6]{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;max-width:1180px;margin:0 auto;padding:0 24px 24px;display:grid}.split .list-body[data-v-cc7f80e6]{border-radius:16px;overflow:clip}.detail-pane[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top) + 12px);min-height:320px;margin-top:12px;position:-webkit-sticky;position:sticky}.sheet[data-v-cc7f80e6]{max-height:88vh;padding-top:8px} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css new file mode 100644 index 000000000..617ba43f7 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css @@ -0,0 +1 @@ +.panel[data-v-bcd7f731]{padding:16px}.panel-head[data-v-bcd7f731]{align-items:flex-start;gap:12px;display:flex}.panel-icon[data-v-bcd7f731]{width:44px;height:44px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:12px;flex:none;place-items:center;font-size:22px;display:grid}.panel-title[data-v-bcd7f731]{flex:1;min-width:0}.panel-title h2[data-v-bcd7f731]{text-overflow:ellipsis;white-space:nowrap;margin:2px 0 4px;font-size:17px;font-weight:600;overflow:hidden}.panel-title p[data-v-bcd7f731]{margin:0;font-size:13px;line-height:1.45}.pill[data-v-bcd7f731]{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.pill .dot[data-v-bcd7f731]{background:currentColor;border-radius:50%;width:7px;height:7px}.pill--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.pill--success .dot[data-v-bcd7f731]{box-shadow:0 0 0 3px var(--app-success-soft);animation:2.2s ease-in-out infinite breathe-bcd7f731}.pill--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.pill--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.pill--muted[data-v-bcd7f731],.pill--primary[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}@keyframes breathe-bcd7f731{50%{opacity:.45}}.stats[data-v-bcd7f731]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0 0;display:grid}.stat[data-v-bcd7f731]{background:var(--app-surface-2);border:1px solid var(--app-border);border-radius:12px;padding:10px 12px}.stat dt[data-v-bcd7f731]{color:var(--app-muted);font-size:12px}.stat dd[data-v-bcd7f731]{margin:4px 0 0;font-size:15px;font-weight:600}.meta[data-v-bcd7f731]{margin:12px 0 0;font-size:12px}.progress[data-v-bcd7f731]{border-radius:12px;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45;display:flex}.progress--primary[data-v-bcd7f731]{color:var(--app-primary);background:var(--app-primary-soft)}.progress--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.progress--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.progress--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.progress--muted[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}.actions[data-v-bcd7f731]{margin-top:16px}.blocked[data-v-bcd7f731]{margin:10px 4px 0;font-size:13px;line-height:1.5}.page[data-v-8ead1f63]{min-height:100%}.page-body[data-v-8ead1f63]{min-height:calc(100vh - 96px);min-height:calc(100dvh - 96px)}.content[data-v-8ead1f63]{max-width:720px;margin:0 auto;padding:12px 16px 24px}.stack[data-v-8ead1f63]{gap:12px;display:grid}.skeleton-card[data-v-8ead1f63]{padding:16px 0}.empty-hint[data-v-8ead1f63]{max-width:280px;color:var(--app-muted);text-align:center;margin:0 auto;font-size:13px;line-height:1.6}.nav-action[data-v-8ead1f63]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.stale[data-v-8ead1f63]{margin:0 4px;font-size:12px}.split[data-v-8ead1f63]{grid-template-columns:minmax(240px,320px) minmax(0,1fr);align-items:start;gap:16px;max-width:1080px;margin:0 auto;padding:16px 24px 24px;display:grid}.split-list[data-v-8ead1f63]{gap:10px;display:grid}.device-row[data-v-8ead1f63]{text-align:left;width:100%;color:inherit;cursor:pointer;align-items:center;gap:12px;padding:12px 14px;display:flex}.device-row.active[data-v-8ead1f63]{border-color:var(--app-primary);box-shadow:0 0 0 2px var(--app-primary-soft)}.device-row-icon[data-v-8ead1f63]{color:var(--app-primary);font-size:20px}.device-row-text[data-v-8ead1f63]{gap:2px;min-width:0;display:grid}.device-row-text strong[data-v-8ead1f63]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;overflow:hidden}.device-row-text small[data-v-8ead1f63]{font-size:12px} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js new file mode 100644 index 000000000..5da14a77e --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n,o as r,r as i,s as a,t as o}from"./format-DEXJ_MrY.js";import{a as s,o as c}from"./cell-group-B1PakqJq.js";import{a as l,i as u,r as d,t as f}from"./AccountSwitcher-5FuHazzf.js";import{Ct as p,Et as m,Ft as h,Jt as g,Kt as _,Lt as v,St as y,Ut as b,Vt as x,X as S,Y as C,Yt as w,Z as T,_ as E,_t as D,bt as O,c as k,f as A,ft as j,g as M,gt as N,ht as P,kt as F,l as I,n as L,o as R,p as z,q as B,qt as V,s as ee,t as H,v as U,vt as W,x as te,xt as G,yt as K}from"./index-Gkjcdjlo.js";var[ne,re]=E(`skeleton-title`),ie=p({name:ne,props:{round:Boolean,titleWidth:S},setup(e){return()=>y(`h3`,{class:re([{round:e.round}]),style:{width:U(e.titleWidth)}},null)}}),q=M(ie),[J,Y]=E(`skeleton-avatar`),ae={avatarSize:S,avatarShape:C(`round`)},oe=p({name:J,props:ae,setup(e){return()=>y(`div`,{class:Y([e.avatarShape]),style:te(e.avatarSize)},null)}}),se=M(oe),X=`100%`,ce={round:Boolean,rowWidth:{type:S,default:X}},[le,ue]=E(`skeleton-paragraph`),de=p({name:le,props:ce,setup(e){return()=>y(`div`,{class:ue([{round:e.round}]),style:{width:e.rowWidth}},null)}}),fe=M(de),[pe,Z]=E(`skeleton`),me=`60%`,he={row:B(0),round:Boolean,title:Boolean,titleWidth:S,avatar:Boolean,avatarSize:S,avatarShape:C(`round`),loading:T,animate:T,rowWidth:{type:[Number,String,Array],default:X}},ge=p({name:pe,inheritAttrs:!1,props:he,setup(e,{slots:t,attrs:n}){let r=()=>{if(e.avatar)return y(se,{avatarShape:e.avatarShape,avatarSize:e.avatarSize},null)},i=()=>{if(e.title)return y(q,{round:e.round,titleWidth:e.titleWidth},null)},a=t=>{let{rowWidth:n}=e;return n===`100%`&&t===e.row-1?me:Array.isArray(n)?n[t]:n},o=()=>Array(+e.row).fill(``).map((t,n)=>y(fe,{key:n,round:e.round,rowWidth:U(a(n))},null)),s=()=>t.template?t.template():y(j,null,[r(),y(`div`,{class:Z(`content`)},[i(),o()])]);return()=>{var r;return e.loading?y(`div`,m({class:Z({animate:e.animate,round:e.round})},n),[s()]):(r=t.default)==null?void 0:r.call(t)}}}),_e=M(ge),ve={class:`panel-head`},ye={class:`panel-icon`,"aria-hidden":`true`},be={class:`panel-title`},xe={class:`app-muted`},Se={class:`stats`},Ce={class:`stat`},we={class:`stat`},Te={class:`stat`},Ee={class:`stat`},De={class:`meta app-muted`},Oe={key:0},ke={key:1},Ae={class:`actions`},je={key:0,class:`blocked app-muted`},Q=t(p({__name:`DevicePanel`,props:{device:{},accountId:{},canControl:{type:Boolean},controlCode:{}},emits:[`changed`],setup(t,{emit:r}){let u=t,d=r,f=_(!1),p=_(null),m,v=0,S=P(()=>n(u.device)),C=P(()=>u.device.status),T=P(()=>{var e;return!!((e=C.value)!=null&&e.listening)}),E=P(()=>T.value?`stop_listening`:`start_listening`),O=P(()=>p.value?o(p.value):null),A=P(()=>{var e;return f.value||!!((e=O.value)!=null&&e.busy)}),j=P(()=>u.device.online?u.device.remote_capable?u.canControl?``:`你的 ZYT 账号没有远程启停权限(${u.controlCode}),请联系管理员开通。`:`电脑上的客服软件版本过低,升级到最新版后才能远程控制。`:`电脑离线:客服软件没有运行,无法远程拉起。请到电脑上打开软件。`);function M(){m!==void 0&&window.clearTimeout(m),m=void 0}async function L(){let e=p.value;if(e&&!e.final){try{let{command:t}=await I.command(e.id);if(p.value=t,t.final){d(`changed`);let e=o(t).text;l({message:e,type:t.status===`succeeded`?`success`:`fail`});return}}catch(e){if(e instanceof k&&e.status===404)return}Date.now()u.device.last_command,e=>{if(!e)return;if(p.value&&p.value.id===e.id){!p.value.final&&e.final&&(p.value=e);return}let t=e.expires_at*1e3>Date.now();!e.final&&t?R(e):!p.value&&e.final&&t&&(p.value=e)},{immediate:!0}),F(M);async function z(){if(A.value||j.value)return;let e=E.value===`stop_listening`;try{await a({title:e?`停止自动回复?`:`启动自动回复?`,message:e?`「${u.device.device_name||`这台电脑`}」将停止监听,新消息不再自动回复。`:`「${u.device.device_name||`这台电脑`}」将开始监听,并按电脑上的设置自动回复新消息。`,confirmButtonText:e?`停止`:`启动`,confirmButtonColor:e?`var(--app-danger)`:`var(--app-primary)`})}catch{return}f.value=!0;try{let{command:e}=await I.sendCommand(u.device.device_id,E.value,u.accountId);R(e)}catch(e){l({message:e instanceof k?e.message:`指令发送失败,请重试`,type:`fail`}),d(`changed`)}finally{f.value=!1}}return(n,r)=>{var a,o,l,u,d;return h(),K(`article`,{class:g([`panel app-card`,`panel--${S.value.tone}`])},[N(`header`,ve,[N(`div`,ye,[y(V(e),{name:`desktop-o`})]),N(`div`,be,[N(`h2`,null,w(t.device.device_name||`未命名电脑`),1),N(`p`,xe,w(S.value.hint),1)]),N(`span`,{class:g([`pill`,`pill--${S.value.tone}`])},[r[0]||(r[0]=N(`span`,{class:`dot`,"aria-hidden":`true`},null,-1)),G(` `+w(S.value.label),1)],2)]),N(`dl`,Se,[N(`div`,Ce,[r[1]||(r[1]=N(`dt`,null,`自动回复`,-1)),N(`dd`,{class:g(T.value?`tone-success`:`tone-muted`)},w(C.value?T.value?`监听中`:`已停止`:`—`),3)]),N(`div`,we,[r[2]||(r[2]=N(`dt`,null,`企业微信`,-1)),N(`dd`,{class:g((a=C.value)!=null&&a.wecom_connected?`tone-success`:`tone-muted`)},w(C.value?C.value.wecom_connected?`已连接`:`未连接`:`—`),3)]),N(`div`,Te,[r[3]||(r[3]=N(`dt`,null,`回复通道`,-1)),N(`dd`,null,w(((o=C.value)==null?void 0:o.transport_label)||`—`),1)]),N(`div`,Ee,[r[4]||(r[4]=N(`dt`,null,`待处理`,-1)),N(`dd`,{class:g((((l=C.value)==null?void 0:l.pending_count)||0)>0?`tone-warning`:``)},w(C.value?`${C.value.pending_count} 条`:`—`),3)])]),N(`p`,De,[N(`span`,null,`版本 `+w(t.device.app_version||`未知`),1),t.device.online&&((u=C.value)==null?void 0:u.reported_seconds)!==null&&((d=C.value)==null?void 0:d.reported_seconds)!==void 0?(h(),K(`span`,Oe,` · 状态 `+w(V(i)(C.value.reported_seconds))+`前上报 `,1)):!t.device.online&&t.device.last_seen_seconds!==null?(h(),K(`span`,ke,` · `+w(V(i)(t.device.last_seen_seconds))+`前在线 `,1)):W(``,!0)]),O.value?(h(),K(`div`,{key:0,class:g([`progress`,`progress--${O.value.tone}`]),role:`status`,"aria-live":`polite`},[O.value.busy?(h(),D(V(c),{key:0,size:`16`})):(h(),D(V(e),{key:1,name:O.value.tone===`success`?`passed`:`info-o`},null,8,[`name`])),N(`span`,null,w(O.value.text),1)],2)):W(``,!0),N(`div`,Ae,[y(V(s),{block:``,round:``,type:T.value?`danger`:`primary`,plain:T.value,icon:T.value?`pause-circle-o`:`play-circle-o`,disabled:!!j.value,loading:A.value,"loading-text":f.value?`正在下发…`:`等待电脑执行…`,onClick:z},{default:b(()=>[G(w(T.value?`停止自动回复`:`启动自动回复`),1)]),_:1},8,[`type`,`plain`,`icon`,`disabled`,`loading`,`loading-text`]),j.value?(h(),K(`p`,je,w(j.value),1)):W(``,!0)])],2)}}}),[[`__scopeId`,`data-v-bcd7f731`]]),Me={class:`page`},Ne={key:0,class:`content`},Pe={class:`app-card skeleton-card`},Fe={key:1,class:`content`},Ie={key:2,class:`content`},Le={class:`empty-hint`},$={key:3,class:`split`},Re={class:`split-list`,"aria-label":`电脑列表`},ze=[`onClick`],Be={class:`device-row-text`},Ve={key:0,class:`stale app-muted`},He={class:`split-detail`},Ue={key:4,class:`content stack`},We={key:0,class:`stale app-muted`},Ge=t(p({name:`DevicesView`,__name:`DevicesView`,setup(t){let i=z(),a=_(null),o=_(!1),c=_(!1),l=_(``),p=_(``),m=_(!1),S=0,C=P(()=>{var e,t;return(e=(t=a.value)==null?void 0:t.items)==null?[]:e}),T=P(()=>{var e,t;return(e=(t=C.value.find(e=>e.device_id===p.value))==null?C.value[0]:t)==null?null:e}),E=P(()=>{var e;return!!((e=H.value)!=null&&e.control)}),M=P(()=>{var e,t;return(e=(t=L.me)==null?void 0:t.permission_codes.control)==null?`wecom_rpa.remote/control`:e}),F=P(()=>R.value===void 0?`我的电脑`:`${ee.value} 的电脑`);async function B(){let e=++S;a.value||(o.value=!0);try{let t=await I.desktops(R.value);if(e!==S)return;a.value=t,l.value=``}catch(t){if(e!==S)return;l.value=t instanceof k?t.message:`加载失败,请重试`}finally{e===S&&(o.value=!1,c.value=!1)}}async function U(){c.value=!0,await B()}return x(R,()=>{a.value=null,p.value=``,B()},{immediate:!0}),A(B,5e3),(t,_)=>{var x,S;return h(),K(`div`,Me,[y(V(r),{title:F.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},O({_:2},[(x=V(H))!=null&&x.control_all?{name:`right`,fn:b(()=>[N(`button`,{type:`button`,class:`nav-action`,onClick:_[0]||(_[0]=e=>m.value=!0)},[y(V(e),{name:`friends-o`}),_[3]||(_[3]=G(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),y(V(d),{modelValue:c.value,"onUpdate:modelValue":_[1]||(_[1]=e=>c.value=e),class:`page-body`,onRefresh:U},{default:b(()=>[o.value&&!a.value?(h(),K(`div`,Ne,[N(`div`,Pe,[y(V(_e),{title:``,row:4})])])):l.value&&!a.value?(h(),K(`div`,Fe,[y(V(u),{image:`network`,description:l.value},{default:b(()=>[y(V(s),{round:``,type:`primary`,size:`small`,onClick:B},{default:b(()=>[..._[4]||(_[4]=[G(`重新加载`,-1)])]),_:1})]),_:1},8,[`description`])])):C.value.length?V(i)?(h(),K(`div`,$,[N(`aside`,Re,[(h(!0),K(j,null,v(C.value,t=>{var r;return h(),K(`button`,{key:t.device_id,type:`button`,class:g([`device-row app-card`,{active:((r=T.value)==null?void 0:r.device_id)===t.device_id}]),onClick:e=>p.value=t.device_id},[y(V(e),{name:`desktop-o`,class:`device-row-icon`}),N(`span`,Be,[N(`strong`,null,w(t.device_name||`未命名电脑`),1),N(`small`,{class:g(`tone-${V(n)(t).tone}`)},w(V(n)(t).label),3)])],10,ze)}),128)),l.value?(h(),K(`p`,Ve,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0)]),N(`section`,He,[T.value?(h(),D(Q,{key:T.value.device_id,device:T.value,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`])):W(``,!0)])])):(h(),K(`div`,Ue,[l.value?(h(),K(`p`,We,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0),(h(!0),K(j,null,v(C.value,e=>(h(),D(Q,{key:e.device_id,device:e,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`]))),128))])):(h(),K(`div`,Ie,[y(V(u),{image:`default`,description:`还没有登录过企微客服助手的电脑`},{default:b(()=>[N(`p`,Le,` 在电脑上打开企微客服助手,用`+w(V(R)===void 0?`同一个`:`该客服的`)+` ZYT 账号登录后,这里会显示电脑状态。 `,1)]),_:1})]))]),_:1},8,[`modelValue`]),(S=V(H))!=null&&S.control_all?(h(),D(f,{key:0,show:m.value,"onUpdate:show":_[2]||(_[2]=e=>m.value=e)},null,8,[`show`])):W(``,!0)])}}}),[[`__scopeId`,`data-v-8ead1f63`]]);export{Ge as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css new file mode 100644 index 000000000..8942d6062 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css @@ -0,0 +1 @@ +.login[data-v-858fb177]{min-height:100vh;min-height:100dvh;padding:calc(env(safe-area-inset-top) + 24px) 16px calc(env(safe-area-inset-bottom) + 24px);background:radial-gradient(120% 60% at 50% 0%, var(--app-primary-soft), transparent 70%), var(--app-bg);justify-content:center;align-items:center;display:flex}.login-card[data-v-858fb177]{width:100%;max-width:420px}.login-head[data-v-858fb177]{text-align:center;margin-bottom:24px}.login-mark[data-v-858fb177]{box-shadow:var(--app-shadow);border-radius:20px}.login-head h1[data-v-858fb177]{letter-spacing:.5px;margin:16px 0 6px;font-size:24px;font-weight:700}.login-head p[data-v-858fb177]{color:var(--app-muted);margin:0;font-size:14px}.login-notice[data-v-858fb177]{border-radius:12px;margin:0 16px 12px}.login-notice.warn[data-v-858fb177]{--van-notice-bar-text-color:var(--app-warning);--van-notice-bar-background:var(--app-warning-soft)}.login-form[data-v-858fb177] .van-cell-group--inset,.login-form[data-v-858fb177]{margin:0}.login-error[data-v-858fb177]{color:var(--app-danger);margin:12px 16px 0;font-size:14px;line-height:1.5}.login-actions[data-v-858fb177]{margin:20px 16px 0}.login-foot[data-v-858fb177]{text-align:center;color:var(--app-muted);margin:16px 16px 0;font-size:12px} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js new file mode 100644 index 000000000..c65cd3fb4 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js @@ -0,0 +1 @@ +import{c as e,m as t,n,t as r}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{a as i,s as a,t as o}from"./cell-group-B1PakqJq.js";import{t as s}from"./field-CRX3eS7h.js";import{B as c,Ct as l,Ft as u,Gt as d,I as f,Kt as p,O as m,R as h,St as g,Ut as _,V as v,Vt as y,W as b,Wt as x,X as S,Yt as C,Z as w,_ as T,_t as E,c as D,d as O,et as k,g as A,gt as j,ht as M,l as N,q as P,qt as F,r as I,u as L,ut as R,vt as z,xt as B,yt as V,z as H}from"./index-Gkjcdjlo.js";var[U,W]=T(`form`),G=l({name:U,props:{colon:Boolean,disabled:Boolean,readonly:Boolean,required:[Boolean,String],showError:Boolean,labelWidth:S,labelAlign:String,inputAlign:String,scrollToError:Boolean,scrollToErrorPosition:String,validateFirst:Boolean,submitOnEnter:w,showErrorMessage:w,errorMessageAlign:String,validateTrigger:{type:[String,Array],default:`onBlur`}},emits:[`submit`,`failed`],setup(e,{emit:n,slots:r}){let{children:i,linkChildren:o}=c(t),s=e=>e?i.filter(t=>e.includes(t.name)):i,l=e=>new Promise((t,n)=>{let r=[];s(e).reduce((e,t)=>e.then(()=>{if(!r.length)return t.validate().then(e=>{e&&r.push(e)})}),Promise.resolve()).then(()=>{r.length?n(r):t()})}),u=e=>new Promise((t,n)=>{let r=s(e);Promise.all(r.map(e=>e.validate())).then(e=>{e=e.filter(Boolean),e.length?n(e):t()})}),d=e=>{let t=i.find(t=>t.name===e);return t?new Promise((e,n)=>{t.validate().then(t=>{t?n(t):e()})}):Promise.reject()},f=t=>typeof t==`string`?d(t):e.validateFirst?l(t):u(t),p=e=>{typeof e==`string`&&(e=[e]),s(e).forEach(e=>{e.resetValidation()})},h=()=>i.reduce((e,t)=>(e[t.name]=t.getValidationStatus(),e),{}),_=(e,t)=>{i.some(n=>n.name===e&&(n.$el.scrollIntoView(t),!0))},v=()=>i.reduce((e,t)=>(t.name!==void 0&&(e[t.name]=t.formValue.value),e),{}),y=()=>{let t=v();f().then(()=>n(`submit`,t)).catch(r=>{n(`failed`,{values:t,errors:r});let{scrollToError:i,scrollToErrorPosition:a}=e;i&&r[0].name&&_(r[0].name,a?{block:a}:void 0)})},b=e=>{m(e),y()};return o({props:e}),a({submit:y,validate:f,getValues:v,scrollToField:_,resetValidation:p,getValidationStatus:h}),()=>{var e;return g(`form`,{class:W(),onSubmit:b},[(e=r.default)==null?void 0:e.call(r)])}}}),K=A(G),[q,J]=T(`notice-bar`),Y={text:String,mode:String,color:String,delay:P(1),speed:P(60),leftIcon:String,wrapable:Boolean,background:String,scrollable:{type:Boolean,default:null}},X=l({name:q,props:Y,emits:[`close`,`replay`],setup(t,{emit:r,slots:i}){let o=0,s=0,c,l=p(),u=p(),m=d({show:!0,offset:0,duration:0}),_=()=>{if(i[`left-icon`])return i[`left-icon`]();if(t.leftIcon)return g(n,{class:J(`left-icon`),name:t.leftIcon},null)},S=()=>{if(t.mode===`closeable`)return`cross`;if(t.mode===`link`)return`arrow`},C=e=>{t.mode===`closeable`&&(m.show=!1,r(`close`,e))},w=()=>{if(i[`right-icon`])return i[`right-icon`]();let e=S();if(e)return g(n,{name:e,class:J(`right-icon`),onClick:C},null)},T=()=>{m.offset=o,m.duration=0,H(()=>{f(()=>{m.offset=-s,m.duration=(s+o)/+t.speed,r(`replay`)})})},E=()=>{let e=t.scrollable===!1&&!t.wrapable,n={transform:m.offset?`translateX(${m.offset}px)`:``,transitionDuration:`${m.duration}s`};return g(`div`,{ref:l,role:`marquee`,class:J(`wrap`)},[g(`div`,{ref:u,style:n,class:[J(`content`),{"van-ellipsis":e}],onTransitionend:T},[i.default?i.default():t.text])])},D=()=>{let{delay:e,speed:n,scrollable:r}=t,i=k(e)?e*1e3:0;o=0,s=0,m.offset=0,m.duration=0,clearTimeout(c),c=setTimeout(()=>{if(!l.value||!u.value||r===!1)return;let e=b(l).width,t=b(u).width;(r||t>e)&&f(()=>{o=e,s=t,m.offset=-s,m.duration=s/+n})},i)};return e(D),h(D),v(`pageshow`,D),a({reset:D}),y(()=>[t.text,t.scrollable],D),()=>{let{color:e,wrapable:n,background:r}=t;return x(g(`div`,{role:`alert`,class:J({wrapable:n}),style:{color:e,background:r}},[_(),E(),w()]),[[R,m.show]])}}}),Z=A(X),Q=`/m/assets/brand-mark-CPUQS9Am.png`,$={class:`login`},ee={class:`login-card`},te={class:`login-head`},ne=[`src`],re={key:0,class:`login-error`,role:`alert`},ie={class:`login-actions`},ae=r(l({__name:`LoginView`,setup(e){let t=L(),n=O(),r=p(``),a=p(``),c=p(!1),l=p(``),d=M(()=>t.query.expired===`1`),f=M(()=>window.location.protocol===`http:`&&![`localhost`,`127.0.0.1`,`[::1]`].includes(window.location.hostname));function m(){let e=navigator.userAgent,t=/iPad/.test(e)?`iPad`:/iPhone/.test(e)?`iPhone`:/HarmonyOS/i.test(e)?`鸿蒙`:/Android/.test(e)?`Android`:/Windows/.test(e)?`Windows`:/Mac OS/.test(e)?`Mac`:`浏览器`,n=/wxwork/i.test(e)?`企业微信`:/MicroMessenger/i.test(e)?`微信`:``;return n?`${t} · ${n}`:t}function h(){let e=t.query.redirect;return typeof e==`string`&&e.startsWith(`/`)&&!e.startsWith(`//`)&&!e.startsWith(`/login`)?e:`/desktops`}async function v(){if(!c.value){l.value=``,c.value=!0;try{let e=await N.login(r.value.trim(),a.value,m());a.value=``,I(e.access_token,e.me),await n.replace(h())}catch(e){e instanceof D?(l.value=e.message,e.code===`invalid_credentials`&&(a.value=``)):l.value=`登录失败,请稍后重试`}finally{c.value=!1}}}return(e,t)=>(u(),V(`main`,$,[j(`section`,ee,[j(`header`,te,[j(`img`,{class:`login-mark`,src:F(Q),alt:`甄养堂`,width:`72`,height:`72`},null,8,ne),t[2]||(t[2]=j(`h1`,null,`企微客服助手`,-1)),t[3]||(t[3]=j(`p`,null,`手机控制台 · 使用 ZYT 账号登录`,-1))]),d.value?(u(),E(F(Z),{key:0,class:`login-notice`,"left-icon":`info-o`,scrollable:!1,wrapable:``,text:`登录已失效或已在管理端被下线,请重新登录。`})):z(``,!0),f.value?(u(),E(F(Z),{key:1,class:`login-notice warn`,"left-icon":`warning-o`,scrollable:!1,wrapable:``,text:`当前是明文 HTTP 连接,只能在公司内网登录;在外网请改用 HTTPS 地址。`})):z(``,!0),g(F(K),{class:`login-form`,onSubmit:v},{default:_(()=>[g(F(o),{inset:``},{default:_(()=>[g(F(s),{modelValue:r.value,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),name:`account`,label:`账号`,placeholder:`ZYT 登录账号`,autocomplete:`username`,autocapitalize:`off`,spellcheck:!1,enterkeyhint:`next`,clearable:``,rules:[{required:!0,message:`请输入账号`}]},null,8,[`modelValue`]),g(F(s),{modelValue:a.value,"onUpdate:modelValue":t[1]||(t[1]=e=>a.value=e),type:`password`,name:`password`,label:`密码`,placeholder:`ZYT 登录密码`,autocomplete:`current-password`,enterkeyhint:`go`,rules:[{required:!0,message:`请输入密码`}]},null,8,[`modelValue`])]),_:1}),l.value?(u(),V(`p`,re,C(l.value),1)):z(``,!0),j(`div`,ie,[g(F(i),{round:``,block:``,type:`primary`,"native-type":`submit`,loading:c.value,"loading-text":`正在登录…`},{default:_(()=>[...t[4]||(t[4]=[B(` 登录 `,-1)])]),_:1},8,[`loading`])])]),_:1}),t[5]||(t[5]=j(`p`,{class:`login-foot`},`密码只用于向 ZYT 验证身份,不会保存在手机上。`,-1))])]))}}),[[`__scopeId`,`data-v-858fb177`]]);export{ae as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css new file mode 100644 index 000000000..5045f6a8e --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css @@ -0,0 +1 @@ +.content[data-v-1710faf3]{max-width:720px;margin:0 auto;padding:12px 0 32px}.profile[data-v-1710faf3]{align-items:center;gap:14px;margin:0 16px;padding:16px;display:flex}.avatar[data-v-1710faf3]{color:#fff;background:linear-gradient(135deg,#2f6fff,#5a65ff);border-radius:50%;flex:none;place-items:center;width:54px;height:54px;font-size:22px;font-weight:700;display:grid}.profile-text[data-v-1710faf3]{flex:1;min-width:0}.profile-text h2[data-v-1710faf3]{margin:0 0 4px;font-size:18px}.profile-text p[data-v-1710faf3]{margin:0;font-size:13px}.root[data-v-1710faf3]{color:var(--app-warning);background:var(--app-warning-soft);border-radius:999px;flex:none;padding:4px 10px;font-size:12px;font-weight:600}.section-title[data-v-1710faf3]{color:var(--app-muted);margin:24px 32px 8px;font-size:13px;font-weight:600}.hint[data-v-1710faf3]{margin:8px 32px 0;font-size:12px;line-height:1.6}.logout[data-v-1710faf3]{margin:32px 16px 0} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js new file mode 100644 index 000000000..175896125 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{a as n,i as r,o as i,s as a}from"./format-DEXJ_MrY.js";import{a as o,n as s,t as c}from"./cell-group-B1PakqJq.js";import{Ct as l,Ft as u,Jt as d,Kt as f,Lt as p,St as m,Ut as h,Yt as g,_t as _,d as v,ft as y,gt as b,ht as x,i as S,n as C,qt as w,vt as T,xt as E,yt as D}from"./index-Gkjcdjlo.js";var O={class:`page`},k={key:0,class:`content`},A={class:`profile app-card`},j={class:`avatar`,"aria-hidden":`true`},M={class:`profile-text`},N={class:`app-muted`},P={key:0,class:`root`},F={class:`hint app-muted`},I={class:`logout`},L=t(l({__name:`MeView`,setup(t){let l=v(),L=f(!1),R=x(()=>C.me),z=x(()=>{let e=R.value;if(!e)return[];let{permissions:t,permission_codes:n}=e;return[{title:`查看本人电脑与会话`,code:`登录即可`,granted:!0},{title:`远程启停本人电脑`,code:n.control,granted:t.control},{title:`查看并控制所有客服电脑`,code:n.control_all,granted:t.control_all},{title:`会话加入黑名单`,code:n.blacklist_add,granted:t.blacklist_add},{title:`会话移出黑名单`,code:n.blacklist_remove,granted:t.blacklist_remove}]}),B=x(()=>{var e;let t=(e=R.value)==null?void 0:e.session.expires_at;return t?new Date(t*1e3).toISOString():``}),V=/iPhone|iPad|iPod/.test(navigator.userAgent);async function H(){try{await a({title:`退出登录?`,message:`退出后需要重新输入 ZYT 账号密码。`,confirmButtonText:`退出`})}catch{return}L.value=!0,await S(),await l.replace({name:`login`})}return(t,a)=>(u(),D(`div`,O,[m(w(i),{title:`我的`,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1}),R.value?(u(),D(`div`,k,[b(`section`,A,[b(`span`,j,g(w(r)(R.value.account.nickname||R.value.account.sn)),1),b(`div`,M,[b(`h2`,null,g(R.value.account.nickname||R.value.account.sn),1),b(`p`,N,`ZYT 账号 `+g(R.value.account.sn||`—`),1)]),R.value.permissions.root?(u(),D(`span`,P,`超级管理员`)):T(``,!0)]),a[1]||(a[1]=b(`h3`,{class:`section-title`},`手机端权限`,-1)),m(w(c),{inset:``},{default:h(()=>[(u(!0),D(y,null,p(z.value,t=>(u(),_(w(s),{key:t.title,title:t.title,label:t.code,center:``},{value:h(()=>[b(`span`,{class:d(t.granted?`tone-success`:`tone-muted`)},[m(w(e),{name:t.granted?`passed`:`close`},null,8,[`name`]),E(` `+g(t.granted?`已开通`:`未开通`),1)],2)]),_:2},1032,[`title`,`label`]))),128))]),_:1}),a[2]||(a[2]=b(`p`,{class:`hint app-muted`},` 权限在 ZYT「权限管理 → 角色管理」里勾选「企微客服助手」下的项目,调整后 10 分钟内自动生效,或退出重新登录。 `,-1)),a[3]||(a[3]=b(`h3`,{class:`section-title`},`登录信息`,-1)),m(w(c),{inset:``},{default:h(()=>[m(w(s),{title:`登录时间`,value:w(n)(R.value.session.created_at)},null,8,[`value`]),m(w(s),{title:`最长有效至`,value:B.value?w(n)(B.value):`—`,label:`ZYT 登录失效后会提前退出`},null,8,[`value`])]),_:1}),a[4]||(a[4]=b(`h3`,{class:`section-title`},`添加到主屏幕`,-1)),b(`p`,F,[w(V)?(u(),D(y,{key:0},[E(`在 Safari 中点底部「分享」按钮,选择「添加到主屏幕」,之后像 App 一样打开。`)],64)):(u(),D(y,{key:1},[E(`在浏览器菜单中选择「添加到主屏幕」或「安装应用」,之后像 App 一样打开。`)],64))]),b(`div`,I,[m(w(o),{block:``,round:``,type:`danger`,plain:``,loading:L.value,onClick:H},{default:h(()=>[...a[0]||(a[0]=[E(`退出登录`,-1)])]),_:1},8,[`loading`])])])):T(``,!0)]))}}),[[`__scopeId`,`data-v-1710faf3`]]);export{L as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js new file mode 100644 index 000000000..fd8f95b99 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js @@ -0,0 +1 @@ +import{a as e,n as t,o as n,p as r,r as i,t as a}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n as o,t as s}from"./use-placeholder-CcVnlsb4.js";import{B as c,Ct as l,Et as u,Ft as d,Kt as f,Q as p,Rt as m,S as h,St as g,U as _,Ut as v,X as y,Z as b,_ as x,_t as S,g as C,ht as w,pt as T,q as E,qt as D,rt as O,wt as k,xt as A,yt as j,zt as M}from"./index-Gkjcdjlo.js";var[N,P]=x(`tabbar`),F={route:Boolean,fixed:b,border:b,zIndex:y,placeholder:Boolean,activeColor:String,beforeChange:Function,inactiveColor:String,modelValue:E(0),safeAreaInsetBottom:{type:Boolean,default:null}},I=Symbol(N),L=l({name:N,props:F,emits:[`change`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i=f(),{linkChildren:a}=c(I),l=s(i,P),u=()=>{var t;return(t=e.safeAreaInsetBottom)==null?e.fixed:t},d=()=>{var t;let{fixed:a,zIndex:o,border:s}=e;return g(`div`,{ref:i,role:`tablist`,style:h(o),class:[P({fixed:a}),{[r]:s,"van-safe-area-bottom":u()}]},[(t=n.default)==null?void 0:t.call(n)])};return a({props:e,setActive:(n,r)=>{o(e.beforeChange,{args:[n],done(){t(`update:modelValue`,n),t(`change`,n),r()}})}}),()=>e.fixed&&e.placeholder?l(d):d()}}),R=C(L),[z,B]=x(`tabbar-item`),V=p({},e,{dot:Boolean,icon:String,name:y,badge:y,badgeProps:Object,iconPrefix:String}),H=l({name:z,props:V,emits:[`click`],setup(e,{emit:r,slots:a}){let o=n(),s=k().proxy,{parent:c,index:l}=_(I);if(!c)return;let d=w(()=>{var t;let{route:n,modelValue:r}=c.props;if(n&&`$route`in s){let{$route:t}=s,{to:n}=e,r=O(n)?n:{path:n};return t.matched.some(e=>{let t=`path`in r&&r.path===e.path,n=`name`in r&&r.name===e.name;return t||n})}return((t=e.name)==null?l.value:t)===r}),f=t=>{var n;d.value||c.setActive((n=e.name)==null?l.value:n,o),r(`click`,t)},p=()=>{if(a.icon)return a.icon({active:d.value});if(e.icon)return g(t,{name:e.icon,classPrefix:e.iconPrefix},null)};return()=>{var t;let{dot:n,badge:r}=e,{activeColor:o,inactiveColor:s}=c.props,l=d.value?o:s;return g(`div`,{role:`tab`,class:B({active:d.value}),style:{color:l},tabindex:0,"aria-selected":d.value,onClick:f},[g(i,u({dot:n,class:B(`icon`),content:r},e.badgeProps),{default:p}),g(`div`,{class:B(`text`)},[(t=a.default)==null?void 0:t.call(a,{active:d.value})])])}}}),U=C(H),W={class:`shell`},G=a(l({__name:`ShellView`,setup(e){return(e,t)=>{let n=m(`RouterView`);return d(),j(`div`,W,[g(n,null,{default:v(({Component:e})=>[(d(),S(T,{include:[`DevicesView`,`ConversationsView`]},[(d(),S(M(e)))],1024))]),_:1}),g(D(R),{route:``,placeholder:``,"safe-area-inset-bottom":``,class:`shell-tabbar`},{default:v(()=>[g(D(U),{replace:``,to:{name:`desktops`},icon:`desktop-o`},{default:v(()=>[...t[0]||(t[0]=[A(`电脑`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`conversations`},icon:`chat-o`},{default:v(()=>[...t[1]||(t[1]=[A(`会话`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`me`},icon:`user-o`},{default:v(()=>[...t[2]||(t[2]=[A(`我的`,-1)])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-5972669c`]]);export{G as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css new file mode 100644 index 000000000..a8ec81749 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css @@ -0,0 +1 @@ +.shell[data-v-5972669c]{min-height:100vh;min-height:100dvh}.shell-tabbar[data-v-5972669c]{border-top:1px solid var(--app-border)} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js new file mode 100644 index 000000000..a9be2cd6a --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js @@ -0,0 +1 @@ +import{Ct as e,Et as t,St as n,Tt as r,Vt as i,X as a,Y as o,Z as s,_ as c,et as l,g as u,ht as d,m as f,nt as p,v as m,wt as h}from"./index-Gkjcdjlo.js";var g=`van-hairline`,_=`${g}--top`,v=`${g}--left`;`${g}`;var y=`${g}--bottom`,b=`${g}--surround`,x=`${g}--top-bottom`;`${g}`;var S=`van-haptics-feedback`,C=Symbol(`van-form`),w=Symbol();function T(e){let t=r(w,null);t&&i(t,t=>{t&&e()})}var E={to:[String,Object],url:String,replace:Boolean};function D({to:e,url:t,replace:n,$router:r}){e&&r?r[n?`replace`:`push`](e):t&&(n?location.replace(t):location.href=t)}function O(){let e=h().proxy;return()=>D(e)}var[k,A]=c(`badge`),j={dot:Boolean,max:a,tag:o(`div`),color:String,offset:Array,content:a,showZero:s,position:o(`top-right`)},M=e({name:k,props:j,setup(e,{slots:t}){let r=()=>{if(t.content)return!0;let{content:n,showZero:r}=e;return l(n)&&n!==``&&(r||n!==0&&n!==`0`)},i=()=>{let{dot:n,max:i,content:a}=e;if(!n&&r())return t.content?t.content():l(i)&&p(a)&&+a>+i?`${i}+`:a},a=e=>e.startsWith(`-`)?e.replace(`-`,``):`-${e}`,o=d(()=>{let n={background:e.color};if(e.offset){let[r,i]=e.offset,{position:o}=e,[s,c]=o.split(`-`);t.default?(n[s]=typeof i==`number`?m(s===`top`?i:-i):s===`top`?m(i):a(i),n[c]=typeof r==`number`?m(c===`left`?r:-r):c===`left`?m(r):a(r)):(n.marginTop=m(i),n.marginLeft=m(r))}return n}),s=()=>{if(r()||e.dot)return n(`div`,{class:A([e.position,{dot:e.dot,fixed:!!t.default}]),style:o.value},[i()])};return()=>{if(t.default){let{tag:r}=e;return n(r,{class:A(`wrapper`)},{default:()=>[t.default(),s()]})}return s()}}}),N=u(M),[P,F]=c(`icon`),I=e=>e==null?void 0:e.includes(`/`),L={dot:Boolean,tag:o(`i`),name:String,size:a,badge:a,color:String,badgeProps:Object,classPrefix:String},R=e({name:P,props:L,setup(e,{slots:i}){let a=r(f,null),o=d(()=>e.classPrefix||(a==null?void 0:a.iconPrefix)||F());return()=>{let{tag:r,dot:a,name:s,size:c,badge:l,color:u}=e,d=I(s);return n(N,t({dot:a,tag:r,class:[o.value,d?``:`${o.value}-${s}`],style:{color:u,fontSize:m(c)},content:l},e.badgeProps),{default:()=>{var e;return[(e=i.default)==null?void 0:e.call(i),d&&n(`img`,{class:F(`image`),src:s},null)]}})}}}),z=u(R),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{E as a,T as c,b as d,_ as f,S as h,D as i,y as l,C as m,z as n,O as o,x as p,N as r,w as s,B as t,v as u}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js new file mode 100644 index 000000000..6bd64f78f --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js @@ -0,0 +1 @@ +import{a as e,d as t,n,o as r,p as i}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{Ct as a,Et as o,O as s,Q as c,St as l,X as u,Y as d,Z as f,_ as p,et as m,ft as h,g,ht as _,v,wt as y,x as b}from"./index-Gkjcdjlo.js";function x(e){let t=y();t&&c(t.proxy,e)}var[S,C]=p(`loading`),w=Array(12).fill(null).map((e,t)=>l(`i`,{class:C(`line`,String(t+1))},null)),T=l(`svg`,{class:C(`circular`),viewBox:`25 25 50 50`},[l(`circle`,{cx:`50`,cy:`50`,r:`20`,fill:`none`},null)]),E={size:u,type:d(`circular`),color:String,vertical:Boolean,textSize:u,textColor:String},D=a({name:S,props:E,setup(e,{slots:t}){let n=_(()=>c({color:e.color},b(e.size))),r=()=>{let r=e.type===`spinner`?w:T;return l(`span`,{class:C(`spinner`,e.type),style:n.value},[t.icon?t.icon():r])},i=()=>{var n;if(t.default)return l(`span`,{class:C(`text`),style:{fontSize:v(e.textSize),color:(n=e.textColor)==null?e.color:n}},[t.default()])};return()=>{let{type:t,vertical:n}=e;return l(`div`,{class:C([t,{vertical:n}]),"aria-live":`polite`,"aria-busy":!0},[r(),i()])}}}),O=g(D),[k,A]=p(`button`),j=c({},e,{tag:d(`button`),text:String,icon:String,type:d(`default`),size:d(`normal`),color:String,block:Boolean,plain:Boolean,round:Boolean,square:Boolean,loading:Boolean,hairline:Boolean,disabled:Boolean,iconPrefix:String,nativeType:d(`button`),loadingSize:u,loadingText:String,loadingType:String,iconPosition:d(`left`)}),M=a({name:k,props:j,emits:[`click`],setup(e,{emit:i,slots:a}){let o=r(),c=()=>a.loading?a.loading():l(O,{size:e.loadingSize,type:e.loadingType,class:A(`loading`)},null),u=()=>{if(e.loading)return c();if(a.icon)return l(`div`,{class:A(`icon`)},[a.icon()]);if(e.icon)return l(n,{name:e.icon,class:A(`icon`),classPrefix:e.iconPrefix},null)},d=()=>{let t;if(t=e.loading?e.loadingText:a.default?a.default():e.text,t)return l(`span`,{class:A(`text`)},[t])},f=()=>{let{color:t,plain:n}=e;if(t){let e={color:n?t:`white`};return n||(e.background=t),t.includes(`gradient`)?e.border=0:e.borderColor=t,e}},p=t=>{e.loading?s(t):e.disabled||(i(`click`,t),o())};return()=>{let{tag:n,type:r,size:i,block:a,round:o,plain:s,square:c,loading:m,disabled:h,hairline:g,nativeType:_,iconPosition:v}=e,y=[A([r,i,{plain:s,block:a,round:o,square:c,loading:m,disabled:h,hairline:g}]),{[t]:g}];return l(n,{type:_,class:y,style:f(),disabled:h,onClick:p},{default:()=>[l(`div`,{class:A(`content`)},[v===`left`&&u(),d(),v===`right`&&u()])]})}}}),N=g(M),P=()=>{var e;let{scopeId:t}=((e=y())==null?void 0:e.vnode)||{};return t?{[t]:``}:null},[F,I]=p(`cell`),L={tag:d(`div`),icon:String,size:String,title:u,value:u,label:u,center:Boolean,isLink:Boolean,border:f,iconPrefix:String,valueClass:null,labelClass:null,titleClass:null,titleStyle:null,arrowDirection:String,required:{type:[Boolean,String],default:null},clickable:{type:Boolean,default:null}},R=c({},L,e),z=a({name:F,props:R,setup(e,{slots:t}){let i=r(),a=()=>{if(t.label||m(e.label))return l(`div`,{class:[I(`label`),e.labelClass]},[t.label?t.label():e.label])},o=()=>{var n;if(t.title||m(e.title)){let r=(n=t.title)==null?void 0:n.call(t);return Array.isArray(r)&&r.length===0?void 0:l(`div`,{class:[I(`title`),e.titleClass],style:e.titleStyle},[r||l(`span`,null,[e.title]),a()])}},s=()=>{let n=t.value||t.default;if(n||m(e.value))return l(`div`,{class:[I(`value`),e.valueClass]},[n?n():l(`span`,null,[e.value])])},c=()=>{if(t.icon)return t.icon();if(e.icon)return l(n,{name:e.icon,class:I(`left-icon`),classPrefix:e.iconPrefix},null)},u=()=>{if(t[`right-icon`])return t[`right-icon`]();if(e.isLink){let t=e.arrowDirection&&e.arrowDirection!==`right`?`arrow-${e.arrowDirection}`:`arrow`;return l(n,{name:t,class:I(`right-icon`)},null)}};return()=>{var n,r,a;let{tag:d,size:f,center:p,border:m,isLink:h,required:g}=e,_=!!((a=(r=(n=e.clickable)==null?e.to:n)==null?e.url:r)==null?h:a),v={center:p,required:!!g,clickable:_,borderless:!m};return f&&(v[f]=!!f),l(d,{class:I(v),role:_?`button`:void 0,tabindex:_?0:void 0,onClick:_?i:void 0},{default:()=>{var e;return[c(),o(),s(),u(),(e=t.extra)==null?void 0:e.call(t)]}})}}}),B=g(z),[V,H]=p(`cell-group`),U=a({name:V,inheritAttrs:!1,props:{title:String,inset:Boolean,border:f},setup(e,{slots:t,attrs:n}){let r=()=>{var r;return l(`div`,o({class:[H({inset:e.inset}),{[i]:e.border&&!e.inset}]},n,P()),[(r=t.default)==null?void 0:r.call(t)])},a=()=>l(`div`,{class:H(`title`,{inset:e.inset})},[t.title?t.title():e.title]);return()=>e.title||t.title?l(h,null,[a(),r()]):r()}}),W=g(U);export{N as a,P as i,B as n,O as o,L as r,x as s,W as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js new file mode 100644 index 000000000..df761d80e --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js @@ -0,0 +1 @@ +import{m as e,n as t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n,r,s as i}from"./cell-group-B1PakqJq.js";import{A as a,Ct as o,Dt as s,Et as c,Gt as ee,It as l,Kt as u,Mt as d,O as f,P as te,Q as p,St as m,T as h,U as g,V as _,Vt as v,X as y,Y as b,_ as x,b as S,et as C,g as w,ht as T,it as E,k as ne,q as D,rt as O,st as k,tt as A,v as j,wt as M,xt as re,y as ie}from"./index-Gkjcdjlo.js";var N=0;function P(){let e=M(),{name:t=`unknown`}=(e==null?void 0:e.type)||{};return`${t}-${++N}`}function F(e){return Array.isArray(e)?!e.length:e!==0&&!e}function ae(e,t){if(F(e)){if(t.required)return!1;if(t.validateEmpty===!1)return!0}return!(t.pattern&&!t.pattern.test(String(e)))}function oe(e,t){return new Promise(n=>{let r=t.validator(e,t);if(E(r)){r.then(n);return}n(r)})}function I(e,t){let{message:n}=t;return A(n)?n(e,t):n||``}function se({target:e}){e.composing=!0}function L({target:e}){e.composing&&(e.composing=!1,e.dispatchEvent(new Event(`input`)))}function ce(e,t){let n=h();e.style.height=`auto`;let r=e.scrollHeight;if(O(t)){let{maxHeight:e,minHeight:n}=t;e!==void 0&&(r=Math.min(r,e)),n!==void 0&&(r=Math.max(r,n))}r&&(e.style.height=`${r}px`,a(n))}function le(e,t){return e===`number`&&(e=`text`,t!=null||(t=`decimal`)),e===`digit`&&(e=`tel`,t!=null||(t=`numeric`)),{type:e,inputmode:t}}function R(e){return[...e].length}function z(e,t){return[...e].slice(0,t).join(``)}var[B,V]=x(`field`),H={id:String,name:String,leftIcon:String,rightIcon:String,autofocus:Boolean,clearable:Boolean,maxlength:y,max:Number,min:Number,formatter:Function,clearIcon:b(`clear`),modelValue:D(``),inputAlign:String,placeholder:String,autocomplete:String,autocapitalize:String,autocorrect:String,errorMessage:String,enterkeyhint:String,clearTrigger:b(`focus`),formatTrigger:b(`onChange`),spellcheck:{type:Boolean,default:null},error:{type:Boolean,default:null},disabled:{type:Boolean,default:null},readonly:{type:Boolean,default:null},inputmode:String},U=p({},r,H,{rows:y,type:b(`text`),rules:Array,autosize:[Boolean,Object],labelWidth:y,labelClass:null,labelAlign:String,showWordLimit:Boolean,errorMessageAlign:String,colon:{type:Boolean,default:null}}),W=o({name:B,props:U,emits:[`blur`,`focus`,`clear`,`keypress`,`clickInput`,`endValidate`,`startValidate`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(r,{emit:a,slots:o}){let p=P(),h=ee({status:`unvalidated`,focused:!1,validateMessage:``}),y=u(),b=u(),x=u(),{parent:w}=g(e),E=()=>{var e;return String((e=r.modelValue)==null?``:e)},D=e=>{if(C(r[e]))return r[e];if(w&&C(w.props[e]))return w.props[e]},O=T(()=>{let e=D(`readonly`);if(r.clearable&&!e){let e=E()!==``,t=r.clearTrigger===`always`||r.clearTrigger===`focus`&&h.focused;return e&&t}return!1}),A=T(()=>x.value&&o.input?x.value():r.modelValue),M=T(()=>{var e;let t=D(`required`);return t===`auto`?(e=r.rules)==null?void 0:e.some(e=>e.required):t}),N=e=>e.reduce((e,t)=>e.then(()=>{if(h.status===`failed`)return;let{value:e}=A;if(t.formatter&&(e=t.formatter(e,t)),!ae(e,t)){h.status=`failed`,h.validateMessage=I(e,t);return}if(t.validator)return F(e)&&t.validateEmpty===!1?void 0:oe(e,t).then(n=>{n&&typeof n==`string`?(h.status=`failed`,h.validateMessage=n):n===!1&&(h.status=`failed`,h.validateMessage=I(e,t))})}),Promise.resolve()),B=()=>{h.status=`unvalidated`,h.validateMessage=``},H=()=>a(`endValidate`,{status:h.status,message:h.validateMessage}),U=(e=r.rules)=>new Promise(t=>{B(),e?(a(`startValidate`),N(e).then(()=>{h.status===`failed`?(t({name:r.name,message:h.validateMessage}),H()):(h.status=`passed`,t(),H())})):t()}),W=e=>{if(w&&r.rules){let{validateTrigger:t}=w.props,n=k(t).includes(e),i=r.rules.filter(t=>t.trigger?k(t.trigger).includes(e):n);i.length&&U(i)}},G=e=>{var t;let{maxlength:n}=r;if(C(n)&&R(e)>+n){let r=E();if(r&&R(r)===+n)return r;let i=(t=y.value)==null?void 0:t.selectionEnd;if(h.focused&&i){let t=[...e],r=t.length-+n;return i=R(e.slice(0,i)),t.splice(i-r,r),t.join(``)}return z(e,+n)}return e},K=(e,t=`onChange`)=>{var n,i;let o=e;e=G(e);let s=o.length-e.length;if(r.type===`number`||r.type===`digit`){let a=r.type===`number`;if(e=S(e,a,a),t===`onBlur`&&e!==``&&(r.min!==void 0||r.max!==void 0)){let t=ie(+e,(n=r.min)==null?-1/0:n,(i=r.max)==null?1/0:i);+e!==t&&(e=t.toString())}}let c=0;if(r.formatter&&t===r.formatTrigger){let{formatter:t,maxlength:n}=r;if(e=t(e),C(n)&&R(e)>+n&&(e=z(e,+n)),y.value&&h.focused){let{selectionEnd:e}=y.value,n=z(o,e);c=t(n).length-n.length}}if(y.value&&y.value.value!==e){if(h.focused){let{selectionStart:t,selectionEnd:n}=y.value;if(y.value.value=e,C(t)&&C(n)){let r=e.length;s?(t-=s,n-=s):c&&(t+=c,n+=c),y.value.setSelectionRange(Math.min(t,r),Math.min(n,r))}}else y.value.value=e}e!==r.modelValue&&a(`update:modelValue`,e)},ue=e=>{e.target.composing||K(e.target.value)},q=()=>{var e;return(e=y.value)==null?void 0:e.blur()},J=()=>{var e;return(e=y.value)==null?void 0:e.focus()},Y=()=>{let e=y.value;r.type===`textarea`&&r.autosize&&e&&ce(e,r.autosize)},de=e=>{h.focused=!0,a(`focus`,e),s(Y),D(`readonly`)&&q()},fe=e=>{h.focused=!1,K(E(),`onBlur`),a(`blur`,e),!D(`readonly`)&&(W(`onBlur`),s(Y),ne())},X=e=>a(`clickInput`,e),pe=e=>a(`clickLeftIcon`,e),me=e=>a(`clickRightIcon`,e),he=e=>{f(e),a(`update:modelValue`,``),a(`clear`,e)},Z=T(()=>{if(typeof r.error==`boolean`)return r.error;if(w&&w.props.showError&&h.status===`failed`)return!0}),ge=T(()=>{let e=D(`labelWidth`),t=D(`labelAlign`);if(e&&t!==`top`)return{width:j(e)}}),Q=e=>{e.keyCode===13&&(!(w&&w.props.submitOnEnter)&&r.type!==`textarea`&&f(e),r.type===`search`&&q()),a(`keypress`,e)},$=()=>r.id||`${p}-input`,_e=()=>h.status,ve=()=>{let e=V(`control`,[D(`inputAlign`),{error:Z.value,custom:!!o.input,"min-height":r.type===`textarea`&&!r.autosize}]);if(o.input)return m(`div`,{class:e,onClick:X},[o.input()]);let t={id:$(),ref:y,name:r.name,rows:r.rows===void 0?void 0:+r.rows,class:e,disabled:D(`disabled`),readonly:D(`readonly`),autofocus:r.autofocus,placeholder:r.placeholder,autocomplete:r.autocomplete,autocapitalize:r.autocapitalize,autocorrect:r.autocorrect,enterkeyhint:r.enterkeyhint,spellcheck:r.spellcheck,"aria-labelledby":r.label?`${p}-label`:void 0,"data-allow-mismatch":`attribute`,onBlur:fe,onFocus:de,onInput:ue,onClick:X,onChange:L,onKeypress:Q,onCompositionend:L,onCompositionstart:se};return r.type===`textarea`?m(`textarea`,c(t,{inputmode:r.inputmode}),null):m(`input`,c(le(r.type,r.inputmode),t),null)},ye=()=>{let e=o[`left-icon`];if(r.leftIcon||e)return m(`div`,{class:V(`left-icon`),onClick:pe},[e?e():m(t,{name:r.leftIcon,classPrefix:r.iconPrefix},null)])},be=()=>{let e=o[`right-icon`];if(r.rightIcon||e)return m(`div`,{class:V(`right-icon`),onClick:me},[e?e():m(t,{name:r.rightIcon,classPrefix:r.iconPrefix},null)])},xe=()=>{if(r.showWordLimit&&r.maxlength){let e=R(E());return m(`div`,{class:V(`word-limit`)},[m(`span`,{class:V(`word-num`)},[e]),re(`/`),r.maxlength])}},Se=()=>{if(w&&w.props.showErrorMessage===!1)return;let e=r.errorMessage||h.validateMessage;if(e){let t=o[`error-message`],n=D(`errorMessageAlign`);return m(`div`,{class:V(`error-message`,n)},[t?t({message:e}):e])}},Ce=()=>{let e=D(`labelWidth`),t=D(`labelAlign`),n=D(`colon`)?`:`:``;if(o.label)return[o.label(),n];if(r.label)return m(`label`,{id:`${p}-label`,for:o.input?void 0:$(),"data-allow-mismatch":`attribute`,onClick:e=>{f(e),J()},style:t===`top`&&e?{width:j(e)}:void 0},[r.label+n])},we=()=>[m(`div`,{class:V(`body`)},[ve(),O.value&&m(t,{ref:b,name:r.clearIcon,class:V(`clear`)},null),be(),o.button&&m(`div`,{class:V(`button`)},[o.button()])]),xe(),Se()];return i({blur:q,focus:J,validate:U,formValue:A,resetValidation:B,getValidationStatus:_e,adjustTextareaSize:Y}),l(te,{customValue:x,resetValidation:B,validateWithTrigger:W}),v(()=>r.modelValue,()=>{K(E()),B(),W(`onChange`),s(Y)}),d(()=>{K(E(),r.formatTrigger),s(Y)}),_(`touchstart`,he,{target:T(()=>{var e;return(e=b.value)==null?void 0:e.$el})}),()=>{let e=D(`disabled`),t=D(`labelAlign`),i=ye(),a=()=>{let e=Ce();return t===`top`?[i,e].filter(Boolean):e||[]};return m(n,{size:r.size,class:V({error:Z.value,disabled:e,[`label-${t}`]:t}),center:r.center,border:r.border,isLink:!e&&r.isLink,clickable:!e&&r.clickable,onClick:e?e=>e.stopImmediatePropagation():void 0,titleStyle:ge.value,valueClass:V(`value`),titleClass:[V(`label`,[t,{required:M.value}]),r.labelClass],arrowDirection:r.arrowDirection},{icon:i&&t!==`top`?()=>i:null,title:a,value:we,extra:o.extra})}}}),G=w(W);export{H as n,P as r,G as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js new file mode 100644 index 000000000..f5338dc42 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js @@ -0,0 +1 @@ +import{a as e,f as t,h as n,l as r,n as i,o as a,s as o,u as s}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n as c,t as l}from"./use-placeholder-CcVnlsb4.js";import{a as u,i as d,s as f}from"./cell-group-B1PakqJq.js";import{$ as p,B as m,Ct as h,Dt as ee,Et as g,Gt as _,It as te,Kt as v,L as y,Mt as b,O as x,Ot as ne,Q as S,R as C,S as w,St as T,U as E,V as re,Vt as D,Wt as O,X as k,Y as A,Z as j,_ as M,at as N,ct as ie,dt as P,et as ae,ft as oe,g as F,h as se,ht as I,jt as ce,kt as le,lt as ue,mt as de,ot as fe,tt as pe,ut as L,v as me}from"./index-Gkjcdjlo.js";var[R,z]=M(`action-bar`),B=Symbol(R),he=h({name:R,props:{placeholder:Boolean,safeAreaInsetBottom:j},setup(e,{slots:t}){let n=v(),r=l(n,z),{linkChildren:i}=m(B);i();let a=()=>{var r;return T(`div`,{ref:n,class:[z(),{"van-safe-area-bottom":e.safeAreaInsetBottom}]},[(r=t.default)==null?void 0:r.call(t)])};return()=>e.placeholder?r(a):a()}}),ge=F(he),[_e,ve]=M(`action-bar-button`),ye=S({},e,{type:String,text:String,icon:String,color:String,loading:Boolean,disabled:Boolean}),be=h({name:_e,props:ye,setup(e,{slots:t}){let n=a(),{parent:r,index:i}=E(B),o=I(()=>{if(r){let e=r.children[i.value-1];return!(e&&`isButton`in e)}}),s=I(()=>{if(r){let e=r.children[i.value+1];return!(e&&`isButton`in e)}});return f({isButton:!0}),()=>{let{type:r,icon:i,text:a,color:c,loading:l,disabled:d}=e;return T(u,{class:ve([r,{last:s.value,first:o.value}]),size:`large`,type:r,icon:i,color:c,loading:l,disabled:d,onClick:n},{default:()=>[t.default?t.default():a]})}}}),V=F(be),H={show:Boolean,zIndex:k,overlay:j,duration:k,teleport:[String,Object],lockScroll:j,lazyRender:j,beforeClose:Function,overlayProps:Object,overlayStyle:Object,overlayClass:null,transitionAppear:Boolean,closeOnClickOverlay:j},xe=Object.keys(H);function Se(e,t){return e>t?`horizontal`:t>e?`vertical`:``}function U(){let e=v(0),t=v(0),n=v(0),r=v(0),i=v(0),a=v(0),o=v(``),s=v(!0),c=()=>o.value===`vertical`,l=()=>o.value===`horizontal`,u=()=>{n.value=0,r.value=0,i.value=0,a.value=0,o.value=``,s.value=!0};return{move:(c=>{let l=c.touches[0];n.value=(l.clientX<0?0:l.clientX)-e.value,r.value=l.clientY-t.value,i.value=Math.abs(n.value),a.value=Math.abs(r.value),(!o.value||i.value<10&&a.value<10)&&(o.value=Se(i.value,a.value)),s.value&&(i.value>5||a.value>5)&&(s.value=!1)}),start:(n=>{u(),e.value=n.touches[0].clientX,t.value=n.touches[0].clientY}),reset:u,startX:e,startY:t,deltaX:n,deltaY:r,offsetX:i,offsetY:a,direction:o,isVertical:c,isHorizontal:l,isTap:s}}var W=0,G=`van-overflow-hidden`;function Ce(e,t){let n=U(),r=t=>{n.move(t);let r=n.deltaY.value>0?`10`:`01`,i=y(t.target,e.value);for(;i.scrollHeight<=i.offsetHeight&&i!==e.value&&i.parentElement;)i=y(i.parentElement,e.value);let{scrollHeight:a,offsetHeight:o,scrollTop:s}=i,c=`11`;s===0?c=o>=a?`00`:`01`:s+o>=a&&(c=`10`),c!==`11`&&n.isVertical()&&!(parseInt(c,2)&parseInt(r,2))&&x(t,!0)},i=()=>{document.addEventListener(`touchstart`,n.start),document.addEventListener(`touchmove`,r,{passive:!1}),W||document.body.classList.add(G),W++},a=()=>{W&&(document.removeEventListener(`touchstart`,n.start),document.removeEventListener(`touchmove`,r),W--,W||document.body.classList.remove(G))},o=()=>t()&&i(),s=()=>t()&&a();C(o),ce(s),le(s),D(t,e=>{e?i():a()})}function K(e){let t=v(!1);return D(e,e=>{e&&(t.value=e)},{immediate:!0}),e=>()=>t.value?e():null}var[we,Te]=M(`overlay`),Ee=h({name:we,inheritAttrs:!1,props:{show:Boolean,zIndex:k,duration:k,className:null,lockScroll:j,lazyRender:j,customStyle:Object,teleport:[String,Object]},setup(e,{attrs:t,slots:n}){let r=v(),i=K(()=>e.show||!e.lazyRender),a=t=>{e.lockScroll&&x(t,!0)},o=i(()=>{var i;let a=S(w(e.zIndex),e.customStyle);return ae(e.duration)&&(a.animationDuration=`${e.duration}s`),O(T(`div`,g({ref:r,style:a,class:[Te(),e.className]},t),[(i=n.default)==null?void 0:i.call(n)]),[[L,e.show]])});return re(`touchmove`,a,{target:r}),()=>{let t=T(ie,{name:`van-fade`,appear:!0},{default:o});return e.teleport?T(de,{to:e.teleport},{default:()=>[t]}):t}}}),De=F(Ee),Oe=S({},H,{round:Boolean,position:A(`center`),closeIcon:A(`cross`),closeable:Boolean,transition:String,iconPrefix:String,closeOnPopstate:Boolean,closeIconPosition:A(`top-right`),destroyOnClose:Boolean,safeAreaInsetTop:Boolean,safeAreaInsetBottom:Boolean}),[ke,q]=M(`popup`),Ae=h({name:ke,inheritAttrs:!1,props:Oe,emits:[`open`,`close`,`opened`,`closed`,`keydown`,`update:show`,`clickOverlay`,`clickCloseIcon`],setup(e,{emit:t,attrs:r,slots:a}){let s,l,u=v(),p=v(),m=K(()=>e.show||!e.lazyRender),h=I(()=>{let t={zIndex:u.value};if(ae(e.duration)){let n=e.position===`center`?`animationDuration`:`transitionDuration`;t[n]=`${e.duration}s`}return t}),_=()=>{s||(s=!0,u.value=e.zIndex===void 0?se():+e.zIndex,t(`open`))},y=()=>{s&&c(e.beforeClose,{done(){s=!1,t(`close`),t(`update:show`,!1)}})},x=n=>{t(`clickOverlay`,n),e.closeOnClickOverlay&&y()},C=()=>{if(e.overlay){let t=S({show:e.show,class:e.overlayClass,zIndex:u.value,duration:e.duration,customStyle:e.overlayStyle,role:e.closeOnClickOverlay?`button`:void 0,tabindex:e.closeOnClickOverlay?0:void 0},e.overlayProps);return T(De,g(t,d(),{onClick:x}),{default:a[`overlay-content`]})}},w=e=>{t(`clickCloseIcon`,e),y()},E=()=>{if(e.closeable)return T(i,{role:`button`,tabindex:0,name:e.closeIcon,class:[q(`close-icon`,e.closeIconPosition),n],classPrefix:e.iconPrefix,onClick:w},null)},k,A=()=>{k&&clearTimeout(k),k=setTimeout(()=>{t(`opened`)})},j=()=>t(`closed`),M=e=>t(`keydown`,e),N=m(()=>{var t;let{destroyOnClose:n,round:i,position:o,safeAreaInsetTop:s,safeAreaInsetBottom:c,show:l}=e;if(l||!n)return O(T(`div`,g({ref:p,style:h.value,role:`dialog`,tabindex:0,class:[q({round:i,[o]:o}),{"van-safe-area-top":s,"van-safe-area-bottom":c}],onKeydown:M},r,d()),[(t=a.default)==null?void 0:t.call(a),E()]),[[L,l]])}),P=()=>{let{position:t,transition:n,transitionAppear:r}=e,i=t===`center`?`van-fade`:`van-popup-slide-${t}`;return T(ie,{name:n||i,appear:r,onAfterEnter:A,onAfterLeave:j},{default:N})};return D(()=>e.show,e=>{e&&!s&&(_(),r.tabindex===0&&ee(()=>{var e;(e=p.value)==null||e.focus()})),!e&&s&&(s=!1,t(`close`))}),f({popupRef:p}),Ce(p,()=>e.show&&e.lockScroll),re(`popstate`,()=>{e.closeOnPopstate&&(y(),l=!1)}),b(()=>{e.show&&_()}),ne(()=>{l&&(t(`update:show`,!0),l=!1)}),ce(()=>{e.show&&e.teleport&&(y(),l=!0)}),te(o,()=>e.show),()=>e.teleport?T(de,{to:e.teleport},{default:()=>[C(),P()]}):T(oe,null,[C(),P()])}}),J=F(Ae);function je(){let e=_({show:!1}),t=t=>{e.show=t},n=n=>{S(e,n,{transitionAppear:!0}),t(!0)},r=()=>t(!1);return f({open:n,close:r,toggle:t}),{open:n,close:r,state:e,toggle:t}}function Me(e){let t=ue(e),n=document.createElement(`div`);return document.body.appendChild(n),{instance:t.mount(n),unmount(){t.unmount(),document.body.removeChild(n)}}}var[Ne,Y,X]=M(`dialog`),Pe=S({},H,{title:String,theme:String,width:k,message:[String,Function],callback:Function,allowHtml:Boolean,className:null,transition:A(`van-dialog-bounce`),messageAlign:String,closeOnPopstate:j,showCancelButton:Boolean,cancelButtonText:String,cancelButtonColor:String,cancelButtonDisabled:Boolean,confirmButtonText:String,confirmButtonColor:String,confirmButtonDisabled:Boolean,showConfirmButton:j,closeOnClickOverlay:Boolean,keyboardEnabled:j,destroyOnClose:Boolean}),Fe=[...xe,`transition`,`closeOnPopstate`,`destroyOnClose`],Ie=h({name:Ne,props:Pe,emits:[`confirm`,`cancel`,`keydown`,`update:show`],setup(e,{emit:n,slots:r}){let i=v(),a=_({confirm:!1,cancel:!1}),o=e=>n(`update:show`,e),l=t=>{var n;o(!1),(n=e.callback)==null||n.call(e,t)},d=t=>()=>{e.show&&(n(t),e.beforeClose?(a[t]=!0,c(e.beforeClose,{args:[t],done(){l(t),a[t]=!1},canceled(){a[t]=!1}})):l(t))},f=d(`cancel`),p=d(`confirm`),m=P(t=>{var r,a;e.keyboardEnabled&&t.target===((a=(r=i.value)==null?void 0:r.popupRef)==null?void 0:a.value)&&({Enter:e.showConfirmButton?p:N,Escape:e.showCancelButton?f:N}[t.key](),n(`keydown`,t))},[`enter`,`esc`]),h=()=>{let t=r.title?r.title():e.title;if(t)return T(`div`,{class:Y(`header`,{isolated:!e.message&&!r.default})},[t])},ee=t=>{let{message:n,allowHtml:r,messageAlign:i}=e,a=Y(`message`,{"has-title":t,[i]:i}),o=pe(n)?n():n;return r&&typeof o==`string`?T(`div`,{class:a,innerHTML:o},null):T(`div`,{class:a},[o])},te=()=>{if(r.default)return T(`div`,{class:Y(`content`)},[r.default()]);let{title:t,message:n,allowHtml:i}=e;if(n){let e=!!(t||r.title);return T(`div`,{key:+!!i,class:Y(`content`,{isolated:!e})},[ee(e)])}},y=()=>T(`div`,{class:[t,Y(`footer`)]},[e.showCancelButton&&T(u,{size:`large`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),style:{color:e.cancelButtonColor},loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(u,{size:`large`,text:e.confirmButtonText||X(`confirm`),class:[Y(`confirm`),{[s]:e.showCancelButton}],style:{color:e.confirmButtonColor},loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]),b=()=>T(ge,{class:Y(`footer`)},{default:()=>[e.showCancelButton&&T(V,{type:`warning`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),color:e.cancelButtonColor,loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(V,{type:`danger`,text:e.confirmButtonText||X(`confirm`),class:Y(`confirm`),color:e.confirmButtonColor,loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]}),x=()=>r.footer?r.footer():e.theme===`round-button`?b():y();return()=>{let{width:t,title:n,theme:r,message:a,className:s}=e;return T(J,g({ref:i,role:`dialog`,class:[Y([r]),s],style:{width:me(t)},tabindex:0,"aria-labelledby":n||a,onKeydown:m,"onUpdate:show":o},fe(e,Fe)),{default:()=>[h(),te(),x()]})}}}),Z,Le=S({},{title:``,width:``,theme:null,message:``,overlay:!0,callback:null,teleport:`body`,className:``,allowHtml:!1,lockScroll:!0,transition:void 0,beforeClose:null,overlayClass:``,overlayStyle:void 0,messageAlign:``,cancelButtonText:``,cancelButtonColor:null,cancelButtonDisabled:!1,confirmButtonText:``,confirmButtonColor:null,confirmButtonDisabled:!1,showConfirmButton:!0,showCancelButton:!1,closeOnPopstate:!0,closeOnClickOverlay:!1,destroyOnClose:!1});function Re(){let e={setup(){let{state:e,toggle:t}=je();return()=>T(Ie,g(e,{"onUpdate:show":t}),null)}};({instance:Z}=Me(e))}function ze(e){return p?new Promise((t,n)=>{Z||Re(),Z.open(S({},Le,e,{callback:e=>{(e===`confirm`?t:n)(e)}}))}):Promise.resolve(void 0)}var Be=e=>ze(S({showCancelButton:!0},e)),[Ve,Q]=M(`nav-bar`),He=h({name:Ve,props:{title:String,fixed:Boolean,zIndex:k,border:j,leftText:String,rightText:String,leftDisabled:Boolean,rightDisabled:Boolean,leftArrow:Boolean,placeholder:Boolean,safeAreaInsetTop:Boolean,clickable:j},emits:[`clickLeft`,`clickRight`],setup(e,{emit:t,slots:n}){let a=v(),o=l(a,Q),s=n=>{e.leftDisabled||t(`clickLeft`,n)},c=n=>{e.rightDisabled||t(`clickRight`,n)},u=()=>n.left?n.left():[e.leftArrow&&T(i,{class:Q(`arrow`),name:`arrow-left`},null),e.leftText&&T(`span`,{class:Q(`text`)},[e.leftText])],d=()=>n.right?n.right():T(`span`,{class:Q(`text`)},[e.rightText]),f=()=>{let{title:t,fixed:i,border:o,zIndex:l}=e,f=w(l),p=e.leftArrow||e.leftText||n.left,m=e.rightText||n.right;return T(`div`,{ref:a,style:f,class:[Q({fixed:i}),{[r]:o,"van-safe-area-top":e.safeAreaInsetTop}]},[T(`div`,{class:Q(`content`)},[p&&T(`div`,{class:[Q(`left`,{disabled:e.leftDisabled}),e.clickable&&!e.leftDisabled?`van-haptics-feedback`:``],onClick:s},[u()]),T(`div`,{class:[Q(`title`),`van-ellipsis`]},[n.title?n.title():t]),m&&T(`div`,{class:[Q(`right`,{disabled:e.rightDisabled}),e.clickable&&!e.rightDisabled?`van-haptics-feedback`:``],onClick:c},[d()])])])};return()=>e.fixed&&e.placeholder?o(f):f()}}),Ue=F(He),$=e=>String(e).padStart(2,`0`);function We(e){let t=String(e||``).trim();if(!t)return null;let n=new Date(t);return Number.isNaN(n.getTime())?null:n}function Ge(e,t=new Date){let n=We(e);if(!n)return`—`;let r=Math.round((t.getTime()-n.getTime())/1e3);if(r<45&&r>-60)return`刚刚`;if(r>=45&&r<3600)return`${Math.max(1,Math.round(r/60))} 分钟前`;let i=`${$(n.getHours())}:${$(n.getMinutes())}`,a=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),o=864e5;return n.getTime()>=a&&n.getTime()=a-o&&n.getTime().van-tabs__wrap{height:var(--van-tabs-card-height)}.van-swipe-item{flex-shrink:0;width:100%;height:100%;position:relative}.van-tab__panel,.van-tab__panel-wrapper{box-sizing:border-box;flex-shrink:0;width:100%}.van-tab__panel-wrapper--inactive{height:0;overflow:visible}:root,:host{--van-overlay-z-index:1;--van-overlay-background:#000000b3}.van-overlay{z-index:var(--van-overlay-z-index);background:var(--van-overlay-background);width:100%;height:100%;position:fixed;top:0;left:0}:root,:host{--van-popup-background:var(--van-background-2);--van-popup-transition:transform var(--van-duration-base);--van-popup-round-radius:16px;--van-popup-close-icon-size:22px;--van-popup-close-icon-color:var(--van-gray-5);--van-popup-close-icon-margin:16px;--van-popup-close-icon-z-index:1}.van-overflow-hidden{overflow:hidden!important}.van-popup{box-sizing:border-box;background:var(--van-popup-background);max-height:100%;transition:var(--van-popup-transition);-webkit-overflow-scrolling:touch;position:fixed;overflow-y:auto}.van-popup--center{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100vw - var(--van-padding-md) * 2);margin:0 auto;top:50%;left:0;right:0;transform:translateY(-50%)}.van-popup--center.van-popup--round{border-radius:var(--van-popup-round-radius)}.van-popup--top{width:100%;top:0;left:0}.van-popup--top.van-popup--round{border-radius:0 0 var(--van-popup-round-radius) var(--van-popup-round-radius)}.van-popup--right{top:50%;right:0;transform:translateY(-50%)}.van-popup--right.van-popup--round{border-radius:var(--van-popup-round-radius) 0 0 var(--van-popup-round-radius)}.van-popup--bottom{width:100%;bottom:0;left:0}.van-popup--bottom.van-popup--round{border-radius:var(--van-popup-round-radius) var(--van-popup-round-radius) 0 0}.van-popup--left{top:50%;left:0;transform:translateY(-50%)}.van-popup--left.van-popup--round{border-radius:0 var(--van-popup-round-radius) var(--van-popup-round-radius) 0}.van-popup-slide-top-enter-active,.van-popup-slide-left-enter-active,.van-popup-slide-right-enter-active,.van-popup-slide-bottom-enter-active{transition-timing-function:var(--van-ease-out)}.van-popup-slide-top-leave-active,.van-popup-slide-left-leave-active,.van-popup-slide-right-leave-active,.van-popup-slide-bottom-leave-active{transition-timing-function:var(--van-ease-in)}.van-popup-slide-top-enter-from,.van-popup-slide-top-leave-active{transform:translateY(-100%)}.van-popup-slide-right-enter-from,.van-popup-slide-right-leave-active{transform:translate(100%,-50%)}.van-popup-slide-bottom-enter-from,.van-popup-slide-bottom-leave-active{transform:translateY(100%)}.van-popup-slide-left-enter-from,.van-popup-slide-left-leave-active{transform:translate(-100%,-50%)}.van-popup__close-icon{z-index:var(--van-popup-close-icon-z-index);color:var(--van-popup-close-icon-color);font-size:var(--van-popup-close-icon-size);position:absolute}.van-popup__close-icon--top-left{top:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--top-right{top:var(--van-popup-close-icon-margin);right:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-left{bottom:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-right{right:var(--van-popup-close-icon-margin);bottom:var(--van-popup-close-icon-margin)}:root,:host{--van-empty-padding:var(--van-padding-xl) 0;--van-empty-image-size:160px;--van-empty-description-margin-top:var(--van-padding-md);--van-empty-description-padding:0 60px;--van-empty-description-color:var(--van-text-color-2);--van-empty-description-font-size:var(--van-font-size-md);--van-empty-description-line-height:var(--van-line-height-md);--van-empty-bottom-margin-top:24px}.van-empty{box-sizing:border-box;padding:var(--van-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-empty__image{width:var(--van-empty-image-size);height:var(--van-empty-image-size)}.van-empty__image img{width:100%;height:100%}.van-empty__description{margin-top:var(--van-empty-description-margin-top);padding:var(--van-empty-description-padding);color:var(--van-empty-description-color);font-size:var(--van-empty-description-font-size);line-height:var(--van-empty-description-line-height)}.van-empty__bottom{margin-top:var(--van-empty-bottom-margin-top)}.van-theme-dark .van-empty{opacity:.5}:root,:host{--van-notice-bar-height:40px;--van-notice-bar-padding:0 var(--van-padding-md);--van-notice-bar-wrapable-padding:var(--van-padding-xs) var(--van-padding-md);--van-notice-bar-text-color:var(--van-orange-dark);--van-notice-bar-font-size:var(--van-font-size-md);--van-notice-bar-line-height:24px;--van-notice-bar-background:var(--van-orange-light);--van-notice-bar-icon-size:16px;--van-notice-bar-icon-min-width:24px}.van-notice-bar{height:var(--van-notice-bar-height);padding:var(--van-notice-bar-padding);color:var(--van-notice-bar-text-color);font-size:var(--van-notice-bar-font-size);line-height:var(--van-notice-bar-line-height);background:var(--van-notice-bar-background);align-items:center;display:flex;position:relative}.van-notice-bar__left-icon,.van-notice-bar__right-icon{min-width:var(--van-notice-bar-icon-min-width);font-size:var(--van-notice-bar-icon-size)}.van-notice-bar__right-icon{text-align:right;cursor:pointer}.van-notice-bar__wrap{flex:1;align-items:center;height:100%;display:flex;position:relative;overflow:hidden}.van-notice-bar__content{white-space:nowrap;transition-timing-function:linear;position:absolute}.van-notice-bar__content.van-ellipsis{max-width:100%}.van-notice-bar--wrapable{height:auto;padding:var(--van-notice-bar-wrapable-padding)}.van-notice-bar--wrapable .van-notice-bar__wrap{height:auto}.van-notice-bar--wrapable .van-notice-bar__content{white-space:normal;word-wrap:break-word;position:relative}:root,:host{--van-skeleton-title-width:40%}.van-skeleton-title{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-title--round{border-radius:var(--van-radius-max)}.van-skeleton-title{width:var(--van-skeleton-title-width);margin:0}.van-skeleton-title+.van-skeleton-paragraph{margin-top:20px}:root,:host{--van-skeleton-avatar-size:32px;--van-skeleton-avatar-background:var(--van-active-color)}.van-skeleton-avatar{width:var(--van-skeleton-avatar-size);height:var(--van-skeleton-avatar-size);margin-right:var(--van-padding-md);background:var(--van-skeleton-avatar-background);flex-shrink:0}.van-skeleton-avatar--round{border-radius:var(--van-radius-max)}.van-skeleton-avatar+.van-skeleton__content{padding-top:var(--van-padding-xs)}:root,:host{--van-skeleton-paragraph-height:16px;--van-skeleton-paragraph-background:var(--van-active-color);--van-skeleton-paragraph-margin-top:var(--van-padding-sm)}.van-skeleton-paragraph{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-paragraph--round{border-radius:var(--van-radius-max)}.van-skeleton-paragraph:not(:first-child){margin-top:var(--van-skeleton-paragraph-margin-top)}:root,:host{--van-skeleton-duration:1.2s}.van-skeleton{padding:0 var(--van-padding-md);display:flex}.van-skeleton__content{width:100%}.van-skeleton--animate{animation:van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite}@keyframes van-skeleton-blink{50%{opacity:.6}}:root,:host{--van-action-bar-background:var(--van-background-2);--van-action-bar-height:50px}.van-action-bar{box-sizing:content-box;height:var(--van-action-bar-height);background:var(--van-action-bar-background);align-items:center;display:flex;position:fixed;bottom:0;left:0;right:0}:root,:host{--van-action-bar-button-height:40px;--van-action-bar-button-warning-color:var(--van-gradient-orange);--van-action-bar-button-danger-color:var(--van-gradient-red)}.van-action-bar-button{height:var(--van-action-bar-button-height);font-weight:var(--van-font-bold);font-size:var(--van-font-size-md);border:none;border-radius:0;flex:1}.van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max);margin-left:5px}.van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max);margin-right:5px}.van-action-bar-button--warning{background:var(--van-action-bar-button-warning-color)}.van-action-bar-button--danger{background:var(--van-action-bar-button-danger-color)}@media (max-width:321px){.van-action-bar-button{font-size:13px}}:root,:host{--van-dialog-width:320px;--van-dialog-small-screen-width:90%;--van-dialog-font-size:var(--van-font-size-lg);--van-dialog-transition:var(--van-duration-base);--van-dialog-radius:16px;--van-dialog-background:var(--van-background-2);--van-dialog-header-font-weight:var(--van-font-bold);--van-dialog-header-line-height:24px;--van-dialog-header-padding-top:26px;--van-dialog-header-isolated-padding:var(--van-padding-lg) 0;--van-dialog-message-padding:var(--van-padding-lg);--van-dialog-message-font-size:var(--van-font-size-md);--van-dialog-message-line-height:var(--van-line-height-md);--van-dialog-message-max-height:60vh;--van-dialog-has-title-message-text-color:var(--van-gray-7);--van-dialog-has-title-message-padding-top:var(--van-padding-xs);--van-dialog-button-height:48px;--van-dialog-round-button-height:36px;--van-dialog-confirm-button-text-color:var(--van-primary-color)}.van-dialog{width:var(--van-dialog-width);font-size:var(--van-dialog-font-size);background:var(--van-dialog-background);border-radius:var(--van-dialog-radius);-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:var(--van-dialog-transition);transition-property:transform,opacity;top:45%;overflow:hidden}@media (max-width:321px){.van-dialog{width:var(--van-dialog-small-screen-width)}}.van-dialog__header{color:var(--van-text-color);padding-top:var(--van-dialog-header-padding-top);font-weight:var(--van-dialog-header-font-weight);line-height:var(--van-dialog-header-line-height);text-align:center}.van-dialog__header--isolated{padding:var(--van-dialog-header-isolated-padding)}.van-dialog__content--isolated{align-items:center;min-height:104px;display:flex}.van-dialog__message{color:var(--van-text-color);max-height:var(--van-dialog-message-max-height);padding:26px var(--van-dialog-message-padding);font-size:var(--van-dialog-message-font-size);line-height:var(--van-dialog-message-line-height);white-space:pre-wrap;text-align:center;word-wrap:break-word;-webkit-overflow-scrolling:touch;flex:1;overflow-y:auto}.van-dialog__message--has-title{padding-top:var(--van-dialog-has-title-message-padding-top);color:var(--van-dialog-has-title-message-text-color)}.van-dialog__message--left{text-align:left}.van-dialog__message--right{text-align:right}.van-dialog__message--justify{text-align:justify}.van-dialog__footer{-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;overflow:hidden}.van-dialog__confirm,.van-dialog__cancel{height:var(--van-dialog-button-height);border:0;border-radius:0;flex:1;margin:0}.van-dialog__confirm,.van-dialog__confirm:active{color:var(--van-dialog-confirm-button-text-color)}.van-dialog--round-button .van-dialog__footer{height:auto;padding:var(--van-padding-xs) var(--van-padding-lg) var(--van-padding-md);position:relative}.van-dialog--round-button .van-dialog__message{padding-bottom:var(--van-padding-md);color:var(--van-text-color)}.van-dialog--round-button .van-dialog__confirm,.van-dialog--round-button .van-dialog__cancel{height:var(--van-dialog-round-button-height)}.van-dialog--round-button .van-dialog__confirm{color:var(--van-white)}.van-dialog--round-button .van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max)}.van-dialog--round-button .van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max)}.van-dialog-bounce-enter-from{opacity:0;transform:translateY(-50%)scale(.7)}.van-dialog-bounce-leave-active{opacity:0;transform:translateY(-50%)scale(.9)}:root,:host{--van-toast-max-width:70%;--van-toast-font-size:var(--van-font-size-md);--van-toast-text-color:var(--van-white);--van-toast-loading-icon-color:var(--van-white);--van-toast-line-height:var(--van-line-height-md);--van-toast-radius:var(--van-radius-lg);--van-toast-background:#000000b3;--van-toast-icon-size:36px;--van-toast-text-min-width:96px;--van-toast-text-padding:var(--van-padding-xs) var(--van-padding-sm);--van-toast-default-padding:var(--van-padding-md);--van-toast-default-width:88px;--van-toast-default-min-height:88px;--van-toast-position-top-distance:20%;--van-toast-position-bottom-distance:20%}.van-toast{box-sizing:content-box;transition:all var(--van-duration-fast);width:var(--van-toast-default-width);max-width:var(--van-toast-max-width);min-height:var(--van-toast-default-min-height);padding:var(--van-toast-default-padding);color:var(--van-toast-text-color);font-size:var(--van-toast-font-size);line-height:var(--van-toast-line-height);white-space:pre-wrap;word-break:break-all;text-align:center;background:var(--van-toast-background);border-radius:var(--van-toast-radius);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-toast--break-normal{word-break:normal;word-wrap:normal}.van-toast--break-word{word-break:normal;word-wrap:break-word}.van-toast--unclickable{cursor:not-allowed;overflow:hidden}.van-toast--unclickable *{pointer-events:none}.van-toast--text,.van-toast--html{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;min-width:var(--van-toast-text-min-width);min-height:0;padding:var(--van-toast-text-padding)}.van-toast--text .van-toast__text,.van-toast--html .van-toast__text{margin-top:0}.van-toast--top{top:var(--van-toast-position-top-distance)}.van-toast--bottom{top:auto;bottom:var(--van-toast-position-bottom-distance)}.van-toast__icon{font-size:var(--van-toast-icon-size)}.van-toast__loading{padding:var(--van-padding-base);color:var(--van-toast-loading-icon-color)}.van-toast__text{margin-top:var(--van-padding-xs)}:root{--app-bg:#f4f7fd;--app-surface:#fff;--app-surface-2:#f6f8fe;--app-border:#6e89c92e;--app-text:#1d315f;--app-text-2:#3f527d;--app-muted:#7b89ab;--app-primary:#2f6fff;--app-primary-soft:#2f6fff1a;--app-success:#0f9e69;--app-success-soft:#13b77a1f;--app-warning:#c46f00;--app-warning-soft:#ff981d24;--app-danger:#e5383e;--app-danger-soft:#ff53581f;--app-muted-soft:#7b89ab24;--app-shadow:0 10px 28px #4e63a214;--app-radius:16px;--app-font:"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;--app-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root{--app-bg:#0b1222;--app-surface:#131c31;--app-surface-2:#18223b;--app-border:#8ca0d229;--app-text:#e6ecfb;--app-text-2:#bcc6e0;--app-muted:#8b97b6;--app-primary:#6f9bff;--app-primary-soft:#5b8cff29;--app-success:#2fd19a;--app-success-soft:#2fd19a24;--app-warning:#ffb45a;--app-warning-soft:#ffae4a24;--app-danger:#ff7a7f;--app-danger-soft:#ff6b7026;--app-muted-soft:#8390b029;--app-shadow:0 10px 28px #00000047;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*,:before,:after{box-sizing:border-box}html,body{background:var(--app-bg);color:var(--app-text);font-family:var(--app-font);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body,#app{min-height:100vh;min-height:100dvh}button,input,textarea{font-family:inherit}.app-card{background:var(--app-surface);border:1px solid var(--app-border);border-radius:var(--app-radius);box-shadow:var(--app-shadow)}.app-muted{color:var(--app-muted)}.app-mono{font-family:var(--app-mono);word-break:break-all;font-size:12px}.tone-success{color:var(--app-success)}.tone-warning{color:var(--app-warning)}.tone-danger{color:var(--app-danger)}.tone-muted{color:var(--app-muted)}.tone-primary{color:var(--app-primary)}.van-nav-bar .van-nav-bar__title{font-weight:600}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js new file mode 100644 index 000000000..b4bf235cd --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/LoginView-nW7aCAJ5.js","assets/_plugin-vue_export-helper-CT0ZqwFS.js","assets/cell-group-B1PakqJq.js","assets/field-CRX3eS7h.js","assets/LoginView-C25eERA6.css","assets/ShellView-CQXCC9xU.js","assets/use-placeholder-CcVnlsb4.js","assets/ShellView-D354hIv5.css","assets/DevicesView-Cgb9V9Z0.js","assets/format-DEXJ_MrY.js","assets/AccountSwitcher-5FuHazzf.js","assets/AccountSwitcher-BZXg15MH.css","assets/DevicesView-CHTvxQxK.css","assets/ConversationsView-CLYafisP.js","assets/ConversationsView-CytDMSuM.css","assets/MeView-uJ3uOzy0.js","assets/MeView-B-MK2tgf.css"])))=>i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>S(e)===`[object Map]`,p=e=>S(e)===`[object Set]`,m=e=>S(e)===`[object Date]`,h=e=>S(e)===`[object RegExp]`,g=e=>typeof e==`function`,_=e=>typeof e==`string`,v=e=>typeof e==`symbol`,y=e=>typeof e==`object`&&!!e,b=e=>(y(e)||g(e))&&g(e.then)&&g(e.catch),x=Object.prototype.toString,S=e=>x.call(e),C=e=>S(e).slice(8,-1),w=e=>S(e)===`[object Object]`,T=e=>_(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,ee=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),E=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},te=/-\w/g,D=E(e=>e.replace(te,e=>e.slice(1).toUpperCase())),ne=/\B([A-Z])/g,O=E(e=>e.replace(ne,`-$1`).toLowerCase()),re=E(e=>e.charAt(0).toUpperCase()+e.slice(1)),ie=E(e=>e?`on${re(e)}`:``),k=(e,t)=>!Object.is(e,t),A=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},oe=e=>{let t=parseFloat(e);return isNaN(t)?e:t},se=e=>{let t=_(e)?Number(e):NaN;return isNaN(t)?e:t},ce,le=()=>ce||(ce=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{});function ue(e){if(d(e)){let t={};for(let n=0;n{if(e){let n=e.split(fe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function he(e){let t=``;if(_(e))t=e;else if(d(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Se=e=>_(e)?e:e==null?``:d(e)||y(e)&&(e.toString===x||!g(e.toString))?xe(e)?Se(e.value):JSON.stringify(e,Ce,2):String(e),Ce=(e,t)=>xe(t)?Ce(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[we(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>we(e))}:v(t)?we(t):y(t)&&!d(t)&&!w(t)?String(t):t,we=(e,t=``)=>{var n;return v(e)?`Symbol(${(n=e.description)==null?t:n})`:e},j,Te=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&j&&(j.active?(this.parent=j,this.index=(j.scopes||(j.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes){let n=this.scopes.slice();for(e=0,t=n.length;e0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(je){let e=je;for(je=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ae;){let t=Ae;for(Ae=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}function Fe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ie(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),ze(r),Be(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Re(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Re(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ke)||(e.globalVersion=Ke,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Le(e))))return;e.flags|=2;let t=e.dep,n=M,r=Ve;M=e,Ve=!0;try{Fe(e);let n=e.fn(e._value);(t.version===0||k(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{M=n,Ve=r,Ie(e),e.flags&=-3}}function ze(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ze(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Be(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var Ve=!0,He=[];function Ue(){He.push(Ve),Ve=!1}function We(){let e=He.pop();Ve=e===void 0||e}function Ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;try{t()}finally{M=e}}}var Ke=0,qe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Je=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!M||!Ve||M===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==M)t=this.activeLink=new qe(M,this),M.deps?(t.prevDep=M.depsTail,M.depsTail.nextDep=t,M.depsTail=t):M.deps=M.depsTail=t,Ye(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=M.depsTail,t.nextDep=void 0,M.depsTail.nextDep=t,M.depsTail=t,M.deps===t&&(M.deps=e)}return t}trigger(e){this.version++,Ke++,this.notify(e)}notify(e){Ne();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pe()}}};function Ye(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Ye(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var Xe=new WeakMap,Ze=Symbol(``),Qe=Symbol(``),$e=Symbol(``);function N(e,t,n){if(Ve&&M){let t=Xe.get(e);t||Xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Je),r.map=t,r.key=n),r.track()}}function et(e,t,n,r,i,a){let o=Xe.get(e);if(!o){Ke++;return}let s=e=>{e&&e.trigger()};if(Ne(),t===`clear`)o.forEach(s);else{let i=d(e),a=i&&T(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===$e||!v(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get($e)),t){case`add`:i?a&&s(o.get(`length`)):(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`delete`:i||(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`set`:f(e)&&s(o.get(Ze))}}Pe()}function tt(e){let t=F(e);return t===e?t:(N(t,`iterate`,$e),P(e)?t:t.map(Ht))}function nt(e){return N(e=F(e),`iterate`,$e),e}function rt(e,t){return zt(e)?Ut(Rt(e)?Ht(t):t):Ht(t)}var it={__proto__:null,[Symbol.iterator](){return at(this,Symbol.iterator,e=>rt(this,e))},concat(...e){return tt(this).concat(...e.map(e=>d(e)?tt(e):e))},entries(){return at(this,`entries`,e=>(e[1]=rt(this,e[1]),e))},every(e,t){return st(this,`every`,e,t,void 0,arguments)},filter(e,t){return st(this,`filter`,e,t,e=>e.map(e=>rt(this,e)),arguments)},find(e,t){return st(this,`find`,e,t,e=>rt(this,e),arguments)},findIndex(e,t){return st(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return st(this,`findLast`,e,t,e=>rt(this,e),arguments)},findLastIndex(e,t){return st(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return st(this,`forEach`,e,t,void 0,arguments)},includes(...e){return lt(this,`includes`,e)},indexOf(...e){return lt(this,`indexOf`,e)},join(e){return tt(this).join(e)},lastIndexOf(...e){return lt(this,`lastIndexOf`,e)},map(e,t){return st(this,`map`,e,t,void 0,arguments)},pop(){return ut(this,`pop`)},push(...e){return ut(this,`push`,e)},reduce(e,...t){return ct(this,`reduce`,e,t)},reduceRight(e,...t){return ct(this,`reduceRight`,e,t)},shift(){return ut(this,`shift`)},some(e,t){return st(this,`some`,e,t,void 0,arguments)},splice(...e){return ut(this,`splice`,e)},toReversed(){return tt(this).toReversed()},toSorted(e){return tt(this).toSorted(e)},toSpliced(...e){return tt(this).toSpliced(...e)},unshift(...e){return ut(this,`unshift`,e)},values(){return at(this,`values`,e=>rt(this,e))}};function at(e,t,n){let r=nt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var ot=Array.prototype;function st(e,t,n,r,i,a){let o=nt(e),s=o!==e&&!P(e),c=o[t];if(c!==ot[t]){let t=c.apply(e,a);return s?Ht(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,rt(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function ct(e,t,n,r){let i=nt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=rt(e,t)),n.call(this,t,rt(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?rt(e,c):c}function lt(e,t,n){let r=F(e);N(r,`iterate`,$e);let i=r[t](...n);return(i===-1||i===!1)&&Bt(n[0])?(n[0]=F(n[0]),r[t](...n)):i}function ut(e,t,n=[]){Ue(),Ne();let r=F(e)[t].apply(e,n);return Pe(),We(),r}var dt=e(`__proto__,__v_isRef,__isVue`),ft=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(v));function pt(e){v(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.hasOwnProperty(e)}var mt=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?Mt:jt:i?At:kt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=it[t]))return e;if(t===`hasOwnProperty`)return pt}let o=Reflect.get(e,t,I(e)?e:n);if((v(t)?ft.has(t):dt(t))||(r||N(e,`get`,t),i))return o;if(I(o)){let e=a&&T(t)?o:o.value;return r&&y(e)?It(e):e}return y(o)?r?It(o):Pt(o):o}},ht=class extends mt{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&T(t);if(!this._isShallow){let e=zt(i);if(!P(n)&&!zt(n)&&(i=F(i),n=F(n)),!a&&I(i)&&!I(n))return e||(i.value=n),!0}let o=a?Number(t)e,xt=e=>Reflect.getPrototypeOf(e);function St(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a),c=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?bt:t?Ut:Ht;return!t&&N(a,`iterate`,l?Qe:Ze),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Ct(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function wt(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(k(n,a)&&N(i,`get`,n),N(i,`get`,a));let{has:o}=xt(i),s=t?bt:e?Ut:Ht;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(F(t),`iterate`,Ze),t.size},has(t){let n=this.__v_raw,r=F(n),i=F(t);return e||(k(t,i)&&N(r,`has`,t),N(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=F(a),s=t?bt:e?Ut:Ht;return!e&&N(o,`iterate`,Ze),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Ct(`add`),set:Ct(`set`),delete:Ct(`delete`),clear:Ct(`clear`)}:{add(e){let n=F(this),r=xt(n),i=F(e),a=!t&&!P(e)&&!zt(e)?i:e;return r.has.call(n,a)||k(e,a)&&r.has.call(n,e)||k(i,a)&&r.has.call(n,i)||(n.add(a),et(n,`add`,a,a)),this},set(e,n){!t&&!P(n)&&!zt(n)&&(n=F(n));let r=F(this),{has:i,get:a}=xt(r),o=i.call(r,e);o||(e=F(e),o=i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?k(n,s)&&et(r,`set`,e,n,s):et(r,`add`,e,n),this},delete(e){let t=F(this),{has:n,get:r}=xt(t),i=n.call(t,e);i||(e=F(e),i=n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&et(t,`delete`,e,void 0,a),o},clear(){let e=F(this),t=e.size!==0,n=e.clear();return t&&et(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=St(r,e,t)}),n}function Tt(e,t){let n=wt(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var Et={get:Tt(!1,!1)},Dt={get:Tt(!1,!0)},Ot={get:Tt(!0,!1)},kt=new WeakMap,At=new WeakMap,jt=new WeakMap,Mt=new WeakMap;function Nt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function Pt(e){return zt(e)?e:Lt(e,!1,_t,Et,kt)}function Ft(e){return Lt(e,!1,yt,Dt,At)}function It(e){return Lt(e,!0,vt,Ot,jt)}function Lt(e,t,n,r,i){if(!y(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let a=i.get(e);if(a)return a;let o=Nt(C(e));if(o===0)return e;let s=new Proxy(e,o===2?r:n);return i.set(e,s),s}function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function P(e){return!!(e&&e.__v_isShallow)}function Bt(e){return e?!!e.__v_raw:!1}function F(e){let t=e&&e.__v_raw;return t?F(t):e}function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&ae(e,`__v_skip`,!0),e}var Ht=e=>y(e)?Pt(e):e,Ut=e=>y(e)?It(e):e;function I(e){return e?e.__v_isRef===!0:!1}function Wt(e){return Kt(e,!1)}function Gt(e){return Kt(e,!0)}function Kt(e,t){return I(e)?e:new qt(e,t)}var qt=class{constructor(e,t){this.dep=new Je,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:F(e),this._value=t?e:Ht(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?e:F(e),k(e,t)&&(this._rawValue=e,this._value=n?e:Ht(e),this.dep.trigger())}};function L(e){return I(e)?e.value:e}var Jt={get:(e,t,n)=>t===`__v_raw`?e:L(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return I(i)&&!I(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return Rt(e)?e:new Proxy(e,Jt)}var Xt=class{constructor(e){this.__v_isRef=!0,this._value=void 0;let t=this.dep=new Je,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}};function Zt(e){return new Xt(e)}var Qt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Je(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ke-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return Me(this,!0),!0}get value(){let e=this.dep.track();return Re(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function $t(e,t,n=!1){let r,i;return g(e)?r=e:(r=e.get,i=e.set),new Qt(r,i,n)}var en={},tn=new WeakMap,nn=void 0;function rn(e,t=!1,n=nn){if(n){let t=tn.get(n);t||tn.set(n,t=[]),t.push(e)}}function an(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:P(e)||o===!1||o===0?on(e,1):on(e),m,h,_,v,y=!1,b=!1;if(I(e)?(h=()=>e.value,y=P(e)):Rt(e)?(h=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Rt(e)||P(e)),h=()=>e.map(e=>{if(I(e))return e.value;if(Rt(e))return p(e);if(g(e))return f?f(e,2):e()})):h=g(e)?n?f?()=>f(e,2):e:()=>{if(_){Ue();try{_()}finally{We()}}let t=nn;nn=m;try{return f?f(e,3,[v]):e(v)}finally{nn=t}}:r,n&&o){let e=h,t=o===!0?1/0:o;h=()=>on(e(),t)}let x=Ee(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{let n=e(...t);return S(),n}}let C=b?Array(e.length).fill(en):en,w=e=>{if(m.flags&1&&(m.dirty||e)){if(n){let t=m.run();if(e||o||y||(b?t.some((e,t)=>k(e,C[t])):k(t,C))){_&&_();let e=nn;nn=m;try{let e=[t,C===en?void 0:b&&C[0]===en?[]:C,v];C=t,f?f(n,3,e):n(...e)}finally{nn=e}}}else m.run()}};return u&&u(w),m=new Oe(h),m.scheduler=l?()=>l(w,!1):w,v=e=>rn(e,!1,m),_=m.onStop=()=>{let e=tn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();tn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function on(e,t=1/0,n){if(t<=0||!y(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,I(e))on(e.value,t,n);else if(d(e))for(let r=0;r{on(e,t,n)});else if(w(e)){for(let r in e)on(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&on(e[r],t,n)}return e}function sn(e,t,n,r){try{return r?e(...r):e()}catch(e){cn(e,t,n)}}function R(e,t,n,r){if(g(e)){let i=sn(e,t,n,r);return i&&b(i)&&i.catch(e=>{cn(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a>>1,i=z[r],a=Cn(i);a=Cn(n)?z.push(e):z.splice(_n(t),0,e),e.flags|=1,yn()}}function yn(){hn||(hn=mn.then(wn))}function bn(e){if(!d(e))fn&&e.id===-1?fn.splice(pn+1,0,e):e.flags&1||(dn.push(e),e.flags|=1);else for(let t=0;tCn(e)-Cn(t));if(dn.length=0,fn){for(let t=0;te.id==null?e.flags&2?-1:1/0:e.id;function wn(e){try{for(un=0;un{r._d&&pa(-1);let i=En(t),a=la.length,o;try{o=e(...n)}finally{for(let e=la.length;e>a;e--)da();En(i),r._d&&pa(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function On(e,n){if(B===null)return e;let r=qa(B),i=e.dirs||(e.dirs=[]);for(let e=0;e1)return n&&g(t)?t.call(r&&r.proxy):t}}var jn=Symbol.for(`v-scx`),Mn=()=>V(jn);function Nn(e,t){return In(e,null,t)}function Pn(e,t){return In(e,null,{flush:`sync`})}function Fn(e,t,n){return In(e,t,n)}function In(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ba){if(c===`sync`){let e=Mn();f=e.__watcherHandles||(e.__watcherHandles=[])}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=X;u.call=(e,t,n)=>R(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{G(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():vn(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=an(e,n,u);return Ba&&(f?f.push(h):d&&h()),h}function Ln(e,t,n){let r=this.proxy,i=_(e)?e.includes(`.`)?Rn(r,e):()=>r[e]:e.bind(r,r),a;g(t)?a=t:(a=t.handler,n=t);let o=La(this),s=In(i,a.bind(r),n);return o(),s}function Rn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;ee.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===``),Un=e=>e&&(e.defer||e.defer===``),Wn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Gn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Kn=(e,t)=>{let n=e&&e.to;return _(n)?t?t(n):null:n},qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g,parentNode:_}}=l,v=Hn(t.props),{dynamicChildren:y}=t,b=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},x=(e=t)=>{let n=Hn(e.props),r=e.target=Kn(e.props,m),a=Qn(r,e,h,p);r&&(o!==`svg`&&Wn(r)?o=`svg`:o!==`mathml`&&Gn(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(b(e,r,a),Zn(e,!1)))},S=e=>{let t=()=>{if(zn.get(e)===t){if(zn.delete(e),Hn(e.props)){let t=_(e.el)||n;b(e,t,e.anchor),Zn(e,!0)}x(e)}};zn.set(e,t),G(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Un(t.props)||a&&a.pendingBranch){S(t);return}v&&(b(t,n,i),Zn(t,!0)),x()}else{t.el=e.el;let r=t.anchor=e.anchor,u=zn.get(e);if(u){u.flags|=8,zn.delete(e),S(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Hn(e.props),_=g?n:p,b=g?r:h;if(o===`svg`||Wn(p)?o=`svg`:(o===`mathml`||Gn(p))&&(o=`mathml`),y?(f(e.dynamicChildren,y,_,i,a,o,s),$i(e,t,!0)):c||d(e,t,_,b,i,a,o,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Jn(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=Kn(t.props,m);e&&(t.target=e,Jn(t,e,null,l,0))}else g&&Jn(t,p,h,l,1);Zn(t,v)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=Hn(f),m=a||!p,h=zn.get(e);if(h&&(h.flags|=8,zn.delete(e)),d&&(i(l),i(u)),a&&i(c),!h&&(p||d)&&o&16)for(let e=0;e{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}var U=[Function,Array],tr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:U,onEnter:U,onAfterEnter:U,onEnterCancelled:U,onBeforeLeave:U,onLeave:U,onAfterLeave:U,onLeaveCancelled:U,onBeforeAppear:U,onAppear:U,onAfterAppear:U,onAppearCancelled:U},nr=e=>{let t=e.subTree;return t.component?nr(t.component):t},rr={name:`BaseTransition`,props:tr,setup(e,{slots:t}){let n=Pa(),r=er();return()=>{let i=t.default&&dr(t.default(),!0),a=i&&i.length?ir(i):n.subTree?Ea():void 0;if(!a)return;let o=F(e),{mode:s}=o;if(r.isLeaving)return cr(a);let c=lr(a);if(!c)return cr(a);let l=sr(c,o,r,n,e=>l=e);c.type!==K&&ur(c,l);let u=n.subTree&&lr(n.subTree);if(u&&u.type!==K&&!va(u,c)&&nr(n).type!==K){let e=sr(u,o,r,n);if(ur(u,e),s===`out-in`&&c.type!==K)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},cr(a);s===`in-out`&&c.type!==K?e.delayLeave=(e,t,n)=>{let i=or(r,u);i[String(u.key)]=u,e[H]=()=>{t(),e[H]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return a}}};function ir(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==K){t=n;break}}return t}var ar=rr;function or(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function sr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=or(n,e),C=(e,t)=>{e&&R(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted){if(a)r=_||c;else return}t[H]&&t[H](!0);let i=S[x];i&&va(e,i)&&i.el[H]&&i.el[H](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted){if(a)r=v||l,i=y||u,o=b||f;else return}let s=!1;t[$n]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[$n]=void 0)};let c=t[$n].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[$n]&&t[$n](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[H]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[H]=void 0,S[i]===e&&delete S[i])};let o=t[H].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=sr(e,t,n,r,i);return i&&i(a),a}};return T}function cr(e){if(yr(e))return e=wa(e),e.children=null,e}function lr(e){if(!yr(e))return Vn(e.type)&&e.children?ir(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&g(n.default))return n.default()}}function ur(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;let n=e.component.subTree;ur(Vn(n.type)&&lr(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function dr(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;egr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(vr(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&gr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?qa(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,h=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=F(v),b=v===t?i:e=>!mr(h,e)&&u(y,e),x=(e,t)=>!(t&&mr(h,t));if(m!=null&&m!==p){if(_r(n),_(m))h[m]=null,b(m)&&(v[m]=null);else if(I(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(h[e.k]=null)}}if(g(p))sn(p,f,12,[l,h]);else{let t=_(p),n=I(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:h[p]:x(p)||!e.k?p.value:h[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)h[p]=[s],b(p)&&(v[p]=h[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(h[e.k]=t)}}else t?(h[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(h[e.k]=l))};if(l){let t=()=>{i(),hr.delete(e)};t.id=-1,hr.set(e,t),G(t,r)}else _r(e),i()}}}function _r(e){let t=hr.get(e);t&&(t.flags|=8,hr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var vr=e=>!!e.type.__asyncLoader,yr=e=>e.type.__isKeepAlive,br={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=Pa(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),G(()=>{a.isDeactivated=!1,a.a&&A(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Y(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;na(t.m),na(t.a),l(e,f,null,1,s),G(()=>{t.da&&A(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Y(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Er(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=Ja(vr(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!va(t,o))?p(t):o&&Er(o),i.delete(e),a.delete(e)}Fn(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>xr(e,t)),t&&m(e=>!xr(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(ia(n.subTree.type)?G(()=>{i.set(g,Dr(n.subTree))},n.subTree.suspense):i.set(g,Dr(n.subTree)))};return jr(_),Nr(_),Pr(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Dr(t);if(e.type===i.type&&e.key===i.key){Er(i);let e=i.component.da;e&&G(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!_a(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Dr(r);if(s.type===K)return o=null,s;let c=s.type,l=Ja(vr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!xr(u,l))||d&&l&&xr(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=wa(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&ur(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,ia(r.type)?r:s}}};function xr(e,t){return d(e)?e.some(e=>xr(e,t)):_(e)?e.split(`,`).includes(t):h(e)?(e.lastIndex=0,e.test(t)):!1}function Sr(e,t){wr(e,`a`,t)}function Cr(e,t){wr(e,`da`,t)}function wr(e,t,n=X){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Or(t,r,n),n){let e=n.parent;for(;e&&e.parent;)yr(e.parent.vnode)&&Tr(r,t,n,e),e=e.parent}}function Tr(e,t,n,r){let i=Or(t,e,r,!0);Fr(()=>{c(r[t],i)},n)}function Er(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Dr(e){return e.shapeFlag&128?e.ssContent:e}function Or(e,t,n=X,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||(t.__weh=(...r)=>{Ue();let i=La(n),a=R(t,n,e,r);return i(),We(),a});return r?i.unshift(a):i.push(a),a}}var kr=e=>(t,n=X)=>{(!Ba||e===`sp`)&&Or(e,(...e)=>t(...e),n)},Ar=kr(`bm`),jr=kr(`m`),Mr=kr(`bu`),Nr=kr(`u`),Pr=kr(`bum`),Fr=kr(`um`),Ir=kr(`sp`),Lr=kr(`rtg`),Rr=kr(`rtc`);function zr(e,t=X){Or(`ec`,e,t)}var Br=`components`;function Vr(e,t){return Wr(Br,e,!0,t)||e}var Hr=Symbol.for(`v-ndc`);function Ur(e){return _(e)?Wr(Br,e,!1)||e:e||Hr}function Wr(e,t,n=!0,r=!1){let i=B||X;if(i){let n=i.type;if(e===Br){let e=Ja(n,!1);if(e&&(e===t||e===D(t)||e===re(D(t))))return n}let a=Gr(i[e]||n[e],t)||Gr(i.appContext[e],t);return!a&&r?n:a}}function Gr(e,t){return e&&(e[t]||e[D(t)]||e[re(D(t))])}function Kr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||_(e)){let n=o&&Rt(e),r=!1,s=!1;n&&(r=!P(e),s=zt(e),e=nt(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}var Jr=e=>e?za(e)?qa(e):Jr(e.parent):null,Yr=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{vn(e.update)}),$nextTick:e=>e.n||(e.n=gn.bind(e.proxy)),$watch:e=>Ln.bind(e)}),Xr=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),Zr={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(Xr(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else $r&&(s[n]=0)}let d=Yr[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return Xr(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||Xr(n,c)||u(o,c)||u(i,c)||u(Yr,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function Qr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var $r=!0;function ei(e){let t=ii(e),n=e.proxy,i=e.ctx;$r=!1,t.beforeCreate&&ni(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:h,updated:_,activated:v,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:E,errorCaptured:te,serverPrefetch:D,expose:ne,inheritAttrs:O,components:re,directives:ie,filters:k}=t;if(u&&ti(u,i,null),s)for(let e in s){let t=s[e];g(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);y(t)&&(e.data=Pt(t))}if($r=!0,o)for(let e in o){let t=o[e],a=Z({get:g(t)?t.bind(n,n):g(t.get)?t.get.bind(n,n):r,set:!g(t)&&g(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)ri(c[e],i,n,e);if(l){let e=g(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{An(t,e[t])})}f&&ni(f,e,`c`);function A(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(A(Ar,p),A(jr,m),A(Mr,h),A(Nr,_),A(Sr,v),A(Cr,b),A(zr,te),A(Rr,ee),A(Lr,E),A(Pr,S),A(Fr,w),A(Ir,D),d(ne)){if(ne.length){let t=e.exposed||(e.exposed={});ne.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={})}T&&e.render===r&&(e.render=T),O!=null&&(e.inheritAttrs=O),re&&(e.components=re),ie&&(e.directives=ie),D&&pr(e)}function ti(e,t,n=r){d(e)&&(e=li(e));for(let n in e){let r=e[n],i;i=y(r)?`default`in r?V(r.from||n,r.default,!0):V(r.from||n):V(r),I(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function ni(e,t,n){R(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function ri(e,t,n,r){let i=r.includes(`.`)?Rn(n,r):()=>n[r];if(_(e)){let n=t[e];g(n)&&Fn(i,n)}else if(g(e))Fn(i,e.bind(n));else if(y(e)){if(d(e))e.forEach(e=>ri(e,t,n,r));else{let r=g(e.handler)?e.handler.bind(n):t[e.handler];g(r)&&Fn(i,r,e)}}}function ii(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>ai(c,e,o,!0)),ai(c,t,o)),y(t)&&a.set(t,c),c}function ai(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&ai(e,a,n,!0),i&&i.forEach(t=>ai(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=oi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var oi={data:si,props:di,emits:di,methods:ui,computed:ui,beforeCreate:W,created:W,beforeMount:W,mounted:W,beforeUpdate:W,updated:W,beforeDestroy:W,beforeUnmount:W,destroyed:W,unmounted:W,activated:W,deactivated:W,errorCaptured:W,serverPrefetch:W,components:ui,directives:ui,watch:fi,provide:si,inject:ci};function si(e,t){return t?e?function(){return s(g(e)?e.call(this,this):e,g(t)?t.call(this,this):t)}:t:e}function ci(e,t){return ui(li(e),li(t))}function li(e){if(d(e)){let t={};for(let n=0;n{let l,u=t,d;return Pn(()=>{let t=e[a];k(l,t)&&(l=t,c())}),{get(){return s(),r.get?r.get(l):l},set(e){let s=r.set?r.set(e):e;if(!k(s,l)&&!(u!==t&&k(e,u)))return;let f=i.vnode.props,p=!!(f&&(n in f||a in f||o in f)&&(`onUpdate:${n}`in f||`onUpdate:${a}`in f||`onUpdate:${o}`in f));p||(l=e,c()),i.emit(`update:${n}`,s),k(e,u)&&(k(e,s)&&!k(s,d)||p&&u!==t&&!k(s,l))&&c(),u=e,d=s}}});return c[Symbol.iterator]=()=>{let e=0;return{next(){return e<2?{value:e++?s||t:c,done:!1}:{done:!0}}}},c}var vi=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${D(t)}Modifiers`]||e[`${O(t)}Modifiers`];function yi(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&vi(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>_(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(D(n))];!l&&o&&(l=i[c=ie(O(n))]),l&&R(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,R(u,e,6,a)}}var bi=new WeakMap;function xi(e,t,n=!1){let r=n?bi:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!g(e)){let r=e=>{let n=xi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(y(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),y(e)&&r.set(e,o),o)}function Si(e,t){return!e||!a(t)?!1:(t=t.slice(2),t=t===`Once`?t:t.replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,O(t))||u(e,t))}function Ci(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=En(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Da(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Da(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:wi(c)}}catch(t){la.length=0,cn(t,e,1),v=J(K)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ti(y,a)),b=wa(b,y,!1,!0))}return n.dirs&&(b=wa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&ur(Vn(b.type)&&lr(b)||b,n.transition),v=b,En(_),v}var wi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Ei(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Di(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;tObject.create(Ai),Mi=e=>Object.getPrototypeOf(e)===Ai;function Ni(e,t,n,r=!1){let i={},a=ji();e.propsDefaults=Object.create(null),Fi(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);e.props=n?r?i:Ft(i):e.type.props?i:a,e.attrs=a}function Pi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{p=!0;let[t,n]=Ri(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return y(e)&&a.set(e,n),n;if(d(c))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Vi=e=>d(e)?e.map(Da):[Da(e)],Hi=(e,t,n)=>{if(t._n)return t;let r=Dn((...e)=>Vi(t(...e)),n);return r._c=!1,r},Ui=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Bi(n))continue;let i=e[n];if(g(i))t[n]=Hi(n,i,r);else if(i!=null){let e=Vi(i);t[n]=()=>e}}},Wi=(e,t)=>{let n=Vi(t);e.slots.default=()=>n},Gi=(e,t,n)=>{for(let r in t)(n||!Bi(r))&&(e[r]=t[r])},Ki=(e,t,n)=>{let r=e.slots=ji();if(e.vnode.shapeFlag&32){let e=t._;e?(Gi(r,t,n),n&&ae(r,`_`,e,!0)):Ui(t,r)}else t&&Wi(e,t)},qi=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:Gi(a,n,r):(o=!n.$stable,Ui(n,a)),s=n}else n&&(Wi(e,n),s={default:1});if(o)for(let e in a)!Bi(e)&&s[e]==null&&delete a[e]},G=aa;function Ji(e){return Yi(e)}function Yi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!va(e,t)&&(r=ve(e),pe(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case sa:y(e,t,n,r);break;case K:b(e,t,n,r);break;case ca:e==null&&x(t,n,r,o);break;case oa:re(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?ie(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,xe)}u!=null&&i?gr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&gr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)T(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),D(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},T=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&te(e.children,d,null,r,i,Xi(e,a),s,u),_&&kn(e,null,r,`created`),E(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!ee(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Y(f,r,e)}_&&kn(e,null,r,`beforeMount`);let v=Qi(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&G(()=>{try{f&&Y(f,r,e),v&&g.enter(d),_&&kn(e,null,r,`mounted`)}finally{}},i)},E=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t{for(let l=c;l{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&Zi(r,!1),(g=h.onVnodeBeforeUpdate)&&Y(g,r,n,e),f&&kn(n,e,r,`beforeUpdate`),r&&Zi(r,!0),d&&(!e.dynamicChildren||e.dynamicChildren.length!==d.length)&&(u=0,s=!1,d=null),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?ne(e.dynamicChildren,d,l,r,i,Xi(n,a),o):s||ce(e,n,l,null,r,i,Xi(n,a),o,!1),u>0){if(u&16)O(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t{g&&Y(g,r,n,e),f&&kn(n,e,r,`updated`)},i)},ne=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(n!==r){if(n!==t)for(let t in n)!ee(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(ee(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},re=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),te(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(ne(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&$i(e,t,!0)):ce(e,t,n,f,i,a,s,c,l)},ie=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):ae(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Na(e,r,i);if(yr(e)&&(s.ctx.renderer=xe),Va(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,oe,o),!e.el){let r=s.subTree=J(K);b(null,r,t,n),e.placeholder=r.el}}else oe(s,e,t,n,i,a,o)},ae=(e,t,n)=>{let r=t.component=e.component;if(Ei(e,t,n)){if(r.asyncDep&&!r.asyncResolved){se(r,t,n);return}r.next=t,r.update()}else t.el=e.el,r.vnode=t},oe=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=ta(e);if(n){t&&(t.el=c.el,se(e,t,o)),n.asyncDep.then(()=>{G(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;Zi(e,!1),t?(t.el=c.el,se(e,t,o)):t=c,n&&A(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Y(d,s,t,c),Zi(e,!0);let f=Ci(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ve(p),e,i,a),t.el=f.el,u===null&&ki(e,f.el),r&&G(r,i),(d=t.props&&t.props.onVnodeUpdated)&&G(()=>Y(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=vr(t);if(Zi(e,!1),l&&A(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Y(o,d,t),Zi(e,!0),s&&Ce){let t=()=>{e.subTree=Ci(e),Ce(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Ci(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&G(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;G(()=>Y(o,d,e),i)}(t.shapeFlag&256||d&&vr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&G(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Oe(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>vn(u),Zi(e,!0),l()},se=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Pi(e,t.props,r,n),qi(e,t.children,n),Ue(),xn(e),We()},ce=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){de(l,d,n,r,i,a,o,s,c);return}if(f&256){ue(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&_e(l,i,a),d!==l&&p(n,d)):u&16?m&16?de(l,d,n,r,i,a,o,s,c):_e(l,i,a,!0):(u&8&&p(n,``),m&16&&te(d,n,r,i,a,o,s,c))},ue=(e,t,r,i,a,o,s,c,l)=>{e=e||n,t=t||n;let u=e.length,d=t.length,f=Math.min(u,d),p=0;for(;pd?_e(e,a,o,!0,!1,f):te(t,r,i,a,o,s,c,l,f)},de=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?Oa(t[u]):Da(t[u]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?Oa(t[p]):Da(t[p]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=ep)for(;u<=f;)pe(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?Oa(t[u]):Da(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u=b){pe(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&va(n,t[_])){i=_;break}i===void 0?pe(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?ea(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){fe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,xe);return}if(c===oa){o(a,t,n);for(let e=0;el.enter(a),i));else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{let e=a._isLeaving||!!a[H];a._isLeaving&&a[H](!0),l.persisted&&!e?u():r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}}else o(a,t,n)},pe=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ue(),gr(s,null,n,e,!0),We()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!vr(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Y(_,t,e),u&6)ge(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&kn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,xe,r):l&&!l.hasOnce&&(a!==oa||d>0&&d&64)?_e(l,t,n,!1,!0):(a===oa&&d&384||!i&&u&16)&&_e(c,t,n),r&&me(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&G(()=>{_&&Y(_,t,e),h&&kn(e,null,t,`unmounted`),v&&(e.el=null)},n)},me=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===oa){he(n,r);return}if(t===ca){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},he=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},ge=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;na(c),na(l),r&&A(r),i.stop(),a&&(a.flags|=8,pe(o,e,t,n)),s&&G(s,t),G(()=>{e.isUnmounted=!0},t)},_e=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return ve(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Bn];return n?h(n):t},ye=!1,be=(e,t,n)=>{let r;e==null?t._vnode&&(pe(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ye||(ye=!0,xn(r),Sn(),ye=!1)},xe={p:v,um:pe,m:fe,r:me,mt:k,mc:te,pc:ce,pbc:ne,n:ve,o:e},Se,Ce;return i&&([Se,Ce]=i(xe)),{render:be,hydrate:Se,createApp:hi(be,Se)}}function Xi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function Zi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Qi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function $i(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-->0;)n[a]=o,o=t[o];return n}function ta(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ta(t)}function na(e){if(e)for(let t=0;te.__isSuspense;function aa(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):bn(e)}var oa=Symbol.for(`v-fgt`),sa=Symbol.for(`v-txt`),K=Symbol.for(`v-cmt`),ca=Symbol.for(`v-stc`),la=[],q=null;function ua(e=!1){la.push(q=e?null:[])}function da(){la.pop(),q=la[la.length-1]||null}var fa=1;function pa(e,t=!1){fa+=e,e<0&&q&&t&&(q.hasOnce=!0)}function ma(e){return e.dynamicChildren=fa>0?q||n:null,da(),fa>0&&q&&q.push(e),e}function ha(e,t,n,r,i,a){return ma(xa(e,t,n,r,i,a,!0))}function ga(e,t,n,r,i){return ma(J(e,t,n,r,i,!0))}function _a(e){return e?e.__v_isVNode===!0:!1}function va(e,t){return e.type===t.type&&e.key===t.key}var ya=({key:e})=>e==null?null:e,ba=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:_(e)||I(e)||g(e)?{i:B,r:e,k:t,f:!!n}:e);function xa(e,t=null,n=null,r=0,i=null,a=e===oa?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ya(t),ref:t&&ba(t),scopeId:Tn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:B};return s?(ka(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=_(n)?8:16),fa>0&&!o&&q&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&q.push(c),c}var J=Sa;function Sa(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Hr)&&(e=K),_a(e)){let r=wa(e,t,!0);return n&&ka(r,n),fa>0&&!a&&q&&(r.shapeFlag&6?q[q.indexOf(e)]=r:q.push(r)),r.patchFlag=-2,r}if(Ya(e)&&(e=e.__vccOpts),t){t=Ca(t);let{class:e,style:n}=t;e&&!_(e)&&(t.class=he(e)),y(n)&&(Bt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=_(e)?1:ia(e)?128:Vn(e)?64:y(e)?4:g(e)?2:0;return xa(e,t,n,r,i,o,a,!0)}function Ca(e){return e?Bt(e)||Mi(e)?s({},e):e:null}function wa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Aa(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ya(l),ref:t&&t.ref?n&&a?d(a)?a.concat(ba(t)):[a,ba(t)]:ba(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==oa?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&wa(e.ssContent),ssFallback:e.ssFallback&&wa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&ur(u,c.clone(u)),u}function Ta(e=` `,t=0){return J(sa,null,e,t)}function Ea(e=``,t=!1){return t?(ua(),ga(K,null,e)):J(K,null,e)}function Da(e){return e==null||typeof e==`boolean`?J(K):d(e)?J(oa,null,e.slice()):_a(e)?Oa(e):J(sa,null,String(e))}function Oa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:wa(e)}function ka(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`){if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),ka(e,n()),n._c&&(n._d=!0));return}{n=32;let r=t._;!r&&!Mi(t)?t._ctx=B:r===3&&B&&(B.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}}else if(g(t)){if(r&65){ka(e,{default:t});return}t={default:t,_ctx:B},n=32}else t=String(t),r&64?(n=16,t=[Ta(t)]):n=8;e.children=t,e.shapeFlag|=n}function Aa(...e){let t={};for(let n=0;nX||B,Fa,Ia;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Fa=t(`__VUE_INSTANCE_SETTERS__`,e=>X=e),Ia=t(`__VUE_SSR_SETTERS__`,e=>Ba=e)}var La=e=>{let t=X;return Fa(e),e.scope.on(),()=>{e.scope.off(),Fa(t)}},Ra=()=>{X&&X.scope.off(),Fa(null)};function za(e){return e.vnode.shapeFlag&4}var Ba=!1;function Va(e,t=!1,n=!1){t&&Ia(t);let{props:r,children:i}=e.vnode,a=za(e);Ni(e,r,a,t),Ki(e,i,n||t);let o=a?Ha(e,t):void 0;return t&&Ia(!1),o}function Ha(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zr);let{setup:r}=n;if(r){Ue();let n=e.setupContext=r.length>1?Ka(e):null,i=La(e),a=sn(r,e,0,[e.props,n]),o=b(a);if(We(),i(),(o||e.sp)&&!vr(e)&&pr(e),o){if(a.then(Ra,Ra),t)return a.then(n=>{Ia(!0);try{Ua(e,n,t)}finally{Ia(!1)}}).catch(t=>{cn(t,e,0)});e.asyncDep=a}else Ua(e,a,t)}else Wa(e,t)}function Ua(e,t,n){g(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:y(t)&&(e.setupState=Yt(t)),Wa(e,n)}function Wa(e,t,n){let i=e.type;e.render||(e.render=i.render||r);{let t=La(e);Ue();try{ei(e)}finally{We(),t()}}}var Ga={get(e,t){return N(e,`get`,``),e[t]}};function Ka(e){return{attrs:new Proxy(e.attrs,Ga),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function qa(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Vt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yr)return Yr[n](e)},has(e,t){return t in e||t in Yr}})):e.proxy}function Ja(e,t=!0){return g(e)?e.displayName||e.name:e.name||t&&e.__name}function Ya(e){return g(e)&&`__vccOpts`in e}var Z=(e,t)=>$t(e,t,Ba);function Xa(e,t,n){try{pa(-1);let r=arguments.length;return r===2?y(t)&&!d(t)?_a(t)?J(e,null,[t]):J(e,t):J(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_a(n)&&(n=[n]),J(e,t,n))}finally{pa(1)}}var Za=`3.5.41`,Qa=void 0,$a=typeof window<`u`&&window.trustedTypes;if($a)try{Qa=$a.createPolicy(`vue`,{createHTML:e=>e})}catch{}var eo=Qa?e=>Qa.createHTML(e):e=>e,to=`http://www.w3.org/2000/svg`,no=`http://www.w3.org/1998/Math/MathML`,ro=typeof document<`u`?document:null,io=ro&&ro.createElement(`template`),ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i=t===`svg`?ro.createElementNS(to,e):t===`mathml`?ro.createElementNS(no,e):n?ro.createElement(e,{is:n}):ro.createElement(e);return e===`select`&&r&&r.multiple!=null&&i.setAttribute(`multiple`,r.multiple),i},createText:e=>ro.createTextNode(e),createComment:e=>ro.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ro.querySelector(e),setScopeId(e,t){e.setAttribute(t,``)},insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;if(i&&(i===a||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==a&&(i=i.nextSibling););else{io.innerHTML=eo(r===`svg`?`${e}`:r===`mathml`?`${e}`:e);let i=io.content;if(r===`svg`||r===`mathml`){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},oo=`transition`,so=`animation`,co=Symbol(`_vtc`),lo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uo=s({},tr,lo),fo=(e=>(e.displayName=`Transition`,e.props=uo,e))((e,{slots:t})=>Xa(ar,ho(e),t)),po=(e,t=[])=>{d(e)?e.forEach(e=>e(...t)):e&&e(...t)},mo=e=>e?d(e)?e.some(e=>e.length>1):e.length>1:!1;function ho(e){let t={};for(let n in e)n in lo||(t[n]=e[n]);if(e.css===!1)return t;let{name:n=`v`,type:r,duration:i,enterFromClass:a=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=a,appearActiveClass:u=o,appearToClass:d=c,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,h=go(i),g=h&&h[0],_=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:C=v,onAppear:w=y,onAppearCancelled:T=b}=t,ee=(e,t,n,r)=>{e._enterCancelled=r,yo(e,t?d:c),yo(e,t?u:o),n&&n()},E=(e,t)=>{e._isLeaving=!1,yo(e,f),yo(e,m),yo(e,p),t&&t()},te=e=>(t,n)=>{let i=e?w:y,o=()=>ee(t,e,n);po(i,[t,o]),bo(()=>{yo(t,e?l:a),vo(t,e?d:c),mo(i)||So(t,r,g,o)})};return s(t,{onBeforeEnter(e){po(v,[e]),vo(e,a),vo(e,o)},onBeforeAppear(e){po(C,[e]),vo(e,l),vo(e,u)},onEnter:te(!1),onAppear:te(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>E(e,t);vo(e,f),e._enterCancelled?(vo(e,p),Eo(e)):(Eo(e),vo(e,p)),bo(()=>{e._isLeaving&&(yo(e,f),vo(e,m),mo(x)||So(e,r,_,n))}),po(x,[e,n])},onEnterCancelled(e){ee(e,!1,void 0,!0),po(b,[e])},onAppearCancelled(e){ee(e,!0,void 0,!0),po(T,[e])},onLeaveCancelled(e){E(e),po(S,[e])}})}function go(e){if(e==null)return null;if(y(e))return[_o(e.enter),_o(e.leave)];{let t=_o(e);return[t,t]}}function _o(e){return se(e)}function vo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[co]||(e[co]=new Set)).add(t)}function yo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[co];n&&(n.delete(t),n.size||(e[co]=void 0))}function bo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}var xo=0;function So(e,t,n,r){let i=e._endId=++xo,a=()=>{i===e._endId&&r()};if(n!=null)return setTimeout(a,n);let{type:o,timeout:s,propCount:c}=Co(e,t);if(!o)return r();let l=o+`end`,u=0,d=()=>{e.removeEventListener(l,f),a()},f=t=>{t.target===e&&++u>=c&&d()};setTimeout(()=>{u(n[e]||``).split(`, `),i=r(`${oo}Delay`),a=r(`${oo}Duration`),o=wo(i,a),s=r(`${so}Delay`),c=r(`${so}Duration`),l=wo(s,c),u=null,d=0,f=0;t===oo?o>0&&(u=oo,d=o,f=a.length):t===so?l>0&&(u=so,d=l,f=c.length):(d=Math.max(o,l),u=d>0?o>l?oo:so:null,f=u?u===oo?a.length:c.length:0);let p=u===oo&&/\b(?:transform|all)(?:,|$)/.test(r(`${oo}Property`).toString());return{type:u,timeout:d,propCount:f,hasTransform:p}}function wo(e,t){for(;e.lengthTo(t)+To(e[n])))}function To(e){return e===`auto`?0:Number(e.slice(0,-1).replace(`,`,`.`))*1e3}function Eo(e){return(e?e.ownerDocument:document).body.offsetHeight}function Do(e,t,n){let r=e[co];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==null?e.removeAttribute(`class`):n?e.setAttribute(`class`,t):e.className=t}var Oo=Symbol(`_vod`),ko=Symbol(`_vsh`),Ao={name:`show`,beforeMount(e,{value:t},{transition:n}){e[Oo]=e.style.display===`none`?``:e.style.display,n&&t?n.beforeEnter(e):jo(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),jo(e,!0),r.enter(e)):r.leave(e,()=>{jo(e,!1)}):jo(e,t))},beforeUnmount(e,{value:t}){jo(e,t)}};function jo(e,t){e.style.display=t?e[Oo]:`none`,e[ko]=!t}var Mo=Symbol(``),No=/(?:^|;)\s*display\s*:/;function Po(e,t,n){let r=e.style,i=_(n),a=!1;if(n&&!i){if(t){if(_(t))for(let e of t.split(`;`)){let t=e.slice(0,e.indexOf(`:`)).trim();n[t]==null&&Io(r,t,``)}else for(let e in t)n[e]==null&&Io(r,e,``)}for(let i in n){i===`display`&&(a=!0);let o=n[i];o==null?Io(r,i,``):Bo(e,i,!_(t)&&t?t[i]:void 0,o)||Io(r,i,o)}}else if(i){if(t!==n){let e=r[Mo];e&&(n+=`;`+e),r.cssText=n,a=No.test(n)}}else t&&e.removeAttribute(`style`);Oo in e&&(e[Oo]=a?r.display:``,e[ko]&&(r.display=`none`))}var Fo=/\s*!important$/;function Io(e,t,n){if(d(n))n.forEach(n=>Io(e,t,n));else if(n==null&&(n=``),t.startsWith(`--`))e.setProperty(t,n);else{let r=zo(e,t);Fo.test(n)?e.setProperty(O(r),n.replace(Fo,``),`important`):e[r]=n}}var Lo=[`Webkit`,`Moz`,`ms`],Ro={};function zo(e,t){let n=Ro[t];if(n)return n;let r=D(t);if(r!==`filter`&&r in e)return Ro[t]=r;r=re(r);for(let n=0;nZo||(Qo.then(()=>Zo=0),Zo=Date.now());function es(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=n.attached)return;let r=n.value;if(d(r)){let n=e.stopImmediatePropagation;e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0};let i=r.slice(),a=[e];for(let n=0;ne.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ns=(e,t,n,r,i,s)=>{let c=i===`svg`;t===`class`?Do(e,r,c):t===`style`?Po(e,n,r):a(t)?o(t)||qo(e,t,n,r,s):(t[0]===`.`?(t=t.slice(1),1):t[0]===`^`?(t=t.slice(1),0):rs(e,t,r,c))?(Uo(e,t,r),!e.tagName.includes(`-`)&&(t===`value`||t===`checked`||t===`selected`)&&Ho(e,t,r,c,s,t!==`value`)):e._isVueCE&&(is(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!_(r)))?Uo(e,D(t),r,s,t):(t===`true-value`?e._trueValue=r:t===`false-value`&&(e._falseValue=r),Ho(e,t,r,c))};function rs(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t in e&&ts(t)&&g(n));if(t===`spellcheck`||t===`draggable`||t===`translate`||t===`autocorrect`||t===`sandbox`&&e.tagName===`IFRAME`||t===`form`||t===`list`&&e.tagName===`INPUT`||t===`type`&&e.tagName===`TEXTAREA`)return!1;if(t===`width`||t===`height`){let t=e.tagName;if(t===`IMG`||t===`VIDEO`||t===`CANVAS`||t===`SOURCE`)return!1}return ts(t)&&_(n)?!1:t in e}function is(e,t){let n=e._def.props;if(!n)return!1;let r=D(t);return Array.isArray(n)?n.some(e=>D(e)===r):Object.keys(n).some(e=>D(e)===r)}var as={esc:`escape`,space:` `,up:`arrow-up`,left:`arrow-left`,right:`arrow-right`,down:`arrow-down`,delete:`backspace`},os=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(`.`);return n[r]||(n[r]=(n=>{if(!(`key`in n))return;let r=O(n.key);if(t.some(e=>e===r||as[e]===r))return e(n)}))},ss=s({patchProp:ns},ao),cs;function ls(){return cs||(cs=Ji(ss))}var us=((...e)=>{let t=ls().createApp(...e),{mount:n}=t;return t.mount=e=>{let r=fs(e);if(!r)return;let i=t._component;!g(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent=``);let a=n(r,!1,ds(r));return r instanceof Element&&(r.removeAttribute(`v-cloak`),r.setAttribute(`data-v-app`,``)),a},t});function ds(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLElement==`function`&&e instanceof MathMLElement)return`mathml`}function fs(e){return _(e)?document.querySelector(e):e}function ps(){}var ms=Object.assign,hs=typeof window<`u`,gs=e=>typeof e==`object`&&!!e,_s=e=>e!=null,vs=e=>typeof e==`function`,ys=e=>gs(e)&&vs(e.then)&&vs(e.catch),bs=e=>typeof e==`number`||/^\d+(\.\d+)?$/.test(e),xs=()=>hs?/ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()):!1;function Ss(e,t){let n=t.split(`.`),r=e;return n.forEach(e=>{var t;r=gs(r)?(t=r[e])==null?``:t:``}),r}function Cs(e,t,n){return t.reduce((t,r)=>((!n||e[r]!==void 0)&&(t[r]=e[r]),t),{})}var ws=e=>Array.isArray(e)?e:[e],Ts=[Number,String],Es={type:Boolean,default:!0},Ds=e=>({type:e,required:!0}),Os=e=>({type:Number,default:e}),ks=e=>({type:Ts,default:e}),As=e=>({type:String,default:e}),js=typeof window<`u`;function Ms(e){return js?requestAnimationFrame(e):-1}function Ns(e){js&&cancelAnimationFrame(e)}function Ps(e){Ms(()=>Ms(e))}var Fs=e=>e===window,Is=(e,t)=>({top:0,left:0,right:e,bottom:t,width:e,height:t}),Ls=e=>{let t=L(e);if(Fs(t)){let e=t.innerWidth,n=t.innerHeight;return Is(e,n)}return t!=null&&t.getBoundingClientRect?t.getBoundingClientRect():Is(0,0)};function Rs(e){let t=V(e,null);if(t){let e=Pa(),{link:n,unlink:r,internalChildren:i}=t;return n(e),Fr(()=>r(e)),{parent:t,index:Z(()=>i.indexOf(e))}}return{parent:null,index:Wt(-1)}}function zs(e){let t=[],n=e=>{Array.isArray(e)&&e.forEach(e=>{var r;_a(e)&&(t.push(e),(r=e.component)!=null&&r.subTree&&(t.push(e.component.subTree),n(e.component.subTree.children)),e.children&&n(e.children))})};return n(e),t}var Bs=(e,t)=>{let n=e.indexOf(t);return n===-1?e.findIndex(e=>t.key!==void 0&&t.key!==null&&e.type===t.type&&e.key===t.key):n};function Vs(e,t,n){let r=zs(e.subTree.children);n.sort((e,t)=>Bs(r,e.vnode)-Bs(r,t.vnode));let i=n.map(e=>e.proxy);t.sort((e,t)=>i.indexOf(e)-i.indexOf(t))}function Hs(e){let t=Pt([]),n=Pt([]),r=Pa();return{children:t,linkChildren:i=>{An(e,Object.assign({link:e=>{e.proxy&&(n.push(e),t.push(e.proxy),Vs(r,t,n))},unlink:e=>{let r=n.indexOf(e);t.splice(r,1),n.splice(r,1)},children:t,internalChildren:n},i))}}}function Us(e){let t;jr(()=>{e(),gn(()=>{t=!0})}),Sr(()=>{t&&e()})}function Ws(e,t,n={}){if(!js)return;let{target:r=window,passive:i=!1,capture:a=!1}=n,o=!1,s,c=n=>{if(o)return;let r=L(n);r&&!s&&(r.addEventListener(e,t,{capture:a,passive:i}),s=!0)},l=n=>{if(o)return;let r=L(n);r&&s&&(r.removeEventListener(e,t,a),s=!1)};Fr(()=>l(r)),Cr(()=>l(r)),Us(()=>c(r));let u;return I(r)&&(u=Fn(r,(e,t)=>{l(t),c(e)})),()=>{u==null||u(),l(r),o=!0}}var Gs,Ks;function qs(){if(!Gs&&(Gs=Wt(0),Ks=Wt(0),js)){let e=()=>{Gs.value=window.innerWidth,Ks.value=window.innerHeight};e(),window.addEventListener(`resize`,e,{passive:!0}),window.addEventListener(`orientationchange`,e,{passive:!0})}return{width:Gs,height:Ks}}var Js=/scroll|auto|overlay/i,Ys=js?window:void 0;function Xs(e){return e.tagName!==`HTML`&&e.tagName!==`BODY`&&e.nodeType===1}function Zs(e,t=Ys){let n=e;for(;n&&n!==t&&Xs(n);){let{overflowY:e}=window.getComputedStyle(n);if(Js.test(e))return n;n=n.parentNode}return t}function Qs(e,t=Ys){let n=Wt();return jr(()=>{e.value&&(n.value=Zs(e.value,t))}),n}var $s;function ec(){if(!$s&&($s=Wt(`visible`),js)){let e=()=>{$s.value=document.hidden?`hidden`:`visible`};e(),window.addEventListener(`visibilitychange`,e)}return $s}var tc=Symbol(`van-field`);function nc(e){let t=`scrollTop`in e?e.scrollTop:e.pageYOffset;return Math.max(t,0)}function rc(e,t){`scrollTop`in e?e.scrollTop=t:e.scrollTo(e.scrollX,t)}function ic(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}function ac(e){rc(window,e),rc(document.body,e)}function oc(e,t){if(e===window)return 0;let n=t?nc(t):ic();return Ls(e).top+n}var sc=xs();function cc(){sc&&ac(ic())}var lc=e=>e.stopPropagation();function uc(e,t){(typeof e.cancelable!=`boolean`||e.cancelable)&&e.preventDefault(),t&&lc(e)}function dc(e){let t=L(e);if(!t)return!1;let n=window.getComputedStyle(t),r=n.display===`none`,i=t.offsetParent===null&&n.position!==`fixed`;return r||i}var{width:fc,height:pc}=qs();function mc(e){if(_s(e))return bs(e)?`${e}px`:String(e)}function hc(e){if(_s(e)){if(Array.isArray(e))return{width:mc(e[0]),height:mc(e[1])};let t=mc(e);return{width:t,height:t}}}function gc(e){let t={};return e!==void 0&&(t.zIndex=+e),t}var _c;function vc(){if(!_c){let e=document.documentElement,t=e.style.fontSize||window.getComputedStyle(e).fontSize;_c=parseFloat(t)}return _c}function yc(e){return e=e.replace(/rem/g,``),+e*vc()}function bc(e){return e=e.replace(/vw/g,``),+e*fc.value/100}function xc(e){return e=e.replace(/vh/g,``),+e*pc.value/100}function Sc(e){if(typeof e==`number`)return e;if(hs){if(e.includes(`rem`))return yc(e);if(e.includes(`vw`))return bc(e);if(e.includes(`vh`))return xc(e)}return parseFloat(e)}var Cc=/-(\w)/g,wc=e=>e.replace(Cc,(e,t)=>t.toUpperCase()),Tc=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase().replace(/^-/,``),Ec=(e,t,n)=>Math.min(Math.max(e,t),n);function Dc(e,t,n){let r=e.indexOf(t);return r===-1?e:t===`-`&&r!==0?e.slice(0,r):e.slice(0,r+1)+e.slice(r).replace(n,``)}function Oc(e,t=!0,n=!0){e=t?Dc(e,`.`,/\./g):e.split(`.`)[0],e=n?Dc(e,`-`,/-/g):e.replace(/-/,``);let r=t?/[^-0-9.]/g:/[^-0-9]/g;return e.replace(r,``)}var{hasOwnProperty:kc}=Object.prototype;function Ac(e,t,n){let r=t[n];_s(r)&&(e[n]=!kc.call(e,n)||!gs(r)?r:jc(Object(e[n]),r))}function jc(e,t){return Object.keys(t).forEach(n=>{Ac(e,t,n)}),e}var Mc={name:`姓名`,tel:`电话`,save:`保存`,clear:`清空`,undo:`撤销`,cancel:`取消`,confirm:`确认`,delete:`删除`,loading:`加载中...`,noCoupon:`暂无优惠券`,nameEmpty:`请填写姓名`,addContact:`添加联系人`,telInvalid:`请填写正确的电话`,vanCalendar:{end:`结束`,start:`开始`,title:`日期选择`,weekdays:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],monthTitle:(e,t)=>`${e}\u5E74${t}\u6708`,rangePrompt:e=>`\u6700\u591A\u9009\u62E9 ${e} \u5929`},vanCascader:{select:`请选择`},vanPagination:{prev:`上一页`,next:`下一页`},vanPullRefresh:{pulling:`下拉即可刷新...`,loosing:`释放即可刷新...`},vanSubmitBar:{label:`合计:`},vanCoupon:{unlimited:`无门槛`,discount:e=>`${e}\u6298`,condition:e=>`\u6EE1${e}\u5143\u53EF\u7528`},vanCouponCell:{title:`优惠券`,count:e=>`${e}\u5F20\u53EF\u7528`},vanCouponList:{exchange:`兑换`,close:`不使用`,enable:`可用`,disabled:`不可用`,placeholder:`输入优惠码`},vanAddressEdit:{area:`地区`,areaEmpty:`请选择地区`,addressEmpty:`请填写详细地址`,addressDetail:`详细地址`,defaultAddress:`设为默认收货地址`},vanAddressList:{add:`新增地址`}},Nc=Wt(`zh-CN`),Pc=Pt({"zh-CN":Mc}),Fc={messages(){return Pc[Nc.value]},use(e,t){Nc.value=e,this.add({[e]:t})},add(e={}){jc(Pc,e)}};function Ic(e){let t=wc(e)+`.`;return(e,...n)=>{let r=Fc.messages(),i=Ss(r,t+e)||Ss(r,e);return vs(i)?i(...n):i}}function Lc(e,t){return t?typeof t==`string`?` ${e}--${t}`:Array.isArray(t)?t.reduce((t,n)=>t+Lc(e,n),``):Object.keys(t).reduce((n,r)=>n+(t[r]?Lc(e,r):``),``):``}function Rc(e){return(t,n)=>(t&&typeof t!=`string`&&(n=t,t=``),t=t?`${e}__${t}`:e,`${t}${Lc(t,n)}`)}function zc(e){let t=`van-${e}`;return[t,Rc(t),Ic(t)]}function Bc(e){return e.install=t=>{let{name:n}=e;n&&(t.component(n,e),t.component(wc(`-${n}`),e))},e}var Vc=2e3,Hc=()=>++Vc,Uc=e=>{Vc=e},[Wc,Gc]=zc(`config-provider`),Kc=Symbol(Wc),qc={tag:As(`div`),theme:As(`light`),zIndex:Number,themeVars:Object,themeVarsDark:Object,themeVarsLight:Object,themeVarsScope:As(`local`),iconPrefix:String};function Jc(e){return e.replace(/([a-zA-Z])(\d)/g,`$1-$2`)}function Yc(e){let t={};return Object.keys(e).forEach(n=>{let r=Jc(Tc(n));t[`--van-${r}`]=e[n]}),t}function Xc(e={},t={}){Object.keys(e).forEach(n=>{e[n]!==t[n]&&document.documentElement.style.setProperty(n,e[n])}),Object.keys(t).forEach(t=>{e[t]||document.documentElement.style.removeProperty(t)})}var Zc=Bc(fr({name:Wc,props:qc,setup(e,{slots:t}){let n=Z(()=>Yc(ms({},e.themeVars,e.theme===`dark`?e.themeVarsDark:e.themeVarsLight)));if(hs){let t=()=>{document.documentElement.classList.add(`van-theme-${e.theme}`)},r=(t=e.theme)=>{document.documentElement.classList.remove(`van-theme-${t}`)};Fn(()=>e.theme,(e,n)=>{n&&r(n),t()},{immediate:!0}),Sr(t),Cr(r),Pr(r),Fn(n,(t,n)=>{e.themeVarsScope===`global`&&Xc(t,n)}),Fn(()=>e.themeVarsScope,(e,t)=>{t===`global`&&Xc({},n.value),e===`global`&&Xc(n.value,{})}),e.themeVarsScope===`global`&&Xc(n.value,{})}return An(Kc,e),Nn(()=>{e.zIndex!==void 0&&Uc(e.zIndex)}),()=>J(e.tag,{class:Gc(),style:e.themeVarsScope===`local`?n.value:void 0},{default:()=>{var e;return[(e=t.default)==null?void 0:e.call(t)]}})}}));function Qc(e=`(min-width: 768px)`){let t=typeof window<`u`&&window.matchMedia?window.matchMedia(e):null,n=Wt(t?t.matches:!1),r=e=>{n.value=e.matches};return jr(()=>{var e;return t==null||(e=t.addEventListener)==null?void 0:e.call(t,`change`,r)}),Pr(()=>{var e;return t==null||(e=t.removeEventListener)==null?void 0:e.call(t,`change`,r)}),n}function $c(){let e=typeof window<`u`&&window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,t=Wt(e?e.matches:!1),n=e=>{t.value=e.matches};return jr(()=>{var t;return e==null||(t=e.addEventListener)==null?void 0:t.call(e,`change`,n)}),Pr(()=>{var t;return e==null||(t=e.removeEventListener)==null?void 0:t.call(e,`change`,n)}),t}function el(e,t){let n,r=!1,i=()=>{n!==void 0&&window.clearInterval(n),n=void 0},a=()=>{i(),n=window.setInterval(()=>{document.visibilityState===`visible`&&e()},t)},o=()=>{r&&(document.visibilityState===`visible`?(e(),a()):i())};jr(()=>{r=!0,a(),document.addEventListener(`visibilitychange`,o)}),Sr(()=>{r||(r=!0,e(),a())}),Cr(()=>{r=!1,i()}),Pr(()=>{r=!1,i(),document.removeEventListener(`visibilitychange`,o)})}var tl=fr({__name:`App`,setup(e){let t=$c(),n=Z(()=>t.value?`dark`:`light`),r={primaryColor:`#2f6fff`,successColor:`#13b77a`,warningColor:`#e8870c`,dangerColor:`#ff5358`,navBarBackground:`var(--app-bg)`,navBarTitleTextColor:`var(--app-text)`,cellGroupInsetRadius:`14px`,buttonRadius:`12px`,buttonLargeHeight:`48px`},i={primaryColor:`#5b8cff`,successColor:`#2fd19a`,warningColor:`#ffae4a`,dangerColor:`#ff6b70`,background:`#0b1222`,background2:`#131c31`,background3:`#18223b`,textColor:`#e6ecfb`,textColor2:`#8b97b6`,textColor3:`#6c7898`,borderColor:`rgba(140,160,210,.16)`,activeColor:`#1c2743`};return(e,t)=>{let a=Vr(`RouterView`);return ua(),ga(L(Zc),{theme:n.value,"theme-vars-scope":`global`,"theme-vars":r,"theme-vars-dark":i},{default:Dn(()=>[J(a)]),_:1},8,[`theme`])}}});function nl(e){"@babel/helpers - typeof";return nl=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},nl(e)}function rl(e,t){if(nl(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(nl(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function il(e){var t=rl(e,`string`);return nl(t)==`symbol`?t:t+``}function al(e,t,n){return(t=il(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ol(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function sl(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&ol(e.default)}var Q=Object.assign;function cl(e,t){let n={};for(let r in t){let i=t[r];n[r]=ul(i)?i.map(e):e(i)}return n}var ll=()=>{},ul=Array.isArray;function dl(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var fl=Symbol(``);function pl(e,t){return Q(Error(),{type:e,[fl]:!0},t)}function ml(e,t){return e instanceof Error&&fl in e&&(t==null||!!(e.type&t))}var hl=Symbol(``),gl=Symbol(``),_l=Symbol(``),vl=Symbol(``),yl=Symbol(``);function bl(){return V(_l)}function xl(e){return V(vl)}var Sl=typeof document<`u`,Cl=/#/g,wl=/&/g,Tl=/\//g,El=/=/g,Dl=/\?/g,Ol=/\+/g,kl=/%5B/g,Al=/%5D/g,jl=/%5E/g,Ml=/%60/g,Nl=/%7B/g,Pl=/%7C/g,Fl=/%7D/g,Il=/%20/g;function Ll(e){return e==null?``:encodeURI(``+e).replace(Pl,`|`).replace(kl,`[`).replace(Al,`]`)}function Rl(e){return Ll(e).replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function zl(e){return Ll(e).replace(Ol,`%2B`).replace(Il,`+`).replace(Cl,`%23`).replace(wl,`%26`).replace(Ml,"`").replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function Bl(e){return zl(e).replace(El,`%3D`)}function Vl(e){return Ll(e).replace(Cl,`%23`).replace(Dl,`%3F`)}function Hl(e){return Vl(e).replace(Tl,`%2F`)}function Ul(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var Wl=/\/$/,Gl=e=>e.replace(Wl,``);function Kl(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r=r||t.slice(0,s),o=t.slice(s,t.length)),r=eu(r==null?t:r,n),{fullPath:r+a+o,path:r,query:i,hash:Ul(o)}}function ql(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Jl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function Yl(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&Xl(t.matched[r],n.matched[i])&&Zl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Xl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Zl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ql(e[n],t[n]))return!1;return!0}function Ql(e,t){return ul(e)?$l(e,t):ul(t)?$l(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function $l(e,t){return ul(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function eu(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o1&&a--;else break}return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var tu={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0};function nu(e){if(!e){if(Sl){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^/]+/,``)}else e=`/`}return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Gl(e)}var ru=/^[^#]+#/;function iu(e,t){return e.replace(ru,`#`)+t}function au(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var ou=()=>({left:window.scrollX,top:window.scrollY});function su(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=au(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function cu(e,t){return(history.state?history.state.position-t:-1)+e}var lu=new Map;function uu(e,t){lu.set(e,t)}function du(e){let t=lu.get(e);return lu.delete(e),t}function fu(e){return typeof e==`string`||e&&typeof e==`object`}function pu(e){return typeof e==`string`||typeof e==`symbol`}function mu(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;ee&&zl(e)):[r&&zl(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function gu(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=ul(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function _u(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function vu(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(pl(4,{from:n,to:t})):e instanceof Error?c(e):fu(e)?c(pl(2,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function yu(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(t===`beforeRouteEnter`||o.instances[e]){if(ol(s)){let c=(s.__vccOpts||s)[t];c&&a.push(vu(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=sl(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&vu(c,n,r,o,e,i)()}))}}}return a}function bu(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;oXl(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>Xl(e,s))||i.push(s))}return[n,r,i]}var xu=()=>location.protocol+`//`+location.host;function Su(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Jl(n,``)}return Jl(n,e)+r+i}function Cu(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=Su(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:`pop`,direction:u?u>0?`forward`:`back`:``})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(Q({},e.state,{scroll:ou()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function wu(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ou():null}}function Tu(e){let{history:t,location:n}=window,r={value:Su(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:xu()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,Q({},t.state,wu(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=Q({},i.value,t.state,{forward:e,scroll:ou()});a(o.current,o,!0),a(e,Q({},wu(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function Eu(e){e=nu(e);let t=Tu(e),n=Cu(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=Q({location:``,base:e,go:r,createHref:iu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Du(e){return e=location.host?e||location.pathname+location.search:``,e.includes(`#`)||(e+=`#`),Eu(e)}var Ou={type:0,value:``},ku=/[a-zA-Z0-9_]/;function Au(e){if(!e)return[[]];if(e===`/`)return[[Ou]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=0,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&(n===0?a.push({type:0,value:l}):n===1||n===2||n===3?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:1,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),l=``)}function f(){l+=c}for(;st.length?t.length===1&&t[0]===80?1:-1:0}function Iu(e,t){let n=0,r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}var Ru={strict:!1,end:!0,sensitive:!1};function zu(e,t,n){let r=Q(Pu(Au(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Bu(e,t){let n=[],r=new Map;t=dl(Ru,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Hu(e);s.aliasOf=r&&r.record;let l=dl(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Hu(Q({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=zu(t,n,l),r?r.alias.push(d):(f=f||d,f!==d&&f.alias.push(d),i&&e.name&&!Wu(d)&&o(e.name)),Ju(d)&&c(d),s.children){let e=s.children;for(let t=0;t{o(f)}:ll}function o(e){if(pu(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=Ku(e,n);n.splice(t,0,e),e.record.name&&!Wu(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw pl(1,{location:e});s=i.record.name,a=Q(Vu(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&Vu(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name,i.keys.forEach(e=>{e.optional&&!a[e.name]&&delete a[e.name]}));else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw pl(1,{location:e,currentLocation:t});s=i.record.name,a=Q({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Gu(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function Vu(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Hu(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Uu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Uu(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Wu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Gu(e){return e.reduce((e,t)=>Q(e,t.meta),{})}function Ku(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Iu(e,t[i])<0?r=i:n=i+1}let i=qu(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function qu(e){let t=e;for(;t=t.parent;)if(Ju(t)&&Iu(e,t)===0)return t}function Ju({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Yu(e){let t=V(_l),n=V(vl),r=Z(()=>{let n=L(e.to);return t.resolve(n)}),i=Z(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(Xl.bind(null,i));if(o>-1)return o;let s=ed(e[t-2]);return t>1&&ed(i)===s&&a[a.length-1].path!==s?a.findIndex(Xl.bind(null,e[t-2])):o}),a=Z(()=>i.value>-1&&$u(n.params,r.value.params)),o=Z(()=>i.value>-1&&i.value===n.matched.length-1&&Zl(n.params,r.value.params));function s(n={}){if(Qu(n)){let n=t[L(e.replace)?`replace`:`push`](L(e.to)).catch(ll);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:Z(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Xu(e){return e.length===1?e[0]:e}var Zu=fr({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:Yu,setup(e,{slots:t}){let n=Pt(Yu(e)),{options:r}=V(_l),i=Z(()=>({[td(e.activeClass,r.linkActiveClass,`router-link-active`)]:n.isActive,[td(e.exactActiveClass,r.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let r=t.default&&Xu(t.default(n));return e.custom?r:Xa(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}});function Qu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(e.button===void 0||e.button===0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function $u(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!ul(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function ed(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var td=(e,t,n)=>e==null?t==null?n:t:e,nd=fr({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let r=V(yl),i=Z(()=>e.route||r.value),a=V(gl,0),o=Z(()=>{let e=L(a),{matched:t}=i.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),s=Z(()=>i.value.matched[o.value]);An(gl,Z(()=>o.value+1)),An(hl,s),An(yl,i);let c=Wt();return Fn(()=>[c.value,s.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!Xl(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let r=i.value,a=e.name,o=s.value,l=o&&o.components[a];if(!l)return rd(n.default,{Component:l,route:r});let u=o.props[a],d=Xa(l,Q({},u?u===!0?r.params:typeof u==`function`?u(r):u:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(o.instances[a]=null)},ref:c}));return rd(n.default,{Component:d,route:r})||d}}});function rd(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var id=nd;function ad(e){let t=Bu(e.routes,e),n=e.parseQuery||mu,r=e.stringifyQuery||hu,i=e.history,a=_u(),o=_u(),s=_u(),c=Gt(tu),l=tu;Sl&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let u=cl.bind(null,e=>``+e),d=cl.bind(null,Hl),f=cl.bind(null,Ul);function p(e,n){let r,i;return pu(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function m(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function h(){return t.getRoutes().map(e=>e.record)}function g(e){return!!t.getRecordMatcher(e)}function _(e,a){if(a=Q({},a||c.value),typeof e==`string`){let r=Kl(n,e,a.path),o=t.resolve({path:r.path},a),s=i.createHref(r.fullPath);return Q(r,o,{params:f(o.params),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=Q({},e,{path:Kl(n,e.path,a.path).path});else{let t=Q({},e.params);for(let e in t)t[e]==null&&delete t[e];o=Q({},e,{params:d(t)}),a.params=d(a.params)}let s=t.resolve(o,a),l=e.hash||``;s.params=u(f(s.params));let p=ql(r,Q({},e,{hash:Rl(l),path:s.path})),m=i.createHref(p);return Q({fullPath:p,hash:l,query:r===hu?gu(e.query):e.query||{}},s,{redirectedFrom:void 0,href:m})}function v(e){return typeof e==`string`?Kl(n,e,c.value.path):Q({},e)}function y(e,t){if(l!==e)return pl(8,{from:t,to:e})}function b(e){return C(e)}function x(e){return b(Q(v(e),{replace:!0}))}function S(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=v(i):{path:i},i.params={}),Q({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function C(e,t){let n=l=_(e),i=c.value,a=e.state,o=e.force,s=e.replace===!0,u=S(n,i);if(u)return C(Q(v(u),{state:typeof u==`object`?Q({},a,u.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&Yl(r,i,n)&&(f=pl(16,{to:d,from:i}),oe(i,i,!0,!1)),(f?Promise.resolve(f):ee(d,i)).catch(e=>ml(e)?ml(e,2)?e:ae(e):k(e,d,i)).then(e=>{if(e){if(ml(e,2))return C(Q({replace:s},v(e.to),{state:typeof e.to==`object`?Q({},a,e.to.state):a,force:o}),t||d)}else e=te(d,i,!0,s,a);return E(d,i,e),e})}function w(e,t){let n=y(e,t);return n?Promise.reject(n):Promise.resolve()}function T(e){let t=le.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function ee(e,t){let n,[r,i,s]=bu(e,t);n=yu(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(vu(r,e,t))});let c=w.bind(null,e,t);return n.push(c),de(n).then(()=>{n=[];for(let r of a.list())n.push(vu(r,e,t));return n.push(c),de(n)}).then(()=>{n=yu(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(vu(r,e,t))});return n.push(c),de(n)}).then(()=>{n=[];for(let r of s)if(r.beforeEnter){if(ul(r.beforeEnter))for(let i of r.beforeEnter)n.push(vu(i,e,t));else n.push(vu(r.beforeEnter,e,t))}return n.push(c),de(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=yu(s,`beforeRouteEnter`,e,t,T),n.push(c),de(n))).then(()=>{n=[];for(let r of o.list())n.push(vu(r,e,t));return n.push(c),de(n)}).catch(e=>ml(e,8)?e:Promise.reject(e))}function E(e,t,n){s.list().forEach(r=>T(()=>r(e,t,n)))}function te(e,t,n,r,a){let o=y(e,t);if(o)return o;let s=t===tu,l=Sl?history.state:{};n&&(r||s?i.replace(e.fullPath,Q({scroll:s&&l&&l.scroll},a)):i.push(e.fullPath,a)),c.value=e,oe(e,t,n,s),ae()}let D;function ne(){D||(D=i.listen((e,t,n)=>{if(!ue.listening)return;let r=_(e),a=S(r,ue.currentRoute.value);if(a){C(Q(a,{replace:!0,force:!0}),r).catch(ll);return}l=r;let o=c.value;Sl&&uu(cu(o.fullPath,n.delta),ou()),ee(r,o).catch(e=>ml(e,12)?e:ml(e,2)?(C(Q(v(e.to),{force:!0}),r).then(e=>{ml(e,20)&&!n.delta&&n.type===`pop`&&i.go(-1,!1)}).catch(ll),Promise.reject()):(n.delta&&i.go(-n.delta,!1),k(e,r,o))).then(e=>{e=e||te(r,o,!1),e&&(n.delta&&!ml(e,8)?i.go(-n.delta,!1):n.type===`pop`&&ml(e,20)&&i.go(-1,!1)),E(r,o,e)}).catch(ll)}))}let O=_u(),re=_u(),ie;function k(e,t,n){ae(e);let r=re.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function A(){return ie&&c.value!==tu?Promise.resolve():new Promise((e,t)=>{O.add([e,t])})}function ae(e){return ie||(ie=!e,ne(),O.list().forEach(([t,n])=>e?n(e):t()),O.reset()),e}function oe(t,n,r,i){let{scrollBehavior:a}=e;if(!Sl||!a)return Promise.resolve();let o=!r&&du(cu(t.fullPath,0))||(i||!r)&&history.state&&history.state.scroll||null;return gn().then(()=>a(t,n,o)).then(e=>t===c.value&&e&&su(e)).catch(e=>t===c.value&&k(e,t,n))}let se=e=>i.go(e),ce,le=new Set,ue={currentRoute:c,listening:!0,addRoute:p,removeRoute:m,clearRoutes:t.clearRoutes,hasRoute:g,getRoutes:h,resolve:_,options:e,push:b,replace:x,go:se,back:()=>se(-1),forward:()=>se(1),beforeEach:a.add,beforeResolve:o.add,afterEach:s.add,onError:re.add,isReady:A,install(e){e.component(`RouterLink`,Zu),e.component(`RouterView`,id),e.config.globalProperties.$router=ue,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>L(c)}),Sl&&!ce&&c.value===tu&&(ce=!0,b(i.location).catch(e=>{}));let t={};for(let e in tu)Object.defineProperty(t,e,{get:()=>c.value[e],enumerable:!0});e.provide(_l,ue),e.provide(vl,Ft(t)),e.provide(yl,c);let n=e.unmount;le.add(e),e.unmount=function(){le.delete(e),le.size<1&&(l=tu,D&&D(),D=null,c.value=tu,ce=!1,ie=!1),n()}}};function de(e){return e.reduce((e,t)=>e.then(()=>T(t)),Promise.resolve())}return ue}var od=`/api/v2/mobile`,sd=`wecom-mobile-token`,cd=class extends Error{constructor(e,t,n,r=0){super(n),al(this,`status`,void 0),al(this,`code`,void 0),al(this,`retryAfter`,void 0),this.status=e,this.code=t,this.retryAfter=r}},ld=null;function ud(e){ld=e}function dd(){try{return window.localStorage.getItem(sd)||``}catch{return``}}function fd(e){try{e?window.localStorage.setItem(sd,e):window.localStorage.removeItem(sd)}catch{}}var pd=``;function md(){return pd||dd()}function hd(e){pd=e,fd(e)}function gd(e,t){let n=t&&typeof t==`object`&&`detail`in t?t.detail:t;if(n&&typeof n==`object`&&!Array.isArray(n)){let t=n;return new cd(e,String(t.code||`http_${e}`),String(t.message||_d(e)),Number(t.retry_after||0))}return Array.isArray(n)?new cd(e,`validation`,`填写的内容不符合要求,请检查后重试`):typeof n==`string`&&n.trim()?new cd(e,`http_${e}`,n.trim()):new cd(e,`http_${e}`,_d(e))}function _d(e){return e===401?`登录已失效,请重新登录`:e===403?`没有权限执行这个操作`:e===404?`内容不存在或已被删除`:e===429?`操作太频繁,请稍后再试`:e>=500?`服务暂时不可用,请稍后重试`:`请求失败(HTTP ${e})`}async function vd(e,t,n={}){let{body:r,query:i,auth:a=!0,signal:o}=n,s=new URLSearchParams;for(let[e,t]of Object.entries(i||{}))t!=null&&t!==``&&s.set(e,String(t));let c=`${od}${t}${s.toString()?`?${s}`:``}`,l={Accept:`application/json`};r!==void 0&&(l[`Content-Type`]=`application/json`);let u=md();a&&u&&(l.Authorization=`Bearer ${u}`);let d;try{d=await fetch(c,{method:e,headers:l,body:r===void 0?void 0:JSON.stringify(r),signal:o,cache:`no-store`,credentials:`omit`})}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?e:new cd(0,`network`,`网络连接失败,请检查网络后重试`)}let f=null,p=await d.text();if(p)try{f=JSON.parse(p)}catch{f=null}if(!d.ok){let e=gd(d.status,f);throw d.status===401&&a&&(ld==null||ld()),e}return f}var yd=e=>encodeURIComponent(e),bd={login(e,t,n){return vd(`POST`,`/auth/login`,{auth:!1,body:{account:e,password:t,client_name:n}})},logout(){return vd(`POST`,`/auth/logout`)},me(){return vd(`GET`,`/me`)},accounts(e=``){return vd(`GET`,`/accounts`,{query:{q:e}})},desktops(e){return vd(`GET`,`/desktops`,{query:{account_id:e}})},sendCommand(e,t,n){return vd(`POST`,`/desktops/${yd(e)}/commands`,{body:{type:t},query:{account_id:n}})},command(e){return vd(`GET`,`/commands/${yd(e)}`)},conversations(e){return vd(`GET`,`/conversations`,{query:{account_id:e.accountId,q:e.q,scope:e.scope,cursor:e.cursor,limit:e.limit},signal:e.signal})},block(e,t,n){return vd(`PUT`,`/blacklist/${yd(e.account)}/${yd(e.conversation)}`,{body:{name:e.name||``,account_name:e.account_name||``,reason:t},query:{account_id:n}})},unblock(e,t,n){return vd(`DELETE`,`/blacklist/${yd(e)}/${yd(t)}`,{query:{account_id:n}})}},xd=`wecom-mobile-view-account`;function Sd(){try{let e=window.localStorage.getItem(xd),t=e?JSON.parse(e):null;return t&&typeof t.id==`number`?t:null}catch{return null}}var $=Pt({token:md(),me:null,viewed:Sd()}),Cd=Z(()=>{var e;return(e=$.me)==null?void 0:e.permissions}),wd=Z(()=>{let e=$.me,t=$.viewed;if(e&&t&&e.permissions.control_all&&t.id!==e.account.id)return t.id}),Td=Z(()=>{var e,t;return wd.value!==void 0&&$.viewed?$.viewed.nickname||$.viewed.sn||`客服 #${$.viewed.id}`:((e=$.me)==null?void 0:e.account.nickname)||((t=$.me)==null?void 0:t.account.sn)||``});function Ed(e){$.viewed=e?{id:e.id,nickname:e.nickname,sn:e.sn}:null;try{$.viewed?window.localStorage.setItem(xd,JSON.stringify($.viewed)):window.localStorage.removeItem(xd)}catch{}}function Dd(e,t){hd(e),$.token=e,$.me=t,t.permissions.control_all||Ed(null)}function Od(){hd(``),$.token=``,$.me=null,Ed(null)}async function kd(){let e=await bd.me();return $.me=e,e.permissions.control_all||Ed(null),e}async function Ad(){try{await bd.logout()}catch{}Od()}var jd=`modulepreload`,Md=function(e){return`/m/`+e},Nd={},Pd=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute(`nonce`));function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=Md(t,n),t=s(t),t in Nd)return;Nd[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:jd,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Fd=ad({history:Du(),routes:[{path:`/login`,name:`login`,component:()=>Pd(()=>import(`./LoginView-nW7aCAJ5.js`),__vite__mapDeps([0,1,2,3,4])),meta:{public:!0}},{path:`/`,component:()=>Pd(()=>import(`./ShellView-CQXCC9xU.js`),__vite__mapDeps([5,1,6,7])),children:[{path:``,redirect:{name:`desktops`}},{path:`desktops`,name:`desktops`,component:()=>Pd(()=>import(`./DevicesView-Cgb9V9Z0.js`),__vite__mapDeps([8,1,9,6,2,10,3,11,12]))},{path:`conversations`,name:`conversations`,component:()=>Pd(()=>import(`./ConversationsView-CLYafisP.js`),__vite__mapDeps([13,1,6,9,2,3,10,11,14]))},{path:`me`,name:`me`,component:()=>Pd(()=>import(`./MeView-uJ3uOzy0.js`),__vite__mapDeps([15,1,9,6,2,16]))}]},{path:`/:pathMatch(.*)*`,redirect:{name:`desktops`}}]});Fd.beforeEach(async e=>{if(e.meta.public)return!0;if(!$.token)return{name:`login`,query:{redirect:e.fullPath}};if(!$.me)try{await kd()}catch(t){if(t instanceof cd&&t.status===401)return Od(),{name:`login`,query:{redirect:e.fullPath}}}return!0}),ud(()=>{Od();let e=Fd.currentRoute.value;e.matched.length&&e.name!==`login`&&Fd.replace({name:`login`,query:{redirect:e.fullPath,expired:`1`}})});var Id=`wecom-mobile-chunk-reload`;Fd.onError((e,t)=>{let n=String((e==null?void 0:e.message)||e);if(!/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(n))return;let r=0;try{r=Number(window.sessionStorage.getItem(Id)||0),window.sessionStorage.setItem(Id,String(Date.now()))}catch{}Date.now()-r>1e4&&(window.location.assign(`/m/#${t.fullPath}`),window.location.reload())}),us(tl).use(Fd).mount(`#app`),`serviceWorker`in navigator&&window.isSecureContext&&window.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/m/sw.js`,{scope:`/m/`}).catch(()=>void 0)});export{hs as $,ac as A,Mr as At,Hs as B,_i as Bt,Sc as C,fr as Ct,dc as D,gn as Dt,nc as E,Aa as Et,Ns as F,ua as Ft,Qs as G,Pt as Gt,ec as H,Nn as Ht,Ps as I,An as It,Ds as J,he as Jt,Os as K,Wt as Kt,Zs as L,Kr as Lt,pc as M,jr as Mt,fc as N,Fr as Nt,uc as O,Sr as Ot,tc as P,Nr as Pt,ms as Q,Us as R,Vr as Rt,gc as S,J as St,ic as T,V as Tt,Rs as U,Dn as Ut,Ws as V,Fn as Vt,Ls as W,On as Wt,Ts as X,As as Y,Se as Yt,Es as Z,zc as _,ga as _t,Ed as a,ps as at,Oc as b,qr as bt,cd as c,fo as ct,bl as d,os as dt,_s as et,el as f,oa as ft,Bc as g,xa as gt,Hc as h,Z as ht,Ad as i,ys as it,rc as j,Cr as jt,cc as k,Pr as kt,bd as l,us as lt,Kc as m,Xn as mt,$ as n,bs as nt,wd as o,Cs as ot,Qc as p,br as pt,ks as q,L as qt,Dd as r,gs as rt,Td as s,ws as st,Cd as t,vs as tt,xl as u,Ao as ut,mc as v,Ea as vt,oc as w,Pa as wt,hc as x,Ta as xt,Ec as y,ha as yt,Ms as z,Ur as zt}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js new file mode 100644 index 000000000..8d5898bfb --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js @@ -0,0 +1 @@ +import{c as e}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{Dt as t,Kt as n,M as r,Mt as i,N as a,St as o,Vt as s,W as c,at as l,it as u}from"./index-Gkjcdjlo.js";function d(e,{args:t=[],done:n,canceled:r,error:i}){if(e){let a=e.apply(null,t);u(a)?a.then(e=>{e?n():r&&r()}).catch(i||l):a?n():r&&r()}else n()}var f=(o,l)=>{let u=n(),d=()=>{u.value=c(o).height};return i(()=>{if(t(d),l)for(let e=1;e<=3;e++)setTimeout(d,100*e)}),e(()=>t(d)),s([a,r],d),u};function p(e,t){let n=f(e,!0);return e=>o(`div`,{class:t(`placeholder`),style:{height:n.value?`${n.value}px`:void 0}},[e()])}export{d as n,p as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png differ diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/index.html b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/index.html new file mode 100644 index 000000000..19d8bc70a --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + + + +
+ + + diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest new file mode 100644 index 000000000..2376a055c --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话黑名单。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/sw.js b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/admin-web/apps/mobile-h5/dist/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/deploy/mobile-remote-20260928/payload/deploy/im-admin/Dockerfile b/deploy/mobile-remote-20260928/payload/deploy/im-admin/Dockerfile new file mode 100644 index 000000000..36761e36f --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/deploy/im-admin/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +COPY deploy/im-admin/requirements.txt /tmp/requirements.txt +RUN pip install --disable-pip-version-check -r /tmp/requirements.txt \ + && useradd --create-home --uid 10001 --shell /usr/sbin/nologin app + +COPY --chown=app:app wechat_rpa/*.py /app/wechat_rpa/ +COPY --chown=app:app admin-web/apps/web-antd/dist/ /app/admin-web/apps/web-antd/dist/ +# 手机远程控制台(admin-web/apps/mobile-h5,独立项目),由 admin_api.py 托管在 /m/ +COPY --chown=app:app admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ + +USER app +WORKDIR /app/wechat_rpa + +EXPOSE 8766 8770 + +CMD ["python", "admin_api.py", "--db", "/data/backend.db", "--host", "0.0.0.0", "--port", "8766"] diff --git a/deploy/mobile-remote-20260928/payload/wechat_rpa/admin_api.py b/deploy/mobile-remote-20260928/payload/wechat_rpa/admin_api.py new file mode 100644 index 000000000..e8f8077ae --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/wechat_rpa/admin_api.py @@ -0,0 +1,1630 @@ +# -*- coding: utf-8 -*- +"""管理后台 JSON API(FastAPI)。 + +当初另起一个,是因为老的 `admin_backend.py` 是服务端渲染的 HTML 后台——表单 POST +加整页刷新。Vue 前端要的是 JSON API + 明确的权限清单,两种形态没法在一个处理器 +里长期共存。 + +绞杀者模式已经走完:老网页后台(8765)整体退役,`admin_backend.py` 只剩数据层 +(Database / 配置校验 / 模型测试),这个模块是唯一的 HTTP 入口。 + + admin_backend.py 数据层,被本模块和 model_gateway.py 共用 + admin_api.py JSON API + Vue 前端静态托管 + 桌面端同步,全在这一个 + +权限的判法只有一种:`Depends(require("model:write"))`。代码里**永远不判角色名** +——判角色名就等于把运营策略焊死在代码里,加个角色都得发版。 + +跑起来: + uvicorn admin_api:create_app --factory --host 127.0.0.1 --port 8766 + # 或 + python admin_api.py --db backend.db --port 8766 +""" + +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import hmac +import json +import os +import re +import time +import urllib.error +import urllib.parse +from pathlib import Path +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, RedirectResponse +from pydantic import BaseModel, Field, field_validator + +import admin_backend as backend +import app_version +import archive_api +import knowledge_api +import remote_api +import zyt_auth + +# 账号下拉框里「全部」的取值。用 -1 而不是 0:0 早就有含义了(旧版未归属数据), +# 而"不传"在 FastAPI 里就是 None,同样已经被占用。 +ALL_ACCOUNTS = -1 + +# Vue 开发服务器的默认端口。生产环境前端和 API 同源,这些只在开发时用得上。 +DEV_ORIGINS = [ + "http://localhost:5173", "http://127.0.0.1:5173", + "http://localhost:3000", "http://127.0.0.1:3000", +] + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class LoginBody(BaseModel): + username: str + password: str + device_name: str = "web" + + +class PasswordBody(BaseModel): + current_password: str + new_password: str + + +class RoleBody(BaseModel): + code: str + name: str = "" + permissions: list[str] = Field(default_factory=list) + + +class UserBody(BaseModel): + username: str = "" + password: str = "" + role: str = "viewer" + active: bool = True + + +class DesktopExchangeBody(BaseModel): + device_id: str = Field(min_length=8, max_length=128) + device_name: str = Field(default="", max_length=120) + app_version: str = Field(default="", max_length=64) + interactive_login: bool = False + + +class DesktopZytSessionBody(BaseModel): + zyt_token: str = Field(min_length=1, max_length=8192, repr=False) + + +class DesktopAccountBody(BaseModel): + status: str = "active" + backup_enabled: bool | None = None + backup_interval_minutes: int | None = Field(default=None, ge=1, le=10080) + + +class DesktopAccountScopeBody(BaseModel): + admin_user_ids: list[int] = Field(default_factory=list) + + +class ProviderBody(BaseModel): + id: str + name: str = "" + kind: str + base_url: str + # auto:按接口类型补全路径;exact:地址原样使用,一个字符不加 + endpoint_mode: str = "auto" + api_key: str = "" # 留空 = 保持原密钥不动 + model: str = "" + capabilities: str = "text" + max_tokens: int = 500 + temperature: float = 0.35 + timeout_ms: int = 30000 + max_inflight: int = 32 + rpm_limit: int = 0 + enabled: bool = True + + +class RolesPlanBody(BaseModel): + answer_ids: str + judge_id: str = "" + vision_id: str = "" + fallback_ids: str = "" + judge_mode: str = "shadow" + + +class AgentRuleBody(BaseModel): + """一条角色规则。 + + 三种类型的必填项不同,逐条校验在 `admin_backend.validate_agent_rules` 里—— + 这里只保证形状是个对象、字段名对得上。把"guide 必须有 instruction"这种规则 + 写进 Pydantic,等于同一套约束维护两份,迟早对不上。 + """ + + id: str = "" + label: str = "" + # guide 补充指令 / forbid 禁止措辞 / reply 固定口径 + type: str = "guide" + keywords: list[str] = Field(default_factory=list) + instruction: str = "" + reply: str = "" + enabled: bool = True + + +class AgentBody(BaseModel): + """一个智能体 = 人设 + 一组规则。""" + + id: str = "" # 留空 = 按名称自动生成 + name: str + role: str = "" + description: str = "" + persona: str = "" + keywords: list[str] = Field(default_factory=list) + rules: list[AgentRuleBody] = Field(default_factory=list) + enabled: bool = True + priority: int = 100 + + +class AgentPlanBody(BaseModel): + """启用与协作方案。single 就是"切换到某一个",collaborate 是"多个同时在场"。""" + + mode: str = "single" + primary_id: str = "" + active_ids: list[str] = Field(default_factory=list) + + +class ConfigBody(BaseModel): + """桌面端下发配置。 + + 字段名保持和老后台的表单 `name` 完全一致(大写下划线),这样两边共用同一个 + `validate_config_form`——校验规则只有一份,不会出现"网页后台拦得住、新后台 + 放得过"这种两套标准。 + + **这里没有模型连接参数**。服务类型 / API 地址 / API Key / 模型名称 / 温度 / + max_tokens / 超时全部搬到了「模型清单 + 角色编排」,理由见 + `admin_backend.CONFIG_KEYS` 上面那段。剩下的都是客户端行为。 + """ + + AI_ENABLED: bool = False + AI_DEVELOPMENT_MODE: bool = False + AI_USE_VISION: bool = False + AI_UI_GUARD_ENABLED: bool = False + AI_CONTEXT_ENABLED: bool = False + AI_CONTEXT_MAX_ROUNDS: int = 8 + AI_COUNTER_INSULT_ENABLED: bool = False + AI_AGENT_NAME: str = "" + AI_HOSPITAL_NAME: str = "" + AI_MCP_ENABLED: bool = False + AI_MCP_MAX_ROUNDS: int = 5 + AI_MCP_SERVERS: list[Any] = Field(default_factory=list) + # 留空 = 按后台自己的地址推算。桌面端不配这个,由后台同步时下发。 + AI_GATEWAY_URL: str = "" + # 桌面软件启动时先向本管理端读取该地址,再把账号密码发给 ZYT。 + ZYT_LOGIN_API_URL: str = "" + ARCHIVE_AUTO_BACKUP_ENABLED: bool = True + ARCHIVE_AUTO_BACKUP_INTERVAL_MINUTES: int = 60 + # 选择性审核:命中才转人工,其余自动发送。逐条形状由 + # `admin_backend.validate_review_rules` 校验,这里只接住一个数组。 + AI_REVIEW_RULES: list[Any] = Field(default_factory=list) + + @field_validator("*", mode="before") + @classmethod + def _null_means_unset(cls, value: Any, info: Any) -> Any: + """把请求体里的 `null` 当成"没填",换成这一项的出厂默认值。 + + 正常情况下 GET /config 现在不会再回 null,前端也就没有 null 可发。这里 + 挡的是另一种情况:浏览器里开着这次修复之前拉取的旧页面,表单里还揣着 + 当时的 null;用户点保存,POST 出去的照样是 null。没有这道,Pydantic 的 + 严格类型校验会直接拒收,报一句"Input should be a valid boolean"—— + 对着这句话,人只会怀疑是不是自己填错了什么,看不出问题出在缓存的旧页面。 + """ + if value is None: + return backend.CONFIG_DEFAULTS.get(info.field_name) + return value + + +class ReleaseBody(BaseModel): + package_media_id: str = "" + latest_version: str + download_url: str = "" + release_notes: str = "" + force_upgrade: bool = False + auto_install: bool = False + package_sha256: str = "" + package_size: int = Field(default=0, ge=0) + signature_required: bool = False + release_channel: str = "stable" + rollout_percent: int = Field(default=100, ge=1, le=100) + status: str = "published" + + +class ReleasePackagePrepareBody(BaseModel): + file_name: str = Field(min_length=1, max_length=512) + package_sha256: str = Field(pattern=r"^[0-9a-fA-F]{64}$") + package_size: int = Field(gt=0, le=1024 * 1024 * 1024) + + +class ReleasePackagePartBody(BaseModel): + part_number: int = Field(ge=1, le=10000) + etag: str = Field(min_length=1, max_length=512) + + +class ReleasePackageMultipartCompleteBody(BaseModel): + upload_id: str = Field(min_length=1, max_length=2048) + parts: list[ReleasePackagePartBody] = Field(min_length=1, max_length=10000) + + +class UpdateReportBody(BaseModel): + current_version: str = Field(default="", max_length=64) + target_version: str = Field(min_length=1, max_length=64) + status: str = Field(min_length=1, max_length=32) + progress: int = Field(default=0, ge=0, le=100) + error: str = Field(default="", max_length=1000) + + +class ModelTestBody(BaseModel): + """连通性测试。 + + 两种用法:给 `provider_id` 就测模型清单里那一条(密钥从库里解出来,永远不 + 经过前端);不给就用页面上临时填的值测——正在新建一个模型、还没保存的时候 + 需要这个。 + """ + + provider_id: str = "" + kind: str = "" + base_url: str = "" + endpoint_mode: str = "" + api_key: str = "" # 留空且指定了 provider_id = 用库里存的密钥 + model: str = "" + timeout_seconds: int = 20 + + +def _public_users(database) -> list[dict[str, Any]]: + """用户列表的对外投影。密码盐和摘要一个字节都不能出接口。""" + return [ + { + "id": row["id"], + "username": row["username"], + "role": row["role"], + "active": bool(row["active"]), + "must_change_password": bool(row["must_change_password"]), + "created_at": row["created_at"], + "updated_at": row["updated_at"], + } + for row in database.list_users() + ] + + +class Principal: + """当前请求的调用者:用户行 + 他实际拥有的权限码。""" + + def __init__(self, user, permissions: set[str]): + self.user = user + self.permissions = permissions + + @property + def id(self) -> int: + return int(self.user["id"]) + + def payload(self) -> dict[str, Any]: + return { + "id": self.id, + "username": self.user["username"], + "role": self.user["role"], + "must_change_password": bool(self.user["must_change_password"]), + "permissions": sorted(self.permissions), + } + + +def _frontend_dist() -> Path: + """Vue 前端的构建产物目录。 + + 单独抽出来是为了能在测试里替换掉——不然测试结果取决于本机有没有跑过 + `vite build`,同一份代码在开发机和 CI 上表现不一样。 + """ + return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "web-antd" / "dist" + + +def _mobile_dist() -> Path: + """手机远程控制台(admin-web/apps/mobile-h5)的构建产物,托管在 /m/。""" + return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "mobile-h5" / "dist" + + +def create_app( + db_path: Path | str = "backend.db", + zyt_token_verifier: Callable[[str], dict[str, Any]] | None = None, + zyt_patient_searcher: Callable[[str, str, int, int], dict[str, Any]] | None = None, + zyt_mobile_auth: Any = None, +) -> FastAPI: + database = backend.Database(Path(db_path).resolve()) + # 每个碰这个库的服务启动时都要把结构升到当前版本。只起 API、不起网页后台的 + # 部署,以前会撞上 `no such column`——而报错里看不出是漏了迁移。 + database.migrate() + release_directory = database.path.parent / "releases" + release_directory.mkdir(parents=True, exist_ok=True) + app = FastAPI(title="企微客服助手管理 API", version="2.0") + app.add_middleware( + CORSMiddleware, + allow_origins=DEV_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + def client_ip(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + return forwarded or (request.client.host if request.client else "") + + def current(request: Request) -> Principal: + raw = request.headers.get("authorization", "") + token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + user = database.session(token) if token else None + if user is not None and str(user["token_kind"]) != "api": + user = None + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已失效,请重新登录" + ) + return Principal(user, database.permissions_for_user(int(user["id"]))) + + def current_desktop(request: Request): + raw = request.headers.get("authorization", "") + token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + account = database.desktop_session(token) + if account is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="桌面登录已失效,请重新登录", + ) + return account + + def require(*codes: str): + """权限依赖。只认权限码,不认角色名。""" + + async def guard(principal: Principal = Depends(current)) -> Principal: + missing = [code for code in codes if code not in principal.permissions] + if missing: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"缺少权限:{'、'.join(missing)}", + ) + return principal + + return guard + + def resolve_admin_tenant( + principal: Principal, account_id: int | None + ) -> str | list[str]: + """把界面上选的客户端账号翻成这次查询的租户范围。 + + 下拉框有三档,这里是它们的含义: + + ALL_ACCOUNTS(-1) 全部。有 `desktop-account:all` 的返回 `"*"`(不限 + 租户);只被授权到若干账号的返回那几个账号的租户, + 「全部」对他就是「我能看的全部」,不是所有人的全部 + 0 / 不传 旧版未归属数据(tenant_id 为空或 default 的老记录) + 具体账号 id 那个账号的租户,且必须先过授权检查 + + 返回值有两种形状(一个租户 / 一组租户),下游统一交给 + `archive_store.tenant_scope` 和 `admin_backend.tenant_filter` 去翻译成 + SQL 条件——范围判断只在这一处做,散到各个接口里迟早会漏一个。 + """ + if account_id == ALL_ACCOUNTS: + if "desktop-account:all" in principal.permissions: + return backend.ALL_TENANTS + scoped = database.desktop_account_ids_for_admin(principal.id) + if not scoped: + # 没有被限制到任何账号 = 不受限,和上面一样能看全部。 + return backend.ALL_TENANTS + tenants = [] + for item in scoped: + account = database.desktop_account(int(item)) + if account is not None: + tenants.append(str(account["tenant_id"])) + if not tenants: + raise HTTPException(status_code=403, detail="没有已授权的客户端账号") + return tenants + if not account_id: + if ( + "desktop-account:all" not in principal.permissions + and database.desktop_account_ids_for_admin(principal.id) + ): + raise HTTPException(status_code=403, detail="请选择已授权的客户端账号") + return "default" + account = database.desktop_account(int(account_id)) + if account is None: + raise HTTPException(status_code=404, detail="客户端账号不存在") + if ( + "desktop-account:all" not in principal.permissions + and not database.admin_can_access_desktop_account(principal.id, int(account_id)) + ): + raise HTTPException(status_code=403, detail="无权查看该客户端账号的数据") + return str(account["tenant_id"]) + + def desktop_login_api_url() -> tuple[str, bool]: + """返回桌面登录服务地址,以及它是否来自管理端持久化配置。""" + + stored = json.loads(database.config()["config_json"]) + configured = str( + backend.effective_config(stored).get("ZYT_LOGIN_API_URL") or "" + ).strip().rstrip("/") + return (configured or zyt_auth.api_base_url(), bool(configured)) + + def release_payload(row: Any, *, direct_download: bool = False) -> dict[str, Any]: + keys = set(row.keys()) if hasattr(row, "keys") else set() + + def value(name: str, default: Any = "") -> Any: + return row[name] if name in keys else default + + result = { + "latest_version": str(value("latest_version")), + "download_url": str(value("download_url")), + "release_notes": str(value("release_notes")), + "force_upgrade": bool(value("force_upgrade", 0)), + "auto_install": bool(value("auto_install", 0)), + "package_sha256": str(value("package_sha256")), + "package_size": int(value("package_size", 0) or 0), + "signature_required": bool(value("signature_required", 0)), + "release_channel": str(value("release_channel", "stable")), + "rollout_percent": int(value("rollout_percent", 100) or 100), + "status": str(value("status", "published")), + "updated_at": str(value("updated_at", value("created_at", ""))), + "updated_by": str( + value("updated_by_name", value("created_by_name", "system")) + or "system" + ), + } + + identity = app.state.archive_store.release_package_identity(result['download_url']) + result['package_media_id'] = identity[0] if identity else '' + if direct_download and identity: + try: + result['download_url'] = app.state.archive_store.release_package_download_url(*identity) + except (KeyError, ValueError, RuntimeError) as exc: + raise HTTPException(status_code=409, detail=f"COS 安装包暂不可用:{exc}") from exc + except Exception as exc: + raise HTTPException(status_code=502, detail="COS 下载地址生成失败,请重试") from exc + return result + + # ── 桌面账号认证 ───────────────────────────────────────────────────── + @app.get("/api/v2/desktop/auth/config") + async def desktop_auth_config() -> dict: + login_api_url, configured = desktop_login_api_url() + return { + "zyt_api_url": login_api_url, + "terminal": zyt_auth.WECOM_RPA_TERMINAL, + "configured": configured, + } + + @app.post("/api/v2/desktop/auth/exchange") + async def desktop_auth_exchange(body: DesktopExchangeBody, request: Request) -> dict: + raw = request.headers.get("authorization", "") + zyt_token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + if not zyt_token: + raise HTTPException(status_code=401, detail="缺少 ZYT 登录令牌") + if zyt_token_verifier is not None: + verifier = zyt_token_verifier + else: + login_api_url, _configured = desktop_login_api_url() + verifier = lambda token: zyt_auth.verify_desktop_token( + token, api_url=login_api_url + ) + try: + identity = await asyncio.to_thread(verifier, zyt_token) + token, account = database.create_desktop_session( + identity, + body.device_id, + body.device_name, + body.app_version, + client_ip(request), + interactive_login=body.interactive_login, + ) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.save_zyt_session( + str(account["tenant_id"]), + zyt_token, + str(identity.get("user_id") or ""), + ) + except (zyt_auth.ZytAuthError, ValueError) as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + return { + "access_token": token, + "expires_in": max(0, int(account["expires_at"]) - int(time.time())), + "account": { + "id": int(account["id"]), + "sn": str(account["zyt_sn"]), + "nickname": str(account["nickname"]), + "tenant_id": str(account["tenant_id"]), + "device_id": str(account["device_id"]), + }, + } + + @app.post("/api/v2/desktop/auth/zyt-session") + async def desktop_refresh_zyt_session( + body: DesktopZytSessionBody, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + raw = request.headers.get("authorization", "") + desktop_token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + if zyt_token_verifier is not None: + verifier = zyt_token_verifier + else: + login_api_url, _configured = desktop_login_api_url() + verifier = lambda token: zyt_auth.verify_desktop_token(token, api_url=login_api_url) + try: + identity = await asyncio.to_thread(verifier, body.zyt_token.strip()) + if (not isinstance(identity, dict) or not identity.get("user_id") + or str(identity.get("terminal")) != str(zyt_auth.WECOM_RPA_TERMINAL) + or str(identity.get("status") or "") != "active"): + raise zyt_auth.ZytAuthError("ZYT 桌面身份验证失败,请重新验证账号") + except zyt_auth.ZytAuthError as exc: + # The identity adapter preserves transport/JSON causes. An outage must + # not send an otherwise logged-in user into a reauthentication loop. + cause = exc.__cause__ + http_code = cause.code if isinstance(cause, urllib.error.HTTPError) else None + if http_code == 403 or any(word in str(exc) for word in ("权限", "无权")): + raise HTTPException(status_code=403, detail="ZYT 账号无权验证患者查询身份,请联系管理员。") from exc + unavailable = ( + (http_code is not None and http_code != 401) + or (http_code is None and isinstance(cause, (urllib.error.URLError, OSError, ValueError, TypeError))) + or any(word in str(exc) for word in ("无法连接", "无效数据", "身份信息不完整", "配置不正确", "尚未部署")) + ) + if unavailable: + raise HTTPException(status_code=502, detail={ + "code": "zyt_unavailable", + "message": "ZYT 身份服务暂不可用,请稍后重试。", + }) from exc + raise HTTPException(status_code=409, detail={ + "code": "zyt_session_expired", + "message": "ZYT 患者查询登录已过期,请重新验证账号。", + }) from exc + if str(identity["user_id"]).strip() != str(account["zyt_user_id"]): + raise HTTPException(status_code=403, detail="请使用当前桌面账号重新验证,不能切换到其他账号") + archive_store = getattr(app.state, "archive_store", None) + if archive_store is None: + raise HTTPException(status_code=503, detail="患者查询服务暂不可用") + try: + await asyncio.to_thread( + archive_store.refresh_zyt_session, desktop_token, int(account["id"]), + str(account["tenant_id"]), body.zyt_token, identity, + ) + except PermissionError as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + return {"ok": True} + + @app.get("/api/v2/desktop/me") + async def desktop_me(account: Any = Depends(current_desktop)) -> dict: + return { + "id": int(account["id"]), + "sn": str(account["zyt_sn"]), + "nickname": str(account["nickname"]), + "tenant_id": str(account["tenant_id"]), + "device_id": str(account["device_id"]), + "expires_at": int(account["expires_at"]), + } + + @app.post("/api/v2/desktop/auth/logout") + async def desktop_auth_logout( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + raw = request.headers.get("authorization", "") + database.revoke_desktop_session(raw[7:].strip()) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(str(account["tenant_id"])) + database.audit( + None, + "desktop.logout", + f"account={account['id']} device={account['device_id']}", + client_ip(request), + ) + return {"ok": True} + + # ── 管理端认证 ─────────────────────────────────────────────────────── + @app.post("/api/v2/auth/login") + async def login(body: LoginBody, request: Request) -> dict: + ip = client_ip(request) + key = f"{ip}:{body.username.lower()}" + if not backend.LOGIN_LIMITER.allowed(key): + raise HTTPException(status_code=429, detail="登录失败次数过多,请稍后再试") + user = database.authenticate(body.username, body.password) + if not user: + backend.LOGIN_LIMITER.failure(key) + database.audit(None, "login.failed", f"username={body.username}", ip) + raise HTTPException(status_code=401, detail="用户名或密码不正确") + backend.LOGIN_LIMITER.success(key) + token, _ = database.create_token( + int(user["id"]), "api", body.device_name, 30 * 86400 + ) + database.audit(int(user["id"]), "login.api", body.device_name, ip) + principal = Principal(user, database.permissions_for_user(int(user["id"]))) + # 首次登录不拦 API:改密页面本身也要调接口。前端凭 + # must_change_password 把用户锁在改密页上,比后端一刀切 403 好用得多 + # ——老实现那样连改密接口都调不通,只能去网页后台改。 + return { + "access_token": token, + "expires_in": 30 * 86400, + "user": principal.payload(), + } + + @app.post("/api/v2/auth/logout") + async def logout(request: Request, principal: Principal = Depends(current)) -> dict: + raw = request.headers.get("authorization", "") + database.revoke(raw[7:].strip() if raw.lower().startswith("bearer ") else "") + database.audit(principal.id, "logout.api", "", client_ip(request)) + return {"ok": True} + + @app.get("/api/v2/me") + async def me(principal: Principal = Depends(current)) -> dict: + """前端路由守卫和按钮显隐的唯一数据源。""" + return principal.payload() + + @app.post("/api/v2/me/password") + async def change_password( + body: PasswordBody, request: Request, principal: Principal = Depends(current) + ) -> dict: + if not backend.valid_password(body.new_password): + raise HTTPException(status_code=400, detail="密码至少 10 位,且需同时含字母和数字") + try: + database.change_password( + principal.id, body.current_password, body.new_password, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"ok": True} + + # ── 角色与权限 ─────────────────────────────────────────────────────── + @app.get("/api/v2/permissions") + async def permissions(_: Principal = Depends(require("role:write"))) -> dict: + return {"permissions": database.permission_catalog()} + + @app.get("/api/v2/roles") + async def list_roles(_: Principal = Depends(require("user:read"))) -> dict: + return {"roles": database.roles()} + + @app.post("/api/v2/roles") + async def save_role( + body: RoleBody, request: Request, principal: Principal = Depends(require("role:write")) + ) -> dict: + try: + saved = database.save_role( + body.code, body.name, body.permissions, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"role": saved, "roles": database.roles()} + + @app.delete("/api/v2/roles/{code}") + async def delete_role( + code: str, request: Request, principal: Principal = Depends(require("role:write")) + ) -> dict: + try: + database.delete_role(code, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"roles": database.roles()} + + # ── 用户 ───────────────────────────────────────────────────────────── + @app.get("/api/v2/users") + async def list_users(_: Principal = Depends(require("user:read"))) -> dict: + return {"users": _public_users(database)} + + @app.post("/api/v2/users") + async def create_user( + body: UserBody, request: Request, principal: Principal = Depends(require("user:write")) + ) -> dict: + if not backend.valid_password(body.password): + raise HTTPException(status_code=400, detail="密码至少 10 位,且需同时含字母和数字") + known = {item["code"] for item in database.roles()} + if body.role not in known: + raise HTTPException(status_code=400, detail=f"角色不存在:{body.role}") + try: + database.create_user( + body.username, body.password, body.role, principal.id, client_ip(request) + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"users": _public_users(database)} + + @app.put("/api/v2/users/{user_id}") + async def update_user( + user_id: int, + body: UserBody, + request: Request, + principal: Principal = Depends(require("user:write")), + ) -> dict: + known = {item["code"] for item in database.roles()} + if body.role not in known: + raise HTTPException(status_code=400, detail=f"角色不存在:{body.role}") + try: + database.update_user( + user_id, body.role, body.active, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"users": _public_users(database)} + + # ── 客户端账号与设备 ───────────────────────────────────────────────── + @app.get("/api/v2/desktop-accounts") + def list_desktop_accounts( + include_counts: bool = True, + principal: Principal = Depends(current), + ) -> dict: + allowed = ( + None + if "desktop-account:all" in principal.permissions + else database.desktop_account_ids_for_admin(principal.id) + ) + accounts = database.desktop_accounts(allowed, include_counts=include_counts) + mobile_counts = app.state.remote_store.mobile_session_counts() + for account in accounts: + account["mobile_session_count"] = mobile_counts.get(int(account["id"]), 0) + return { + "accounts": accounts, + "can_view_all": "desktop-account:all" in principal.permissions, + # 尚未分配任何账号的旧角色仍能查看升级前的未归属数据;一旦分配账号, + # 就必须明确选择授权账号,不能再借默认值绕过范围限制。 + "can_view_legacy": ( + "desktop-account:all" in principal.permissions or not allowed + ), + } + + @app.put("/api/v2/desktop-accounts/{account_id}") + async def update_desktop_account( + account_id: int, + body: DesktopAccountBody, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + try: + account = database.update_desktop_account( + account_id, + status=body.status, + backup_enabled=body.backup_enabled, + backup_interval_minutes=body.backup_interval_minutes, + actor_id=principal.id, + ip=client_ip(request), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if str(account.get("status") or "") == "disabled": + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(str(account["tenant_id"])) + app.state.remote_store.revoke_mobile_sessions_for_account( + account_id, reason="account_disabled" + ) + return {"account": account} + + @app.post("/api/v2/desktop-accounts/{account_id}/revoke-devices") + async def revoke_desktop_devices( + account_id: int, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + tenant = resolve_admin_tenant(principal, account_id) + database.revoke_desktop_devices(account_id, principal.id, client_ip(request)) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(tenant) + # 「强制下线」同时收回手机控制台的登录,否则被下线的人还能远程启停。 + app.state.remote_store.revoke_mobile_sessions_for_account( + account_id, reason="admin_revoke" + ) + return {"ok": True} + + @app.get("/api/v2/desktop-accounts/{account_id}/admins") + async def desktop_account_admins( + account_id: int, + principal: Principal = Depends(require("desktop-account:read")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + return {"admin_user_ids": database.desktop_account_admin_ids(account_id)} + + @app.put("/api/v2/desktop-accounts/{account_id}/admins") + async def update_desktop_account_admins( + account_id: int, + body: DesktopAccountScopeBody, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + try: + database.set_desktop_account_admins( + account_id, body.admin_user_ids, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"admin_user_ids": database.desktop_account_admin_ids(account_id)} + + # ── 模型清单与编排 ─────────────────────────────────────────────────── + @app.get("/api/v2/models") + async def list_models(_: Principal = Depends(require("model:read"))) -> dict: + return { + "models": database.model_providers(), + "roles": database.model_roles(), + "kinds": ["dify", "openai", "claude", "comfyui"], + "judge_modes": ["shadow", "score_only", "arbitrate"], + } + + @app.post("/api/v2/models") + async def save_model( + body: ProviderBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + database.save_model_provider( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"models": database.model_providers()} + + @app.delete("/api/v2/models/{provider_id}") + async def delete_model( + provider_id: str, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + database.delete_model_provider(provider_id, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"models": database.model_providers()} + + @app.post("/api/v2/models/plan") + async def save_plan( + body: RolesPlanBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + version = database.save_model_roles( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"version": version, "roles": database.model_roles()} + + # ── 智能体 ─────────────────────────────────────────────────────────── + # 模型清单管的是"用哪个模型",这里管的是"用什么身份、按什么规矩说话"。分开 + # 是因为这两件事的变更频率差一个数量级:模型半年动一次,话术一周动三次。 + @app.get("/api/v2/agents") + async def list_agents(_: Principal = Depends(require("agent:read"))) -> dict: + return { + "agents": database.agents(), + "plan": database.agent_plan(), + "rule_types": list(backend.AGENT_RULE_TYPES), + "plan_modes": list(backend.AGENT_PLAN_MODES), + } + + @app.post("/api/v2/agents") + async def save_agent( + body: AgentBody, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + database.save_agent(body.model_dump(), principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"agents": database.agents(), "plan": database.agent_plan()} + + @app.delete("/api/v2/agents/{agent_id}") + async def delete_agent( + agent_id: str, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + database.delete_agent(agent_id, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"agents": database.agents(), "plan": database.agent_plan()} + + @app.post("/api/v2/agents/plan") + async def save_agent_plan( + body: AgentPlanBody, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + version = database.save_agent_plan( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"version": version, "plan": database.agent_plan()} + + # ── 运营 ───────────────────────────────────────────────────────────── + @app.get("/api/v2/stats/model-calls") + async def model_call_stats( + days: int = 7, + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return { + "days": days, + **database.model_call_stats( + days, resolve_admin_tenant(principal, account_id) + ), + } + + @app.get("/api/v2/stats/model-calls/log") + async def model_call_log( + days: int = 7, + limit: int = 50, + offset: int = 0, + q: str = "", + # chat = 真实对话(默认);guard = 界面识别;留空 = 全都要 + purpose: str = "chat", + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.list_model_calls( + days, + limit, + offset, + q, + purpose, + resolve_admin_tenant(principal, account_id), + ) + + @app.get("/api/v2/stats/model-usage") + async def model_usage_stats( + days: int = Query(default=7, ge=1, le=90), + purpose: str = Query(default="", pattern="^(chat|guard|knowledge)?$"), + provider_id: str = Query(default="", max_length=256), + model: str = Query(default="", max_length=512), + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.model_usage_stats( + days, resolve_admin_tenant(principal, account_id), purpose, provider_id, model) + + @app.get("/api/v2/stats/model-usage/log") + async def model_usage_log( + days: int = Query(default=7, ge=1, le=90), + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + purpose: str = Query(default="", pattern="^(chat|guard|knowledge)?$"), + provider_id: str = Query(default="", max_length=256), + model: str = Query(default="", max_length=512), + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.list_model_usage( + days, limit, offset, resolve_admin_tenant(principal, account_id), purpose, provider_id, model) + + # ── 桌面端配置 ─────────────────────────────────────────────────────── + # 这一组是从老网页后台整体搬过来的("能力开关 / 模型与身份 / MCP 服务器" + # 三个区块)。桌面端启动和定时同步都拉这份配置,改错一个字段全线客户端 + # 立刻受影响——所以校验函数直接复用老后台那一份,不另写一套。 + @app.get("/api/v2/config") + async def get_config( + request: Request, _: Principal = Depends(require("config:read")) + ) -> dict: + row = database.config() + stored = json.loads(row["config_json"]) + # 只回当前在用的字段。库里老记录还带着已退休的模型参数(不删用户 + # 数据),但把它们发给前端只会让人以为那里还能改。 + config = backend.effective_config(stored) + return { + "config": config, + "version": int(row["version"]), + "updated_at": row["updated_at"], + "updated_by": row["updated_by_name"] or "system", + "bool_keys": sorted(backend.BOOL_KEYS), + # 已经搬走的字段,连同它们现在的去处。前端拿它渲染一条指路说明, + # 而不是让人对着一个"以前在这儿的东西不见了"的页面发愣。 + "retired_keys": list(backend.RETIRED_CONFIG_KEYS), + "model_settings_moved_to": "AI 模型 → 模型清单 / 角色编排", + # 留空时实际会下发什么。不显示出来的话,"自动推算"对使用的人就是 + # 一个黑盒——出问题时没法判断是推错了还是网关没起。 + "gateway_url_effective": backend.derive_gateway_url( + str(config.get("AI_GATEWAY_URL") or ""), + request.headers.get("x-forwarded-proto", request.url.scheme), + request.headers.get("host", ""), + ), + # 登录窗口在用户输入账号前会读取公开配置接口。这里显示它最终会 + # 拿到哪个地址,便于管理员保存后当场核对。 + "login_api_url_effective": desktop_login_api_url()[0], + } + + @app.post("/api/v2/config") + async def save_config( + body: ConfigBody, + request: Request, + principal: Principal = Depends(require("config:write")), + ) -> dict: + current = json.loads(database.config()["config_json"]) + # validate_config_form 收的是 HTML 表单那种"全是字符串"的字典,布尔用 + # "1"/"" 表示。这里把 JSON 转成它认识的形状,而不是把它改成认 JSON—— + # 老后台还在跑,改它等于同时动两个正在服务的系统。 + form: dict[str, str] = {} + for key, value in body.model_dump().items(): + if key in backend.BOOL_KEYS: + form[key] = "1" if value else "" + elif key in ("AI_MCP_SERVERS", "AI_REVIEW_RULES"): + form[key] = json.dumps(value, ensure_ascii=False) + else: + form[key] = str(value) + try: + config = backend.validate_config_form(form, current) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + version = database.save_config(config, principal.id, client_ip(request)) + return {"ok": True, "version": version} + + # ── 桌面端版本升级 ─────────────────────────────────────────────────── + @app.get("/api/v2/release") + async def get_release(_: Principal = Depends(require("config:read"))) -> dict: + return release_payload(database.release(), direct_download=True) + + @app.post("/api/v2/release") + async def save_release( + body: ReleaseBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + download_url = app.state.archive_store.normalize_release_download( + body.download_url, body.package_sha256, body.package_size, body.package_media_id + ) + release = backend.validate_release_form( + { + "latest_version": body.latest_version, + "download_url": download_url, + "release_notes": body.release_notes, + "force_upgrade": "1" if body.force_upgrade else "", + "auto_install": "1" if body.auto_install else "", + "package_sha256": body.package_sha256, + "package_size": str(body.package_size), + "signature_required": "1" if body.signature_required else "", + "release_channel": body.release_channel, + "rollout_percent": str(body.rollout_percent), + "status": body.status, + } + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + database.save_release( + release["latest_version"], + release["download_url"], + release["release_notes"], + release["force_upgrade"], + principal.id, + client_ip(request), + auto_install=release["auto_install"], + package_sha256=release["package_sha256"], + package_size=release["package_size"], + signature_required=release["signature_required"], + release_channel=release["release_channel"], + rollout_percent=release["rollout_percent"], + status=release["status"], + ) + return {"ok": True, **release_payload(database.release(), direct_download=True)} + + @app.post("/api/v2/release/package/prepare") + async def prepare_release_package( + body: ReleasePackagePrepareBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + """签发 COS 直传地址;EXE 内容不会进入管理端进程或本机磁盘。""" + + try: + store = app.state.archive_store + prepared = store.prepare_release_package( + { + "original_filename": body.file_name, + "sha256": body.package_sha256.lower(), + "size_bytes": body.package_size, + } + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException( + status_code=502, detail=f"COS 预上传失败:{exc}" + ) from exc + database.audit( + principal.id, + "release.package.prepare", + f"file={prepared['file_name']} size={prepared['package_size']}", + client_ip(request), + ) + return prepared + + @app.post("/api/v2/release/package/{media_id}/complete") + async def complete_release_package( + media_id: str, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + package = app.state.archive_store.complete_release_package(media_id) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 文件校验失败:{exc}") from exc + database.audit( + principal.id, + "release.package.complete", + f"media={media_id} size={package['package_size']}", + client_ip(request), + ) + return package + + @app.post("/api/v2/release/package/{media_id}/multipart-complete") + async def complete_multipart_release_package( + media_id: str, + body: ReleasePackageMultipartCompleteBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + package = app.state.archive_store.complete_multipart_release_package( + media_id, + body.upload_id, + [part.model_dump() for part in body.parts], + ) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 分块合并失败:{exc}") from exc + database.audit( + principal.id, + "release.package.complete", + f"media={media_id} multipart=1 size={package['package_size']}", + client_ip(request), + ) + return package + + @app.get("/api/v2/releases") + async def release_history( + _: Principal = Depends(require("config:read")), + ) -> dict: + current = release_payload(database.release()) + return { + "history": [release_payload(row) for row in database.release_history()], + "reports": database.update_report_summary(current["latest_version"]), + } + + @app.get("/api/v2/desktop/update/packages/{filename}") + async def desktop_update_package(filename: str) -> FileResponse: + if Path(filename).name != filename or not filename.lower().endswith(".exe"): + raise HTTPException(status_code=404, detail="升级包不存在") + target = (release_directory / filename).resolve() + if target.parent != release_directory.resolve() or not target.is_file(): + raise HTTPException(status_code=404, detail="升级包不存在") + return FileResponse( + target, + media_type="application/vnd.microsoft.portable-executable", + filename=filename, + ) + + @app.get("/api/v2/desktop/update/cos-packages/{media_id}/{filename}") + async def desktop_update_cos_package(media_id: str, filename: str) -> RedirectResponse: + try: + url = app.state.archive_store.release_package_download_url( + media_id, filename + ) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 下载签名失败:{exc}") from exc + return RedirectResponse(url=url, status_code=307) + + @app.get("/api/v2/desktop/update/manifest") + async def desktop_update_manifest( + request: Request, + current_version: str = "", + device_id: str = "", + channel: str = "stable", + ) -> dict: + release = release_payload(database.release()) + latest = release["latest_version"] + eligible = release["status"] == "published" and release["release_channel"] == channel + if eligible and release["rollout_percent"] < 100: + bucket = int( + hashlib.sha256(f"{device_id}:{latest}".encode("utf-8")).hexdigest()[:8], + 16, + ) % 100 + 1 + eligible = bucket <= release["rollout_percent"] + update_available = bool( + eligible and app_version.is_newer_version(latest, current_version) + ) + download_url = release["download_url"] + identity = app.state.archive_store.release_package_identity(download_url) + if identity: + # Each check gets a fresh COS HTTPS URL, including old release rows. + download_url = release_payload(release, direct_download=True)["download_url"] if update_available else "" + elif download_url.startswith("/"): + forwarded_scheme = str( + request.headers.get("x-forwarded-proto") or request.url.scheme + ).split(",", 1)[0].strip() + forwarded_host = str( + request.headers.get("x-forwarded-host") + or request.headers.get("host") + or request.url.netloc + ).split(",", 1)[0].strip() + public_base = ( + f"{forwarded_scheme}://{forwarded_host}/" + if forwarded_scheme and forwarded_host + else str(request.base_url) + ) + download_url = urllib.parse.urljoin( + public_base, download_url.lstrip("/") + ) + return { + **release, + "download_url": download_url, + "download_url_expires_in": 900 if identity and update_available else 0, + "update_available": update_available, + "eligible": eligible, + } + + @app.post("/api/v2/desktop/update/report") + async def desktop_update_report( + body: UpdateReportBody, + account: Any = Depends(current_desktop), + ) -> dict: + allowed = { + "available", + "downloading", + "downloaded", + "installing", + "installed", + "failed", + "deferred", + } + if body.status not in allowed: + raise HTTPException(status_code=400, detail="升级状态不正确") + if not app_version.normalize_version(body.target_version): + raise HTTPException(status_code=400, detail="目标版本号不正确") + database.record_update_report( + int(account["id"]), + str(account["device_id"]), + body.current_version, + body.target_version, + body.status, + body.progress, + body.error, + ) + return {"ok": True} + + # ── 模型连通性测试 ─────────────────────────────────────────────────── + @app.post("/api/v2/models/test") + async def test_model( + body: ModelTestBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + """真的向模型发一次最小请求,返回不含密钥的诊断结果。 + + 要 `model:write` 而不是 `model:read`:这个动作会拿着真实密钥向外网发请 + 求,还可能产生费用。只读用户能看模型清单,但不该能替公司花钱。 + + 密钥来源有两处,都不经过前端:指定了 `provider_id` 就从库里解密取;否则 + 用请求里临时填的(新建模型还没保存时要用)。 + """ + kind = str(body.kind or "").strip().lower() + base_url = str(body.base_url or "").strip() + model = str(body.model or "").strip() + api_key = str(body.api_key or "").strip() + endpoint_mode = str(body.endpoint_mode or "").strip().lower() + label = "" + + if body.provider_id: + match = next( + ( + item + for item in database.model_providers(include_secrets=True) + if str(item.get("id")) == body.provider_id + ), + None, + ) + if match is None: + raise HTTPException(status_code=404, detail="模型不存在") + label = str(match.get("name") or match.get("id") or "") + kind = kind or str(match.get("kind") or "") + base_url = base_url or str(match.get("base_url") or "") + model = model or str(match.get("model") or "") + # 前端提交空密钥 = 用库里存的那把。这样"测一下现有配置通不通" + # 不需要把密钥再发一遍,也就没有它在网络上多走一趟的机会。 + api_key = api_key or str(match.get("api_key") or "") + endpoint_mode = endpoint_mode or str(match.get("endpoint_mode") or "auto") + + if kind == "claude": + # 老后台的测试器只认 openai / dify / comfyui 三种。Claude 是新增 + # 的出口类型,端点和鉴权头都不一样,硬套 openai 只会得到 404。 + raise HTTPException( + status_code=400, + detail="Claude 出口暂不支持一键连通性测试,请在模型网关的调用统计里确认", + ) + if kind not in backend.PROVIDER_TYPES: + raise HTTPException( + status_code=400, + detail=f"不支持的接口类型:{kind or '(空)'}", + ) + if not base_url: + raise HTTPException(status_code=400, detail="接口地址不能为空") + + try: + config = backend.model_test_config( + { + "AI_PROVIDER_TYPE": kind, + "AI_API_BASE": base_url, + "AI_MODEL": model, + "AI_API_KEY": api_key, + "AI_TIMEOUT": body.timeout_seconds, + "AI_ENDPOINT_MODE": endpoint_mode or "auto", + }, + {}, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + result = backend.test_model_connection(config) + database.audit( + principal.id, + "model.test", + f"provider={label or body.provider_id or '临时'}, ok={int(bool(result['ok']))}, " + f"http={result.get('http_status')}, endpoint={str(result.get('endpoint') or '')[:200]}", + client_ip(request), + ) + # 故意返回 200 而不是 502:测试本身跑成功了,"连不上"是它的结论, + # 不是这个接口的失败。返 5xx 会让前端的错误拦截器弹一个通用报错, + # 把真正有用的诊断信息盖掉。 + return {"result": result, "label": label} + + # ── 桌面端同步 ─────────────────────────────────────────────────────── + # 桌面客户端拉的就是这一个接口。老后台(8765)已退役,这里是唯一来源; + # 客户端仍保留 v2→v1 回退,是为了兼容还没升级到新服务端的部署。 + @app.get("/api/v2/desktop/config") + async def desktop_config( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + # 这一份的结构由 `admin_backend.desktop_config_payload` 统一给出。 + # 两处各写一份的话迟早漂移,表现成"某台客户端少了个字段",极难查。 + payload = backend.desktop_config_payload( + database, + scheme=request.headers.get("x-forwarded-proto", request.url.scheme), + host=request.headers.get("host", ""), + desktop_account=account, + ) + knowledge = app.state.knowledge_store + tenant = str(account["tenant_id"]) + counts = await asyncio.to_thread(knowledge.stats, tenant) + settings = await asyncio.to_thread(knowledge.settings, tenant) + payload["knowledge"] = {"published": counts.get("published", 0), + "enabled": bool(settings["enabled"]), "updated_at": settings["updated_at"]} + return payload + + # 桌面端回流一次编排调用。补上这条之前,客户端改指 8766 会丢掉全部调用留痕 + # ——配置照常同步、回复照常发,唯独调用记录一条不进库,而那恰恰是出事后用来 + # 解释"这句话怎么来的"的东西。它是老后台退役的最后一块拼图。 + @app.post("/api/v2/model/calls") + async def log_model_call( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + try: + record = await request.json() + except Exception: + record = {} + # 落库失败也返回 200:这是观测数据,不能因为它出问题就让桌面端以为 + # 回复流程失败了。`log_model_call` 内部已经吞掉异常并打日志。 + if isinstance(record, dict): + record["desktop_account_id"] = int(account["id"]) + record["tenant_id"] = str(account["tenant_id"]) + database.log_model_call(record) + return {"ok": True} + + @app.get("/api/v2/audit") + async def audit_log( + limit: int = 200, _: Principal = Depends(require("audit:read")) + ) -> dict: + return {"entries": database.audit_entries(limit)} + + @app.get("/api/v2/health") + async def health() -> dict: + return {"status": "ok", "time": time.strftime("%Y-%m-%d %H:%M:%S")} + + # 聊天归档是独立新模块,只复用当前登录、权限和审计能力。 + # 必须在 SPA 通配路由之前注册,避免 /api 请求被静态页入口截住。 + archive_api.register_archive_routes( + app, + database, + current, + require, + client_ip, + current_desktop=current_desktop, + resolve_admin_tenant=resolve_admin_tenant, + zyt_api_url=lambda: desktop_login_api_url()[0], + zyt_patient_searcher=zyt_patient_searcher, + ) + + knowledge_api.register_knowledge_routes( + app, database, require, client_ip, resolve_admin_tenant, current_desktop, + ) + + remote_api.register_remote_routes( + app, + database, + current_desktop=current_desktop, + zyt_api_url=lambda: desktop_login_api_url()[0], + zyt_mobile_auth=zyt_mobile_auth, + ) + + # ── 手机远程控制台 /m/ ─────────────────────────────────────────────── + # 必须注册在管理端 SPA 的通配路由之前,否则 /m/ 会被当成管理端的前端路由。 + mobile_dist = _mobile_dist() + if (mobile_dist / "index.html").is_file(): + from starlette.responses import FileResponse as _MobileFile + + mobile_root = mobile_dist.resolve() + + @app.get("/m", include_in_schema=False) + async def mobile_entry_redirect(): + return RedirectResponse("/m/", status_code=308) + + # 远程启停只要"点按钮、点确认"两下,页面不许被别的站点嵌进 iframe 诱导点击。 + mobile_headers = { + "X-Frame-Options": "DENY", + "Content-Security-Policy": ( + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; font-src 'self' data:; connect-src 'self'; " + "manifest-src 'self'; worker-src 'self'; frame-ancestors 'none'; " + "base-uri 'self'; form-action 'self'" + ), + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + } + + @app.get("/m/{path:path}", include_in_schema=False) + async def mobile_spa(path: str): + candidate = (mobile_root / path).resolve() if path else mobile_root / "index.html" + if not candidate.is_relative_to(mobile_root) or not candidate.is_file(): + # 发版后还开着的旧页面会去拉旧的哈希文件:回 404 让前端整页刷新到 + # 新版本;回 index.html 的话浏览器会把 HTML 当脚本执行,页面直接卡死。 + if path.startswith("assets/"): + raise HTTPException(status_code=404, detail="文件不存在") + candidate = mobile_root / "index.html" + response = _MobileFile(candidate) + # 文件名带哈希的构建产物可以长缓存;入口、清单和 Service Worker 每次都 + # 要重新验证,否则发版后手机上会一直卡在旧界面。 + if candidate.parent.name == "assets" and candidate.parent.parent == mobile_root: + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "no-cache" + response.headers.update(mobile_headers) + return response + + # ── 前端静态资源 ───────────────────────────────────────────────────── + # 构建产物存在时由本服务一起托管,前后端同源——省掉生产环境的 CORS 配置, + # 也省掉一个 Nginx location。开发时前端跑 Vite dev server,走它自己的代理。 + dist = _frontend_dist() + if (dist / "index.html").is_file(): + from fastapi.staticfiles import StaticFiles + from starlette.responses import FileResponse + + # 判据用 index.html 而不是目录本身:构建中途、或者只剩一个空 dist 目录 + # 的时候,目录是在的但没有可服务的东西,挂上去只会让 SPA 兜底路由把 + # 每个请求都回一个不存在的文件。 + # + # 静态子目录也必须逐个确认存在——StaticFiles 的构造函数遇到不存在的 + # 目录**直接抛异常**,整个服务起不来。vite 默认输出的是 js/ 和 css/, + # 不是 assets/;写死 assets 的话,一旦真的有了构建产物,API 就崩在启动。 + for name in ("assets", "js", "css", "static"): + sub = dist / name + if sub.is_dir(): + app.mount(f"/{name}", StaticFiles(directory=sub), name=name) + + @app.get("/{full_path:path}") + async def spa(full_path: str): + """SPA 的 history 路由:非 /api 的路径一律回 index.html。 + + 少了这一条,用户在 /system/roles 上按 F5 会拿到 404——前端路由是 + 浏览器端的,服务器上并不存在那个文件。 + """ + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="接口不存在") + candidate = dist / full_path + if full_path and candidate.is_file(): + return FileResponse(candidate) + return FileResponse(dist / "index.html") + + return app + + +def main() -> None: + parser = argparse.ArgumentParser(description="管理后台 JSON API") + parser.add_argument("--db", default="backend.db") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8766) + parser.add_argument( + "--zyt-api-url", + default=os.environ.get("ZYT_API_BASE_URL", zyt_auth.DEFAULT_ZYT_API_BASE_URL), + help="ZYT 用户 API 根地址,例如 https://example.com", + ) + parser.add_argument( + "--runtime-file", + default=str(backend.DEFAULT_RUNTIME_FILE), + help="发布实际监听地址,供同机桌面端自动发现", + ) + parser.add_argument( + "--initial-admin-password", + default=os.environ.get( + "WECOM_ADMIN_INITIAL_PASSWORD", backend.DEFAULT_ADMIN_PASSWORD + ), + help="首次创建数据库时的 admin 密码", + ) + parser.add_argument( + "--reset-admin-password", + action="store_true", + help="交互式重置 admin 密码后退出", + ) + args = parser.parse_args() + zyt_auth.DEFAULT_ZYT_API_BASE_URL = str(args.zyt_api_url).strip().rstrip("/") + + # 建库并按需播种 admin。这两件事以前只有老网页后台的 main() 做——不搬过来的 + # 话,全新部署起来之后库里一个用户都没有,Vue 管理端登录页永远进不去,而且 + # 报的是"用户名或密码不正确",完全看不出是根本没建过账号。 + database = backend.Database(Path(args.db).resolve()) + created = database.initialize(args.initial_admin_password) + + if args.reset_admin_password: + import getpass + + first = getpass.getpass("新的 admin 密码:") + if first != getpass.getpass("再次输入:"): + raise SystemExit("两次输入不一致") + if not backend.valid_password(first): + raise SystemExit("密码至少 10 位,并同时包含字母和数字") + database.reset_admin_password(first) + print("admin 密码已重置,下次登录时必须再次修改。") + return + + if created: + print("首次登录账号:admin") + print(f"首次登录密码:{args.initial_admin_password}") + print("登录后必须立即修改初始密码。") + if args.host not in ("127.0.0.1", "localhost", "::1"): + print("警告:当前监听非本机地址;生产环境请通过 HTTPS 反向代理访问。") + + import uvicorn + + # 发布本机地址,桌面端靠它自动发现后台。 + # + # 这件事以前只有老网页后台(8765)做。不搬过来的话,老后台一停,桌面端的 + # 自动发现就永远指向一个没人监听的 8765,报出来的是"无法连接后台"——而真正 + # 在跑的 8766 从来没被试过。这是老后台能退役的前提之一。 + runtime_path = Path(args.runtime_file) + backend.write_runtime_info(runtime_path, args.host, args.port) + try: + uvicorn.run( + create_app(args.db), host=args.host, port=args.port, log_level="info" + ) + finally: + backend.clear_runtime_info(runtime_path, args.port) + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-remote-20260928/payload/wechat_rpa/remote_api.py b/deploy/mobile-remote-20260928/payload/wechat_rpa/remote_api.py new file mode 100644 index 000000000..ee06afa2e --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/wechat_rpa/remote_api.py @@ -0,0 +1,802 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台与桌面命令通道的 HTTP 接口。 + + /api/v2/mobile/* 手机 / 平板 H5 用,ZYT 账号密码登录(终端 9,由本服务代登) + /api/v2/desktop/commands/* 桌面端长轮询领取指令、回执结果、顺带上报状态 + /api/v2/desktop/blacklist* 桌面端同步服务端会话黑名单 + +手机令牌和桌面令牌分别由 ``mobile_sessions`` 和 ``desktop_sessions`` 签发,互相 +不能冒用:手机端拿不到归档上传之类的桌面接口,桌面端也调不了手机接口。 +""" + +from __future__ import annotations + +import asyncio +import ipaddress +import os +import threading +import time +from collections import defaultdict, deque +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request +from pydantic import BaseModel, Field + +import zyt_auth +from remote_store import ( + COMMAND_TYPES, + MOBILE_IDENTITY_RECHECK_SECONDS, + PERM_BLACKLIST_ADD, + PERM_BLACKLIST_REMOVE, + PERM_REMOTE_ALL, + PERM_REMOTE_CONTROL, + RemoteStore, + account_label, + account_permissions, + valid_device_id, +) + +# 桌面端长轮询最长挂起时间。uvicorn 停机时会等在途请求结束,这里必须明显短于 +# compose 里 20 秒的 stop_grace_period,否则每次发版都会被强杀;指令本身靠进程内 +# 通知即时送达,挂起时长只影响"没事时多久回来报一次状态"。 +MAX_POLL_WAIT_SECONDS = 15 +# 挂起期间多久回库复查一次;进程内通知失效(多进程部署)时的兜底。 +POLL_RECHECK_SECONDS = 5 + + +def _error(status_code: int, code: str, message: str, **extra: Any) -> HTTPException: + return HTTPException(status_code=status_code, detail={"code": code, "message": message, **extra}) + + +def _bearer(request: Request) -> str: + raw = request.headers.get("authorization", "") + return raw[7:].strip() if raw.lower().startswith("bearer ") else "" + + +def trusted_client_ip(request: Request) -> str: + """做安全判断用的客户端地址。 + + X-Forwarded-For 的第一段是客户端自己填的,可以伪造;X-Real-IP 由 Nginx 按 + $remote_addr 覆盖写入,API 端口又只监听本机回环,只有 Nginx 能连进来。 + """ + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return real_ip + return request.client.host if request.client else "" + + +def _is_private_address(value: str) -> bool: + try: + address = ipaddress.ip_address(str(value or "").strip()) + except ValueError: + return False + return address.is_private or address.is_loopback or address.is_link_local + + +def request_scheme(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-proto", "").split(",")[0].strip().lower() + return forwarded or request.url.scheme.lower() + + +def https_required(request: Request) -> bool: + """手机端要在公网上提交 ZYT 密码,明文 HTTP 只允许在内网试用。 + + MOBILE_REQUIRE_HTTPS:auto(默认,公网地址必须 HTTPS)/ always / never。 + + 没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker 网桥 + (172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 + (开发时直接访问 127.0.0.1)才在没有这个头时被信任。 + """ + policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "auto").strip().lower() + if policy == "never" or request_scheme(request) == "https": + return False + if policy == "always": + return True + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return not _is_private_address(real_ip) + peer = request.client.host if request.client else "" + try: + return not ipaddress.ip_address(peer).is_loopback + except ValueError: + return True + + +class LoginAttempt: + __slots__ = ("ip", "account", "finished") + + def __init__(self, ip: str, account: str): + self.ip, self.account, self.finished = ip, account, False + + +class MobileLoginGuard: + """手机登录限流。 + + ZYT 按请求来源 IP 计数锁定(默认 30 分钟内错 5 次锁 30 分钟),而手机端的 + 登录全部由本服务代发,在 ZYT 看来都来自服务器出口 IP——如果公司电脑也走同一 + 个出口,锁住的就是全公司的 ZYT 登录。所以这里除了按账号、按客户端 IP 限流, + 还给"真正发到 ZYT 的密码错误"设了一个全局预算,并在 ZYT 已锁定时直接短路。 + + 限流必须把"正在进行"的尝试也算进去:先检查、等 ZYT 回来再记账的话,一次并发 + 发来十几个请求会全部通过检查,一起打到 ZYT 上。`begin()` 在锁里占位, + `finish()` 按结果转成失败记录或释放。 + """ + + def __init__( + self, *, account_limit: int = 5, account_window: int = 600, + ip_limit: int = 10, ip_window: int = 600, + upstream_budget: int | None = None, upstream_window: int = 1800, + ): + if upstream_budget is None: + try: + upstream_budget = int(os.environ.get("MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET", "4")) + except ValueError: + upstream_budget = 4 + self.account_limit, self.account_window = account_limit, account_window + self.ip_limit, self.ip_window = ip_limit, ip_window + self.upstream_budget, self.upstream_window = max(0, upstream_budget), upstream_window + self._accounts: dict[str, deque[float]] = {} + self._ips: dict[str, deque[float]] = {} + self._upstream: deque[float] = deque() + self._pending_accounts: dict[str, int] = defaultdict(int) + self._pending_ips: dict[str, int] = defaultdict(int) + self._pending_upstream = 0 + self._locked_until = 0.0 + self._lock = threading.Lock() + + @staticmethod + def _recent(book: dict[str, deque[float]], key: str, window: int, now: float) -> deque[float]: + """剪掉过期记录;空了就把这个键删掉,随机账号名刷不大内存。""" + items = book.get(key) + if items is None: + return deque() + while items and items[0] <= now - window: + items.popleft() + if not items: + del book[key] + return items + + @staticmethod + def _minutes(seconds: float) -> int: + return max(1, int((seconds + 59) // 60)) + + def begin(self, ip: str, account: str) -> LoginAttempt: + now = time.time() + with self._lock: + if self._locked_until > now: + wait = self._locked_until - now + raise _error( + 429, "zyt_locked", + f"ZYT 已暂时锁定登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + failures = self._recent(self._accounts, account, self.account_window, now) + if len(failures) >= self.account_limit: + wait = failures[0] + self.account_window - now + raise _error( + 429, "rate_limited", + f"该账号登录失败次数过多,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(failures) + self._pending_accounts.get(account, 0) >= self.account_limit: + raise _error(429, "rate_limited", "该账号正在登录,请稍候几秒再试", retry_after=5) + failures = self._recent(self._ips, ip, self.ip_window, now) + if len(failures) + self._pending_ips.get(ip, 0) >= self.ip_limit: + wait = (failures[0] + self.ip_window - now) if failures else 5 + raise _error( + 429, "rate_limited", + f"登录尝试过于频繁,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + while self._upstream and self._upstream[0] <= now - self.upstream_window: + self._upstream.popleft() + if self.upstream_budget: + if len(self._upstream) >= self.upstream_budget: + wait = self._upstream[0] + self.upstream_window - now + raise _error( + 429, "upstream_budget", + "近期手机端密码错误次数较多,为避免 ZYT 锁定服务器出口 IP," + f"已暂停手机登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(self._upstream) + self._pending_upstream >= self.upstream_budget: + raise _error(429, "rate_limited", "同时登录的人较多,请几秒后再试", retry_after=5) + self._pending_accounts[account] += 1 + self._pending_ips[ip] += 1 + self._pending_upstream += 1 + return LoginAttempt(ip, account) + + def finish(self, attempt: LoginAttempt, outcome: str) -> None: + """outcome:credentials(密码错)、unknown_account(账号不存在)、success(ZYT 认可了密码)、 + other(网络、终端未开通等,不记账)。""" + if attempt.finished: + return + attempt.finished = True + now = time.time() + with self._lock: + for book, key in ((self._pending_accounts, attempt.account), (self._pending_ips, attempt.ip)): + book[key] -= 1 + if book[key] <= 0: + del book[key] + self._pending_upstream = max(0, self._pending_upstream - 1) + if outcome in ("credentials", "unknown_account"): + self._accounts.setdefault(attempt.account, deque()).append(now) + self._ips.setdefault(attempt.ip, deque()).append(now) + # ZYT 只给"账号存在、密码不对"累计出口 IP 的失败次数。 + if outcome == "credentials": + self._upstream.append(now) + elif outcome == "success": + self._accounts.pop(attempt.account, None) + # 密码校验通过时 ZYT 会清零该出口 IP 的失败计数(AdminAccountSafeCache::relieve)。 + self._upstream.clear() + + def locked(self, seconds: int) -> None: + with self._lock: + self._locked_until = max(self._locked_until, time.time() + max(60, int(seconds))) + + +class ZytMobileAuth: + """默认实现:真的去调 ZYT。测试里换成假的。""" + + def __init__(self, api_url: Callable[[], str]): + self._api_url = api_url + + def login(self, account: str, password: str) -> str: + return zyt_auth.login_admin( + account, password, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def identity(self, token: str) -> dict[str, Any]: + return zyt_auth.verify_admin_session( + token, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def logout(self, token: str) -> None: + zyt_auth.logout_admin(token, api_url=self._api_url()) + + +class _Waiter: + __slots__ = ("key", "loop", "event") + + def __init__(self, key: tuple[int, str]): + self.key = key + self.loop = asyncio.get_running_loop() + self.event = asyncio.Event() + + async def wait(self, timeout: float) -> None: + try: + await asyncio.wait_for(self.event.wait(), timeout=max(0.0, timeout)) + except asyncio.TimeoutError: + pass + + +class CommandNotifier: + """进程内唤醒:有新指令或黑名单变化时,立刻结束对应电脑挂着的长轮询。 + + 先登记再查库:登记和查库之间落库的指令也能把它叫醒,不会白等一个复查周期。 + 唤醒走 ``call_soon_threadsafe``,发通知的请求和挂起的长轮询不在同一个事件 + 循环(线程)里也不会漏掉。 + """ + + def __init__(self) -> None: + self._waiters: dict[tuple[int, str], set[_Waiter]] = {} + self._lock = threading.Lock() + + def register(self, account_id: int, device_id: str) -> _Waiter: + waiter = _Waiter((int(account_id), str(device_id))) + with self._lock: + self._waiters.setdefault(waiter.key, set()).add(waiter) + return waiter + + def unregister(self, waiter: _Waiter) -> None: + with self._lock: + waiters = self._waiters.get(waiter.key) + if waiters is not None: + waiters.discard(waiter) + if not waiters: + self._waiters.pop(waiter.key, None) + + def notify(self, account_id: int, device_id: str | None = None) -> None: + with self._lock: + targets = [ + waiter + for key, waiters in self._waiters.items() + if key[0] == int(account_id) and (device_id is None or key[1] == device_id) + for waiter in waiters + ] + for waiter in targets: + try: + waiter.loop.call_soon_threadsafe(waiter.event.set) + except RuntimeError: + # 事件循环已经关闭(请求刚好结束),没有人需要被叫醒。 + continue + + +class MobilePrincipal: + def __init__(self, row: dict[str, Any], token: str): + self.row = row + self.token = token + self.permissions = account_permissions(row) + + @property + def account_id(self) -> int: + return int(self.row["id"]) + + @property + def session_id(self) -> int: + return int(self.row["session_id"]) + + @property + def label(self) -> str: + return f"{account_label(self.row)}(手机)" + + def me(self) -> dict[str, Any]: + return { + "account": { + "id": self.account_id, + "nickname": str(self.row.get("nickname") or ""), + "sn": str(self.row.get("zyt_sn") or ""), + "avatar": str(self.row.get("avatar") or ""), + }, + "permissions": { + "control": self.permissions["control"], + "control_all": self.permissions["control_all"], + "blacklist_add": self.permissions["blacklist_add"], + "blacklist_remove": self.permissions["blacklist_remove"], + "root": self.permissions["root"], + "known": self.permissions["known"], + }, + "permission_codes": { + "control": PERM_REMOTE_CONTROL, + "control_all": PERM_REMOTE_ALL, + "blacklist_add": PERM_BLACKLIST_ADD, + "blacklist_remove": PERM_BLACKLIST_REMOVE, + }, + "session": { + "created_at": str(self.row.get("session_created_at") or ""), + "expires_at": int(self.row.get("session_expires_at") or 0), + }, + } + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class MobileLoginBody(BaseModel): + account: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=1, max_length=128, repr=False) + client_name: str = Field(default="", max_length=120) + + +class CommandBody(BaseModel): + type: str = Field(min_length=1, max_length=32) + + +class BlacklistBody(BaseModel): + name: str = Field(default="", max_length=191) + account_name: str = Field(default="", max_length=191) + reason: str = Field(default="", max_length=200) + + +class StatusBody(BaseModel): + listening: bool = False + state: str = Field(default="", max_length=32) + transport: str = Field(default="", max_length=16) + wecom_connected: bool = False + pending_count: int = Field(default=0, ge=0, le=1_000_000) + app_version: str = Field(default="", max_length=64) + capabilities: list[str] = Field(default_factory=list, max_length=16) + + +class DesktopPollBody(BaseModel): + wait: int = Field(default=15, ge=0, le=60) + status: StatusBody | None = None + # 桌面端当前持有的黑名单版本;与服务端不一致时立即返回,让它去同步。 + blacklist_version: int | None = Field(default=None, ge=0) + + +class AckBody(BaseModel): + ok: bool + result: dict[str, Any] = Field(default_factory=dict) + error: str = Field(default="", max_length=500) + status: StatusBody | None = None + + +class DesktopBlacklistSyncBody(BaseModel): + add: list[dict[str, Any]] = Field(default_factory=list, max_length=500) + + +def register_remote_routes( + app: FastAPI, + database: Any, + *, + current_desktop: Callable[..., Any], + zyt_api_url: Callable[[], str], + zyt_mobile_auth: Any = None, + login_guard: MobileLoginGuard | None = None, +) -> RemoteStore: + store = RemoteStore(database) + store.initialize() + app.state.remote_store = store + notifier = CommandNotifier() + app.state.remote_notifier = notifier + guard = login_guard or MobileLoginGuard() + auth = zyt_mobile_auth or ZytMobileAuth(zyt_api_url) + identity_locks: dict[int, asyncio.Lock] = {} + + # ── 手机端鉴权 ─────────────────────────────────────────────────────── + async def _recheck_identity(row: dict[str, Any], request: Request) -> dict[str, Any] | None: + # 同一会话并发的几个请求只复核一次;用完即删,锁表不随会话数增长。 + session_id = int(row["session_id"]) + lock = identity_locks.setdefault(session_id, asyncio.Lock()) + try: + async with lock: + return await _recheck_identity_locked(session_id, request) + finally: + if not lock.locked() and identity_locks.get(session_id) is lock: + identity_locks.pop(session_id, None) + + async def _recheck_identity_locked(session_id: int, request: Request) -> dict[str, Any] | None: + fresh = await asyncio.to_thread(store.mobile_session, _bearer(request)) + if fresh is None: + return None + if time.time() - int(fresh["identity_checked_at"] or 0) < MOBILE_IDENTITY_RECHECK_SECONDS: + return fresh + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, session_id) + try: + if not zyt_token: + raise zyt_auth.ZytLoginError("ZYT 会话缺失", kind="rejected") + identity = await asyncio.to_thread(auth.identity, zyt_token) + if str(identity.get("user_id") or "") != str(fresh["zyt_user_id"]): + raise zyt_auth.ZytLoginError("ZYT 身份与手机会话不一致", kind="rejected") + except zyt_auth.ZytAuthError as exc: + if getattr(exc, "kind", "") == "unavailable": + # ZYT 暂时连不上不算下线:沿用上次同步的权限,一分钟后再试。 + await asyncio.to_thread(store.mark_identity_checked, session_id, retry_in=60) + return fresh + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="zyt_invalid", ip=trusted_client_ip(request), + ) + return None + try: + await asyncio.to_thread(store.refresh_identity, session_id, identity) + except PermissionError: + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="account_disabled", ip=trusted_client_ip(request), + ) + return None + return await asyncio.to_thread(store.mobile_session, _bearer(request)) + + async def current_mobile(request: Request) -> MobilePrincipal: + token = _bearer(request) + row = await asyncio.to_thread(store.mobile_session, token) if token else None + if row is not None and time.time() - int(row["identity_checked_at"] or 0) >= MOBILE_IDENTITY_RECHECK_SECONDS: + row = await _recheck_identity(row, request) + if row is None: + raise _error(401, "session_expired", "登录已失效,请重新登录") + return MobilePrincipal(row, token) + + def target_account(principal: MobilePrincipal, account_id: int | None) -> dict[str, Any]: + if account_id is None or int(account_id) == principal.account_id: + return principal.row + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", "只能查看和控制自己的电脑") + account = store.account(int(account_id)) + if account is None or str(account.get("status")) != "active": + raise _error(404, "account_not_found", "客服账号不存在或已停用") + return account + + # ── 手机端:登录 ───────────────────────────────────────────────────── + @app.post("/api/v2/mobile/auth/login") + async def mobile_login(body: MobileLoginBody, request: Request) -> dict: + ip = trusted_client_ip(request) + if https_required(request): + raise _error(403, "https_required", "请通过 HTTPS 地址访问手机控制台,明文连接不能提交密码") + account_key = body.account.strip().lower() + attempt = guard.begin(ip, account_key) + outcome = "other" + try: + zyt_token = await asyncio.to_thread(auth.login, body.account.strip(), body.password) + outcome = "success" + except zyt_auth.ZytLoginError as exc: + if exc.kind in ("credentials", "unknown_account"): + outcome = exc.kind + database.audit(None, "mobile.login.failed", f"account={account_key[:64]}", ip) + raise _error(401, "invalid_credentials", "账号或密码错误") from exc + if exc.kind == "password_change_required": + outcome = "success" + raise _error(403, "password_change_required", str(exc)) from exc + if exc.kind == "locked": + guard.locked(exc.retry_after or 1800) + database.audit(None, "mobile.login.locked", f"account={account_key[:64]}", ip) + raise _error(429, "zyt_locked", str(exc), retry_after=exc.retry_after) from exc + if exc.kind == "disabled": + raise _error(403, "account_disabled", str(exc)) from exc + if exc.kind == "terminal_unsupported": + raise _error(503, "terminal_unsupported", str(exc)) from exc + if exc.kind == "unavailable": + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + raise _error(400, "login_rejected", str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + finally: + guard.finish(attempt, outcome) + try: + identity = await asyncio.to_thread(auth.identity, zyt_token) + account = await asyncio.to_thread(store.upsert_account, identity) + except zyt_auth.ZytLoginError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + status_code = {"terminal_unsupported": 503, "disabled": 403}.get(exc.kind, 502) + code = {"terminal_unsupported": "terminal_unsupported", + "disabled": "account_disabled"}.get(exc.kind, "zyt_unavailable") + raise _error(status_code, code, str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + except PermissionError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(403, "account_disabled", str(exc)) from exc + except ValueError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + token, session = await asyncio.to_thread( + store.create_mobile_session, int(account["id"]), zyt_token=zyt_token, + client_name=body.client_name, user_agent=request.headers.get("user-agent", ""), ip=ip, + ) + row = await asyncio.to_thread(store.mobile_session, token) + principal = MobilePrincipal(row, token) + return {"access_token": token, "expires_at": session["expires_at"], "me": principal.me()} + + @app.post("/api/v2/mobile/auth/logout") + async def mobile_logout(request: Request, principal: MobilePrincipal = Depends(current_mobile)) -> dict: + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, principal.session_id) + await asyncio.to_thread( + store.revoke_mobile_session, session_id=principal.session_id, reason="logout", + ip=trusted_client_ip(request), + ) + if zyt_token: + await asyncio.to_thread(auth.logout, zyt_token) + return {"ok": True} + + @app.get("/api/v2/mobile/me") + async def mobile_me(principal: MobilePrincipal = Depends(current_mobile)) -> dict: + return principal.me() + + # ── 手机端:电脑 ───────────────────────────────────────────────────── + @app.get("/api/v2/mobile/accounts") + async def mobile_accounts( + q: str = Query(default="", max_length=64), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", f"查看全部客服电脑需要 ZYT 权限 {PERM_REMOTE_ALL}") + return {"items": await asyncio.to_thread(store.accounts_overview, q)} + + @app.get("/api/v2/mobile/desktops") + async def mobile_desktops( + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + items = await asyncio.to_thread(store.devices, int(account["id"])) + return { + "account": { + "id": int(account["id"]), + "nickname": str(account.get("nickname") or ""), + "sn": str(account.get("zyt_sn") or ""), + }, + "items": items, + "server_time": int(time.time()), + } + + @app.post("/api/v2/mobile/desktops/{device_id}/commands") + async def mobile_create_command( + device_id: str, body: CommandBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + if not principal.permissions["control"]: + raise _error(403, "forbidden", f"远程启停需要 ZYT 权限 {PERM_REMOTE_CONTROL}") + if body.type not in COMMAND_TYPES: + raise _error(400, "invalid_command", "不支持的指令") + try: + device_id = valid_device_id(device_id) + except ValueError as exc: + raise _error(404, "device_not_found", "电脑不存在") from exc + device = await asyncio.to_thread(store.device, int(account["id"]), device_id) + if device is None: + raise _error(404, "device_not_found", "电脑不存在或已被管理端下线") + if not device["online"]: + raise _error(409, "device_offline", "电脑离线:客服软件没有运行,无法远程拉起") + if not device["remote_capable"]: + raise _error(409, "client_outdated", "该电脑上的客服软件版本过低,请升级后再远程控制") + try: + command = await asyncio.to_thread( + store.create_command, account_id=int(account["id"]), device_id=device_id, + command_type=body.type, actor_account_id=principal.account_id, + actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "device_not_found", str(exc)) from exc + notifier.notify(int(account["id"]), device_id) + return {"command": command} + + @app.get("/api/v2/mobile/commands/{command_id}") + async def mobile_command( + command_id: str, principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + command = await asyncio.to_thread(store.command, command_id) + if command is None: + raise _error(404, "command_not_found", "指令不存在") + if command["account_id"] != principal.account_id and not principal.permissions["control_all"]: + raise _error(404, "command_not_found", "指令不存在") + command.pop("account_id", None) + return {"command": command} + + # ── 手机端:会话与黑名单 ───────────────────────────────────────────── + @app.get("/api/v2/mobile/conversations") + async def mobile_conversations( + account_id: int | None = Query(default=None, ge=1), + q: str = Query(default="", max_length=100), + scope: str = Query(default="all", pattern="^(all|blocked)$"), + cursor: str = Query(default="", max_length=200), + limit: int = Query(default=30, ge=1, le=100), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + try: + return await asyncio.to_thread( + store.conversations, str(account["tenant_id"]), query=q, scope=scope, + cursor=cursor, limit=limit, + ) + except ValueError as exc: + raise _error(400, "invalid_cursor", str(exc)) from exc + + @app.put("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_add( + account: str, conversation: str, body: BlacklistBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_add"]: + raise _error(403, "forbidden", f"加入黑名单需要 ZYT 权限 {PERM_BLACKLIST_ADD}") + try: + entry, version = await asyncio.to_thread( + store.add_blacklist, str(target["tenant_id"]), + {"account": account, "conversation": conversation, **body.model_dump()}, + source="mobile", actor_label=principal.label, + actor_account_id=principal.account_id, ip=trusted_client_ip(request), + ) + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"entry": entry, "version": version} + + @app.delete("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_remove( + account: str, conversation: str, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_remove"]: + raise _error(403, "forbidden", f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管") + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(target["tenant_id"]), account, conversation, + source="mobile", actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"ok": True, "version": version} + + # ── 桌面端:命令通道 ───────────────────────────────────────────────── + def _desktop_label(account: Any) -> str: + return f"{account_label(account)}(电脑 {str(account['device_name'] or '')[:40]})" + + async def _report(account: Any, status: StatusBody | None) -> None: + if status is None: + return + await asyncio.to_thread( + store.report_status, int(account["id"]), str(account["device_id"]), + status.model_dump(), app_version=str(account["app_version"] or ""), + ) + + @app.post("/api/v2/desktop/commands/poll") + async def desktop_poll( + body: DesktopPollBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + account_id = int(account["id"]) + device_id = str(account["device_id"]) + tenant = str(account["tenant_id"]) + await _report(account, body.status) + wait = float(min(body.wait, MAX_POLL_WAIT_SECONDS)) + deadline = time.monotonic() + wait + while True: + waiter = notifier.register(account_id, device_id) + try: + commands = await asyncio.to_thread(store.take_commands, account_id, device_id) + version = await asyncio.to_thread(store.blacklist_version, tenant) + remaining = deadline - time.monotonic() + if ( + commands + or (body.blacklist_version is not None and version != body.blacklist_version) + or remaining <= 0 + or await request.is_disconnected() + ): + break + await waiter.wait(min(remaining, POLL_RECHECK_SECONDS)) + finally: + notifier.unregister(waiter) + return {"commands": commands, "blacklist_version": version, "server_time": int(time.time())} + + @app.post("/api/v2/desktop/commands/{command_id}/ack") + async def desktop_ack( + command_id: str, body: AckBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + await _report(account, body.status) + try: + command = await asyncio.to_thread( + store.ack_command, account_id=int(account["id"]), + device_id=str(account["device_id"]), command_id=command_id, ok=body.ok, + result=body.result, error=body.error, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "command_not_found", str(exc)) from exc + return {"ok": True, "command": command} + + @app.post("/api/v2/desktop/status") + async def desktop_status(body: StatusBody, account: Any = Depends(current_desktop)) -> dict: + await _report(account, body) + return {"ok": True} + + # ── 桌面端:黑名单 ─────────────────────────────────────────────────── + @app.get("/api/v2/desktop/blacklist") + async def desktop_blacklist(account: Any = Depends(current_desktop)) -> dict: + version, items = await asyncio.to_thread(store.blacklist, str(account["tenant_id"])) + return {"version": version, "items": items} + + @app.post("/api/v2/desktop/blacklist/sync") + async def desktop_blacklist_sync( + body: DesktopBlacklistSyncBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + # 电脑前的人加黑名单不要求 ZYT 权限:拉黑只会让系统少发消息,是安全方向; + # 离线时加的、升级前本地就有的条目也都从这里补传。 + before = await asyncio.to_thread(store.blacklist_version, str(account["tenant_id"])) + version, items, rejected = await asyncio.to_thread( + store.sync_desktop_blacklist, str(account["tenant_id"]), body.add, + actor_label=_desktop_label(account), actor_account_id=int(account["id"]), + ip=trusted_client_ip(request), + ) + if version != before: + notifier.notify(int(account["id"])) + return {"version": version, "items": items, "rejected": rejected} + + @app.delete("/api/v2/desktop/blacklist/{account_key}/{conversation:path}") + async def desktop_blacklist_remove( + account_key: str, conversation: str, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + if not account_permissions(account)["blacklist_remove"]: + raise _error( + 403, "forbidden", + f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管在手机控制台操作", + ) + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(account["tenant_id"]), account_key, conversation, + source="desktop", actor_label=_desktop_label(account), ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(account["id"])) + return {"ok": True, "version": version} + + return store diff --git a/deploy/mobile-remote-20260928/payload/wechat_rpa/remote_store.py b/deploy/mobile-remote-20260928/payload/wechat_rpa/remote_store.py new file mode 100644 index 000000000..350de5ed8 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/wechat_rpa/remote_store.py @@ -0,0 +1,1149 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台的数据层。 + +四张表,全部挂在已有的 ``desktop_accounts`` 上: + + mobile_sessions 手机端登录会话。和桌面会话分表存放:桌面会话表上有 + "每个账号只能有一个有效会话"的唯一索引,手机登录如果 + 也写进去,就会把正在值班的电脑顶下线。分表之后两种 + 令牌也天然不能互相冒用。 + desktop_commands 下发给某台电脑的一次性命令(启动 / 停止监听)。 + desktop_status 每台电脑随长轮询上报的最新运行状态。 + conversation_blacklist 服务端会话黑名单。桌面端本地库降级为缓存,协议引擎和 + 视觉流程仍只读本地库,离线时照旧生效。 + +客服电脑在内网、没有入站端口,服务端永远不主动连电脑:命令只是落库,等电脑下一次 +长轮询时带走,执行完再回执。 +""" + +from __future__ import annotations + +import json +import re +import secrets +import time +import uuid +from datetime import datetime +from typing import Any, Iterable + +from admin_backend import now_text, token_hash + +# ── ZYT 菜单权限码(在 ZYT 角色管理里勾选,客服后台只做校验)────────────── +PERM_REMOTE_CONTROL = "wecom_rpa.remote/control" +PERM_REMOTE_ALL = "wecom_rpa.remote/all" +PERM_BLACKLIST_ADD = "wecom_rpa.blacklist/add" +PERM_BLACKLIST_REMOVE = "wecom_rpa.blacklist/remove" + +COMMAND_TYPES = {"start_listening": "启动监听", "stop_listening": "停止监听"} +COMMAND_STATUS_LABELS = { + "pending": "已下发,等待电脑领取", + "delivered": "电脑已领取,正在执行", + "succeeded": "已执行", + "failed": "执行失败", + "expired": "电脑未在 10 分钟内确认,已过期", + "superseded": "已被更新的指令取代", +} +FINAL_COMMAND_STATUSES = frozenset({"succeeded", "failed", "expired", "superseded"}) +COMMAND_TTL_SECONDS = 10 * 60 +MOBILE_SESSION_LIFETIME_SECONDS = 30 * 24 * 60 * 60 +# 手机会话每 10 分钟向 ZYT 复核一次身份与权限;复核本身也会让 ZYT 顺延令牌。 +MOBILE_IDENTITY_RECHECK_SECONDS = 10 * 60 +# 桌面端每 5 秒心跳一次(/desktop/me),30 秒没有心跳就算程序没开。 +DEVICE_ONLINE_SECONDS = 30 +# 命令通道每次长轮询(最长 25 秒)都会顺带上报状态。 +STATUS_FRESH_SECONDS = 90 +CAPABILITY_COMMANDS = "commands_v1" +CAPABILITY_BLACKLIST = "blacklist_sync_v1" + +STATE_LABELS = { + "running": "监听中", + "connecting": "正在连接企业微信", + "waiting": "等待企业微信", + "verification": "等待企业微信验证", + "recovering": "自动恢复中", + "stopping": "正在停止", + "stopped": "已停止", + "error": "连接失败", +} +TRANSPORT_LABELS = {"visual": "视觉通道", "protocol": "协议通道"} +CONVERSATION_TYPE_LABELS = { + "direct_wechat": "微信联系人", + "direct_wecom": "企业微信联系人", + "group": "群聊", + "application": "应用消息", + "service": "服务通知", + "unknown": "会话", +} + +_DEVICE_ID_RE = re.compile(r"^[A-Za-z0-9._:@-]{1,128}$") +_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f]") +MAX_SYNC_ITEMS = 500 + + +def _now_epoch() -> int: + return int(time.time()) + + +def _epoch_of(text: Any) -> int | None: + value = str(text or "").strip() + if not value: + return None + try: + return int(datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()) + except ValueError: + return None + + +def _seconds_since(epoch: int | None, now: int) -> int | None: + return None if epoch is None else max(0, now - int(epoch)) + + +def valid_device_id(value: Any) -> str: + text = str(value or "").strip() + if not _DEVICE_ID_RE.fullmatch(text): + raise ValueError("设备标识不正确") + return text + + +def clean_key(value: Any, label: str, *, limit: int) -> str: + """黑名单的账号 / 会话标识:来自企业微信本地库,原样保存,只挡控制字符。""" + text = str(value or "").strip() + if not text or len(text) > limit or _CONTROL_CHARS_RE.search(text): + raise ValueError(f"{label}不正确") + return text + + +def clean_text(value: Any, *, limit: int) -> str: + return _CONTROL_CHARS_RE.sub(" ", str(value or "")).strip()[:limit] + + +def _like(value: str) -> str: + escaped = str(value).replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + return f"%{escaped}%" + + +def account_permissions(account: Any) -> dict[str, bool]: + """从 desktop_accounts 行里同步下来的 ZYT 权限推出手机端能做什么。 + + ZYT 对 root 以及"拥有全部菜单"的角色返回 ``*``。remote/all 包含了控制本人 + 电脑的能力。 + """ + try: + raw = json.loads(str(account["zyt_permissions_json"] or "[]")) + except (KeyError, IndexError, TypeError, ValueError): + raw = [] + granted = {str(item).strip().lower() for item in raw if str(item).strip()} if isinstance(raw, list) else set() + try: + root = bool(int(account["zyt_root"] or 0)) or "*" in granted + except (KeyError, IndexError, TypeError, ValueError): + root = "*" in granted + try: + known = bool(int(account["zyt_permissions_known"] or 0)) + except (KeyError, IndexError, TypeError, ValueError): + known = False + + def has(code: str) -> bool: + return root or code in granted + + return { + "root": root, + "known": known, + "control": has(PERM_REMOTE_CONTROL) or has(PERM_REMOTE_ALL), + "control_all": has(PERM_REMOTE_ALL), + "blacklist_add": has(PERM_BLACKLIST_ADD), + "blacklist_remove": has(PERM_BLACKLIST_REMOVE), + } + + +def account_label(account: Any) -> str: + try: + return str(account["nickname"] or account["zyt_sn"] or f"账号{account['id']}") + except (KeyError, IndexError): + return "未知账号" + + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS mobile_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + token_digest TEXT NOT NULL UNIQUE, + zyt_token_enc TEXT NOT NULL DEFAULT '', + client_name TEXT NOT NULL DEFAULT '', + user_agent TEXT NOT NULL DEFAULT '', + ip_address TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + last_used_at TEXT NOT NULL, + last_used_epoch INTEGER NOT NULL DEFAULT 0, + expires_at INTEGER NOT NULL, + identity_checked_at INTEGER NOT NULL DEFAULT 0, + revoked_at TEXT NOT NULL DEFAULT '', + revoke_reason TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_mobile_sessions_account + ON mobile_sessions(account_id, revoked_at, expires_at); + +CREATE TABLE IF NOT EXISTS desktop_commands ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command_id TEXT NOT NULL UNIQUE, + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + device_id TEXT NOT NULL, + type TEXT NOT NULL CHECK(type IN ('start_listening','stop_listening')), + payload_json TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'pending' + CHECK(status IN ('pending','delivered','succeeded','failed','expired','superseded')), + created_by_account_id INTEGER, + created_by TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + expires_at INTEGER NOT NULL, + delivered_at TEXT NOT NULL DEFAULT '', + delivery_count INTEGER NOT NULL DEFAULT 0, + acked_at TEXT NOT NULL DEFAULT '', + result_json TEXT NOT NULL DEFAULT '{}', + error TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_desktop_commands_target + ON desktop_commands(account_id, device_id, status, id); + +CREATE TABLE IF NOT EXISTS desktop_status ( + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + device_id TEXT NOT NULL, + listening INTEGER NOT NULL DEFAULT 0, + state TEXT NOT NULL DEFAULT '', + transport TEXT NOT NULL DEFAULT '', + wecom_connected INTEGER NOT NULL DEFAULT 0, + pending_count INTEGER NOT NULL DEFAULT 0, + app_version TEXT NOT NULL DEFAULT '', + capabilities_json TEXT NOT NULL DEFAULT '[]', + reported_at TEXT NOT NULL, + reported_epoch INTEGER NOT NULL, + PRIMARY KEY(account_id, device_id) +); + +CREATE TABLE IF NOT EXISTS conversation_blacklist ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id TEXT NOT NULL, + account TEXT NOT NULL, + conversation TEXT NOT NULL, + display_name TEXT NOT NULL DEFAULT '', + account_name TEXT NOT NULL DEFAULT '', + reason TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT 'desktop', + created_by TEXT NOT NULL DEFAULT '', + created_by_account_id INTEGER, + created_at TEXT NOT NULL, + removed_at TEXT NOT NULL DEFAULT '', + removed_by TEXT NOT NULL DEFAULT '', + removed_source TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 0, + UNIQUE(tenant_id, account, conversation) +); +CREATE INDEX IF NOT EXISTS idx_conversation_blacklist_active + ON conversation_blacklist(tenant_id, removed_at, id DESC); +CREATE INDEX IF NOT EXISTS idx_conversation_blacklist_version + ON conversation_blacklist(tenant_id, version); +""" + + +class RemoteStore: + def __init__(self, database: Any): + self.database = database + + def initialize(self) -> None: + with self.database.connect() as db: + db.executescript(SCHEMA) + db.commit() + + # ── 账号 ───────────────────────────────────────────────────────────── + def upsert_account(self, identity: dict[str, Any]) -> dict[str, Any]: + """按 ZYT 身份落到同一条 desktop_accounts,并同步权限;不碰设备与桌面会话。 + + 没用过桌面端的主管第一次登录手机端时会新建一行,这一行在管理端的 + 「客户端账号」里显示为没有设备。 + """ + zyt_user_id = str(identity.get("user_id") or "").strip() + if not zyt_user_id: + raise ValueError("ZYT 用户标识不能为空") + permissions_value = identity.get("permissions") + permissions = sorted( + {str(item).strip() for item in permissions_value if str(item).strip()} + ) if isinstance(permissions_value, list) else [] + role_ids: list[int] = [] + for item in identity.get("role_ids") if isinstance(identity.get("role_ids"), list) else []: + try: + role_ids.append(int(item)) + except (TypeError, ValueError): + continue + root = int(bool(identity.get("root")) or "*" in permissions) + known = int(bool(identity.get("permissions_known", isinstance(permissions_value, list)))) + mobile = str(identity.get("mobile") or "") + if len(mobile) >= 7: + mobile = f"{mobile[:3]}****{mobile[-4:]}" + stamp = now_text() + tenant_id = self.database._desktop_tenant_id(zyt_user_id) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + db.execute( + """INSERT INTO desktop_accounts + (zyt_user_id,tenant_id,zyt_sn,nickname,mobile_masked,avatar, + zyt_root,zyt_role_ids_json,zyt_permissions_json,zyt_permissions_known, + status,created_at,updated_at,last_login_at,last_online_at) + VALUES (?,?,?,?,?,?,?,?,?,?,'active',?,?,'','') + ON CONFLICT(zyt_user_id) DO UPDATE SET + zyt_sn=excluded.zyt_sn,nickname=excluded.nickname, + avatar=excluded.avatar,zyt_root=excluded.zyt_root, + zyt_role_ids_json=excluded.zyt_role_ids_json, + zyt_permissions_json=excluded.zyt_permissions_json, + zyt_permissions_known=excluded.zyt_permissions_known, + updated_at=excluded.updated_at""", + ( + zyt_user_id, tenant_id, + str(identity.get("sn") or "")[:128], + str(identity.get("nickname") or "")[:191], + mobile[:32], + str(identity.get("avatar") or "")[:1024], + root, + json.dumps(sorted(set(role_ids))), + json.dumps(permissions, ensure_ascii=False), + known, stamp, stamp, + ), + ) + account = db.execute( + "SELECT * FROM desktop_accounts WHERE zyt_user_id=?", (zyt_user_id,) + ).fetchone() + if account is None or str(account["status"]) != "active": + db.rollback() + raise PermissionError("该账号已被管理端禁用,不能使用手机控制台") + db.commit() + return dict(account) + + def account(self, account_id: int) -> dict[str, Any] | None: + row = self.database.desktop_account(int(account_id)) + return dict(row) if row is not None else None + + # ── 手机会话 ───────────────────────────────────────────────────────── + def create_mobile_session( + self, account_id: int, *, zyt_token: str, client_name: str = "", + user_agent: str = "", ip: str = "", + lifetime: int = MOBILE_SESSION_LIFETIME_SECONDS, + ) -> tuple[str, dict[str, Any]]: + import secret_box + + token = secrets.token_urlsafe(48) + now = _now_epoch() + stamp = now_text() + expires_at = now + max(300, min(int(lifetime), MOBILE_SESSION_LIFETIME_SECONDS)) + encrypted = secret_box.encrypt(str(zyt_token or ""), self.database._secret_key()) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + account = db.execute( + "SELECT * FROM desktop_accounts WHERE id=? AND status='active'", (int(account_id),) + ).fetchone() + if account is None: + db.rollback() + raise PermissionError("该账号已被管理端禁用,不能使用手机控制台") + cursor = db.execute( + """INSERT INTO mobile_sessions + (account_id,token_digest,zyt_token_enc,client_name,user_agent,ip_address, + created_at,last_used_at,last_used_epoch,expires_at,identity_checked_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?)""", + ( + int(account_id), token_hash(token), encrypted, + clean_text(client_name, limit=120), clean_text(user_agent, limit=255), + str(ip or "")[:80], stamp, stamp, now, expires_at, now, + ), + ) + db.execute( + "DELETE FROM mobile_sessions WHERE expires_at'' AND revoked_at dict[str, Any] | None: + if not token: + return None + now = _now_epoch() + with self.database.connect() as db: + row = db.execute( + """SELECT a.*,s.id AS session_id,s.expires_at AS session_expires_at, + s.created_at AS session_created_at,s.identity_checked_at, + s.last_used_epoch + FROM mobile_sessions s JOIN desktop_accounts a ON a.id=s.account_id + WHERE s.token_digest=? AND s.revoked_at='' AND s.expires_at>=? + AND a.status='active'""", + (token_hash(token), now), + ).fetchone() + if row is None: + return None + if now - int(row["last_used_epoch"] or 0) >= 60: + db.execute( + "UPDATE mobile_sessions SET last_used_at=?,last_used_epoch=? WHERE id=?", + (now_text(), now, int(row["session_id"])), + ) + db.commit() + return dict(row) + + def mobile_zyt_token(self, session_id: int) -> str: + import secret_box + + with self.database.connect() as db: + row = db.execute( + "SELECT zyt_token_enc FROM mobile_sessions WHERE id=?", (int(session_id),) + ).fetchone() + if row is None or not str(row["zyt_token_enc"] or ""): + return "" + try: + return secret_box.decrypt(str(row["zyt_token_enc"]), self.database._secret_key()) + except Exception: + return "" + + def mark_identity_checked(self, session_id: int, *, retry_in: int | None = None) -> None: + """记录复核时间。ZYT 暂时连不上时用 retry_in 让下一次复核提前。""" + checked = _now_epoch() + if retry_in is not None: + checked = checked - MOBILE_IDENTITY_RECHECK_SECONDS + max(10, int(retry_in)) + with self.database.connect() as db: + db.execute( + "UPDATE mobile_sessions SET identity_checked_at=? WHERE id=?", + (checked, int(session_id)), + ) + db.commit() + + def refresh_identity(self, session_id: int, identity: dict[str, Any]) -> None: + account = self.upsert_account(identity) + with self.database.connect() as db: + db.execute( + "UPDATE mobile_sessions SET identity_checked_at=? WHERE id=? AND account_id=?", + (_now_epoch(), int(session_id), int(account["id"])), + ) + db.commit() + + def revoke_mobile_session( + self, *, token: str = "", session_id: int | None = None, reason: str = "logout", + ip: str = "", + ) -> None: + with self.database.connect() as db: + if token: + row = db.execute( + "SELECT id,account_id FROM mobile_sessions WHERE token_digest=?", + (token_hash(token),), + ).fetchone() + else: + row = db.execute( + "SELECT id,account_id FROM mobile_sessions WHERE id=?", (int(session_id or 0),) + ).fetchone() + if row is None: + return + db.execute( + """UPDATE mobile_sessions SET revoked_at=?,revoke_reason=?,zyt_token_enc='' + WHERE id=? AND revoked_at=''""", + (now_text(), str(reason)[:40], int(row["id"])), + ) + self.database._audit( + db, None, "mobile.logout" if reason == "logout" else "mobile.session.revoke", + f"account={row['account_id']} session={row['id']} reason={reason}", str(ip or ""), + ) + db.commit() + + def revoke_mobile_sessions_for_account(self, account_id: int, *, reason: str) -> int: + with self.database.connect() as db: + cursor = db.execute( + """UPDATE mobile_sessions SET revoked_at=?,revoke_reason=?,zyt_token_enc='' + WHERE account_id=? AND revoked_at=''""", + (now_text(), str(reason)[:40], int(account_id)), + ) + db.commit() + return int(cursor.rowcount or 0) + + def mobile_session_counts(self) -> dict[int, int]: + with self.database.connect() as db: + rows = db.execute( + """SELECT account_id,COUNT(*) FROM mobile_sessions + WHERE revoked_at='' AND expires_at>=? GROUP BY account_id""", + (_now_epoch(),), + ).fetchall() + return {int(row[0]): int(row[1]) for row in rows} + + # ── 设备与状态 ─────────────────────────────────────────────────────── + def report_status( + self, account_id: int, device_id: str, status: dict[str, Any], *, app_version: str = "" + ) -> None: + capabilities = status.get("capabilities") + capabilities = sorted({ + clean_text(item, limit=32) for item in capabilities if clean_text(item, limit=32) + })[:16] if isinstance(capabilities, list) else [] + try: + pending = max(0, min(int(status.get("pending_count") or 0), 1_000_000)) + except (TypeError, ValueError): + pending = 0 + now = _now_epoch() + with self.database.connect() as db: + db.execute( + """INSERT INTO desktop_status + (account_id,device_id,listening,state,transport,wecom_connected,pending_count, + app_version,capabilities_json,reported_at,reported_epoch) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(account_id,device_id) DO UPDATE SET + listening=excluded.listening,state=excluded.state, + transport=excluded.transport,wecom_connected=excluded.wecom_connected, + pending_count=excluded.pending_count,app_version=excluded.app_version, + capabilities_json=excluded.capabilities_json, + reported_at=excluded.reported_at,reported_epoch=excluded.reported_epoch""", + ( + int(account_id), valid_device_id(device_id), + int(bool(status.get("listening"))), + clean_text(status.get("state"), limit=32), + clean_text(status.get("transport"), limit=16), + int(bool(status.get("wecom_connected"))), + pending, + clean_text(status.get("app_version") or app_version, limit=64), + json.dumps(capabilities), now_text(), now, + ), + ) + db.commit() + + def devices(self, account_id: int, *, device_id: str = "") -> list[dict[str, Any]]: + now = _now_epoch() + params: list[Any] = [now, int(account_id)] + extra = "" + if device_id: + extra = " AND d.device_id=?" + params.append(valid_device_id(device_id)) + with self.database.connect() as db: + rows = db.execute( + f"""SELECT d.id AS device_row_id,d.device_id,d.device_name,d.app_version, + d.last_seen_at, + (SELECT COUNT(*) FROM desktop_sessions s + WHERE s.device_row_id=d.id AND s.revoked_at='' + AND s.expires_at>=?) AS active_sessions, + st.listening,st.state,st.transport,st.wecom_connected, + st.pending_count,st.app_version AS status_app_version, + st.capabilities_json,st.reported_at,st.reported_epoch + FROM desktop_devices d + LEFT JOIN desktop_status st + ON st.account_id=d.account_id AND st.device_id=d.device_id + WHERE d.account_id=? AND d.revoked_at=''{extra} + ORDER BY d.last_seen_at DESC,d.id DESC""", + tuple(params), + ).fetchall() + commands = {} + for row in rows: + latest = db.execute( + """SELECT * FROM desktop_commands WHERE account_id=? AND device_id=? + ORDER BY id DESC LIMIT 1""", + (int(account_id), str(row["device_id"])), + ).fetchone() + if latest is not None: + commands[str(row["device_id"])] = self._public_command(latest, now) + return [self._public_device(row, commands.get(str(row["device_id"])), now) for row in rows] + + def device(self, account_id: int, device_id: str) -> dict[str, Any] | None: + items = self.devices(account_id, device_id=device_id) + return items[0] if items else None + + @staticmethod + def _public_device(row: Any, command: dict[str, Any] | None, now: int) -> dict[str, Any]: + seen_epoch = _epoch_of(row["last_seen_at"]) + seen_seconds = _seconds_since(seen_epoch, now) + session_active = int(row["active_sessions"] or 0) > 0 + online = bool(session_active and seen_seconds is not None and seen_seconds <= DEVICE_ONLINE_SECONDS) + status = None + remote_capable = False + blacklist_sync = False + if row["reported_epoch"] is not None: + try: + capabilities = set(json.loads(str(row["capabilities_json"] or "[]"))) + except (TypeError, ValueError): + capabilities = set() + reported_seconds = _seconds_since(int(row["reported_epoch"]), now) + fresh = reported_seconds is not None and reported_seconds <= STATUS_FRESH_SECONDS + same_build = str(row["status_app_version"] or "") == str(row["app_version"] or "") + # 同一台电脑降级回旧版本时,旧的能力声明不能继续算数。 + remote_capable = CAPABILITY_COMMANDS in capabilities and (fresh or same_build) + blacklist_sync = CAPABILITY_BLACKLIST in capabilities and (fresh or same_build) + state = str(row["state"] or "") + transport = str(row["transport"] or "") + status = { + "listening": bool(row["listening"]), + "state": state, + "state_label": STATE_LABELS.get(state, state or "未知"), + "transport": transport, + "transport_label": TRANSPORT_LABELS.get(transport, transport or "未知"), + "wecom_connected": bool(row["wecom_connected"]), + "pending_count": int(row["pending_count"] or 0), + "reported_at": str(row["reported_at"] or ""), + "reported_seconds": reported_seconds, + "fresh": fresh, + } + return { + "device_id": str(row["device_id"]), + "device_name": str(row["device_name"] or ""), + "app_version": str(row["app_version"] or ""), + "online": online, + "session_active": session_active, + "last_seen_at": str(row["last_seen_at"] or ""), + "last_seen_seconds": seen_seconds, + "remote_capable": remote_capable, + "blacklist_sync": blacklist_sync, + "status": status, + "last_command": command, + } + + def accounts_overview(self, query: str = "", limit: int = 100) -> list[dict[str, Any]]: + """全部客服电脑的概况,只给有 remote/all 权限的人看。""" + params: list[Any] = [] + where = "WHERE a.status='active'" + needle = str(query or "").strip() + if needle: + where += " AND (a.nickname LIKE ? ESCAPE '\\' OR a.zyt_sn LIKE ? ESCAPE '\\')" + params.extend([_like(needle), _like(needle)]) + params.append(max(1, min(int(limit or 100), 300))) + with self.database.connect() as db: + rows = db.execute( + f"""SELECT a.id,a.nickname,a.zyt_sn,a.last_online_at, + (SELECT COUNT(*) FROM desktop_devices d + WHERE d.account_id=a.id AND d.revoked_at='') AS device_count + FROM desktop_accounts a {where} + ORDER BY a.last_online_at DESC,a.id DESC LIMIT ?""", + tuple(params), + ).fetchall() + result = [] + for row in rows: + devices = self.devices(int(row["id"])) if int(row["device_count"] or 0) else [] + result.append({ + "id": int(row["id"]), + "nickname": str(row["nickname"] or ""), + "sn": str(row["zyt_sn"] or ""), + "device_count": len(devices), + "online": any(item["online"] for item in devices), + "listening": any( + item["online"] and item["status"] and item["status"]["listening"] + for item in devices + ), + "last_online_at": str(row["last_online_at"] or ""), + }) + return result + + # ── 命令 ───────────────────────────────────────────────────────────── + @staticmethod + def _public_command(row: Any, now: int | None = None) -> dict[str, Any]: + now = _now_epoch() if now is None else now + status = str(row["status"]) + if status in {"pending", "delivered"} and int(row["expires_at"]) < now: + status = "expired" + try: + result = json.loads(str(row["result_json"] or "{}")) + except (TypeError, ValueError): + result = {} + return { + "id": str(row["command_id"]), + "type": str(row["type"]), + "type_label": COMMAND_TYPES.get(str(row["type"]), str(row["type"])), + "device_id": str(row["device_id"]), + "status": status, + "status_label": COMMAND_STATUS_LABELS.get(status, status), + "final": status in FINAL_COMMAND_STATUSES, + "created_at": str(row["created_at"]), + "created_by": str(row["created_by"] or ""), + "delivered_at": str(row["delivered_at"] or ""), + "acked_at": str(row["acked_at"] or ""), + "expires_at": int(row["expires_at"]), + "result": result if isinstance(result, dict) else {}, + "error": str(row["error"] or ""), + } + + def create_command( + self, *, account_id: int, device_id: str, command_type: str, + actor_account_id: int, actor_label: str, ip: str, + ) -> dict[str, Any]: + if command_type not in COMMAND_TYPES: + raise ValueError("不支持的指令") + device_id = valid_device_id(device_id) + now = _now_epoch() + stamp = now_text() + command_id = uuid.uuid4().hex + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + if db.execute( + """SELECT 1 FROM desktop_devices + WHERE account_id=? AND device_id=? AND revoked_at=''""", + (int(account_id), device_id), + ).fetchone() is None: + db.rollback() + raise LookupError("电脑不存在或已被管理端下线") + # 还没被电脑领走的旧指令直接作废:连点"停止、启动"时,电脑只该执行最后一次。 + # 已经领走的正在执行,不能撤回,交给电脑按顺序执行。 + db.execute( + """UPDATE desktop_commands SET status='superseded',acked_at=? + WHERE account_id=? AND device_id=? AND status='pending'""", + (stamp, int(account_id), device_id), + ) + db.execute( + """INSERT INTO desktop_commands + (command_id,account_id,device_id,type,payload_json,status, + created_by_account_id,created_by,created_at,expires_at) + VALUES (?,?,?,?,?,'pending',?,?,?,?)""", + ( + command_id, int(account_id), device_id, command_type, "{}", + int(actor_account_id), clean_text(actor_label, limit=120), stamp, + now + COMMAND_TTL_SECONDS, + ), + ) + self.database._audit( + db, None, "remote.command.create", + f"command={command_id} account={int(account_id)} device={device_id[:64]} " + f"type={command_type} by_account={int(actor_account_id)}", + str(ip or ""), + ) + db.commit() + row = db.execute( + "SELECT * FROM desktop_commands WHERE command_id=?", (command_id,) + ).fetchone() + return self._public_command(row, now) + + def command(self, command_id: str) -> dict[str, Any] | None: + with self.database.connect() as db: + row = db.execute( + "SELECT * FROM desktop_commands WHERE command_id=?", (str(command_id or ""),) + ).fetchone() + if row is None: + return None + payload = self._public_command(row) + payload["account_id"] = int(row["account_id"]) + return payload + + def has_open_commands(self, account_id: int, device_id: str) -> bool: + with self.database.connect() as db: + return db.execute( + """SELECT 1 FROM desktop_commands + WHERE account_id=? AND device_id=? AND status IN ('pending','delivered') + LIMIT 1""", + (int(account_id), str(device_id)), + ).fetchone() is not None + + def take_commands(self, account_id: int, device_id: str) -> list[dict[str, Any]]: + """把待执行指令交给电脑。 + + 领取不等于完成:电脑回执之前每次轮询都会再拿到一次,由电脑按指令 id 去重。 + 这样长轮询的响应在路上丢了,指令也不会跟着丢。 + """ + if not self.has_open_commands(account_id, device_id): + return [] + now = _now_epoch() + stamp = now_text() + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + db.execute( + """UPDATE desktop_commands SET status='expired' + WHERE account_id=? AND device_id=? AND status IN ('pending','delivered') + AND expires_at dict[str, Any]: + stamp = now_text() + now = _now_epoch() + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + """SELECT * FROM desktop_commands + WHERE command_id=? AND account_id=? AND device_id=?""", + (str(command_id or ""), int(account_id), str(device_id)), + ).fetchone() + if row is None: + db.rollback() + raise LookupError("指令不存在") + status = str(row["status"]) + if status in {"succeeded", "failed"}: + db.rollback() + return self._public_command(row, now) + if status in {"pending", "delivered"} and int(row["expires_at"]) >= now: + status = "succeeded" if ok else "failed" + elif status in {"pending", "delivered"}: + status = "expired" + safe_result = result if isinstance(result, dict) else {} + encoded = json.dumps(safe_result, ensure_ascii=False)[:4000] + db.execute( + """UPDATE desktop_commands SET status=?,acked_at=?,result_json=?,error=? + WHERE id=?""", + (status, stamp, encoded, clean_text(error, limit=500), int(row["id"])), + ) + self.database._audit( + db, None, "remote.command.ack", + f"command={row['command_id']} account={int(account_id)} status={status}", + str(ip or ""), + ) + db.commit() + row = db.execute("SELECT * FROM desktop_commands WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_command(row, now) + + # ── 黑名单 ─────────────────────────────────────────────────────────── + @staticmethod + def _public_entry(row: Any) -> dict[str, Any]: + return { + "account": str(row["account"]), + "account_name": str(row["account_name"] or ""), + "conversation": str(row["conversation"]), + "name": str(row["display_name"] or ""), + "reason": str(row["reason"] or ""), + "source": str(row["source"] or ""), + "created_by": str(row["created_by"] or ""), + "created_at": str(row["created_at"] or ""), + } + + @staticmethod + def _version(db: Any, tenant_id: str) -> int: + return int(db.execute( + "SELECT COALESCE(MAX(version),0) FROM conversation_blacklist WHERE tenant_id=?", + (tenant_id,), + ).fetchone()[0]) + + def blacklist_version(self, tenant_id: str) -> int: + with self.database.connect() as db: + return self._version(db, str(tenant_id)) + + def blacklist(self, tenant_id: str) -> tuple[int, list[dict[str, Any]]]: + with self.database.connect() as db: + rows = db.execute( + """SELECT * FROM conversation_blacklist WHERE tenant_id=? AND removed_at='' + ORDER BY account,conversation""", + (str(tenant_id),), + ).fetchall() + version = self._version(db, str(tenant_id)) + return version, [self._public_entry(row) for row in rows] + + @staticmethod + def _archive_names(db: Any, tenant_id: str, account: str, conversation: str) -> tuple[str, str]: + """补上会话名和企微账号名;归档表不存在或没有这条会话时返回空串。""" + try: + row = db.execute( + """SELECT c.name,a.display_name FROM archive_conversation c + JOIN archive_source_account a ON a.id=c.source_account_id + WHERE c.tenant_id=? AND a.external_account_id=? AND c.external_id=? + LIMIT 1""", + (tenant_id, account, conversation), + ).fetchone() + except Exception: + return "", "" + if row is None: + return "", "" + return str(row[0] or ""), str(row[1] or "") + + def _add_entry( + self, db: Any, tenant_id: str, item: dict[str, Any], *, source: str, + actor_label: str, actor_account_id: int | None, version: int, + ) -> tuple[dict[str, Any], bool]: + account = clean_key(item.get("account"), "企业微信账号", limit=191) + conversation = clean_key(item.get("conversation"), "会话", limit=512) + name = clean_text(item.get("name"), limit=191) + account_name = clean_text(item.get("account_name"), limit=191) + reason = clean_text(item.get("reason"), limit=200) + if not name or not account_name: + archive_name, archive_account = self._archive_names(db, tenant_id, account, conversation) + name = name or archive_name + account_name = account_name or archive_account + stamp = now_text() + row = db.execute( + """SELECT * FROM conversation_blacklist + WHERE tenant_id=? AND account=? AND conversation=?""", + (tenant_id, account, conversation), + ).fetchone() + if row is not None and not str(row["removed_at"] or ""): + changed = ( + (name and name != row["display_name"]) + or (account_name and account_name != row["account_name"]) + or (reason and reason != row["reason"]) + ) + if changed: + db.execute( + """UPDATE conversation_blacklist SET + display_name=CASE WHEN ?<>'' THEN ? ELSE display_name END, + account_name=CASE WHEN ?<>'' THEN ? ELSE account_name END, + reason=CASE WHEN ?<>'' THEN ? ELSE reason END, + updated_at=?,version=? WHERE id=?""", + (name, name, account_name, account_name, reason, reason, stamp, version, + int(row["id"])), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_entry(fresh), bool(changed) + if row is not None: + db.execute( + """UPDATE conversation_blacklist SET display_name=?,account_name=?,reason=?, + source=?,created_by=?,created_by_account_id=?,created_at=?, + removed_at='',removed_by='',removed_source='',updated_at=?,version=? + WHERE id=?""", + (name or row["display_name"], account_name or row["account_name"], reason, + source, clean_text(actor_label, limit=120), actor_account_id, stamp, stamp, + version, int(row["id"])), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_entry(fresh), True + cursor = db.execute( + """INSERT INTO conversation_blacklist + (tenant_id,account,conversation,display_name,account_name,reason,source, + created_by,created_by_account_id,created_at,updated_at,version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + (tenant_id, account, conversation, name, account_name, reason, source, + clean_text(actor_label, limit=120), actor_account_id, stamp, stamp, version), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(cursor.lastrowid),)).fetchone() + return self._public_entry(fresh), True + + def add_blacklist( + self, tenant_id: str, item: dict[str, Any], *, source: str, actor_label: str, + actor_account_id: int | None, ip: str, + ) -> tuple[dict[str, Any], int]: + tenant_id = str(tenant_id) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + version = self._version(db, tenant_id) + 1 + entry, changed = self._add_entry( + db, tenant_id, item, source=source, actor_label=actor_label, + actor_account_id=actor_account_id, version=version, + ) + if changed: + self.database._audit( + db, None, "blacklist.add", + f"tenant={tenant_id} account={entry['account'][:64]} " + f"conversation={entry['conversation'][:120]} source={source} " + f"by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + else: + db.rollback() + current = self._version(db, tenant_id) + return entry, current + + def remove_blacklist( + self, tenant_id: str, account: str, conversation: str, *, source: str, + actor_label: str, ip: str, + ) -> int: + tenant_id = str(tenant_id) + account = clean_key(account, "企业微信账号", limit=191) + conversation = clean_key(conversation, "会话", limit=512) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + """SELECT id FROM conversation_blacklist + WHERE tenant_id=? AND account=? AND conversation=? AND removed_at=''""", + (tenant_id, account, conversation), + ).fetchone() + if row is None: + db.rollback() + raise LookupError("该会话不在黑名单中") + version = self._version(db, tenant_id) + 1 + stamp = now_text() + db.execute( + """UPDATE conversation_blacklist SET removed_at=?,removed_by=?,removed_source=?, + updated_at=?,version=? WHERE id=?""", + (stamp, clean_text(actor_label, limit=120), source, stamp, version, int(row["id"])), + ) + self.database._audit( + db, None, "blacklist.remove", + f"tenant={tenant_id} account={account[:64]} conversation={conversation[:120]} " + f"source={source} by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + return version + + def sync_desktop_blacklist( + self, tenant_id: str, adds: Iterable[dict[str, Any]], *, actor_label: str, + actor_account_id: int | None, ip: str, + ) -> tuple[int, list[dict[str, Any]], list[dict[str, Any]]]: + """桌面端补传本地新增(含升级前的历史条目),然后拿回全量。""" + tenant_id = str(tenant_id) + items = list(adds or [])[:MAX_SYNC_ITEMS] + rejected: list[dict[str, Any]] = [] + added = 0 + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + version = self._version(db, tenant_id) + 1 + for item in items: + if not isinstance(item, dict): + continue + try: + _entry, changed = self._add_entry( + db, tenant_id, item, source="desktop", actor_label=actor_label, + actor_account_id=actor_account_id, version=version, + ) + except ValueError as exc: + rejected.append({ + "account": str(item.get("account") or "")[:191], + "conversation": str(item.get("conversation") or "")[:512], + "error": str(exc), + }) + continue + added += int(changed) + if added: + self.database._audit( + db, None, "blacklist.sync", + f"tenant={tenant_id} added={added} by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + else: + db.rollback() + version, entries = self.blacklist(tenant_id) + return version, entries, rejected + + # ── 会话 ───────────────────────────────────────────────────────────── + def conversations( + self, tenant_id: str, *, query: str = "", scope: str = "all", cursor: str = "", + limit: int = 30, + ) -> dict[str, Any]: + """手机端会话列表。只给名称、类型、最近时间和黑名单状态,不出聊天正文。""" + tenant_id = str(tenant_id) + limit = max(1, min(int(limit or 30), 100)) + needle = str(query or "").strip()[:100] + with self.database.connect() as db: + blocked_total = int(db.execute( + "SELECT COUNT(*) FROM conversation_blacklist WHERE tenant_id=? AND removed_at=''", + (tenant_id,), + ).fetchone()[0]) + if scope == "blocked": + params: list[Any] = [tenant_id] + where = "b.tenant_id=? AND b.removed_at=''" + if needle: + where += (" AND (b.display_name LIKE ? ESCAPE '\\' OR b.conversation LIKE ? ESCAPE '\\'" + " OR b.account_name LIKE ? ESCAPE '\\')") + params.extend([_like(needle)] * 3) + if cursor: + try: + where += " AND b.id limit + rows = rows[:limit] + items = [] + for row in rows: + archive_name, archive_account = ("", "") + if not row["display_name"] or not row["account_name"]: + archive_name, archive_account = self._archive_names( + db, tenant_id, str(row["account"]), str(row["conversation"]) + ) + conversation = str(row["conversation"]) + kind = _conversation_kind(conversation) + items.append({ + "account": str(row["account"]), + "account_name": str(row["account_name"] or archive_account or ""), + "conversation": conversation, + "name": str(row["display_name"] or archive_name or conversation), + "type": kind, + "type_label": CONVERSATION_TYPE_LABELS.get(kind, "会话"), + "last_message_at": "", + "blocked": True, + "blacklist": self._public_entry(row), + }) + next_cursor = str(rows[-1]["id"]) if has_more and rows else "" + return {"items": items, "next_cursor": next_cursor, "has_more": has_more, + "blocked_total": blocked_total} + + params = [tenant_id] + where = "c.tenant_id=?" + if needle: + where += " AND (c.name LIKE ? ESCAPE '\\' OR c.external_id LIKE ? ESCAPE '\\')" + params.extend([_like(needle), _like(needle)]) + if cursor: + try: + cursor_time, cursor_id = cursor.split("|", 1) + except ValueError as exc: + raise ValueError("分页游标不正确") from exc + where += " AND (c.last_message_at,c.id)<(?,?)" + params.extend([cursor_time, cursor_id]) + params.append(limit + 1) + try: + rows = db.execute( + f"""SELECT c.id,c.external_id,c.name,c.conversation_type,c.last_message_at, + a.external_account_id,a.display_name AS account_display_name, + b.id AS blacklist_id,b.account,b.conversation,b.display_name, + b.account_name,b.reason,b.source,b.created_by,b.created_at + FROM archive_conversation c + JOIN archive_source_account a ON a.id=c.source_account_id + LEFT JOIN conversation_blacklist b + ON b.tenant_id=c.tenant_id AND b.account=a.external_account_id + AND b.conversation=c.external_id AND b.removed_at='' + WHERE {where} + ORDER BY c.last_message_at DESC,c.id DESC LIMIT ?""", + tuple(params), + ).fetchall() + except Exception as exc: + if "no such table" in str(exc): + rows = [] + else: + raise + has_more = len(rows) > limit + rows = rows[:limit] + items = [] + for row in rows: + kind = str(row["conversation_type"] or "unknown") + items.append({ + "account": str(row["external_account_id"]), + "account_name": str(row["account_display_name"] or ""), + "conversation": str(row["external_id"]), + "name": str(row["name"] or row["external_id"]), + "type": kind, + "type_label": CONVERSATION_TYPE_LABELS.get(kind, "会话"), + "last_message_at": str(row["last_message_at"] or ""), + "blocked": row["blacklist_id"] is not None, + "blacklist": self._public_entry(row) if row["blacklist_id"] is not None else None, + }) + next_cursor = f"{rows[-1]['last_message_at']}|{rows[-1]['id']}" if has_more and rows else "" + return {"items": items, "next_cursor": next_cursor, "has_more": has_more, + "blocked_total": blocked_total} + + +def _conversation_kind(external_id: str) -> str: + prefix = str(external_id or "").split(":", 1)[0].upper() + return { + "M": "direct_wechat", "S": "direct_wecom", "R": "group", + "Y": "application", "O": "service", + }.get(prefix, "unknown") + + +def now_text_days_ago(days: int) -> str: + from datetime import timedelta + + return (datetime.now().astimezone() - timedelta(days=int(days))).isoformat(timespec="seconds") diff --git a/deploy/mobile-remote-20260928/payload/wechat_rpa/zyt_auth.py b/deploy/mobile-remote-20260928/payload/wechat_rpa/zyt_auth.py new file mode 100644 index 000000000..6fad0c3b9 --- /dev/null +++ b/deploy/mobile-remote-20260928/payload/wechat_rpa/zyt_auth.py @@ -0,0 +1,357 @@ +"""ZYT 管理员身份服务适配器。 + +桌面软件的密码只发送给 ZYT;客服后台拿到管理员 token 后通过最小会话接口验证, +再签发仅能访问客服数据的短期 token。 +""" + +from __future__ import annotations + +import json +import os +import re +import socket +import urllib.error +import urllib.parse +import urllib.request +from typing import Any + + +WECOM_RPA_TERMINAL = 7 +# 手机远程控制台。和桌面端(7)、网页后台(1)分开保存 ZYT 会话,互不顶号。 +# 8 已被 ZYT 的 AI 浏览器(app/mcp/service/ConsoleService)占用,不能复用。 +# 这个终端的登录只由客服后台代为发起,密码不经过任何第三方页面。 +WECOM_RPA_MOBILE_TERMINAL = 9 +DEFAULT_ZYT_API_BASE_URL = os.environ.get( + "ZYT_API_BASE_URL", "http://127.0.0.1" +).strip().rstrip("/") + + +class ZytAuthError(RuntimeError): + pass + + +class ZytLoginError(ZytAuthError): + """ZYT 登录失败,并带上可供界面区分的原因。 + + kind 取值: + credentials 密码错误(ZYT 会按出口 IP 计入锁定次数) + unknown_account 账号不存在(ZYT 不计入锁定次数;对外提示与密码错误相同) + password_change_required 密码正确,但账号仍在用初始密码,须先到 ZYT 网页后台修改 + locked ZYT 已按出口 IP 锁定登录,retry_after 为剩余秒数 + disabled 管理员账号已停用 + terminal_unsupported 线上 ZYT 还不认识手机终端 9 + unavailable 网络、超时或 ZYT 返回了非预期数据 + rejected 其他明确的拒绝(原样展示 ZYT 的提示) + """ + + def __init__(self, message: str, *, kind: str = "rejected", retry_after: int = 0): + super().__init__(message) + self.kind = kind + self.retry_after = max(0, int(retry_after or 0)) + + +def api_base_url() -> str: + value = DEFAULT_ZYT_API_BASE_URL + parsed = urllib.parse.urlparse(value) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytAuthError("ZYT_API_BASE_URL 配置不正确") + return value.rstrip("/") + + +def verify_desktop_token( + token: str, *, api_url: str | None = None, timeout: float = 8.0 +) -> dict[str, Any]: + token = str(token or "").strip() + if not token: + raise ZytAuthError("缺少 ZYT 登录令牌") + base_url = str(api_url or api_base_url()).strip().rstrip("/") + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytAuthError("ZYT 登录 API 域名配置不正确") + last_error = "ZYT 登录已失效" + # 第一条是新版专用接口;第二条兼容尚未发布桌面终端的线上旧版。 + endpoints = ( + ("/adminapi/desktop/session", False), + ("/adminapi/auth.admin/mySelf", True), + ) + for path, legacy in endpoints: + request = urllib.request.Request( + f"{base_url}{path}", + headers={ + "Accept": "application/json", + "User-Agent": "WeCom-RPA-Backend/1.0", + # ZYT 管理端登录中间件读取 token 头,不是 Bearer。 + "token": token, + }, + method="GET", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + payload = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + last_error = f"ZYT 身份服务拒绝请求(HTTP {exc.code})" + # 只兼容尚未部署的路由;鉴权拒绝不能降级为网页身份。 + if not legacy and exc.code in {404, 405}: + continue + raise ZytAuthError(last_error) from exc + except (urllib.error.URLError, TimeoutError, socket.timeout) as exc: + raise ZytAuthError( + f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytAuthError("ZYT 身份服务返回了无效数据") from exc + + if not isinstance(payload, dict): + raise ZytAuthError("ZYT 身份服务返回了无效数据") + if str(payload.get("code")) != "1": + last_error = str( + payload.get("msg") if isinstance(payload, dict) else "" + ) or "ZYT 登录已失效" + if legacy and "ip" in last_error.lower() and "变化" in last_error: + raise ZytAuthError( + "ZYT 桌面认证接口尚未部署,旧版接口拒绝跨服务器验证。" + "请管理员部署 /adminapi/desktop/session 后重新登录" + ) + raise ZytAuthError(last_error) + data = payload.get("data") + if not isinstance(data, dict): + raise ZytAuthError("ZYT 身份信息不完整") + + if legacy: + admin = data.get("user") + if not isinstance(admin, dict) or not admin.get("id"): + raise ZytAuthError("ZYT 管理员身份信息不完整") + if int(admin.get("disable") or 0) != 0: + raise ZytAuthError("ZYT 管理员账号已停用") + permissions_value = data.get("permissions") + permissions = ( + sorted( + { + str(item).strip() + for item in permissions_value + if str(item).strip() + } + ) + if isinstance(permissions_value, list) + else [] + ) + role_ids_value = admin.get("role_ids") + role_ids: list[int] = [] + if isinstance(role_ids_value, list): + for item in role_ids_value: + try: + role_ids.append(int(item)) + except (TypeError, ValueError): + continue + profile_identity = { + "user_id": f"admin:{admin['id']}", + "sn": str(admin.get("account") or ""), + "nickname": str(admin.get("name") or admin.get("account") or ""), + "mobile": str(admin.get("phone") or ""), + "avatar": str(admin.get("avatar") or ""), + "terminal": WECOM_RPA_TERMINAL, + "status": "active", + "account_type": "admin", + "root": bool(int(admin.get("root") or 0)), + "role_ids": sorted(set(role_ids)), + "permissions": permissions, + "permissions_known": isinstance(permissions_value, list), + } + return profile_identity + + identity = dict(data) + if not identity.get("user_id"): + raise ZytAuthError("ZYT 身份信息不完整") + if str(identity.get("terminal")) != str(WECOM_RPA_TERMINAL): + raise ZytAuthError("当前令牌不是企业微信客服桌面端令牌") + if str(identity.get("status") or "") != "active": + raise ZytAuthError("ZYT 管理员账号已停用") + permissions_value = identity.get("permissions") + identity["permissions"] = ( + sorted( + { + str(item).strip() + for item in permissions_value + if str(item).strip() + } + ) + if isinstance(permissions_value, list) + else [] + ) + identity["permissions_known"] = isinstance(permissions_value, list) + identity["root"] = bool(int(identity.get("root") or 0)) + # 新版专用接口完整返回权限;旧版缺权限时保留未知状态,患者功能 + # 按最小权限关闭,不再跨 IP 请求网页 mySelf 补权限。 + return identity + + raise ZytAuthError(last_error) + + +def _checked_base_url(api_url: str | None) -> str: + base_url = str(api_url or api_base_url()).strip().rstrip("/") + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytLoginError("ZYT 登录 API 域名配置不正确", kind="unavailable") + return base_url + + +def _zyt_json( + method: str, url: str, *, payload: dict[str, Any] | None = None, + token: str = "", timeout: float, +) -> dict[str, Any]: + headers = {"Accept": "application/json", "User-Agent": "WeCom-RPA-Backend/1.0"} + body = None + if payload is not None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + headers["Content-Type"] = "application/json; charset=utf-8" + if token: + headers["token"] = token + request = urllib.request.Request(url, data=body, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + payload_out = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + raise ZytLoginError( + f"ZYT 身份服务拒绝请求(HTTP {exc.code})", kind="unavailable" + ) from exc + except (urllib.error.URLError, TimeoutError, socket.timeout, OSError) as exc: + raise ZytLoginError( + f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}", kind="unavailable" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytLoginError("ZYT 身份服务返回了无效数据", kind="unavailable") from exc + if not isinstance(payload_out, dict): + raise ZytLoginError("ZYT 身份服务返回了无效数据", kind="unavailable") + return payload_out + + +_LOCK_MINUTES_RE = re.compile(r"(\d+)\s*分钟") + + +def _login_failure(message: str) -> ZytLoginError: + """把 ZYT 登录接口的中文提示归类。 + + 账号不存在和密码错误合并成同一句,免得手机端被拿来探测哪些账号存在。 + """ + text = str(message or "").strip() or "ZYT 登录失败" + if "连续" in text and "分钟" in text: + match = _LOCK_MINUTES_RE.search(text.split("请", 1)[-1]) + minutes = int(match.group(1)) if match else 30 + return ZytLoginError(text, kind="locked", retry_after=max(1, minutes) * 60) + # ZYT 只在"账号存在、密码不对"时累计出口 IP 的失败次数(LoginValidate::password), + # 账号不存在直接返回、不计数。两者对外同一句话,对内要分开:限流预算只该为前者消耗。 + if "密码错误" in text: + return ZytLoginError("账号或密码错误", kind="credentials") + if text == "账号不存在": + return ZytLoginError("账号或密码错误", kind="unknown_account") + if "禁用" in text or "停用" in text: + return ZytLoginError("该 ZYT 账号已停用,请联系管理员", kind="disabled") + if "terminal" in text.lower() or "终端" in text: + return ZytLoginError( + "ZYT 尚未开通企微客服手机端登录(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + return ZytLoginError(text, kind="rejected") + + +def login_admin( + account: str, password: str, *, terminal: int = WECOM_RPA_MOBILE_TERMINAL, + api_url: str | None = None, timeout: float = 10.0, +) -> str: + """用 ZYT 管理员账号密码换取指定终端的 token。只在服务端调用。""" + + account = str(account or "").strip() + if not account or not password: + raise ZytLoginError("请输入账号和密码", kind="rejected") + base_url = _checked_base_url(api_url) + result = _zyt_json( + "POST", f"{base_url}/adminapi/login/account", + payload={"account": account, "password": str(password), "terminal": int(terminal)}, + timeout=timeout, + ) + if str(result.get("code")) != "1": + raise _login_failure(str(result.get("msg") or "")) + data = result.get("data") + token = str(data.get("token") or "").strip() if isinstance(data, dict) else "" + if not token: + raise ZytLoginError("ZYT 没有返回登录令牌", kind="unavailable") + # is_paw=0:管理员发放的初始密码还没改过。网页后台会强制先改密;这里是面向公网的 + # 长期会话,同样不放行,并立即注销刚拿到的 ZYT 令牌。 + if str(data.get("is_paw", 1)).strip() == "0": + logout_admin(token, api_url=base_url, timeout=timeout) + raise ZytLoginError( + "该账号仍在使用初始密码,请先登录 ZYT 网页后台修改密码,再使用手机控制台", + kind="password_change_required", + ) + return token + + +def verify_admin_session( + token: str, *, terminal: int = WECOM_RPA_MOBILE_TERMINAL, + api_url: str | None = None, timeout: float = 8.0, +) -> dict[str, Any]: + """通过 /adminapi/desktop/session 读取当前身份与权限,并核对终端类型。 + + 与 ``verify_desktop_token`` 不同,这里没有旧版 mySelf 兜底:手机终端本身 + 就要求 ZYT 已升级,旧接口也不会返回终端 9。 + """ + + token = str(token or "").strip() + if not token: + raise ZytLoginError("缺少 ZYT 登录令牌", kind="rejected") + base_url = _checked_base_url(api_url) + try: + result = _zyt_json( + "GET", f"{base_url}/adminapi/desktop/session", token=token, timeout=timeout + ) + except ZytLoginError as exc: + cause = exc.__cause__ + if isinstance(cause, urllib.error.HTTPError) and cause.code in {404, 405}: + raise ZytLoginError( + "ZYT 尚未部署桌面身份接口,暂不能使用手机端", kind="terminal_unsupported" + ) from exc + raise + if str(result.get("code")) != "1": + message = str(result.get("msg") or "").strip() or "ZYT 登录已失效" + if "不属于" in message and "桌面" in message: + raise ZytLoginError( + "ZYT 尚未开通企微客服手机端身份校验(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + if "禁用" in message or "不存在" in message: + raise ZytLoginError("ZYT 管理员账号不存在或已停用", kind="disabled") + raise ZytLoginError(message, kind="rejected") + data = result.get("data") + if not isinstance(data, dict) or not data.get("user_id"): + raise ZytLoginError("ZYT 身份信息不完整", kind="unavailable") + identity = dict(data) + if str(identity.get("terminal")) != str(int(terminal)): + raise ZytLoginError( + "ZYT 尚未开通企微客服手机端身份校验(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + if str(identity.get("status") or "") != "active": + raise ZytLoginError("ZYT 管理员账号已停用", kind="disabled") + permissions_value = identity.get("permissions") + identity["permissions"] = ( + sorted({str(item).strip() for item in permissions_value if str(item).strip()}) + if isinstance(permissions_value, list) else [] + ) + identity["permissions_known"] = isinstance(permissions_value, list) + identity["root"] = bool(int(identity.get("root") or 0)) + return identity + + +def logout_admin(token: str, *, api_url: str | None = None, timeout: float = 5.0) -> None: + """尽力注销 ZYT 侧会话;失败不影响本地登出。""" + + token = str(token or "").strip() + if not token: + return + try: + _zyt_json( + "POST", f"{_checked_base_url(api_url)}/adminapi/login/logout", + payload={}, token=token, timeout=timeout, + ) + except ZytAuthError: + return diff --git a/deploy/mobile-remote-20260928/prepare_bundle.py b/deploy/mobile-remote-20260928/prepare_bundle.py new file mode 100644 index 000000000..e1e8d7f23 --- /dev/null +++ b/deploy/mobile-remote-20260928/prepare_bundle.py @@ -0,0 +1,98 @@ +"""Assemble the server bundle locally: payload + manifest + tar.gz. No upload, no deployment. + + python prepare_bundle.py + +Sources come from the working trees under C:/kefu (backend in wechat_rpa, the phone +console build in admin-web/apps/mobile-h5/dist). baseline-snapshot/ holds the exact bytes +production runs; every change production carries on top of git HEAD must already be in +the payload, so a lagging local copy cannot roll production back. +""" +from pathlib import Path +import hashlib, json, shutil, subprocess, tarfile, tempfile + +folder = Path(__file__).resolve().parent +kefu = folder.parent.parent +plan = json.loads((folder / 'release-plan.json').read_text(encoding='utf-8')) +base = json.loads((folder / 'server-baseline.json').read_text(encoding='utf-8')) +sha = lambda data: hashlib.sha256(data).hexdigest() +lf = lambda data: data.replace(b'\r\n', b'\n') + +# 1. Production is not git HEAD (the 2026-09-23 cos-direct release shipped admin_api.py from +# another working tree), so merge HEAD -> production into the payload: it must be a no-op. +for name, wanted in base['expected'].items(): + snapshot = (folder / 'baseline-snapshot' / name).read_bytes() + assert sha(snapshot) == wanted, 'baseline-snapshot differs from server-baseline.json: ' + name + ours = lf((kefu / name).read_bytes()) + committed = lf(subprocess.check_output(['git', '-C', str(kefu), 'show', f'HEAD:{name}'])) + with tempfile.TemporaryDirectory() as tmp: + paths = [] + for label, data in (('ours', ours), ('head', committed), ('production', lf(snapshot))): + (Path(tmp) / label).write_bytes(data) + paths.append(str(Path(tmp) / label)) + merged = subprocess.run(['git', 'merge-file', '-p', *paths], capture_output=True) + assert merged.returncode == 0 and merged.stdout == ours, 'Payload is missing production changes: ' + name +# The phone console is served as-is; make sure the build did not pick up CRLF from Windows. +for text in (kefu / plan['frontendDir']).rglob('*'): + if text.is_file() and text.suffix in {'.html', '.js', '.css', '.json', '.webmanifest'}: + assert b'\r\n' not in text.read_bytes(), 'CRLF in build output: ' + str(text) + +# 2. Fresh payload from the working trees. +payload = folder / 'payload' +if payload.exists(): + shutil.rmtree(payload) +for name in plan['backendFiles'] + plan['newBackendFiles'] + plan['deployFiles']: + # Windows 工作区是 autocrlf 检出的 CRLF;仓库和生产服务器上都是 LF。 + data = (kefu / name).read_bytes().replace(b'\r\n', b'\n') + assert b'\r' not in data, 'Stray carriage return: ' + name + if name.endswith('.py'): + compile(data, name, 'exec') + target = payload / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) +dist = kefu / plan['frontendDir'] +assert (dist / 'index.html').is_file(), 'Build the phone console first: cd admin-web/apps/mobile-h5 && pnpm build' +shutil.copytree(dist, payload / plan['frontendDir']) + +tests = folder / 'tests' +if tests.exists(): + shutil.rmtree(tests) +tests.mkdir() +for module in plan['serverTestModules']: + data = (kefu / 'wechat_rpa' / f'{module}.py').read_bytes().replace(b'\r\n', b'\n') + compile(data, module, 'exec') + (tests / f'{module}.py').write_bytes(data) + +scripts = ['release-plan.json', 'server-baseline.json', 'Dockerfile.overlay', 'remote_server_guard.py', + '01-preflight.sh', '02-backup-build.sh', '03-deploy.sh', '04-verify.sh', 'rollback.sh'] + +# 3. Manifest over everything the server will verify. +manifest = [] +for path in sorted(p for p in list(payload.rglob('*')) + list(tests.rglob('*')) if p.is_file()): + data = path.read_bytes() + manifest.append({'path': path.relative_to(folder).as_posix(), 'sha256': sha(data), 'bytes': len(data)}) +(folder / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8') + +# 4. Server bundle (LF line endings for shell scripts) and archive. +bundle = folder / 'server-bundle' +if bundle.exists(): + shutil.rmtree(bundle) +bundle.mkdir() +shutil.copytree(payload, bundle / 'payload') +shutil.copytree(tests, bundle / 'tests') +for name in scripts + ['manifest.json']: + (bundle / name).write_bytes((folder / name).read_bytes().replace(b'\r\n', b'\n')) +archive = folder / 'mobile-remote-server-bundle.tar.gz' +if archive.exists(): + archive.unlink() +with tarfile.open(archive, 'x:gz') as tf: + for p in sorted(bundle.rglob('*')): + if p.is_file(): + arc = p.relative_to(bundle).as_posix(); assert not arc.startswith(('/', '..')) + info = tf.gettarinfo(str(p), arcname=arc) + info.mode = 0o755 if arc.endswith('.sh') else 0o644 + info.uid = info.gid = 0 + info.uname = info.gname = '' + with p.open('rb') as handle: + tf.addfile(info, handle) +print(json.dumps({'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes()), + 'files': len(manifest), 'uploaded': False}, ensure_ascii=False, indent=1)) diff --git a/deploy/mobile-remote-20260928/release-plan.json b/deploy/mobile-remote-20260928/release-plan.json new file mode 100644 index 000000000..bb6a856ca --- /dev/null +++ b/deploy/mobile-remote-20260928/release-plan.json @@ -0,0 +1,30 @@ +{ + "release": "mobile-remote-20260928", + "candidateRoot": "C:/kefu", + "backendFiles": [ + "wechat_rpa/admin_api.py", + "wechat_rpa/zyt_auth.py" + ], + "newBackendFiles": [ + "wechat_rpa/remote_api.py", + "wechat_rpa/remote_store.py" + ], + "deployFiles": [ + "deploy/im-admin/Dockerfile" + ], + "frontendDir": "admin-web/apps/mobile-h5/dist", + "serverTestModules": [ + "test_remote_api", + "test_zyt_auth", + "test_backend" + ], + "schemaMigration": "additive", + "restartServices": [ + "api" + ], + "notes": "Mobile remote console (/m/, /api/v2/mobile/*), desktop command channel and server-side conversation blacklist. New tables mobile_sessions, desktop_commands, desktop_status, conversation_blacklist are created with CREATE TABLE IF NOT EXISTS on API start; existing tables are not altered. Rollback keeps the new (unused) tables.", + "imageTestModules": [ + "test_archive_api" + ], + "testNotes": "serverTestModules ship from C:/kefu/wechat_rpa (test_backend there is newer than the copy in the production image). imageTestModules run the image's own copy. The image's own test_admin_api and test_backend already fail on untouched production code (one predates the 2026-09-23 COS release-package URLs, the other still mocks the old two-request ZYT session flow), so they are only run locally." +} diff --git a/deploy/mobile-remote-20260928/remote_server_guard.py b/deploy/mobile-remote-20260928/remote_server_guard.py new file mode 100644 index 000000000..ea536699e --- /dev/null +++ b/deploy/mobile-remote-20260928/remote_server_guard.py @@ -0,0 +1,121 @@ +"""Read-only checks for mobile-remote-20260928: baseline / candidate / deployed. + +Prints hashes and status only; never reads chat content, patients or credentials. +""" +from pathlib import Path +import argparse, hashlib, json, subprocess, urllib.error, urllib.request + +parser = argparse.ArgumentParser() +parser.add_argument('mode', choices=['baseline', 'candidate', 'deployed']) +args = parser.parse_args() +RELEASE = 'mobile-remote-20260928' +folder = Path(__file__).resolve().parent +project = Path('/opt/im-admin') +base = json.loads((folder / 'server-baseline.json').read_text()) +manifest = json.loads((folder / 'manifest.json').read_text()) +payload_rows = [row for row in manifest if row['path'].startswith('payload/')] +sha = lambda data: hashlib.sha256(data).hexdigest() +image = lambda ref: subprocess.check_output(['docker', 'image', 'inspect', ref, '--format', '{{.Id}}'], text=True).strip() + +# The unpacked bundle itself must be intact. +for row in manifest: + path = (folder / row['path']).resolve() + assert path.is_relative_to(folder), row['path'] + assert path.stat().st_size == row['bytes'] and sha(path.read_bytes()) == row['sha256'], row['path'] + +# project-relative path -> expected sha256 on the host +if args.mode == 'baseline': + expected_host = dict(base['expected']) +else: + expected_host = {row['path'][len('payload/'):]: row['sha256'] for row in payload_rows} +container_code = {k: v for k, v in expected_host.items() if k.startswith(('wechat_rpa/', 'admin-web/'))} + + +def container_hashes(target, ref=None): + code = ('from pathlib import Path;import hashlib,json,sys;names=json.load(sys.stdin);' + 'print(json.dumps({n:(hashlib.sha256((Path("/app")/n).read_bytes()).hexdigest() ' + 'if (Path("/app")/n).is_file() else None) for n in names}))') + command = (['docker', 'run', '--rm', '-i', '--network=none', '--entrypoint', 'python', ref, '-c', code] + if ref else ['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code]) + return json.loads(subprocess.check_output(command, input=json.dumps(target).encode())) + + +current = image('zyt/wecom-admin:current') +result = {'mode': args.mode, 'release': RELEASE, 'image': current} +# 只重建 api:网关和知识库 worker 继续跑它们自己的镜像(2026-09-24 那次前端发布同样只换了 api), +# 这里记录下来,部署后核对它们没有被动过。 +OTHERS = ['im-admin-gateway-1', 'im-admin-knowledge-worker-1'] + + +def inspect(container): + raw = json.loads(subprocess.check_output(['docker', 'inspect', container]))[0] + return {'id': raw['Id'], 'image': raw['Image'], 'running': raw['State']['Running'], + 'health': raw['State'].get('Health', {}).get('Status', 'not_configured')} + + +if args.mode == 'baseline': + api = inspect('im-admin-api-1') + assert api['image'] == current, 'API container is not on zyt/wecom-admin:current' + assert api['running'] and api['health'] == 'healthy', 'API container is not healthy' + others = {name: inspect(name) for name in OTHERS} + for name, state in others.items(): + assert state['running'], 'Not running: ' + name + result['others'] = others + for name in base.get('must_not_exist', []): + assert not (project / name).exists(), 'Unexpected existing path (partially deployed?): ' + name + +if args.mode in ('baseline', 'deployed'): + for name, wanted in expected_host.items(): + assert sha((project / name).read_bytes()) == wanted, 'Host drift: ' + name + actual = container_hashes(sorted(container_code)) + assert actual == container_code, 'Running api container differs from expected code' + result['files'] = len(expected_host) + +if args.mode == 'candidate': + candidate = 'zyt/wecom-admin:' + RELEASE + actual = container_hashes(sorted(container_code), candidate) + assert actual == container_code, 'Candidate image differs from manifest' + result['candidate'] = image(candidate) + result['files'] = len(container_code) + +if args.mode == 'deployed': + wanted = image('zyt/wecom-admin:' + RELEASE) + assert current == wanted, 'current tag does not point at the release image' + raw = json.loads(subprocess.check_output(['docker', 'inspect', 'im-admin-api-1']))[0] + assert raw['Image'] == wanted and raw['State']['Running'] and raw['RestartCount'] == 0 + assert raw['State'].get('Health', {}).get('Status') == 'healthy' + recorded = folder / 'baseline-check.json' + if recorded.exists(): + before = json.loads(recorded.read_text())['others'] + for name in OTHERS: + now = inspect(name) + assert now['id'] == before[name]['id'] and now['running'], 'Unrelated container changed: ' + name + result['others_unchanged'] = OTHERS + for url in ['http://127.0.0.1:18766/api/v2/health', 'http://127.0.0.1:18770/health']: + with urllib.request.urlopen(url, timeout=15) as response: + assert response.status == 200, url + # New routes exist and refuse anonymous callers. No login is attempted. + for method, path in [('GET', '/api/v2/mobile/me'), ('GET', '/api/v2/mobile/desktops'), + ('POST', '/api/v2/desktop/commands/poll'), ('GET', '/api/v2/desktop/blacklist')]: + request = urllib.request.Request('http://127.0.0.1:18766' + path, method=method, + data=b'{}' if method == 'POST' else None, + headers={'Content-Type': 'application/json'}) + try: + urllib.request.urlopen(request, timeout=15) + except urllib.error.HTTPError as exc: + assert exc.code == 401, (path, exc.code) + else: + raise AssertionError('Accessible without authentication: ' + path) + with urllib.request.urlopen('http://127.0.0.1:18766/m/', timeout=15) as response: + body = response.read() + assert response.status == 200 and '企微客服助手'.encode() in body, 'mobile console entry missing' + code = ("import sqlite3,json;c=sqlite3.connect('file:/data/backend.db?mode=ro',uri=True,timeout=30);" + "names={r[0] for r in c.execute(\"SELECT name FROM sqlite_master WHERE type='table'\")};" + "print(json.dumps({'tables':sorted(names & {'mobile_sessions','desktop_commands','desktop_status','conversation_blacklist'})," + "'quick_check':c.execute('PRAGMA quick_check').fetchone()[0]}))") + db = json.loads(subprocess.check_output(['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code])) + assert db['quick_check'] == 'ok' and len(db['tables']) == 4, db + result['database'] = db + +result['status'] = 'passed' +print(json.dumps(result, ensure_ascii=False)) diff --git a/deploy/mobile-remote-20260928/rollback.sh b/deploy/mobile-remote-20260928/rollback.sh new file mode 100644 index 000000000..7098a1e8d --- /dev/null +++ b/deploy/mobile-remote-20260928/rollback.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# 恢复 2 个后端文件和 Dockerfile,删除新增文件与 /m/ 前端,切回旧镜像,只重建 api。 +# 数据库保留:新增的 4 张表旧代码不会读写,留着不影响回滚后的运行。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd /opt/im-admin/deploy/im-admin +test -d "$release/source-before" +test "$(docker image inspect zyt/wecom-admin:before-mobile-remote-20260928 --format '{{.Id}}')" = "$(cat "$release/image-before.txt")" +python3 - <<'PY' +from pathlib import Path +import hashlib, json, os, shutil +r = Path('/home/ps/mobile-remote-20260928'); project = Path('/opt/im-admin') +base = json.loads((r / 'server-baseline.json').read_text()) +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + data = (r / 'source-before' / name).read_bytes(); assert hashlib.sha256(data).hexdigest() == base['expected'][name] + target = project / name; tmp = target.with_name(target.name + '.rollback-mobile.tmp') + if tmp.exists(): + tmp.unlink() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, target.stat().st_mode & 0o777); os.replace(tmp, target) +for name in plan['newBackendFiles']: + target = project / name + if target.exists(): + target.unlink() +for path in [project / plan['frontendDir'], project / (plan['frontendDir'] + '.mobile-remote.tmp')]: + if path.exists(): + shutil.rmtree(path) +mobile_app = project / 'admin-web/apps/mobile-h5' +if mobile_app.exists() and not any(mobile_app.iterdir()): + mobile_app.rmdir() +PY +docker tag zyt/wecom-admin:before-mobile-remote-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +curl -fsS -o /dev/null http://127.0.0.1:18766/api/v2/health +curl -fsS -o /dev/null http://127.0.0.1:18770/health +printf '%s\n' 'Previous image and source files restored; live database retained (new tables unused).' diff --git a/deploy/mobile-remote-20260928/server-baseline.json b/deploy/mobile-remote-20260928/server-baseline.json new file mode 100644 index 000000000..b2ef2c887 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-baseline.json @@ -0,0 +1,14 @@ +{ + "host": "192.168.110.111", + "user": "ps", + "project": "/opt/im-admin", + "compose": "/opt/im-admin/deploy/im-admin", + "checked_at": "2026-09-28", + "source": "Read from production /opt/im-admin on 2026-09-28 (exact bytes kept in baseline-snapshot/). admin_api.py = cos-direct release of 2026-09-23 (C:/wechat_rpa/tmp/cos-direct-server-payload, CRLF), not C:/kefu HEAD; zyt_auth.py = C:/kefu HEAD content with CRLF line endings; Dockerfile = C:/kefu HEAD. The payload admin_api.py is production + this release's changes (3-way merge, checked by prepare_bundle.py).", + "expected": { + "wechat_rpa/admin_api.py": "fcc94534fee7d1cd524d6923eea035a212a3f6d3ba35f19d41f9ee17b3c74bdc", + "wechat_rpa/zyt_auth.py": "6b8ded1ad17766957cedbbcae7841ca8bd783bfa9c0f47266d0d42ac7ae66691", + "deploy/im-admin/Dockerfile": "2bc1a6c1a0d9eeea512a560b676dfa0a4e060513d24cbc2c12e251c061fbe1a7" + }, + "must_not_exist": ["wechat_rpa/remote_api.py", "wechat_rpa/remote_store.py", "admin-web/apps/mobile-h5"] +} diff --git a/deploy/mobile-remote-20260928/server-bundle/01-preflight.sh b/deploy/mobile-remote-20260928/server-bundle/01-preflight.sh new file mode 100644 index 000000000..2cf39bd53 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/01-preflight.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# 基线核对、备份将被替换的源文件、给当前镜像打回滚标签。只读线上 + 写本目录。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +test ! -e baseline-check.json +python3 remote_server_guard.py baseline > baseline-check.json.tmp +mv baseline-check.json.tmp baseline-check.json +cat baseline-check.json +test ! -e source-before +test ! -e preflight-complete +if docker image inspect zyt/wecom-admin:before-mobile-remote-20260928 >/dev/null 2>&1; then + echo 'Refuse to replace existing rollback tag' >&2; exit 1 +fi +mkdir -m 700 source-before +python3 - <<'PY' +from pathlib import Path +import json, shutil +r = Path('/home/ps/mobile-remote-20260928'); project = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + target = r / 'source-before' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(project / name, target) +PY +docker tag zyt/wecom-admin:current zyt/wecom-admin:before-mobile-remote-20260928 +docker image inspect zyt/wecom-admin:current --format '{{.Id}}' > image-before.txt +printf '%s\n' ready > preflight-complete +echo "preflight complete; rollback image tagged before-mobile-remote-20260928 ($(cat image-before.txt))" diff --git a/deploy/mobile-remote-20260928/server-bundle/02-backup-build.sh b/deploy/mobile-remote-20260928/server-bundle/02-backup-build.sh new file mode 100644 index 000000000..8cbfa6c94 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/02-backup-build.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# 数据库一致性备份 + 构建候选镜像 + 离线跑测试 + 核对候选镜像文件哈希。线上服务不受影响。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +test -f preflight-complete +test ! -e candidate-passed +python3 remote_server_guard.py baseline +test "$(docker image inspect zyt/wecom-admin:before-mobile-remote-20260928 --format '{{.Id}}')" = "$(cat image-before.txt)" +# 新表都是 CREATE TABLE IF NOT EXISTS 的纯新增;备份仍然先做,作为安全网。 +# backup.json 只在备份和 quick_check 都成功后才出现;本脚本在测试阶段停下后重跑时沿用它,不重复拷整库。 +if [ -f backup.json ]; then + echo "reusing backup: $(cat backup.json)" + docker exec -i im-admin-api-1 python - "$(cat backup.json)" <<'PY' +from pathlib import Path +import json, sys +info = json.loads(sys.argv[1]); folder = Path(info['backup']) +assert info['quick_check'] == 'ok' and (folder / 'backend.db').stat().st_size == info['bytes'], 'Recorded backup changed' +assert len(list(folder.glob('*.key'))) == info['keyCount'], 'Key backup missing' +PY +else +docker exec -i im-admin-api-1 python - <<'PY' > backup.json.tmp +from pathlib import Path +import sqlite3, shutil, os, json +folder = Path('/data/backups/mobile-remote-20260928'); folder.mkdir(mode=0o700) +source = sqlite3.connect('file:/data/backend.db?mode=ro', uri=True, timeout=30) +backup = sqlite3.connect(folder / 'backend.db') +try: + source.backup(backup, pages=0) +finally: + backup.close(); source.close() +with sqlite3.connect('file:' + str(folder / 'backend.db') + '?mode=ro', uri=True) as check: + assert check.execute('PRAGMA quick_check').fetchone()[0] == 'ok' +os.chmod(folder / 'backend.db', 0o600) +keys = list(Path('/data').glob('*.key')); assert keys, 'Encryption key backup missing' +for p in keys: + shutil.copy2(p, folder / p.name); os.chmod(folder / p.name, 0o600) +print(json.dumps({'backup': str(folder), 'quick_check': 'ok', 'bytes': (folder / 'backend.db').stat().st_size, 'keyCount': len(keys)})) +PY +mv backup.json.tmp backup.json +cat backup.json +fi +docker build --network=none --pull=false -f Dockerfile.overlay -t zyt/wecom-admin:mobile-remote-20260928 . > candidate-build.log 2>&1 +# serverTestModules 随包放在 tests/;imageTestModules 用镜像自带的测试文件(PYTHONPATH 后一项)。 +# -P:不把工作目录 /app/wechat_rpa 放到 sys.path 最前面,否则镜像里同名的旧测试会盖住随包的新测试。 +mapfile -t modules < <(python3 -c 'import json;p=json.load(open("release-plan.json"));print("\n".join(p["serverTestModules"]+p.get("imageTestModules",[])))') +test "${#modules[@]}" -gt 0 +docker run --rm --network=none --read-only --tmpfs /tmp:size=512m,mode=1777 --tmpfs /data:size=256m,mode=1777 \ + -v "$release/tests:/release-tests:ro" -e PYTHONPATH=/release-tests:/app/wechat_rpa \ + zyt/wecom-admin:mobile-remote-20260928 python -X utf8 -B -P -m unittest "${modules[@]}" -q > candidate-tests.log 2>&1 +tail -n 3 candidate-tests.log +python3 remote_server_guard.py candidate +printf '%s\n' passed > candidate-passed diff --git a/deploy/mobile-remote-20260928/server-bundle/03-deploy.sh b/deploy/mobile-remote-20260928/server-bundle/03-deploy.sh new file mode 100644 index 000000000..ee376a9d1 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/03-deploy.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# 替换 2 个后端文件、新增 2 个后端文件和 /m/ 前端、更新 Dockerfile;切换镜像,只重建 api 容器。 +# 任何一步失败自动调用 rollback.sh。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +test -f candidate-passed +test -f backup.json +python3 remote_server_guard.py baseline +cd /opt/im-admin/deploy/im-admin +docker compose config --quiet +rollback_on_error() { rc=$?; trap - ERR; bash "$release/rollback.sh"; exit "$rc"; } +trap rollback_on_error ERR +python3 - <<'PY' +from pathlib import Path +import json, hashlib, os +r = Path('/home/ps/mobile-remote-20260928'); target = Path('/opt/im-admin') +plan = json.loads((r / 'release-plan.json').read_text()) +reference = (target / 'wechat_rpa/admin_api.py').stat().st_mode & 0o777 +rows = [row for row in json.loads((r / 'manifest.json').read_text()) if row['path'].startswith('payload/')] +frontend = plan['frontendDir'] +# 前端整体先放临时目录,写完再一次性改名,外面看不到半套静态文件。 +staging = target / (frontend + '.mobile-remote.tmp'); assert not staging.exists() +for row in rows: + rel = row['path'][len('payload/'):] + data = (r / row['path']).read_bytes(); assert hashlib.sha256(data).hexdigest() == row['sha256'], rel + if rel.startswith(frontend + '/'): + dest = staging / rel[len(frontend) + 1:] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data); os.chmod(dest, 0o644) + continue + dest = (target / rel).resolve(); assert dest.is_relative_to(target) + tmp = dest.with_name(dest.name + '.mobile-remote.tmp'); assert not tmp.exists() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, dest.stat().st_mode & 0o777 if dest.exists() else reference); os.replace(tmp, dest) +for directory in [staging] + [p for p in staging.rglob('*') if p.is_dir()]: + os.chmod(directory, 0o755) +final = target / frontend; assert not final.exists() +final.parent.mkdir(parents=True, exist_ok=True) +os.replace(staging, final) +PY +docker tag zyt/wecom-admin:mobile-remote-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +bash "$release/04-verify.sh" +trap - ERR +printf '%s\n' 'Mobile remote console deployed and verified.' diff --git a/deploy/mobile-remote-20260928/server-bundle/04-verify.sh b/deploy/mobile-remote-20260928/server-bundle/04-verify.sh new file mode 100644 index 000000000..8ada5445d --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/04-verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd "$release" +python3 remote_server_guard.py deployed | tee deployed-verification.json diff --git a/deploy/mobile-remote-20260928/server-bundle/Dockerfile.overlay b/deploy/mobile-remote-20260928/server-bundle/Dockerfile.overlay new file mode 100644 index 000000000..2ca792b68 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/Dockerfile.overlay @@ -0,0 +1,8 @@ +ARG BASE_IMAGE=zyt/wecom-admin:before-mobile-remote-20260928 +FROM ${BASE_IMAGE} +COPY --chown=app:app payload/wechat_rpa/admin_api.py /app/wechat_rpa/admin_api.py +COPY --chown=app:app payload/wechat_rpa/zyt_auth.py /app/wechat_rpa/zyt_auth.py +COPY --chown=app:app payload/wechat_rpa/remote_api.py /app/wechat_rpa/remote_api.py +COPY --chown=app:app payload/wechat_rpa/remote_store.py /app/wechat_rpa/remote_store.py +COPY --chown=app:app payload/admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ +WORKDIR /app/wechat_rpa diff --git a/deploy/mobile-remote-20260928/server-bundle/manifest.json b/deploy/mobile-remote-20260928/server-bundle/manifest.json new file mode 100644 index 000000000..d7086302a --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/manifest.json @@ -0,0 +1,182 @@ +[ + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js", + "sha256": "97abcbc7c960add5c448947dad40ea8ca6ca2ae18069a632b62b61723e43f24f", + "bytes": 2308 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js", + "sha256": "8c06092db48aca97b0d3fb35871315e4434b5ad5ccd9c5d44a7cfc3ba343db22", + "bytes": 14528 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css", + "sha256": "b61f1f1af65c2c3438066ffe41683af7e38062eb9389cda3f8fdad8ee77498ff", + "bytes": 359 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png", + "sha256": "1caa8c492da279e8d01f676fe9232ccad016321699d519b3834692052cc5f5ee", + "bytes": 16743 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js", + "sha256": "7a648b566b9c00c1c8d1b6d2ca9737ca237e521f0e25065cedf86e82fa2471d8", + "bytes": 4490 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js", + "sha256": "c145f55146ff5373b293e2922b021f55c6851fa34db65fdacb64139fb88c4fa8", + "bytes": 28526 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css", + "sha256": "1f646a0178eb9e3f837ac62b5521ec60c0e7d5b4b086b9076c4bf2a7448f98ae", + "bytes": 4220 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js", + "sha256": "ddb9c705e5cc4822e9166686a7eab319c772540981134f30a9f850fc85ac8692", + "bytes": 10460 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css", + "sha256": "40fd5adaea4d4aed376a57c75ebec4544d50f424863704b00ce80b98f7881ddd", + "bytes": 3983 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js", + "sha256": "393394f3545f3224264be9d1eb87c72b6ce474651b3794b3839f43bfbd69aa86", + "bytes": 8422 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js", + "sha256": "d6a7fc2e067a2adcf92b498812422755d5ac8f00acfcf07fd935201eee35c056", + "bytes": 14363 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-D80qzV0-.css", + "sha256": "c0c7a7706d3d4a4ca2baa88f31aa2fd5c6080b4f2e539b4812a99d51f2de9fd0", + "bytes": 97051 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js", + "sha256": "63a10401ab035b495ba24c55f9b1bd7ef54c76c6d6bc549f4b0eda62dbd915ac", + "bytes": 117757 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css", + "sha256": "b561a20e6d76366db7634b028098ed9f848f665b95f0ffe538f02abce87e8991", + "bytes": 1242 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js", + "sha256": "ded80bbda052a9da5f7beaab6af536258b706f2bf0688331fa8bccd9e23355c6", + "bytes": 6668 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css", + "sha256": "a3bd246d2658ee50d4cac9afa1d051c33e8676be3155260256e7106a9c5dd136", + "bytes": 927 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js", + "sha256": "711a690039d1fe0d648c285da7bd1ef5b62afe8249d10ad7e76208318fc3094d", + "bytes": 3465 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js", + "sha256": "8a2afb226d06ece5a929c2c85c4c73bbf68977b4ccde960aa7f6a5ae4614af62", + "bytes": 2939 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css", + "sha256": "513a381d7ecfe8c24ca22166d658c8604df9d9478a8765ffcef7beb031917dcd", + "bytes": 130 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js", + "sha256": "168066192a95a203466245545388b756dfd035c466b007e65ccadc1c94f3fd80", + "bytes": 607 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png", + "sha256": "3167d1d07f22a44ea393b377e5181918ae4890c137de37fe91e2f3780b6ce70e", + "bytes": 24681 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png", + "sha256": "7c9d031dcd296a45a79d960481c34eb150dd33e4a2bace6f28385be01c1a675e", + "bytes": 5439 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png", + "sha256": "9637a4d3e4cbb385ed0b35069d6bb3522b90d1b8aaec439cfdf5ec1264040b5a", + "bytes": 26433 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png", + "sha256": "803ed98fb41fc724cb5319f45409d5e9725465f6f37cb6312e6b79170f852974", + "bytes": 144310 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png", + "sha256": "88b6cecdb691f9e2f4179fd00eae937a5f6863cd61d34aadda1e6ac45e6217a2", + "bytes": 95961 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/index.html", + "sha256": "d11023af89db5c20bde0c4eaaa35fd9ad192a85353165c0a675e139ae5d94f13", + "bytes": 1357 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest", + "sha256": "ac64783b6865e3f68fdc735d5d66cad0c13ffc8b6be6ae5fbebc0e2eac5a61fd", + "bytes": 684 + }, + { + "path": "payload/admin-web/apps/mobile-h5/dist/sw.js", + "sha256": "d908e9a6ec5c75ffcca8f9f6d97ec683b005fa211975f89beee372849df44a69", + "bytes": 339 + }, + { + "path": "payload/deploy/im-admin/Dockerfile", + "sha256": "d52249fe81d3537742ecd500f3b1b4c6f64d5048f74177d188946cbf2ac4224b", + "bytes": 802 + }, + { + "path": "payload/wechat_rpa/admin_api.py", + "sha256": "109919284e4d9877a1f9699a27a8fdba11db8be104b99379862232c880368485", + "bytes": 72796 + }, + { + "path": "payload/wechat_rpa/remote_api.py", + "sha256": "843f0a814a0de4f8078f7f9f23a4ac92388d7556c9fe378987f67eda46c55c5b", + "bytes": 37534 + }, + { + "path": "payload/wechat_rpa/remote_store.py", + "sha256": "38304e831a7085808e7395dada5cdb3ecad2335fe49ba3dab29a6267d1a95006", + "bytes": 53499 + }, + { + "path": "payload/wechat_rpa/zyt_auth.py", + "sha256": "f5fd01f08668fa223cf9b303df3d9b82758575b0ae17db447c7bf70d14be63e8", + "bytes": 15750 + }, + { + "path": "tests/test_backend.py", + "sha256": "7473ec5583c97941e132ade7a1f66abcf54fa6108ec6bb2a182f0152964fff61", + "bytes": 66279 + }, + { + "path": "tests/test_remote_api.py", + "sha256": "d3d4eef5dc7a6774da1f6fc30fe5aa65e9e11eb49b0ace8a72883544f2cbb09a", + "bytes": 38197 + }, + { + "path": "tests/test_zyt_auth.py", + "sha256": "40ab2d37f51780af26497e22948ef665c92b7f3707f6ba30a6d711bf5b15dfa6", + "bytes": 4907 + } +] \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js new file mode 100644 index 000000000..9d43b6f76 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-5FuHazzf.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{c as n,d as r,l as i,u as a}from"./format-DEXJ_MrY.js";import{n as o,o as s,s as c,t as l}from"./cell-group-B1PakqJq.js";import{n as u,r as d,t as f}from"./field-CRX3eS7h.js";import{$ as p,Bt as m,Ct as h,Dt as g,E as _,Et as v,Ft as y,G as b,Gt as x,Jt as S,K as C,Kt as w,Lt as T,Mt as E,Nt as ee,O as D,Q as O,St as k,Ut as A,V as te,Vt as j,X as M,Y as N,Yt as ne,Z as re,_ as P,_t as F,a as ie,c as ae,et as oe,ft as se,g as I,gt as L,ht as ce,l as le,n as ue,o as R,ot as z,q as B,qt as V,rt as de,vt as fe,wt as pe,x as me,yt as H}from"./index-Gkjcdjlo.js";var U=0;function he(e){e?(U||document.body.classList.add(`van-toast--unclickable`),U++):U&&(U--,U||document.body.classList.remove(`van-toast--unclickable`))}var[ge,W]=P(`toast`),_e=[`show`,`overlay`,`teleport`,`transition`,`overlayClass`,`overlayStyle`,`closeOnClickOverlay`,`zIndex`],ve={icon:String,show:Boolean,type:N(`text`),overlay:Boolean,message:M,iconSize:M,duration:C(2e3),position:N(`middle`),teleport:[String,Object],wordBreak:String,className:null,iconPrefix:String,transition:N(`van-fade`),loadingType:String,forbidClick:Boolean,overlayClass:null,overlayStyle:Object,closeOnClick:Boolean,closeOnClickOverlay:Boolean,zIndex:M},ye=h({name:ge,props:ve,emits:[`update:show`],setup(t,{emit:n,slots:r}){let i,o=!1,c=()=>{let e=t.show&&t.forbidClick;o!==e&&(o=e,he(o))},l=e=>n(`update:show`,e),u=()=>{t.closeOnClick&&l(!1)},d=()=>clearTimeout(i),f=()=>{let{icon:n,type:r,iconSize:i,iconPrefix:a,loadingType:o}=t;if(n||r===`success`||r===`fail`)return k(e,{name:n||r,size:i,class:W(`icon`),classPrefix:a},null);if(r===`loading`)return k(s,{class:W(`loading`),size:i,type:o},null)},p=()=>{let{type:e,message:n}=t;if(r.message)return k(`div`,{class:W(`text`)},[r.message()]);if(oe(n)&&n!==``)return e===`html`?k(`div`,{key:0,class:W(`text`),innerHTML:String(n)},null):k(`div`,{class:W(`text`)},[n])};return j(()=>[t.show,t.forbidClick],c),j(()=>[t.show,t.type,t.message,t.duration],()=>{d(),t.show&&t.duration>0&&(i=setTimeout(()=>{l(!1)},t.duration))}),E(c),ee(c),()=>k(a,v({class:[W([t.position,t.wordBreak===`normal`?`break-normal`:t.wordBreak,{[t.type]:!t.icon}]),t.className],lockScroll:!1,onClick:u,onClosed:d,"onUpdate:show":l},z(t,_e)),{default:()=>[f(),p()]})}}),be={icon:``,type:`text`,message:``,className:``,overlay:!1,onClose:void 0,onOpened:void 0,duration:2e3,teleport:`body`,iconSize:void 0,iconPrefix:void 0,position:`middle`,transition:`van-fade`,forbidClick:!1,loadingType:void 0,overlayClass:``,overlayStyle:void 0,closeOnClick:!1,closeOnClickOverlay:!1},G=[],K=O({},be),xe=new Map;function Se(e){return de(e)?e:{message:e}}function Ce(){let{instance:e,unmount:t}=n({setup(){let e=w(``),{open:t,state:n,close:r,toggle:a}=i(),o=()=>{},s=()=>k(ye,v(n,{onClosed:o,"onUpdate:show":a}),null);return j(e,e=>{n.message=e}),pe().render=s,{open:t,close:r,message:e}}});return e}function we(){if(!G.length){let e=Ce();G.push(e)}return G[G.length-1]}function Te(e={}){if(!p)return{};let t=we(),n=Se(e);return t.open(O({},K,xe.get(n.type||K.type),n)),t}var[Ee,q]=P(`empty`),De={image:N(`default`),imageSize:[Number,String,Array],description:String},Oe=h({name:Ee,props:De,setup(e,{slots:t}){let n=()=>{let n=t.description?t.description():e.description;if(n)return k(`p`,{class:q(`description`)},[n])},r=()=>{if(t.default)return k(`div`,{class:q(`bottom`)},[t.default()])},i=d(),a=e=>`${i}-${e}`,o=e=>`url(#${a(e)})`,s=(e,t,n)=>k(`stop`,{"stop-color":e,offset:`${t}%`,"stop-opacity":n},null),c=(e,t)=>[s(e,0),s(t,100)],l=e=>[k(`defs`,null,[k(`radialGradient`,{id:a(e),cx:`50%`,cy:`54%`,fx:`50%`,fy:`54%`,r:`297%`,gradientTransform:`matrix(-.16 0 0 -.33 .58 .72)`,"data-allow-mismatch":`attribute`},[s(`#EBEDF0`,0),s(`#F2F3F5`,100,.3)])]),k(`ellipse`,{fill:o(e),opacity:`.8`,cx:`80`,cy:`140`,rx:`46`,ry:`8`,"data-allow-mismatch":`attribute`},null)],u=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`a`),x1:`64%`,y1:`100%`,x2:`64%`,"data-allow-mismatch":`attribute`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M36 131V53H16v20H2v58h34z`,fill:o(`a`)},null),k(`path`,{d:`M123 15h22v14h9v77h-31V15z`,fill:o(`a`)},null)])],f=()=>[k(`defs`,null,[k(`linearGradient`,{id:a(`b`),x1:`64%`,y1:`97%`,x2:`64%`,y2:`0%`,"data-allow-mismatch":`attribute`},[s(`#F2F3F5`,0,.3),s(`#F2F3F5`,100)])]),k(`g`,{opacity:`.8`,"data-allow-mismatch":`children`},[k(`path`,{d:`M87 6c3 0 7 3 8 6a8 8 0 1 1-1 16H80a7 7 0 0 1-8-6c0-4 3-7 6-7 0-5 4-9 9-9Z`,fill:o(`b`)},null),k(`path`,{d:`M19 23c2 0 3 1 4 3 2 0 4 2 4 4a4 4 0 0 1-4 3v1h-7v-1l-1 1c-2 0-3-2-3-4 0-1 1-3 3-3 0-2 2-4 4-4Z`,fill:o(`b`)},null)])],p=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{id:a(1),x1:`64%`,y1:`100%`,x2:`64%`},[s(`#FFF`,0,.5),s(`#F2F3F5`,100)]),k(`linearGradient`,{id:a(2),x1:`50%`,x2:`50%`,y2:`84%`},[s(`#EBEDF0`,0),s(`#DCDEE0`,100,0)]),k(`linearGradient`,{id:a(3),x1:`100%`,x2:`100%`,y2:`100%`},[c(`#EAEDF0`,`#DCDEE0`)]),k(`radialGradient`,{id:a(4),cx:`50%`,cy:`0%`,fx:`50%`,fy:`0%`,r:`100%`,gradientTransform:`matrix(0 1 -.54 0 .5 -.5)`},[s(`#EBEDF0`,0),s(`#FFF`,100,0)])]),k(`g`,{fill:`none`},[u(),k(`path`,{fill:o(4),d:`M0 139h160v21H0z`,"data-allow-mismatch":`attribute`},null),k(`path`,{d:`M80 54a7 7 0 0 1 3 13v27l-2 2h-2a2 2 0 0 1-2-2V67a7 7 0 0 1 3-13z`,fill:o(2),"data-allow-mismatch":`attribute`},null),k(`g`,{opacity:`.6`,"stroke-linecap":`round`,"stroke-width":`7`,"data-allow-mismatch":`children`},[k(`path`,{d:`M64 47a19 19 0 0 0-5 13c0 5 2 10 5 13`,stroke:o(3)},null),k(`path`,{d:`M53 36a34 34 0 0 0 0 48`,stroke:o(3)},null),k(`path`,{d:`M95 73a19 19 0 0 0 6-13c0-5-2-9-6-13`,stroke:o(3)},null),k(`path`,{d:`M106 84a34 34 0 0 0 0-48`,stroke:o(3)},null)]),k(`g`,{transform:`translate(31 105)`},[k(`rect`,{fill:`#EBEDF0`,width:`98`,height:`34`,rx:`2`},null),k(`rect`,{fill:`#FFF`,x:`9`,y:`8`,width:`80`,height:`18`,rx:`1.1`},null),k(`rect`,{fill:`#EBEDF0`,x:`15`,y:`12`,width:`18`,height:`6`,rx:`1.1`},null)])])]),m=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(5)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`95%`,y1:`48%`,x2:`5.5%`,y2:`51%`,id:a(6)},[c(`#EAEDF1`,`#DCDEE0`)]),k(`linearGradient`,{y1:`45%`,x2:`100%`,y2:`54%`,id:a(7)},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),k(`g`,{transform:`translate(36 50)`,fill:`none`},[k(`g`,{transform:`translate(8)`},[k(`rect`,{fill:`#EBEDF0`,opacity:`.6`,x:`38`,y:`13`,width:`36`,height:`53`,rx:`2`},null),k(`rect`,{fill:o(5),width:`64`,height:`66`,rx:`2`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#FFF`,x:`6`,y:`6`,width:`52`,height:`55`,rx:`1`},null),k(`g`,{transform:`translate(15 17)`,fill:o(6),"data-allow-mismatch":`attribute`},[k(`rect`,{width:`34`,height:`6`,rx:`1`},null),k(`path`,{d:`M0 14h34v6H0z`},null),k(`rect`,{y:`28`,width:`34`,height:`6`,rx:`1`},null)])]),k(`rect`,{fill:o(7),y:`61`,width:`88`,height:`28`,rx:`1`,"data-allow-mismatch":`attribute`},null),k(`rect`,{fill:`#F7F8FA`,x:`29`,y:`72`,width:`30`,height:`6`,rx:`1`},null)])]),h=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,null,[k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(8),"data-allow-mismatch":`attribute`},[c(`#EAEDF1`,`#DCDEE0`)])]),u(),f(),l(`c`),k(`path`,{d:`m59 60 21 21 21-21h3l9 9v3L92 93l21 21v3l-9 9h-3l-21-21-21 21h-3l-9-9v-3l21-21-21-21v-3l9-9h3Z`,fill:o(8),"data-allow-mismatch":`attribute`},null)]),g=()=>k(`svg`,{viewBox:`0 0 160 160`},[k(`defs`,{"data-allow-mismatch":`children`},[k(`linearGradient`,{x1:`50%`,y1:`100%`,x2:`50%`,id:a(9)},[c(`#EEE`,`#D8D8D8`)]),k(`linearGradient`,{x1:`100%`,y1:`50%`,y2:`50%`,id:a(10)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(11)},[c(`#F2F3F5`,`#DCDEE0`)]),k(`linearGradient`,{x1:`50%`,x2:`50%`,y2:`100%`,id:a(12)},[c(`#FFF`,`#F7F8FA`)])]),u(),f(),l(`d`),k(`g`,{transform:`rotate(-45 113 -4)`,fill:`none`,"data-allow-mismatch":`children`},[k(`rect`,{fill:o(9),x:`24`,y:`52.8`,width:`5.8`,height:`19`,rx:`1`},null),k(`rect`,{fill:o(10),x:`22.1`,y:`67.3`,width:`9.9`,height:`28`,rx:`1`},null),k(`circle`,{stroke:o(11),"stroke-width":`8`,cx:`27`,cy:`27`,r:`27`},null),k(`circle`,{fill:o(12),cx:`27`,cy:`27`,r:`16`},null),k(`path`,{d:`M37 7c-8 0-15 5-16 12`,stroke:o(11),"stroke-width":`3`,opacity:`.5`,"stroke-linecap":`round`,transform:`rotate(45 29 13)`},null)])]),_=()=>{var n;if(t.image)return t.image();let r={error:h,search:g,network:p,default:m};return((n=r[e.image])==null?void 0:n.call(r))||k(`img`,{src:e.image},null)};return()=>k(`div`,{class:q()},[k(`div`,{class:q(`image`),style:me(e.imageSize)},[_()]),n(),r()])}}),J=I(Oe),[ke,Y,Ae]=P(`pull-refresh`),X=50,je=[`pulling`,`loosing`,`success`],Me={disabled:Boolean,modelValue:Boolean,headHeight:B(X),successText:String,pullingText:String,loosingText:String,loadingText:String,pullDistance:M,successDuration:B(500),animationDuration:B(300)},Ne=h({name:ke,props:Me,emits:[`change`,`refresh`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i,a=w(),o=w(),c=b(a),l=x({status:`normal`,distance:0,duration:0}),u=r(),d=()=>{if(e.headHeight!==X)return{height:`${e.headHeight}px`}},f=()=>l.status!==`loading`&&l.status!==`success`&&!e.disabled,p=t=>{let n=+(e.pullDistance||e.headHeight);return t>n&&(t=t{let i=+(e.pullDistance||e.headHeight);l.distance=n,r?l.status=`loading`:n===0?l.status=`normal`:n{let{status:t}=l;return t===`normal`?``:e[`${t}Text`]||Ae(t)},v=()=>{let{status:e,distance:t}=l;if(n[e])return n[e]({distance:t});let r=[];return je.includes(e)&&r.push(k(`div`,{class:Y(`text`)},[h()])),e===`loading`&&r.push(k(s,{class:Y(`loading`)},{default:h})),r},y=()=>{l.status=`success`,setTimeout(()=>{m(0)},+e.successDuration)},S=e=>{i=_(c.value)===0,i&&(l.duration=0,u.start(e))},C=e=>{f()&&S(e)},T=e=>{if(f()){i||S(e);let{deltaY:t}=u;u.move(e),i&&t.value>=0&&u.isVertical()&&(D(e),m(p(t.value)))}},E=()=>{i&&u.deltaY.value&&f()&&(l.duration=+e.animationDuration,l.status===`loosing`?(m(+e.headHeight,!0),t(`update:modelValue`,!0),g(()=>t(`refresh`))):m(0))};return j(()=>e.modelValue,t=>{l.duration=+e.animationDuration,t?m(+e.headHeight,!0):n.success||e.successText?y():m(0,!1)}),te(`touchmove`,T,{target:o}),()=>{var e;let t={transitionDuration:`${l.duration}ms`,transform:l.distance?`translate3d(0,${l.distance}px, 0)`:``};return k(`div`,{ref:a,class:Y()},[k(`div`,{ref:o,class:Y(`track`),style:t,onTouchstartPassive:C,onTouchend:E,onTouchcancel:E},[k(`div`,{class:Y(`head`),style:d()},[v()]),(e=n.default)==null?void 0:e.call(n)])])}}}),Pe=I(Ne),[Fe,Z,Q]=P(`search`),Ie=O({},u,{label:String,shape:N(`square`),leftIcon:N(`search`),clearable:re,actionText:String,background:String,showAction:Boolean}),Le=h({name:Fe,props:Ie,emits:[`blur`,`focus`,`clear`,`search`,`cancel`,`clickInput`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(e,{emit:t,slots:n,attrs:r}){let i=d(),a=w(),o=()=>{n.action||(t(`update:modelValue`,``),t(`cancel`))},s=n=>{n.keyCode===13&&(D(n),t(`search`,e.modelValue))},l=()=>e.id||`${i}-input`,p=()=>{if(n.label||e.label)return k(`label`,{class:Z(`label`),for:l(),"data-allow-mismatch":`attribute`},[n.label?n.label():e.label])},m=()=>{if(e.showAction){let t=e.actionText||Q(`cancel`);return k(`div`,{class:Z(`action`),role:`button`,tabindex:0,onClick:o},[n.action?n.action():t])}},h=()=>{var e;return(e=a.value)==null?void 0:e.blur()},g=()=>{var e;return(e=a.value)==null?void 0:e.focus()},_=e=>t(`blur`,e),y=e=>t(`focus`,e),b=e=>t(`clear`,e),x=e=>t(`clickInput`,e),S=e=>t(`clickLeftIcon`,e),C=e=>t(`clickRightIcon`,e),T=Object.keys(u),E=()=>{let i=O({},r,z(e,T),{id:l()});return k(f,v({ref:a,type:`search`,class:Z(`field`,{"with-message":i.errorMessage}),border:!1,labelAlign:`left`,onBlur:_,onFocus:y,onClear:b,onKeypress:s,onClickInput:x,onClickLeftIcon:S,onClickRightIcon:C,"onUpdate:modelValue":e=>t(`update:modelValue`,e)},i),z(n,[`left-icon`,`right-icon`]))};return c({focus:g,blur:h}),()=>{var t;return k(`div`,{class:Z({"show-action":e.showAction}),style:{background:e.background}},[(t=n.left)==null?void 0:t.call(n),k(`div`,{class:Z(`content`,e.shape)},[p(),E()]),m()])}}}),$=I(Le),Re={class:`switcher-body`},ze={key:0,class:`switcher-state`},Be={key:0,class:`current`},Ve=t(h({__name:`AccountSwitcher`,props:{show:{type:Boolean,default:!1},showModifiers:{}},emits:[`update:show`],setup(e){let t=m(e,`show`),n=w(``),r=w([]),i=w(!1),c=w(``),u=0,d,f=ce(()=>{var e;return(e=ue.me)==null?void 0:e.account});async function p(){let e=++u;i.value=!0,c.value=``;try{let{items:t}=await le.accounts(n.value.trim());e===u&&(r.value=t.filter(e=>{var t;return e.id!==((t=f.value)==null?void 0:t.id)}))}catch(t){e===u&&(c.value=t instanceof ae?t.message:`加载失败`)}finally{e===u&&(i.value=!1)}}j(t,e=>{e&&p()}),j(n,()=>{window.clearTimeout(d),d=window.setTimeout(p,300)});function h(e){ie(e),t.value=!1}function g(e){return e.device_count?e.online?e.listening?`监听中`:`已停止`:`离线`:`没有电脑`}function _(e){return e.online?e.listening?`tone-success`:`tone-warning`:`tone-muted`}return(e,u)=>(y(),F(V(a),{show:t.value,"onUpdate:show":u[2]||(u[2]=e=>t.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`switcher`},{default:A(()=>[L(`div`,Re,[u[3]||(u[3]=L(`h3`,null,`切换客服`,-1)),k(V($),{modelValue:n.value,"onUpdate:modelValue":u[0]||(u[0]=e=>n.value=e),shape:`round`,placeholder:`搜索客服姓名或账号`},null,8,[`modelValue`]),k(V(l),{inset:``},{default:A(()=>[k(V(o),{title:`我自己`,label:f.value?`${f.value.nickname||f.value.sn}`:``,icon:`user-o`,clickable:``,value:V(R)===void 0?`当前`:``,onClick:u[1]||(u[1]=e=>h(null))},null,8,[`label`,`value`])]),_:1}),i.value&&!r.value.length?(y(),H(`div`,ze,[k(V(s),{size:`20`})])):c.value?(y(),F(V(J),{key:1,image:`network`,description:c.value},null,8,[`description`])):r.value.length?(y(),F(V(l),{key:3,inset:``,class:`switcher-list`},{default:A(()=>[(y(!0),H(se,null,T(r.value,e=>(y(),F(V(o),{key:e.id,title:e.nickname||e.sn||`客服 #${e.id}`,label:e.sn,clickable:``,onClick:t=>h(e)},{value:A(()=>[L(`span`,{class:S(_(e))},ne(g(e)),3),V(R)===e.id?(y(),H(`span`,Be,`当前`)):fe(``,!0)]),_:2},1032,[`title`,`label`,`onClick`]))),128))]),_:1})):(y(),F(V(J),{key:2,image:`search`,description:`没有找到其他客服`}))])]),_:1},8,[`show`]))}}),[[`__scopeId`,`data-v-91cf283c`]]);export{Te as a,J as i,$ as n,Pe as r,Ve as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css new file mode 100644 index 000000000..03b9e2d0d --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/AccountSwitcher-BZXg15MH.css @@ -0,0 +1 @@ +.switcher[data-v-91cf283c]{max-height:80vh}.switcher-body[data-v-91cf283c]{padding:16px 0 12px}.switcher-body h3[data-v-91cf283c]{margin:0 16px 8px;font-size:17px}.switcher-state[data-v-91cf283c]{place-items:center;padding:32px 0;display:grid}.switcher-list[data-v-91cf283c]{margin-top:12px}.current[data-v-91cf283c]{color:var(--app-primary);margin-left:6px} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js new file mode 100644 index 000000000..a58d214a6 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CLYafisP.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,n as r,p as i,r as a,t as o}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n as s}from"./use-placeholder-CcVnlsb4.js";import{a as c,d as l,i as u,o as d,s as f,u as p}from"./format-DEXJ_MrY.js";import{a as m,n as h,o as g,s as _,t as v}from"./cell-group-B1PakqJq.js";import{r as y,t as b}from"./field-CRX3eS7h.js";import{a as x,i as S,n as ee,r as C,t as w}from"./AccountSwitcher-5FuHazzf.js";import{$ as te,A as ne,At as T,B as E,C as D,Ct as O,D as k,Dt as A,E as j,Et as M,F as N,Ft as P,G as re,Gt as ie,H as ae,Ht as F,I,It as oe,J as se,Jt as ce,Kt as L,Lt as le,M as ue,Mt as de,N as fe,O as pe,Ot as me,Pt as he,Q as ge,R as _e,S as ve,St as R,Tt as ye,U as be,Ut as z,V as xe,Vt as B,W as V,Wt as Se,X as H,Y as Ce,Yt as U,Z as W,_ as G,_t as K,bt as we,c as Te,et as Ee,ft as De,g as Oe,gt as q,ht as J,j as ke,jt as Ae,kt as je,l as Me,n as Ne,o as Pe,ot as Fe,p as Ie,q as Y,qt as X,s as Le,t as Re,ut as ze,v as Be,vt as Z,w as Ve,wt as He,xt as Q,y as Ue,yt as $,z as We}from"./index-Gkjcdjlo.js";var Ge=Array.isArray,Ke=e=>typeof e==`string`,qe=e=>typeof e==`object`&&!!e,Je=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ye=/\B([A-Z])/g,Xe=Je(e=>e.replace(Ye,`-$1`).toLowerCase());function Ze(e){if(Ge(e)){let t={};for(let n=0;ne.startsWith(`/*`)?``:e).split(Qe).forEach(e=>{if(e){let n=e.split($e);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function nt(e){if(!e)return``;if(Ke(e))return e;let t=``;for(let n in e){let r=e[n];if(Ke(r)||typeof r==`number`){let e=n.startsWith(`--`)?n:Xe(n);t+=`${e}:${r};`}}return t}function rt(e){let t=``;if(Ke(e))t=e;else if(Ge(e))for(let n=0;nt||!o&&at?i=We(u):r&&(i=We(r))}return u(),l}function ot(){let e=L([]),t=[];return T(()=>{e.value=[]}),[e,n=>(t[n]||(t[n]=t=>{e.value[n]=t}),t[n])]}function st(e,t){if(!te||!window.IntersectionObserver)return;let n=new IntersectionObserver(e=>{t(e[0].intersectionRatio>0)},{root:document.body}),r=()=>{e.value&&n.observe(e.value)},i=()=>{e.value&&n.unobserve(e.value)};Ae(i),je(i),_e(r)}var[ct,lt]=G(`sticky`),ut={zIndex:H,position:Ce(`top`),container:Object,offsetTop:Y(0),offsetBottom:Y(0)},dt=O({name:ct,props:ut,emits:[`scroll`,`change`],setup(e,{emit:t,slots:n}){let r=L(),i=re(r),a=ie({fixed:!1,width:0,height:0,transform:0}),o=L(!1),s=J(()=>D(e.position===`top`?e.offsetTop:e.offsetBottom)),c=J(()=>{if(o.value)return;let{fixed:e,height:t,width:n}=a;if(e)return{width:`${n}px`,height:`${t}px`}}),l=J(()=>{if(!a.fixed||o.value)return;let t=ge(ve(e.zIndex),{width:`${a.width}px`,height:`${a.height}px`,[e.position]:`${s.value}px`});return a.transform&&(t.transform=`translate3d(0, ${a.transform}px, 0)`),t}),u=e=>t(`scroll`,{scrollTop:e,isFixed:a.fixed}),d=()=>{if(!r.value||k(r))return;let{container:t,position:n}=e,i=V(r),o=j(window);if(a.width=i.width,a.height=i.height,n===`top`){if(t){let e=V(t),n=e.bottom-s.value-a.height;a.fixed=s.value>i.top&&e.bottom>0,a.transform=n<0?n:0}else a.fixed=s.value>i.top}else{let{clientHeight:e}=document.documentElement;if(t){let n=V(t),r=e-n.top-s.value-a.height;a.fixed=e-s.valuen.top,a.transform=r<0?-r:0}else a.fixed=e-s.valuea.fixed,e=>t(`change`,e)),xe(`scroll`,d,{target:i,passive:!0}),st(r,d),B([fe,ue],()=>{r.value&&!k(r)&&a.fixed&&(o.value=!0,A(()=>{let e=V(r);a.width=e.width,a.height=e.height,o.value=!1}))}),()=>{var e;return R(`div`,{ref:r,style:c.value},[R(`div`,{class:lt({fixed:a.fixed&&!o.value}),style:l.value},[(e=n.default)==null?void 0:e.call(n)])])}}}),ft=Oe(dt),[pt,mt]=G(`swipe`),ht={loop:W,width:H,height:H,vertical:Boolean,autoplay:Y(0),duration:Y(500),touchable:W,lazyRender:Boolean,initialSwipe:Y(0),indicatorColor:String,showIndicators:W,stopPropagation:W},gt=Symbol(pt),_t=O({name:pt,props:ht,emits:[`change`,`dragStart`,`dragEnd`],setup(e,{emit:n,slots:r}){let i=L(),a=L(),o=ie({rect:null,width:0,height:0,offset:0,active:0,swiping:!1}),s=!1,c=l(),{children:u,linkChildren:d}=E(gt),f=J(()=>u.length),p=J(()=>o[e.vertical?`height`:`width`]),m=J(()=>e.vertical?c.deltaY.value:c.deltaX.value),h=J(()=>o.rect?(e.vertical?o.rect.height:o.rect.width)-p.value*f.value:0),g=J(()=>p.value?Math.ceil(Math.abs(h.value)/p.value):f.value),v=J(()=>f.value*p.value),y=J(()=>(o.active+f.value)%f.value),b=J(()=>{let t=e.vertical?`vertical`:`horizontal`;return c.direction.value===t}),x=J(()=>{let t={transitionDuration:`${o.swiping?0:e.duration}ms`,transform:`translate${e.vertical?`Y`:`X`}(${+o.offset.toFixed(2)}px)`};if(p.value){let n=e.vertical?`height`:`width`,r=e.vertical?`width`:`height`;t[n]=`${v.value}px`,t[r]=e[r]?`${e[r]}px`:``}return t}),S=t=>{let{active:n}=o;return t?e.loop?Ue(n+t,-1,f.value):Ue(n+t,0,g.value):n},ee=(t,n=0)=>{let r=t*p.value;e.loop||(r=Math.min(r,-h.value));let i=n-r;return e.loop||(i=Ue(i,h.value,0)),i},C=({pace:t=0,offset:r=0,emitChange:i})=>{if(f.value<=1)return;let{active:a}=o,s=S(t),c=ee(s,r);if(e.loop){if(u[0]&&c!==h.value){let e=c0;u[f.value-1].setOffset(e?-v.value:0)}}o.active=s,o.offset=c,i&&s!==a&&n(`change`,y.value)},w=()=>{o.swiping=!0,o.active<=-1?C({pace:f.value}):o.active>=f.value&&C({pace:-f.value})},te=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:-1,emitChange:!0})})},ne=()=>{w(),c.reset(),I(()=>{o.swiping=!1,C({pace:1,emitChange:!0})})},T,D=()=>clearTimeout(T),O=()=>{D(),+e.autoplay>0&&f.value>1&&(T=setTimeout(()=>{ne(),O()},+e.autoplay))},j=(t=+e.initialSwipe)=>{if(!i.value)return;let n=()=>{var n,r;if(!k(i)){let t={width:i.value.offsetWidth,height:i.value.offsetHeight};o.rect=t,o.width=+((n=e.width)==null?t.width:n),o.height=+((r=e.height)==null?t.height:r)}f.value&&(t=Math.min(f.value-1,t),t===-1&&(t=f.value-1)),o.active=t,o.swiping=!0,o.offset=ee(t),u.forEach(e=>{e.setOffset(0)}),O()};k(i)?A().then(n):n()},M=()=>j(o.active),N,P=t=>{!e.touchable||t.touches.length>1||(c.start(t),s=!1,N=Date.now(),D(),w())},re=t=>{e.touchable&&o.swiping&&(c.move(t),b.value&&(!e.loop&&(o.active===0&&m.value>0||o.active===f.value-1&&m.value<0)||(pe(t,e.stopPropagation),C({offset:m.value}),s||(n(`dragStart`,{index:y.value}),s=!0))))},F=()=>{if(!e.touchable||!o.swiping)return;let t=Date.now()-N,r=m.value/t;if((Math.abs(r)>.25||Math.abs(m.value)>p.value/2)&&b.value){let t=e.vertical?c.offsetY.value:c.offsetX.value,n=0;n=e.loop?t>0?m.value>0?-1:1:0:-Math[m.value>0?`ceil`:`floor`](m.value/p.value),C({pace:n,emitChange:!0})}else m.value&&C({pace:0});s=!1,o.swiping=!1,n(`dragEnd`,{index:y.value}),O()},oe=(t,n={})=>{w(),c.reset(),I(()=>{let r;r=e.loop&&t===f.value?o.active===0?0:t:t%f.value,n.immediate?I(()=>{o.swiping=!1}):o.swiping=!1,C({pace:r-o.active,emitChange:!0})})},se=(t,n)=>{let r=n===y.value,i=r?{backgroundColor:e.indicatorColor}:void 0;return R(`i`,{style:i,class:mt(`indicator`,{active:r})},null)},ce=()=>{if(r.indicator)return r.indicator({active:y.value,total:f.value});if(e.showIndicators&&f.value>1)return R(`div`,{class:mt(`indicators`,{vertical:e.vertical})},[Array(f.value).fill(``).map(se)])};return _({prev:te,next:ne,state:o,resize:M,swipeTo:oe}),d({size:p,props:e,count:f,activeIndicator:y}),B(()=>e.initialSwipe,e=>j(+e)),B(f,()=>j(o.active)),B(()=>e.autoplay,O),B([fe,ue,()=>e.width,()=>e.height],M),B(ae(),e=>{e===`visible`?O():D()}),de(j),me(()=>j(o.active)),t(()=>j(o.active)),Ae(D),je(D),xe(`touchmove`,re,{target:a}),()=>{var t;return R(`div`,{ref:i,class:mt()},[R(`div`,{ref:a,style:x.value,class:mt(`track`,{vertical:e.vertical}),onTouchstartPassive:P,onTouchend:F,onTouchcancel:F},[(t=r.default)==null?void 0:t.call(r)]),ce()])}}}),vt=Oe(_t),[yt,bt]=G(`tabs`),xt=O({name:yt,props:{count:se(Number),inited:Boolean,animated:Boolean,duration:se(H),swipeable:Boolean,lazyRender:Boolean,currentIndex:se(Number)},emits:[`change`],setup(e,{emit:t,slots:n}){let r=L(),i=e=>t(`change`,e),a=()=>{var t;let a=(t=n.default)==null?void 0:t.call(n);return e.animated||e.swipeable?R(vt,{ref:r,loop:!1,class:bt(`track`),duration:e.duration*1e3,touchable:e.swipeable,lazyRender:e.lazyRender,showIndicators:!1,onChange:i},{default:()=>[a]}):a},o=t=>{let n=r.value;n&&n.state.active!==t&&n.swipeTo(t,{immediate:!e.inited})};return B(()=>e.currentIndex,o),de(()=>{o(e.currentIndex)}),_({swipeRef:r}),()=>R(`div`,{class:bt(`content`,{animated:e.animated||e.swipeable})},[a()])}}),[St,Ct]=G(`tabs`),wt={type:Ce(`line`),color:String,border:Boolean,sticky:Boolean,shrink:Boolean,active:Y(0),duration:Y(.3),animated:Boolean,ellipsis:W,swipeable:Boolean,scrollspy:Boolean,offsetTop:Y(0),background:String,lazyRender:W,showHeader:W,lineWidth:H,lineHeight:H,beforeChange:Function,swipeThreshold:Y(5),titleActiveColor:String,titleInactiveColor:String},Tt=Symbol(St),Et=O({name:St,props:wt,emits:[`change`,`scroll`,`rendered`,`clickTab`,`update:active`],setup(e,{emit:r,slots:a}){let o,c,l,u,d,f=L(),p=L(),m=L(),h=L(),g=y(),v=re(f),[b,x]=ot(),{children:S,linkChildren:ee}=E(Tt),C=ie({inited:!1,position:``,lineStyle:{},currentIndex:-1}),w=J(()=>S.length>+e.swipeThreshold||!e.ellipsis||e.shrink),te=J(()=>({borderColor:e.color,background:e.background})),T=(e,t)=>{var n;return(n=e.name)==null?t:n},O=J(()=>{let e=S[C.currentIndex];if(e)return T(e,C.currentIndex)}),j=J(()=>D(e.offsetTop)),M=J(()=>e.sticky?j.value+o:0),N=t=>{let n=p.value,r=b.value;if(!w.value||!n||!r||!r[C.currentIndex])return;let i=r[C.currentIndex].$el,a=i.offsetLeft-(n.offsetWidth-i.offsetWidth)/2;u&&u(),u=it(n,a,t?0:+e.duration)},P=()=>{let t=C.inited;A(()=>{let n=b.value;if(!n||!n[C.currentIndex]||e.type!==`line`||k(f.value))return;let r=n[C.currentIndex].$el,{lineWidth:i,lineHeight:a}=e,o=r.offsetLeft+r.offsetWidth/2,s={width:Be(i),backgroundColor:e.color,transform:`translateX(${o}px) translateX(-50%)`};if(t&&(s.transitionDuration=`${e.duration}s`),Ee(a)){let e=Be(a);s.height=e,s.borderRadius=e}C.lineStyle=s})},ae=e=>{let t=e=0&&e{let i=ae(t);if(!Ee(i))return;let a=S[i],o=T(a,i),s=C.currentIndex!==null;C.currentIndex!==i&&(C.currentIndex=i,n||N(),P()),o!==e.active&&(r(`update:active`,o),s&&r(`change`,o,a.title)),l&&!e.scrollspy&&ne(Math.ceil(Ve(f.value)-j.value))},I=(e,t)=>{let n=S.findIndex((t,n)=>T(t,n)===e);F(n===-1?0:n,t)},oe=(t=!1)=>{if(e.scrollspy){let n=S[C.currentIndex].$el;if(n&&v.value){let r=Ve(n,v.value)-M.value;c=!0,d&&d(),d=at(v.value,r,t?0:+e.duration,()=>{c=!1})}}},se=(t,i,a)=>{let{title:o,disabled:c}=S[i],l=T(S[i],i);c||(s(e.beforeChange,{args:[l],done:()=>{F(i),oe()}}),n(t)),r(`clickTab`,{name:l,title:o,event:a,disabled:c})},ce=e=>{l=e.isFixed,r(`scroll`,e)},le=e=>{A(()=>{I(e),oe(!0)})},ue=()=>{for(let e=0;eM.value)return e===0?0:e-1}return S.length-1},de=()=>{if(e.scrollspy&&!c){let e=ue();F(e)}},pe=()=>{if(e.type===`line`&&S.length)return R(`div`,{class:Ct(`line`),style:C.lineStyle},null)},he=()=>{var t,n,r;let{type:o,border:s,sticky:c}=e,l=[R(`div`,{ref:c?void 0:m,class:[Ct(`wrap`),{[i]:o===`line`&&s}]},[R(`div`,{ref:p,role:`tablist`,class:Ct(`nav`,[o,{shrink:e.shrink,complete:w.value}]),style:te.value,"aria-orientation":`horizontal`},[(t=a[`nav-left`])==null?void 0:t.call(a),S.map(e=>e.renderTitle(se)),pe(),(n=a[`nav-right`])==null?void 0:n.call(a)])]),(r=a[`nav-bottom`])==null?void 0:r.call(a)];return c?R(`div`,{ref:m},[l]):l},ge=()=>{P(),A(()=>{var e,t;N(!0),(t=(e=h.value)==null?void 0:e.swipeRef.value)==null||t.resize()})};return B(()=>[e.color,e.duration,e.lineWidth,e.lineHeight],P),B(fe,ge),B(()=>e.active,e=>{e!==O.value&&I(e)}),B(()=>S.length,()=>{C.inited&&(I(e.active),P(),A(()=>{N(!0)}))}),_({resize:ge,scrollTo:le}),me(P),t(P),_e(()=>{I(e.active,!0),A(()=>{C.inited=!0,m.value&&(o=V(m.value).height),N(!0)})}),st(f,P),xe(`scroll`,de,{target:v,passive:!0}),ee({id:g,props:e,setLine:P,scrollable:w,onRendered:(e,t)=>r(`rendered`,e,t),currentName:O,setTitleRefs:x,scrollIntoView:N}),()=>R(`div`,{ref:f,class:Ct([e.type])},[e.showHeader?e.sticky?R(ft,{container:f.value,offsetTop:j.value,onScroll:ce},{default:()=>[he()]}):he():null,R(xt,{ref:h,count:S.length,inited:C.inited,animated:e.animated,duration:e.duration,swipeable:e.swipeable,lazyRender:e.lazyRender,currentIndex:C.currentIndex,onChange:F},{default:()=>{var e;return[(e=a.default)==null?void 0:e.call(a)]}})])}}),Dt=Symbol(),Ot=Symbol(),kt=()=>ye(Ot,null),At=e=>{let t=kt();oe(Dt,e),oe(Ot,J(()=>(t==null||t.value)&&e.value))},[jt,Mt]=G(`tab`),Nt=O({name:jt,props:{id:String,dot:Boolean,type:String,color:String,title:String,badge:H,shrink:Boolean,isActive:Boolean,disabled:Boolean,controls:String,scrollable:Boolean,activeColor:String,inactiveColor:String,showZeroBadge:W},setup(e,{slots:t}){let n=J(()=>{let t={},{type:n,color:r,disabled:i,isActive:a,activeColor:o,inactiveColor:s}=e;r&&n===`card`&&(t.borderColor=r,i||(a?t.backgroundColor=r:t.color=r));let c=a?o:s;return c&&(t.color=c),t}),r=()=>{let n=R(`span`,{class:Mt(`text`,{ellipsis:!e.scrollable})},[t.title?t.title():e.title]);return e.dot||Ee(e.badge)&&e.badge!==``?R(a,{dot:e.dot,content:e.badge,showZero:e.showZeroBadge},{default:()=>[n]}):n};return()=>R(`div`,{id:e.id,role:`tab`,class:[Mt([e.type,{grow:e.scrollable&&!e.shrink,shrink:e.shrink,active:e.isActive,disabled:e.disabled}])],style:n.value,tabindex:e.disabled?void 0:e.isActive?0:-1,"aria-selected":e.isActive,"aria-disabled":e.disabled||void 0,"aria-controls":e.controls,"data-allow-mismatch":`attribute`},[r()])}}),[Pt,Ft]=G(`swipe-item`),It=O({name:Pt,setup(e,{slots:t}){let n,r=ie({offset:0,inited:!1,mounted:!1}),{parent:i,index:a}=be(gt);if(!i)return;let o=J(()=>{let e={},{vertical:t}=i.props;return i.size.value&&(e[t?`height`:`width`]=`${i.size.value}px`),r.offset&&(e.transform=`translate${t?`Y`:`X`}(${r.offset}px)`),e}),s=J(()=>{let{loop:e,lazyRender:t}=i.props;if(!t||n)return!0;if(!r.mounted)return!1;let o=i.activeIndicator.value,s=i.count.value-1,c=o===0&&e?s:o-1,l=o===s&&e?0:o+1;return n=a.value===o||a.value===c||a.value===l,n});return de(()=>{A(()=>{r.mounted=!0})}),_({setOffset:e=>{r.offset=e}}),()=>{var e;return R(`div`,{class:Ft(),style:o.value},[s.value?(e=t.default)==null?void 0:e.call(t):null])}}}),Lt=Oe(It),[Rt,zt]=G(`tab`),Bt=ge({},e,{dot:Boolean,name:H,badge:H,title:String,disabled:Boolean,titleClass:null,titleStyle:[String,Object],showZeroBadge:W}),Vt=O({name:Rt,props:Bt,setup(e,{slots:t}){let n=y(),r=L(!1),i=He(),{parent:a,index:o}=be(Tt);if(!a)return;let s=()=>{var t;return(t=e.name)==null?o.value:t},c=()=>{r.value=!0,a.props.lazyRender&&A(()=>{a.onRendered(s(),e.title)})},l=J(()=>{let e=s()===a.currentName.value;return e&&!r.value&&c(),e}),u=L(``),d=L(``);F(()=>{let{titleClass:t,titleStyle:n}=e;u.value=t?rt(t):``,d.value=n&&typeof n!=`string`?nt(Ze(n)):n});let f=r=>R(Nt,M({key:n,id:`${a.id}-${o.value}`,ref:a.setTitleRefs(o.value),style:d.value,class:u.value,isActive:l.value,controls:n,scrollable:a.scrollable.value,activeColor:a.props.titleActiveColor,inactiveColor:a.props.titleInactiveColor,onClick:e=>r(i.proxy,o.value,e)},Fe(a.props,[`type`,`color`,`shrink`]),Fe(e,[`dot`,`badge`,`title`,`disabled`,`showZeroBadge`])),{title:t.title}),p=L(!l.value);return B(l,e=>{e?p.value=!1:I(()=>{p.value=!0})}),B(()=>e.title,()=>{a.setLine(),a.scrollIntoView()}),At(l),_({id:n,renderTitle:f}),()=>{var e;let i=`${a.id}-${o.value}`,{animated:s,swipeable:c,scrollspy:u,lazyRender:d}=a.props;if(!t.default&&!s)return;let f=u||l.value;if(s||c)return R(Lt,{id:n,role:`tabpanel`,class:zt(`panel-wrapper`,{inactive:p.value}),tabindex:l.value?0:-1,"aria-hidden":!l.value,"aria-labelledby":i,"data-allow-mismatch":`attribute`},{default:()=>{var e;return[R(`div`,{class:zt(`panel`)},[(e=t.default)==null?void 0:e.call(t)])]}});let m=r.value||u||!d?(e=t.default)==null?void 0:e.call(t):null;return Se(R(`div`,{id:n,role:`tabpanel`,class:zt(`panel`),tabindex:f?0:-1,"aria-labelledby":i,"data-allow-mismatch":`attribute`},[m]),[[ze,f]])}}}),Ht=Oe(Vt),Ut=Oe(Et),[Wt,Gt,Kt]=G(`list`),qt={error:Boolean,offset:Y(300),loading:Boolean,disabled:Boolean,finished:Boolean,scroller:Object,errorText:String,direction:Ce(`down`),loadingText:{type:String,default:``},finishedText:String,immediateCheck:W},Jt=O({name:Wt,props:qt,emits:[`load`,`update:error`,`update:loading`],setup(e,{emit:t,slots:n}){let r=L(e.loading),i=L(),a=L(),o=kt(),s=re(i),c=J(()=>e.scroller||s.value),l=()=>{A(()=>{if(r.value||e.finished||e.disabled||e.error||(o==null?void 0:o.value)===!1)return;let{direction:n}=e,s=+e.offset,l=V(c);if(!l.height||k(i))return;let u=!1,d=V(a);u=n===`up`?l.top-d.top<=s:d.bottom-l.bottom<=s,u&&(r.value=!0,t(`update:loading`,!0),t(`load`))})},u=()=>{if(e.finished){let t=n.finished?n.finished():e.finishedText;if(t)return R(`div`,{class:Gt(`finished-text`)},[t])}},d=()=>{t(`update:error`,!1),l()},f=()=>{if(e.error){let t=n.error?n.error():e.errorText;if(t)return R(`div`,{role:`button`,class:Gt(`error-text`),tabindex:0,onClick:d},[t])}},p=()=>{if(r.value&&!e.finished&&!e.disabled)return R(`div`,{class:Gt(`loading`)},[n.loading?n.loading():e.loadingText!=null&&R(g,{class:Gt(`loading-icon`)},{default:()=>[e.loadingText||Kt(`loading`)]})])};return B(()=>[e.loading,e.finished,e.error],l),o&&B(o,e=>{e&&l()}),he(()=>{r.value=e.loading}),de(()=>{e.immediateCheck&&l()}),_({check:l}),xe(`scroll`,l,{target:c,passive:!0}),()=>{var t;let o=(t=n.default)==null?void 0:t.call(n),s=R(`div`,{ref:a,class:Gt(`placeholder`)},null);return R(`div`,{ref:i,role:`feed`,class:Gt(),"aria-busy":r.value},[e.direction===`down`?o:s,p(),u(),f(),e.direction===`up`?o:s])}}}),Yt=Oe(Jt),Xt={class:`detail`},Zt={class:`detail-head`},Qt={class:`avatar`,"aria-hidden":`true`},$t={class:`detail-title`},en={class:`app-muted`},tn={key:0,class:`badge`},nn={class:`app-mono`},rn={key:0,class:`state state--blocked`},an={key:1,class:`note`},on={key:1,class:`state`},sn={key:1,class:`note`},cn=o(O({__name:`ConversationDetail`,props:{conversation:{},accountId:{},canAdd:{type:Boolean},canRemove:{type:Boolean},addCode:{},removeCode:{}},emits:[`updated`],setup(e,{emit:t}){let n=e,i=t,a=L(``),o=L(!1);B(()=>`${n.conversation.account}|${n.conversation.conversation}`,()=>{a.value=``});let s=J(()=>n.conversation.blacklist),l=J(()=>{var e;let t=(e=s.value)==null?void 0:e.source;return t===`mobile`?`手机控制台`:t===`desktop`?`客服电脑`:t||`—`});async function d(){if(!o.value){try{await f({title:`加入黑名单?`,message:`加入后,系统不再向「${n.conversation.name}」自动发送任何消息,只能由客服手动处理。`,confirmButtonText:`加入黑名单`,confirmButtonColor:`var(--app-danger)`})}catch{return}o.value=!0;try{let{entry:e}=await Me.block(n.conversation,a.value.trim(),n.accountId);i(`updated`,{...n.conversation,blocked:!0,blacklist:e}),x({message:`已加入黑名单,电脑同步后立即生效`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}async function p(){if(!o.value){try{await f({title:`移出黑名单?`,message:`移出后,系统会恢复向「${n.conversation.name}」自动回复。`,confirmButtonText:`移出`})}catch{return}o.value=!0;try{await Me.unblock(n.conversation.account,n.conversation.conversation,n.accountId),i(`updated`,{...n.conversation,blocked:!1,blacklist:null}),x({message:`已移出黑名单`,type:`success`})}catch(e){x({message:e instanceof Te?e.message:`操作失败,请重试`,type:`fail`})}finally{o.value=!1}}}return(t,n)=>(P(),$(`section`,Xt,[q(`header`,Zt,[q(`span`,Qt,U(X(u)(e.conversation.name)),1),q(`div`,$t,[q(`h2`,null,U(e.conversation.name),1),q(`p`,en,U(e.conversation.type_label),1)]),e.conversation.blocked?(P(),$(`span`,tn,[R(X(r),{name:`shield-o`}),n[1]||(n[1]=Q(` 黑名单`,-1))])):Z(``,!0)]),R(X(v),{inset:``,class:`group`},{default:z(()=>[R(X(h),{title:`企业微信账号`,value:e.conversation.account_name||`—`,label:e.conversation.account},null,8,[`value`,`label`]),R(X(h),{title:`最近消息`,value:e.conversation.last_message_at?X(c)(e.conversation.last_message_at):`—`},null,8,[`value`]),R(X(h),{title:`会话标识`},{label:z(()=>[q(`span`,nn,U(e.conversation.conversation),1)]),_:1})]),_:1}),e.conversation.blocked&&s.value?(P(),$(`div`,rn,[q(`h3`,null,[R(X(r),{name:`shield-o`}),n[2]||(n[2]=Q(` 已加入黑名单`,-1))]),n[8]||(n[8]=q(`p`,null,`系统不会向这个会话自动发送消息;电脑离线时,下次联网自动同步。`,-1)),q(`dl`,null,[q(`div`,null,[n[3]||(n[3]=q(`dt`,null,`原因`,-1)),q(`dd`,null,U(s.value.reason||`未填写`),1)]),q(`div`,null,[n[4]||(n[4]=q(`dt`,null,`操作人`,-1)),q(`dd`,null,U(s.value.created_by||`—`),1)]),q(`div`,null,[n[5]||(n[5]=q(`dt`,null,`时间`,-1)),q(`dd`,null,U(X(c)(s.value.created_at)),1)]),q(`div`,null,[n[6]||(n[6]=q(`dt`,null,`来源`,-1)),q(`dd`,null,U(l.value),1)])]),e.canRemove?(P(),K(X(m),{key:0,block:``,round:``,plain:``,type:`primary`,icon:`revoke`,loading:o.value,onClick:p},{default:z(()=>[...n[7]||(n[7]=[Q(` 移出黑名单 `,-1)])]),_:1},8,[`loading`])):(P(),$(`p`,an,`移出黑名单需要主管权限(`+U(e.removeCode)+`),请联系主管处理。`,1))])):(P(),$(`div`,on,[q(`h3`,null,[R(X(r),{name:`info-o`}),n[9]||(n[9]=Q(` 正常接收自动回复`,-1))]),n[11]||(n[11]=q(`p`,null,`客户投诉、需要人工跟进或不适合机器人回复的会话,可以加入黑名单。`,-1)),e.canAdd?(P(),$(De,{key:0},[R(X(v),{inset:``,class:`reason`},{default:z(()=>[R(X(b),{modelValue:a.value,"onUpdate:modelValue":n[0]||(n[0]=e=>a.value=e),label:`原因`,placeholder:`选填,例如:客户要求人工服务`,maxlength:`200`,"show-word-limit":``,rows:`2`,autosize:``,type:`textarea`},null,8,[`modelValue`])]),_:1}),R(X(m),{block:``,round:``,type:`danger`,icon:`shield-o`,loading:o.value,onClick:d},{default:z(()=>[...n[10]||(n[10]=[Q(` 加入黑名单 `,-1)])]),_:1},8,[`loading`])],64)):(P(),$(`p`,sn,`加入黑名单需要 ZYT 权限(`+U(e.addCode)+`),请联系管理员开通。`,1))]))]))}}),[[`__scopeId`,`data-v-70465908`]]),ln={class:`body`},un={class:`line`},dn={class:`name`},fn={class:`time`},pn={class:`line sub`},mn={class:`meta`},hn={key:0,class:`blocked`},gn=o(O({__name:`ConversationRow`,props:{item:{},active:{type:Boolean}},emits:[`select`],setup(e){let t=e,n=J(()=>t.item.type===`group`?`group`:t.item.type===`direct_wechat`?`wechat`:`wecom`);return(t,r)=>(P(),$(`button`,{type:`button`,class:ce([`row`,{active:e.active}]),onClick:r[0]||(r[0]=n=>t.$emit(`select`,e.item))},[q(`span`,{class:ce([`avatar`,`avatar--${n.value}`]),"aria-hidden":`true`},U(X(u)(e.item.name)),3),q(`span`,ln,[q(`span`,un,[q(`strong`,dn,U(e.item.name),1),q(`span`,fn,U(e.item.last_message_at?X(c)(e.item.last_message_at):``),1)]),q(`span`,pn,[q(`span`,mn,[Q(U(e.item.type_label),1),e.item.account_name?(P(),$(De,{key:0},[Q(` · `+U(e.item.account_name),1)],64)):Z(``,!0)]),e.item.blocked?(P(),$(`span`,hn,`已拉黑`)):Z(``,!0)])])],2))}}),[[`__scopeId`,`data-v-645c253b`]]),_n={class:`page`},vn={class:`list-pane`},yn={class:`toolbar`},bn={key:0,class:`detail-pane app-card`},xn=o(O({name:`ConversationsView`,__name:`ConversationsView`,setup(e){let t=Ie(),n=L(``),i=L(`all`),a=L([]),o=L(``),s=L(!1),c=L(!1),l=L(!1),u=L(``),f=L(!1),h=L(0),g=L(null),_=L(!1),v=L(!1),y=0,b=null,x,te=J(()=>{var e;return(e=Ne.me)==null?void 0:e.permission_codes}),ne=J(()=>Pe.value===void 0?`会话`:`${Le.value} 的会话`),T=e=>`${e.account}|${e.conversation}`;function E(){y+=1,b==null||b.abort(),b=null,a.value=[],o.value=``,s.value=!1,l.value=!1,u.value=``,c.value=!1,D()}async function D(){if(s.value)return;let e=y;b=new AbortController,c.value=!0;try{let r=await Me.conversations({accountId:Pe.value,q:n.value.trim(),scope:i.value,cursor:o.value,limit:30,signal:b.signal});if(e!==y)return;let c=new Set(a.value.map(T));a.value=a.value.concat(r.items.filter(e=>!c.has(T(e)))),o.value=r.next_cursor,s.value=!r.has_more,h.value=r.blocked_total,l.value=!1,t.value&&!g.value&&a.value.length&&(g.value=a.value[0])}catch(t){if(e!==y||t instanceof DOMException&&t.name===`AbortError`)return;l.value=!0,u.value=t instanceof Te?t.message:`加载失败`}finally{e===y&&(c.value=!1,f.value=!1)}}function O(){f.value=!0,E()}function k(e){g.value=e,t.value||(_.value=!0)}function A(e){var t;let n=a.value.findIndex(t=>T(t)===T(e)),r=n>=0?a.value[n].blocked:(t=g.value)==null?void 0:t.blocked;n>=0&&(i.value===`blocked`&&!e.blocked?a.value.splice(n,1):a.value.splice(n,1,e)),r!==e.blocked&&(h.value+=e.blocked?1:-1),g.value=e}return B(n,()=>{window.clearTimeout(x),x=window.setTimeout(E,350)}),B(i,E),B(Pe,()=>{g.value=null,_.value=!1,E()}),E(),(e,o)=>{var y,b,x,E,j,M,N,re;return P(),$(`div`,_n,[R(X(d),{title:ne.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},we({_:2},[(y=X(Re))!=null&&y.control_all?{name:`right`,fn:z(()=>[q(`button`,{type:`button`,class:`nav-action`,onClick:o[0]||(o[0]=e=>v.value=!0)},[R(X(r),{name:`friends-o`}),o[9]||(o[9]=Q(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),q(`div`,{class:ce(X(t)?`split`:`single`)},[q(`section`,vn,[q(`div`,yn,[R(X(ee),{modelValue:n.value,"onUpdate:modelValue":o[1]||(o[1]=e=>n.value=e),shape:`round`,placeholder:`搜索会话名称`,clearable:!0},null,8,[`modelValue`]),R(X(Ut),{active:i.value,"onUpdate:active":o[2]||(o[2]=e=>i.value=e),shrink:``,border:!1,class:`tabs`},{default:z(()=>[R(X(Ht),{name:`all`,title:`全部会话`}),R(X(Ht),{name:`blocked`,title:h.value?`黑名单 ${h.value}`:`黑名单`},null,8,[`title`])]),_:1},8,[`active`])]),R(X(C),{modelValue:f.value,"onUpdate:modelValue":o[6]||(o[6]=e=>f.value=e),class:`list-body`,onRefresh:O},{default:z(()=>[R(X(Yt),{loading:c.value,"onUpdate:loading":o[3]||(o[3]=e=>c.value=e),error:l.value,"onUpdate:error":o[4]||(o[4]=e=>l.value=e),finished:s.value,"error-text":`${u.value},点击重试`,"finished-text":a.value.length?`没有更多了`:``,"immediate-check":!1,onLoad:D},{default:z(()=>[(P(!0),$(De,null,le(a.value,e=>(P(),K(gn,{key:T(e),item:e,active:X(t)&&g.value!==null&&T(g.value)===T(e),onSelect:k},null,8,[`item`,`active`]))),128))]),_:1},8,[`loading`,`error`,`finished`,`error-text`,`finished-text`]),s.value&&!a.value.length&&!l.value?(P(),K(X(S),{key:0,image:n.value?`search`:`default`,description:n.value?`没有找到匹配的会话`:i.value===`blocked`?`黑名单是空的`:`还没有归档的会话:电脑开启聊天备份后会自动出现`},{default:z(()=>[n.value?(P(),K(X(m),{key:0,round:``,size:`small`,onClick:o[5]||(o[5]=e=>n.value=``)},{default:z(()=>[...o[10]||(o[10]=[Q(`清除搜索`,-1)])]),_:1})):Z(``,!0)]),_:1},8,[`image`,`description`])):Z(``,!0)]),_:1},8,[`modelValue`])]),X(t)?(P(),$(`section`,bn,[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((b=X(Re))!=null&&b.blacklist_add),"can-remove":!!((x=X(Re))!=null&&x.blacklist_remove),"add-code":(E=(j=te.value)==null?void 0:j.blacklist_add)==null?``:E,"remove-code":(M=(N=te.value)==null?void 0:N.blacklist_remove)==null?``:M,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):(P(),K(X(S),{key:1,image:`default`,description:`选择左侧的会话查看详情`}))])):Z(``,!0)],2),X(t)?Z(``,!0):(P(),K(X(p),{key:0,show:_.value,"onUpdate:show":o[7]||(o[7]=e=>_.value=e),position:`bottom`,round:``,closeable:``,"safe-area-inset-bottom":``,class:`sheet`},{default:z(()=>{var e,t,n,r,i,a;return[g.value?(P(),K(cn,{key:0,conversation:g.value,"account-id":X(Pe),"can-add":!!((e=X(Re))!=null&&e.blacklist_add),"can-remove":!!((t=X(Re))!=null&&t.blacklist_remove),"add-code":(n=(r=te.value)==null?void 0:r.blacklist_add)==null?``:n,"remove-code":(i=(a=te.value)==null?void 0:a.blacklist_remove)==null?``:i,onUpdated:A},null,8,[`conversation`,`account-id`,`can-add`,`can-remove`,`add-code`,`remove-code`])):Z(``,!0)]}),_:1},8,[`show`])),(re=X(Re))!=null&&re.control_all?(P(),K(w,{key:1,show:v.value,"onUpdate:show":o[8]||(o[8]=e=>v.value=e)},null,8,[`show`])):Z(``,!0)])}}}),[[`__scopeId`,`data-v-cc7f80e6`]]);export{xn as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css new file mode 100644 index 000000000..464684d56 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ConversationsView-CytDMSuM.css @@ -0,0 +1 @@ +.detail[data-v-70465908]{padding-bottom:16px}.detail-head[data-v-70465908]{align-items:center;gap:12px;padding:16px;display:flex}.avatar[data-v-70465908]{width:52px;height:52px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:14px;flex:none;place-items:center;font-size:22px;font-weight:600;display:grid}.detail-title[data-v-70465908]{flex:1;min-width:0}.detail-title h2[data-v-70465908]{text-overflow:ellipsis;white-space:nowrap;margin:0 0 4px;font-size:18px;overflow:hidden}.detail-title p[data-v-70465908]{margin:0;font-size:13px}.badge[data-v-70465908]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.group[data-v-70465908]{margin-bottom:12px}.state[data-v-70465908]{background:var(--app-surface);border:1px solid var(--app-border);border-radius:14px;margin:0 16px;padding:16px}.state--blocked[data-v-70465908]{border-color:var(--app-danger-soft);background:linear-gradient(0deg, var(--app-danger-soft), var(--app-danger-soft)), var(--app-surface)}.state h3[data-v-70465908]{align-items:center;gap:6px;margin:0 0 6px;font-size:15px;display:flex}.state--blocked h3[data-v-70465908]{color:var(--app-danger)}.state>p[data-v-70465908]{color:var(--app-text-2);margin:0 0 12px;font-size:13px;line-height:1.6}.state dl[data-v-70465908]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin:0 0 14px;display:grid}.state dt[data-v-70465908]{color:var(--app-muted);font-size:12px}.state dd[data-v-70465908]{word-break:break-all;margin:2px 0 0;font-size:14px}.reason[data-v-70465908]{margin:0 0 12px}.reason[data-v-70465908] .van-cell-group--inset{margin:0}.state[data-v-70465908] .van-cell-group--inset{margin:0 0 12px}.note[data-v-70465908]{color:var(--app-muted);margin:0;font-size:13px;line-height:1.6}.row[data-v-645c253b]{border:0;border-bottom:1px solid var(--app-border);background:var(--app-surface);width:100%;min-height:64px;color:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;padding:10px 16px;display:flex}.row[data-v-645c253b]:active,.row.active[data-v-645c253b]{background:var(--app-primary-soft)}.avatar[data-v-645c253b]{border-radius:12px;flex:none;place-items:center;width:42px;height:42px;font-size:17px;font-weight:600;display:grid}.avatar--wecom[data-v-645c253b]{color:#2f6fff;background:#2f6fff1f}.avatar--wechat[data-v-645c253b]{color:#0f9e69;background:#13b77a24}.avatar--group[data-v-645c253b]{color:#7a5cff;background:#8a6bff24}.body[data-v-645c253b]{flex:1;gap:4px;min-width:0;display:grid}.line[data-v-645c253b]{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.name[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.time[data-v-645c253b]{color:var(--app-muted);flex:none;font-size:12px}.sub[data-v-645c253b]{color:var(--app-muted);font-size:12px}.meta[data-v-645c253b]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.blocked[data-v-645c253b]{color:var(--app-danger);background:var(--app-danger-soft);border-radius:999px;flex:none;padding:1px 8px;font-weight:600}.page[data-v-cc7f80e6]{min-height:100%}.nav-action[data-v-cc7f80e6]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.toolbar[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top));z-index:2;background:var(--app-bg);position:-webkit-sticky;position:sticky}.toolbar[data-v-cc7f80e6] .van-search{background:0 0;padding-bottom:4px}.tabs[data-v-cc7f80e6]{--van-tabs-nav-background:transparent;padding:0 4px}.list-body[data-v-cc7f80e6]{min-height:60vh}.single .list-pane[data-v-cc7f80e6]{max-width:720px;margin:0 auto}.split[data-v-cc7f80e6]{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;max-width:1180px;margin:0 auto;padding:0 24px 24px;display:grid}.split .list-body[data-v-cc7f80e6]{border-radius:16px;overflow:clip}.detail-pane[data-v-cc7f80e6]{top:calc(46px + env(safe-area-inset-top) + 12px);min-height:320px;margin-top:12px;position:-webkit-sticky;position:sticky}.sheet[data-v-cc7f80e6]{max-height:88vh;padding-top:8px} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css new file mode 100644 index 000000000..617ba43f7 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-CHTvxQxK.css @@ -0,0 +1 @@ +.panel[data-v-bcd7f731]{padding:16px}.panel-head[data-v-bcd7f731]{align-items:flex-start;gap:12px;display:flex}.panel-icon[data-v-bcd7f731]{width:44px;height:44px;color:var(--app-primary);background:var(--app-primary-soft);border-radius:12px;flex:none;place-items:center;font-size:22px;display:grid}.panel-title[data-v-bcd7f731]{flex:1;min-width:0}.panel-title h2[data-v-bcd7f731]{text-overflow:ellipsis;white-space:nowrap;margin:2px 0 4px;font-size:17px;font-weight:600;overflow:hidden}.panel-title p[data-v-bcd7f731]{margin:0;font-size:13px;line-height:1.45}.pill[data-v-bcd7f731]{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:600;display:inline-flex}.pill .dot[data-v-bcd7f731]{background:currentColor;border-radius:50%;width:7px;height:7px}.pill--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.pill--success .dot[data-v-bcd7f731]{box-shadow:0 0 0 3px var(--app-success-soft);animation:2.2s ease-in-out infinite breathe-bcd7f731}.pill--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.pill--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.pill--muted[data-v-bcd7f731],.pill--primary[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}@keyframes breathe-bcd7f731{50%{opacity:.45}}.stats[data-v-bcd7f731]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0 0;display:grid}.stat[data-v-bcd7f731]{background:var(--app-surface-2);border:1px solid var(--app-border);border-radius:12px;padding:10px 12px}.stat dt[data-v-bcd7f731]{color:var(--app-muted);font-size:12px}.stat dd[data-v-bcd7f731]{margin:4px 0 0;font-size:15px;font-weight:600}.meta[data-v-bcd7f731]{margin:12px 0 0;font-size:12px}.progress[data-v-bcd7f731]{border-radius:12px;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45;display:flex}.progress--primary[data-v-bcd7f731]{color:var(--app-primary);background:var(--app-primary-soft)}.progress--success[data-v-bcd7f731]{color:var(--app-success);background:var(--app-success-soft)}.progress--danger[data-v-bcd7f731]{color:var(--app-danger);background:var(--app-danger-soft)}.progress--warning[data-v-bcd7f731]{color:var(--app-warning);background:var(--app-warning-soft)}.progress--muted[data-v-bcd7f731]{color:var(--app-muted);background:var(--app-muted-soft)}.actions[data-v-bcd7f731]{margin-top:16px}.blocked[data-v-bcd7f731]{margin:10px 4px 0;font-size:13px;line-height:1.5}.page[data-v-8ead1f63]{min-height:100%}.page-body[data-v-8ead1f63]{min-height:calc(100vh - 96px);min-height:calc(100dvh - 96px)}.content[data-v-8ead1f63]{max-width:720px;margin:0 auto;padding:12px 16px 24px}.stack[data-v-8ead1f63]{gap:12px;display:grid}.skeleton-card[data-v-8ead1f63]{padding:16px 0}.empty-hint[data-v-8ead1f63]{max-width:280px;color:var(--app-muted);text-align:center;margin:0 auto;font-size:13px;line-height:1.6}.nav-action[data-v-8ead1f63]{color:var(--app-primary);background:0 0;border:0;align-items:center;gap:4px;padding:6px 0;font-size:14px;display:inline-flex}.stale[data-v-8ead1f63]{margin:0 4px;font-size:12px}.split[data-v-8ead1f63]{grid-template-columns:minmax(240px,320px) minmax(0,1fr);align-items:start;gap:16px;max-width:1080px;margin:0 auto;padding:16px 24px 24px;display:grid}.split-list[data-v-8ead1f63]{gap:10px;display:grid}.device-row[data-v-8ead1f63]{text-align:left;width:100%;color:inherit;cursor:pointer;align-items:center;gap:12px;padding:12px 14px;display:flex}.device-row.active[data-v-8ead1f63]{border-color:var(--app-primary);box-shadow:0 0 0 2px var(--app-primary-soft)}.device-row-icon[data-v-8ead1f63]{color:var(--app-primary);font-size:20px}.device-row-text[data-v-8ead1f63]{gap:2px;min-width:0;display:grid}.device-row-text strong[data-v-8ead1f63]{text-overflow:ellipsis;white-space:nowrap;font-size:15px;overflow:hidden}.device-row-text small[data-v-8ead1f63]{font-size:12px} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js new file mode 100644 index 000000000..5da14a77e --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/DevicesView-Cgb9V9Z0.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n,o as r,r as i,s as a,t as o}from"./format-DEXJ_MrY.js";import{a as s,o as c}from"./cell-group-B1PakqJq.js";import{a as l,i as u,r as d,t as f}from"./AccountSwitcher-5FuHazzf.js";import{Ct as p,Et as m,Ft as h,Jt as g,Kt as _,Lt as v,St as y,Ut as b,Vt as x,X as S,Y as C,Yt as w,Z as T,_ as E,_t as D,bt as O,c as k,f as A,ft as j,g as M,gt as N,ht as P,kt as F,l as I,n as L,o as R,p as z,q as B,qt as V,s as ee,t as H,v as U,vt as W,x as te,xt as G,yt as K}from"./index-Gkjcdjlo.js";var[ne,re]=E(`skeleton-title`),ie=p({name:ne,props:{round:Boolean,titleWidth:S},setup(e){return()=>y(`h3`,{class:re([{round:e.round}]),style:{width:U(e.titleWidth)}},null)}}),q=M(ie),[J,Y]=E(`skeleton-avatar`),ae={avatarSize:S,avatarShape:C(`round`)},oe=p({name:J,props:ae,setup(e){return()=>y(`div`,{class:Y([e.avatarShape]),style:te(e.avatarSize)},null)}}),se=M(oe),X=`100%`,ce={round:Boolean,rowWidth:{type:S,default:X}},[le,ue]=E(`skeleton-paragraph`),de=p({name:le,props:ce,setup(e){return()=>y(`div`,{class:ue([{round:e.round}]),style:{width:e.rowWidth}},null)}}),fe=M(de),[pe,Z]=E(`skeleton`),me=`60%`,he={row:B(0),round:Boolean,title:Boolean,titleWidth:S,avatar:Boolean,avatarSize:S,avatarShape:C(`round`),loading:T,animate:T,rowWidth:{type:[Number,String,Array],default:X}},ge=p({name:pe,inheritAttrs:!1,props:he,setup(e,{slots:t,attrs:n}){let r=()=>{if(e.avatar)return y(se,{avatarShape:e.avatarShape,avatarSize:e.avatarSize},null)},i=()=>{if(e.title)return y(q,{round:e.round,titleWidth:e.titleWidth},null)},a=t=>{let{rowWidth:n}=e;return n===`100%`&&t===e.row-1?me:Array.isArray(n)?n[t]:n},o=()=>Array(+e.row).fill(``).map((t,n)=>y(fe,{key:n,round:e.round,rowWidth:U(a(n))},null)),s=()=>t.template?t.template():y(j,null,[r(),y(`div`,{class:Z(`content`)},[i(),o()])]);return()=>{var r;return e.loading?y(`div`,m({class:Z({animate:e.animate,round:e.round})},n),[s()]):(r=t.default)==null?void 0:r.call(t)}}}),_e=M(ge),ve={class:`panel-head`},ye={class:`panel-icon`,"aria-hidden":`true`},be={class:`panel-title`},xe={class:`app-muted`},Se={class:`stats`},Ce={class:`stat`},we={class:`stat`},Te={class:`stat`},Ee={class:`stat`},De={class:`meta app-muted`},Oe={key:0},ke={key:1},Ae={class:`actions`},je={key:0,class:`blocked app-muted`},Q=t(p({__name:`DevicePanel`,props:{device:{},accountId:{},canControl:{type:Boolean},controlCode:{}},emits:[`changed`],setup(t,{emit:r}){let u=t,d=r,f=_(!1),p=_(null),m,v=0,S=P(()=>n(u.device)),C=P(()=>u.device.status),T=P(()=>{var e;return!!((e=C.value)!=null&&e.listening)}),E=P(()=>T.value?`stop_listening`:`start_listening`),O=P(()=>p.value?o(p.value):null),A=P(()=>{var e;return f.value||!!((e=O.value)!=null&&e.busy)}),j=P(()=>u.device.online?u.device.remote_capable?u.canControl?``:`你的 ZYT 账号没有远程启停权限(${u.controlCode}),请联系管理员开通。`:`电脑上的客服软件版本过低,升级到最新版后才能远程控制。`:`电脑离线:客服软件没有运行,无法远程拉起。请到电脑上打开软件。`);function M(){m!==void 0&&window.clearTimeout(m),m=void 0}async function L(){let e=p.value;if(e&&!e.final){try{let{command:t}=await I.command(e.id);if(p.value=t,t.final){d(`changed`);let e=o(t).text;l({message:e,type:t.status===`succeeded`?`success`:`fail`});return}}catch(e){if(e instanceof k&&e.status===404)return}Date.now()u.device.last_command,e=>{if(!e)return;if(p.value&&p.value.id===e.id){!p.value.final&&e.final&&(p.value=e);return}let t=e.expires_at*1e3>Date.now();!e.final&&t?R(e):!p.value&&e.final&&t&&(p.value=e)},{immediate:!0}),F(M);async function z(){if(A.value||j.value)return;let e=E.value===`stop_listening`;try{await a({title:e?`停止自动回复?`:`启动自动回复?`,message:e?`「${u.device.device_name||`这台电脑`}」将停止监听,新消息不再自动回复。`:`「${u.device.device_name||`这台电脑`}」将开始监听,并按电脑上的设置自动回复新消息。`,confirmButtonText:e?`停止`:`启动`,confirmButtonColor:e?`var(--app-danger)`:`var(--app-primary)`})}catch{return}f.value=!0;try{let{command:e}=await I.sendCommand(u.device.device_id,E.value,u.accountId);R(e)}catch(e){l({message:e instanceof k?e.message:`指令发送失败,请重试`,type:`fail`}),d(`changed`)}finally{f.value=!1}}return(n,r)=>{var a,o,l,u,d;return h(),K(`article`,{class:g([`panel app-card`,`panel--${S.value.tone}`])},[N(`header`,ve,[N(`div`,ye,[y(V(e),{name:`desktop-o`})]),N(`div`,be,[N(`h2`,null,w(t.device.device_name||`未命名电脑`),1),N(`p`,xe,w(S.value.hint),1)]),N(`span`,{class:g([`pill`,`pill--${S.value.tone}`])},[r[0]||(r[0]=N(`span`,{class:`dot`,"aria-hidden":`true`},null,-1)),G(` `+w(S.value.label),1)],2)]),N(`dl`,Se,[N(`div`,Ce,[r[1]||(r[1]=N(`dt`,null,`自动回复`,-1)),N(`dd`,{class:g(T.value?`tone-success`:`tone-muted`)},w(C.value?T.value?`监听中`:`已停止`:`—`),3)]),N(`div`,we,[r[2]||(r[2]=N(`dt`,null,`企业微信`,-1)),N(`dd`,{class:g((a=C.value)!=null&&a.wecom_connected?`tone-success`:`tone-muted`)},w(C.value?C.value.wecom_connected?`已连接`:`未连接`:`—`),3)]),N(`div`,Te,[r[3]||(r[3]=N(`dt`,null,`回复通道`,-1)),N(`dd`,null,w(((o=C.value)==null?void 0:o.transport_label)||`—`),1)]),N(`div`,Ee,[r[4]||(r[4]=N(`dt`,null,`待处理`,-1)),N(`dd`,{class:g((((l=C.value)==null?void 0:l.pending_count)||0)>0?`tone-warning`:``)},w(C.value?`${C.value.pending_count} 条`:`—`),3)])]),N(`p`,De,[N(`span`,null,`版本 `+w(t.device.app_version||`未知`),1),t.device.online&&((u=C.value)==null?void 0:u.reported_seconds)!==null&&((d=C.value)==null?void 0:d.reported_seconds)!==void 0?(h(),K(`span`,Oe,` · 状态 `+w(V(i)(C.value.reported_seconds))+`前上报 `,1)):!t.device.online&&t.device.last_seen_seconds!==null?(h(),K(`span`,ke,` · `+w(V(i)(t.device.last_seen_seconds))+`前在线 `,1)):W(``,!0)]),O.value?(h(),K(`div`,{key:0,class:g([`progress`,`progress--${O.value.tone}`]),role:`status`,"aria-live":`polite`},[O.value.busy?(h(),D(V(c),{key:0,size:`16`})):(h(),D(V(e),{key:1,name:O.value.tone===`success`?`passed`:`info-o`},null,8,[`name`])),N(`span`,null,w(O.value.text),1)],2)):W(``,!0),N(`div`,Ae,[y(V(s),{block:``,round:``,type:T.value?`danger`:`primary`,plain:T.value,icon:T.value?`pause-circle-o`:`play-circle-o`,disabled:!!j.value,loading:A.value,"loading-text":f.value?`正在下发…`:`等待电脑执行…`,onClick:z},{default:b(()=>[G(w(T.value?`停止自动回复`:`启动自动回复`),1)]),_:1},8,[`type`,`plain`,`icon`,`disabled`,`loading`,`loading-text`]),j.value?(h(),K(`p`,je,w(j.value),1)):W(``,!0)])],2)}}}),[[`__scopeId`,`data-v-bcd7f731`]]),Me={class:`page`},Ne={key:0,class:`content`},Pe={class:`app-card skeleton-card`},Fe={key:1,class:`content`},Ie={key:2,class:`content`},Le={class:`empty-hint`},$={key:3,class:`split`},Re={class:`split-list`,"aria-label":`电脑列表`},ze=[`onClick`],Be={class:`device-row-text`},Ve={key:0,class:`stale app-muted`},He={class:`split-detail`},Ue={key:4,class:`content stack`},We={key:0,class:`stale app-muted`},Ge=t(p({name:`DevicesView`,__name:`DevicesView`,setup(t){let i=z(),a=_(null),o=_(!1),c=_(!1),l=_(``),p=_(``),m=_(!1),S=0,C=P(()=>{var e,t;return(e=(t=a.value)==null?void 0:t.items)==null?[]:e}),T=P(()=>{var e,t;return(e=(t=C.value.find(e=>e.device_id===p.value))==null?C.value[0]:t)==null?null:e}),E=P(()=>{var e;return!!((e=H.value)!=null&&e.control)}),M=P(()=>{var e,t;return(e=(t=L.me)==null?void 0:t.permission_codes.control)==null?`wecom_rpa.remote/control`:e}),F=P(()=>R.value===void 0?`我的电脑`:`${ee.value} 的电脑`);async function B(){let e=++S;a.value||(o.value=!0);try{let t=await I.desktops(R.value);if(e!==S)return;a.value=t,l.value=``}catch(t){if(e!==S)return;l.value=t instanceof k?t.message:`加载失败,请重试`}finally{e===S&&(o.value=!1,c.value=!1)}}async function U(){c.value=!0,await B()}return x(R,()=>{a.value=null,p.value=``,B()},{immediate:!0}),A(B,5e3),(t,_)=>{var x,S;return h(),K(`div`,Me,[y(V(r),{title:F.value,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1},O({_:2},[(x=V(H))!=null&&x.control_all?{name:`right`,fn:b(()=>[N(`button`,{type:`button`,class:`nav-action`,onClick:_[0]||(_[0]=e=>m.value=!0)},[y(V(e),{name:`friends-o`}),_[3]||(_[3]=G(` 切换客服 `,-1))])]),key:`0`}:void 0]),1032,[`title`]),y(V(d),{modelValue:c.value,"onUpdate:modelValue":_[1]||(_[1]=e=>c.value=e),class:`page-body`,onRefresh:U},{default:b(()=>[o.value&&!a.value?(h(),K(`div`,Ne,[N(`div`,Pe,[y(V(_e),{title:``,row:4})])])):l.value&&!a.value?(h(),K(`div`,Fe,[y(V(u),{image:`network`,description:l.value},{default:b(()=>[y(V(s),{round:``,type:`primary`,size:`small`,onClick:B},{default:b(()=>[..._[4]||(_[4]=[G(`重新加载`,-1)])]),_:1})]),_:1},8,[`description`])])):C.value.length?V(i)?(h(),K(`div`,$,[N(`aside`,Re,[(h(!0),K(j,null,v(C.value,t=>{var r;return h(),K(`button`,{key:t.device_id,type:`button`,class:g([`device-row app-card`,{active:((r=T.value)==null?void 0:r.device_id)===t.device_id}]),onClick:e=>p.value=t.device_id},[y(V(e),{name:`desktop-o`,class:`device-row-icon`}),N(`span`,Be,[N(`strong`,null,w(t.device_name||`未命名电脑`),1),N(`small`,{class:g(`tone-${V(n)(t).tone}`)},w(V(n)(t).label),3)])],10,ze)}),128)),l.value?(h(),K(`p`,Ve,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0)]),N(`section`,He,[T.value?(h(),D(Q,{key:T.value.device_id,device:T.value,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`])):W(``,!0)])])):(h(),K(`div`,Ue,[l.value?(h(),K(`p`,We,w(l.value)+`(显示的是上次的状态)`,1)):W(``,!0),(h(!0),K(j,null,v(C.value,e=>(h(),D(Q,{key:e.device_id,device:e,"account-id":V(R),"can-control":E.value,"control-code":M.value,onChanged:B},null,8,[`device`,`account-id`,`can-control`,`control-code`]))),128))])):(h(),K(`div`,Ie,[y(V(u),{image:`default`,description:`还没有登录过企微客服助手的电脑`},{default:b(()=>[N(`p`,Le,` 在电脑上打开企微客服助手,用`+w(V(R)===void 0?`同一个`:`该客服的`)+` ZYT 账号登录后,这里会显示电脑状态。 `,1)]),_:1})]))]),_:1},8,[`modelValue`]),(S=V(H))!=null&&S.control_all?(h(),D(f,{key:0,show:m.value,"onUpdate:show":_[2]||(_[2]=e=>m.value=e)},null,8,[`show`])):W(``,!0)])}}}),[[`__scopeId`,`data-v-8ead1f63`]]);export{Ge as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css new file mode 100644 index 000000000..8942d6062 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-C25eERA6.css @@ -0,0 +1 @@ +.login[data-v-858fb177]{min-height:100vh;min-height:100dvh;padding:calc(env(safe-area-inset-top) + 24px) 16px calc(env(safe-area-inset-bottom) + 24px);background:radial-gradient(120% 60% at 50% 0%, var(--app-primary-soft), transparent 70%), var(--app-bg);justify-content:center;align-items:center;display:flex}.login-card[data-v-858fb177]{width:100%;max-width:420px}.login-head[data-v-858fb177]{text-align:center;margin-bottom:24px}.login-mark[data-v-858fb177]{box-shadow:var(--app-shadow);border-radius:20px}.login-head h1[data-v-858fb177]{letter-spacing:.5px;margin:16px 0 6px;font-size:24px;font-weight:700}.login-head p[data-v-858fb177]{color:var(--app-muted);margin:0;font-size:14px}.login-notice[data-v-858fb177]{border-radius:12px;margin:0 16px 12px}.login-notice.warn[data-v-858fb177]{--van-notice-bar-text-color:var(--app-warning);--van-notice-bar-background:var(--app-warning-soft)}.login-form[data-v-858fb177] .van-cell-group--inset,.login-form[data-v-858fb177]{margin:0}.login-error[data-v-858fb177]{color:var(--app-danger);margin:12px 16px 0;font-size:14px;line-height:1.5}.login-actions[data-v-858fb177]{margin:20px 16px 0}.login-foot[data-v-858fb177]{text-align:center;color:var(--app-muted);margin:16px 16px 0;font-size:12px} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js new file mode 100644 index 000000000..c65cd3fb4 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/LoginView-nW7aCAJ5.js @@ -0,0 +1 @@ +import{c as e,m as t,n,t as r}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{a as i,s as a,t as o}from"./cell-group-B1PakqJq.js";import{t as s}from"./field-CRX3eS7h.js";import{B as c,Ct as l,Ft as u,Gt as d,I as f,Kt as p,O as m,R as h,St as g,Ut as _,V as v,Vt as y,W as b,Wt as x,X as S,Yt as C,Z as w,_ as T,_t as E,c as D,d as O,et as k,g as A,gt as j,ht as M,l as N,q as P,qt as F,r as I,u as L,ut as R,vt as z,xt as B,yt as V,z as H}from"./index-Gkjcdjlo.js";var[U,W]=T(`form`),G=l({name:U,props:{colon:Boolean,disabled:Boolean,readonly:Boolean,required:[Boolean,String],showError:Boolean,labelWidth:S,labelAlign:String,inputAlign:String,scrollToError:Boolean,scrollToErrorPosition:String,validateFirst:Boolean,submitOnEnter:w,showErrorMessage:w,errorMessageAlign:String,validateTrigger:{type:[String,Array],default:`onBlur`}},emits:[`submit`,`failed`],setup(e,{emit:n,slots:r}){let{children:i,linkChildren:o}=c(t),s=e=>e?i.filter(t=>e.includes(t.name)):i,l=e=>new Promise((t,n)=>{let r=[];s(e).reduce((e,t)=>e.then(()=>{if(!r.length)return t.validate().then(e=>{e&&r.push(e)})}),Promise.resolve()).then(()=>{r.length?n(r):t()})}),u=e=>new Promise((t,n)=>{let r=s(e);Promise.all(r.map(e=>e.validate())).then(e=>{e=e.filter(Boolean),e.length?n(e):t()})}),d=e=>{let t=i.find(t=>t.name===e);return t?new Promise((e,n)=>{t.validate().then(t=>{t?n(t):e()})}):Promise.reject()},f=t=>typeof t==`string`?d(t):e.validateFirst?l(t):u(t),p=e=>{typeof e==`string`&&(e=[e]),s(e).forEach(e=>{e.resetValidation()})},h=()=>i.reduce((e,t)=>(e[t.name]=t.getValidationStatus(),e),{}),_=(e,t)=>{i.some(n=>n.name===e&&(n.$el.scrollIntoView(t),!0))},v=()=>i.reduce((e,t)=>(t.name!==void 0&&(e[t.name]=t.formValue.value),e),{}),y=()=>{let t=v();f().then(()=>n(`submit`,t)).catch(r=>{n(`failed`,{values:t,errors:r});let{scrollToError:i,scrollToErrorPosition:a}=e;i&&r[0].name&&_(r[0].name,a?{block:a}:void 0)})},b=e=>{m(e),y()};return o({props:e}),a({submit:y,validate:f,getValues:v,scrollToField:_,resetValidation:p,getValidationStatus:h}),()=>{var e;return g(`form`,{class:W(),onSubmit:b},[(e=r.default)==null?void 0:e.call(r)])}}}),K=A(G),[q,J]=T(`notice-bar`),Y={text:String,mode:String,color:String,delay:P(1),speed:P(60),leftIcon:String,wrapable:Boolean,background:String,scrollable:{type:Boolean,default:null}},X=l({name:q,props:Y,emits:[`close`,`replay`],setup(t,{emit:r,slots:i}){let o=0,s=0,c,l=p(),u=p(),m=d({show:!0,offset:0,duration:0}),_=()=>{if(i[`left-icon`])return i[`left-icon`]();if(t.leftIcon)return g(n,{class:J(`left-icon`),name:t.leftIcon},null)},S=()=>{if(t.mode===`closeable`)return`cross`;if(t.mode===`link`)return`arrow`},C=e=>{t.mode===`closeable`&&(m.show=!1,r(`close`,e))},w=()=>{if(i[`right-icon`])return i[`right-icon`]();let e=S();if(e)return g(n,{name:e,class:J(`right-icon`),onClick:C},null)},T=()=>{m.offset=o,m.duration=0,H(()=>{f(()=>{m.offset=-s,m.duration=(s+o)/+t.speed,r(`replay`)})})},E=()=>{let e=t.scrollable===!1&&!t.wrapable,n={transform:m.offset?`translateX(${m.offset}px)`:``,transitionDuration:`${m.duration}s`};return g(`div`,{ref:l,role:`marquee`,class:J(`wrap`)},[g(`div`,{ref:u,style:n,class:[J(`content`),{"van-ellipsis":e}],onTransitionend:T},[i.default?i.default():t.text])])},D=()=>{let{delay:e,speed:n,scrollable:r}=t,i=k(e)?e*1e3:0;o=0,s=0,m.offset=0,m.duration=0,clearTimeout(c),c=setTimeout(()=>{if(!l.value||!u.value||r===!1)return;let e=b(l).width,t=b(u).width;(r||t>e)&&f(()=>{o=e,s=t,m.offset=-s,m.duration=s/+n})},i)};return e(D),h(D),v(`pageshow`,D),a({reset:D}),y(()=>[t.text,t.scrollable],D),()=>{let{color:e,wrapable:n,background:r}=t;return x(g(`div`,{role:`alert`,class:J({wrapable:n}),style:{color:e,background:r}},[_(),E(),w()]),[[R,m.show]])}}}),Z=A(X),Q=`/m/assets/brand-mark-CPUQS9Am.png`,$={class:`login`},ee={class:`login-card`},te={class:`login-head`},ne=[`src`],re={key:0,class:`login-error`,role:`alert`},ie={class:`login-actions`},ae=r(l({__name:`LoginView`,setup(e){let t=L(),n=O(),r=p(``),a=p(``),c=p(!1),l=p(``),d=M(()=>t.query.expired===`1`),f=M(()=>window.location.protocol===`http:`&&![`localhost`,`127.0.0.1`,`[::1]`].includes(window.location.hostname));function m(){let e=navigator.userAgent,t=/iPad/.test(e)?`iPad`:/iPhone/.test(e)?`iPhone`:/HarmonyOS/i.test(e)?`鸿蒙`:/Android/.test(e)?`Android`:/Windows/.test(e)?`Windows`:/Mac OS/.test(e)?`Mac`:`浏览器`,n=/wxwork/i.test(e)?`企业微信`:/MicroMessenger/i.test(e)?`微信`:``;return n?`${t} · ${n}`:t}function h(){let e=t.query.redirect;return typeof e==`string`&&e.startsWith(`/`)&&!e.startsWith(`//`)&&!e.startsWith(`/login`)?e:`/desktops`}async function v(){if(!c.value){l.value=``,c.value=!0;try{let e=await N.login(r.value.trim(),a.value,m());a.value=``,I(e.access_token,e.me),await n.replace(h())}catch(e){e instanceof D?(l.value=e.message,e.code===`invalid_credentials`&&(a.value=``)):l.value=`登录失败,请稍后重试`}finally{c.value=!1}}}return(e,t)=>(u(),V(`main`,$,[j(`section`,ee,[j(`header`,te,[j(`img`,{class:`login-mark`,src:F(Q),alt:`甄养堂`,width:`72`,height:`72`},null,8,ne),t[2]||(t[2]=j(`h1`,null,`企微客服助手`,-1)),t[3]||(t[3]=j(`p`,null,`手机控制台 · 使用 ZYT 账号登录`,-1))]),d.value?(u(),E(F(Z),{key:0,class:`login-notice`,"left-icon":`info-o`,scrollable:!1,wrapable:``,text:`登录已失效或已在管理端被下线,请重新登录。`})):z(``,!0),f.value?(u(),E(F(Z),{key:1,class:`login-notice warn`,"left-icon":`warning-o`,scrollable:!1,wrapable:``,text:`当前是明文 HTTP 连接,只能在公司内网登录;在外网请改用 HTTPS 地址。`})):z(``,!0),g(F(K),{class:`login-form`,onSubmit:v},{default:_(()=>[g(F(o),{inset:``},{default:_(()=>[g(F(s),{modelValue:r.value,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),name:`account`,label:`账号`,placeholder:`ZYT 登录账号`,autocomplete:`username`,autocapitalize:`off`,spellcheck:!1,enterkeyhint:`next`,clearable:``,rules:[{required:!0,message:`请输入账号`}]},null,8,[`modelValue`]),g(F(s),{modelValue:a.value,"onUpdate:modelValue":t[1]||(t[1]=e=>a.value=e),type:`password`,name:`password`,label:`密码`,placeholder:`ZYT 登录密码`,autocomplete:`current-password`,enterkeyhint:`go`,rules:[{required:!0,message:`请输入密码`}]},null,8,[`modelValue`])]),_:1}),l.value?(u(),V(`p`,re,C(l.value),1)):z(``,!0),j(`div`,ie,[g(F(i),{round:``,block:``,type:`primary`,"native-type":`submit`,loading:c.value,"loading-text":`正在登录…`},{default:_(()=>[...t[4]||(t[4]=[B(` 登录 `,-1)])]),_:1},8,[`loading`])])]),_:1}),t[5]||(t[5]=j(`p`,{class:`login-foot`},`密码只用于向 ZYT 验证身份,不会保存在手机上。`,-1))])]))}}),[[`__scopeId`,`data-v-858fb177`]]);export{ae as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css new file mode 100644 index 000000000..5045f6a8e --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-B-MK2tgf.css @@ -0,0 +1 @@ +.content[data-v-1710faf3]{max-width:720px;margin:0 auto;padding:12px 0 32px}.profile[data-v-1710faf3]{align-items:center;gap:14px;margin:0 16px;padding:16px;display:flex}.avatar[data-v-1710faf3]{color:#fff;background:linear-gradient(135deg,#2f6fff,#5a65ff);border-radius:50%;flex:none;place-items:center;width:54px;height:54px;font-size:22px;font-weight:700;display:grid}.profile-text[data-v-1710faf3]{flex:1;min-width:0}.profile-text h2[data-v-1710faf3]{margin:0 0 4px;font-size:18px}.profile-text p[data-v-1710faf3]{margin:0;font-size:13px}.root[data-v-1710faf3]{color:var(--app-warning);background:var(--app-warning-soft);border-radius:999px;flex:none;padding:4px 10px;font-size:12px;font-weight:600}.section-title[data-v-1710faf3]{color:var(--app-muted);margin:24px 32px 8px;font-size:13px;font-weight:600}.hint[data-v-1710faf3]{margin:8px 32px 0;font-size:12px;line-height:1.6}.logout[data-v-1710faf3]{margin:32px 16px 0} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js new file mode 100644 index 000000000..175896125 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/MeView-uJ3uOzy0.js @@ -0,0 +1 @@ +import{n as e,t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{a as n,i as r,o as i,s as a}from"./format-DEXJ_MrY.js";import{a as o,n as s,t as c}from"./cell-group-B1PakqJq.js";import{Ct as l,Ft as u,Jt as d,Kt as f,Lt as p,St as m,Ut as h,Yt as g,_t as _,d as v,ft as y,gt as b,ht as x,i as S,n as C,qt as w,vt as T,xt as E,yt as D}from"./index-Gkjcdjlo.js";var O={class:`page`},k={key:0,class:`content`},A={class:`profile app-card`},j={class:`avatar`,"aria-hidden":`true`},M={class:`profile-text`},N={class:`app-muted`},P={key:0,class:`root`},F={class:`hint app-muted`},I={class:`logout`},L=t(l({__name:`MeView`,setup(t){let l=v(),L=f(!1),R=x(()=>C.me),z=x(()=>{let e=R.value;if(!e)return[];let{permissions:t,permission_codes:n}=e;return[{title:`查看本人电脑与会话`,code:`登录即可`,granted:!0},{title:`远程启停本人电脑`,code:n.control,granted:t.control},{title:`查看并控制所有客服电脑`,code:n.control_all,granted:t.control_all},{title:`会话加入黑名单`,code:n.blacklist_add,granted:t.blacklist_add},{title:`会话移出黑名单`,code:n.blacklist_remove,granted:t.blacklist_remove}]}),B=x(()=>{var e;let t=(e=R.value)==null?void 0:e.session.expires_at;return t?new Date(t*1e3).toISOString():``}),V=/iPhone|iPad|iPod/.test(navigator.userAgent);async function H(){try{await a({title:`退出登录?`,message:`退出后需要重新输入 ZYT 账号密码。`,confirmButtonText:`退出`})}catch{return}L.value=!0,await S(),await l.replace({name:`login`})}return(t,a)=>(u(),D(`div`,O,[m(w(i),{title:`我的`,"safe-area-inset-top":``,fixed:``,placeholder:``,border:!1}),R.value?(u(),D(`div`,k,[b(`section`,A,[b(`span`,j,g(w(r)(R.value.account.nickname||R.value.account.sn)),1),b(`div`,M,[b(`h2`,null,g(R.value.account.nickname||R.value.account.sn),1),b(`p`,N,`ZYT 账号 `+g(R.value.account.sn||`—`),1)]),R.value.permissions.root?(u(),D(`span`,P,`超级管理员`)):T(``,!0)]),a[1]||(a[1]=b(`h3`,{class:`section-title`},`手机端权限`,-1)),m(w(c),{inset:``},{default:h(()=>[(u(!0),D(y,null,p(z.value,t=>(u(),_(w(s),{key:t.title,title:t.title,label:t.code,center:``},{value:h(()=>[b(`span`,{class:d(t.granted?`tone-success`:`tone-muted`)},[m(w(e),{name:t.granted?`passed`:`close`},null,8,[`name`]),E(` `+g(t.granted?`已开通`:`未开通`),1)],2)]),_:2},1032,[`title`,`label`]))),128))]),_:1}),a[2]||(a[2]=b(`p`,{class:`hint app-muted`},` 权限在 ZYT「权限管理 → 角色管理」里勾选「企微客服助手」下的项目,调整后 10 分钟内自动生效,或退出重新登录。 `,-1)),a[3]||(a[3]=b(`h3`,{class:`section-title`},`登录信息`,-1)),m(w(c),{inset:``},{default:h(()=>[m(w(s),{title:`登录时间`,value:w(n)(R.value.session.created_at)},null,8,[`value`]),m(w(s),{title:`最长有效至`,value:B.value?w(n)(B.value):`—`,label:`ZYT 登录失效后会提前退出`},null,8,[`value`])]),_:1}),a[4]||(a[4]=b(`h3`,{class:`section-title`},`添加到主屏幕`,-1)),b(`p`,F,[w(V)?(u(),D(y,{key:0},[E(`在 Safari 中点底部「分享」按钮,选择「添加到主屏幕」,之后像 App 一样打开。`)],64)):(u(),D(y,{key:1},[E(`在浏览器菜单中选择「添加到主屏幕」或「安装应用」,之后像 App 一样打开。`)],64))]),b(`div`,I,[m(w(o),{block:``,round:``,type:`danger`,plain:``,loading:L.value,onClick:H},{default:h(()=>[...a[0]||(a[0]=[E(`退出登录`,-1)])]),_:1},8,[`loading`])])])):T(``,!0)]))}}),[[`__scopeId`,`data-v-1710faf3`]]);export{L as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js new file mode 100644 index 000000000..fd8f95b99 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-CQXCC9xU.js @@ -0,0 +1 @@ +import{a as e,n as t,o as n,p as r,r as i,t as a}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n as o,t as s}from"./use-placeholder-CcVnlsb4.js";import{B as c,Ct as l,Et as u,Ft as d,Kt as f,Q as p,Rt as m,S as h,St as g,U as _,Ut as v,X as y,Z as b,_ as x,_t as S,g as C,ht as w,pt as T,q as E,qt as D,rt as O,wt as k,xt as A,yt as j,zt as M}from"./index-Gkjcdjlo.js";var[N,P]=x(`tabbar`),F={route:Boolean,fixed:b,border:b,zIndex:y,placeholder:Boolean,activeColor:String,beforeChange:Function,inactiveColor:String,modelValue:E(0),safeAreaInsetBottom:{type:Boolean,default:null}},I=Symbol(N),L=l({name:N,props:F,emits:[`change`,`update:modelValue`],setup(e,{emit:t,slots:n}){let i=f(),{linkChildren:a}=c(I),l=s(i,P),u=()=>{var t;return(t=e.safeAreaInsetBottom)==null?e.fixed:t},d=()=>{var t;let{fixed:a,zIndex:o,border:s}=e;return g(`div`,{ref:i,role:`tablist`,style:h(o),class:[P({fixed:a}),{[r]:s,"van-safe-area-bottom":u()}]},[(t=n.default)==null?void 0:t.call(n)])};return a({props:e,setActive:(n,r)=>{o(e.beforeChange,{args:[n],done(){t(`update:modelValue`,n),t(`change`,n),r()}})}}),()=>e.fixed&&e.placeholder?l(d):d()}}),R=C(L),[z,B]=x(`tabbar-item`),V=p({},e,{dot:Boolean,icon:String,name:y,badge:y,badgeProps:Object,iconPrefix:String}),H=l({name:z,props:V,emits:[`click`],setup(e,{emit:r,slots:a}){let o=n(),s=k().proxy,{parent:c,index:l}=_(I);if(!c)return;let d=w(()=>{var t;let{route:n,modelValue:r}=c.props;if(n&&`$route`in s){let{$route:t}=s,{to:n}=e,r=O(n)?n:{path:n};return t.matched.some(e=>{let t=`path`in r&&r.path===e.path,n=`name`in r&&r.name===e.name;return t||n})}return((t=e.name)==null?l.value:t)===r}),f=t=>{var n;d.value||c.setActive((n=e.name)==null?l.value:n,o),r(`click`,t)},p=()=>{if(a.icon)return a.icon({active:d.value});if(e.icon)return g(t,{name:e.icon,classPrefix:e.iconPrefix},null)};return()=>{var t;let{dot:n,badge:r}=e,{activeColor:o,inactiveColor:s}=c.props,l=d.value?o:s;return g(`div`,{role:`tab`,class:B({active:d.value}),style:{color:l},tabindex:0,"aria-selected":d.value,onClick:f},[g(i,u({dot:n,class:B(`icon`),content:r},e.badgeProps),{default:p}),g(`div`,{class:B(`text`)},[(t=a.default)==null?void 0:t.call(a,{active:d.value})])])}}}),U=C(H),W={class:`shell`},G=a(l({__name:`ShellView`,setup(e){return(e,t)=>{let n=m(`RouterView`);return d(),j(`div`,W,[g(n,null,{default:v(({Component:e})=>[(d(),S(T,{include:[`DevicesView`,`ConversationsView`]},[(d(),S(M(e)))],1024))]),_:1}),g(D(R),{route:``,placeholder:``,"safe-area-inset-bottom":``,class:`shell-tabbar`},{default:v(()=>[g(D(U),{replace:``,to:{name:`desktops`},icon:`desktop-o`},{default:v(()=>[...t[0]||(t[0]=[A(`电脑`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`conversations`},icon:`chat-o`},{default:v(()=>[...t[1]||(t[1]=[A(`会话`,-1)])]),_:1}),g(D(U),{replace:``,to:{name:`me`},icon:`user-o`},{default:v(()=>[...t[2]||(t[2]=[A(`我的`,-1)])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-5972669c`]]);export{G as default}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css new file mode 100644 index 000000000..a8ec81749 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/ShellView-D354hIv5.css @@ -0,0 +1 @@ +.shell[data-v-5972669c]{min-height:100vh;min-height:100dvh}.shell-tabbar[data-v-5972669c]{border-top:1px solid var(--app-border)} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js new file mode 100644 index 000000000..a9be2cd6a --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/_plugin-vue_export-helper-CT0ZqwFS.js @@ -0,0 +1 @@ +import{Ct as e,Et as t,St as n,Tt as r,Vt as i,X as a,Y as o,Z as s,_ as c,et as l,g as u,ht as d,m as f,nt as p,v as m,wt as h}from"./index-Gkjcdjlo.js";var g=`van-hairline`,_=`${g}--top`,v=`${g}--left`;`${g}`;var y=`${g}--bottom`,b=`${g}--surround`,x=`${g}--top-bottom`;`${g}`;var S=`van-haptics-feedback`,C=Symbol(`van-form`),w=Symbol();function T(e){let t=r(w,null);t&&i(t,t=>{t&&e()})}var E={to:[String,Object],url:String,replace:Boolean};function D({to:e,url:t,replace:n,$router:r}){e&&r?r[n?`replace`:`push`](e):t&&(n?location.replace(t):location.href=t)}function O(){let e=h().proxy;return()=>D(e)}var[k,A]=c(`badge`),j={dot:Boolean,max:a,tag:o(`div`),color:String,offset:Array,content:a,showZero:s,position:o(`top-right`)},M=e({name:k,props:j,setup(e,{slots:t}){let r=()=>{if(t.content)return!0;let{content:n,showZero:r}=e;return l(n)&&n!==``&&(r||n!==0&&n!==`0`)},i=()=>{let{dot:n,max:i,content:a}=e;if(!n&&r())return t.content?t.content():l(i)&&p(a)&&+a>+i?`${i}+`:a},a=e=>e.startsWith(`-`)?e.replace(`-`,``):`-${e}`,o=d(()=>{let n={background:e.color};if(e.offset){let[r,i]=e.offset,{position:o}=e,[s,c]=o.split(`-`);t.default?(n[s]=typeof i==`number`?m(s===`top`?i:-i):s===`top`?m(i):a(i),n[c]=typeof r==`number`?m(c===`left`?r:-r):c===`left`?m(r):a(r)):(n.marginTop=m(i),n.marginLeft=m(r))}return n}),s=()=>{if(r()||e.dot)return n(`div`,{class:A([e.position,{dot:e.dot,fixed:!!t.default}]),style:o.value},[i()])};return()=>{if(t.default){let{tag:r}=e;return n(r,{class:A(`wrapper`)},{default:()=>[t.default(),s()]})}return s()}}}),N=u(M),[P,F]=c(`icon`),I=e=>e==null?void 0:e.includes(`/`),L={dot:Boolean,tag:o(`i`),name:String,size:a,badge:a,color:String,badgeProps:Object,classPrefix:String},R=e({name:P,props:L,setup(e,{slots:i}){let a=r(f,null),o=d(()=>e.classPrefix||(a==null?void 0:a.iconPrefix)||F());return()=>{let{tag:r,dot:a,name:s,size:c,badge:l,color:u}=e,d=I(s);return n(N,t({dot:a,tag:r,class:[o.value,d?``:`${o.value}-${s}`],style:{color:u,fontSize:m(c)},content:l},e.badgeProps),{default:()=>{var e;return[(e=i.default)==null?void 0:e.call(i),d&&n(`img`,{class:F(`image`),src:s},null)]}})}}}),z=u(R),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{E as a,T as c,b as d,_ as f,S as h,D as i,y as l,C as m,z as n,O as o,x as p,N as r,w as s,B as t,v as u}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png new file mode 100644 index 000000000..178549acd Binary files /dev/null and b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/brand-mark-CPUQS9Am.png differ diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js new file mode 100644 index 000000000..6bd64f78f --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/cell-group-B1PakqJq.js @@ -0,0 +1 @@ +import{a as e,d as t,n,o as r,p as i}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{Ct as a,Et as o,O as s,Q as c,St as l,X as u,Y as d,Z as f,_ as p,et as m,ft as h,g,ht as _,v,wt as y,x as b}from"./index-Gkjcdjlo.js";function x(e){let t=y();t&&c(t.proxy,e)}var[S,C]=p(`loading`),w=Array(12).fill(null).map((e,t)=>l(`i`,{class:C(`line`,String(t+1))},null)),T=l(`svg`,{class:C(`circular`),viewBox:`25 25 50 50`},[l(`circle`,{cx:`50`,cy:`50`,r:`20`,fill:`none`},null)]),E={size:u,type:d(`circular`),color:String,vertical:Boolean,textSize:u,textColor:String},D=a({name:S,props:E,setup(e,{slots:t}){let n=_(()=>c({color:e.color},b(e.size))),r=()=>{let r=e.type===`spinner`?w:T;return l(`span`,{class:C(`spinner`,e.type),style:n.value},[t.icon?t.icon():r])},i=()=>{var n;if(t.default)return l(`span`,{class:C(`text`),style:{fontSize:v(e.textSize),color:(n=e.textColor)==null?e.color:n}},[t.default()])};return()=>{let{type:t,vertical:n}=e;return l(`div`,{class:C([t,{vertical:n}]),"aria-live":`polite`,"aria-busy":!0},[r(),i()])}}}),O=g(D),[k,A]=p(`button`),j=c({},e,{tag:d(`button`),text:String,icon:String,type:d(`default`),size:d(`normal`),color:String,block:Boolean,plain:Boolean,round:Boolean,square:Boolean,loading:Boolean,hairline:Boolean,disabled:Boolean,iconPrefix:String,nativeType:d(`button`),loadingSize:u,loadingText:String,loadingType:String,iconPosition:d(`left`)}),M=a({name:k,props:j,emits:[`click`],setup(e,{emit:i,slots:a}){let o=r(),c=()=>a.loading?a.loading():l(O,{size:e.loadingSize,type:e.loadingType,class:A(`loading`)},null),u=()=>{if(e.loading)return c();if(a.icon)return l(`div`,{class:A(`icon`)},[a.icon()]);if(e.icon)return l(n,{name:e.icon,class:A(`icon`),classPrefix:e.iconPrefix},null)},d=()=>{let t;if(t=e.loading?e.loadingText:a.default?a.default():e.text,t)return l(`span`,{class:A(`text`)},[t])},f=()=>{let{color:t,plain:n}=e;if(t){let e={color:n?t:`white`};return n||(e.background=t),t.includes(`gradient`)?e.border=0:e.borderColor=t,e}},p=t=>{e.loading?s(t):e.disabled||(i(`click`,t),o())};return()=>{let{tag:n,type:r,size:i,block:a,round:o,plain:s,square:c,loading:m,disabled:h,hairline:g,nativeType:_,iconPosition:v}=e,y=[A([r,i,{plain:s,block:a,round:o,square:c,loading:m,disabled:h,hairline:g}]),{[t]:g}];return l(n,{type:_,class:y,style:f(),disabled:h,onClick:p},{default:()=>[l(`div`,{class:A(`content`)},[v===`left`&&u(),d(),v===`right`&&u()])]})}}}),N=g(M),P=()=>{var e;let{scopeId:t}=((e=y())==null?void 0:e.vnode)||{};return t?{[t]:``}:null},[F,I]=p(`cell`),L={tag:d(`div`),icon:String,size:String,title:u,value:u,label:u,center:Boolean,isLink:Boolean,border:f,iconPrefix:String,valueClass:null,labelClass:null,titleClass:null,titleStyle:null,arrowDirection:String,required:{type:[Boolean,String],default:null},clickable:{type:Boolean,default:null}},R=c({},L,e),z=a({name:F,props:R,setup(e,{slots:t}){let i=r(),a=()=>{if(t.label||m(e.label))return l(`div`,{class:[I(`label`),e.labelClass]},[t.label?t.label():e.label])},o=()=>{var n;if(t.title||m(e.title)){let r=(n=t.title)==null?void 0:n.call(t);return Array.isArray(r)&&r.length===0?void 0:l(`div`,{class:[I(`title`),e.titleClass],style:e.titleStyle},[r||l(`span`,null,[e.title]),a()])}},s=()=>{let n=t.value||t.default;if(n||m(e.value))return l(`div`,{class:[I(`value`),e.valueClass]},[n?n():l(`span`,null,[e.value])])},c=()=>{if(t.icon)return t.icon();if(e.icon)return l(n,{name:e.icon,class:I(`left-icon`),classPrefix:e.iconPrefix},null)},u=()=>{if(t[`right-icon`])return t[`right-icon`]();if(e.isLink){let t=e.arrowDirection&&e.arrowDirection!==`right`?`arrow-${e.arrowDirection}`:`arrow`;return l(n,{name:t,class:I(`right-icon`)},null)}};return()=>{var n,r,a;let{tag:d,size:f,center:p,border:m,isLink:h,required:g}=e,_=!!((a=(r=(n=e.clickable)==null?e.to:n)==null?e.url:r)==null?h:a),v={center:p,required:!!g,clickable:_,borderless:!m};return f&&(v[f]=!!f),l(d,{class:I(v),role:_?`button`:void 0,tabindex:_?0:void 0,onClick:_?i:void 0},{default:()=>{var e;return[c(),o(),s(),u(),(e=t.extra)==null?void 0:e.call(t)]}})}}}),B=g(z),[V,H]=p(`cell-group`),U=a({name:V,inheritAttrs:!1,props:{title:String,inset:Boolean,border:f},setup(e,{slots:t,attrs:n}){let r=()=>{var r;return l(`div`,o({class:[H({inset:e.inset}),{[i]:e.border&&!e.inset}]},n,P()),[(r=t.default)==null?void 0:r.call(t)])},a=()=>l(`div`,{class:H(`title`,{inset:e.inset})},[t.title?t.title():e.title]);return()=>e.title||t.title?l(h,null,[a(),r()]):r()}}),W=g(U);export{N as a,P as i,B as n,O as o,L as r,x as s,W as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js new file mode 100644 index 000000000..df761d80e --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/field-CRX3eS7h.js @@ -0,0 +1 @@ +import{m as e,n as t}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n,r,s as i}from"./cell-group-B1PakqJq.js";import{A as a,Ct as o,Dt as s,Et as c,Gt as ee,It as l,Kt as u,Mt as d,O as f,P as te,Q as p,St as m,T as h,U as g,V as _,Vt as v,X as y,Y as b,_ as x,b as S,et as C,g as w,ht as T,it as E,k as ne,q as D,rt as O,st as k,tt as A,v as j,wt as M,xt as re,y as ie}from"./index-Gkjcdjlo.js";var N=0;function P(){let e=M(),{name:t=`unknown`}=(e==null?void 0:e.type)||{};return`${t}-${++N}`}function F(e){return Array.isArray(e)?!e.length:e!==0&&!e}function ae(e,t){if(F(e)){if(t.required)return!1;if(t.validateEmpty===!1)return!0}return!(t.pattern&&!t.pattern.test(String(e)))}function oe(e,t){return new Promise(n=>{let r=t.validator(e,t);if(E(r)){r.then(n);return}n(r)})}function I(e,t){let{message:n}=t;return A(n)?n(e,t):n||``}function se({target:e}){e.composing=!0}function L({target:e}){e.composing&&(e.composing=!1,e.dispatchEvent(new Event(`input`)))}function ce(e,t){let n=h();e.style.height=`auto`;let r=e.scrollHeight;if(O(t)){let{maxHeight:e,minHeight:n}=t;e!==void 0&&(r=Math.min(r,e)),n!==void 0&&(r=Math.max(r,n))}r&&(e.style.height=`${r}px`,a(n))}function le(e,t){return e===`number`&&(e=`text`,t!=null||(t=`decimal`)),e===`digit`&&(e=`tel`,t!=null||(t=`numeric`)),{type:e,inputmode:t}}function R(e){return[...e].length}function z(e,t){return[...e].slice(0,t).join(``)}var[B,V]=x(`field`),H={id:String,name:String,leftIcon:String,rightIcon:String,autofocus:Boolean,clearable:Boolean,maxlength:y,max:Number,min:Number,formatter:Function,clearIcon:b(`clear`),modelValue:D(``),inputAlign:String,placeholder:String,autocomplete:String,autocapitalize:String,autocorrect:String,errorMessage:String,enterkeyhint:String,clearTrigger:b(`focus`),formatTrigger:b(`onChange`),spellcheck:{type:Boolean,default:null},error:{type:Boolean,default:null},disabled:{type:Boolean,default:null},readonly:{type:Boolean,default:null},inputmode:String},U=p({},r,H,{rows:y,type:b(`text`),rules:Array,autosize:[Boolean,Object],labelWidth:y,labelClass:null,labelAlign:String,showWordLimit:Boolean,errorMessageAlign:String,colon:{type:Boolean,default:null}}),W=o({name:B,props:U,emits:[`blur`,`focus`,`clear`,`keypress`,`clickInput`,`endValidate`,`startValidate`,`clickLeftIcon`,`clickRightIcon`,`update:modelValue`],setup(r,{emit:a,slots:o}){let p=P(),h=ee({status:`unvalidated`,focused:!1,validateMessage:``}),y=u(),b=u(),x=u(),{parent:w}=g(e),E=()=>{var e;return String((e=r.modelValue)==null?``:e)},D=e=>{if(C(r[e]))return r[e];if(w&&C(w.props[e]))return w.props[e]},O=T(()=>{let e=D(`readonly`);if(r.clearable&&!e){let e=E()!==``,t=r.clearTrigger===`always`||r.clearTrigger===`focus`&&h.focused;return e&&t}return!1}),A=T(()=>x.value&&o.input?x.value():r.modelValue),M=T(()=>{var e;let t=D(`required`);return t===`auto`?(e=r.rules)==null?void 0:e.some(e=>e.required):t}),N=e=>e.reduce((e,t)=>e.then(()=>{if(h.status===`failed`)return;let{value:e}=A;if(t.formatter&&(e=t.formatter(e,t)),!ae(e,t)){h.status=`failed`,h.validateMessage=I(e,t);return}if(t.validator)return F(e)&&t.validateEmpty===!1?void 0:oe(e,t).then(n=>{n&&typeof n==`string`?(h.status=`failed`,h.validateMessage=n):n===!1&&(h.status=`failed`,h.validateMessage=I(e,t))})}),Promise.resolve()),B=()=>{h.status=`unvalidated`,h.validateMessage=``},H=()=>a(`endValidate`,{status:h.status,message:h.validateMessage}),U=(e=r.rules)=>new Promise(t=>{B(),e?(a(`startValidate`),N(e).then(()=>{h.status===`failed`?(t({name:r.name,message:h.validateMessage}),H()):(h.status=`passed`,t(),H())})):t()}),W=e=>{if(w&&r.rules){let{validateTrigger:t}=w.props,n=k(t).includes(e),i=r.rules.filter(t=>t.trigger?k(t.trigger).includes(e):n);i.length&&U(i)}},G=e=>{var t;let{maxlength:n}=r;if(C(n)&&R(e)>+n){let r=E();if(r&&R(r)===+n)return r;let i=(t=y.value)==null?void 0:t.selectionEnd;if(h.focused&&i){let t=[...e],r=t.length-+n;return i=R(e.slice(0,i)),t.splice(i-r,r),t.join(``)}return z(e,+n)}return e},K=(e,t=`onChange`)=>{var n,i;let o=e;e=G(e);let s=o.length-e.length;if(r.type===`number`||r.type===`digit`){let a=r.type===`number`;if(e=S(e,a,a),t===`onBlur`&&e!==``&&(r.min!==void 0||r.max!==void 0)){let t=ie(+e,(n=r.min)==null?-1/0:n,(i=r.max)==null?1/0:i);+e!==t&&(e=t.toString())}}let c=0;if(r.formatter&&t===r.formatTrigger){let{formatter:t,maxlength:n}=r;if(e=t(e),C(n)&&R(e)>+n&&(e=z(e,+n)),y.value&&h.focused){let{selectionEnd:e}=y.value,n=z(o,e);c=t(n).length-n.length}}if(y.value&&y.value.value!==e){if(h.focused){let{selectionStart:t,selectionEnd:n}=y.value;if(y.value.value=e,C(t)&&C(n)){let r=e.length;s?(t-=s,n-=s):c&&(t+=c,n+=c),y.value.setSelectionRange(Math.min(t,r),Math.min(n,r))}}else y.value.value=e}e!==r.modelValue&&a(`update:modelValue`,e)},ue=e=>{e.target.composing||K(e.target.value)},q=()=>{var e;return(e=y.value)==null?void 0:e.blur()},J=()=>{var e;return(e=y.value)==null?void 0:e.focus()},Y=()=>{let e=y.value;r.type===`textarea`&&r.autosize&&e&&ce(e,r.autosize)},de=e=>{h.focused=!0,a(`focus`,e),s(Y),D(`readonly`)&&q()},fe=e=>{h.focused=!1,K(E(),`onBlur`),a(`blur`,e),!D(`readonly`)&&(W(`onBlur`),s(Y),ne())},X=e=>a(`clickInput`,e),pe=e=>a(`clickLeftIcon`,e),me=e=>a(`clickRightIcon`,e),he=e=>{f(e),a(`update:modelValue`,``),a(`clear`,e)},Z=T(()=>{if(typeof r.error==`boolean`)return r.error;if(w&&w.props.showError&&h.status===`failed`)return!0}),ge=T(()=>{let e=D(`labelWidth`),t=D(`labelAlign`);if(e&&t!==`top`)return{width:j(e)}}),Q=e=>{e.keyCode===13&&(!(w&&w.props.submitOnEnter)&&r.type!==`textarea`&&f(e),r.type===`search`&&q()),a(`keypress`,e)},$=()=>r.id||`${p}-input`,_e=()=>h.status,ve=()=>{let e=V(`control`,[D(`inputAlign`),{error:Z.value,custom:!!o.input,"min-height":r.type===`textarea`&&!r.autosize}]);if(o.input)return m(`div`,{class:e,onClick:X},[o.input()]);let t={id:$(),ref:y,name:r.name,rows:r.rows===void 0?void 0:+r.rows,class:e,disabled:D(`disabled`),readonly:D(`readonly`),autofocus:r.autofocus,placeholder:r.placeholder,autocomplete:r.autocomplete,autocapitalize:r.autocapitalize,autocorrect:r.autocorrect,enterkeyhint:r.enterkeyhint,spellcheck:r.spellcheck,"aria-labelledby":r.label?`${p}-label`:void 0,"data-allow-mismatch":`attribute`,onBlur:fe,onFocus:de,onInput:ue,onClick:X,onChange:L,onKeypress:Q,onCompositionend:L,onCompositionstart:se};return r.type===`textarea`?m(`textarea`,c(t,{inputmode:r.inputmode}),null):m(`input`,c(le(r.type,r.inputmode),t),null)},ye=()=>{let e=o[`left-icon`];if(r.leftIcon||e)return m(`div`,{class:V(`left-icon`),onClick:pe},[e?e():m(t,{name:r.leftIcon,classPrefix:r.iconPrefix},null)])},be=()=>{let e=o[`right-icon`];if(r.rightIcon||e)return m(`div`,{class:V(`right-icon`),onClick:me},[e?e():m(t,{name:r.rightIcon,classPrefix:r.iconPrefix},null)])},xe=()=>{if(r.showWordLimit&&r.maxlength){let e=R(E());return m(`div`,{class:V(`word-limit`)},[m(`span`,{class:V(`word-num`)},[e]),re(`/`),r.maxlength])}},Se=()=>{if(w&&w.props.showErrorMessage===!1)return;let e=r.errorMessage||h.validateMessage;if(e){let t=o[`error-message`],n=D(`errorMessageAlign`);return m(`div`,{class:V(`error-message`,n)},[t?t({message:e}):e])}},Ce=()=>{let e=D(`labelWidth`),t=D(`labelAlign`),n=D(`colon`)?`:`:``;if(o.label)return[o.label(),n];if(r.label)return m(`label`,{id:`${p}-label`,for:o.input?void 0:$(),"data-allow-mismatch":`attribute`,onClick:e=>{f(e),J()},style:t===`top`&&e?{width:j(e)}:void 0},[r.label+n])},we=()=>[m(`div`,{class:V(`body`)},[ve(),O.value&&m(t,{ref:b,name:r.clearIcon,class:V(`clear`)},null),be(),o.button&&m(`div`,{class:V(`button`)},[o.button()])]),xe(),Se()];return i({blur:q,focus:J,validate:U,formValue:A,resetValidation:B,getValidationStatus:_e,adjustTextareaSize:Y}),l(te,{customValue:x,resetValidation:B,validateWithTrigger:W}),v(()=>r.modelValue,()=>{K(E()),B(),W(`onChange`),s(Y)}),d(()=>{K(E(),r.formatTrigger),s(Y)}),_(`touchstart`,he,{target:T(()=>{var e;return(e=b.value)==null?void 0:e.$el})}),()=>{let e=D(`disabled`),t=D(`labelAlign`),i=ye(),a=()=>{let e=Ce();return t===`top`?[i,e].filter(Boolean):e||[]};return m(n,{size:r.size,class:V({error:Z.value,disabled:e,[`label-${t}`]:t}),center:r.center,border:r.border,isLink:!e&&r.isLink,clickable:!e&&r.clickable,onClick:e?e=>e.stopImmediatePropagation():void 0,titleStyle:ge.value,valueClass:V(`value`),titleClass:[V(`label`,[t,{required:M.value}]),r.labelClass],arrowDirection:r.arrowDirection},{icon:i&&t!==`top`?()=>i:null,title:a,value:we,extra:o.extra})}}}),G=w(W);export{H as n,P as r,G as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js new file mode 100644 index 000000000..f5338dc42 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/format-DEXJ_MrY.js @@ -0,0 +1 @@ +import{a as e,f as t,h as n,l as r,n as i,o as a,s as o,u as s}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{n as c,t as l}from"./use-placeholder-CcVnlsb4.js";import{a as u,i as d,s as f}from"./cell-group-B1PakqJq.js";import{$ as p,B as m,Ct as h,Dt as ee,Et as g,Gt as _,It as te,Kt as v,L as y,Mt as b,O as x,Ot as ne,Q as S,R as C,S as w,St as T,U as E,V as re,Vt as D,Wt as O,X as k,Y as A,Z as j,_ as M,at as N,ct as ie,dt as P,et as ae,ft as oe,g as F,h as se,ht as I,jt as ce,kt as le,lt as ue,mt as de,ot as fe,tt as pe,ut as L,v as me}from"./index-Gkjcdjlo.js";var[R,z]=M(`action-bar`),B=Symbol(R),he=h({name:R,props:{placeholder:Boolean,safeAreaInsetBottom:j},setup(e,{slots:t}){let n=v(),r=l(n,z),{linkChildren:i}=m(B);i();let a=()=>{var r;return T(`div`,{ref:n,class:[z(),{"van-safe-area-bottom":e.safeAreaInsetBottom}]},[(r=t.default)==null?void 0:r.call(t)])};return()=>e.placeholder?r(a):a()}}),ge=F(he),[_e,ve]=M(`action-bar-button`),ye=S({},e,{type:String,text:String,icon:String,color:String,loading:Boolean,disabled:Boolean}),be=h({name:_e,props:ye,setup(e,{slots:t}){let n=a(),{parent:r,index:i}=E(B),o=I(()=>{if(r){let e=r.children[i.value-1];return!(e&&`isButton`in e)}}),s=I(()=>{if(r){let e=r.children[i.value+1];return!(e&&`isButton`in e)}});return f({isButton:!0}),()=>{let{type:r,icon:i,text:a,color:c,loading:l,disabled:d}=e;return T(u,{class:ve([r,{last:s.value,first:o.value}]),size:`large`,type:r,icon:i,color:c,loading:l,disabled:d,onClick:n},{default:()=>[t.default?t.default():a]})}}}),V=F(be),H={show:Boolean,zIndex:k,overlay:j,duration:k,teleport:[String,Object],lockScroll:j,lazyRender:j,beforeClose:Function,overlayProps:Object,overlayStyle:Object,overlayClass:null,transitionAppear:Boolean,closeOnClickOverlay:j},xe=Object.keys(H);function Se(e,t){return e>t?`horizontal`:t>e?`vertical`:``}function U(){let e=v(0),t=v(0),n=v(0),r=v(0),i=v(0),a=v(0),o=v(``),s=v(!0),c=()=>o.value===`vertical`,l=()=>o.value===`horizontal`,u=()=>{n.value=0,r.value=0,i.value=0,a.value=0,o.value=``,s.value=!0};return{move:(c=>{let l=c.touches[0];n.value=(l.clientX<0?0:l.clientX)-e.value,r.value=l.clientY-t.value,i.value=Math.abs(n.value),a.value=Math.abs(r.value),(!o.value||i.value<10&&a.value<10)&&(o.value=Se(i.value,a.value)),s.value&&(i.value>5||a.value>5)&&(s.value=!1)}),start:(n=>{u(),e.value=n.touches[0].clientX,t.value=n.touches[0].clientY}),reset:u,startX:e,startY:t,deltaX:n,deltaY:r,offsetX:i,offsetY:a,direction:o,isVertical:c,isHorizontal:l,isTap:s}}var W=0,G=`van-overflow-hidden`;function Ce(e,t){let n=U(),r=t=>{n.move(t);let r=n.deltaY.value>0?`10`:`01`,i=y(t.target,e.value);for(;i.scrollHeight<=i.offsetHeight&&i!==e.value&&i.parentElement;)i=y(i.parentElement,e.value);let{scrollHeight:a,offsetHeight:o,scrollTop:s}=i,c=`11`;s===0?c=o>=a?`00`:`01`:s+o>=a&&(c=`10`),c!==`11`&&n.isVertical()&&!(parseInt(c,2)&parseInt(r,2))&&x(t,!0)},i=()=>{document.addEventListener(`touchstart`,n.start),document.addEventListener(`touchmove`,r,{passive:!1}),W||document.body.classList.add(G),W++},a=()=>{W&&(document.removeEventListener(`touchstart`,n.start),document.removeEventListener(`touchmove`,r),W--,W||document.body.classList.remove(G))},o=()=>t()&&i(),s=()=>t()&&a();C(o),ce(s),le(s),D(t,e=>{e?i():a()})}function K(e){let t=v(!1);return D(e,e=>{e&&(t.value=e)},{immediate:!0}),e=>()=>t.value?e():null}var[we,Te]=M(`overlay`),Ee=h({name:we,inheritAttrs:!1,props:{show:Boolean,zIndex:k,duration:k,className:null,lockScroll:j,lazyRender:j,customStyle:Object,teleport:[String,Object]},setup(e,{attrs:t,slots:n}){let r=v(),i=K(()=>e.show||!e.lazyRender),a=t=>{e.lockScroll&&x(t,!0)},o=i(()=>{var i;let a=S(w(e.zIndex),e.customStyle);return ae(e.duration)&&(a.animationDuration=`${e.duration}s`),O(T(`div`,g({ref:r,style:a,class:[Te(),e.className]},t),[(i=n.default)==null?void 0:i.call(n)]),[[L,e.show]])});return re(`touchmove`,a,{target:r}),()=>{let t=T(ie,{name:`van-fade`,appear:!0},{default:o});return e.teleport?T(de,{to:e.teleport},{default:()=>[t]}):t}}}),De=F(Ee),Oe=S({},H,{round:Boolean,position:A(`center`),closeIcon:A(`cross`),closeable:Boolean,transition:String,iconPrefix:String,closeOnPopstate:Boolean,closeIconPosition:A(`top-right`),destroyOnClose:Boolean,safeAreaInsetTop:Boolean,safeAreaInsetBottom:Boolean}),[ke,q]=M(`popup`),Ae=h({name:ke,inheritAttrs:!1,props:Oe,emits:[`open`,`close`,`opened`,`closed`,`keydown`,`update:show`,`clickOverlay`,`clickCloseIcon`],setup(e,{emit:t,attrs:r,slots:a}){let s,l,u=v(),p=v(),m=K(()=>e.show||!e.lazyRender),h=I(()=>{let t={zIndex:u.value};if(ae(e.duration)){let n=e.position===`center`?`animationDuration`:`transitionDuration`;t[n]=`${e.duration}s`}return t}),_=()=>{s||(s=!0,u.value=e.zIndex===void 0?se():+e.zIndex,t(`open`))},y=()=>{s&&c(e.beforeClose,{done(){s=!1,t(`close`),t(`update:show`,!1)}})},x=n=>{t(`clickOverlay`,n),e.closeOnClickOverlay&&y()},C=()=>{if(e.overlay){let t=S({show:e.show,class:e.overlayClass,zIndex:u.value,duration:e.duration,customStyle:e.overlayStyle,role:e.closeOnClickOverlay?`button`:void 0,tabindex:e.closeOnClickOverlay?0:void 0},e.overlayProps);return T(De,g(t,d(),{onClick:x}),{default:a[`overlay-content`]})}},w=e=>{t(`clickCloseIcon`,e),y()},E=()=>{if(e.closeable)return T(i,{role:`button`,tabindex:0,name:e.closeIcon,class:[q(`close-icon`,e.closeIconPosition),n],classPrefix:e.iconPrefix,onClick:w},null)},k,A=()=>{k&&clearTimeout(k),k=setTimeout(()=>{t(`opened`)})},j=()=>t(`closed`),M=e=>t(`keydown`,e),N=m(()=>{var t;let{destroyOnClose:n,round:i,position:o,safeAreaInsetTop:s,safeAreaInsetBottom:c,show:l}=e;if(l||!n)return O(T(`div`,g({ref:p,style:h.value,role:`dialog`,tabindex:0,class:[q({round:i,[o]:o}),{"van-safe-area-top":s,"van-safe-area-bottom":c}],onKeydown:M},r,d()),[(t=a.default)==null?void 0:t.call(a),E()]),[[L,l]])}),P=()=>{let{position:t,transition:n,transitionAppear:r}=e,i=t===`center`?`van-fade`:`van-popup-slide-${t}`;return T(ie,{name:n||i,appear:r,onAfterEnter:A,onAfterLeave:j},{default:N})};return D(()=>e.show,e=>{e&&!s&&(_(),r.tabindex===0&&ee(()=>{var e;(e=p.value)==null||e.focus()})),!e&&s&&(s=!1,t(`close`))}),f({popupRef:p}),Ce(p,()=>e.show&&e.lockScroll),re(`popstate`,()=>{e.closeOnPopstate&&(y(),l=!1)}),b(()=>{e.show&&_()}),ne(()=>{l&&(t(`update:show`,!0),l=!1)}),ce(()=>{e.show&&e.teleport&&(y(),l=!0)}),te(o,()=>e.show),()=>e.teleport?T(de,{to:e.teleport},{default:()=>[C(),P()]}):T(oe,null,[C(),P()])}}),J=F(Ae);function je(){let e=_({show:!1}),t=t=>{e.show=t},n=n=>{S(e,n,{transitionAppear:!0}),t(!0)},r=()=>t(!1);return f({open:n,close:r,toggle:t}),{open:n,close:r,state:e,toggle:t}}function Me(e){let t=ue(e),n=document.createElement(`div`);return document.body.appendChild(n),{instance:t.mount(n),unmount(){t.unmount(),document.body.removeChild(n)}}}var[Ne,Y,X]=M(`dialog`),Pe=S({},H,{title:String,theme:String,width:k,message:[String,Function],callback:Function,allowHtml:Boolean,className:null,transition:A(`van-dialog-bounce`),messageAlign:String,closeOnPopstate:j,showCancelButton:Boolean,cancelButtonText:String,cancelButtonColor:String,cancelButtonDisabled:Boolean,confirmButtonText:String,confirmButtonColor:String,confirmButtonDisabled:Boolean,showConfirmButton:j,closeOnClickOverlay:Boolean,keyboardEnabled:j,destroyOnClose:Boolean}),Fe=[...xe,`transition`,`closeOnPopstate`,`destroyOnClose`],Ie=h({name:Ne,props:Pe,emits:[`confirm`,`cancel`,`keydown`,`update:show`],setup(e,{emit:n,slots:r}){let i=v(),a=_({confirm:!1,cancel:!1}),o=e=>n(`update:show`,e),l=t=>{var n;o(!1),(n=e.callback)==null||n.call(e,t)},d=t=>()=>{e.show&&(n(t),e.beforeClose?(a[t]=!0,c(e.beforeClose,{args:[t],done(){l(t),a[t]=!1},canceled(){a[t]=!1}})):l(t))},f=d(`cancel`),p=d(`confirm`),m=P(t=>{var r,a;e.keyboardEnabled&&t.target===((a=(r=i.value)==null?void 0:r.popupRef)==null?void 0:a.value)&&({Enter:e.showConfirmButton?p:N,Escape:e.showCancelButton?f:N}[t.key](),n(`keydown`,t))},[`enter`,`esc`]),h=()=>{let t=r.title?r.title():e.title;if(t)return T(`div`,{class:Y(`header`,{isolated:!e.message&&!r.default})},[t])},ee=t=>{let{message:n,allowHtml:r,messageAlign:i}=e,a=Y(`message`,{"has-title":t,[i]:i}),o=pe(n)?n():n;return r&&typeof o==`string`?T(`div`,{class:a,innerHTML:o},null):T(`div`,{class:a},[o])},te=()=>{if(r.default)return T(`div`,{class:Y(`content`)},[r.default()]);let{title:t,message:n,allowHtml:i}=e;if(n){let e=!!(t||r.title);return T(`div`,{key:+!!i,class:Y(`content`,{isolated:!e})},[ee(e)])}},y=()=>T(`div`,{class:[t,Y(`footer`)]},[e.showCancelButton&&T(u,{size:`large`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),style:{color:e.cancelButtonColor},loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(u,{size:`large`,text:e.confirmButtonText||X(`confirm`),class:[Y(`confirm`),{[s]:e.showCancelButton}],style:{color:e.confirmButtonColor},loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]),b=()=>T(ge,{class:Y(`footer`)},{default:()=>[e.showCancelButton&&T(V,{type:`warning`,text:e.cancelButtonText||X(`cancel`),class:Y(`cancel`),color:e.cancelButtonColor,loading:a.cancel,disabled:e.cancelButtonDisabled,onClick:f},null),e.showConfirmButton&&T(V,{type:`danger`,text:e.confirmButtonText||X(`confirm`),class:Y(`confirm`),color:e.confirmButtonColor,loading:a.confirm,disabled:e.confirmButtonDisabled,onClick:p},null)]}),x=()=>r.footer?r.footer():e.theme===`round-button`?b():y();return()=>{let{width:t,title:n,theme:r,message:a,className:s}=e;return T(J,g({ref:i,role:`dialog`,class:[Y([r]),s],style:{width:me(t)},tabindex:0,"aria-labelledby":n||a,onKeydown:m,"onUpdate:show":o},fe(e,Fe)),{default:()=>[h(),te(),x()]})}}}),Z,Le=S({},{title:``,width:``,theme:null,message:``,overlay:!0,callback:null,teleport:`body`,className:``,allowHtml:!1,lockScroll:!0,transition:void 0,beforeClose:null,overlayClass:``,overlayStyle:void 0,messageAlign:``,cancelButtonText:``,cancelButtonColor:null,cancelButtonDisabled:!1,confirmButtonText:``,confirmButtonColor:null,confirmButtonDisabled:!1,showConfirmButton:!0,showCancelButton:!1,closeOnPopstate:!0,closeOnClickOverlay:!1,destroyOnClose:!1});function Re(){let e={setup(){let{state:e,toggle:t}=je();return()=>T(Ie,g(e,{"onUpdate:show":t}),null)}};({instance:Z}=Me(e))}function ze(e){return p?new Promise((t,n)=>{Z||Re(),Z.open(S({},Le,e,{callback:e=>{(e===`confirm`?t:n)(e)}}))}):Promise.resolve(void 0)}var Be=e=>ze(S({showCancelButton:!0},e)),[Ve,Q]=M(`nav-bar`),He=h({name:Ve,props:{title:String,fixed:Boolean,zIndex:k,border:j,leftText:String,rightText:String,leftDisabled:Boolean,rightDisabled:Boolean,leftArrow:Boolean,placeholder:Boolean,safeAreaInsetTop:Boolean,clickable:j},emits:[`clickLeft`,`clickRight`],setup(e,{emit:t,slots:n}){let a=v(),o=l(a,Q),s=n=>{e.leftDisabled||t(`clickLeft`,n)},c=n=>{e.rightDisabled||t(`clickRight`,n)},u=()=>n.left?n.left():[e.leftArrow&&T(i,{class:Q(`arrow`),name:`arrow-left`},null),e.leftText&&T(`span`,{class:Q(`text`)},[e.leftText])],d=()=>n.right?n.right():T(`span`,{class:Q(`text`)},[e.rightText]),f=()=>{let{title:t,fixed:i,border:o,zIndex:l}=e,f=w(l),p=e.leftArrow||e.leftText||n.left,m=e.rightText||n.right;return T(`div`,{ref:a,style:f,class:[Q({fixed:i}),{[r]:o,"van-safe-area-top":e.safeAreaInsetTop}]},[T(`div`,{class:Q(`content`)},[p&&T(`div`,{class:[Q(`left`,{disabled:e.leftDisabled}),e.clickable&&!e.leftDisabled?`van-haptics-feedback`:``],onClick:s},[u()]),T(`div`,{class:[Q(`title`),`van-ellipsis`]},[n.title?n.title():t]),m&&T(`div`,{class:[Q(`right`,{disabled:e.rightDisabled}),e.clickable&&!e.rightDisabled?`van-haptics-feedback`:``],onClick:c},[d()])])])};return()=>e.fixed&&e.placeholder?o(f):f()}}),Ue=F(He),$=e=>String(e).padStart(2,`0`);function We(e){let t=String(e||``).trim();if(!t)return null;let n=new Date(t);return Number.isNaN(n.getTime())?null:n}function Ge(e,t=new Date){let n=We(e);if(!n)return`—`;let r=Math.round((t.getTime()-n.getTime())/1e3);if(r<45&&r>-60)return`刚刚`;if(r>=45&&r<3600)return`${Math.max(1,Math.round(r/60))} 分钟前`;let i=`${$(n.getHours())}:${$(n.getMinutes())}`,a=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),o=864e5;return n.getTime()>=a&&n.getTime()=a-o&&n.getTime().van-tabs__wrap{height:var(--van-tabs-card-height)}.van-swipe-item{flex-shrink:0;width:100%;height:100%;position:relative}.van-tab__panel,.van-tab__panel-wrapper{box-sizing:border-box;flex-shrink:0;width:100%}.van-tab__panel-wrapper--inactive{height:0;overflow:visible}:root,:host{--van-overlay-z-index:1;--van-overlay-background:#000000b3}.van-overlay{z-index:var(--van-overlay-z-index);background:var(--van-overlay-background);width:100%;height:100%;position:fixed;top:0;left:0}:root,:host{--van-popup-background:var(--van-background-2);--van-popup-transition:transform var(--van-duration-base);--van-popup-round-radius:16px;--van-popup-close-icon-size:22px;--van-popup-close-icon-color:var(--van-gray-5);--van-popup-close-icon-margin:16px;--van-popup-close-icon-z-index:1}.van-overflow-hidden{overflow:hidden!important}.van-popup{box-sizing:border-box;background:var(--van-popup-background);max-height:100%;transition:var(--van-popup-transition);-webkit-overflow-scrolling:touch;position:fixed;overflow-y:auto}.van-popup--center{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100vw - var(--van-padding-md) * 2);margin:0 auto;top:50%;left:0;right:0;transform:translateY(-50%)}.van-popup--center.van-popup--round{border-radius:var(--van-popup-round-radius)}.van-popup--top{width:100%;top:0;left:0}.van-popup--top.van-popup--round{border-radius:0 0 var(--van-popup-round-radius) var(--van-popup-round-radius)}.van-popup--right{top:50%;right:0;transform:translateY(-50%)}.van-popup--right.van-popup--round{border-radius:var(--van-popup-round-radius) 0 0 var(--van-popup-round-radius)}.van-popup--bottom{width:100%;bottom:0;left:0}.van-popup--bottom.van-popup--round{border-radius:var(--van-popup-round-radius) var(--van-popup-round-radius) 0 0}.van-popup--left{top:50%;left:0;transform:translateY(-50%)}.van-popup--left.van-popup--round{border-radius:0 var(--van-popup-round-radius) var(--van-popup-round-radius) 0}.van-popup-slide-top-enter-active,.van-popup-slide-left-enter-active,.van-popup-slide-right-enter-active,.van-popup-slide-bottom-enter-active{transition-timing-function:var(--van-ease-out)}.van-popup-slide-top-leave-active,.van-popup-slide-left-leave-active,.van-popup-slide-right-leave-active,.van-popup-slide-bottom-leave-active{transition-timing-function:var(--van-ease-in)}.van-popup-slide-top-enter-from,.van-popup-slide-top-leave-active{transform:translateY(-100%)}.van-popup-slide-right-enter-from,.van-popup-slide-right-leave-active{transform:translate(100%,-50%)}.van-popup-slide-bottom-enter-from,.van-popup-slide-bottom-leave-active{transform:translateY(100%)}.van-popup-slide-left-enter-from,.van-popup-slide-left-leave-active{transform:translate(-100%,-50%)}.van-popup__close-icon{z-index:var(--van-popup-close-icon-z-index);color:var(--van-popup-close-icon-color);font-size:var(--van-popup-close-icon-size);position:absolute}.van-popup__close-icon--top-left{top:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--top-right{top:var(--van-popup-close-icon-margin);right:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-left{bottom:var(--van-popup-close-icon-margin);left:var(--van-popup-close-icon-margin)}.van-popup__close-icon--bottom-right{right:var(--van-popup-close-icon-margin);bottom:var(--van-popup-close-icon-margin)}:root,:host{--van-empty-padding:var(--van-padding-xl) 0;--van-empty-image-size:160px;--van-empty-description-margin-top:var(--van-padding-md);--van-empty-description-padding:0 60px;--van-empty-description-color:var(--van-text-color-2);--van-empty-description-font-size:var(--van-font-size-md);--van-empty-description-line-height:var(--van-line-height-md);--van-empty-bottom-margin-top:24px}.van-empty{box-sizing:border-box;padding:var(--van-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-empty__image{width:var(--van-empty-image-size);height:var(--van-empty-image-size)}.van-empty__image img{width:100%;height:100%}.van-empty__description{margin-top:var(--van-empty-description-margin-top);padding:var(--van-empty-description-padding);color:var(--van-empty-description-color);font-size:var(--van-empty-description-font-size);line-height:var(--van-empty-description-line-height)}.van-empty__bottom{margin-top:var(--van-empty-bottom-margin-top)}.van-theme-dark .van-empty{opacity:.5}:root,:host{--van-notice-bar-height:40px;--van-notice-bar-padding:0 var(--van-padding-md);--van-notice-bar-wrapable-padding:var(--van-padding-xs) var(--van-padding-md);--van-notice-bar-text-color:var(--van-orange-dark);--van-notice-bar-font-size:var(--van-font-size-md);--van-notice-bar-line-height:24px;--van-notice-bar-background:var(--van-orange-light);--van-notice-bar-icon-size:16px;--van-notice-bar-icon-min-width:24px}.van-notice-bar{height:var(--van-notice-bar-height);padding:var(--van-notice-bar-padding);color:var(--van-notice-bar-text-color);font-size:var(--van-notice-bar-font-size);line-height:var(--van-notice-bar-line-height);background:var(--van-notice-bar-background);align-items:center;display:flex;position:relative}.van-notice-bar__left-icon,.van-notice-bar__right-icon{min-width:var(--van-notice-bar-icon-min-width);font-size:var(--van-notice-bar-icon-size)}.van-notice-bar__right-icon{text-align:right;cursor:pointer}.van-notice-bar__wrap{flex:1;align-items:center;height:100%;display:flex;position:relative;overflow:hidden}.van-notice-bar__content{white-space:nowrap;transition-timing-function:linear;position:absolute}.van-notice-bar__content.van-ellipsis{max-width:100%}.van-notice-bar--wrapable{height:auto;padding:var(--van-notice-bar-wrapable-padding)}.van-notice-bar--wrapable .van-notice-bar__wrap{height:auto}.van-notice-bar--wrapable .van-notice-bar__content{white-space:normal;word-wrap:break-word;position:relative}:root,:host{--van-skeleton-title-width:40%}.van-skeleton-title{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-title--round{border-radius:var(--van-radius-max)}.van-skeleton-title{width:var(--van-skeleton-title-width);margin:0}.van-skeleton-title+.van-skeleton-paragraph{margin-top:20px}:root,:host{--van-skeleton-avatar-size:32px;--van-skeleton-avatar-background:var(--van-active-color)}.van-skeleton-avatar{width:var(--van-skeleton-avatar-size);height:var(--van-skeleton-avatar-size);margin-right:var(--van-padding-md);background:var(--van-skeleton-avatar-background);flex-shrink:0}.van-skeleton-avatar--round{border-radius:var(--van-radius-max)}.van-skeleton-avatar+.van-skeleton__content{padding-top:var(--van-padding-xs)}:root,:host{--van-skeleton-paragraph-height:16px;--van-skeleton-paragraph-background:var(--van-active-color);--van-skeleton-paragraph-margin-top:var(--van-padding-sm)}.van-skeleton-paragraph{height:var(--van-skeleton-paragraph-height);background:var(--van-skeleton-paragraph-background)}.van-skeleton-paragraph--round{border-radius:var(--van-radius-max)}.van-skeleton-paragraph:not(:first-child){margin-top:var(--van-skeleton-paragraph-margin-top)}:root,:host{--van-skeleton-duration:1.2s}.van-skeleton{padding:0 var(--van-padding-md);display:flex}.van-skeleton__content{width:100%}.van-skeleton--animate{animation:van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite}@keyframes van-skeleton-blink{50%{opacity:.6}}:root,:host{--van-action-bar-background:var(--van-background-2);--van-action-bar-height:50px}.van-action-bar{box-sizing:content-box;height:var(--van-action-bar-height);background:var(--van-action-bar-background);align-items:center;display:flex;position:fixed;bottom:0;left:0;right:0}:root,:host{--van-action-bar-button-height:40px;--van-action-bar-button-warning-color:var(--van-gradient-orange);--van-action-bar-button-danger-color:var(--van-gradient-red)}.van-action-bar-button{height:var(--van-action-bar-button-height);font-weight:var(--van-font-bold);font-size:var(--van-font-size-md);border:none;border-radius:0;flex:1}.van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max);margin-left:5px}.van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max);margin-right:5px}.van-action-bar-button--warning{background:var(--van-action-bar-button-warning-color)}.van-action-bar-button--danger{background:var(--van-action-bar-button-danger-color)}@media (max-width:321px){.van-action-bar-button{font-size:13px}}:root,:host{--van-dialog-width:320px;--van-dialog-small-screen-width:90%;--van-dialog-font-size:var(--van-font-size-lg);--van-dialog-transition:var(--van-duration-base);--van-dialog-radius:16px;--van-dialog-background:var(--van-background-2);--van-dialog-header-font-weight:var(--van-font-bold);--van-dialog-header-line-height:24px;--van-dialog-header-padding-top:26px;--van-dialog-header-isolated-padding:var(--van-padding-lg) 0;--van-dialog-message-padding:var(--van-padding-lg);--van-dialog-message-font-size:var(--van-font-size-md);--van-dialog-message-line-height:var(--van-line-height-md);--van-dialog-message-max-height:60vh;--van-dialog-has-title-message-text-color:var(--van-gray-7);--van-dialog-has-title-message-padding-top:var(--van-padding-xs);--van-dialog-button-height:48px;--van-dialog-round-button-height:36px;--van-dialog-confirm-button-text-color:var(--van-primary-color)}.van-dialog{width:var(--van-dialog-width);font-size:var(--van-dialog-font-size);background:var(--van-dialog-background);border-radius:var(--van-dialog-radius);-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:var(--van-dialog-transition);transition-property:transform,opacity;top:45%;overflow:hidden}@media (max-width:321px){.van-dialog{width:var(--van-dialog-small-screen-width)}}.van-dialog__header{color:var(--van-text-color);padding-top:var(--van-dialog-header-padding-top);font-weight:var(--van-dialog-header-font-weight);line-height:var(--van-dialog-header-line-height);text-align:center}.van-dialog__header--isolated{padding:var(--van-dialog-header-isolated-padding)}.van-dialog__content--isolated{align-items:center;min-height:104px;display:flex}.van-dialog__message{color:var(--van-text-color);max-height:var(--van-dialog-message-max-height);padding:26px var(--van-dialog-message-padding);font-size:var(--van-dialog-message-font-size);line-height:var(--van-dialog-message-line-height);white-space:pre-wrap;text-align:center;word-wrap:break-word;-webkit-overflow-scrolling:touch;flex:1;overflow-y:auto}.van-dialog__message--has-title{padding-top:var(--van-dialog-has-title-message-padding-top);color:var(--van-dialog-has-title-message-text-color)}.van-dialog__message--left{text-align:left}.van-dialog__message--right{text-align:right}.van-dialog__message--justify{text-align:justify}.van-dialog__footer{-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;overflow:hidden}.van-dialog__confirm,.van-dialog__cancel{height:var(--van-dialog-button-height);border:0;border-radius:0;flex:1;margin:0}.van-dialog__confirm,.van-dialog__confirm:active{color:var(--van-dialog-confirm-button-text-color)}.van-dialog--round-button .van-dialog__footer{height:auto;padding:var(--van-padding-xs) var(--van-padding-lg) var(--van-padding-md);position:relative}.van-dialog--round-button .van-dialog__message{padding-bottom:var(--van-padding-md);color:var(--van-text-color)}.van-dialog--round-button .van-dialog__confirm,.van-dialog--round-button .van-dialog__cancel{height:var(--van-dialog-round-button-height)}.van-dialog--round-button .van-dialog__confirm{color:var(--van-white)}.van-dialog--round-button .van-action-bar-button--first{border-top-left-radius:var(--van-radius-max);border-bottom-left-radius:var(--van-radius-max)}.van-dialog--round-button .van-action-bar-button--last{border-top-right-radius:var(--van-radius-max);border-bottom-right-radius:var(--van-radius-max)}.van-dialog-bounce-enter-from{opacity:0;transform:translateY(-50%)scale(.7)}.van-dialog-bounce-leave-active{opacity:0;transform:translateY(-50%)scale(.9)}:root,:host{--van-toast-max-width:70%;--van-toast-font-size:var(--van-font-size-md);--van-toast-text-color:var(--van-white);--van-toast-loading-icon-color:var(--van-white);--van-toast-line-height:var(--van-line-height-md);--van-toast-radius:var(--van-radius-lg);--van-toast-background:#000000b3;--van-toast-icon-size:36px;--van-toast-text-min-width:96px;--van-toast-text-padding:var(--van-padding-xs) var(--van-padding-sm);--van-toast-default-padding:var(--van-padding-md);--van-toast-default-width:88px;--van-toast-default-min-height:88px;--van-toast-position-top-distance:20%;--van-toast-position-bottom-distance:20%}.van-toast{box-sizing:content-box;transition:all var(--van-duration-fast);width:var(--van-toast-default-width);max-width:var(--van-toast-max-width);min-height:var(--van-toast-default-min-height);padding:var(--van-toast-default-padding);color:var(--van-toast-text-color);font-size:var(--van-toast-font-size);line-height:var(--van-toast-line-height);white-space:pre-wrap;word-break:break-all;text-align:center;background:var(--van-toast-background);border-radius:var(--van-toast-radius);flex-direction:column;justify-content:center;align-items:center;display:flex}.van-toast--break-normal{word-break:normal;word-wrap:normal}.van-toast--break-word{word-break:normal;word-wrap:break-word}.van-toast--unclickable{cursor:not-allowed;overflow:hidden}.van-toast--unclickable *{pointer-events:none}.van-toast--text,.van-toast--html{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;min-width:var(--van-toast-text-min-width);min-height:0;padding:var(--van-toast-text-padding)}.van-toast--text .van-toast__text,.van-toast--html .van-toast__text{margin-top:0}.van-toast--top{top:var(--van-toast-position-top-distance)}.van-toast--bottom{top:auto;bottom:var(--van-toast-position-bottom-distance)}.van-toast__icon{font-size:var(--van-toast-icon-size)}.van-toast__loading{padding:var(--van-padding-base);color:var(--van-toast-loading-icon-color)}.van-toast__text{margin-top:var(--van-padding-xs)}:root{--app-bg:#f4f7fd;--app-surface:#fff;--app-surface-2:#f6f8fe;--app-border:#6e89c92e;--app-text:#1d315f;--app-text-2:#3f527d;--app-muted:#7b89ab;--app-primary:#2f6fff;--app-primary-soft:#2f6fff1a;--app-success:#0f9e69;--app-success-soft:#13b77a1f;--app-warning:#c46f00;--app-warning-soft:#ff981d24;--app-danger:#e5383e;--app-danger-soft:#ff53581f;--app-muted-soft:#7b89ab24;--app-shadow:0 10px 28px #4e63a214;--app-radius:16px;--app-font:"PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;--app-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root{--app-bg:#0b1222;--app-surface:#131c31;--app-surface-2:#18223b;--app-border:#8ca0d229;--app-text:#e6ecfb;--app-text-2:#bcc6e0;--app-muted:#8b97b6;--app-primary:#6f9bff;--app-primary-soft:#5b8cff29;--app-success:#2fd19a;--app-success-soft:#2fd19a24;--app-warning:#ffb45a;--app-warning-soft:#ffae4a24;--app-danger:#ff7a7f;--app-danger-soft:#ff6b7026;--app-muted-soft:#8390b029;--app-shadow:0 10px 28px #00000047;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*,:before,:after{box-sizing:border-box}html,body{background:var(--app-bg);color:var(--app-text);font-family:var(--app-font);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body,#app{min-height:100vh;min-height:100dvh}button,input,textarea{font-family:inherit}.app-card{background:var(--app-surface);border:1px solid var(--app-border);border-radius:var(--app-radius);box-shadow:var(--app-shadow)}.app-muted{color:var(--app-muted)}.app-mono{font-family:var(--app-mono);word-break:break-all;font-size:12px}.tone-success{color:var(--app-success)}.tone-warning{color:var(--app-warning)}.tone-danger{color:var(--app-danger)}.tone-muted{color:var(--app-muted)}.tone-primary{color:var(--app-primary)}.van-nav-bar .van-nav-bar__title{font-weight:600}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js new file mode 100644 index 000000000..b4bf235cd --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/index-Gkjcdjlo.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/LoginView-nW7aCAJ5.js","assets/_plugin-vue_export-helper-CT0ZqwFS.js","assets/cell-group-B1PakqJq.js","assets/field-CRX3eS7h.js","assets/LoginView-C25eERA6.css","assets/ShellView-CQXCC9xU.js","assets/use-placeholder-CcVnlsb4.js","assets/ShellView-D354hIv5.css","assets/DevicesView-Cgb9V9Z0.js","assets/format-DEXJ_MrY.js","assets/AccountSwitcher-5FuHazzf.js","assets/AccountSwitcher-BZXg15MH.css","assets/DevicesView-CHTvxQxK.css","assets/ConversationsView-CLYafisP.js","assets/ConversationsView-CytDMSuM.css","assets/MeView-uJ3uOzy0.js","assets/MeView-B-MK2tgf.css"])))=>i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>S(e)===`[object Map]`,p=e=>S(e)===`[object Set]`,m=e=>S(e)===`[object Date]`,h=e=>S(e)===`[object RegExp]`,g=e=>typeof e==`function`,_=e=>typeof e==`string`,v=e=>typeof e==`symbol`,y=e=>typeof e==`object`&&!!e,b=e=>(y(e)||g(e))&&g(e.then)&&g(e.catch),x=Object.prototype.toString,S=e=>x.call(e),C=e=>S(e).slice(8,-1),w=e=>S(e)===`[object Object]`,T=e=>_(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,ee=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),E=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},te=/-\w/g,D=E(e=>e.replace(te,e=>e.slice(1).toUpperCase())),ne=/\B([A-Z])/g,O=E(e=>e.replace(ne,`-$1`).toLowerCase()),re=E(e=>e.charAt(0).toUpperCase()+e.slice(1)),ie=E(e=>e?`on${re(e)}`:``),k=(e,t)=>!Object.is(e,t),A=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},oe=e=>{let t=parseFloat(e);return isNaN(t)?e:t},se=e=>{let t=_(e)?Number(e):NaN;return isNaN(t)?e:t},ce,le=()=>ce||(ce=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{});function ue(e){if(d(e)){let t={};for(let n=0;n{if(e){let n=e.split(fe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function he(e){let t=``;if(_(e))t=e;else if(d(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Se=e=>_(e)?e:e==null?``:d(e)||y(e)&&(e.toString===x||!g(e.toString))?xe(e)?Se(e.value):JSON.stringify(e,Ce,2):String(e),Ce=(e,t)=>xe(t)?Ce(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[we(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>we(e))}:v(t)?we(t):y(t)&&!d(t)&&!w(t)?String(t):t,we=(e,t=``)=>{var n;return v(e)?`Symbol(${(n=e.description)==null?t:n})`:e},j,Te=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&j&&(j.active?(this.parent=j,this.index=(j.scopes||(j.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes){let n=this.scopes.slice();for(e=0,t=n.length;e0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(je){let e=je;for(je=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ae;){let t=Ae;for(Ae=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}function Fe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ie(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),ze(r),Be(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Re(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Re(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ke)||(e.globalVersion=Ke,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Le(e))))return;e.flags|=2;let t=e.dep,n=M,r=Ve;M=e,Ve=!0;try{Fe(e);let n=e.fn(e._value);(t.version===0||k(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{M=n,Ve=r,Ie(e),e.flags&=-3}}function ze(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ze(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Be(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var Ve=!0,He=[];function Ue(){He.push(Ve),Ve=!1}function We(){let e=He.pop();Ve=e===void 0||e}function Ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;try{t()}finally{M=e}}}var Ke=0,qe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Je=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!M||!Ve||M===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==M)t=this.activeLink=new qe(M,this),M.deps?(t.prevDep=M.depsTail,M.depsTail.nextDep=t,M.depsTail=t):M.deps=M.depsTail=t,Ye(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=M.depsTail,t.nextDep=void 0,M.depsTail.nextDep=t,M.depsTail=t,M.deps===t&&(M.deps=e)}return t}trigger(e){this.version++,Ke++,this.notify(e)}notify(e){Ne();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pe()}}};function Ye(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Ye(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var Xe=new WeakMap,Ze=Symbol(``),Qe=Symbol(``),$e=Symbol(``);function N(e,t,n){if(Ve&&M){let t=Xe.get(e);t||Xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Je),r.map=t,r.key=n),r.track()}}function et(e,t,n,r,i,a){let o=Xe.get(e);if(!o){Ke++;return}let s=e=>{e&&e.trigger()};if(Ne(),t===`clear`)o.forEach(s);else{let i=d(e),a=i&&T(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===$e||!v(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get($e)),t){case`add`:i?a&&s(o.get(`length`)):(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`delete`:i||(s(o.get(Ze)),f(e)&&s(o.get(Qe)));break;case`set`:f(e)&&s(o.get(Ze))}}Pe()}function tt(e){let t=F(e);return t===e?t:(N(t,`iterate`,$e),P(e)?t:t.map(Ht))}function nt(e){return N(e=F(e),`iterate`,$e),e}function rt(e,t){return zt(e)?Ut(Rt(e)?Ht(t):t):Ht(t)}var it={__proto__:null,[Symbol.iterator](){return at(this,Symbol.iterator,e=>rt(this,e))},concat(...e){return tt(this).concat(...e.map(e=>d(e)?tt(e):e))},entries(){return at(this,`entries`,e=>(e[1]=rt(this,e[1]),e))},every(e,t){return st(this,`every`,e,t,void 0,arguments)},filter(e,t){return st(this,`filter`,e,t,e=>e.map(e=>rt(this,e)),arguments)},find(e,t){return st(this,`find`,e,t,e=>rt(this,e),arguments)},findIndex(e,t){return st(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return st(this,`findLast`,e,t,e=>rt(this,e),arguments)},findLastIndex(e,t){return st(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return st(this,`forEach`,e,t,void 0,arguments)},includes(...e){return lt(this,`includes`,e)},indexOf(...e){return lt(this,`indexOf`,e)},join(e){return tt(this).join(e)},lastIndexOf(...e){return lt(this,`lastIndexOf`,e)},map(e,t){return st(this,`map`,e,t,void 0,arguments)},pop(){return ut(this,`pop`)},push(...e){return ut(this,`push`,e)},reduce(e,...t){return ct(this,`reduce`,e,t)},reduceRight(e,...t){return ct(this,`reduceRight`,e,t)},shift(){return ut(this,`shift`)},some(e,t){return st(this,`some`,e,t,void 0,arguments)},splice(...e){return ut(this,`splice`,e)},toReversed(){return tt(this).toReversed()},toSorted(e){return tt(this).toSorted(e)},toSpliced(...e){return tt(this).toSpliced(...e)},unshift(...e){return ut(this,`unshift`,e)},values(){return at(this,`values`,e=>rt(this,e))}};function at(e,t,n){let r=nt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var ot=Array.prototype;function st(e,t,n,r,i,a){let o=nt(e),s=o!==e&&!P(e),c=o[t];if(c!==ot[t]){let t=c.apply(e,a);return s?Ht(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,rt(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function ct(e,t,n,r){let i=nt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=rt(e,t)),n.call(this,t,rt(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?rt(e,c):c}function lt(e,t,n){let r=F(e);N(r,`iterate`,$e);let i=r[t](...n);return(i===-1||i===!1)&&Bt(n[0])?(n[0]=F(n[0]),r[t](...n)):i}function ut(e,t,n=[]){Ue(),Ne();let r=F(e)[t].apply(e,n);return Pe(),We(),r}var dt=e(`__proto__,__v_isRef,__isVue`),ft=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(v));function pt(e){v(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.hasOwnProperty(e)}var mt=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?Mt:jt:i?At:kt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=it[t]))return e;if(t===`hasOwnProperty`)return pt}let o=Reflect.get(e,t,I(e)?e:n);if((v(t)?ft.has(t):dt(t))||(r||N(e,`get`,t),i))return o;if(I(o)){let e=a&&T(t)?o:o.value;return r&&y(e)?It(e):e}return y(o)?r?It(o):Pt(o):o}},ht=class extends mt{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&T(t);if(!this._isShallow){let e=zt(i);if(!P(n)&&!zt(n)&&(i=F(i),n=F(n)),!a&&I(i)&&!I(n))return e||(i.value=n),!0}let o=a?Number(t)e,xt=e=>Reflect.getPrototypeOf(e);function St(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a),c=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?bt:t?Ut:Ht;return!t&&N(a,`iterate`,l?Qe:Ze),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Ct(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function wt(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(k(n,a)&&N(i,`get`,n),N(i,`get`,a));let{has:o}=xt(i),s=t?bt:e?Ut:Ht;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(F(t),`iterate`,Ze),t.size},has(t){let n=this.__v_raw,r=F(n),i=F(t);return e||(k(t,i)&&N(r,`has`,t),N(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=F(a),s=t?bt:e?Ut:Ht;return!e&&N(o,`iterate`,Ze),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Ct(`add`),set:Ct(`set`),delete:Ct(`delete`),clear:Ct(`clear`)}:{add(e){let n=F(this),r=xt(n),i=F(e),a=!t&&!P(e)&&!zt(e)?i:e;return r.has.call(n,a)||k(e,a)&&r.has.call(n,e)||k(i,a)&&r.has.call(n,i)||(n.add(a),et(n,`add`,a,a)),this},set(e,n){!t&&!P(n)&&!zt(n)&&(n=F(n));let r=F(this),{has:i,get:a}=xt(r),o=i.call(r,e);o||(e=F(e),o=i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?k(n,s)&&et(r,`set`,e,n,s):et(r,`add`,e,n),this},delete(e){let t=F(this),{has:n,get:r}=xt(t),i=n.call(t,e);i||(e=F(e),i=n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&et(t,`delete`,e,void 0,a),o},clear(){let e=F(this),t=e.size!==0,n=e.clear();return t&&et(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=St(r,e,t)}),n}function Tt(e,t){let n=wt(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var Et={get:Tt(!1,!1)},Dt={get:Tt(!1,!0)},Ot={get:Tt(!0,!1)},kt=new WeakMap,At=new WeakMap,jt=new WeakMap,Mt=new WeakMap;function Nt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function Pt(e){return zt(e)?e:Lt(e,!1,_t,Et,kt)}function Ft(e){return Lt(e,!1,yt,Dt,At)}function It(e){return Lt(e,!0,vt,Ot,jt)}function Lt(e,t,n,r,i){if(!y(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let a=i.get(e);if(a)return a;let o=Nt(C(e));if(o===0)return e;let s=new Proxy(e,o===2?r:n);return i.set(e,s),s}function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function P(e){return!!(e&&e.__v_isShallow)}function Bt(e){return e?!!e.__v_raw:!1}function F(e){let t=e&&e.__v_raw;return t?F(t):e}function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&ae(e,`__v_skip`,!0),e}var Ht=e=>y(e)?Pt(e):e,Ut=e=>y(e)?It(e):e;function I(e){return e?e.__v_isRef===!0:!1}function Wt(e){return Kt(e,!1)}function Gt(e){return Kt(e,!0)}function Kt(e,t){return I(e)?e:new qt(e,t)}var qt=class{constructor(e,t){this.dep=new Je,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:F(e),this._value=t?e:Ht(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?e:F(e),k(e,t)&&(this._rawValue=e,this._value=n?e:Ht(e),this.dep.trigger())}};function L(e){return I(e)?e.value:e}var Jt={get:(e,t,n)=>t===`__v_raw`?e:L(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return I(i)&&!I(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return Rt(e)?e:new Proxy(e,Jt)}var Xt=class{constructor(e){this.__v_isRef=!0,this._value=void 0;let t=this.dep=new Je,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}};function Zt(e){return new Xt(e)}var Qt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Je(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ke-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return Me(this,!0),!0}get value(){let e=this.dep.track();return Re(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function $t(e,t,n=!1){let r,i;return g(e)?r=e:(r=e.get,i=e.set),new Qt(r,i,n)}var en={},tn=new WeakMap,nn=void 0;function rn(e,t=!1,n=nn){if(n){let t=tn.get(n);t||tn.set(n,t=[]),t.push(e)}}function an(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:P(e)||o===!1||o===0?on(e,1):on(e),m,h,_,v,y=!1,b=!1;if(I(e)?(h=()=>e.value,y=P(e)):Rt(e)?(h=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Rt(e)||P(e)),h=()=>e.map(e=>{if(I(e))return e.value;if(Rt(e))return p(e);if(g(e))return f?f(e,2):e()})):h=g(e)?n?f?()=>f(e,2):e:()=>{if(_){Ue();try{_()}finally{We()}}let t=nn;nn=m;try{return f?f(e,3,[v]):e(v)}finally{nn=t}}:r,n&&o){let e=h,t=o===!0?1/0:o;h=()=>on(e(),t)}let x=Ee(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{let n=e(...t);return S(),n}}let C=b?Array(e.length).fill(en):en,w=e=>{if(m.flags&1&&(m.dirty||e)){if(n){let t=m.run();if(e||o||y||(b?t.some((e,t)=>k(e,C[t])):k(t,C))){_&&_();let e=nn;nn=m;try{let e=[t,C===en?void 0:b&&C[0]===en?[]:C,v];C=t,f?f(n,3,e):n(...e)}finally{nn=e}}}else m.run()}};return u&&u(w),m=new Oe(h),m.scheduler=l?()=>l(w,!1):w,v=e=>rn(e,!1,m),_=m.onStop=()=>{let e=tn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();tn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function on(e,t=1/0,n){if(t<=0||!y(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,I(e))on(e.value,t,n);else if(d(e))for(let r=0;r{on(e,t,n)});else if(w(e)){for(let r in e)on(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&on(e[r],t,n)}return e}function sn(e,t,n,r){try{return r?e(...r):e()}catch(e){cn(e,t,n)}}function R(e,t,n,r){if(g(e)){let i=sn(e,t,n,r);return i&&b(i)&&i.catch(e=>{cn(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a>>1,i=z[r],a=Cn(i);a=Cn(n)?z.push(e):z.splice(_n(t),0,e),e.flags|=1,yn()}}function yn(){hn||(hn=mn.then(wn))}function bn(e){if(!d(e))fn&&e.id===-1?fn.splice(pn+1,0,e):e.flags&1||(dn.push(e),e.flags|=1);else for(let t=0;tCn(e)-Cn(t));if(dn.length=0,fn){for(let t=0;te.id==null?e.flags&2?-1:1/0:e.id;function wn(e){try{for(un=0;un{r._d&&pa(-1);let i=En(t),a=la.length,o;try{o=e(...n)}finally{for(let e=la.length;e>a;e--)da();En(i),r._d&&pa(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function On(e,n){if(B===null)return e;let r=qa(B),i=e.dirs||(e.dirs=[]);for(let e=0;e1)return n&&g(t)?t.call(r&&r.proxy):t}}var jn=Symbol.for(`v-scx`),Mn=()=>V(jn);function Nn(e,t){return In(e,null,t)}function Pn(e,t){return In(e,null,{flush:`sync`})}function Fn(e,t,n){return In(e,t,n)}function In(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ba){if(c===`sync`){let e=Mn();f=e.__watcherHandles||(e.__watcherHandles=[])}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=X;u.call=(e,t,n)=>R(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{G(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():vn(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=an(e,n,u);return Ba&&(f?f.push(h):d&&h()),h}function Ln(e,t,n){let r=this.proxy,i=_(e)?e.includes(`.`)?Rn(r,e):()=>r[e]:e.bind(r,r),a;g(t)?a=t:(a=t.handler,n=t);let o=La(this),s=In(i,a.bind(r),n);return o(),s}function Rn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;ee.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===``),Un=e=>e&&(e.defer||e.defer===``),Wn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Gn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Kn=(e,t)=>{let n=e&&e.to;return _(n)?t?t(n):null:n},qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g,parentNode:_}}=l,v=Hn(t.props),{dynamicChildren:y}=t,b=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},x=(e=t)=>{let n=Hn(e.props),r=e.target=Kn(e.props,m),a=Qn(r,e,h,p);r&&(o!==`svg`&&Wn(r)?o=`svg`:o!==`mathml`&&Gn(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(b(e,r,a),Zn(e,!1)))},S=e=>{let t=()=>{if(zn.get(e)===t){if(zn.delete(e),Hn(e.props)){let t=_(e.el)||n;b(e,t,e.anchor),Zn(e,!0)}x(e)}};zn.set(e,t),G(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Un(t.props)||a&&a.pendingBranch){S(t);return}v&&(b(t,n,i),Zn(t,!0)),x()}else{t.el=e.el;let r=t.anchor=e.anchor,u=zn.get(e);if(u){u.flags|=8,zn.delete(e),S(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Hn(e.props),_=g?n:p,b=g?r:h;if(o===`svg`||Wn(p)?o=`svg`:(o===`mathml`||Gn(p))&&(o=`mathml`),y?(f(e.dynamicChildren,y,_,i,a,o,s),$i(e,t,!0)):c||d(e,t,_,b,i,a,o,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Jn(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=Kn(t.props,m);e&&(t.target=e,Jn(t,e,null,l,0))}else g&&Jn(t,p,h,l,1);Zn(t,v)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=Hn(f),m=a||!p,h=zn.get(e);if(h&&(h.flags|=8,zn.delete(e)),d&&(i(l),i(u)),a&&i(c),!h&&(p||d)&&o&16)for(let e=0;e{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}var U=[Function,Array],tr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:U,onEnter:U,onAfterEnter:U,onEnterCancelled:U,onBeforeLeave:U,onLeave:U,onAfterLeave:U,onLeaveCancelled:U,onBeforeAppear:U,onAppear:U,onAfterAppear:U,onAppearCancelled:U},nr=e=>{let t=e.subTree;return t.component?nr(t.component):t},rr={name:`BaseTransition`,props:tr,setup(e,{slots:t}){let n=Pa(),r=er();return()=>{let i=t.default&&dr(t.default(),!0),a=i&&i.length?ir(i):n.subTree?Ea():void 0;if(!a)return;let o=F(e),{mode:s}=o;if(r.isLeaving)return cr(a);let c=lr(a);if(!c)return cr(a);let l=sr(c,o,r,n,e=>l=e);c.type!==K&&ur(c,l);let u=n.subTree&&lr(n.subTree);if(u&&u.type!==K&&!va(u,c)&&nr(n).type!==K){let e=sr(u,o,r,n);if(ur(u,e),s===`out-in`&&c.type!==K)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},cr(a);s===`in-out`&&c.type!==K?e.delayLeave=(e,t,n)=>{let i=or(r,u);i[String(u.key)]=u,e[H]=()=>{t(),e[H]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return a}}};function ir(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==K){t=n;break}}return t}var ar=rr;function or(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function sr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=or(n,e),C=(e,t)=>{e&&R(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted){if(a)r=_||c;else return}t[H]&&t[H](!0);let i=S[x];i&&va(e,i)&&i.el[H]&&i.el[H](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted){if(a)r=v||l,i=y||u,o=b||f;else return}let s=!1;t[$n]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[$n]=void 0)};let c=t[$n].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[$n]&&t[$n](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[H]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[H]=void 0,S[i]===e&&delete S[i])};let o=t[H].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=sr(e,t,n,r,i);return i&&i(a),a}};return T}function cr(e){if(yr(e))return e=wa(e),e.children=null,e}function lr(e){if(!yr(e))return Vn(e.type)&&e.children?ir(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&g(n.default))return n.default()}}function ur(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;let n=e.component.subTree;ur(Vn(n.type)&&lr(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function dr(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;egr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(vr(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&gr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?qa(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,h=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=F(v),b=v===t?i:e=>!mr(h,e)&&u(y,e),x=(e,t)=>!(t&&mr(h,t));if(m!=null&&m!==p){if(_r(n),_(m))h[m]=null,b(m)&&(v[m]=null);else if(I(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(h[e.k]=null)}}if(g(p))sn(p,f,12,[l,h]);else{let t=_(p),n=I(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:h[p]:x(p)||!e.k?p.value:h[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)h[p]=[s],b(p)&&(v[p]=h[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(h[e.k]=t)}}else t?(h[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(h[e.k]=l))};if(l){let t=()=>{i(),hr.delete(e)};t.id=-1,hr.set(e,t),G(t,r)}else _r(e),i()}}}function _r(e){let t=hr.get(e);t&&(t.flags|=8,hr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var vr=e=>!!e.type.__asyncLoader,yr=e=>e.type.__isKeepAlive,br={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=Pa(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),G(()=>{a.isDeactivated=!1,a.a&&A(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Y(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;na(t.m),na(t.a),l(e,f,null,1,s),G(()=>{t.da&&A(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Y(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Er(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=Ja(vr(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!va(t,o))?p(t):o&&Er(o),i.delete(e),a.delete(e)}Fn(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>xr(e,t)),t&&m(e=>!xr(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(ia(n.subTree.type)?G(()=>{i.set(g,Dr(n.subTree))},n.subTree.suspense):i.set(g,Dr(n.subTree)))};return jr(_),Nr(_),Pr(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Dr(t);if(e.type===i.type&&e.key===i.key){Er(i);let e=i.component.da;e&&G(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!_a(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Dr(r);if(s.type===K)return o=null,s;let c=s.type,l=Ja(vr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!xr(u,l))||d&&l&&xr(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=wa(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&ur(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,ia(r.type)?r:s}}};function xr(e,t){return d(e)?e.some(e=>xr(e,t)):_(e)?e.split(`,`).includes(t):h(e)?(e.lastIndex=0,e.test(t)):!1}function Sr(e,t){wr(e,`a`,t)}function Cr(e,t){wr(e,`da`,t)}function wr(e,t,n=X){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Or(t,r,n),n){let e=n.parent;for(;e&&e.parent;)yr(e.parent.vnode)&&Tr(r,t,n,e),e=e.parent}}function Tr(e,t,n,r){let i=Or(t,e,r,!0);Fr(()=>{c(r[t],i)},n)}function Er(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Dr(e){return e.shapeFlag&128?e.ssContent:e}function Or(e,t,n=X,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||(t.__weh=(...r)=>{Ue();let i=La(n),a=R(t,n,e,r);return i(),We(),a});return r?i.unshift(a):i.push(a),a}}var kr=e=>(t,n=X)=>{(!Ba||e===`sp`)&&Or(e,(...e)=>t(...e),n)},Ar=kr(`bm`),jr=kr(`m`),Mr=kr(`bu`),Nr=kr(`u`),Pr=kr(`bum`),Fr=kr(`um`),Ir=kr(`sp`),Lr=kr(`rtg`),Rr=kr(`rtc`);function zr(e,t=X){Or(`ec`,e,t)}var Br=`components`;function Vr(e,t){return Wr(Br,e,!0,t)||e}var Hr=Symbol.for(`v-ndc`);function Ur(e){return _(e)?Wr(Br,e,!1)||e:e||Hr}function Wr(e,t,n=!0,r=!1){let i=B||X;if(i){let n=i.type;if(e===Br){let e=Ja(n,!1);if(e&&(e===t||e===D(t)||e===re(D(t))))return n}let a=Gr(i[e]||n[e],t)||Gr(i.appContext[e],t);return!a&&r?n:a}}function Gr(e,t){return e&&(e[t]||e[D(t)]||e[re(D(t))])}function Kr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||_(e)){let n=o&&Rt(e),r=!1,s=!1;n&&(r=!P(e),s=zt(e),e=nt(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}var Jr=e=>e?za(e)?qa(e):Jr(e.parent):null,Yr=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{vn(e.update)}),$nextTick:e=>e.n||(e.n=gn.bind(e.proxy)),$watch:e=>Ln.bind(e)}),Xr=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),Zr={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(Xr(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else $r&&(s[n]=0)}let d=Yr[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return Xr(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||Xr(n,c)||u(o,c)||u(i,c)||u(Yr,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function Qr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var $r=!0;function ei(e){let t=ii(e),n=e.proxy,i=e.ctx;$r=!1,t.beforeCreate&&ni(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:h,updated:_,activated:v,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:E,errorCaptured:te,serverPrefetch:D,expose:ne,inheritAttrs:O,components:re,directives:ie,filters:k}=t;if(u&&ti(u,i,null),s)for(let e in s){let t=s[e];g(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);y(t)&&(e.data=Pt(t))}if($r=!0,o)for(let e in o){let t=o[e],a=Z({get:g(t)?t.bind(n,n):g(t.get)?t.get.bind(n,n):r,set:!g(t)&&g(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)ri(c[e],i,n,e);if(l){let e=g(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{An(t,e[t])})}f&&ni(f,e,`c`);function A(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(A(Ar,p),A(jr,m),A(Mr,h),A(Nr,_),A(Sr,v),A(Cr,b),A(zr,te),A(Rr,ee),A(Lr,E),A(Pr,S),A(Fr,w),A(Ir,D),d(ne)){if(ne.length){let t=e.exposed||(e.exposed={});ne.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={})}T&&e.render===r&&(e.render=T),O!=null&&(e.inheritAttrs=O),re&&(e.components=re),ie&&(e.directives=ie),D&&pr(e)}function ti(e,t,n=r){d(e)&&(e=li(e));for(let n in e){let r=e[n],i;i=y(r)?`default`in r?V(r.from||n,r.default,!0):V(r.from||n):V(r),I(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function ni(e,t,n){R(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function ri(e,t,n,r){let i=r.includes(`.`)?Rn(n,r):()=>n[r];if(_(e)){let n=t[e];g(n)&&Fn(i,n)}else if(g(e))Fn(i,e.bind(n));else if(y(e)){if(d(e))e.forEach(e=>ri(e,t,n,r));else{let r=g(e.handler)?e.handler.bind(n):t[e.handler];g(r)&&Fn(i,r,e)}}}function ii(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>ai(c,e,o,!0)),ai(c,t,o)),y(t)&&a.set(t,c),c}function ai(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&ai(e,a,n,!0),i&&i.forEach(t=>ai(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=oi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var oi={data:si,props:di,emits:di,methods:ui,computed:ui,beforeCreate:W,created:W,beforeMount:W,mounted:W,beforeUpdate:W,updated:W,beforeDestroy:W,beforeUnmount:W,destroyed:W,unmounted:W,activated:W,deactivated:W,errorCaptured:W,serverPrefetch:W,components:ui,directives:ui,watch:fi,provide:si,inject:ci};function si(e,t){return t?e?function(){return s(g(e)?e.call(this,this):e,g(t)?t.call(this,this):t)}:t:e}function ci(e,t){return ui(li(e),li(t))}function li(e){if(d(e)){let t={};for(let n=0;n{let l,u=t,d;return Pn(()=>{let t=e[a];k(l,t)&&(l=t,c())}),{get(){return s(),r.get?r.get(l):l},set(e){let s=r.set?r.set(e):e;if(!k(s,l)&&!(u!==t&&k(e,u)))return;let f=i.vnode.props,p=!!(f&&(n in f||a in f||o in f)&&(`onUpdate:${n}`in f||`onUpdate:${a}`in f||`onUpdate:${o}`in f));p||(l=e,c()),i.emit(`update:${n}`,s),k(e,u)&&(k(e,s)&&!k(s,d)||p&&u!==t&&!k(s,l))&&c(),u=e,d=s}}});return c[Symbol.iterator]=()=>{let e=0;return{next(){return e<2?{value:e++?s||t:c,done:!1}:{done:!0}}}},c}var vi=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${D(t)}Modifiers`]||e[`${O(t)}Modifiers`];function yi(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&vi(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>_(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(D(n))];!l&&o&&(l=i[c=ie(O(n))]),l&&R(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,R(u,e,6,a)}}var bi=new WeakMap;function xi(e,t,n=!1){let r=n?bi:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!g(e)){let r=e=>{let n=xi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(y(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),y(e)&&r.set(e,o),o)}function Si(e,t){return!e||!a(t)?!1:(t=t.slice(2),t=t===`Once`?t:t.replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,O(t))||u(e,t))}function Ci(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=En(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Da(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Da(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:wi(c)}}catch(t){la.length=0,cn(t,e,1),v=J(K)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ti(y,a)),b=wa(b,y,!1,!0))}return n.dirs&&(b=wa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&ur(Vn(b.type)&&lr(b)||b,n.transition),v=b,En(_),v}var wi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Ei(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Di(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;tObject.create(Ai),Mi=e=>Object.getPrototypeOf(e)===Ai;function Ni(e,t,n,r=!1){let i={},a=ji();e.propsDefaults=Object.create(null),Fi(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);e.props=n?r?i:Ft(i):e.type.props?i:a,e.attrs=a}function Pi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{p=!0;let[t,n]=Ri(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return y(e)&&a.set(e,n),n;if(d(c))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Vi=e=>d(e)?e.map(Da):[Da(e)],Hi=(e,t,n)=>{if(t._n)return t;let r=Dn((...e)=>Vi(t(...e)),n);return r._c=!1,r},Ui=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Bi(n))continue;let i=e[n];if(g(i))t[n]=Hi(n,i,r);else if(i!=null){let e=Vi(i);t[n]=()=>e}}},Wi=(e,t)=>{let n=Vi(t);e.slots.default=()=>n},Gi=(e,t,n)=>{for(let r in t)(n||!Bi(r))&&(e[r]=t[r])},Ki=(e,t,n)=>{let r=e.slots=ji();if(e.vnode.shapeFlag&32){let e=t._;e?(Gi(r,t,n),n&&ae(r,`_`,e,!0)):Ui(t,r)}else t&&Wi(e,t)},qi=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:Gi(a,n,r):(o=!n.$stable,Ui(n,a)),s=n}else n&&(Wi(e,n),s={default:1});if(o)for(let e in a)!Bi(e)&&s[e]==null&&delete a[e]},G=aa;function Ji(e){return Yi(e)}function Yi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!va(e,t)&&(r=ve(e),pe(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case sa:y(e,t,n,r);break;case K:b(e,t,n,r);break;case ca:e==null&&x(t,n,r,o);break;case oa:re(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?ie(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,xe)}u!=null&&i?gr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&gr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)T(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),D(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},T=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&te(e.children,d,null,r,i,Xi(e,a),s,u),_&&kn(e,null,r,`created`),E(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!ee(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Y(f,r,e)}_&&kn(e,null,r,`beforeMount`);let v=Qi(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&G(()=>{try{f&&Y(f,r,e),v&&g.enter(d),_&&kn(e,null,r,`mounted`)}finally{}},i)},E=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t{for(let l=c;l{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&Zi(r,!1),(g=h.onVnodeBeforeUpdate)&&Y(g,r,n,e),f&&kn(n,e,r,`beforeUpdate`),r&&Zi(r,!0),d&&(!e.dynamicChildren||e.dynamicChildren.length!==d.length)&&(u=0,s=!1,d=null),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?ne(e.dynamicChildren,d,l,r,i,Xi(n,a),o):s||ce(e,n,l,null,r,i,Xi(n,a),o,!1),u>0){if(u&16)O(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t{g&&Y(g,r,n,e),f&&kn(n,e,r,`updated`)},i)},ne=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(n!==r){if(n!==t)for(let t in n)!ee(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(ee(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},re=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),te(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(ne(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&$i(e,t,!0)):ce(e,t,n,f,i,a,s,c,l)},ie=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):ae(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Na(e,r,i);if(yr(e)&&(s.ctx.renderer=xe),Va(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,oe,o),!e.el){let r=s.subTree=J(K);b(null,r,t,n),e.placeholder=r.el}}else oe(s,e,t,n,i,a,o)},ae=(e,t,n)=>{let r=t.component=e.component;if(Ei(e,t,n)){if(r.asyncDep&&!r.asyncResolved){se(r,t,n);return}r.next=t,r.update()}else t.el=e.el,r.vnode=t},oe=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=ta(e);if(n){t&&(t.el=c.el,se(e,t,o)),n.asyncDep.then(()=>{G(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;Zi(e,!1),t?(t.el=c.el,se(e,t,o)):t=c,n&&A(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Y(d,s,t,c),Zi(e,!0);let f=Ci(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ve(p),e,i,a),t.el=f.el,u===null&&ki(e,f.el),r&&G(r,i),(d=t.props&&t.props.onVnodeUpdated)&&G(()=>Y(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=vr(t);if(Zi(e,!1),l&&A(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Y(o,d,t),Zi(e,!0),s&&Ce){let t=()=>{e.subTree=Ci(e),Ce(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Ci(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&G(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;G(()=>Y(o,d,e),i)}(t.shapeFlag&256||d&&vr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&G(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Oe(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>vn(u),Zi(e,!0),l()},se=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Pi(e,t.props,r,n),qi(e,t.children,n),Ue(),xn(e),We()},ce=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){de(l,d,n,r,i,a,o,s,c);return}if(f&256){ue(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&_e(l,i,a),d!==l&&p(n,d)):u&16?m&16?de(l,d,n,r,i,a,o,s,c):_e(l,i,a,!0):(u&8&&p(n,``),m&16&&te(d,n,r,i,a,o,s,c))},ue=(e,t,r,i,a,o,s,c,l)=>{e=e||n,t=t||n;let u=e.length,d=t.length,f=Math.min(u,d),p=0;for(;pd?_e(e,a,o,!0,!1,f):te(t,r,i,a,o,s,c,l,f)},de=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?Oa(t[u]):Da(t[u]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?Oa(t[p]):Da(t[p]);if(va(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=ep)for(;u<=f;)pe(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?Oa(t[u]):Da(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u=b){pe(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&va(n,t[_])){i=_;break}i===void 0?pe(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?ea(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){fe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,xe);return}if(c===oa){o(a,t,n);for(let e=0;el.enter(a),i));else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{let e=a._isLeaving||!!a[H];a._isLeaving&&a[H](!0),l.persisted&&!e?u():r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}}else o(a,t,n)},pe=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ue(),gr(s,null,n,e,!0),We()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!vr(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Y(_,t,e),u&6)ge(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&kn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,xe,r):l&&!l.hasOnce&&(a!==oa||d>0&&d&64)?_e(l,t,n,!1,!0):(a===oa&&d&384||!i&&u&16)&&_e(c,t,n),r&&me(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&G(()=>{_&&Y(_,t,e),h&&kn(e,null,t,`unmounted`),v&&(e.el=null)},n)},me=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===oa){he(n,r);return}if(t===ca){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},he=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},ge=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;na(c),na(l),r&&A(r),i.stop(),a&&(a.flags|=8,pe(o,e,t,n)),s&&G(s,t),G(()=>{e.isUnmounted=!0},t)},_e=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return ve(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Bn];return n?h(n):t},ye=!1,be=(e,t,n)=>{let r;e==null?t._vnode&&(pe(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ye||(ye=!0,xn(r),Sn(),ye=!1)},xe={p:v,um:pe,m:fe,r:me,mt:k,mc:te,pc:ce,pbc:ne,n:ve,o:e},Se,Ce;return i&&([Se,Ce]=i(xe)),{render:be,hydrate:Se,createApp:hi(be,Se)}}function Xi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function Zi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Qi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function $i(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-->0;)n[a]=o,o=t[o];return n}function ta(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ta(t)}function na(e){if(e)for(let t=0;te.__isSuspense;function aa(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):bn(e)}var oa=Symbol.for(`v-fgt`),sa=Symbol.for(`v-txt`),K=Symbol.for(`v-cmt`),ca=Symbol.for(`v-stc`),la=[],q=null;function ua(e=!1){la.push(q=e?null:[])}function da(){la.pop(),q=la[la.length-1]||null}var fa=1;function pa(e,t=!1){fa+=e,e<0&&q&&t&&(q.hasOnce=!0)}function ma(e){return e.dynamicChildren=fa>0?q||n:null,da(),fa>0&&q&&q.push(e),e}function ha(e,t,n,r,i,a){return ma(xa(e,t,n,r,i,a,!0))}function ga(e,t,n,r,i){return ma(J(e,t,n,r,i,!0))}function _a(e){return e?e.__v_isVNode===!0:!1}function va(e,t){return e.type===t.type&&e.key===t.key}var ya=({key:e})=>e==null?null:e,ba=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:_(e)||I(e)||g(e)?{i:B,r:e,k:t,f:!!n}:e);function xa(e,t=null,n=null,r=0,i=null,a=e===oa?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ya(t),ref:t&&ba(t),scopeId:Tn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:B};return s?(ka(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=_(n)?8:16),fa>0&&!o&&q&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&q.push(c),c}var J=Sa;function Sa(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Hr)&&(e=K),_a(e)){let r=wa(e,t,!0);return n&&ka(r,n),fa>0&&!a&&q&&(r.shapeFlag&6?q[q.indexOf(e)]=r:q.push(r)),r.patchFlag=-2,r}if(Ya(e)&&(e=e.__vccOpts),t){t=Ca(t);let{class:e,style:n}=t;e&&!_(e)&&(t.class=he(e)),y(n)&&(Bt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=_(e)?1:ia(e)?128:Vn(e)?64:y(e)?4:g(e)?2:0;return xa(e,t,n,r,i,o,a,!0)}function Ca(e){return e?Bt(e)||Mi(e)?s({},e):e:null}function wa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Aa(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ya(l),ref:t&&t.ref?n&&a?d(a)?a.concat(ba(t)):[a,ba(t)]:ba(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==oa?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&wa(e.ssContent),ssFallback:e.ssFallback&&wa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&ur(u,c.clone(u)),u}function Ta(e=` `,t=0){return J(sa,null,e,t)}function Ea(e=``,t=!1){return t?(ua(),ga(K,null,e)):J(K,null,e)}function Da(e){return e==null||typeof e==`boolean`?J(K):d(e)?J(oa,null,e.slice()):_a(e)?Oa(e):J(sa,null,String(e))}function Oa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:wa(e)}function ka(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`){if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),ka(e,n()),n._c&&(n._d=!0));return}{n=32;let r=t._;!r&&!Mi(t)?t._ctx=B:r===3&&B&&(B.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}}else if(g(t)){if(r&65){ka(e,{default:t});return}t={default:t,_ctx:B},n=32}else t=String(t),r&64?(n=16,t=[Ta(t)]):n=8;e.children=t,e.shapeFlag|=n}function Aa(...e){let t={};for(let n=0;nX||B,Fa,Ia;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Fa=t(`__VUE_INSTANCE_SETTERS__`,e=>X=e),Ia=t(`__VUE_SSR_SETTERS__`,e=>Ba=e)}var La=e=>{let t=X;return Fa(e),e.scope.on(),()=>{e.scope.off(),Fa(t)}},Ra=()=>{X&&X.scope.off(),Fa(null)};function za(e){return e.vnode.shapeFlag&4}var Ba=!1;function Va(e,t=!1,n=!1){t&&Ia(t);let{props:r,children:i}=e.vnode,a=za(e);Ni(e,r,a,t),Ki(e,i,n||t);let o=a?Ha(e,t):void 0;return t&&Ia(!1),o}function Ha(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zr);let{setup:r}=n;if(r){Ue();let n=e.setupContext=r.length>1?Ka(e):null,i=La(e),a=sn(r,e,0,[e.props,n]),o=b(a);if(We(),i(),(o||e.sp)&&!vr(e)&&pr(e),o){if(a.then(Ra,Ra),t)return a.then(n=>{Ia(!0);try{Ua(e,n,t)}finally{Ia(!1)}}).catch(t=>{cn(t,e,0)});e.asyncDep=a}else Ua(e,a,t)}else Wa(e,t)}function Ua(e,t,n){g(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:y(t)&&(e.setupState=Yt(t)),Wa(e,n)}function Wa(e,t,n){let i=e.type;e.render||(e.render=i.render||r);{let t=La(e);Ue();try{ei(e)}finally{We(),t()}}}var Ga={get(e,t){return N(e,`get`,``),e[t]}};function Ka(e){return{attrs:new Proxy(e.attrs,Ga),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function qa(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Vt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yr)return Yr[n](e)},has(e,t){return t in e||t in Yr}})):e.proxy}function Ja(e,t=!0){return g(e)?e.displayName||e.name:e.name||t&&e.__name}function Ya(e){return g(e)&&`__vccOpts`in e}var Z=(e,t)=>$t(e,t,Ba);function Xa(e,t,n){try{pa(-1);let r=arguments.length;return r===2?y(t)&&!d(t)?_a(t)?J(e,null,[t]):J(e,t):J(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_a(n)&&(n=[n]),J(e,t,n))}finally{pa(1)}}var Za=`3.5.41`,Qa=void 0,$a=typeof window<`u`&&window.trustedTypes;if($a)try{Qa=$a.createPolicy(`vue`,{createHTML:e=>e})}catch{}var eo=Qa?e=>Qa.createHTML(e):e=>e,to=`http://www.w3.org/2000/svg`,no=`http://www.w3.org/1998/Math/MathML`,ro=typeof document<`u`?document:null,io=ro&&ro.createElement(`template`),ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i=t===`svg`?ro.createElementNS(to,e):t===`mathml`?ro.createElementNS(no,e):n?ro.createElement(e,{is:n}):ro.createElement(e);return e===`select`&&r&&r.multiple!=null&&i.setAttribute(`multiple`,r.multiple),i},createText:e=>ro.createTextNode(e),createComment:e=>ro.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ro.querySelector(e),setScopeId(e,t){e.setAttribute(t,``)},insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;if(i&&(i===a||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==a&&(i=i.nextSibling););else{io.innerHTML=eo(r===`svg`?`${e}`:r===`mathml`?`${e}`:e);let i=io.content;if(r===`svg`||r===`mathml`){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},oo=`transition`,so=`animation`,co=Symbol(`_vtc`),lo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uo=s({},tr,lo),fo=(e=>(e.displayName=`Transition`,e.props=uo,e))((e,{slots:t})=>Xa(ar,ho(e),t)),po=(e,t=[])=>{d(e)?e.forEach(e=>e(...t)):e&&e(...t)},mo=e=>e?d(e)?e.some(e=>e.length>1):e.length>1:!1;function ho(e){let t={};for(let n in e)n in lo||(t[n]=e[n]);if(e.css===!1)return t;let{name:n=`v`,type:r,duration:i,enterFromClass:a=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=a,appearActiveClass:u=o,appearToClass:d=c,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,h=go(i),g=h&&h[0],_=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:C=v,onAppear:w=y,onAppearCancelled:T=b}=t,ee=(e,t,n,r)=>{e._enterCancelled=r,yo(e,t?d:c),yo(e,t?u:o),n&&n()},E=(e,t)=>{e._isLeaving=!1,yo(e,f),yo(e,m),yo(e,p),t&&t()},te=e=>(t,n)=>{let i=e?w:y,o=()=>ee(t,e,n);po(i,[t,o]),bo(()=>{yo(t,e?l:a),vo(t,e?d:c),mo(i)||So(t,r,g,o)})};return s(t,{onBeforeEnter(e){po(v,[e]),vo(e,a),vo(e,o)},onBeforeAppear(e){po(C,[e]),vo(e,l),vo(e,u)},onEnter:te(!1),onAppear:te(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>E(e,t);vo(e,f),e._enterCancelled?(vo(e,p),Eo(e)):(Eo(e),vo(e,p)),bo(()=>{e._isLeaving&&(yo(e,f),vo(e,m),mo(x)||So(e,r,_,n))}),po(x,[e,n])},onEnterCancelled(e){ee(e,!1,void 0,!0),po(b,[e])},onAppearCancelled(e){ee(e,!0,void 0,!0),po(T,[e])},onLeaveCancelled(e){E(e),po(S,[e])}})}function go(e){if(e==null)return null;if(y(e))return[_o(e.enter),_o(e.leave)];{let t=_o(e);return[t,t]}}function _o(e){return se(e)}function vo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[co]||(e[co]=new Set)).add(t)}function yo(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[co];n&&(n.delete(t),n.size||(e[co]=void 0))}function bo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}var xo=0;function So(e,t,n,r){let i=e._endId=++xo,a=()=>{i===e._endId&&r()};if(n!=null)return setTimeout(a,n);let{type:o,timeout:s,propCount:c}=Co(e,t);if(!o)return r();let l=o+`end`,u=0,d=()=>{e.removeEventListener(l,f),a()},f=t=>{t.target===e&&++u>=c&&d()};setTimeout(()=>{u(n[e]||``).split(`, `),i=r(`${oo}Delay`),a=r(`${oo}Duration`),o=wo(i,a),s=r(`${so}Delay`),c=r(`${so}Duration`),l=wo(s,c),u=null,d=0,f=0;t===oo?o>0&&(u=oo,d=o,f=a.length):t===so?l>0&&(u=so,d=l,f=c.length):(d=Math.max(o,l),u=d>0?o>l?oo:so:null,f=u?u===oo?a.length:c.length:0);let p=u===oo&&/\b(?:transform|all)(?:,|$)/.test(r(`${oo}Property`).toString());return{type:u,timeout:d,propCount:f,hasTransform:p}}function wo(e,t){for(;e.lengthTo(t)+To(e[n])))}function To(e){return e===`auto`?0:Number(e.slice(0,-1).replace(`,`,`.`))*1e3}function Eo(e){return(e?e.ownerDocument:document).body.offsetHeight}function Do(e,t,n){let r=e[co];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==null?e.removeAttribute(`class`):n?e.setAttribute(`class`,t):e.className=t}var Oo=Symbol(`_vod`),ko=Symbol(`_vsh`),Ao={name:`show`,beforeMount(e,{value:t},{transition:n}){e[Oo]=e.style.display===`none`?``:e.style.display,n&&t?n.beforeEnter(e):jo(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),jo(e,!0),r.enter(e)):r.leave(e,()=>{jo(e,!1)}):jo(e,t))},beforeUnmount(e,{value:t}){jo(e,t)}};function jo(e,t){e.style.display=t?e[Oo]:`none`,e[ko]=!t}var Mo=Symbol(``),No=/(?:^|;)\s*display\s*:/;function Po(e,t,n){let r=e.style,i=_(n),a=!1;if(n&&!i){if(t){if(_(t))for(let e of t.split(`;`)){let t=e.slice(0,e.indexOf(`:`)).trim();n[t]==null&&Io(r,t,``)}else for(let e in t)n[e]==null&&Io(r,e,``)}for(let i in n){i===`display`&&(a=!0);let o=n[i];o==null?Io(r,i,``):Bo(e,i,!_(t)&&t?t[i]:void 0,o)||Io(r,i,o)}}else if(i){if(t!==n){let e=r[Mo];e&&(n+=`;`+e),r.cssText=n,a=No.test(n)}}else t&&e.removeAttribute(`style`);Oo in e&&(e[Oo]=a?r.display:``,e[ko]&&(r.display=`none`))}var Fo=/\s*!important$/;function Io(e,t,n){if(d(n))n.forEach(n=>Io(e,t,n));else if(n==null&&(n=``),t.startsWith(`--`))e.setProperty(t,n);else{let r=zo(e,t);Fo.test(n)?e.setProperty(O(r),n.replace(Fo,``),`important`):e[r]=n}}var Lo=[`Webkit`,`Moz`,`ms`],Ro={};function zo(e,t){let n=Ro[t];if(n)return n;let r=D(t);if(r!==`filter`&&r in e)return Ro[t]=r;r=re(r);for(let n=0;nZo||(Qo.then(()=>Zo=0),Zo=Date.now());function es(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=n.attached)return;let r=n.value;if(d(r)){let n=e.stopImmediatePropagation;e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0};let i=r.slice(),a=[e];for(let n=0;ne.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ns=(e,t,n,r,i,s)=>{let c=i===`svg`;t===`class`?Do(e,r,c):t===`style`?Po(e,n,r):a(t)?o(t)||qo(e,t,n,r,s):(t[0]===`.`?(t=t.slice(1),1):t[0]===`^`?(t=t.slice(1),0):rs(e,t,r,c))?(Uo(e,t,r),!e.tagName.includes(`-`)&&(t===`value`||t===`checked`||t===`selected`)&&Ho(e,t,r,c,s,t!==`value`)):e._isVueCE&&(is(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!_(r)))?Uo(e,D(t),r,s,t):(t===`true-value`?e._trueValue=r:t===`false-value`&&(e._falseValue=r),Ho(e,t,r,c))};function rs(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t in e&&ts(t)&&g(n));if(t===`spellcheck`||t===`draggable`||t===`translate`||t===`autocorrect`||t===`sandbox`&&e.tagName===`IFRAME`||t===`form`||t===`list`&&e.tagName===`INPUT`||t===`type`&&e.tagName===`TEXTAREA`)return!1;if(t===`width`||t===`height`){let t=e.tagName;if(t===`IMG`||t===`VIDEO`||t===`CANVAS`||t===`SOURCE`)return!1}return ts(t)&&_(n)?!1:t in e}function is(e,t){let n=e._def.props;if(!n)return!1;let r=D(t);return Array.isArray(n)?n.some(e=>D(e)===r):Object.keys(n).some(e=>D(e)===r)}var as={esc:`escape`,space:` `,up:`arrow-up`,left:`arrow-left`,right:`arrow-right`,down:`arrow-down`,delete:`backspace`},os=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(`.`);return n[r]||(n[r]=(n=>{if(!(`key`in n))return;let r=O(n.key);if(t.some(e=>e===r||as[e]===r))return e(n)}))},ss=s({patchProp:ns},ao),cs;function ls(){return cs||(cs=Ji(ss))}var us=((...e)=>{let t=ls().createApp(...e),{mount:n}=t;return t.mount=e=>{let r=fs(e);if(!r)return;let i=t._component;!g(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent=``);let a=n(r,!1,ds(r));return r instanceof Element&&(r.removeAttribute(`v-cloak`),r.setAttribute(`data-v-app`,``)),a},t});function ds(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLElement==`function`&&e instanceof MathMLElement)return`mathml`}function fs(e){return _(e)?document.querySelector(e):e}function ps(){}var ms=Object.assign,hs=typeof window<`u`,gs=e=>typeof e==`object`&&!!e,_s=e=>e!=null,vs=e=>typeof e==`function`,ys=e=>gs(e)&&vs(e.then)&&vs(e.catch),bs=e=>typeof e==`number`||/^\d+(\.\d+)?$/.test(e),xs=()=>hs?/ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()):!1;function Ss(e,t){let n=t.split(`.`),r=e;return n.forEach(e=>{var t;r=gs(r)?(t=r[e])==null?``:t:``}),r}function Cs(e,t,n){return t.reduce((t,r)=>((!n||e[r]!==void 0)&&(t[r]=e[r]),t),{})}var ws=e=>Array.isArray(e)?e:[e],Ts=[Number,String],Es={type:Boolean,default:!0},Ds=e=>({type:e,required:!0}),Os=e=>({type:Number,default:e}),ks=e=>({type:Ts,default:e}),As=e=>({type:String,default:e}),js=typeof window<`u`;function Ms(e){return js?requestAnimationFrame(e):-1}function Ns(e){js&&cancelAnimationFrame(e)}function Ps(e){Ms(()=>Ms(e))}var Fs=e=>e===window,Is=(e,t)=>({top:0,left:0,right:e,bottom:t,width:e,height:t}),Ls=e=>{let t=L(e);if(Fs(t)){let e=t.innerWidth,n=t.innerHeight;return Is(e,n)}return t!=null&&t.getBoundingClientRect?t.getBoundingClientRect():Is(0,0)};function Rs(e){let t=V(e,null);if(t){let e=Pa(),{link:n,unlink:r,internalChildren:i}=t;return n(e),Fr(()=>r(e)),{parent:t,index:Z(()=>i.indexOf(e))}}return{parent:null,index:Wt(-1)}}function zs(e){let t=[],n=e=>{Array.isArray(e)&&e.forEach(e=>{var r;_a(e)&&(t.push(e),(r=e.component)!=null&&r.subTree&&(t.push(e.component.subTree),n(e.component.subTree.children)),e.children&&n(e.children))})};return n(e),t}var Bs=(e,t)=>{let n=e.indexOf(t);return n===-1?e.findIndex(e=>t.key!==void 0&&t.key!==null&&e.type===t.type&&e.key===t.key):n};function Vs(e,t,n){let r=zs(e.subTree.children);n.sort((e,t)=>Bs(r,e.vnode)-Bs(r,t.vnode));let i=n.map(e=>e.proxy);t.sort((e,t)=>i.indexOf(e)-i.indexOf(t))}function Hs(e){let t=Pt([]),n=Pt([]),r=Pa();return{children:t,linkChildren:i=>{An(e,Object.assign({link:e=>{e.proxy&&(n.push(e),t.push(e.proxy),Vs(r,t,n))},unlink:e=>{let r=n.indexOf(e);t.splice(r,1),n.splice(r,1)},children:t,internalChildren:n},i))}}}function Us(e){let t;jr(()=>{e(),gn(()=>{t=!0})}),Sr(()=>{t&&e()})}function Ws(e,t,n={}){if(!js)return;let{target:r=window,passive:i=!1,capture:a=!1}=n,o=!1,s,c=n=>{if(o)return;let r=L(n);r&&!s&&(r.addEventListener(e,t,{capture:a,passive:i}),s=!0)},l=n=>{if(o)return;let r=L(n);r&&s&&(r.removeEventListener(e,t,a),s=!1)};Fr(()=>l(r)),Cr(()=>l(r)),Us(()=>c(r));let u;return I(r)&&(u=Fn(r,(e,t)=>{l(t),c(e)})),()=>{u==null||u(),l(r),o=!0}}var Gs,Ks;function qs(){if(!Gs&&(Gs=Wt(0),Ks=Wt(0),js)){let e=()=>{Gs.value=window.innerWidth,Ks.value=window.innerHeight};e(),window.addEventListener(`resize`,e,{passive:!0}),window.addEventListener(`orientationchange`,e,{passive:!0})}return{width:Gs,height:Ks}}var Js=/scroll|auto|overlay/i,Ys=js?window:void 0;function Xs(e){return e.tagName!==`HTML`&&e.tagName!==`BODY`&&e.nodeType===1}function Zs(e,t=Ys){let n=e;for(;n&&n!==t&&Xs(n);){let{overflowY:e}=window.getComputedStyle(n);if(Js.test(e))return n;n=n.parentNode}return t}function Qs(e,t=Ys){let n=Wt();return jr(()=>{e.value&&(n.value=Zs(e.value,t))}),n}var $s;function ec(){if(!$s&&($s=Wt(`visible`),js)){let e=()=>{$s.value=document.hidden?`hidden`:`visible`};e(),window.addEventListener(`visibilitychange`,e)}return $s}var tc=Symbol(`van-field`);function nc(e){let t=`scrollTop`in e?e.scrollTop:e.pageYOffset;return Math.max(t,0)}function rc(e,t){`scrollTop`in e?e.scrollTop=t:e.scrollTo(e.scrollX,t)}function ic(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}function ac(e){rc(window,e),rc(document.body,e)}function oc(e,t){if(e===window)return 0;let n=t?nc(t):ic();return Ls(e).top+n}var sc=xs();function cc(){sc&&ac(ic())}var lc=e=>e.stopPropagation();function uc(e,t){(typeof e.cancelable!=`boolean`||e.cancelable)&&e.preventDefault(),t&&lc(e)}function dc(e){let t=L(e);if(!t)return!1;let n=window.getComputedStyle(t),r=n.display===`none`,i=t.offsetParent===null&&n.position!==`fixed`;return r||i}var{width:fc,height:pc}=qs();function mc(e){if(_s(e))return bs(e)?`${e}px`:String(e)}function hc(e){if(_s(e)){if(Array.isArray(e))return{width:mc(e[0]),height:mc(e[1])};let t=mc(e);return{width:t,height:t}}}function gc(e){let t={};return e!==void 0&&(t.zIndex=+e),t}var _c;function vc(){if(!_c){let e=document.documentElement,t=e.style.fontSize||window.getComputedStyle(e).fontSize;_c=parseFloat(t)}return _c}function yc(e){return e=e.replace(/rem/g,``),+e*vc()}function bc(e){return e=e.replace(/vw/g,``),+e*fc.value/100}function xc(e){return e=e.replace(/vh/g,``),+e*pc.value/100}function Sc(e){if(typeof e==`number`)return e;if(hs){if(e.includes(`rem`))return yc(e);if(e.includes(`vw`))return bc(e);if(e.includes(`vh`))return xc(e)}return parseFloat(e)}var Cc=/-(\w)/g,wc=e=>e.replace(Cc,(e,t)=>t.toUpperCase()),Tc=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase().replace(/^-/,``),Ec=(e,t,n)=>Math.min(Math.max(e,t),n);function Dc(e,t,n){let r=e.indexOf(t);return r===-1?e:t===`-`&&r!==0?e.slice(0,r):e.slice(0,r+1)+e.slice(r).replace(n,``)}function Oc(e,t=!0,n=!0){e=t?Dc(e,`.`,/\./g):e.split(`.`)[0],e=n?Dc(e,`-`,/-/g):e.replace(/-/,``);let r=t?/[^-0-9.]/g:/[^-0-9]/g;return e.replace(r,``)}var{hasOwnProperty:kc}=Object.prototype;function Ac(e,t,n){let r=t[n];_s(r)&&(e[n]=!kc.call(e,n)||!gs(r)?r:jc(Object(e[n]),r))}function jc(e,t){return Object.keys(t).forEach(n=>{Ac(e,t,n)}),e}var Mc={name:`姓名`,tel:`电话`,save:`保存`,clear:`清空`,undo:`撤销`,cancel:`取消`,confirm:`确认`,delete:`删除`,loading:`加载中...`,noCoupon:`暂无优惠券`,nameEmpty:`请填写姓名`,addContact:`添加联系人`,telInvalid:`请填写正确的电话`,vanCalendar:{end:`结束`,start:`开始`,title:`日期选择`,weekdays:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],monthTitle:(e,t)=>`${e}\u5E74${t}\u6708`,rangePrompt:e=>`\u6700\u591A\u9009\u62E9 ${e} \u5929`},vanCascader:{select:`请选择`},vanPagination:{prev:`上一页`,next:`下一页`},vanPullRefresh:{pulling:`下拉即可刷新...`,loosing:`释放即可刷新...`},vanSubmitBar:{label:`合计:`},vanCoupon:{unlimited:`无门槛`,discount:e=>`${e}\u6298`,condition:e=>`\u6EE1${e}\u5143\u53EF\u7528`},vanCouponCell:{title:`优惠券`,count:e=>`${e}\u5F20\u53EF\u7528`},vanCouponList:{exchange:`兑换`,close:`不使用`,enable:`可用`,disabled:`不可用`,placeholder:`输入优惠码`},vanAddressEdit:{area:`地区`,areaEmpty:`请选择地区`,addressEmpty:`请填写详细地址`,addressDetail:`详细地址`,defaultAddress:`设为默认收货地址`},vanAddressList:{add:`新增地址`}},Nc=Wt(`zh-CN`),Pc=Pt({"zh-CN":Mc}),Fc={messages(){return Pc[Nc.value]},use(e,t){Nc.value=e,this.add({[e]:t})},add(e={}){jc(Pc,e)}};function Ic(e){let t=wc(e)+`.`;return(e,...n)=>{let r=Fc.messages(),i=Ss(r,t+e)||Ss(r,e);return vs(i)?i(...n):i}}function Lc(e,t){return t?typeof t==`string`?` ${e}--${t}`:Array.isArray(t)?t.reduce((t,n)=>t+Lc(e,n),``):Object.keys(t).reduce((n,r)=>n+(t[r]?Lc(e,r):``),``):``}function Rc(e){return(t,n)=>(t&&typeof t!=`string`&&(n=t,t=``),t=t?`${e}__${t}`:e,`${t}${Lc(t,n)}`)}function zc(e){let t=`van-${e}`;return[t,Rc(t),Ic(t)]}function Bc(e){return e.install=t=>{let{name:n}=e;n&&(t.component(n,e),t.component(wc(`-${n}`),e))},e}var Vc=2e3,Hc=()=>++Vc,Uc=e=>{Vc=e},[Wc,Gc]=zc(`config-provider`),Kc=Symbol(Wc),qc={tag:As(`div`),theme:As(`light`),zIndex:Number,themeVars:Object,themeVarsDark:Object,themeVarsLight:Object,themeVarsScope:As(`local`),iconPrefix:String};function Jc(e){return e.replace(/([a-zA-Z])(\d)/g,`$1-$2`)}function Yc(e){let t={};return Object.keys(e).forEach(n=>{let r=Jc(Tc(n));t[`--van-${r}`]=e[n]}),t}function Xc(e={},t={}){Object.keys(e).forEach(n=>{e[n]!==t[n]&&document.documentElement.style.setProperty(n,e[n])}),Object.keys(t).forEach(t=>{e[t]||document.documentElement.style.removeProperty(t)})}var Zc=Bc(fr({name:Wc,props:qc,setup(e,{slots:t}){let n=Z(()=>Yc(ms({},e.themeVars,e.theme===`dark`?e.themeVarsDark:e.themeVarsLight)));if(hs){let t=()=>{document.documentElement.classList.add(`van-theme-${e.theme}`)},r=(t=e.theme)=>{document.documentElement.classList.remove(`van-theme-${t}`)};Fn(()=>e.theme,(e,n)=>{n&&r(n),t()},{immediate:!0}),Sr(t),Cr(r),Pr(r),Fn(n,(t,n)=>{e.themeVarsScope===`global`&&Xc(t,n)}),Fn(()=>e.themeVarsScope,(e,t)=>{t===`global`&&Xc({},n.value),e===`global`&&Xc(n.value,{})}),e.themeVarsScope===`global`&&Xc(n.value,{})}return An(Kc,e),Nn(()=>{e.zIndex!==void 0&&Uc(e.zIndex)}),()=>J(e.tag,{class:Gc(),style:e.themeVarsScope===`local`?n.value:void 0},{default:()=>{var e;return[(e=t.default)==null?void 0:e.call(t)]}})}}));function Qc(e=`(min-width: 768px)`){let t=typeof window<`u`&&window.matchMedia?window.matchMedia(e):null,n=Wt(t?t.matches:!1),r=e=>{n.value=e.matches};return jr(()=>{var e;return t==null||(e=t.addEventListener)==null?void 0:e.call(t,`change`,r)}),Pr(()=>{var e;return t==null||(e=t.removeEventListener)==null?void 0:e.call(t,`change`,r)}),n}function $c(){let e=typeof window<`u`&&window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,t=Wt(e?e.matches:!1),n=e=>{t.value=e.matches};return jr(()=>{var t;return e==null||(t=e.addEventListener)==null?void 0:t.call(e,`change`,n)}),Pr(()=>{var t;return e==null||(t=e.removeEventListener)==null?void 0:t.call(e,`change`,n)}),t}function el(e,t){let n,r=!1,i=()=>{n!==void 0&&window.clearInterval(n),n=void 0},a=()=>{i(),n=window.setInterval(()=>{document.visibilityState===`visible`&&e()},t)},o=()=>{r&&(document.visibilityState===`visible`?(e(),a()):i())};jr(()=>{r=!0,a(),document.addEventListener(`visibilitychange`,o)}),Sr(()=>{r||(r=!0,e(),a())}),Cr(()=>{r=!1,i()}),Pr(()=>{r=!1,i(),document.removeEventListener(`visibilitychange`,o)})}var tl=fr({__name:`App`,setup(e){let t=$c(),n=Z(()=>t.value?`dark`:`light`),r={primaryColor:`#2f6fff`,successColor:`#13b77a`,warningColor:`#e8870c`,dangerColor:`#ff5358`,navBarBackground:`var(--app-bg)`,navBarTitleTextColor:`var(--app-text)`,cellGroupInsetRadius:`14px`,buttonRadius:`12px`,buttonLargeHeight:`48px`},i={primaryColor:`#5b8cff`,successColor:`#2fd19a`,warningColor:`#ffae4a`,dangerColor:`#ff6b70`,background:`#0b1222`,background2:`#131c31`,background3:`#18223b`,textColor:`#e6ecfb`,textColor2:`#8b97b6`,textColor3:`#6c7898`,borderColor:`rgba(140,160,210,.16)`,activeColor:`#1c2743`};return(e,t)=>{let a=Vr(`RouterView`);return ua(),ga(L(Zc),{theme:n.value,"theme-vars-scope":`global`,"theme-vars":r,"theme-vars-dark":i},{default:Dn(()=>[J(a)]),_:1},8,[`theme`])}}});function nl(e){"@babel/helpers - typeof";return nl=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},nl(e)}function rl(e,t){if(nl(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(nl(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function il(e){var t=rl(e,`string`);return nl(t)==`symbol`?t:t+``}function al(e,t,n){return(t=il(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ol(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function sl(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&ol(e.default)}var Q=Object.assign;function cl(e,t){let n={};for(let r in t){let i=t[r];n[r]=ul(i)?i.map(e):e(i)}return n}var ll=()=>{},ul=Array.isArray;function dl(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var fl=Symbol(``);function pl(e,t){return Q(Error(),{type:e,[fl]:!0},t)}function ml(e,t){return e instanceof Error&&fl in e&&(t==null||!!(e.type&t))}var hl=Symbol(``),gl=Symbol(``),_l=Symbol(``),vl=Symbol(``),yl=Symbol(``);function bl(){return V(_l)}function xl(e){return V(vl)}var Sl=typeof document<`u`,Cl=/#/g,wl=/&/g,Tl=/\//g,El=/=/g,Dl=/\?/g,Ol=/\+/g,kl=/%5B/g,Al=/%5D/g,jl=/%5E/g,Ml=/%60/g,Nl=/%7B/g,Pl=/%7C/g,Fl=/%7D/g,Il=/%20/g;function Ll(e){return e==null?``:encodeURI(``+e).replace(Pl,`|`).replace(kl,`[`).replace(Al,`]`)}function Rl(e){return Ll(e).replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function zl(e){return Ll(e).replace(Ol,`%2B`).replace(Il,`+`).replace(Cl,`%23`).replace(wl,`%26`).replace(Ml,"`").replace(Nl,`{`).replace(Fl,`}`).replace(jl,`^`)}function Bl(e){return zl(e).replace(El,`%3D`)}function Vl(e){return Ll(e).replace(Cl,`%23`).replace(Dl,`%3F`)}function Hl(e){return Vl(e).replace(Tl,`%2F`)}function Ul(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var Wl=/\/$/,Gl=e=>e.replace(Wl,``);function Kl(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r=r||t.slice(0,s),o=t.slice(s,t.length)),r=eu(r==null?t:r,n),{fullPath:r+a+o,path:r,query:i,hash:Ul(o)}}function ql(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Jl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function Yl(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&Xl(t.matched[r],n.matched[i])&&Zl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Xl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Zl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ql(e[n],t[n]))return!1;return!0}function Ql(e,t){return ul(e)?$l(e,t):ul(t)?$l(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function $l(e,t){return ul(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function eu(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o1&&a--;else break}return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var tu={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0};function nu(e){if(!e){if(Sl){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^/]+/,``)}else e=`/`}return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Gl(e)}var ru=/^[^#]+#/;function iu(e,t){return e.replace(ru,`#`)+t}function au(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var ou=()=>({left:window.scrollX,top:window.scrollY});function su(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=au(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function cu(e,t){return(history.state?history.state.position-t:-1)+e}var lu=new Map;function uu(e,t){lu.set(e,t)}function du(e){let t=lu.get(e);return lu.delete(e),t}function fu(e){return typeof e==`string`||e&&typeof e==`object`}function pu(e){return typeof e==`string`||typeof e==`symbol`}function mu(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;ee&&zl(e)):[r&&zl(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function gu(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=ul(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function _u(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function vu(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(pl(4,{from:n,to:t})):e instanceof Error?c(e):fu(e)?c(pl(2,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function yu(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(t===`beforeRouteEnter`||o.instances[e]){if(ol(s)){let c=(s.__vccOpts||s)[t];c&&a.push(vu(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=sl(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&vu(c,n,r,o,e,i)()}))}}}return a}function bu(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;oXl(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>Xl(e,s))||i.push(s))}return[n,r,i]}var xu=()=>location.protocol+`//`+location.host;function Su(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Jl(n,``)}return Jl(n,e)+r+i}function Cu(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=Su(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:`pop`,direction:u?u>0?`forward`:`back`:``})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(Q({},e.state,{scroll:ou()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function wu(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ou():null}}function Tu(e){let{history:t,location:n}=window,r={value:Su(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:xu()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,Q({},t.state,wu(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=Q({},i.value,t.state,{forward:e,scroll:ou()});a(o.current,o,!0),a(e,Q({},wu(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function Eu(e){e=nu(e);let t=Tu(e),n=Cu(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=Q({location:``,base:e,go:r,createHref:iu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Du(e){return e=location.host?e||location.pathname+location.search:``,e.includes(`#`)||(e+=`#`),Eu(e)}var Ou={type:0,value:``},ku=/[a-zA-Z0-9_]/;function Au(e){if(!e)return[[]];if(e===`/`)return[[Ou]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=0,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&(n===0?a.push({type:0,value:l}):n===1||n===2||n===3?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:1,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),l=``)}function f(){l+=c}for(;st.length?t.length===1&&t[0]===80?1:-1:0}function Iu(e,t){let n=0,r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}var Ru={strict:!1,end:!0,sensitive:!1};function zu(e,t,n){let r=Q(Pu(Au(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Bu(e,t){let n=[],r=new Map;t=dl(Ru,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Hu(e);s.aliasOf=r&&r.record;let l=dl(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Hu(Q({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=zu(t,n,l),r?r.alias.push(d):(f=f||d,f!==d&&f.alias.push(d),i&&e.name&&!Wu(d)&&o(e.name)),Ju(d)&&c(d),s.children){let e=s.children;for(let t=0;t{o(f)}:ll}function o(e){if(pu(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=Ku(e,n);n.splice(t,0,e),e.record.name&&!Wu(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw pl(1,{location:e});s=i.record.name,a=Q(Vu(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&Vu(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name,i.keys.forEach(e=>{e.optional&&!a[e.name]&&delete a[e.name]}));else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw pl(1,{location:e,currentLocation:t});s=i.record.name,a=Q({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Gu(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function Vu(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Hu(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Uu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Uu(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Wu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Gu(e){return e.reduce((e,t)=>Q(e,t.meta),{})}function Ku(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Iu(e,t[i])<0?r=i:n=i+1}let i=qu(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function qu(e){let t=e;for(;t=t.parent;)if(Ju(t)&&Iu(e,t)===0)return t}function Ju({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Yu(e){let t=V(_l),n=V(vl),r=Z(()=>{let n=L(e.to);return t.resolve(n)}),i=Z(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(Xl.bind(null,i));if(o>-1)return o;let s=ed(e[t-2]);return t>1&&ed(i)===s&&a[a.length-1].path!==s?a.findIndex(Xl.bind(null,e[t-2])):o}),a=Z(()=>i.value>-1&&$u(n.params,r.value.params)),o=Z(()=>i.value>-1&&i.value===n.matched.length-1&&Zl(n.params,r.value.params));function s(n={}){if(Qu(n)){let n=t[L(e.replace)?`replace`:`push`](L(e.to)).catch(ll);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:Z(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Xu(e){return e.length===1?e[0]:e}var Zu=fr({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:Yu,setup(e,{slots:t}){let n=Pt(Yu(e)),{options:r}=V(_l),i=Z(()=>({[td(e.activeClass,r.linkActiveClass,`router-link-active`)]:n.isActive,[td(e.exactActiveClass,r.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let r=t.default&&Xu(t.default(n));return e.custom?r:Xa(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}});function Qu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(e.button===void 0||e.button===0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function $u(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!ul(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function ed(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var td=(e,t,n)=>e==null?t==null?n:t:e,nd=fr({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let r=V(yl),i=Z(()=>e.route||r.value),a=V(gl,0),o=Z(()=>{let e=L(a),{matched:t}=i.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),s=Z(()=>i.value.matched[o.value]);An(gl,Z(()=>o.value+1)),An(hl,s),An(yl,i);let c=Wt();return Fn(()=>[c.value,s.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!Xl(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let r=i.value,a=e.name,o=s.value,l=o&&o.components[a];if(!l)return rd(n.default,{Component:l,route:r});let u=o.props[a],d=Xa(l,Q({},u?u===!0?r.params:typeof u==`function`?u(r):u:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(o.instances[a]=null)},ref:c}));return rd(n.default,{Component:d,route:r})||d}}});function rd(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var id=nd;function ad(e){let t=Bu(e.routes,e),n=e.parseQuery||mu,r=e.stringifyQuery||hu,i=e.history,a=_u(),o=_u(),s=_u(),c=Gt(tu),l=tu;Sl&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let u=cl.bind(null,e=>``+e),d=cl.bind(null,Hl),f=cl.bind(null,Ul);function p(e,n){let r,i;return pu(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function m(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function h(){return t.getRoutes().map(e=>e.record)}function g(e){return!!t.getRecordMatcher(e)}function _(e,a){if(a=Q({},a||c.value),typeof e==`string`){let r=Kl(n,e,a.path),o=t.resolve({path:r.path},a),s=i.createHref(r.fullPath);return Q(r,o,{params:f(o.params),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=Q({},e,{path:Kl(n,e.path,a.path).path});else{let t=Q({},e.params);for(let e in t)t[e]==null&&delete t[e];o=Q({},e,{params:d(t)}),a.params=d(a.params)}let s=t.resolve(o,a),l=e.hash||``;s.params=u(f(s.params));let p=ql(r,Q({},e,{hash:Rl(l),path:s.path})),m=i.createHref(p);return Q({fullPath:p,hash:l,query:r===hu?gu(e.query):e.query||{}},s,{redirectedFrom:void 0,href:m})}function v(e){return typeof e==`string`?Kl(n,e,c.value.path):Q({},e)}function y(e,t){if(l!==e)return pl(8,{from:t,to:e})}function b(e){return C(e)}function x(e){return b(Q(v(e),{replace:!0}))}function S(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=v(i):{path:i},i.params={}),Q({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function C(e,t){let n=l=_(e),i=c.value,a=e.state,o=e.force,s=e.replace===!0,u=S(n,i);if(u)return C(Q(v(u),{state:typeof u==`object`?Q({},a,u.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&Yl(r,i,n)&&(f=pl(16,{to:d,from:i}),oe(i,i,!0,!1)),(f?Promise.resolve(f):ee(d,i)).catch(e=>ml(e)?ml(e,2)?e:ae(e):k(e,d,i)).then(e=>{if(e){if(ml(e,2))return C(Q({replace:s},v(e.to),{state:typeof e.to==`object`?Q({},a,e.to.state):a,force:o}),t||d)}else e=te(d,i,!0,s,a);return E(d,i,e),e})}function w(e,t){let n=y(e,t);return n?Promise.reject(n):Promise.resolve()}function T(e){let t=le.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function ee(e,t){let n,[r,i,s]=bu(e,t);n=yu(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(vu(r,e,t))});let c=w.bind(null,e,t);return n.push(c),de(n).then(()=>{n=[];for(let r of a.list())n.push(vu(r,e,t));return n.push(c),de(n)}).then(()=>{n=yu(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(vu(r,e,t))});return n.push(c),de(n)}).then(()=>{n=[];for(let r of s)if(r.beforeEnter){if(ul(r.beforeEnter))for(let i of r.beforeEnter)n.push(vu(i,e,t));else n.push(vu(r.beforeEnter,e,t))}return n.push(c),de(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=yu(s,`beforeRouteEnter`,e,t,T),n.push(c),de(n))).then(()=>{n=[];for(let r of o.list())n.push(vu(r,e,t));return n.push(c),de(n)}).catch(e=>ml(e,8)?e:Promise.reject(e))}function E(e,t,n){s.list().forEach(r=>T(()=>r(e,t,n)))}function te(e,t,n,r,a){let o=y(e,t);if(o)return o;let s=t===tu,l=Sl?history.state:{};n&&(r||s?i.replace(e.fullPath,Q({scroll:s&&l&&l.scroll},a)):i.push(e.fullPath,a)),c.value=e,oe(e,t,n,s),ae()}let D;function ne(){D||(D=i.listen((e,t,n)=>{if(!ue.listening)return;let r=_(e),a=S(r,ue.currentRoute.value);if(a){C(Q(a,{replace:!0,force:!0}),r).catch(ll);return}l=r;let o=c.value;Sl&&uu(cu(o.fullPath,n.delta),ou()),ee(r,o).catch(e=>ml(e,12)?e:ml(e,2)?(C(Q(v(e.to),{force:!0}),r).then(e=>{ml(e,20)&&!n.delta&&n.type===`pop`&&i.go(-1,!1)}).catch(ll),Promise.reject()):(n.delta&&i.go(-n.delta,!1),k(e,r,o))).then(e=>{e=e||te(r,o,!1),e&&(n.delta&&!ml(e,8)?i.go(-n.delta,!1):n.type===`pop`&&ml(e,20)&&i.go(-1,!1)),E(r,o,e)}).catch(ll)}))}let O=_u(),re=_u(),ie;function k(e,t,n){ae(e);let r=re.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function A(){return ie&&c.value!==tu?Promise.resolve():new Promise((e,t)=>{O.add([e,t])})}function ae(e){return ie||(ie=!e,ne(),O.list().forEach(([t,n])=>e?n(e):t()),O.reset()),e}function oe(t,n,r,i){let{scrollBehavior:a}=e;if(!Sl||!a)return Promise.resolve();let o=!r&&du(cu(t.fullPath,0))||(i||!r)&&history.state&&history.state.scroll||null;return gn().then(()=>a(t,n,o)).then(e=>t===c.value&&e&&su(e)).catch(e=>t===c.value&&k(e,t,n))}let se=e=>i.go(e),ce,le=new Set,ue={currentRoute:c,listening:!0,addRoute:p,removeRoute:m,clearRoutes:t.clearRoutes,hasRoute:g,getRoutes:h,resolve:_,options:e,push:b,replace:x,go:se,back:()=>se(-1),forward:()=>se(1),beforeEach:a.add,beforeResolve:o.add,afterEach:s.add,onError:re.add,isReady:A,install(e){e.component(`RouterLink`,Zu),e.component(`RouterView`,id),e.config.globalProperties.$router=ue,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>L(c)}),Sl&&!ce&&c.value===tu&&(ce=!0,b(i.location).catch(e=>{}));let t={};for(let e in tu)Object.defineProperty(t,e,{get:()=>c.value[e],enumerable:!0});e.provide(_l,ue),e.provide(vl,Ft(t)),e.provide(yl,c);let n=e.unmount;le.add(e),e.unmount=function(){le.delete(e),le.size<1&&(l=tu,D&&D(),D=null,c.value=tu,ce=!1,ie=!1),n()}}};function de(e){return e.reduce((e,t)=>e.then(()=>T(t)),Promise.resolve())}return ue}var od=`/api/v2/mobile`,sd=`wecom-mobile-token`,cd=class extends Error{constructor(e,t,n,r=0){super(n),al(this,`status`,void 0),al(this,`code`,void 0),al(this,`retryAfter`,void 0),this.status=e,this.code=t,this.retryAfter=r}},ld=null;function ud(e){ld=e}function dd(){try{return window.localStorage.getItem(sd)||``}catch{return``}}function fd(e){try{e?window.localStorage.setItem(sd,e):window.localStorage.removeItem(sd)}catch{}}var pd=``;function md(){return pd||dd()}function hd(e){pd=e,fd(e)}function gd(e,t){let n=t&&typeof t==`object`&&`detail`in t?t.detail:t;if(n&&typeof n==`object`&&!Array.isArray(n)){let t=n;return new cd(e,String(t.code||`http_${e}`),String(t.message||_d(e)),Number(t.retry_after||0))}return Array.isArray(n)?new cd(e,`validation`,`填写的内容不符合要求,请检查后重试`):typeof n==`string`&&n.trim()?new cd(e,`http_${e}`,n.trim()):new cd(e,`http_${e}`,_d(e))}function _d(e){return e===401?`登录已失效,请重新登录`:e===403?`没有权限执行这个操作`:e===404?`内容不存在或已被删除`:e===429?`操作太频繁,请稍后再试`:e>=500?`服务暂时不可用,请稍后重试`:`请求失败(HTTP ${e})`}async function vd(e,t,n={}){let{body:r,query:i,auth:a=!0,signal:o}=n,s=new URLSearchParams;for(let[e,t]of Object.entries(i||{}))t!=null&&t!==``&&s.set(e,String(t));let c=`${od}${t}${s.toString()?`?${s}`:``}`,l={Accept:`application/json`};r!==void 0&&(l[`Content-Type`]=`application/json`);let u=md();a&&u&&(l.Authorization=`Bearer ${u}`);let d;try{d=await fetch(c,{method:e,headers:l,body:r===void 0?void 0:JSON.stringify(r),signal:o,cache:`no-store`,credentials:`omit`})}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?e:new cd(0,`network`,`网络连接失败,请检查网络后重试`)}let f=null,p=await d.text();if(p)try{f=JSON.parse(p)}catch{f=null}if(!d.ok){let e=gd(d.status,f);throw d.status===401&&a&&(ld==null||ld()),e}return f}var yd=e=>encodeURIComponent(e),bd={login(e,t,n){return vd(`POST`,`/auth/login`,{auth:!1,body:{account:e,password:t,client_name:n}})},logout(){return vd(`POST`,`/auth/logout`)},me(){return vd(`GET`,`/me`)},accounts(e=``){return vd(`GET`,`/accounts`,{query:{q:e}})},desktops(e){return vd(`GET`,`/desktops`,{query:{account_id:e}})},sendCommand(e,t,n){return vd(`POST`,`/desktops/${yd(e)}/commands`,{body:{type:t},query:{account_id:n}})},command(e){return vd(`GET`,`/commands/${yd(e)}`)},conversations(e){return vd(`GET`,`/conversations`,{query:{account_id:e.accountId,q:e.q,scope:e.scope,cursor:e.cursor,limit:e.limit},signal:e.signal})},block(e,t,n){return vd(`PUT`,`/blacklist/${yd(e.account)}/${yd(e.conversation)}`,{body:{name:e.name||``,account_name:e.account_name||``,reason:t},query:{account_id:n}})},unblock(e,t,n){return vd(`DELETE`,`/blacklist/${yd(e)}/${yd(t)}`,{query:{account_id:n}})}},xd=`wecom-mobile-view-account`;function Sd(){try{let e=window.localStorage.getItem(xd),t=e?JSON.parse(e):null;return t&&typeof t.id==`number`?t:null}catch{return null}}var $=Pt({token:md(),me:null,viewed:Sd()}),Cd=Z(()=>{var e;return(e=$.me)==null?void 0:e.permissions}),wd=Z(()=>{let e=$.me,t=$.viewed;if(e&&t&&e.permissions.control_all&&t.id!==e.account.id)return t.id}),Td=Z(()=>{var e,t;return wd.value!==void 0&&$.viewed?$.viewed.nickname||$.viewed.sn||`客服 #${$.viewed.id}`:((e=$.me)==null?void 0:e.account.nickname)||((t=$.me)==null?void 0:t.account.sn)||``});function Ed(e){$.viewed=e?{id:e.id,nickname:e.nickname,sn:e.sn}:null;try{$.viewed?window.localStorage.setItem(xd,JSON.stringify($.viewed)):window.localStorage.removeItem(xd)}catch{}}function Dd(e,t){hd(e),$.token=e,$.me=t,t.permissions.control_all||Ed(null)}function Od(){hd(``),$.token=``,$.me=null,Ed(null)}async function kd(){let e=await bd.me();return $.me=e,e.permissions.control_all||Ed(null),e}async function Ad(){try{await bd.logout()}catch{}Od()}var jd=`modulepreload`,Md=function(e){return`/m/`+e},Nd={},Pd=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute(`nonce`));function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=Md(t,n),t=s(t),t in Nd)return;Nd[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:jd,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Fd=ad({history:Du(),routes:[{path:`/login`,name:`login`,component:()=>Pd(()=>import(`./LoginView-nW7aCAJ5.js`),__vite__mapDeps([0,1,2,3,4])),meta:{public:!0}},{path:`/`,component:()=>Pd(()=>import(`./ShellView-CQXCC9xU.js`),__vite__mapDeps([5,1,6,7])),children:[{path:``,redirect:{name:`desktops`}},{path:`desktops`,name:`desktops`,component:()=>Pd(()=>import(`./DevicesView-Cgb9V9Z0.js`),__vite__mapDeps([8,1,9,6,2,10,3,11,12]))},{path:`conversations`,name:`conversations`,component:()=>Pd(()=>import(`./ConversationsView-CLYafisP.js`),__vite__mapDeps([13,1,6,9,2,3,10,11,14]))},{path:`me`,name:`me`,component:()=>Pd(()=>import(`./MeView-uJ3uOzy0.js`),__vite__mapDeps([15,1,9,6,2,16]))}]},{path:`/:pathMatch(.*)*`,redirect:{name:`desktops`}}]});Fd.beforeEach(async e=>{if(e.meta.public)return!0;if(!$.token)return{name:`login`,query:{redirect:e.fullPath}};if(!$.me)try{await kd()}catch(t){if(t instanceof cd&&t.status===401)return Od(),{name:`login`,query:{redirect:e.fullPath}}}return!0}),ud(()=>{Od();let e=Fd.currentRoute.value;e.matched.length&&e.name!==`login`&&Fd.replace({name:`login`,query:{redirect:e.fullPath,expired:`1`}})});var Id=`wecom-mobile-chunk-reload`;Fd.onError((e,t)=>{let n=String((e==null?void 0:e.message)||e);if(!/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(n))return;let r=0;try{r=Number(window.sessionStorage.getItem(Id)||0),window.sessionStorage.setItem(Id,String(Date.now()))}catch{}Date.now()-r>1e4&&(window.location.assign(`/m/#${t.fullPath}`),window.location.reload())}),us(tl).use(Fd).mount(`#app`),`serviceWorker`in navigator&&window.isSecureContext&&window.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/m/sw.js`,{scope:`/m/`}).catch(()=>void 0)});export{hs as $,ac as A,Mr as At,Hs as B,_i as Bt,Sc as C,fr as Ct,dc as D,gn as Dt,nc as E,Aa as Et,Ns as F,ua as Ft,Qs as G,Pt as Gt,ec as H,Nn as Ht,Ps as I,An as It,Ds as J,he as Jt,Os as K,Wt as Kt,Zs as L,Kr as Lt,pc as M,jr as Mt,fc as N,Fr as Nt,uc as O,Sr as Ot,tc as P,Nr as Pt,ms as Q,Us as R,Vr as Rt,gc as S,J as St,ic as T,V as Tt,Rs as U,Dn as Ut,Ws as V,Fn as Vt,Ls as W,On as Wt,Ts as X,As as Y,Se as Yt,Es as Z,zc as _,ga as _t,Ed as a,ps as at,Oc as b,qr as bt,cd as c,fo as ct,bl as d,os as dt,_s as et,el as f,oa as ft,Bc as g,xa as gt,Hc as h,Z as ht,Ad as i,ys as it,rc as j,Cr as jt,cc as k,Pr as kt,bd as l,us as lt,Kc as m,Xn as mt,$ as n,bs as nt,wd as o,Cs as ot,Qc as p,br as pt,ks as q,L as qt,Dd as r,gs as rt,Td as s,ws as st,Cd as t,vs as tt,xl as u,Ao as ut,mc as v,Ea as vt,oc as w,Pa as wt,hc as x,Ta as xt,Ec as y,ha as yt,Ms as z,Ur as zt}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js new file mode 100644 index 000000000..8d5898bfb --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/assets/use-placeholder-CcVnlsb4.js @@ -0,0 +1 @@ +import{c as e}from"./_plugin-vue_export-helper-CT0ZqwFS.js";import{Dt as t,Kt as n,M as r,Mt as i,N as a,St as o,Vt as s,W as c,at as l,it as u}from"./index-Gkjcdjlo.js";function d(e,{args:t=[],done:n,canceled:r,error:i}){if(e){let a=e.apply(null,t);u(a)?a.then(e=>{e?n():r&&r()}).catch(i||l):a?n():r&&r()}else n()}var f=(o,l)=>{let u=n(),d=()=>{u.value=c(o).height};return i(()=>{if(t(d),l)for(let e=1;e<=3;e++)setTimeout(d,100*e)}),e(()=>t(d)),s([a,r],d),u};function p(e,t){let n=f(e,!0);return e=>o(`div`,{class:t(`placeholder`),style:{height:n.value?`${n.value}px`:void 0}},[e()])}export{d as n,p as t}; \ No newline at end of file diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png new file mode 100644 index 000000000..b2c9e5bfb Binary files /dev/null and b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/apple-touch-icon.png differ diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png new file mode 100644 index 000000000..0e484db4f Binary files /dev/null and b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/favicon-64.png differ diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png new file mode 100644 index 000000000..d9f6a089c Binary files /dev/null and b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-192.png differ diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png new file mode 100644 index 000000000..bceb2a62e Binary files /dev/null and b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-512.png differ diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png new file mode 100644 index 000000000..402813945 Binary files /dev/null and b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/icons/icon-maskable-512.png differ diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html new file mode 100644 index 000000000..19d8bc70a --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 企微客服助手 · 手机控制台 + + + + +
+ + + diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest new file mode 100644 index 000000000..2376a055c --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "企微客服助手 · 手机控制台", + "short_name": "客服控制台", + "description": "远程查看和启停客服电脑的自动回复,管理会话黑名单。", + "lang": "zh-CN", + "start_url": "/m/", + "scope": "/m/", + "display": "standalone", + "orientation": "any", + "background_color": "#f4f7fd", + "theme_color": "#2f6fff", + "icons": [ + { "src": "/m/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/m/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js new file mode 100644 index 000000000..de2999d7b --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/admin-web/apps/mobile-h5/dist/sw.js @@ -0,0 +1,4 @@ +// 手机控制台的 Service Worker 只为"添加到主屏幕"存在,不缓存任何内容: +// 远程控制看到的必须是实时状态,离线缓存只会展示过期的电脑状态。 +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/deploy/im-admin/Dockerfile b/deploy/mobile-remote-20260928/server-bundle/payload/deploy/im-admin/Dockerfile new file mode 100644 index 000000000..36761e36f --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/deploy/im-admin/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +COPY deploy/im-admin/requirements.txt /tmp/requirements.txt +RUN pip install --disable-pip-version-check -r /tmp/requirements.txt \ + && useradd --create-home --uid 10001 --shell /usr/sbin/nologin app + +COPY --chown=app:app wechat_rpa/*.py /app/wechat_rpa/ +COPY --chown=app:app admin-web/apps/web-antd/dist/ /app/admin-web/apps/web-antd/dist/ +# 手机远程控制台(admin-web/apps/mobile-h5,独立项目),由 admin_api.py 托管在 /m/ +COPY --chown=app:app admin-web/apps/mobile-h5/dist/ /app/admin-web/apps/mobile-h5/dist/ + +USER app +WORKDIR /app/wechat_rpa + +EXPOSE 8766 8770 + +CMD ["python", "admin_api.py", "--db", "/data/backend.db", "--host", "0.0.0.0", "--port", "8766"] diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/admin_api.py b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/admin_api.py new file mode 100644 index 000000000..e8f8077ae --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/admin_api.py @@ -0,0 +1,1630 @@ +# -*- coding: utf-8 -*- +"""管理后台 JSON API(FastAPI)。 + +当初另起一个,是因为老的 `admin_backend.py` 是服务端渲染的 HTML 后台——表单 POST +加整页刷新。Vue 前端要的是 JSON API + 明确的权限清单,两种形态没法在一个处理器 +里长期共存。 + +绞杀者模式已经走完:老网页后台(8765)整体退役,`admin_backend.py` 只剩数据层 +(Database / 配置校验 / 模型测试),这个模块是唯一的 HTTP 入口。 + + admin_backend.py 数据层,被本模块和 model_gateway.py 共用 + admin_api.py JSON API + Vue 前端静态托管 + 桌面端同步,全在这一个 + +权限的判法只有一种:`Depends(require("model:write"))`。代码里**永远不判角色名** +——判角色名就等于把运营策略焊死在代码里,加个角色都得发版。 + +跑起来: + uvicorn admin_api:create_app --factory --host 127.0.0.1 --port 8766 + # 或 + python admin_api.py --db backend.db --port 8766 +""" + +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import hmac +import json +import os +import re +import time +import urllib.error +import urllib.parse +from pathlib import Path +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, RedirectResponse +from pydantic import BaseModel, Field, field_validator + +import admin_backend as backend +import app_version +import archive_api +import knowledge_api +import remote_api +import zyt_auth + +# 账号下拉框里「全部」的取值。用 -1 而不是 0:0 早就有含义了(旧版未归属数据), +# 而"不传"在 FastAPI 里就是 None,同样已经被占用。 +ALL_ACCOUNTS = -1 + +# Vue 开发服务器的默认端口。生产环境前端和 API 同源,这些只在开发时用得上。 +DEV_ORIGINS = [ + "http://localhost:5173", "http://127.0.0.1:5173", + "http://localhost:3000", "http://127.0.0.1:3000", +] + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class LoginBody(BaseModel): + username: str + password: str + device_name: str = "web" + + +class PasswordBody(BaseModel): + current_password: str + new_password: str + + +class RoleBody(BaseModel): + code: str + name: str = "" + permissions: list[str] = Field(default_factory=list) + + +class UserBody(BaseModel): + username: str = "" + password: str = "" + role: str = "viewer" + active: bool = True + + +class DesktopExchangeBody(BaseModel): + device_id: str = Field(min_length=8, max_length=128) + device_name: str = Field(default="", max_length=120) + app_version: str = Field(default="", max_length=64) + interactive_login: bool = False + + +class DesktopZytSessionBody(BaseModel): + zyt_token: str = Field(min_length=1, max_length=8192, repr=False) + + +class DesktopAccountBody(BaseModel): + status: str = "active" + backup_enabled: bool | None = None + backup_interval_minutes: int | None = Field(default=None, ge=1, le=10080) + + +class DesktopAccountScopeBody(BaseModel): + admin_user_ids: list[int] = Field(default_factory=list) + + +class ProviderBody(BaseModel): + id: str + name: str = "" + kind: str + base_url: str + # auto:按接口类型补全路径;exact:地址原样使用,一个字符不加 + endpoint_mode: str = "auto" + api_key: str = "" # 留空 = 保持原密钥不动 + model: str = "" + capabilities: str = "text" + max_tokens: int = 500 + temperature: float = 0.35 + timeout_ms: int = 30000 + max_inflight: int = 32 + rpm_limit: int = 0 + enabled: bool = True + + +class RolesPlanBody(BaseModel): + answer_ids: str + judge_id: str = "" + vision_id: str = "" + fallback_ids: str = "" + judge_mode: str = "shadow" + + +class AgentRuleBody(BaseModel): + """一条角色规则。 + + 三种类型的必填项不同,逐条校验在 `admin_backend.validate_agent_rules` 里—— + 这里只保证形状是个对象、字段名对得上。把"guide 必须有 instruction"这种规则 + 写进 Pydantic,等于同一套约束维护两份,迟早对不上。 + """ + + id: str = "" + label: str = "" + # guide 补充指令 / forbid 禁止措辞 / reply 固定口径 + type: str = "guide" + keywords: list[str] = Field(default_factory=list) + instruction: str = "" + reply: str = "" + enabled: bool = True + + +class AgentBody(BaseModel): + """一个智能体 = 人设 + 一组规则。""" + + id: str = "" # 留空 = 按名称自动生成 + name: str + role: str = "" + description: str = "" + persona: str = "" + keywords: list[str] = Field(default_factory=list) + rules: list[AgentRuleBody] = Field(default_factory=list) + enabled: bool = True + priority: int = 100 + + +class AgentPlanBody(BaseModel): + """启用与协作方案。single 就是"切换到某一个",collaborate 是"多个同时在场"。""" + + mode: str = "single" + primary_id: str = "" + active_ids: list[str] = Field(default_factory=list) + + +class ConfigBody(BaseModel): + """桌面端下发配置。 + + 字段名保持和老后台的表单 `name` 完全一致(大写下划线),这样两边共用同一个 + `validate_config_form`——校验规则只有一份,不会出现"网页后台拦得住、新后台 + 放得过"这种两套标准。 + + **这里没有模型连接参数**。服务类型 / API 地址 / API Key / 模型名称 / 温度 / + max_tokens / 超时全部搬到了「模型清单 + 角色编排」,理由见 + `admin_backend.CONFIG_KEYS` 上面那段。剩下的都是客户端行为。 + """ + + AI_ENABLED: bool = False + AI_DEVELOPMENT_MODE: bool = False + AI_USE_VISION: bool = False + AI_UI_GUARD_ENABLED: bool = False + AI_CONTEXT_ENABLED: bool = False + AI_CONTEXT_MAX_ROUNDS: int = 8 + AI_COUNTER_INSULT_ENABLED: bool = False + AI_AGENT_NAME: str = "" + AI_HOSPITAL_NAME: str = "" + AI_MCP_ENABLED: bool = False + AI_MCP_MAX_ROUNDS: int = 5 + AI_MCP_SERVERS: list[Any] = Field(default_factory=list) + # 留空 = 按后台自己的地址推算。桌面端不配这个,由后台同步时下发。 + AI_GATEWAY_URL: str = "" + # 桌面软件启动时先向本管理端读取该地址,再把账号密码发给 ZYT。 + ZYT_LOGIN_API_URL: str = "" + ARCHIVE_AUTO_BACKUP_ENABLED: bool = True + ARCHIVE_AUTO_BACKUP_INTERVAL_MINUTES: int = 60 + # 选择性审核:命中才转人工,其余自动发送。逐条形状由 + # `admin_backend.validate_review_rules` 校验,这里只接住一个数组。 + AI_REVIEW_RULES: list[Any] = Field(default_factory=list) + + @field_validator("*", mode="before") + @classmethod + def _null_means_unset(cls, value: Any, info: Any) -> Any: + """把请求体里的 `null` 当成"没填",换成这一项的出厂默认值。 + + 正常情况下 GET /config 现在不会再回 null,前端也就没有 null 可发。这里 + 挡的是另一种情况:浏览器里开着这次修复之前拉取的旧页面,表单里还揣着 + 当时的 null;用户点保存,POST 出去的照样是 null。没有这道,Pydantic 的 + 严格类型校验会直接拒收,报一句"Input should be a valid boolean"—— + 对着这句话,人只会怀疑是不是自己填错了什么,看不出问题出在缓存的旧页面。 + """ + if value is None: + return backend.CONFIG_DEFAULTS.get(info.field_name) + return value + + +class ReleaseBody(BaseModel): + package_media_id: str = "" + latest_version: str + download_url: str = "" + release_notes: str = "" + force_upgrade: bool = False + auto_install: bool = False + package_sha256: str = "" + package_size: int = Field(default=0, ge=0) + signature_required: bool = False + release_channel: str = "stable" + rollout_percent: int = Field(default=100, ge=1, le=100) + status: str = "published" + + +class ReleasePackagePrepareBody(BaseModel): + file_name: str = Field(min_length=1, max_length=512) + package_sha256: str = Field(pattern=r"^[0-9a-fA-F]{64}$") + package_size: int = Field(gt=0, le=1024 * 1024 * 1024) + + +class ReleasePackagePartBody(BaseModel): + part_number: int = Field(ge=1, le=10000) + etag: str = Field(min_length=1, max_length=512) + + +class ReleasePackageMultipartCompleteBody(BaseModel): + upload_id: str = Field(min_length=1, max_length=2048) + parts: list[ReleasePackagePartBody] = Field(min_length=1, max_length=10000) + + +class UpdateReportBody(BaseModel): + current_version: str = Field(default="", max_length=64) + target_version: str = Field(min_length=1, max_length=64) + status: str = Field(min_length=1, max_length=32) + progress: int = Field(default=0, ge=0, le=100) + error: str = Field(default="", max_length=1000) + + +class ModelTestBody(BaseModel): + """连通性测试。 + + 两种用法:给 `provider_id` 就测模型清单里那一条(密钥从库里解出来,永远不 + 经过前端);不给就用页面上临时填的值测——正在新建一个模型、还没保存的时候 + 需要这个。 + """ + + provider_id: str = "" + kind: str = "" + base_url: str = "" + endpoint_mode: str = "" + api_key: str = "" # 留空且指定了 provider_id = 用库里存的密钥 + model: str = "" + timeout_seconds: int = 20 + + +def _public_users(database) -> list[dict[str, Any]]: + """用户列表的对外投影。密码盐和摘要一个字节都不能出接口。""" + return [ + { + "id": row["id"], + "username": row["username"], + "role": row["role"], + "active": bool(row["active"]), + "must_change_password": bool(row["must_change_password"]), + "created_at": row["created_at"], + "updated_at": row["updated_at"], + } + for row in database.list_users() + ] + + +class Principal: + """当前请求的调用者:用户行 + 他实际拥有的权限码。""" + + def __init__(self, user, permissions: set[str]): + self.user = user + self.permissions = permissions + + @property + def id(self) -> int: + return int(self.user["id"]) + + def payload(self) -> dict[str, Any]: + return { + "id": self.id, + "username": self.user["username"], + "role": self.user["role"], + "must_change_password": bool(self.user["must_change_password"]), + "permissions": sorted(self.permissions), + } + + +def _frontend_dist() -> Path: + """Vue 前端的构建产物目录。 + + 单独抽出来是为了能在测试里替换掉——不然测试结果取决于本机有没有跑过 + `vite build`,同一份代码在开发机和 CI 上表现不一样。 + """ + return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "web-antd" / "dist" + + +def _mobile_dist() -> Path: + """手机远程控制台(admin-web/apps/mobile-h5)的构建产物,托管在 /m/。""" + return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "mobile-h5" / "dist" + + +def create_app( + db_path: Path | str = "backend.db", + zyt_token_verifier: Callable[[str], dict[str, Any]] | None = None, + zyt_patient_searcher: Callable[[str, str, int, int], dict[str, Any]] | None = None, + zyt_mobile_auth: Any = None, +) -> FastAPI: + database = backend.Database(Path(db_path).resolve()) + # 每个碰这个库的服务启动时都要把结构升到当前版本。只起 API、不起网页后台的 + # 部署,以前会撞上 `no such column`——而报错里看不出是漏了迁移。 + database.migrate() + release_directory = database.path.parent / "releases" + release_directory.mkdir(parents=True, exist_ok=True) + app = FastAPI(title="企微客服助手管理 API", version="2.0") + app.add_middleware( + CORSMiddleware, + allow_origins=DEV_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + def client_ip(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + return forwarded or (request.client.host if request.client else "") + + def current(request: Request) -> Principal: + raw = request.headers.get("authorization", "") + token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + user = database.session(token) if token else None + if user is not None and str(user["token_kind"]) != "api": + user = None + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已失效,请重新登录" + ) + return Principal(user, database.permissions_for_user(int(user["id"]))) + + def current_desktop(request: Request): + raw = request.headers.get("authorization", "") + token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + account = database.desktop_session(token) + if account is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="桌面登录已失效,请重新登录", + ) + return account + + def require(*codes: str): + """权限依赖。只认权限码,不认角色名。""" + + async def guard(principal: Principal = Depends(current)) -> Principal: + missing = [code for code in codes if code not in principal.permissions] + if missing: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"缺少权限:{'、'.join(missing)}", + ) + return principal + + return guard + + def resolve_admin_tenant( + principal: Principal, account_id: int | None + ) -> str | list[str]: + """把界面上选的客户端账号翻成这次查询的租户范围。 + + 下拉框有三档,这里是它们的含义: + + ALL_ACCOUNTS(-1) 全部。有 `desktop-account:all` 的返回 `"*"`(不限 + 租户);只被授权到若干账号的返回那几个账号的租户, + 「全部」对他就是「我能看的全部」,不是所有人的全部 + 0 / 不传 旧版未归属数据(tenant_id 为空或 default 的老记录) + 具体账号 id 那个账号的租户,且必须先过授权检查 + + 返回值有两种形状(一个租户 / 一组租户),下游统一交给 + `archive_store.tenant_scope` 和 `admin_backend.tenant_filter` 去翻译成 + SQL 条件——范围判断只在这一处做,散到各个接口里迟早会漏一个。 + """ + if account_id == ALL_ACCOUNTS: + if "desktop-account:all" in principal.permissions: + return backend.ALL_TENANTS + scoped = database.desktop_account_ids_for_admin(principal.id) + if not scoped: + # 没有被限制到任何账号 = 不受限,和上面一样能看全部。 + return backend.ALL_TENANTS + tenants = [] + for item in scoped: + account = database.desktop_account(int(item)) + if account is not None: + tenants.append(str(account["tenant_id"])) + if not tenants: + raise HTTPException(status_code=403, detail="没有已授权的客户端账号") + return tenants + if not account_id: + if ( + "desktop-account:all" not in principal.permissions + and database.desktop_account_ids_for_admin(principal.id) + ): + raise HTTPException(status_code=403, detail="请选择已授权的客户端账号") + return "default" + account = database.desktop_account(int(account_id)) + if account is None: + raise HTTPException(status_code=404, detail="客户端账号不存在") + if ( + "desktop-account:all" not in principal.permissions + and not database.admin_can_access_desktop_account(principal.id, int(account_id)) + ): + raise HTTPException(status_code=403, detail="无权查看该客户端账号的数据") + return str(account["tenant_id"]) + + def desktop_login_api_url() -> tuple[str, bool]: + """返回桌面登录服务地址,以及它是否来自管理端持久化配置。""" + + stored = json.loads(database.config()["config_json"]) + configured = str( + backend.effective_config(stored).get("ZYT_LOGIN_API_URL") or "" + ).strip().rstrip("/") + return (configured or zyt_auth.api_base_url(), bool(configured)) + + def release_payload(row: Any, *, direct_download: bool = False) -> dict[str, Any]: + keys = set(row.keys()) if hasattr(row, "keys") else set() + + def value(name: str, default: Any = "") -> Any: + return row[name] if name in keys else default + + result = { + "latest_version": str(value("latest_version")), + "download_url": str(value("download_url")), + "release_notes": str(value("release_notes")), + "force_upgrade": bool(value("force_upgrade", 0)), + "auto_install": bool(value("auto_install", 0)), + "package_sha256": str(value("package_sha256")), + "package_size": int(value("package_size", 0) or 0), + "signature_required": bool(value("signature_required", 0)), + "release_channel": str(value("release_channel", "stable")), + "rollout_percent": int(value("rollout_percent", 100) or 100), + "status": str(value("status", "published")), + "updated_at": str(value("updated_at", value("created_at", ""))), + "updated_by": str( + value("updated_by_name", value("created_by_name", "system")) + or "system" + ), + } + + identity = app.state.archive_store.release_package_identity(result['download_url']) + result['package_media_id'] = identity[0] if identity else '' + if direct_download and identity: + try: + result['download_url'] = app.state.archive_store.release_package_download_url(*identity) + except (KeyError, ValueError, RuntimeError) as exc: + raise HTTPException(status_code=409, detail=f"COS 安装包暂不可用:{exc}") from exc + except Exception as exc: + raise HTTPException(status_code=502, detail="COS 下载地址生成失败,请重试") from exc + return result + + # ── 桌面账号认证 ───────────────────────────────────────────────────── + @app.get("/api/v2/desktop/auth/config") + async def desktop_auth_config() -> dict: + login_api_url, configured = desktop_login_api_url() + return { + "zyt_api_url": login_api_url, + "terminal": zyt_auth.WECOM_RPA_TERMINAL, + "configured": configured, + } + + @app.post("/api/v2/desktop/auth/exchange") + async def desktop_auth_exchange(body: DesktopExchangeBody, request: Request) -> dict: + raw = request.headers.get("authorization", "") + zyt_token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + if not zyt_token: + raise HTTPException(status_code=401, detail="缺少 ZYT 登录令牌") + if zyt_token_verifier is not None: + verifier = zyt_token_verifier + else: + login_api_url, _configured = desktop_login_api_url() + verifier = lambda token: zyt_auth.verify_desktop_token( + token, api_url=login_api_url + ) + try: + identity = await asyncio.to_thread(verifier, zyt_token) + token, account = database.create_desktop_session( + identity, + body.device_id, + body.device_name, + body.app_version, + client_ip(request), + interactive_login=body.interactive_login, + ) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.save_zyt_session( + str(account["tenant_id"]), + zyt_token, + str(identity.get("user_id") or ""), + ) + except (zyt_auth.ZytAuthError, ValueError) as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + return { + "access_token": token, + "expires_in": max(0, int(account["expires_at"]) - int(time.time())), + "account": { + "id": int(account["id"]), + "sn": str(account["zyt_sn"]), + "nickname": str(account["nickname"]), + "tenant_id": str(account["tenant_id"]), + "device_id": str(account["device_id"]), + }, + } + + @app.post("/api/v2/desktop/auth/zyt-session") + async def desktop_refresh_zyt_session( + body: DesktopZytSessionBody, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + raw = request.headers.get("authorization", "") + desktop_token = raw[7:].strip() if raw.lower().startswith("bearer ") else "" + if zyt_token_verifier is not None: + verifier = zyt_token_verifier + else: + login_api_url, _configured = desktop_login_api_url() + verifier = lambda token: zyt_auth.verify_desktop_token(token, api_url=login_api_url) + try: + identity = await asyncio.to_thread(verifier, body.zyt_token.strip()) + if (not isinstance(identity, dict) or not identity.get("user_id") + or str(identity.get("terminal")) != str(zyt_auth.WECOM_RPA_TERMINAL) + or str(identity.get("status") or "") != "active"): + raise zyt_auth.ZytAuthError("ZYT 桌面身份验证失败,请重新验证账号") + except zyt_auth.ZytAuthError as exc: + # The identity adapter preserves transport/JSON causes. An outage must + # not send an otherwise logged-in user into a reauthentication loop. + cause = exc.__cause__ + http_code = cause.code if isinstance(cause, urllib.error.HTTPError) else None + if http_code == 403 or any(word in str(exc) for word in ("权限", "无权")): + raise HTTPException(status_code=403, detail="ZYT 账号无权验证患者查询身份,请联系管理员。") from exc + unavailable = ( + (http_code is not None and http_code != 401) + or (http_code is None and isinstance(cause, (urllib.error.URLError, OSError, ValueError, TypeError))) + or any(word in str(exc) for word in ("无法连接", "无效数据", "身份信息不完整", "配置不正确", "尚未部署")) + ) + if unavailable: + raise HTTPException(status_code=502, detail={ + "code": "zyt_unavailable", + "message": "ZYT 身份服务暂不可用,请稍后重试。", + }) from exc + raise HTTPException(status_code=409, detail={ + "code": "zyt_session_expired", + "message": "ZYT 患者查询登录已过期,请重新验证账号。", + }) from exc + if str(identity["user_id"]).strip() != str(account["zyt_user_id"]): + raise HTTPException(status_code=403, detail="请使用当前桌面账号重新验证,不能切换到其他账号") + archive_store = getattr(app.state, "archive_store", None) + if archive_store is None: + raise HTTPException(status_code=503, detail="患者查询服务暂不可用") + try: + await asyncio.to_thread( + archive_store.refresh_zyt_session, desktop_token, int(account["id"]), + str(account["tenant_id"]), body.zyt_token, identity, + ) + except PermissionError as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + return {"ok": True} + + @app.get("/api/v2/desktop/me") + async def desktop_me(account: Any = Depends(current_desktop)) -> dict: + return { + "id": int(account["id"]), + "sn": str(account["zyt_sn"]), + "nickname": str(account["nickname"]), + "tenant_id": str(account["tenant_id"]), + "device_id": str(account["device_id"]), + "expires_at": int(account["expires_at"]), + } + + @app.post("/api/v2/desktop/auth/logout") + async def desktop_auth_logout( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + raw = request.headers.get("authorization", "") + database.revoke_desktop_session(raw[7:].strip()) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(str(account["tenant_id"])) + database.audit( + None, + "desktop.logout", + f"account={account['id']} device={account['device_id']}", + client_ip(request), + ) + return {"ok": True} + + # ── 管理端认证 ─────────────────────────────────────────────────────── + @app.post("/api/v2/auth/login") + async def login(body: LoginBody, request: Request) -> dict: + ip = client_ip(request) + key = f"{ip}:{body.username.lower()}" + if not backend.LOGIN_LIMITER.allowed(key): + raise HTTPException(status_code=429, detail="登录失败次数过多,请稍后再试") + user = database.authenticate(body.username, body.password) + if not user: + backend.LOGIN_LIMITER.failure(key) + database.audit(None, "login.failed", f"username={body.username}", ip) + raise HTTPException(status_code=401, detail="用户名或密码不正确") + backend.LOGIN_LIMITER.success(key) + token, _ = database.create_token( + int(user["id"]), "api", body.device_name, 30 * 86400 + ) + database.audit(int(user["id"]), "login.api", body.device_name, ip) + principal = Principal(user, database.permissions_for_user(int(user["id"]))) + # 首次登录不拦 API:改密页面本身也要调接口。前端凭 + # must_change_password 把用户锁在改密页上,比后端一刀切 403 好用得多 + # ——老实现那样连改密接口都调不通,只能去网页后台改。 + return { + "access_token": token, + "expires_in": 30 * 86400, + "user": principal.payload(), + } + + @app.post("/api/v2/auth/logout") + async def logout(request: Request, principal: Principal = Depends(current)) -> dict: + raw = request.headers.get("authorization", "") + database.revoke(raw[7:].strip() if raw.lower().startswith("bearer ") else "") + database.audit(principal.id, "logout.api", "", client_ip(request)) + return {"ok": True} + + @app.get("/api/v2/me") + async def me(principal: Principal = Depends(current)) -> dict: + """前端路由守卫和按钮显隐的唯一数据源。""" + return principal.payload() + + @app.post("/api/v2/me/password") + async def change_password( + body: PasswordBody, request: Request, principal: Principal = Depends(current) + ) -> dict: + if not backend.valid_password(body.new_password): + raise HTTPException(status_code=400, detail="密码至少 10 位,且需同时含字母和数字") + try: + database.change_password( + principal.id, body.current_password, body.new_password, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"ok": True} + + # ── 角色与权限 ─────────────────────────────────────────────────────── + @app.get("/api/v2/permissions") + async def permissions(_: Principal = Depends(require("role:write"))) -> dict: + return {"permissions": database.permission_catalog()} + + @app.get("/api/v2/roles") + async def list_roles(_: Principal = Depends(require("user:read"))) -> dict: + return {"roles": database.roles()} + + @app.post("/api/v2/roles") + async def save_role( + body: RoleBody, request: Request, principal: Principal = Depends(require("role:write")) + ) -> dict: + try: + saved = database.save_role( + body.code, body.name, body.permissions, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"role": saved, "roles": database.roles()} + + @app.delete("/api/v2/roles/{code}") + async def delete_role( + code: str, request: Request, principal: Principal = Depends(require("role:write")) + ) -> dict: + try: + database.delete_role(code, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"roles": database.roles()} + + # ── 用户 ───────────────────────────────────────────────────────────── + @app.get("/api/v2/users") + async def list_users(_: Principal = Depends(require("user:read"))) -> dict: + return {"users": _public_users(database)} + + @app.post("/api/v2/users") + async def create_user( + body: UserBody, request: Request, principal: Principal = Depends(require("user:write")) + ) -> dict: + if not backend.valid_password(body.password): + raise HTTPException(status_code=400, detail="密码至少 10 位,且需同时含字母和数字") + known = {item["code"] for item in database.roles()} + if body.role not in known: + raise HTTPException(status_code=400, detail=f"角色不存在:{body.role}") + try: + database.create_user( + body.username, body.password, body.role, principal.id, client_ip(request) + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"users": _public_users(database)} + + @app.put("/api/v2/users/{user_id}") + async def update_user( + user_id: int, + body: UserBody, + request: Request, + principal: Principal = Depends(require("user:write")), + ) -> dict: + known = {item["code"] for item in database.roles()} + if body.role not in known: + raise HTTPException(status_code=400, detail=f"角色不存在:{body.role}") + try: + database.update_user( + user_id, body.role, body.active, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"users": _public_users(database)} + + # ── 客户端账号与设备 ───────────────────────────────────────────────── + @app.get("/api/v2/desktop-accounts") + def list_desktop_accounts( + include_counts: bool = True, + principal: Principal = Depends(current), + ) -> dict: + allowed = ( + None + if "desktop-account:all" in principal.permissions + else database.desktop_account_ids_for_admin(principal.id) + ) + accounts = database.desktop_accounts(allowed, include_counts=include_counts) + mobile_counts = app.state.remote_store.mobile_session_counts() + for account in accounts: + account["mobile_session_count"] = mobile_counts.get(int(account["id"]), 0) + return { + "accounts": accounts, + "can_view_all": "desktop-account:all" in principal.permissions, + # 尚未分配任何账号的旧角色仍能查看升级前的未归属数据;一旦分配账号, + # 就必须明确选择授权账号,不能再借默认值绕过范围限制。 + "can_view_legacy": ( + "desktop-account:all" in principal.permissions or not allowed + ), + } + + @app.put("/api/v2/desktop-accounts/{account_id}") + async def update_desktop_account( + account_id: int, + body: DesktopAccountBody, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + try: + account = database.update_desktop_account( + account_id, + status=body.status, + backup_enabled=body.backup_enabled, + backup_interval_minutes=body.backup_interval_minutes, + actor_id=principal.id, + ip=client_ip(request), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if str(account.get("status") or "") == "disabled": + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(str(account["tenant_id"])) + app.state.remote_store.revoke_mobile_sessions_for_account( + account_id, reason="account_disabled" + ) + return {"account": account} + + @app.post("/api/v2/desktop-accounts/{account_id}/revoke-devices") + async def revoke_desktop_devices( + account_id: int, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + tenant = resolve_admin_tenant(principal, account_id) + database.revoke_desktop_devices(account_id, principal.id, client_ip(request)) + archive_store = getattr(app.state, "archive_store", None) + if archive_store is not None: + archive_store.clear_zyt_session(tenant) + # 「强制下线」同时收回手机控制台的登录,否则被下线的人还能远程启停。 + app.state.remote_store.revoke_mobile_sessions_for_account( + account_id, reason="admin_revoke" + ) + return {"ok": True} + + @app.get("/api/v2/desktop-accounts/{account_id}/admins") + async def desktop_account_admins( + account_id: int, + principal: Principal = Depends(require("desktop-account:read")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + return {"admin_user_ids": database.desktop_account_admin_ids(account_id)} + + @app.put("/api/v2/desktop-accounts/{account_id}/admins") + async def update_desktop_account_admins( + account_id: int, + body: DesktopAccountScopeBody, + request: Request, + principal: Principal = Depends(require("desktop-account:write")), + ) -> dict: + resolve_admin_tenant(principal, account_id) + try: + database.set_desktop_account_admins( + account_id, body.admin_user_ids, principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"admin_user_ids": database.desktop_account_admin_ids(account_id)} + + # ── 模型清单与编排 ─────────────────────────────────────────────────── + @app.get("/api/v2/models") + async def list_models(_: Principal = Depends(require("model:read"))) -> dict: + return { + "models": database.model_providers(), + "roles": database.model_roles(), + "kinds": ["dify", "openai", "claude", "comfyui"], + "judge_modes": ["shadow", "score_only", "arbitrate"], + } + + @app.post("/api/v2/models") + async def save_model( + body: ProviderBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + database.save_model_provider( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"models": database.model_providers()} + + @app.delete("/api/v2/models/{provider_id}") + async def delete_model( + provider_id: str, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + database.delete_model_provider(provider_id, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"models": database.model_providers()} + + @app.post("/api/v2/models/plan") + async def save_plan( + body: RolesPlanBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + try: + version = database.save_model_roles( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"version": version, "roles": database.model_roles()} + + # ── 智能体 ─────────────────────────────────────────────────────────── + # 模型清单管的是"用哪个模型",这里管的是"用什么身份、按什么规矩说话"。分开 + # 是因为这两件事的变更频率差一个数量级:模型半年动一次,话术一周动三次。 + @app.get("/api/v2/agents") + async def list_agents(_: Principal = Depends(require("agent:read"))) -> dict: + return { + "agents": database.agents(), + "plan": database.agent_plan(), + "rule_types": list(backend.AGENT_RULE_TYPES), + "plan_modes": list(backend.AGENT_PLAN_MODES), + } + + @app.post("/api/v2/agents") + async def save_agent( + body: AgentBody, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + database.save_agent(body.model_dump(), principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"agents": database.agents(), "plan": database.agent_plan()} + + @app.delete("/api/v2/agents/{agent_id}") + async def delete_agent( + agent_id: str, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + database.delete_agent(agent_id, principal.id, client_ip(request)) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"agents": database.agents(), "plan": database.agent_plan()} + + @app.post("/api/v2/agents/plan") + async def save_agent_plan( + body: AgentPlanBody, + request: Request, + principal: Principal = Depends(require("agent:write")), + ) -> dict: + try: + version = database.save_agent_plan( + body.model_dump(), principal.id, client_ip(request) + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"version": version, "plan": database.agent_plan()} + + # ── 运营 ───────────────────────────────────────────────────────────── + @app.get("/api/v2/stats/model-calls") + async def model_call_stats( + days: int = 7, + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return { + "days": days, + **database.model_call_stats( + days, resolve_admin_tenant(principal, account_id) + ), + } + + @app.get("/api/v2/stats/model-calls/log") + async def model_call_log( + days: int = 7, + limit: int = 50, + offset: int = 0, + q: str = "", + # chat = 真实对话(默认);guard = 界面识别;留空 = 全都要 + purpose: str = "chat", + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.list_model_calls( + days, + limit, + offset, + q, + purpose, + resolve_admin_tenant(principal, account_id), + ) + + @app.get("/api/v2/stats/model-usage") + async def model_usage_stats( + days: int = Query(default=7, ge=1, le=90), + purpose: str = Query(default="", pattern="^(chat|guard|knowledge)?$"), + provider_id: str = Query(default="", max_length=256), + model: str = Query(default="", max_length=512), + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.model_usage_stats( + days, resolve_admin_tenant(principal, account_id), purpose, provider_id, model) + + @app.get("/api/v2/stats/model-usage/log") + async def model_usage_log( + days: int = Query(default=7, ge=1, le=90), + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + purpose: str = Query(default="", pattern="^(chat|guard|knowledge)?$"), + provider_id: str = Query(default="", max_length=256), + model: str = Query(default="", max_length=512), + account_id: int | None = None, + principal: Principal = Depends(require("stats:read")), + ) -> dict: + return database.list_model_usage( + days, limit, offset, resolve_admin_tenant(principal, account_id), purpose, provider_id, model) + + # ── 桌面端配置 ─────────────────────────────────────────────────────── + # 这一组是从老网页后台整体搬过来的("能力开关 / 模型与身份 / MCP 服务器" + # 三个区块)。桌面端启动和定时同步都拉这份配置,改错一个字段全线客户端 + # 立刻受影响——所以校验函数直接复用老后台那一份,不另写一套。 + @app.get("/api/v2/config") + async def get_config( + request: Request, _: Principal = Depends(require("config:read")) + ) -> dict: + row = database.config() + stored = json.loads(row["config_json"]) + # 只回当前在用的字段。库里老记录还带着已退休的模型参数(不删用户 + # 数据),但把它们发给前端只会让人以为那里还能改。 + config = backend.effective_config(stored) + return { + "config": config, + "version": int(row["version"]), + "updated_at": row["updated_at"], + "updated_by": row["updated_by_name"] or "system", + "bool_keys": sorted(backend.BOOL_KEYS), + # 已经搬走的字段,连同它们现在的去处。前端拿它渲染一条指路说明, + # 而不是让人对着一个"以前在这儿的东西不见了"的页面发愣。 + "retired_keys": list(backend.RETIRED_CONFIG_KEYS), + "model_settings_moved_to": "AI 模型 → 模型清单 / 角色编排", + # 留空时实际会下发什么。不显示出来的话,"自动推算"对使用的人就是 + # 一个黑盒——出问题时没法判断是推错了还是网关没起。 + "gateway_url_effective": backend.derive_gateway_url( + str(config.get("AI_GATEWAY_URL") or ""), + request.headers.get("x-forwarded-proto", request.url.scheme), + request.headers.get("host", ""), + ), + # 登录窗口在用户输入账号前会读取公开配置接口。这里显示它最终会 + # 拿到哪个地址,便于管理员保存后当场核对。 + "login_api_url_effective": desktop_login_api_url()[0], + } + + @app.post("/api/v2/config") + async def save_config( + body: ConfigBody, + request: Request, + principal: Principal = Depends(require("config:write")), + ) -> dict: + current = json.loads(database.config()["config_json"]) + # validate_config_form 收的是 HTML 表单那种"全是字符串"的字典,布尔用 + # "1"/"" 表示。这里把 JSON 转成它认识的形状,而不是把它改成认 JSON—— + # 老后台还在跑,改它等于同时动两个正在服务的系统。 + form: dict[str, str] = {} + for key, value in body.model_dump().items(): + if key in backend.BOOL_KEYS: + form[key] = "1" if value else "" + elif key in ("AI_MCP_SERVERS", "AI_REVIEW_RULES"): + form[key] = json.dumps(value, ensure_ascii=False) + else: + form[key] = str(value) + try: + config = backend.validate_config_form(form, current) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + version = database.save_config(config, principal.id, client_ip(request)) + return {"ok": True, "version": version} + + # ── 桌面端版本升级 ─────────────────────────────────────────────────── + @app.get("/api/v2/release") + async def get_release(_: Principal = Depends(require("config:read"))) -> dict: + return release_payload(database.release(), direct_download=True) + + @app.post("/api/v2/release") + async def save_release( + body: ReleaseBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + download_url = app.state.archive_store.normalize_release_download( + body.download_url, body.package_sha256, body.package_size, body.package_media_id + ) + release = backend.validate_release_form( + { + "latest_version": body.latest_version, + "download_url": download_url, + "release_notes": body.release_notes, + "force_upgrade": "1" if body.force_upgrade else "", + "auto_install": "1" if body.auto_install else "", + "package_sha256": body.package_sha256, + "package_size": str(body.package_size), + "signature_required": "1" if body.signature_required else "", + "release_channel": body.release_channel, + "rollout_percent": str(body.rollout_percent), + "status": body.status, + } + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + database.save_release( + release["latest_version"], + release["download_url"], + release["release_notes"], + release["force_upgrade"], + principal.id, + client_ip(request), + auto_install=release["auto_install"], + package_sha256=release["package_sha256"], + package_size=release["package_size"], + signature_required=release["signature_required"], + release_channel=release["release_channel"], + rollout_percent=release["rollout_percent"], + status=release["status"], + ) + return {"ok": True, **release_payload(database.release(), direct_download=True)} + + @app.post("/api/v2/release/package/prepare") + async def prepare_release_package( + body: ReleasePackagePrepareBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + """签发 COS 直传地址;EXE 内容不会进入管理端进程或本机磁盘。""" + + try: + store = app.state.archive_store + prepared = store.prepare_release_package( + { + "original_filename": body.file_name, + "sha256": body.package_sha256.lower(), + "size_bytes": body.package_size, + } + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException( + status_code=502, detail=f"COS 预上传失败:{exc}" + ) from exc + database.audit( + principal.id, + "release.package.prepare", + f"file={prepared['file_name']} size={prepared['package_size']}", + client_ip(request), + ) + return prepared + + @app.post("/api/v2/release/package/{media_id}/complete") + async def complete_release_package( + media_id: str, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + package = app.state.archive_store.complete_release_package(media_id) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 文件校验失败:{exc}") from exc + database.audit( + principal.id, + "release.package.complete", + f"media={media_id} size={package['package_size']}", + client_ip(request), + ) + return package + + @app.post("/api/v2/release/package/{media_id}/multipart-complete") + async def complete_multipart_release_package( + media_id: str, + body: ReleasePackageMultipartCompleteBody, + request: Request, + principal: Principal = Depends(require("release:write")), + ) -> dict: + try: + package = app.state.archive_store.complete_multipart_release_package( + media_id, + body.upload_id, + [part.model_dump() for part in body.parts], + ) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 分块合并失败:{exc}") from exc + database.audit( + principal.id, + "release.package.complete", + f"media={media_id} multipart=1 size={package['package_size']}", + client_ip(request), + ) + return package + + @app.get("/api/v2/releases") + async def release_history( + _: Principal = Depends(require("config:read")), + ) -> dict: + current = release_payload(database.release()) + return { + "history": [release_payload(row) for row in database.release_history()], + "reports": database.update_report_summary(current["latest_version"]), + } + + @app.get("/api/v2/desktop/update/packages/{filename}") + async def desktop_update_package(filename: str) -> FileResponse: + if Path(filename).name != filename or not filename.lower().endswith(".exe"): + raise HTTPException(status_code=404, detail="升级包不存在") + target = (release_directory / filename).resolve() + if target.parent != release_directory.resolve() or not target.is_file(): + raise HTTPException(status_code=404, detail="升级包不存在") + return FileResponse( + target, + media_type="application/vnd.microsoft.portable-executable", + filename=filename, + ) + + @app.get("/api/v2/desktop/update/cos-packages/{media_id}/{filename}") + async def desktop_update_cos_package(media_id: str, filename: str) -> RedirectResponse: + try: + url = app.state.archive_store.release_package_download_url( + media_id, filename + ) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc).strip("'")) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=f"COS 下载签名失败:{exc}") from exc + return RedirectResponse(url=url, status_code=307) + + @app.get("/api/v2/desktop/update/manifest") + async def desktop_update_manifest( + request: Request, + current_version: str = "", + device_id: str = "", + channel: str = "stable", + ) -> dict: + release = release_payload(database.release()) + latest = release["latest_version"] + eligible = release["status"] == "published" and release["release_channel"] == channel + if eligible and release["rollout_percent"] < 100: + bucket = int( + hashlib.sha256(f"{device_id}:{latest}".encode("utf-8")).hexdigest()[:8], + 16, + ) % 100 + 1 + eligible = bucket <= release["rollout_percent"] + update_available = bool( + eligible and app_version.is_newer_version(latest, current_version) + ) + download_url = release["download_url"] + identity = app.state.archive_store.release_package_identity(download_url) + if identity: + # Each check gets a fresh COS HTTPS URL, including old release rows. + download_url = release_payload(release, direct_download=True)["download_url"] if update_available else "" + elif download_url.startswith("/"): + forwarded_scheme = str( + request.headers.get("x-forwarded-proto") or request.url.scheme + ).split(",", 1)[0].strip() + forwarded_host = str( + request.headers.get("x-forwarded-host") + or request.headers.get("host") + or request.url.netloc + ).split(",", 1)[0].strip() + public_base = ( + f"{forwarded_scheme}://{forwarded_host}/" + if forwarded_scheme and forwarded_host + else str(request.base_url) + ) + download_url = urllib.parse.urljoin( + public_base, download_url.lstrip("/") + ) + return { + **release, + "download_url": download_url, + "download_url_expires_in": 900 if identity and update_available else 0, + "update_available": update_available, + "eligible": eligible, + } + + @app.post("/api/v2/desktop/update/report") + async def desktop_update_report( + body: UpdateReportBody, + account: Any = Depends(current_desktop), + ) -> dict: + allowed = { + "available", + "downloading", + "downloaded", + "installing", + "installed", + "failed", + "deferred", + } + if body.status not in allowed: + raise HTTPException(status_code=400, detail="升级状态不正确") + if not app_version.normalize_version(body.target_version): + raise HTTPException(status_code=400, detail="目标版本号不正确") + database.record_update_report( + int(account["id"]), + str(account["device_id"]), + body.current_version, + body.target_version, + body.status, + body.progress, + body.error, + ) + return {"ok": True} + + # ── 模型连通性测试 ─────────────────────────────────────────────────── + @app.post("/api/v2/models/test") + async def test_model( + body: ModelTestBody, + request: Request, + principal: Principal = Depends(require("model:write")), + ) -> dict: + """真的向模型发一次最小请求,返回不含密钥的诊断结果。 + + 要 `model:write` 而不是 `model:read`:这个动作会拿着真实密钥向外网发请 + 求,还可能产生费用。只读用户能看模型清单,但不该能替公司花钱。 + + 密钥来源有两处,都不经过前端:指定了 `provider_id` 就从库里解密取;否则 + 用请求里临时填的(新建模型还没保存时要用)。 + """ + kind = str(body.kind or "").strip().lower() + base_url = str(body.base_url or "").strip() + model = str(body.model or "").strip() + api_key = str(body.api_key or "").strip() + endpoint_mode = str(body.endpoint_mode or "").strip().lower() + label = "" + + if body.provider_id: + match = next( + ( + item + for item in database.model_providers(include_secrets=True) + if str(item.get("id")) == body.provider_id + ), + None, + ) + if match is None: + raise HTTPException(status_code=404, detail="模型不存在") + label = str(match.get("name") or match.get("id") or "") + kind = kind or str(match.get("kind") or "") + base_url = base_url or str(match.get("base_url") or "") + model = model or str(match.get("model") or "") + # 前端提交空密钥 = 用库里存的那把。这样"测一下现有配置通不通" + # 不需要把密钥再发一遍,也就没有它在网络上多走一趟的机会。 + api_key = api_key or str(match.get("api_key") or "") + endpoint_mode = endpoint_mode or str(match.get("endpoint_mode") or "auto") + + if kind == "claude": + # 老后台的测试器只认 openai / dify / comfyui 三种。Claude 是新增 + # 的出口类型,端点和鉴权头都不一样,硬套 openai 只会得到 404。 + raise HTTPException( + status_code=400, + detail="Claude 出口暂不支持一键连通性测试,请在模型网关的调用统计里确认", + ) + if kind not in backend.PROVIDER_TYPES: + raise HTTPException( + status_code=400, + detail=f"不支持的接口类型:{kind or '(空)'}", + ) + if not base_url: + raise HTTPException(status_code=400, detail="接口地址不能为空") + + try: + config = backend.model_test_config( + { + "AI_PROVIDER_TYPE": kind, + "AI_API_BASE": base_url, + "AI_MODEL": model, + "AI_API_KEY": api_key, + "AI_TIMEOUT": body.timeout_seconds, + "AI_ENDPOINT_MODE": endpoint_mode or "auto", + }, + {}, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + result = backend.test_model_connection(config) + database.audit( + principal.id, + "model.test", + f"provider={label or body.provider_id or '临时'}, ok={int(bool(result['ok']))}, " + f"http={result.get('http_status')}, endpoint={str(result.get('endpoint') or '')[:200]}", + client_ip(request), + ) + # 故意返回 200 而不是 502:测试本身跑成功了,"连不上"是它的结论, + # 不是这个接口的失败。返 5xx 会让前端的错误拦截器弹一个通用报错, + # 把真正有用的诊断信息盖掉。 + return {"result": result, "label": label} + + # ── 桌面端同步 ─────────────────────────────────────────────────────── + # 桌面客户端拉的就是这一个接口。老后台(8765)已退役,这里是唯一来源; + # 客户端仍保留 v2→v1 回退,是为了兼容还没升级到新服务端的部署。 + @app.get("/api/v2/desktop/config") + async def desktop_config( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + # 这一份的结构由 `admin_backend.desktop_config_payload` 统一给出。 + # 两处各写一份的话迟早漂移,表现成"某台客户端少了个字段",极难查。 + payload = backend.desktop_config_payload( + database, + scheme=request.headers.get("x-forwarded-proto", request.url.scheme), + host=request.headers.get("host", ""), + desktop_account=account, + ) + knowledge = app.state.knowledge_store + tenant = str(account["tenant_id"]) + counts = await asyncio.to_thread(knowledge.stats, tenant) + settings = await asyncio.to_thread(knowledge.settings, tenant) + payload["knowledge"] = {"published": counts.get("published", 0), + "enabled": bool(settings["enabled"]), "updated_at": settings["updated_at"]} + return payload + + # 桌面端回流一次编排调用。补上这条之前,客户端改指 8766 会丢掉全部调用留痕 + # ——配置照常同步、回复照常发,唯独调用记录一条不进库,而那恰恰是出事后用来 + # 解释"这句话怎么来的"的东西。它是老后台退役的最后一块拼图。 + @app.post("/api/v2/model/calls") + async def log_model_call( + request: Request, account: Any = Depends(current_desktop) + ) -> dict: + try: + record = await request.json() + except Exception: + record = {} + # 落库失败也返回 200:这是观测数据,不能因为它出问题就让桌面端以为 + # 回复流程失败了。`log_model_call` 内部已经吞掉异常并打日志。 + if isinstance(record, dict): + record["desktop_account_id"] = int(account["id"]) + record["tenant_id"] = str(account["tenant_id"]) + database.log_model_call(record) + return {"ok": True} + + @app.get("/api/v2/audit") + async def audit_log( + limit: int = 200, _: Principal = Depends(require("audit:read")) + ) -> dict: + return {"entries": database.audit_entries(limit)} + + @app.get("/api/v2/health") + async def health() -> dict: + return {"status": "ok", "time": time.strftime("%Y-%m-%d %H:%M:%S")} + + # 聊天归档是独立新模块,只复用当前登录、权限和审计能力。 + # 必须在 SPA 通配路由之前注册,避免 /api 请求被静态页入口截住。 + archive_api.register_archive_routes( + app, + database, + current, + require, + client_ip, + current_desktop=current_desktop, + resolve_admin_tenant=resolve_admin_tenant, + zyt_api_url=lambda: desktop_login_api_url()[0], + zyt_patient_searcher=zyt_patient_searcher, + ) + + knowledge_api.register_knowledge_routes( + app, database, require, client_ip, resolve_admin_tenant, current_desktop, + ) + + remote_api.register_remote_routes( + app, + database, + current_desktop=current_desktop, + zyt_api_url=lambda: desktop_login_api_url()[0], + zyt_mobile_auth=zyt_mobile_auth, + ) + + # ── 手机远程控制台 /m/ ─────────────────────────────────────────────── + # 必须注册在管理端 SPA 的通配路由之前,否则 /m/ 会被当成管理端的前端路由。 + mobile_dist = _mobile_dist() + if (mobile_dist / "index.html").is_file(): + from starlette.responses import FileResponse as _MobileFile + + mobile_root = mobile_dist.resolve() + + @app.get("/m", include_in_schema=False) + async def mobile_entry_redirect(): + return RedirectResponse("/m/", status_code=308) + + # 远程启停只要"点按钮、点确认"两下,页面不许被别的站点嵌进 iframe 诱导点击。 + mobile_headers = { + "X-Frame-Options": "DENY", + "Content-Security-Policy": ( + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; font-src 'self' data:; connect-src 'self'; " + "manifest-src 'self'; worker-src 'self'; frame-ancestors 'none'; " + "base-uri 'self'; form-action 'self'" + ), + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + } + + @app.get("/m/{path:path}", include_in_schema=False) + async def mobile_spa(path: str): + candidate = (mobile_root / path).resolve() if path else mobile_root / "index.html" + if not candidate.is_relative_to(mobile_root) or not candidate.is_file(): + # 发版后还开着的旧页面会去拉旧的哈希文件:回 404 让前端整页刷新到 + # 新版本;回 index.html 的话浏览器会把 HTML 当脚本执行,页面直接卡死。 + if path.startswith("assets/"): + raise HTTPException(status_code=404, detail="文件不存在") + candidate = mobile_root / "index.html" + response = _MobileFile(candidate) + # 文件名带哈希的构建产物可以长缓存;入口、清单和 Service Worker 每次都 + # 要重新验证,否则发版后手机上会一直卡在旧界面。 + if candidate.parent.name == "assets" and candidate.parent.parent == mobile_root: + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "no-cache" + response.headers.update(mobile_headers) + return response + + # ── 前端静态资源 ───────────────────────────────────────────────────── + # 构建产物存在时由本服务一起托管,前后端同源——省掉生产环境的 CORS 配置, + # 也省掉一个 Nginx location。开发时前端跑 Vite dev server,走它自己的代理。 + dist = _frontend_dist() + if (dist / "index.html").is_file(): + from fastapi.staticfiles import StaticFiles + from starlette.responses import FileResponse + + # 判据用 index.html 而不是目录本身:构建中途、或者只剩一个空 dist 目录 + # 的时候,目录是在的但没有可服务的东西,挂上去只会让 SPA 兜底路由把 + # 每个请求都回一个不存在的文件。 + # + # 静态子目录也必须逐个确认存在——StaticFiles 的构造函数遇到不存在的 + # 目录**直接抛异常**,整个服务起不来。vite 默认输出的是 js/ 和 css/, + # 不是 assets/;写死 assets 的话,一旦真的有了构建产物,API 就崩在启动。 + for name in ("assets", "js", "css", "static"): + sub = dist / name + if sub.is_dir(): + app.mount(f"/{name}", StaticFiles(directory=sub), name=name) + + @app.get("/{full_path:path}") + async def spa(full_path: str): + """SPA 的 history 路由:非 /api 的路径一律回 index.html。 + + 少了这一条,用户在 /system/roles 上按 F5 会拿到 404——前端路由是 + 浏览器端的,服务器上并不存在那个文件。 + """ + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="接口不存在") + candidate = dist / full_path + if full_path and candidate.is_file(): + return FileResponse(candidate) + return FileResponse(dist / "index.html") + + return app + + +def main() -> None: + parser = argparse.ArgumentParser(description="管理后台 JSON API") + parser.add_argument("--db", default="backend.db") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8766) + parser.add_argument( + "--zyt-api-url", + default=os.environ.get("ZYT_API_BASE_URL", zyt_auth.DEFAULT_ZYT_API_BASE_URL), + help="ZYT 用户 API 根地址,例如 https://example.com", + ) + parser.add_argument( + "--runtime-file", + default=str(backend.DEFAULT_RUNTIME_FILE), + help="发布实际监听地址,供同机桌面端自动发现", + ) + parser.add_argument( + "--initial-admin-password", + default=os.environ.get( + "WECOM_ADMIN_INITIAL_PASSWORD", backend.DEFAULT_ADMIN_PASSWORD + ), + help="首次创建数据库时的 admin 密码", + ) + parser.add_argument( + "--reset-admin-password", + action="store_true", + help="交互式重置 admin 密码后退出", + ) + args = parser.parse_args() + zyt_auth.DEFAULT_ZYT_API_BASE_URL = str(args.zyt_api_url).strip().rstrip("/") + + # 建库并按需播种 admin。这两件事以前只有老网页后台的 main() 做——不搬过来的 + # 话,全新部署起来之后库里一个用户都没有,Vue 管理端登录页永远进不去,而且 + # 报的是"用户名或密码不正确",完全看不出是根本没建过账号。 + database = backend.Database(Path(args.db).resolve()) + created = database.initialize(args.initial_admin_password) + + if args.reset_admin_password: + import getpass + + first = getpass.getpass("新的 admin 密码:") + if first != getpass.getpass("再次输入:"): + raise SystemExit("两次输入不一致") + if not backend.valid_password(first): + raise SystemExit("密码至少 10 位,并同时包含字母和数字") + database.reset_admin_password(first) + print("admin 密码已重置,下次登录时必须再次修改。") + return + + if created: + print("首次登录账号:admin") + print(f"首次登录密码:{args.initial_admin_password}") + print("登录后必须立即修改初始密码。") + if args.host not in ("127.0.0.1", "localhost", "::1"): + print("警告:当前监听非本机地址;生产环境请通过 HTTPS 反向代理访问。") + + import uvicorn + + # 发布本机地址,桌面端靠它自动发现后台。 + # + # 这件事以前只有老网页后台(8765)做。不搬过来的话,老后台一停,桌面端的 + # 自动发现就永远指向一个没人监听的 8765,报出来的是"无法连接后台"——而真正 + # 在跑的 8766 从来没被试过。这是老后台能退役的前提之一。 + runtime_path = Path(args.runtime_file) + backend.write_runtime_info(runtime_path, args.host, args.port) + try: + uvicorn.run( + create_app(args.db), host=args.host, port=args.port, log_level="info" + ) + finally: + backend.clear_runtime_info(runtime_path, args.port) + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/remote_api.py b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/remote_api.py new file mode 100644 index 000000000..ee06afa2e --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/remote_api.py @@ -0,0 +1,802 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台与桌面命令通道的 HTTP 接口。 + + /api/v2/mobile/* 手机 / 平板 H5 用,ZYT 账号密码登录(终端 9,由本服务代登) + /api/v2/desktop/commands/* 桌面端长轮询领取指令、回执结果、顺带上报状态 + /api/v2/desktop/blacklist* 桌面端同步服务端会话黑名单 + +手机令牌和桌面令牌分别由 ``mobile_sessions`` 和 ``desktop_sessions`` 签发,互相 +不能冒用:手机端拿不到归档上传之类的桌面接口,桌面端也调不了手机接口。 +""" + +from __future__ import annotations + +import asyncio +import ipaddress +import os +import threading +import time +from collections import defaultdict, deque +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request +from pydantic import BaseModel, Field + +import zyt_auth +from remote_store import ( + COMMAND_TYPES, + MOBILE_IDENTITY_RECHECK_SECONDS, + PERM_BLACKLIST_ADD, + PERM_BLACKLIST_REMOVE, + PERM_REMOTE_ALL, + PERM_REMOTE_CONTROL, + RemoteStore, + account_label, + account_permissions, + valid_device_id, +) + +# 桌面端长轮询最长挂起时间。uvicorn 停机时会等在途请求结束,这里必须明显短于 +# compose 里 20 秒的 stop_grace_period,否则每次发版都会被强杀;指令本身靠进程内 +# 通知即时送达,挂起时长只影响"没事时多久回来报一次状态"。 +MAX_POLL_WAIT_SECONDS = 15 +# 挂起期间多久回库复查一次;进程内通知失效(多进程部署)时的兜底。 +POLL_RECHECK_SECONDS = 5 + + +def _error(status_code: int, code: str, message: str, **extra: Any) -> HTTPException: + return HTTPException(status_code=status_code, detail={"code": code, "message": message, **extra}) + + +def _bearer(request: Request) -> str: + raw = request.headers.get("authorization", "") + return raw[7:].strip() if raw.lower().startswith("bearer ") else "" + + +def trusted_client_ip(request: Request) -> str: + """做安全判断用的客户端地址。 + + X-Forwarded-For 的第一段是客户端自己填的,可以伪造;X-Real-IP 由 Nginx 按 + $remote_addr 覆盖写入,API 端口又只监听本机回环,只有 Nginx 能连进来。 + """ + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return real_ip + return request.client.host if request.client else "" + + +def _is_private_address(value: str) -> bool: + try: + address = ipaddress.ip_address(str(value or "").strip()) + except ValueError: + return False + return address.is_private or address.is_loopback or address.is_link_local + + +def request_scheme(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-proto", "").split(",")[0].strip().lower() + return forwarded or request.url.scheme.lower() + + +def https_required(request: Request) -> bool: + """手机端要在公网上提交 ZYT 密码,明文 HTTP 只允许在内网试用。 + + MOBILE_REQUIRE_HTTPS:auto(默认,公网地址必须 HTTPS)/ always / never。 + + 没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker 网桥 + (172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 + (开发时直接访问 127.0.0.1)才在没有这个头时被信任。 + """ + policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "auto").strip().lower() + if policy == "never" or request_scheme(request) == "https": + return False + if policy == "always": + return True + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return not _is_private_address(real_ip) + peer = request.client.host if request.client else "" + try: + return not ipaddress.ip_address(peer).is_loopback + except ValueError: + return True + + +class LoginAttempt: + __slots__ = ("ip", "account", "finished") + + def __init__(self, ip: str, account: str): + self.ip, self.account, self.finished = ip, account, False + + +class MobileLoginGuard: + """手机登录限流。 + + ZYT 按请求来源 IP 计数锁定(默认 30 分钟内错 5 次锁 30 分钟),而手机端的 + 登录全部由本服务代发,在 ZYT 看来都来自服务器出口 IP——如果公司电脑也走同一 + 个出口,锁住的就是全公司的 ZYT 登录。所以这里除了按账号、按客户端 IP 限流, + 还给"真正发到 ZYT 的密码错误"设了一个全局预算,并在 ZYT 已锁定时直接短路。 + + 限流必须把"正在进行"的尝试也算进去:先检查、等 ZYT 回来再记账的话,一次并发 + 发来十几个请求会全部通过检查,一起打到 ZYT 上。`begin()` 在锁里占位, + `finish()` 按结果转成失败记录或释放。 + """ + + def __init__( + self, *, account_limit: int = 5, account_window: int = 600, + ip_limit: int = 10, ip_window: int = 600, + upstream_budget: int | None = None, upstream_window: int = 1800, + ): + if upstream_budget is None: + try: + upstream_budget = int(os.environ.get("MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET", "4")) + except ValueError: + upstream_budget = 4 + self.account_limit, self.account_window = account_limit, account_window + self.ip_limit, self.ip_window = ip_limit, ip_window + self.upstream_budget, self.upstream_window = max(0, upstream_budget), upstream_window + self._accounts: dict[str, deque[float]] = {} + self._ips: dict[str, deque[float]] = {} + self._upstream: deque[float] = deque() + self._pending_accounts: dict[str, int] = defaultdict(int) + self._pending_ips: dict[str, int] = defaultdict(int) + self._pending_upstream = 0 + self._locked_until = 0.0 + self._lock = threading.Lock() + + @staticmethod + def _recent(book: dict[str, deque[float]], key: str, window: int, now: float) -> deque[float]: + """剪掉过期记录;空了就把这个键删掉,随机账号名刷不大内存。""" + items = book.get(key) + if items is None: + return deque() + while items and items[0] <= now - window: + items.popleft() + if not items: + del book[key] + return items + + @staticmethod + def _minutes(seconds: float) -> int: + return max(1, int((seconds + 59) // 60)) + + def begin(self, ip: str, account: str) -> LoginAttempt: + now = time.time() + with self._lock: + if self._locked_until > now: + wait = self._locked_until - now + raise _error( + 429, "zyt_locked", + f"ZYT 已暂时锁定登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + failures = self._recent(self._accounts, account, self.account_window, now) + if len(failures) >= self.account_limit: + wait = failures[0] + self.account_window - now + raise _error( + 429, "rate_limited", + f"该账号登录失败次数过多,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(failures) + self._pending_accounts.get(account, 0) >= self.account_limit: + raise _error(429, "rate_limited", "该账号正在登录,请稍候几秒再试", retry_after=5) + failures = self._recent(self._ips, ip, self.ip_window, now) + if len(failures) + self._pending_ips.get(ip, 0) >= self.ip_limit: + wait = (failures[0] + self.ip_window - now) if failures else 5 + raise _error( + 429, "rate_limited", + f"登录尝试过于频繁,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + while self._upstream and self._upstream[0] <= now - self.upstream_window: + self._upstream.popleft() + if self.upstream_budget: + if len(self._upstream) >= self.upstream_budget: + wait = self._upstream[0] + self.upstream_window - now + raise _error( + 429, "upstream_budget", + "近期手机端密码错误次数较多,为避免 ZYT 锁定服务器出口 IP," + f"已暂停手机登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(self._upstream) + self._pending_upstream >= self.upstream_budget: + raise _error(429, "rate_limited", "同时登录的人较多,请几秒后再试", retry_after=5) + self._pending_accounts[account] += 1 + self._pending_ips[ip] += 1 + self._pending_upstream += 1 + return LoginAttempt(ip, account) + + def finish(self, attempt: LoginAttempt, outcome: str) -> None: + """outcome:credentials(密码错)、unknown_account(账号不存在)、success(ZYT 认可了密码)、 + other(网络、终端未开通等,不记账)。""" + if attempt.finished: + return + attempt.finished = True + now = time.time() + with self._lock: + for book, key in ((self._pending_accounts, attempt.account), (self._pending_ips, attempt.ip)): + book[key] -= 1 + if book[key] <= 0: + del book[key] + self._pending_upstream = max(0, self._pending_upstream - 1) + if outcome in ("credentials", "unknown_account"): + self._accounts.setdefault(attempt.account, deque()).append(now) + self._ips.setdefault(attempt.ip, deque()).append(now) + # ZYT 只给"账号存在、密码不对"累计出口 IP 的失败次数。 + if outcome == "credentials": + self._upstream.append(now) + elif outcome == "success": + self._accounts.pop(attempt.account, None) + # 密码校验通过时 ZYT 会清零该出口 IP 的失败计数(AdminAccountSafeCache::relieve)。 + self._upstream.clear() + + def locked(self, seconds: int) -> None: + with self._lock: + self._locked_until = max(self._locked_until, time.time() + max(60, int(seconds))) + + +class ZytMobileAuth: + """默认实现:真的去调 ZYT。测试里换成假的。""" + + def __init__(self, api_url: Callable[[], str]): + self._api_url = api_url + + def login(self, account: str, password: str) -> str: + return zyt_auth.login_admin( + account, password, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def identity(self, token: str) -> dict[str, Any]: + return zyt_auth.verify_admin_session( + token, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def logout(self, token: str) -> None: + zyt_auth.logout_admin(token, api_url=self._api_url()) + + +class _Waiter: + __slots__ = ("key", "loop", "event") + + def __init__(self, key: tuple[int, str]): + self.key = key + self.loop = asyncio.get_running_loop() + self.event = asyncio.Event() + + async def wait(self, timeout: float) -> None: + try: + await asyncio.wait_for(self.event.wait(), timeout=max(0.0, timeout)) + except asyncio.TimeoutError: + pass + + +class CommandNotifier: + """进程内唤醒:有新指令或黑名单变化时,立刻结束对应电脑挂着的长轮询。 + + 先登记再查库:登记和查库之间落库的指令也能把它叫醒,不会白等一个复查周期。 + 唤醒走 ``call_soon_threadsafe``,发通知的请求和挂起的长轮询不在同一个事件 + 循环(线程)里也不会漏掉。 + """ + + def __init__(self) -> None: + self._waiters: dict[tuple[int, str], set[_Waiter]] = {} + self._lock = threading.Lock() + + def register(self, account_id: int, device_id: str) -> _Waiter: + waiter = _Waiter((int(account_id), str(device_id))) + with self._lock: + self._waiters.setdefault(waiter.key, set()).add(waiter) + return waiter + + def unregister(self, waiter: _Waiter) -> None: + with self._lock: + waiters = self._waiters.get(waiter.key) + if waiters is not None: + waiters.discard(waiter) + if not waiters: + self._waiters.pop(waiter.key, None) + + def notify(self, account_id: int, device_id: str | None = None) -> None: + with self._lock: + targets = [ + waiter + for key, waiters in self._waiters.items() + if key[0] == int(account_id) and (device_id is None or key[1] == device_id) + for waiter in waiters + ] + for waiter in targets: + try: + waiter.loop.call_soon_threadsafe(waiter.event.set) + except RuntimeError: + # 事件循环已经关闭(请求刚好结束),没有人需要被叫醒。 + continue + + +class MobilePrincipal: + def __init__(self, row: dict[str, Any], token: str): + self.row = row + self.token = token + self.permissions = account_permissions(row) + + @property + def account_id(self) -> int: + return int(self.row["id"]) + + @property + def session_id(self) -> int: + return int(self.row["session_id"]) + + @property + def label(self) -> str: + return f"{account_label(self.row)}(手机)" + + def me(self) -> dict[str, Any]: + return { + "account": { + "id": self.account_id, + "nickname": str(self.row.get("nickname") or ""), + "sn": str(self.row.get("zyt_sn") or ""), + "avatar": str(self.row.get("avatar") or ""), + }, + "permissions": { + "control": self.permissions["control"], + "control_all": self.permissions["control_all"], + "blacklist_add": self.permissions["blacklist_add"], + "blacklist_remove": self.permissions["blacklist_remove"], + "root": self.permissions["root"], + "known": self.permissions["known"], + }, + "permission_codes": { + "control": PERM_REMOTE_CONTROL, + "control_all": PERM_REMOTE_ALL, + "blacklist_add": PERM_BLACKLIST_ADD, + "blacklist_remove": PERM_BLACKLIST_REMOVE, + }, + "session": { + "created_at": str(self.row.get("session_created_at") or ""), + "expires_at": int(self.row.get("session_expires_at") or 0), + }, + } + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class MobileLoginBody(BaseModel): + account: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=1, max_length=128, repr=False) + client_name: str = Field(default="", max_length=120) + + +class CommandBody(BaseModel): + type: str = Field(min_length=1, max_length=32) + + +class BlacklistBody(BaseModel): + name: str = Field(default="", max_length=191) + account_name: str = Field(default="", max_length=191) + reason: str = Field(default="", max_length=200) + + +class StatusBody(BaseModel): + listening: bool = False + state: str = Field(default="", max_length=32) + transport: str = Field(default="", max_length=16) + wecom_connected: bool = False + pending_count: int = Field(default=0, ge=0, le=1_000_000) + app_version: str = Field(default="", max_length=64) + capabilities: list[str] = Field(default_factory=list, max_length=16) + + +class DesktopPollBody(BaseModel): + wait: int = Field(default=15, ge=0, le=60) + status: StatusBody | None = None + # 桌面端当前持有的黑名单版本;与服务端不一致时立即返回,让它去同步。 + blacklist_version: int | None = Field(default=None, ge=0) + + +class AckBody(BaseModel): + ok: bool + result: dict[str, Any] = Field(default_factory=dict) + error: str = Field(default="", max_length=500) + status: StatusBody | None = None + + +class DesktopBlacklistSyncBody(BaseModel): + add: list[dict[str, Any]] = Field(default_factory=list, max_length=500) + + +def register_remote_routes( + app: FastAPI, + database: Any, + *, + current_desktop: Callable[..., Any], + zyt_api_url: Callable[[], str], + zyt_mobile_auth: Any = None, + login_guard: MobileLoginGuard | None = None, +) -> RemoteStore: + store = RemoteStore(database) + store.initialize() + app.state.remote_store = store + notifier = CommandNotifier() + app.state.remote_notifier = notifier + guard = login_guard or MobileLoginGuard() + auth = zyt_mobile_auth or ZytMobileAuth(zyt_api_url) + identity_locks: dict[int, asyncio.Lock] = {} + + # ── 手机端鉴权 ─────────────────────────────────────────────────────── + async def _recheck_identity(row: dict[str, Any], request: Request) -> dict[str, Any] | None: + # 同一会话并发的几个请求只复核一次;用完即删,锁表不随会话数增长。 + session_id = int(row["session_id"]) + lock = identity_locks.setdefault(session_id, asyncio.Lock()) + try: + async with lock: + return await _recheck_identity_locked(session_id, request) + finally: + if not lock.locked() and identity_locks.get(session_id) is lock: + identity_locks.pop(session_id, None) + + async def _recheck_identity_locked(session_id: int, request: Request) -> dict[str, Any] | None: + fresh = await asyncio.to_thread(store.mobile_session, _bearer(request)) + if fresh is None: + return None + if time.time() - int(fresh["identity_checked_at"] or 0) < MOBILE_IDENTITY_RECHECK_SECONDS: + return fresh + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, session_id) + try: + if not zyt_token: + raise zyt_auth.ZytLoginError("ZYT 会话缺失", kind="rejected") + identity = await asyncio.to_thread(auth.identity, zyt_token) + if str(identity.get("user_id") or "") != str(fresh["zyt_user_id"]): + raise zyt_auth.ZytLoginError("ZYT 身份与手机会话不一致", kind="rejected") + except zyt_auth.ZytAuthError as exc: + if getattr(exc, "kind", "") == "unavailable": + # ZYT 暂时连不上不算下线:沿用上次同步的权限,一分钟后再试。 + await asyncio.to_thread(store.mark_identity_checked, session_id, retry_in=60) + return fresh + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="zyt_invalid", ip=trusted_client_ip(request), + ) + return None + try: + await asyncio.to_thread(store.refresh_identity, session_id, identity) + except PermissionError: + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="account_disabled", ip=trusted_client_ip(request), + ) + return None + return await asyncio.to_thread(store.mobile_session, _bearer(request)) + + async def current_mobile(request: Request) -> MobilePrincipal: + token = _bearer(request) + row = await asyncio.to_thread(store.mobile_session, token) if token else None + if row is not None and time.time() - int(row["identity_checked_at"] or 0) >= MOBILE_IDENTITY_RECHECK_SECONDS: + row = await _recheck_identity(row, request) + if row is None: + raise _error(401, "session_expired", "登录已失效,请重新登录") + return MobilePrincipal(row, token) + + def target_account(principal: MobilePrincipal, account_id: int | None) -> dict[str, Any]: + if account_id is None or int(account_id) == principal.account_id: + return principal.row + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", "只能查看和控制自己的电脑") + account = store.account(int(account_id)) + if account is None or str(account.get("status")) != "active": + raise _error(404, "account_not_found", "客服账号不存在或已停用") + return account + + # ── 手机端:登录 ───────────────────────────────────────────────────── + @app.post("/api/v2/mobile/auth/login") + async def mobile_login(body: MobileLoginBody, request: Request) -> dict: + ip = trusted_client_ip(request) + if https_required(request): + raise _error(403, "https_required", "请通过 HTTPS 地址访问手机控制台,明文连接不能提交密码") + account_key = body.account.strip().lower() + attempt = guard.begin(ip, account_key) + outcome = "other" + try: + zyt_token = await asyncio.to_thread(auth.login, body.account.strip(), body.password) + outcome = "success" + except zyt_auth.ZytLoginError as exc: + if exc.kind in ("credentials", "unknown_account"): + outcome = exc.kind + database.audit(None, "mobile.login.failed", f"account={account_key[:64]}", ip) + raise _error(401, "invalid_credentials", "账号或密码错误") from exc + if exc.kind == "password_change_required": + outcome = "success" + raise _error(403, "password_change_required", str(exc)) from exc + if exc.kind == "locked": + guard.locked(exc.retry_after or 1800) + database.audit(None, "mobile.login.locked", f"account={account_key[:64]}", ip) + raise _error(429, "zyt_locked", str(exc), retry_after=exc.retry_after) from exc + if exc.kind == "disabled": + raise _error(403, "account_disabled", str(exc)) from exc + if exc.kind == "terminal_unsupported": + raise _error(503, "terminal_unsupported", str(exc)) from exc + if exc.kind == "unavailable": + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + raise _error(400, "login_rejected", str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + finally: + guard.finish(attempt, outcome) + try: + identity = await asyncio.to_thread(auth.identity, zyt_token) + account = await asyncio.to_thread(store.upsert_account, identity) + except zyt_auth.ZytLoginError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + status_code = {"terminal_unsupported": 503, "disabled": 403}.get(exc.kind, 502) + code = {"terminal_unsupported": "terminal_unsupported", + "disabled": "account_disabled"}.get(exc.kind, "zyt_unavailable") + raise _error(status_code, code, str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + except PermissionError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(403, "account_disabled", str(exc)) from exc + except ValueError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + token, session = await asyncio.to_thread( + store.create_mobile_session, int(account["id"]), zyt_token=zyt_token, + client_name=body.client_name, user_agent=request.headers.get("user-agent", ""), ip=ip, + ) + row = await asyncio.to_thread(store.mobile_session, token) + principal = MobilePrincipal(row, token) + return {"access_token": token, "expires_at": session["expires_at"], "me": principal.me()} + + @app.post("/api/v2/mobile/auth/logout") + async def mobile_logout(request: Request, principal: MobilePrincipal = Depends(current_mobile)) -> dict: + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, principal.session_id) + await asyncio.to_thread( + store.revoke_mobile_session, session_id=principal.session_id, reason="logout", + ip=trusted_client_ip(request), + ) + if zyt_token: + await asyncio.to_thread(auth.logout, zyt_token) + return {"ok": True} + + @app.get("/api/v2/mobile/me") + async def mobile_me(principal: MobilePrincipal = Depends(current_mobile)) -> dict: + return principal.me() + + # ── 手机端:电脑 ───────────────────────────────────────────────────── + @app.get("/api/v2/mobile/accounts") + async def mobile_accounts( + q: str = Query(default="", max_length=64), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", f"查看全部客服电脑需要 ZYT 权限 {PERM_REMOTE_ALL}") + return {"items": await asyncio.to_thread(store.accounts_overview, q)} + + @app.get("/api/v2/mobile/desktops") + async def mobile_desktops( + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + items = await asyncio.to_thread(store.devices, int(account["id"])) + return { + "account": { + "id": int(account["id"]), + "nickname": str(account.get("nickname") or ""), + "sn": str(account.get("zyt_sn") or ""), + }, + "items": items, + "server_time": int(time.time()), + } + + @app.post("/api/v2/mobile/desktops/{device_id}/commands") + async def mobile_create_command( + device_id: str, body: CommandBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + if not principal.permissions["control"]: + raise _error(403, "forbidden", f"远程启停需要 ZYT 权限 {PERM_REMOTE_CONTROL}") + if body.type not in COMMAND_TYPES: + raise _error(400, "invalid_command", "不支持的指令") + try: + device_id = valid_device_id(device_id) + except ValueError as exc: + raise _error(404, "device_not_found", "电脑不存在") from exc + device = await asyncio.to_thread(store.device, int(account["id"]), device_id) + if device is None: + raise _error(404, "device_not_found", "电脑不存在或已被管理端下线") + if not device["online"]: + raise _error(409, "device_offline", "电脑离线:客服软件没有运行,无法远程拉起") + if not device["remote_capable"]: + raise _error(409, "client_outdated", "该电脑上的客服软件版本过低,请升级后再远程控制") + try: + command = await asyncio.to_thread( + store.create_command, account_id=int(account["id"]), device_id=device_id, + command_type=body.type, actor_account_id=principal.account_id, + actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "device_not_found", str(exc)) from exc + notifier.notify(int(account["id"]), device_id) + return {"command": command} + + @app.get("/api/v2/mobile/commands/{command_id}") + async def mobile_command( + command_id: str, principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + command = await asyncio.to_thread(store.command, command_id) + if command is None: + raise _error(404, "command_not_found", "指令不存在") + if command["account_id"] != principal.account_id and not principal.permissions["control_all"]: + raise _error(404, "command_not_found", "指令不存在") + command.pop("account_id", None) + return {"command": command} + + # ── 手机端:会话与黑名单 ───────────────────────────────────────────── + @app.get("/api/v2/mobile/conversations") + async def mobile_conversations( + account_id: int | None = Query(default=None, ge=1), + q: str = Query(default="", max_length=100), + scope: str = Query(default="all", pattern="^(all|blocked)$"), + cursor: str = Query(default="", max_length=200), + limit: int = Query(default=30, ge=1, le=100), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + try: + return await asyncio.to_thread( + store.conversations, str(account["tenant_id"]), query=q, scope=scope, + cursor=cursor, limit=limit, + ) + except ValueError as exc: + raise _error(400, "invalid_cursor", str(exc)) from exc + + @app.put("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_add( + account: str, conversation: str, body: BlacklistBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_add"]: + raise _error(403, "forbidden", f"加入黑名单需要 ZYT 权限 {PERM_BLACKLIST_ADD}") + try: + entry, version = await asyncio.to_thread( + store.add_blacklist, str(target["tenant_id"]), + {"account": account, "conversation": conversation, **body.model_dump()}, + source="mobile", actor_label=principal.label, + actor_account_id=principal.account_id, ip=trusted_client_ip(request), + ) + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"entry": entry, "version": version} + + @app.delete("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_remove( + account: str, conversation: str, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_remove"]: + raise _error(403, "forbidden", f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管") + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(target["tenant_id"]), account, conversation, + source="mobile", actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"ok": True, "version": version} + + # ── 桌面端:命令通道 ───────────────────────────────────────────────── + def _desktop_label(account: Any) -> str: + return f"{account_label(account)}(电脑 {str(account['device_name'] or '')[:40]})" + + async def _report(account: Any, status: StatusBody | None) -> None: + if status is None: + return + await asyncio.to_thread( + store.report_status, int(account["id"]), str(account["device_id"]), + status.model_dump(), app_version=str(account["app_version"] or ""), + ) + + @app.post("/api/v2/desktop/commands/poll") + async def desktop_poll( + body: DesktopPollBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + account_id = int(account["id"]) + device_id = str(account["device_id"]) + tenant = str(account["tenant_id"]) + await _report(account, body.status) + wait = float(min(body.wait, MAX_POLL_WAIT_SECONDS)) + deadline = time.monotonic() + wait + while True: + waiter = notifier.register(account_id, device_id) + try: + commands = await asyncio.to_thread(store.take_commands, account_id, device_id) + version = await asyncio.to_thread(store.blacklist_version, tenant) + remaining = deadline - time.monotonic() + if ( + commands + or (body.blacklist_version is not None and version != body.blacklist_version) + or remaining <= 0 + or await request.is_disconnected() + ): + break + await waiter.wait(min(remaining, POLL_RECHECK_SECONDS)) + finally: + notifier.unregister(waiter) + return {"commands": commands, "blacklist_version": version, "server_time": int(time.time())} + + @app.post("/api/v2/desktop/commands/{command_id}/ack") + async def desktop_ack( + command_id: str, body: AckBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + await _report(account, body.status) + try: + command = await asyncio.to_thread( + store.ack_command, account_id=int(account["id"]), + device_id=str(account["device_id"]), command_id=command_id, ok=body.ok, + result=body.result, error=body.error, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "command_not_found", str(exc)) from exc + return {"ok": True, "command": command} + + @app.post("/api/v2/desktop/status") + async def desktop_status(body: StatusBody, account: Any = Depends(current_desktop)) -> dict: + await _report(account, body) + return {"ok": True} + + # ── 桌面端:黑名单 ─────────────────────────────────────────────────── + @app.get("/api/v2/desktop/blacklist") + async def desktop_blacklist(account: Any = Depends(current_desktop)) -> dict: + version, items = await asyncio.to_thread(store.blacklist, str(account["tenant_id"])) + return {"version": version, "items": items} + + @app.post("/api/v2/desktop/blacklist/sync") + async def desktop_blacklist_sync( + body: DesktopBlacklistSyncBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + # 电脑前的人加黑名单不要求 ZYT 权限:拉黑只会让系统少发消息,是安全方向; + # 离线时加的、升级前本地就有的条目也都从这里补传。 + before = await asyncio.to_thread(store.blacklist_version, str(account["tenant_id"])) + version, items, rejected = await asyncio.to_thread( + store.sync_desktop_blacklist, str(account["tenant_id"]), body.add, + actor_label=_desktop_label(account), actor_account_id=int(account["id"]), + ip=trusted_client_ip(request), + ) + if version != before: + notifier.notify(int(account["id"])) + return {"version": version, "items": items, "rejected": rejected} + + @app.delete("/api/v2/desktop/blacklist/{account_key}/{conversation:path}") + async def desktop_blacklist_remove( + account_key: str, conversation: str, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + if not account_permissions(account)["blacklist_remove"]: + raise _error( + 403, "forbidden", + f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管在手机控制台操作", + ) + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(account["tenant_id"]), account_key, conversation, + source="desktop", actor_label=_desktop_label(account), ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(account["id"])) + return {"ok": True, "version": version} + + return store diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/remote_store.py b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/remote_store.py new file mode 100644 index 000000000..350de5ed8 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/remote_store.py @@ -0,0 +1,1149 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台的数据层。 + +四张表,全部挂在已有的 ``desktop_accounts`` 上: + + mobile_sessions 手机端登录会话。和桌面会话分表存放:桌面会话表上有 + "每个账号只能有一个有效会话"的唯一索引,手机登录如果 + 也写进去,就会把正在值班的电脑顶下线。分表之后两种 + 令牌也天然不能互相冒用。 + desktop_commands 下发给某台电脑的一次性命令(启动 / 停止监听)。 + desktop_status 每台电脑随长轮询上报的最新运行状态。 + conversation_blacklist 服务端会话黑名单。桌面端本地库降级为缓存,协议引擎和 + 视觉流程仍只读本地库,离线时照旧生效。 + +客服电脑在内网、没有入站端口,服务端永远不主动连电脑:命令只是落库,等电脑下一次 +长轮询时带走,执行完再回执。 +""" + +from __future__ import annotations + +import json +import re +import secrets +import time +import uuid +from datetime import datetime +from typing import Any, Iterable + +from admin_backend import now_text, token_hash + +# ── ZYT 菜单权限码(在 ZYT 角色管理里勾选,客服后台只做校验)────────────── +PERM_REMOTE_CONTROL = "wecom_rpa.remote/control" +PERM_REMOTE_ALL = "wecom_rpa.remote/all" +PERM_BLACKLIST_ADD = "wecom_rpa.blacklist/add" +PERM_BLACKLIST_REMOVE = "wecom_rpa.blacklist/remove" + +COMMAND_TYPES = {"start_listening": "启动监听", "stop_listening": "停止监听"} +COMMAND_STATUS_LABELS = { + "pending": "已下发,等待电脑领取", + "delivered": "电脑已领取,正在执行", + "succeeded": "已执行", + "failed": "执行失败", + "expired": "电脑未在 10 分钟内确认,已过期", + "superseded": "已被更新的指令取代", +} +FINAL_COMMAND_STATUSES = frozenset({"succeeded", "failed", "expired", "superseded"}) +COMMAND_TTL_SECONDS = 10 * 60 +MOBILE_SESSION_LIFETIME_SECONDS = 30 * 24 * 60 * 60 +# 手机会话每 10 分钟向 ZYT 复核一次身份与权限;复核本身也会让 ZYT 顺延令牌。 +MOBILE_IDENTITY_RECHECK_SECONDS = 10 * 60 +# 桌面端每 5 秒心跳一次(/desktop/me),30 秒没有心跳就算程序没开。 +DEVICE_ONLINE_SECONDS = 30 +# 命令通道每次长轮询(最长 25 秒)都会顺带上报状态。 +STATUS_FRESH_SECONDS = 90 +CAPABILITY_COMMANDS = "commands_v1" +CAPABILITY_BLACKLIST = "blacklist_sync_v1" + +STATE_LABELS = { + "running": "监听中", + "connecting": "正在连接企业微信", + "waiting": "等待企业微信", + "verification": "等待企业微信验证", + "recovering": "自动恢复中", + "stopping": "正在停止", + "stopped": "已停止", + "error": "连接失败", +} +TRANSPORT_LABELS = {"visual": "视觉通道", "protocol": "协议通道"} +CONVERSATION_TYPE_LABELS = { + "direct_wechat": "微信联系人", + "direct_wecom": "企业微信联系人", + "group": "群聊", + "application": "应用消息", + "service": "服务通知", + "unknown": "会话", +} + +_DEVICE_ID_RE = re.compile(r"^[A-Za-z0-9._:@-]{1,128}$") +_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f]") +MAX_SYNC_ITEMS = 500 + + +def _now_epoch() -> int: + return int(time.time()) + + +def _epoch_of(text: Any) -> int | None: + value = str(text or "").strip() + if not value: + return None + try: + return int(datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()) + except ValueError: + return None + + +def _seconds_since(epoch: int | None, now: int) -> int | None: + return None if epoch is None else max(0, now - int(epoch)) + + +def valid_device_id(value: Any) -> str: + text = str(value or "").strip() + if not _DEVICE_ID_RE.fullmatch(text): + raise ValueError("设备标识不正确") + return text + + +def clean_key(value: Any, label: str, *, limit: int) -> str: + """黑名单的账号 / 会话标识:来自企业微信本地库,原样保存,只挡控制字符。""" + text = str(value or "").strip() + if not text or len(text) > limit or _CONTROL_CHARS_RE.search(text): + raise ValueError(f"{label}不正确") + return text + + +def clean_text(value: Any, *, limit: int) -> str: + return _CONTROL_CHARS_RE.sub(" ", str(value or "")).strip()[:limit] + + +def _like(value: str) -> str: + escaped = str(value).replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + return f"%{escaped}%" + + +def account_permissions(account: Any) -> dict[str, bool]: + """从 desktop_accounts 行里同步下来的 ZYT 权限推出手机端能做什么。 + + ZYT 对 root 以及"拥有全部菜单"的角色返回 ``*``。remote/all 包含了控制本人 + 电脑的能力。 + """ + try: + raw = json.loads(str(account["zyt_permissions_json"] or "[]")) + except (KeyError, IndexError, TypeError, ValueError): + raw = [] + granted = {str(item).strip().lower() for item in raw if str(item).strip()} if isinstance(raw, list) else set() + try: + root = bool(int(account["zyt_root"] or 0)) or "*" in granted + except (KeyError, IndexError, TypeError, ValueError): + root = "*" in granted + try: + known = bool(int(account["zyt_permissions_known"] or 0)) + except (KeyError, IndexError, TypeError, ValueError): + known = False + + def has(code: str) -> bool: + return root or code in granted + + return { + "root": root, + "known": known, + "control": has(PERM_REMOTE_CONTROL) or has(PERM_REMOTE_ALL), + "control_all": has(PERM_REMOTE_ALL), + "blacklist_add": has(PERM_BLACKLIST_ADD), + "blacklist_remove": has(PERM_BLACKLIST_REMOVE), + } + + +def account_label(account: Any) -> str: + try: + return str(account["nickname"] or account["zyt_sn"] or f"账号{account['id']}") + except (KeyError, IndexError): + return "未知账号" + + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS mobile_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + token_digest TEXT NOT NULL UNIQUE, + zyt_token_enc TEXT NOT NULL DEFAULT '', + client_name TEXT NOT NULL DEFAULT '', + user_agent TEXT NOT NULL DEFAULT '', + ip_address TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + last_used_at TEXT NOT NULL, + last_used_epoch INTEGER NOT NULL DEFAULT 0, + expires_at INTEGER NOT NULL, + identity_checked_at INTEGER NOT NULL DEFAULT 0, + revoked_at TEXT NOT NULL DEFAULT '', + revoke_reason TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_mobile_sessions_account + ON mobile_sessions(account_id, revoked_at, expires_at); + +CREATE TABLE IF NOT EXISTS desktop_commands ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command_id TEXT NOT NULL UNIQUE, + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + device_id TEXT NOT NULL, + type TEXT NOT NULL CHECK(type IN ('start_listening','stop_listening')), + payload_json TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'pending' + CHECK(status IN ('pending','delivered','succeeded','failed','expired','superseded')), + created_by_account_id INTEGER, + created_by TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + expires_at INTEGER NOT NULL, + delivered_at TEXT NOT NULL DEFAULT '', + delivery_count INTEGER NOT NULL DEFAULT 0, + acked_at TEXT NOT NULL DEFAULT '', + result_json TEXT NOT NULL DEFAULT '{}', + error TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_desktop_commands_target + ON desktop_commands(account_id, device_id, status, id); + +CREATE TABLE IF NOT EXISTS desktop_status ( + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + device_id TEXT NOT NULL, + listening INTEGER NOT NULL DEFAULT 0, + state TEXT NOT NULL DEFAULT '', + transport TEXT NOT NULL DEFAULT '', + wecom_connected INTEGER NOT NULL DEFAULT 0, + pending_count INTEGER NOT NULL DEFAULT 0, + app_version TEXT NOT NULL DEFAULT '', + capabilities_json TEXT NOT NULL DEFAULT '[]', + reported_at TEXT NOT NULL, + reported_epoch INTEGER NOT NULL, + PRIMARY KEY(account_id, device_id) +); + +CREATE TABLE IF NOT EXISTS conversation_blacklist ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id TEXT NOT NULL, + account TEXT NOT NULL, + conversation TEXT NOT NULL, + display_name TEXT NOT NULL DEFAULT '', + account_name TEXT NOT NULL DEFAULT '', + reason TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT 'desktop', + created_by TEXT NOT NULL DEFAULT '', + created_by_account_id INTEGER, + created_at TEXT NOT NULL, + removed_at TEXT NOT NULL DEFAULT '', + removed_by TEXT NOT NULL DEFAULT '', + removed_source TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 0, + UNIQUE(tenant_id, account, conversation) +); +CREATE INDEX IF NOT EXISTS idx_conversation_blacklist_active + ON conversation_blacklist(tenant_id, removed_at, id DESC); +CREATE INDEX IF NOT EXISTS idx_conversation_blacklist_version + ON conversation_blacklist(tenant_id, version); +""" + + +class RemoteStore: + def __init__(self, database: Any): + self.database = database + + def initialize(self) -> None: + with self.database.connect() as db: + db.executescript(SCHEMA) + db.commit() + + # ── 账号 ───────────────────────────────────────────────────────────── + def upsert_account(self, identity: dict[str, Any]) -> dict[str, Any]: + """按 ZYT 身份落到同一条 desktop_accounts,并同步权限;不碰设备与桌面会话。 + + 没用过桌面端的主管第一次登录手机端时会新建一行,这一行在管理端的 + 「客户端账号」里显示为没有设备。 + """ + zyt_user_id = str(identity.get("user_id") or "").strip() + if not zyt_user_id: + raise ValueError("ZYT 用户标识不能为空") + permissions_value = identity.get("permissions") + permissions = sorted( + {str(item).strip() for item in permissions_value if str(item).strip()} + ) if isinstance(permissions_value, list) else [] + role_ids: list[int] = [] + for item in identity.get("role_ids") if isinstance(identity.get("role_ids"), list) else []: + try: + role_ids.append(int(item)) + except (TypeError, ValueError): + continue + root = int(bool(identity.get("root")) or "*" in permissions) + known = int(bool(identity.get("permissions_known", isinstance(permissions_value, list)))) + mobile = str(identity.get("mobile") or "") + if len(mobile) >= 7: + mobile = f"{mobile[:3]}****{mobile[-4:]}" + stamp = now_text() + tenant_id = self.database._desktop_tenant_id(zyt_user_id) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + db.execute( + """INSERT INTO desktop_accounts + (zyt_user_id,tenant_id,zyt_sn,nickname,mobile_masked,avatar, + zyt_root,zyt_role_ids_json,zyt_permissions_json,zyt_permissions_known, + status,created_at,updated_at,last_login_at,last_online_at) + VALUES (?,?,?,?,?,?,?,?,?,?,'active',?,?,'','') + ON CONFLICT(zyt_user_id) DO UPDATE SET + zyt_sn=excluded.zyt_sn,nickname=excluded.nickname, + avatar=excluded.avatar,zyt_root=excluded.zyt_root, + zyt_role_ids_json=excluded.zyt_role_ids_json, + zyt_permissions_json=excluded.zyt_permissions_json, + zyt_permissions_known=excluded.zyt_permissions_known, + updated_at=excluded.updated_at""", + ( + zyt_user_id, tenant_id, + str(identity.get("sn") or "")[:128], + str(identity.get("nickname") or "")[:191], + mobile[:32], + str(identity.get("avatar") or "")[:1024], + root, + json.dumps(sorted(set(role_ids))), + json.dumps(permissions, ensure_ascii=False), + known, stamp, stamp, + ), + ) + account = db.execute( + "SELECT * FROM desktop_accounts WHERE zyt_user_id=?", (zyt_user_id,) + ).fetchone() + if account is None or str(account["status"]) != "active": + db.rollback() + raise PermissionError("该账号已被管理端禁用,不能使用手机控制台") + db.commit() + return dict(account) + + def account(self, account_id: int) -> dict[str, Any] | None: + row = self.database.desktop_account(int(account_id)) + return dict(row) if row is not None else None + + # ── 手机会话 ───────────────────────────────────────────────────────── + def create_mobile_session( + self, account_id: int, *, zyt_token: str, client_name: str = "", + user_agent: str = "", ip: str = "", + lifetime: int = MOBILE_SESSION_LIFETIME_SECONDS, + ) -> tuple[str, dict[str, Any]]: + import secret_box + + token = secrets.token_urlsafe(48) + now = _now_epoch() + stamp = now_text() + expires_at = now + max(300, min(int(lifetime), MOBILE_SESSION_LIFETIME_SECONDS)) + encrypted = secret_box.encrypt(str(zyt_token or ""), self.database._secret_key()) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + account = db.execute( + "SELECT * FROM desktop_accounts WHERE id=? AND status='active'", (int(account_id),) + ).fetchone() + if account is None: + db.rollback() + raise PermissionError("该账号已被管理端禁用,不能使用手机控制台") + cursor = db.execute( + """INSERT INTO mobile_sessions + (account_id,token_digest,zyt_token_enc,client_name,user_agent,ip_address, + created_at,last_used_at,last_used_epoch,expires_at,identity_checked_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?)""", + ( + int(account_id), token_hash(token), encrypted, + clean_text(client_name, limit=120), clean_text(user_agent, limit=255), + str(ip or "")[:80], stamp, stamp, now, expires_at, now, + ), + ) + db.execute( + "DELETE FROM mobile_sessions WHERE expires_at'' AND revoked_at dict[str, Any] | None: + if not token: + return None + now = _now_epoch() + with self.database.connect() as db: + row = db.execute( + """SELECT a.*,s.id AS session_id,s.expires_at AS session_expires_at, + s.created_at AS session_created_at,s.identity_checked_at, + s.last_used_epoch + FROM mobile_sessions s JOIN desktop_accounts a ON a.id=s.account_id + WHERE s.token_digest=? AND s.revoked_at='' AND s.expires_at>=? + AND a.status='active'""", + (token_hash(token), now), + ).fetchone() + if row is None: + return None + if now - int(row["last_used_epoch"] or 0) >= 60: + db.execute( + "UPDATE mobile_sessions SET last_used_at=?,last_used_epoch=? WHERE id=?", + (now_text(), now, int(row["session_id"])), + ) + db.commit() + return dict(row) + + def mobile_zyt_token(self, session_id: int) -> str: + import secret_box + + with self.database.connect() as db: + row = db.execute( + "SELECT zyt_token_enc FROM mobile_sessions WHERE id=?", (int(session_id),) + ).fetchone() + if row is None or not str(row["zyt_token_enc"] or ""): + return "" + try: + return secret_box.decrypt(str(row["zyt_token_enc"]), self.database._secret_key()) + except Exception: + return "" + + def mark_identity_checked(self, session_id: int, *, retry_in: int | None = None) -> None: + """记录复核时间。ZYT 暂时连不上时用 retry_in 让下一次复核提前。""" + checked = _now_epoch() + if retry_in is not None: + checked = checked - MOBILE_IDENTITY_RECHECK_SECONDS + max(10, int(retry_in)) + with self.database.connect() as db: + db.execute( + "UPDATE mobile_sessions SET identity_checked_at=? WHERE id=?", + (checked, int(session_id)), + ) + db.commit() + + def refresh_identity(self, session_id: int, identity: dict[str, Any]) -> None: + account = self.upsert_account(identity) + with self.database.connect() as db: + db.execute( + "UPDATE mobile_sessions SET identity_checked_at=? WHERE id=? AND account_id=?", + (_now_epoch(), int(session_id), int(account["id"])), + ) + db.commit() + + def revoke_mobile_session( + self, *, token: str = "", session_id: int | None = None, reason: str = "logout", + ip: str = "", + ) -> None: + with self.database.connect() as db: + if token: + row = db.execute( + "SELECT id,account_id FROM mobile_sessions WHERE token_digest=?", + (token_hash(token),), + ).fetchone() + else: + row = db.execute( + "SELECT id,account_id FROM mobile_sessions WHERE id=?", (int(session_id or 0),) + ).fetchone() + if row is None: + return + db.execute( + """UPDATE mobile_sessions SET revoked_at=?,revoke_reason=?,zyt_token_enc='' + WHERE id=? AND revoked_at=''""", + (now_text(), str(reason)[:40], int(row["id"])), + ) + self.database._audit( + db, None, "mobile.logout" if reason == "logout" else "mobile.session.revoke", + f"account={row['account_id']} session={row['id']} reason={reason}", str(ip or ""), + ) + db.commit() + + def revoke_mobile_sessions_for_account(self, account_id: int, *, reason: str) -> int: + with self.database.connect() as db: + cursor = db.execute( + """UPDATE mobile_sessions SET revoked_at=?,revoke_reason=?,zyt_token_enc='' + WHERE account_id=? AND revoked_at=''""", + (now_text(), str(reason)[:40], int(account_id)), + ) + db.commit() + return int(cursor.rowcount or 0) + + def mobile_session_counts(self) -> dict[int, int]: + with self.database.connect() as db: + rows = db.execute( + """SELECT account_id,COUNT(*) FROM mobile_sessions + WHERE revoked_at='' AND expires_at>=? GROUP BY account_id""", + (_now_epoch(),), + ).fetchall() + return {int(row[0]): int(row[1]) for row in rows} + + # ── 设备与状态 ─────────────────────────────────────────────────────── + def report_status( + self, account_id: int, device_id: str, status: dict[str, Any], *, app_version: str = "" + ) -> None: + capabilities = status.get("capabilities") + capabilities = sorted({ + clean_text(item, limit=32) for item in capabilities if clean_text(item, limit=32) + })[:16] if isinstance(capabilities, list) else [] + try: + pending = max(0, min(int(status.get("pending_count") or 0), 1_000_000)) + except (TypeError, ValueError): + pending = 0 + now = _now_epoch() + with self.database.connect() as db: + db.execute( + """INSERT INTO desktop_status + (account_id,device_id,listening,state,transport,wecom_connected,pending_count, + app_version,capabilities_json,reported_at,reported_epoch) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(account_id,device_id) DO UPDATE SET + listening=excluded.listening,state=excluded.state, + transport=excluded.transport,wecom_connected=excluded.wecom_connected, + pending_count=excluded.pending_count,app_version=excluded.app_version, + capabilities_json=excluded.capabilities_json, + reported_at=excluded.reported_at,reported_epoch=excluded.reported_epoch""", + ( + int(account_id), valid_device_id(device_id), + int(bool(status.get("listening"))), + clean_text(status.get("state"), limit=32), + clean_text(status.get("transport"), limit=16), + int(bool(status.get("wecom_connected"))), + pending, + clean_text(status.get("app_version") or app_version, limit=64), + json.dumps(capabilities), now_text(), now, + ), + ) + db.commit() + + def devices(self, account_id: int, *, device_id: str = "") -> list[dict[str, Any]]: + now = _now_epoch() + params: list[Any] = [now, int(account_id)] + extra = "" + if device_id: + extra = " AND d.device_id=?" + params.append(valid_device_id(device_id)) + with self.database.connect() as db: + rows = db.execute( + f"""SELECT d.id AS device_row_id,d.device_id,d.device_name,d.app_version, + d.last_seen_at, + (SELECT COUNT(*) FROM desktop_sessions s + WHERE s.device_row_id=d.id AND s.revoked_at='' + AND s.expires_at>=?) AS active_sessions, + st.listening,st.state,st.transport,st.wecom_connected, + st.pending_count,st.app_version AS status_app_version, + st.capabilities_json,st.reported_at,st.reported_epoch + FROM desktop_devices d + LEFT JOIN desktop_status st + ON st.account_id=d.account_id AND st.device_id=d.device_id + WHERE d.account_id=? AND d.revoked_at=''{extra} + ORDER BY d.last_seen_at DESC,d.id DESC""", + tuple(params), + ).fetchall() + commands = {} + for row in rows: + latest = db.execute( + """SELECT * FROM desktop_commands WHERE account_id=? AND device_id=? + ORDER BY id DESC LIMIT 1""", + (int(account_id), str(row["device_id"])), + ).fetchone() + if latest is not None: + commands[str(row["device_id"])] = self._public_command(latest, now) + return [self._public_device(row, commands.get(str(row["device_id"])), now) for row in rows] + + def device(self, account_id: int, device_id: str) -> dict[str, Any] | None: + items = self.devices(account_id, device_id=device_id) + return items[0] if items else None + + @staticmethod + def _public_device(row: Any, command: dict[str, Any] | None, now: int) -> dict[str, Any]: + seen_epoch = _epoch_of(row["last_seen_at"]) + seen_seconds = _seconds_since(seen_epoch, now) + session_active = int(row["active_sessions"] or 0) > 0 + online = bool(session_active and seen_seconds is not None and seen_seconds <= DEVICE_ONLINE_SECONDS) + status = None + remote_capable = False + blacklist_sync = False + if row["reported_epoch"] is not None: + try: + capabilities = set(json.loads(str(row["capabilities_json"] or "[]"))) + except (TypeError, ValueError): + capabilities = set() + reported_seconds = _seconds_since(int(row["reported_epoch"]), now) + fresh = reported_seconds is not None and reported_seconds <= STATUS_FRESH_SECONDS + same_build = str(row["status_app_version"] or "") == str(row["app_version"] or "") + # 同一台电脑降级回旧版本时,旧的能力声明不能继续算数。 + remote_capable = CAPABILITY_COMMANDS in capabilities and (fresh or same_build) + blacklist_sync = CAPABILITY_BLACKLIST in capabilities and (fresh or same_build) + state = str(row["state"] or "") + transport = str(row["transport"] or "") + status = { + "listening": bool(row["listening"]), + "state": state, + "state_label": STATE_LABELS.get(state, state or "未知"), + "transport": transport, + "transport_label": TRANSPORT_LABELS.get(transport, transport or "未知"), + "wecom_connected": bool(row["wecom_connected"]), + "pending_count": int(row["pending_count"] or 0), + "reported_at": str(row["reported_at"] or ""), + "reported_seconds": reported_seconds, + "fresh": fresh, + } + return { + "device_id": str(row["device_id"]), + "device_name": str(row["device_name"] or ""), + "app_version": str(row["app_version"] or ""), + "online": online, + "session_active": session_active, + "last_seen_at": str(row["last_seen_at"] or ""), + "last_seen_seconds": seen_seconds, + "remote_capable": remote_capable, + "blacklist_sync": blacklist_sync, + "status": status, + "last_command": command, + } + + def accounts_overview(self, query: str = "", limit: int = 100) -> list[dict[str, Any]]: + """全部客服电脑的概况,只给有 remote/all 权限的人看。""" + params: list[Any] = [] + where = "WHERE a.status='active'" + needle = str(query or "").strip() + if needle: + where += " AND (a.nickname LIKE ? ESCAPE '\\' OR a.zyt_sn LIKE ? ESCAPE '\\')" + params.extend([_like(needle), _like(needle)]) + params.append(max(1, min(int(limit or 100), 300))) + with self.database.connect() as db: + rows = db.execute( + f"""SELECT a.id,a.nickname,a.zyt_sn,a.last_online_at, + (SELECT COUNT(*) FROM desktop_devices d + WHERE d.account_id=a.id AND d.revoked_at='') AS device_count + FROM desktop_accounts a {where} + ORDER BY a.last_online_at DESC,a.id DESC LIMIT ?""", + tuple(params), + ).fetchall() + result = [] + for row in rows: + devices = self.devices(int(row["id"])) if int(row["device_count"] or 0) else [] + result.append({ + "id": int(row["id"]), + "nickname": str(row["nickname"] or ""), + "sn": str(row["zyt_sn"] or ""), + "device_count": len(devices), + "online": any(item["online"] for item in devices), + "listening": any( + item["online"] and item["status"] and item["status"]["listening"] + for item in devices + ), + "last_online_at": str(row["last_online_at"] or ""), + }) + return result + + # ── 命令 ───────────────────────────────────────────────────────────── + @staticmethod + def _public_command(row: Any, now: int | None = None) -> dict[str, Any]: + now = _now_epoch() if now is None else now + status = str(row["status"]) + if status in {"pending", "delivered"} and int(row["expires_at"]) < now: + status = "expired" + try: + result = json.loads(str(row["result_json"] or "{}")) + except (TypeError, ValueError): + result = {} + return { + "id": str(row["command_id"]), + "type": str(row["type"]), + "type_label": COMMAND_TYPES.get(str(row["type"]), str(row["type"])), + "device_id": str(row["device_id"]), + "status": status, + "status_label": COMMAND_STATUS_LABELS.get(status, status), + "final": status in FINAL_COMMAND_STATUSES, + "created_at": str(row["created_at"]), + "created_by": str(row["created_by"] or ""), + "delivered_at": str(row["delivered_at"] or ""), + "acked_at": str(row["acked_at"] or ""), + "expires_at": int(row["expires_at"]), + "result": result if isinstance(result, dict) else {}, + "error": str(row["error"] or ""), + } + + def create_command( + self, *, account_id: int, device_id: str, command_type: str, + actor_account_id: int, actor_label: str, ip: str, + ) -> dict[str, Any]: + if command_type not in COMMAND_TYPES: + raise ValueError("不支持的指令") + device_id = valid_device_id(device_id) + now = _now_epoch() + stamp = now_text() + command_id = uuid.uuid4().hex + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + if db.execute( + """SELECT 1 FROM desktop_devices + WHERE account_id=? AND device_id=? AND revoked_at=''""", + (int(account_id), device_id), + ).fetchone() is None: + db.rollback() + raise LookupError("电脑不存在或已被管理端下线") + # 还没被电脑领走的旧指令直接作废:连点"停止、启动"时,电脑只该执行最后一次。 + # 已经领走的正在执行,不能撤回,交给电脑按顺序执行。 + db.execute( + """UPDATE desktop_commands SET status='superseded',acked_at=? + WHERE account_id=? AND device_id=? AND status='pending'""", + (stamp, int(account_id), device_id), + ) + db.execute( + """INSERT INTO desktop_commands + (command_id,account_id,device_id,type,payload_json,status, + created_by_account_id,created_by,created_at,expires_at) + VALUES (?,?,?,?,?,'pending',?,?,?,?)""", + ( + command_id, int(account_id), device_id, command_type, "{}", + int(actor_account_id), clean_text(actor_label, limit=120), stamp, + now + COMMAND_TTL_SECONDS, + ), + ) + self.database._audit( + db, None, "remote.command.create", + f"command={command_id} account={int(account_id)} device={device_id[:64]} " + f"type={command_type} by_account={int(actor_account_id)}", + str(ip or ""), + ) + db.commit() + row = db.execute( + "SELECT * FROM desktop_commands WHERE command_id=?", (command_id,) + ).fetchone() + return self._public_command(row, now) + + def command(self, command_id: str) -> dict[str, Any] | None: + with self.database.connect() as db: + row = db.execute( + "SELECT * FROM desktop_commands WHERE command_id=?", (str(command_id or ""),) + ).fetchone() + if row is None: + return None + payload = self._public_command(row) + payload["account_id"] = int(row["account_id"]) + return payload + + def has_open_commands(self, account_id: int, device_id: str) -> bool: + with self.database.connect() as db: + return db.execute( + """SELECT 1 FROM desktop_commands + WHERE account_id=? AND device_id=? AND status IN ('pending','delivered') + LIMIT 1""", + (int(account_id), str(device_id)), + ).fetchone() is not None + + def take_commands(self, account_id: int, device_id: str) -> list[dict[str, Any]]: + """把待执行指令交给电脑。 + + 领取不等于完成:电脑回执之前每次轮询都会再拿到一次,由电脑按指令 id 去重。 + 这样长轮询的响应在路上丢了,指令也不会跟着丢。 + """ + if not self.has_open_commands(account_id, device_id): + return [] + now = _now_epoch() + stamp = now_text() + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + db.execute( + """UPDATE desktop_commands SET status='expired' + WHERE account_id=? AND device_id=? AND status IN ('pending','delivered') + AND expires_at dict[str, Any]: + stamp = now_text() + now = _now_epoch() + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + """SELECT * FROM desktop_commands + WHERE command_id=? AND account_id=? AND device_id=?""", + (str(command_id or ""), int(account_id), str(device_id)), + ).fetchone() + if row is None: + db.rollback() + raise LookupError("指令不存在") + status = str(row["status"]) + if status in {"succeeded", "failed"}: + db.rollback() + return self._public_command(row, now) + if status in {"pending", "delivered"} and int(row["expires_at"]) >= now: + status = "succeeded" if ok else "failed" + elif status in {"pending", "delivered"}: + status = "expired" + safe_result = result if isinstance(result, dict) else {} + encoded = json.dumps(safe_result, ensure_ascii=False)[:4000] + db.execute( + """UPDATE desktop_commands SET status=?,acked_at=?,result_json=?,error=? + WHERE id=?""", + (status, stamp, encoded, clean_text(error, limit=500), int(row["id"])), + ) + self.database._audit( + db, None, "remote.command.ack", + f"command={row['command_id']} account={int(account_id)} status={status}", + str(ip or ""), + ) + db.commit() + row = db.execute("SELECT * FROM desktop_commands WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_command(row, now) + + # ── 黑名单 ─────────────────────────────────────────────────────────── + @staticmethod + def _public_entry(row: Any) -> dict[str, Any]: + return { + "account": str(row["account"]), + "account_name": str(row["account_name"] or ""), + "conversation": str(row["conversation"]), + "name": str(row["display_name"] or ""), + "reason": str(row["reason"] or ""), + "source": str(row["source"] or ""), + "created_by": str(row["created_by"] or ""), + "created_at": str(row["created_at"] or ""), + } + + @staticmethod + def _version(db: Any, tenant_id: str) -> int: + return int(db.execute( + "SELECT COALESCE(MAX(version),0) FROM conversation_blacklist WHERE tenant_id=?", + (tenant_id,), + ).fetchone()[0]) + + def blacklist_version(self, tenant_id: str) -> int: + with self.database.connect() as db: + return self._version(db, str(tenant_id)) + + def blacklist(self, tenant_id: str) -> tuple[int, list[dict[str, Any]]]: + with self.database.connect() as db: + rows = db.execute( + """SELECT * FROM conversation_blacklist WHERE tenant_id=? AND removed_at='' + ORDER BY account,conversation""", + (str(tenant_id),), + ).fetchall() + version = self._version(db, str(tenant_id)) + return version, [self._public_entry(row) for row in rows] + + @staticmethod + def _archive_names(db: Any, tenant_id: str, account: str, conversation: str) -> tuple[str, str]: + """补上会话名和企微账号名;归档表不存在或没有这条会话时返回空串。""" + try: + row = db.execute( + """SELECT c.name,a.display_name FROM archive_conversation c + JOIN archive_source_account a ON a.id=c.source_account_id + WHERE c.tenant_id=? AND a.external_account_id=? AND c.external_id=? + LIMIT 1""", + (tenant_id, account, conversation), + ).fetchone() + except Exception: + return "", "" + if row is None: + return "", "" + return str(row[0] or ""), str(row[1] or "") + + def _add_entry( + self, db: Any, tenant_id: str, item: dict[str, Any], *, source: str, + actor_label: str, actor_account_id: int | None, version: int, + ) -> tuple[dict[str, Any], bool]: + account = clean_key(item.get("account"), "企业微信账号", limit=191) + conversation = clean_key(item.get("conversation"), "会话", limit=512) + name = clean_text(item.get("name"), limit=191) + account_name = clean_text(item.get("account_name"), limit=191) + reason = clean_text(item.get("reason"), limit=200) + if not name or not account_name: + archive_name, archive_account = self._archive_names(db, tenant_id, account, conversation) + name = name or archive_name + account_name = account_name or archive_account + stamp = now_text() + row = db.execute( + """SELECT * FROM conversation_blacklist + WHERE tenant_id=? AND account=? AND conversation=?""", + (tenant_id, account, conversation), + ).fetchone() + if row is not None and not str(row["removed_at"] or ""): + changed = ( + (name and name != row["display_name"]) + or (account_name and account_name != row["account_name"]) + or (reason and reason != row["reason"]) + ) + if changed: + db.execute( + """UPDATE conversation_blacklist SET + display_name=CASE WHEN ?<>'' THEN ? ELSE display_name END, + account_name=CASE WHEN ?<>'' THEN ? ELSE account_name END, + reason=CASE WHEN ?<>'' THEN ? ELSE reason END, + updated_at=?,version=? WHERE id=?""", + (name, name, account_name, account_name, reason, reason, stamp, version, + int(row["id"])), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_entry(fresh), bool(changed) + if row is not None: + db.execute( + """UPDATE conversation_blacklist SET display_name=?,account_name=?,reason=?, + source=?,created_by=?,created_by_account_id=?,created_at=?, + removed_at='',removed_by='',removed_source='',updated_at=?,version=? + WHERE id=?""", + (name or row["display_name"], account_name or row["account_name"], reason, + source, clean_text(actor_label, limit=120), actor_account_id, stamp, stamp, + version, int(row["id"])), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_entry(fresh), True + cursor = db.execute( + """INSERT INTO conversation_blacklist + (tenant_id,account,conversation,display_name,account_name,reason,source, + created_by,created_by_account_id,created_at,updated_at,version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + (tenant_id, account, conversation, name, account_name, reason, source, + clean_text(actor_label, limit=120), actor_account_id, stamp, stamp, version), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(cursor.lastrowid),)).fetchone() + return self._public_entry(fresh), True + + def add_blacklist( + self, tenant_id: str, item: dict[str, Any], *, source: str, actor_label: str, + actor_account_id: int | None, ip: str, + ) -> tuple[dict[str, Any], int]: + tenant_id = str(tenant_id) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + version = self._version(db, tenant_id) + 1 + entry, changed = self._add_entry( + db, tenant_id, item, source=source, actor_label=actor_label, + actor_account_id=actor_account_id, version=version, + ) + if changed: + self.database._audit( + db, None, "blacklist.add", + f"tenant={tenant_id} account={entry['account'][:64]} " + f"conversation={entry['conversation'][:120]} source={source} " + f"by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + else: + db.rollback() + current = self._version(db, tenant_id) + return entry, current + + def remove_blacklist( + self, tenant_id: str, account: str, conversation: str, *, source: str, + actor_label: str, ip: str, + ) -> int: + tenant_id = str(tenant_id) + account = clean_key(account, "企业微信账号", limit=191) + conversation = clean_key(conversation, "会话", limit=512) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + """SELECT id FROM conversation_blacklist + WHERE tenant_id=? AND account=? AND conversation=? AND removed_at=''""", + (tenant_id, account, conversation), + ).fetchone() + if row is None: + db.rollback() + raise LookupError("该会话不在黑名单中") + version = self._version(db, tenant_id) + 1 + stamp = now_text() + db.execute( + """UPDATE conversation_blacklist SET removed_at=?,removed_by=?,removed_source=?, + updated_at=?,version=? WHERE id=?""", + (stamp, clean_text(actor_label, limit=120), source, stamp, version, int(row["id"])), + ) + self.database._audit( + db, None, "blacklist.remove", + f"tenant={tenant_id} account={account[:64]} conversation={conversation[:120]} " + f"source={source} by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + return version + + def sync_desktop_blacklist( + self, tenant_id: str, adds: Iterable[dict[str, Any]], *, actor_label: str, + actor_account_id: int | None, ip: str, + ) -> tuple[int, list[dict[str, Any]], list[dict[str, Any]]]: + """桌面端补传本地新增(含升级前的历史条目),然后拿回全量。""" + tenant_id = str(tenant_id) + items = list(adds or [])[:MAX_SYNC_ITEMS] + rejected: list[dict[str, Any]] = [] + added = 0 + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + version = self._version(db, tenant_id) + 1 + for item in items: + if not isinstance(item, dict): + continue + try: + _entry, changed = self._add_entry( + db, tenant_id, item, source="desktop", actor_label=actor_label, + actor_account_id=actor_account_id, version=version, + ) + except ValueError as exc: + rejected.append({ + "account": str(item.get("account") or "")[:191], + "conversation": str(item.get("conversation") or "")[:512], + "error": str(exc), + }) + continue + added += int(changed) + if added: + self.database._audit( + db, None, "blacklist.sync", + f"tenant={tenant_id} added={added} by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + else: + db.rollback() + version, entries = self.blacklist(tenant_id) + return version, entries, rejected + + # ── 会话 ───────────────────────────────────────────────────────────── + def conversations( + self, tenant_id: str, *, query: str = "", scope: str = "all", cursor: str = "", + limit: int = 30, + ) -> dict[str, Any]: + """手机端会话列表。只给名称、类型、最近时间和黑名单状态,不出聊天正文。""" + tenant_id = str(tenant_id) + limit = max(1, min(int(limit or 30), 100)) + needle = str(query or "").strip()[:100] + with self.database.connect() as db: + blocked_total = int(db.execute( + "SELECT COUNT(*) FROM conversation_blacklist WHERE tenant_id=? AND removed_at=''", + (tenant_id,), + ).fetchone()[0]) + if scope == "blocked": + params: list[Any] = [tenant_id] + where = "b.tenant_id=? AND b.removed_at=''" + if needle: + where += (" AND (b.display_name LIKE ? ESCAPE '\\' OR b.conversation LIKE ? ESCAPE '\\'" + " OR b.account_name LIKE ? ESCAPE '\\')") + params.extend([_like(needle)] * 3) + if cursor: + try: + where += " AND b.id limit + rows = rows[:limit] + items = [] + for row in rows: + archive_name, archive_account = ("", "") + if not row["display_name"] or not row["account_name"]: + archive_name, archive_account = self._archive_names( + db, tenant_id, str(row["account"]), str(row["conversation"]) + ) + conversation = str(row["conversation"]) + kind = _conversation_kind(conversation) + items.append({ + "account": str(row["account"]), + "account_name": str(row["account_name"] or archive_account or ""), + "conversation": conversation, + "name": str(row["display_name"] or archive_name or conversation), + "type": kind, + "type_label": CONVERSATION_TYPE_LABELS.get(kind, "会话"), + "last_message_at": "", + "blocked": True, + "blacklist": self._public_entry(row), + }) + next_cursor = str(rows[-1]["id"]) if has_more and rows else "" + return {"items": items, "next_cursor": next_cursor, "has_more": has_more, + "blocked_total": blocked_total} + + params = [tenant_id] + where = "c.tenant_id=?" + if needle: + where += " AND (c.name LIKE ? ESCAPE '\\' OR c.external_id LIKE ? ESCAPE '\\')" + params.extend([_like(needle), _like(needle)]) + if cursor: + try: + cursor_time, cursor_id = cursor.split("|", 1) + except ValueError as exc: + raise ValueError("分页游标不正确") from exc + where += " AND (c.last_message_at,c.id)<(?,?)" + params.extend([cursor_time, cursor_id]) + params.append(limit + 1) + try: + rows = db.execute( + f"""SELECT c.id,c.external_id,c.name,c.conversation_type,c.last_message_at, + a.external_account_id,a.display_name AS account_display_name, + b.id AS blacklist_id,b.account,b.conversation,b.display_name, + b.account_name,b.reason,b.source,b.created_by,b.created_at + FROM archive_conversation c + JOIN archive_source_account a ON a.id=c.source_account_id + LEFT JOIN conversation_blacklist b + ON b.tenant_id=c.tenant_id AND b.account=a.external_account_id + AND b.conversation=c.external_id AND b.removed_at='' + WHERE {where} + ORDER BY c.last_message_at DESC,c.id DESC LIMIT ?""", + tuple(params), + ).fetchall() + except Exception as exc: + if "no such table" in str(exc): + rows = [] + else: + raise + has_more = len(rows) > limit + rows = rows[:limit] + items = [] + for row in rows: + kind = str(row["conversation_type"] or "unknown") + items.append({ + "account": str(row["external_account_id"]), + "account_name": str(row["account_display_name"] or ""), + "conversation": str(row["external_id"]), + "name": str(row["name"] or row["external_id"]), + "type": kind, + "type_label": CONVERSATION_TYPE_LABELS.get(kind, "会话"), + "last_message_at": str(row["last_message_at"] or ""), + "blocked": row["blacklist_id"] is not None, + "blacklist": self._public_entry(row) if row["blacklist_id"] is not None else None, + }) + next_cursor = f"{rows[-1]['last_message_at']}|{rows[-1]['id']}" if has_more and rows else "" + return {"items": items, "next_cursor": next_cursor, "has_more": has_more, + "blocked_total": blocked_total} + + +def _conversation_kind(external_id: str) -> str: + prefix = str(external_id or "").split(":", 1)[0].upper() + return { + "M": "direct_wechat", "S": "direct_wecom", "R": "group", + "Y": "application", "O": "service", + }.get(prefix, "unknown") + + +def now_text_days_ago(days: int) -> str: + from datetime import timedelta + + return (datetime.now().astimezone() - timedelta(days=int(days))).isoformat(timespec="seconds") diff --git a/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/zyt_auth.py b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/zyt_auth.py new file mode 100644 index 000000000..6fad0c3b9 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/payload/wechat_rpa/zyt_auth.py @@ -0,0 +1,357 @@ +"""ZYT 管理员身份服务适配器。 + +桌面软件的密码只发送给 ZYT;客服后台拿到管理员 token 后通过最小会话接口验证, +再签发仅能访问客服数据的短期 token。 +""" + +from __future__ import annotations + +import json +import os +import re +import socket +import urllib.error +import urllib.parse +import urllib.request +from typing import Any + + +WECOM_RPA_TERMINAL = 7 +# 手机远程控制台。和桌面端(7)、网页后台(1)分开保存 ZYT 会话,互不顶号。 +# 8 已被 ZYT 的 AI 浏览器(app/mcp/service/ConsoleService)占用,不能复用。 +# 这个终端的登录只由客服后台代为发起,密码不经过任何第三方页面。 +WECOM_RPA_MOBILE_TERMINAL = 9 +DEFAULT_ZYT_API_BASE_URL = os.environ.get( + "ZYT_API_BASE_URL", "http://127.0.0.1" +).strip().rstrip("/") + + +class ZytAuthError(RuntimeError): + pass + + +class ZytLoginError(ZytAuthError): + """ZYT 登录失败,并带上可供界面区分的原因。 + + kind 取值: + credentials 密码错误(ZYT 会按出口 IP 计入锁定次数) + unknown_account 账号不存在(ZYT 不计入锁定次数;对外提示与密码错误相同) + password_change_required 密码正确,但账号仍在用初始密码,须先到 ZYT 网页后台修改 + locked ZYT 已按出口 IP 锁定登录,retry_after 为剩余秒数 + disabled 管理员账号已停用 + terminal_unsupported 线上 ZYT 还不认识手机终端 9 + unavailable 网络、超时或 ZYT 返回了非预期数据 + rejected 其他明确的拒绝(原样展示 ZYT 的提示) + """ + + def __init__(self, message: str, *, kind: str = "rejected", retry_after: int = 0): + super().__init__(message) + self.kind = kind + self.retry_after = max(0, int(retry_after or 0)) + + +def api_base_url() -> str: + value = DEFAULT_ZYT_API_BASE_URL + parsed = urllib.parse.urlparse(value) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytAuthError("ZYT_API_BASE_URL 配置不正确") + return value.rstrip("/") + + +def verify_desktop_token( + token: str, *, api_url: str | None = None, timeout: float = 8.0 +) -> dict[str, Any]: + token = str(token or "").strip() + if not token: + raise ZytAuthError("缺少 ZYT 登录令牌") + base_url = str(api_url or api_base_url()).strip().rstrip("/") + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytAuthError("ZYT 登录 API 域名配置不正确") + last_error = "ZYT 登录已失效" + # 第一条是新版专用接口;第二条兼容尚未发布桌面终端的线上旧版。 + endpoints = ( + ("/adminapi/desktop/session", False), + ("/adminapi/auth.admin/mySelf", True), + ) + for path, legacy in endpoints: + request = urllib.request.Request( + f"{base_url}{path}", + headers={ + "Accept": "application/json", + "User-Agent": "WeCom-RPA-Backend/1.0", + # ZYT 管理端登录中间件读取 token 头,不是 Bearer。 + "token": token, + }, + method="GET", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + payload = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + last_error = f"ZYT 身份服务拒绝请求(HTTP {exc.code})" + # 只兼容尚未部署的路由;鉴权拒绝不能降级为网页身份。 + if not legacy and exc.code in {404, 405}: + continue + raise ZytAuthError(last_error) from exc + except (urllib.error.URLError, TimeoutError, socket.timeout) as exc: + raise ZytAuthError( + f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytAuthError("ZYT 身份服务返回了无效数据") from exc + + if not isinstance(payload, dict): + raise ZytAuthError("ZYT 身份服务返回了无效数据") + if str(payload.get("code")) != "1": + last_error = str( + payload.get("msg") if isinstance(payload, dict) else "" + ) or "ZYT 登录已失效" + if legacy and "ip" in last_error.lower() and "变化" in last_error: + raise ZytAuthError( + "ZYT 桌面认证接口尚未部署,旧版接口拒绝跨服务器验证。" + "请管理员部署 /adminapi/desktop/session 后重新登录" + ) + raise ZytAuthError(last_error) + data = payload.get("data") + if not isinstance(data, dict): + raise ZytAuthError("ZYT 身份信息不完整") + + if legacy: + admin = data.get("user") + if not isinstance(admin, dict) or not admin.get("id"): + raise ZytAuthError("ZYT 管理员身份信息不完整") + if int(admin.get("disable") or 0) != 0: + raise ZytAuthError("ZYT 管理员账号已停用") + permissions_value = data.get("permissions") + permissions = ( + sorted( + { + str(item).strip() + for item in permissions_value + if str(item).strip() + } + ) + if isinstance(permissions_value, list) + else [] + ) + role_ids_value = admin.get("role_ids") + role_ids: list[int] = [] + if isinstance(role_ids_value, list): + for item in role_ids_value: + try: + role_ids.append(int(item)) + except (TypeError, ValueError): + continue + profile_identity = { + "user_id": f"admin:{admin['id']}", + "sn": str(admin.get("account") or ""), + "nickname": str(admin.get("name") or admin.get("account") or ""), + "mobile": str(admin.get("phone") or ""), + "avatar": str(admin.get("avatar") or ""), + "terminal": WECOM_RPA_TERMINAL, + "status": "active", + "account_type": "admin", + "root": bool(int(admin.get("root") or 0)), + "role_ids": sorted(set(role_ids)), + "permissions": permissions, + "permissions_known": isinstance(permissions_value, list), + } + return profile_identity + + identity = dict(data) + if not identity.get("user_id"): + raise ZytAuthError("ZYT 身份信息不完整") + if str(identity.get("terminal")) != str(WECOM_RPA_TERMINAL): + raise ZytAuthError("当前令牌不是企业微信客服桌面端令牌") + if str(identity.get("status") or "") != "active": + raise ZytAuthError("ZYT 管理员账号已停用") + permissions_value = identity.get("permissions") + identity["permissions"] = ( + sorted( + { + str(item).strip() + for item in permissions_value + if str(item).strip() + } + ) + if isinstance(permissions_value, list) + else [] + ) + identity["permissions_known"] = isinstance(permissions_value, list) + identity["root"] = bool(int(identity.get("root") or 0)) + # 新版专用接口完整返回权限;旧版缺权限时保留未知状态,患者功能 + # 按最小权限关闭,不再跨 IP 请求网页 mySelf 补权限。 + return identity + + raise ZytAuthError(last_error) + + +def _checked_base_url(api_url: str | None) -> str: + base_url = str(api_url or api_base_url()).strip().rstrip("/") + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytLoginError("ZYT 登录 API 域名配置不正确", kind="unavailable") + return base_url + + +def _zyt_json( + method: str, url: str, *, payload: dict[str, Any] | None = None, + token: str = "", timeout: float, +) -> dict[str, Any]: + headers = {"Accept": "application/json", "User-Agent": "WeCom-RPA-Backend/1.0"} + body = None + if payload is not None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + headers["Content-Type"] = "application/json; charset=utf-8" + if token: + headers["token"] = token + request = urllib.request.Request(url, data=body, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + payload_out = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + raise ZytLoginError( + f"ZYT 身份服务拒绝请求(HTTP {exc.code})", kind="unavailable" + ) from exc + except (urllib.error.URLError, TimeoutError, socket.timeout, OSError) as exc: + raise ZytLoginError( + f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}", kind="unavailable" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytLoginError("ZYT 身份服务返回了无效数据", kind="unavailable") from exc + if not isinstance(payload_out, dict): + raise ZytLoginError("ZYT 身份服务返回了无效数据", kind="unavailable") + return payload_out + + +_LOCK_MINUTES_RE = re.compile(r"(\d+)\s*分钟") + + +def _login_failure(message: str) -> ZytLoginError: + """把 ZYT 登录接口的中文提示归类。 + + 账号不存在和密码错误合并成同一句,免得手机端被拿来探测哪些账号存在。 + """ + text = str(message or "").strip() or "ZYT 登录失败" + if "连续" in text and "分钟" in text: + match = _LOCK_MINUTES_RE.search(text.split("请", 1)[-1]) + minutes = int(match.group(1)) if match else 30 + return ZytLoginError(text, kind="locked", retry_after=max(1, minutes) * 60) + # ZYT 只在"账号存在、密码不对"时累计出口 IP 的失败次数(LoginValidate::password), + # 账号不存在直接返回、不计数。两者对外同一句话,对内要分开:限流预算只该为前者消耗。 + if "密码错误" in text: + return ZytLoginError("账号或密码错误", kind="credentials") + if text == "账号不存在": + return ZytLoginError("账号或密码错误", kind="unknown_account") + if "禁用" in text or "停用" in text: + return ZytLoginError("该 ZYT 账号已停用,请联系管理员", kind="disabled") + if "terminal" in text.lower() or "终端" in text: + return ZytLoginError( + "ZYT 尚未开通企微客服手机端登录(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + return ZytLoginError(text, kind="rejected") + + +def login_admin( + account: str, password: str, *, terminal: int = WECOM_RPA_MOBILE_TERMINAL, + api_url: str | None = None, timeout: float = 10.0, +) -> str: + """用 ZYT 管理员账号密码换取指定终端的 token。只在服务端调用。""" + + account = str(account or "").strip() + if not account or not password: + raise ZytLoginError("请输入账号和密码", kind="rejected") + base_url = _checked_base_url(api_url) + result = _zyt_json( + "POST", f"{base_url}/adminapi/login/account", + payload={"account": account, "password": str(password), "terminal": int(terminal)}, + timeout=timeout, + ) + if str(result.get("code")) != "1": + raise _login_failure(str(result.get("msg") or "")) + data = result.get("data") + token = str(data.get("token") or "").strip() if isinstance(data, dict) else "" + if not token: + raise ZytLoginError("ZYT 没有返回登录令牌", kind="unavailable") + # is_paw=0:管理员发放的初始密码还没改过。网页后台会强制先改密;这里是面向公网的 + # 长期会话,同样不放行,并立即注销刚拿到的 ZYT 令牌。 + if str(data.get("is_paw", 1)).strip() == "0": + logout_admin(token, api_url=base_url, timeout=timeout) + raise ZytLoginError( + "该账号仍在使用初始密码,请先登录 ZYT 网页后台修改密码,再使用手机控制台", + kind="password_change_required", + ) + return token + + +def verify_admin_session( + token: str, *, terminal: int = WECOM_RPA_MOBILE_TERMINAL, + api_url: str | None = None, timeout: float = 8.0, +) -> dict[str, Any]: + """通过 /adminapi/desktop/session 读取当前身份与权限,并核对终端类型。 + + 与 ``verify_desktop_token`` 不同,这里没有旧版 mySelf 兜底:手机终端本身 + 就要求 ZYT 已升级,旧接口也不会返回终端 9。 + """ + + token = str(token or "").strip() + if not token: + raise ZytLoginError("缺少 ZYT 登录令牌", kind="rejected") + base_url = _checked_base_url(api_url) + try: + result = _zyt_json( + "GET", f"{base_url}/adminapi/desktop/session", token=token, timeout=timeout + ) + except ZytLoginError as exc: + cause = exc.__cause__ + if isinstance(cause, urllib.error.HTTPError) and cause.code in {404, 405}: + raise ZytLoginError( + "ZYT 尚未部署桌面身份接口,暂不能使用手机端", kind="terminal_unsupported" + ) from exc + raise + if str(result.get("code")) != "1": + message = str(result.get("msg") or "").strip() or "ZYT 登录已失效" + if "不属于" in message and "桌面" in message: + raise ZytLoginError( + "ZYT 尚未开通企微客服手机端身份校验(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + if "禁用" in message or "不存在" in message: + raise ZytLoginError("ZYT 管理员账号不存在或已停用", kind="disabled") + raise ZytLoginError(message, kind="rejected") + data = result.get("data") + if not isinstance(data, dict) or not data.get("user_id"): + raise ZytLoginError("ZYT 身份信息不完整", kind="unavailable") + identity = dict(data) + if str(identity.get("terminal")) != str(int(terminal)): + raise ZytLoginError( + "ZYT 尚未开通企微客服手机端身份校验(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + if str(identity.get("status") or "") != "active": + raise ZytLoginError("ZYT 管理员账号已停用", kind="disabled") + permissions_value = identity.get("permissions") + identity["permissions"] = ( + sorted({str(item).strip() for item in permissions_value if str(item).strip()}) + if isinstance(permissions_value, list) else [] + ) + identity["permissions_known"] = isinstance(permissions_value, list) + identity["root"] = bool(int(identity.get("root") or 0)) + return identity + + +def logout_admin(token: str, *, api_url: str | None = None, timeout: float = 5.0) -> None: + """尽力注销 ZYT 侧会话;失败不影响本地登出。""" + + token = str(token or "").strip() + if not token: + return + try: + _zyt_json( + "POST", f"{_checked_base_url(api_url)}/adminapi/login/logout", + payload={}, token=token, timeout=timeout, + ) + except ZytAuthError: + return diff --git a/deploy/mobile-remote-20260928/server-bundle/release-plan.json b/deploy/mobile-remote-20260928/server-bundle/release-plan.json new file mode 100644 index 000000000..bb6a856ca --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/release-plan.json @@ -0,0 +1,30 @@ +{ + "release": "mobile-remote-20260928", + "candidateRoot": "C:/kefu", + "backendFiles": [ + "wechat_rpa/admin_api.py", + "wechat_rpa/zyt_auth.py" + ], + "newBackendFiles": [ + "wechat_rpa/remote_api.py", + "wechat_rpa/remote_store.py" + ], + "deployFiles": [ + "deploy/im-admin/Dockerfile" + ], + "frontendDir": "admin-web/apps/mobile-h5/dist", + "serverTestModules": [ + "test_remote_api", + "test_zyt_auth", + "test_backend" + ], + "schemaMigration": "additive", + "restartServices": [ + "api" + ], + "notes": "Mobile remote console (/m/, /api/v2/mobile/*), desktop command channel and server-side conversation blacklist. New tables mobile_sessions, desktop_commands, desktop_status, conversation_blacklist are created with CREATE TABLE IF NOT EXISTS on API start; existing tables are not altered. Rollback keeps the new (unused) tables.", + "imageTestModules": [ + "test_archive_api" + ], + "testNotes": "serverTestModules ship from C:/kefu/wechat_rpa (test_backend there is newer than the copy in the production image). imageTestModules run the image's own copy. The image's own test_admin_api and test_backend already fail on untouched production code (one predates the 2026-09-23 COS release-package URLs, the other still mocks the old two-request ZYT session flow), so they are only run locally." +} diff --git a/deploy/mobile-remote-20260928/server-bundle/remote_server_guard.py b/deploy/mobile-remote-20260928/server-bundle/remote_server_guard.py new file mode 100644 index 000000000..ea536699e --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/remote_server_guard.py @@ -0,0 +1,121 @@ +"""Read-only checks for mobile-remote-20260928: baseline / candidate / deployed. + +Prints hashes and status only; never reads chat content, patients or credentials. +""" +from pathlib import Path +import argparse, hashlib, json, subprocess, urllib.error, urllib.request + +parser = argparse.ArgumentParser() +parser.add_argument('mode', choices=['baseline', 'candidate', 'deployed']) +args = parser.parse_args() +RELEASE = 'mobile-remote-20260928' +folder = Path(__file__).resolve().parent +project = Path('/opt/im-admin') +base = json.loads((folder / 'server-baseline.json').read_text()) +manifest = json.loads((folder / 'manifest.json').read_text()) +payload_rows = [row for row in manifest if row['path'].startswith('payload/')] +sha = lambda data: hashlib.sha256(data).hexdigest() +image = lambda ref: subprocess.check_output(['docker', 'image', 'inspect', ref, '--format', '{{.Id}}'], text=True).strip() + +# The unpacked bundle itself must be intact. +for row in manifest: + path = (folder / row['path']).resolve() + assert path.is_relative_to(folder), row['path'] + assert path.stat().st_size == row['bytes'] and sha(path.read_bytes()) == row['sha256'], row['path'] + +# project-relative path -> expected sha256 on the host +if args.mode == 'baseline': + expected_host = dict(base['expected']) +else: + expected_host = {row['path'][len('payload/'):]: row['sha256'] for row in payload_rows} +container_code = {k: v for k, v in expected_host.items() if k.startswith(('wechat_rpa/', 'admin-web/'))} + + +def container_hashes(target, ref=None): + code = ('from pathlib import Path;import hashlib,json,sys;names=json.load(sys.stdin);' + 'print(json.dumps({n:(hashlib.sha256((Path("/app")/n).read_bytes()).hexdigest() ' + 'if (Path("/app")/n).is_file() else None) for n in names}))') + command = (['docker', 'run', '--rm', '-i', '--network=none', '--entrypoint', 'python', ref, '-c', code] + if ref else ['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code]) + return json.loads(subprocess.check_output(command, input=json.dumps(target).encode())) + + +current = image('zyt/wecom-admin:current') +result = {'mode': args.mode, 'release': RELEASE, 'image': current} +# 只重建 api:网关和知识库 worker 继续跑它们自己的镜像(2026-09-24 那次前端发布同样只换了 api), +# 这里记录下来,部署后核对它们没有被动过。 +OTHERS = ['im-admin-gateway-1', 'im-admin-knowledge-worker-1'] + + +def inspect(container): + raw = json.loads(subprocess.check_output(['docker', 'inspect', container]))[0] + return {'id': raw['Id'], 'image': raw['Image'], 'running': raw['State']['Running'], + 'health': raw['State'].get('Health', {}).get('Status', 'not_configured')} + + +if args.mode == 'baseline': + api = inspect('im-admin-api-1') + assert api['image'] == current, 'API container is not on zyt/wecom-admin:current' + assert api['running'] and api['health'] == 'healthy', 'API container is not healthy' + others = {name: inspect(name) for name in OTHERS} + for name, state in others.items(): + assert state['running'], 'Not running: ' + name + result['others'] = others + for name in base.get('must_not_exist', []): + assert not (project / name).exists(), 'Unexpected existing path (partially deployed?): ' + name + +if args.mode in ('baseline', 'deployed'): + for name, wanted in expected_host.items(): + assert sha((project / name).read_bytes()) == wanted, 'Host drift: ' + name + actual = container_hashes(sorted(container_code)) + assert actual == container_code, 'Running api container differs from expected code' + result['files'] = len(expected_host) + +if args.mode == 'candidate': + candidate = 'zyt/wecom-admin:' + RELEASE + actual = container_hashes(sorted(container_code), candidate) + assert actual == container_code, 'Candidate image differs from manifest' + result['candidate'] = image(candidate) + result['files'] = len(container_code) + +if args.mode == 'deployed': + wanted = image('zyt/wecom-admin:' + RELEASE) + assert current == wanted, 'current tag does not point at the release image' + raw = json.loads(subprocess.check_output(['docker', 'inspect', 'im-admin-api-1']))[0] + assert raw['Image'] == wanted and raw['State']['Running'] and raw['RestartCount'] == 0 + assert raw['State'].get('Health', {}).get('Status') == 'healthy' + recorded = folder / 'baseline-check.json' + if recorded.exists(): + before = json.loads(recorded.read_text())['others'] + for name in OTHERS: + now = inspect(name) + assert now['id'] == before[name]['id'] and now['running'], 'Unrelated container changed: ' + name + result['others_unchanged'] = OTHERS + for url in ['http://127.0.0.1:18766/api/v2/health', 'http://127.0.0.1:18770/health']: + with urllib.request.urlopen(url, timeout=15) as response: + assert response.status == 200, url + # New routes exist and refuse anonymous callers. No login is attempted. + for method, path in [('GET', '/api/v2/mobile/me'), ('GET', '/api/v2/mobile/desktops'), + ('POST', '/api/v2/desktop/commands/poll'), ('GET', '/api/v2/desktop/blacklist')]: + request = urllib.request.Request('http://127.0.0.1:18766' + path, method=method, + data=b'{}' if method == 'POST' else None, + headers={'Content-Type': 'application/json'}) + try: + urllib.request.urlopen(request, timeout=15) + except urllib.error.HTTPError as exc: + assert exc.code == 401, (path, exc.code) + else: + raise AssertionError('Accessible without authentication: ' + path) + with urllib.request.urlopen('http://127.0.0.1:18766/m/', timeout=15) as response: + body = response.read() + assert response.status == 200 and '企微客服助手'.encode() in body, 'mobile console entry missing' + code = ("import sqlite3,json;c=sqlite3.connect('file:/data/backend.db?mode=ro',uri=True,timeout=30);" + "names={r[0] for r in c.execute(\"SELECT name FROM sqlite_master WHERE type='table'\")};" + "print(json.dumps({'tables':sorted(names & {'mobile_sessions','desktop_commands','desktop_status','conversation_blacklist'})," + "'quick_check':c.execute('PRAGMA quick_check').fetchone()[0]}))") + db = json.loads(subprocess.check_output(['docker', 'exec', '-i', 'im-admin-api-1', 'python', '-c', code])) + assert db['quick_check'] == 'ok' and len(db['tables']) == 4, db + result['database'] = db + +result['status'] = 'passed' +print(json.dumps(result, ensure_ascii=False)) diff --git a/deploy/mobile-remote-20260928/server-bundle/rollback.sh b/deploy/mobile-remote-20260928/server-bundle/rollback.sh new file mode 100644 index 000000000..7098a1e8d --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/rollback.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# 恢复 2 个后端文件和 Dockerfile,删除新增文件与 /m/ 前端,切回旧镜像,只重建 api。 +# 数据库保留:新增的 4 张表旧代码不会读写,留着不影响回滚后的运行。 +set -euo pipefail +test "${REMOTE_RELEASE_APPROVED:-}" = mobile-remote-20260928 +release=/home/ps/mobile-remote-20260928 +cd /opt/im-admin/deploy/im-admin +test -d "$release/source-before" +test "$(docker image inspect zyt/wecom-admin:before-mobile-remote-20260928 --format '{{.Id}}')" = "$(cat "$release/image-before.txt")" +python3 - <<'PY' +from pathlib import Path +import hashlib, json, os, shutil +r = Path('/home/ps/mobile-remote-20260928'); project = Path('/opt/im-admin') +base = json.loads((r / 'server-baseline.json').read_text()) +plan = json.loads((r / 'release-plan.json').read_text()) +for name in plan['backendFiles'] + plan['deployFiles']: + data = (r / 'source-before' / name).read_bytes(); assert hashlib.sha256(data).hexdigest() == base['expected'][name] + target = project / name; tmp = target.with_name(target.name + '.rollback-mobile.tmp') + if tmp.exists(): + tmp.unlink() + with tmp.open('xb') as f: + f.write(data) + os.chmod(tmp, target.stat().st_mode & 0o777); os.replace(tmp, target) +for name in plan['newBackendFiles']: + target = project / name + if target.exists(): + target.unlink() +for path in [project / plan['frontendDir'], project / (plan['frontendDir'] + '.mobile-remote.tmp')]: + if path.exists(): + shutil.rmtree(path) +mobile_app = project / 'admin-web/apps/mobile-h5' +if mobile_app.exists() and not any(mobile_app.iterdir()): + mobile_app.rmdir() +PY +docker tag zyt/wecom-admin:before-mobile-remote-20260928 zyt/wecom-admin:current +docker compose up -d --no-deps --no-build --wait --wait-timeout 180 api +curl -fsS -o /dev/null http://127.0.0.1:18766/api/v2/health +curl -fsS -o /dev/null http://127.0.0.1:18770/health +printf '%s\n' 'Previous image and source files restored; live database retained (new tables unused).' diff --git a/deploy/mobile-remote-20260928/server-bundle/server-baseline.json b/deploy/mobile-remote-20260928/server-bundle/server-baseline.json new file mode 100644 index 000000000..b2ef2c887 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/server-baseline.json @@ -0,0 +1,14 @@ +{ + "host": "192.168.110.111", + "user": "ps", + "project": "/opt/im-admin", + "compose": "/opt/im-admin/deploy/im-admin", + "checked_at": "2026-09-28", + "source": "Read from production /opt/im-admin on 2026-09-28 (exact bytes kept in baseline-snapshot/). admin_api.py = cos-direct release of 2026-09-23 (C:/wechat_rpa/tmp/cos-direct-server-payload, CRLF), not C:/kefu HEAD; zyt_auth.py = C:/kefu HEAD content with CRLF line endings; Dockerfile = C:/kefu HEAD. The payload admin_api.py is production + this release's changes (3-way merge, checked by prepare_bundle.py).", + "expected": { + "wechat_rpa/admin_api.py": "fcc94534fee7d1cd524d6923eea035a212a3f6d3ba35f19d41f9ee17b3c74bdc", + "wechat_rpa/zyt_auth.py": "6b8ded1ad17766957cedbbcae7841ca8bd783bfa9c0f47266d0d42ac7ae66691", + "deploy/im-admin/Dockerfile": "2bc1a6c1a0d9eeea512a560b676dfa0a4e060513d24cbc2c12e251c061fbe1a7" + }, + "must_not_exist": ["wechat_rpa/remote_api.py", "wechat_rpa/remote_store.py", "admin-web/apps/mobile-h5"] +} diff --git a/deploy/mobile-remote-20260928/server-bundle/tests/test_backend.py b/deploy/mobile-remote-20260928/server-bundle/tests/test_backend.py new file mode 100644 index 000000000..6a460176b --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/tests/test_backend.py @@ -0,0 +1,1527 @@ +# -*- coding: utf-8 -*- +"""配置后台与桌面端同步的本地闭环测试。""" + +from __future__ import annotations + +import json +import hashlib +import shutil +import model_protocol +import socket +import sqlite3 +import tempfile +import threading +import unittest +import urllib.error +import urllib.request +from unittest import mock +from pathlib import Path + +from test_support import local_state_redirect +import admin_backend +import app_version +import backend_client +import zyt_auth + + +class BackendIntegrationTest(unittest.TestCase): + + + def test_development_diagnostics_are_cloud_controlled_and_redacted(self) -> None: + config = { + "AI_DEVELOPMENT_MODE": True, + "AI_PROVIDER_TYPE": "openai", + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_MCP_SERVERS": [ + { + "headers": {"Authorization": "Bearer hidden-auth"}, + "env": {"ACCESS_TOKEN": "hidden-token"}, + "args": [ + "--token", + "hidden-argument", + "--password=hidden-inline", + "Bearer hidden-bearer", + ], + } + ], + } + response = {"version": 8, "updated_at": "now"} + diagnostics = backend_client._config_diagnostics( + config, + response, + "https://cloud.example/api/v1/desktop/config?api_key=query-secret", + ) + rendered = "\n".join(diagnostics) + self.assertIn("https://cloud.example/api/v1/desktop/config", rendered) + self.assertIn("云端配置版本: v8", rendered) + self.assertIn('"AI_MODEL": "test-model"', rendered) + for secret in ( + "top-secret-key", + "hidden-auth", + "hidden-token", + "hidden-argument", + "hidden-inline", + "hidden-bearer", + "query-secret", + ): + self.assertNotIn(secret, rendered) + self.assertEqual( + backend_client._config_diagnostics( + {**config, "AI_DEVELOPMENT_MODE": False}, response, "https://cloud.example/config" + ), + [], + ) + + def test_model_request_diagnostics_never_print_the_api_key(self) -> None: + import ai_chat + import ai_config + + previous = { + "AI_DEVELOPMENT_MODE": getattr(ai_config, "AI_DEVELOPMENT_MODE", False), + "AI_API_BASE": ai_config.AI_API_BASE, + "AI_API_KEY": ai_config.AI_API_KEY, + "AI_MODEL": ai_config.AI_MODEL, + } + try: + ai_config.AI_DEVELOPMENT_MODE = True + ai_config.AI_API_BASE = "https://api.example/v1" + ai_config.AI_API_KEY = "never-print-this-key" + ai_config.AI_MODEL = "diagnostic-model" + with mock.patch("builtins.print") as printer: + ai_chat._log_request_diagnostics( + "https://api.example/v1/chat/completions", "OpenAI 兼容" + ) + rendered = "\n".join( + " ".join(str(item) for item in call.args) + for call in printer.call_args_list + ) + self.assertIn("https://api.example/v1/chat/completions", rendered) + self.assertIn("diagnostic-model", rendered) + self.assertNotIn("never-print-this-key", rendered) + finally: + for key, value in previous.items(): + setattr(ai_config, key, value) + + def test_model_connection_uses_unsaved_values_and_saved_key(self) -> None: + current = { + "AI_API_BASE": "https://saved.example/v1", + "AI_API_KEY": "saved-secret", + "AI_MODEL": "saved-model", + "AI_TIMEOUT": 120, + } + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://new.example/v1", + "AI_API_KEY": "", + "AI_MODEL": "new-model", + "AI_TIMEOUT": "180", + }, + current, + ) + self.assertEqual(config["endpoint"], "https://new.example/v1/chat/completions") + self.assertEqual(config["api_key"], "saved-secret") + self.assertEqual(config["model"], "new-model") + self.assertEqual(config["timeout"], 60) + + def test_model_connection_success_does_not_expose_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"choices": [{"message": {"content": "OK"}}]} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(200, response) + ) as call: + result = admin_backend.test_model_connection(config) + self.assertTrue(result["ok"]) + self.assertEqual(result["http_status"], 200) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + self.assertEqual(call.call_args.args[0], config["endpoint"]) + self.assertEqual( + call.call_args.kwargs["headers"]["Authorization"], "Bearer top-secret-key" + ) + payload = call.call_args.kwargs["payload"] + self.assertEqual(payload["model"], "test-model") + + def test_model_connection_error_redacts_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"error": {"message": "invalid top-secret-key"}} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(401, response) + ): + result = admin_backend.test_model_connection(config) + self.assertFalse(result["ok"]) + self.assertEqual(result["http_status"], 401) + self.assertIn("API Key 无效", result["message"]) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + + def test_dify_and_comfyui_use_provider_specific_endpoints(self) -> None: + dify = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "dify", + "AI_API_BASE": "https://dify.example/v1", + "AI_API_KEY": "app-secret", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(dify["endpoint"], "https://dify.example/v1/chat-messages") + self.assertEqual(dify["provider_type"], "dify") + + comfyui = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "comfyui", + "AI_API_BASE": "http://127.0.0.1:8188", + "AI_API_KEY": "", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(comfyui["endpoint"], "http://127.0.0.1:8188/system_stats") + with mock.patch( + "admin_backend._perform_http_request", + return_value=(200, b'{"system": {"os": "windows"}, "devices": []}'), + ) as call: + result = admin_backend.test_model_connection(comfyui) + self.assertTrue(result["ok"]) + self.assertEqual(call.call_args.kwargs["method"], "GET") + self.assertIsNone(call.call_args.kwargs["payload"]) + + def test_desktop_ai_respects_explicit_dify_provider(self) -> None: + import ai_chat + import ai_config + + old_provider = ai_config.AI_PROVIDER_TYPE + old_base = ai_config.AI_API_BASE + try: + ai_config.AI_PROVIDER_TYPE = "dify" + ai_config.AI_API_BASE = "https://dify.example/v1" + self.assertTrue(ai_chat._is_dify_endpoint()) + self.assertEqual( + ai_chat._completions_url(), + "https://dify.example/v1/chat-messages", + ) + ai_config.AI_PROVIDER_TYPE = "openai" + self.assertFalse(ai_chat._is_dify_endpoint()) + finally: + ai_config.AI_PROVIDER_TYPE = old_provider + ai_config.AI_API_BASE = old_base + + + def test_release_status_detects_optional_and_forced_updates(self) -> None: + optional = app_version.release_status( + {"latest_version": "99.0.0", "force_upgrade": False} + ) + self.assertTrue(optional["update_available"]) + self.assertFalse(optional["force_upgrade"]) + + forced = app_version.release_status( + {"latest_version": "2.0.0", "force_upgrade": True} + ) + self.assertTrue(forced["force_upgrade"]) + + current = app_version.release_status( + {"latest_version": app_version.APP_VERSION, "force_upgrade": True} + ) + self.assertFalse(current["update_available"]) + self.assertFalse(current["force_upgrade"]) + + downgrade = app_version.release_status( + {"latest_version": "1.0.1", "force_upgrade": True} + ) + self.assertFalse(downgrade["update_available"]) + self.assertFalse(downgrade["force_upgrade"]) + + def test_release_form_validation(self) -> None: + release = admin_backend.validate_release_form( + { + "latest_version": "v1.2.3", + "download_url": "https://example.com/client.exe", + "release_notes": "修复已知问题", + "force_upgrade": "1", + } + ) + self.assertEqual(release["latest_version"], "1.2.3") + self.assertTrue(release["force_upgrade"]) + automatic = admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "package_sha256": "a" * 64, + "package_size": "1024", + "auto_install": "1", + "release_channel": "stable", + "rollout_percent": "25", + "status": "published", + } + ) + self.assertTrue(automatic["auto_install"]) + self.assertEqual(automatic["rollout_percent"], 25) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2", "force_upgrade": "1"} + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "auto_install": "1", + } + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2.3", "force_upgrade": "1"} + ) + + def test_pbkdf2_fallback_matches_standard_library(self) -> None: + password = b"FallbackPassword123" + salt = b"0123456789abcdef" + expected = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000) + native_pbkdf2 = admin_backend.hashlib.pbkdf2_hmac + try: + admin_backend.hashlib.pbkdf2_hmac = None + actual = admin_backend.pbkdf2_sha256(password, salt, 1_000) + finally: + admin_backend.hashlib.pbkdf2_hmac = native_pbkdf2 + self.assertEqual(actual, expected) + + def test_stale_runtime_file_is_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + runtime_file = root / "backend_runtime.json" + runtime_file.write_text( + json.dumps( + { + "pid": 0, + "host": "127.0.0.1", + "port": 8765, + "server_url": "http://127.0.0.1:8765", + "local_sync_token": "stale-token", + } + ), + encoding="utf-8", + ) + old_runtime_file = backend_client.RUNTIME_FILE + try: + backend_client.RUNTIME_FILE = runtime_file + self.assertEqual(backend_client.discover_local_runtime(), {}) + self.assertEqual(backend_client.discover_local_sync_token(), "") + finally: + backend_client.RUNTIME_FILE = old_runtime_file + + def test_local_agent_name_overrides_cloud_and_blank_follows_cloud(self) -> None: + import ai_config + + old_agent = ai_config.AI_AGENT_NAME + old_cloud = ai_config.AI_CLOUD_AGENT_NAME + old_local = ai_config.AI_LOCAL_AGENT_NAME + old_prompt = ai_config.AI_SYSTEM_PROMPT + try: + ai_config.AI_CLOUD_AGENT_NAME = "旧云端客服" + ai_config.AI_LOCAL_AGENT_NAME = "" + ai_config.AI_AGENT_NAME = "旧云端客服" + ai_config.set_local_agent_name_override("本机客服", persist=False) + ai_config.apply_settings({"AI_AGENT_NAME": "新云端客服"}, persist=False) + self.assertEqual(ai_config.AI_CLOUD_AGENT_NAME, "新云端客服") + self.assertEqual(ai_config.AI_AGENT_NAME, "本机客服") + ai_config.set_local_agent_name_override("", persist=False) + self.assertEqual(ai_config.AI_AGENT_NAME, "新云端客服") + finally: + ai_config.AI_AGENT_NAME = old_agent + ai_config.AI_CLOUD_AGENT_NAME = old_cloud + ai_config.AI_LOCAL_AGENT_NAME = old_local + ai_config.AI_SYSTEM_PROMPT = old_prompt + + +class DesktopConfigFallbackTest(unittest.TestCase): + """桌面端拉配置:新接口优先,老接口兜底。 + + 客户端和服务端是分批更新的,两边版本对不齐是常态而不是意外。直接把路径改 + 成 v2,那些服务端还没升级的部署会当场同步失败——而且失败得很安静,界面上 + 只是配置一直显示旧的,没人会立刻发现。 + """ + + def _fail(self, message: str): + return backend_client.BackendError(message) + + def test_startup_auth_config_is_read_from_management_and_cached(self) -> None: + calls = [] + saved = {} + + def fake(method, server, path, **kwargs): + calls.append((method, server, path)) + return 200, { + "zyt_api_url": "https://account.example.com/zyt/", + "terminal": 7, + "configured": True, + } + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", lambda: {}), + mock.patch.object(backend_client, "save_settings", saved.update), + ): + config = backend_client.fetch_desktop_auth_config("https://admin.example.com/") + + self.assertEqual( + calls, + [("GET", "https://admin.example.com", "/api/v2/desktop/auth/config")], + ) + self.assertEqual(config["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["server_url"], "https://admin.example.com") + + def test_startup_auth_config_rejects_an_incomplete_response(self) -> None: + with mock.patch.object( + backend_client, "_request", return_value=(200, {"terminal": 7}) + ): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client.fetch_desktop_auth_config("https://admin.example.com") + self.assertIn("登录 API", str(caught.exception)) + + def test_zyt_admin_login_uses_admin_endpoint_and_desktop_terminal(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append((method, server, path, kwargs.get("payload"))) + return 200, { + "code": 1, + "msg": "登录成功", + "data": {"token": "admin-desktop-token", "name": "管理员"}, + } + + with mock.patch.object(backend_client, "_request", fake): + token, user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(token, "admin-desktop-token") + self.assertEqual(user["name"], "管理员") + self.assertEqual(calls[0][2], "/adminapi/login/account") + self.assertEqual(calls[0][3]["terminal"], 7) + self.assertEqual(calls[0][3]["account"], "admin") + + def test_zyt_admin_login_falls_back_while_server_has_no_terminal_7(self) -> None: + terminals = [] + + def fake(method, server, path, **kwargs): + terminal = kwargs["payload"]["terminal"] + terminals.append(terminal) + if terminal == 7: + return 200, {"code": 0, "msg": "terminal必须在 1,2 范围内"} + return 200, {"code": 1, "data": {"token": "legacy-admin-token"}} + + with mock.patch.object(backend_client, "_request", fake): + token, _user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(terminals, [7, 1]) + self.assertEqual(token, "legacy-admin-token") + + def test_desktop_login_can_remember_password_with_dpapi_protection(self) -> None: + settings = backend_client.default_settings() + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, + "_protect_secret", + side_effect=lambda value: f"protected:{value}", + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=True, + ) + + self.assertTrue(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["zyt_account"], "admin") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:secret" + ) + self.assertNotIn("secret", json.dumps(saved[-1], ensure_ascii=False).replace( + "protected:secret", "" + )) + + def test_desktop_login_unchecked_forgets_the_saved_password(self) -> None: + settings = backend_client.default_settings() + settings["remembered_password_protected"] = "protected:old-password" + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected:token" + ), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=False, + ) + + self.assertFalse(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["remembered_password_protected"], "") + + def test_forced_logout_keeps_explicitly_remembered_password(self) -> None: + settings = backend_client.default_settings() + settings.update( + { + "zyt_account": "admin", + "remember_login": True, + "remembered_password_protected": "protected:password", + "zyt_token_protected": "protected:zyt-token", + "desktop_token_protected": "protected:desktop-token", + } + ) + saved = [] + with ( + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + ): + backend_client._clear_desktop_credentials(settings) + + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:password" + ) + + def test_session_heartbeat_never_silently_reauthenticates(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + } + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object(backend_client, "save_settings"), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object( + backend_client, + "_request", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + with self.assertRaises(backend_client.AuthenticationError): + backend_client.validate_desktop_session() + exchange.assert_not_called() + + def test_session_heartbeat_persists_the_renewed_expiration(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "desktop_expires_at": 1200, + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_request", + return_value=( + 200, + { + "id": 1, + "sn": "admin", + "expires_at": 50_000, + }, + ), + ), + ): + account = backend_client.validate_desktop_session() + + self.assertEqual(account["id"], 1) + self.assertEqual(saved[-1]["desktop_expires_at"], 50_000) + + def test_revoked_unexpired_session_requires_password_again(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 2000, + "zyt_account": "admin", + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "validate_desktop_session", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + self.assertIsNone(backend_client.restore_desktop_session()) + + exchange.assert_not_called() + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["zyt_account"], "admin") + + def test_naturally_expired_session_can_refresh_from_zyt(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-old", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 999, + } + saved = [] + + def unprotect(value): + return { + "protected-old": "old-token", + "protected-zyt": "zyt-token", + }.get(value, "") + + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object(backend_client, "_unprotect_secret", side_effect=unprotect), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected-new" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "new-token", + "expires_in": 1800, + "account": {"id": 1, "sn": "admin"}, + }, + ), + ): + account = backend_client.restore_desktop_session() + + self.assertEqual(account, {"id": 1, "sn": "admin"}) + self.assertEqual(saved[-1]["desktop_token_protected"], "protected-new") + self.assertEqual(saved[-1]["desktop_expires_at"], 2800) + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"config": {}, "version": 3} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + self.assertEqual(used, "/api/v2/desktop/config") + self.assertEqual(response["version"], 3) + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """服务端还是老版本时,必须照常同步,不能报错。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("HTTP 404", status=404) + return 200, {"config": {}, "version": 7} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config", "/api/v1/desktop/config"]) + self.assertEqual(used, "/api/v1/desktop/config") + self.assertEqual(response["version"], 7) + + def test_the_fallback_is_decided_by_status_not_by_wording(self) -> None: + """老后台对未知 GET 路径回的是"页面不存在",未知 POST 才回"接口不存在"。 + + 一个字之差。曾经这里靠文案匹配,结果对着真的老后台跑就整个失效了—— + 判据必须是状态码。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("页面不存在", status=404) + return 200, {"config": {}} + + with mock.patch.object(backend_client, "_request", fake): + _, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(used, "/api/v1/desktop/config") + + def test_a_connection_failure_is_not_retried_on_the_old_path(self) -> None: + """连不上就是连不上,换个路径同样连不上。 + + 真正的危害不是多发一个请求,而是把"服务器宕了"这个明确故障,伪装成 + "接口不存在"往下走——最后一层抛出来的是老接口的错误,人会去查一个根本 + 没坏的地方。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + # 连不上根本没拿到响应,所以没有状态码 + raise backend_client.BackendError("无法连接后台:timed out") + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"], "不该退回去重试") + self.assertIn("无法连接", str(caught.exception)) + + def test_a_bad_sync_key_surfaces_instead_of_falling_back(self) -> None: + """401/403 说明服务在、只是凭证不对。这是要人去改配置的错误,不能吞。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.AuthenticationError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + + def test_a_404_on_both_paths_reports_the_last_error(self) -> None: + def fake(method, server, path, **kwargs): + raise backend_client.BackendError("HTTP 404", status=404) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + def test_the_legacy_constant_still_points_at_a_real_path(self) -> None: + """DESKTOP_CONFIG_PATH 可能被别处引用,不能变成空字符串。""" + self.assertIn(backend_client.DESKTOP_CONFIG_PATH, backend_client.DESKTOP_CONFIG_PATHS) + self.assertTrue(backend_client.DESKTOP_CONFIG_PATH.startswith("/api/")) + + +class ZytAdminTokenVerificationTest(unittest.TestCase): + def test_backend_verifies_the_admin_desktop_session_endpoint(self) -> None: + session_response = mock.MagicMock() + session_response.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user_id": "admin:1", + "sn": "admin", + "nickname": "管理员", + "mobile": "", + "avatar": "", + "terminal": 7, + "status": "active", + "root": False, + "role_ids": [2], + "permissions": ["tcm.diagnosis/lists"], + }, + } + ).encode("utf-8") + with mock.patch.object( + zyt_auth.urllib.request, + "urlopen", + return_value=session_response, + ) as opened: + identity = zyt_auth.verify_desktop_token( + "admin-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 1) + request = opened.call_args_list[0].args[0] + self.assertEqual( + request.full_url, + "https://admin.example.com/adminapi/desktop/session", + ) + self.assertEqual(request.get_header("Token"), "admin-token") + self.assertEqual(identity["user_id"], "admin:1") + self.assertTrue(identity["permissions_known"]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + def test_old_server_falls_back_to_current_admin_profile(self) -> None: + profile = mock.MagicMock() + profile.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user": { + "id": 9, + "account": "doctor9", + "name": "九号医助", + "phone": "13800138000", + "avatar": "https://img.example/a.png", + "disable": 0, + "root": 0, + "role_ids": [2], + } + ,"permissions": ["tcm.diagnosis/lists"] + }, + } + ).encode("utf-8") + missing = urllib.error.HTTPError( + "https://admin.example.com/adminapi/desktop/session", + 404, + "Not Found", + None, + None, + ) + + with mock.patch.object( + zyt_auth.urllib.request, "urlopen", side_effect=[missing, profile] + ) as opened: + identity = zyt_auth.verify_desktop_token( + "legacy-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 2) + fallback_request = opened.call_args_list[1].args[0] + self.assertEqual( + fallback_request.full_url, + "https://admin.example.com/adminapi/auth.admin/mySelf", + ) + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(identity["nickname"], "九号医助") + self.assertEqual(identity["terminal"], 7) + self.assertEqual(identity["role_ids"], [2]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + +class ModelCallReportFallbackTest(unittest.TestCase): + """调用留痕上报:和拉配置同一套 v2 → v1 顺序。 + + 补这组测试是因为老后台(8765)退役前卡在这一条上:配置同步早有 v2, + 上报却只有 v1。桌面端只要改指到 8766,配置照常同步、回复照常发,唯独 + 调用记录一条都不进库——静悄悄地丢掉的正是出事后用来解释"这句话怎么来的" + 那份证据。 + """ + + def _settings(self): + return {"server_url": "http://x"} + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls"], "新接口在就不该再打老的") + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """还指着老后台(8765)的客户端必须照常上报,不能悄悄丢数据。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("接口不存在", status=404) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls", "/api/v1/model/calls"]) + + def test_a_401_does_not_fall_back(self) -> None: + """凭证不对换个路径重试同样不对,只会多打一个请求。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertFalse(ok) + self.assertEqual(calls, ["/api/v2/model/calls"]) + + def test_a_failure_never_raises_into_the_reply_path(self) -> None: + """上报是观测。它出问题绝不能把已经生成好的回复流程带下水。""" + def boom(*_args, **_kwargs): + raise RuntimeError("断网") + + with ( + mock.patch.object(backend_client, "_request", boom), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + self.assertFalse(backend_client.report_model_call({"task_id": "t1"})) + + +class GatewayUrlDerivationTest(unittest.TestCase): + """网关地址由后台算出来告诉客户端。 + + 这是"只改一个域名"的关键。桌面端配置里只有后台地址一项,网关在哪、模型怎么 + 编排全部由后台决定——运维换网关位置只动后台一处,不用挨个改客户端。 + """ + + def test_direct_port_access_points_at_the_local_gateway_port(self) -> None: + """本机开发:三个服务各占一个端口,网关就在同机的 8770。""" + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "127.0.0.1:8765"), + "http://127.0.0.1:8770/v1/answer", + ) + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "192.168.1.20:8766"), + "http://192.168.1.20:8770/v1/answer", + ) + + def test_a_proxied_domain_gets_a_same_origin_path(self) -> None: + """反代后面只有 80/443 对外,公网根本连不到 8770。 + + 这两种推法必须分开。用一套规则套两种部署,总有一边是错的——而且错得很 + 安静:客户端拿到一个连不上的地址,表现成"模型不回复"。 + """ + self.assertEqual( + admin_backend.derive_gateway_url("", "https", "xchat.example.com"), + "https://xchat.example.com/gateway/v1/answer", + ) + + def test_an_absolute_override_is_used_as_is(self) -> None: + """网关在别的域名或别的机器上时用这个。""" + self.assertEqual( + admin_backend.derive_gateway_url( + "https://gw.example.com/v1/answer", "https", "xchat.example.com" + ), + "https://gw.example.com/v1/answer", + ) + + def test_a_path_override_hangs_off_the_backend_domain(self) -> None: + self.assertEqual( + admin_backend.derive_gateway_url( + "/model-gw/v1/answer", "https", "xchat.example.com" + ), + "https://xchat.example.com/model-gw/v1/answer", + ) + + def test_a_missing_host_still_yields_something_usable(self) -> None: + """拿不到 Host 时给本机地址,而不是空串。 + + 空串会让客户端把 gateway 当成"没配",静默回落到本机单模型——而本机 + 没有密钥,最后报的是一个看不懂的 401。 + """ + self.assertTrue( + admin_backend.derive_gateway_url("", "http", "").startswith("http://") + ) + + def test_an_override_without_a_scheme_is_refused_on_save(self) -> None: + """漏了 http:// 会被拼成 https://你的域名/gw.example.com/v1/answer。 + + 那是个没意义的地址,而且失败要等到桌面端下次发模型请求才暴露。 + """ + base = { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + } + with self.assertRaises(ValueError) as caught: + admin_backend.validate_config_form( + {**base, "AI_GATEWAY_URL": "gw.example.com/v1/answer"}, {} + ) + self.assertIn("http", str(caught.exception)) + + def test_an_empty_override_is_accepted_and_means_auto(self) -> None: + config = admin_backend.validate_config_form( + { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + "AI_GATEWAY_URL": "", + }, + {}, + ) + self.assertEqual(config["AI_GATEWAY_URL"], "") + + +class GatewayArrivesBySyncTest(unittest.TestCase): + """桌面端不用手工配网关——同步时后台会把地址带过来。""" + + def _settings(self, response, existing=None): + """跑一遍 _apply_config_response,返回它写下的设置。""" + saved = {} + base = backend_client.default_settings() + if existing: + base.update(existing) + + with ( + mock.patch.object(backend_client, "load_settings", lambda: dict(base)), + mock.patch.object(backend_client, "save_settings", saved.update), + mock.patch("ai_config.apply_settings", lambda cfg, persist=True: cfg), + ): + backend_client._apply_config_response(response, dict(base)) + return saved + + def test_the_synced_gateway_address_lands_in_the_settings(self) -> None: + saved = self._settings( + { + "version": 3, + "config": {}, + "gateway": {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + } + ) + self.assertEqual( + saved["gateway"], + {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + ) + + def test_an_old_backend_without_a_gateway_block_keeps_the_last_one(self) -> None: + """老版本后端不返回这一段。清空等于把已经能用的客户端打回"没有网关"。""" + existing = {"gateway": {"enabled": True, "url": "https://old.example.com/v1/answer"}} + saved = self._settings({"version": 4, "config": {}}, existing=existing) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + def test_a_blank_url_from_the_backend_is_ignored(self) -> None: + """后台配错回了空地址时,宁可保持原样也不要把客户端弄成没网关。""" + existing = {"gateway": {"enabled": True, "url": "https://good.example.com/v1/answer"}} + saved = self._settings( + {"version": 5, "config": {}, "gateway": {"enabled": True, "url": " "}}, + existing=existing, + ) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + +class EndpointModeTest(unittest.TestCase): + """接口地址:自动补全 vs 原样使用。 + + 加这个开关是因为 auto 那套拼接规则只覆盖得了"服务商标准形状"。自建服务的路径 + 常常不按套路,比如 `https://api.example.com/custom/llm/invoke`——auto 会把它 + 拼成 `.../invoke/chat/completions`,请求发到一个不存在的地址,报 404,而排查的 + 人会去怀疑密钥和网络。 + + 为什么用显式开关而不是"更聪明的猜测":`https://api.example.com/openai` 到底是 + 前缀还是完整端点,光看地址分不出来。猜错的两种方向都会静默地把请求发歪。 + """ + + def test_auto_completes_the_path_for_each_kind(self) -> None: + cases = [ + ("openai", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"), + ("openai", "https://api.deepseek.com", "https://api.deepseek.com/chat/completions"), + ("claude", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"), + ("dify", "https://api.dify.ai/v1", "https://api.dify.ai/v1/chat-messages"), + ] + for kind, base, expected in cases: + with self.subTest(kind=kind, base=base): + self.assertEqual(model_protocol.endpoint_url(kind, base), expected) + + def test_auto_does_not_double_append_a_complete_url(self) -> None: + """已经写全的标准地址不该再被拼一次。""" + full = "https://api.openai.com/v1/chat/completions" + self.assertEqual(model_protocol.endpoint_url("openai", full), full) + + def test_exact_uses_the_address_verbatim(self) -> None: + odd = "https://api.example.com/custom/llm/invoke" + self.assertEqual(model_protocol.endpoint_url("openai", odd), f"{odd}/chat/completions") + self.assertEqual(model_protocol.endpoint_url("openai", odd, "exact"), odd) + + def test_exact_wins_for_every_kind(self) -> None: + """exact 是"别动我的地址",不该被接口类型的规则翻掉。""" + odd = "https://gw.internal/llm" + for kind in ("openai", "claude", "dify", "comfyui"): + with self.subTest(kind=kind): + self.assertEqual(model_protocol.endpoint_url(kind, odd, "exact"), odd) + + def test_a_trailing_slash_is_trimmed_in_both_modes(self) -> None: + self.assertEqual( + model_protocol.endpoint_url("openai", "https://x.com/llm/", "exact"), + "https://x.com/llm", + ) + + def test_the_connectivity_test_uses_the_same_rule_as_the_real_call(self) -> None: + """两边规则若不一致,测试会去戳一个和实际调用不同的地址。 + + 那是最没用的一种测试:测通了照样用不了,或者反过来。 + """ + odd = "https://api.example.com/custom/llm/invoke" + for mode in ("auto", "exact"): + with self.subTest(mode=mode): + self.assertEqual( + admin_backend._model_endpoint(odd, "openai", mode), + model_protocol.endpoint_url("openai", odd, mode), + ) + + def test_dify_upload_root_follows_the_mode(self) -> None: + """Dify 传图要拼 /files/upload,根地址得跟着 exact 走。""" + self.assertEqual( + model_protocol.dify_api_root("https://dify.internal/app/chat", "exact"), + "https://dify.internal/app/chat", + ) + + +class EndpointModeStorageTest(unittest.TestCase): + """存和读。老库要能平滑升级,老记录的行为必须一个字都不变。""" + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + self.uid = self.db.authenticate("admin", "Admin@123456")["id"] + + def _save(self, **extra): + item = { + "id": "p1", "name": "出口", "kind": "openai", + "base_url": "https://api.example.com/custom/llm/invoke", + "api_key": "sk-x", "model": "m", + } + item.update(extra) + return self.db.save_model_provider(item, self.uid, "1.1.1.1") + + def test_the_mode_round_trips(self) -> None: + self._save(endpoint_mode="exact") + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "exact") + self.assertEqual(item["endpoint"], "https://api.example.com/custom/llm/invoke") + + def test_the_default_is_auto(self) -> None: + """不填就是老行为。这个默认值是整件事零回归的前提。""" + self._save() + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "auto") + self.assertTrue(item["endpoint"].endswith("/chat/completions")) + + def test_an_unknown_mode_is_refused(self) -> None: + with self.assertRaises(ValueError) as caught: + self._save(endpoint_mode="随便写") + self.assertIn("地址模式", str(caught.exception)) + + def test_an_old_database_without_the_column_is_upgraded_in_place(self) -> None: + """已经在用的库不能因为加一列就要重建表——重建的每一步都可能丢数据。""" + path = self.root / "old.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_providers ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, + base_url TEXT NOT NULL, api_key_enc TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', capabilities TEXT NOT NULL DEFAULT 'text', + max_tokens INTEGER NOT NULL DEFAULT 500, temperature REAL NOT NULL DEFAULT 0.35, + timeout_ms INTEGER NOT NULL DEFAULT 30000, max_inflight INTEGER NOT NULL DEFAULT 32, + rpm_limit INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, + health TEXT NOT NULL DEFAULT 'unknown', health_checked_at TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by INTEGER); + INSERT INTO model_providers (id,name,kind,base_url,created_at,updated_at) + VALUES ('old-1','老出口','openai','https://api.openai.com/v1','x','x'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + item = next( + x for x in admin_backend.Database(path).model_providers() if x["id"] == "old-1" + ) + self.assertEqual(item["endpoint_mode"], "auto", "老记录必须还是老行为") + self.assertEqual(item["endpoint"], "https://api.openai.com/v1/chat/completions") + + def test_running_the_column_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.assertTrue(self.db.model_providers() is not None) + + +class ModelCallTraceabilityTest(unittest.TestCase): + """`model_calls` 存客户原话、模型原话、审核原因——用来"跟踪问题"。 + + 网关和桌面端各自都会往这张表报一次同一次调用(网关看得到候选和裁判, + 桌面端才知道审核规则命中原因)。两边共用 task_id,谁先落盘谁建行,后到 + 的一方只补自己独有的字段——这里测的就是这道合并逻辑不多写也不少写。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _record(self, **extra) -> dict: + record = { + "device_id": "dev-1", + "task_id": "task-1", + "roles_version": 1, + "judge_mode": "arbitrate", + "chosen": "openai-primary", + "judge": {"winner": "A", "score": 0.8, "risk": "low"}, + "candidates": [{"provider": "openai-primary", "text": "建议您注意休息"}], + "total_ms": 900, + "customer_text": "我这几天总是失眠", + "reply_text": "建议您注意休息", + "review_reason": "", + } + record.update(extra) + return record + + def _row(self, task_id: str) -> sqlite3.Row: + with self.db.connect() as con: + return con.execute( + "SELECT * FROM model_calls WHERE task_id = ?", (task_id,) + ).fetchone() + + def test_a_call_round_trips_customer_and_reply_text(self) -> None: + self.db.log_model_call(self._record()) + row = self._row("task-1") + self.assertEqual(row["customer_text"], "我这几天总是失眠") + self.assertEqual(row["reply_text"], "建议您注意休息") + self.assertEqual(row["review_reason"], "") + + def test_a_second_report_with_the_same_task_id_only_merges_review_reason(self) -> None: + """网关先落盘(候选、裁判都是权威数据),桌面端后到——不能把网关的数据覆盖掉。""" + self.db.log_model_call(self._record()) + self.db.log_model_call(self._record( + chosen="不该生效的值", + candidates=[{"provider": "不该生效的值", "text": "x"}], + total_ms=1, + review_reason="命中审核规则「诊断」", + )) + with self.db.connect() as con: + rows = con.execute( + "SELECT * FROM model_calls WHERE task_id='task-1'" + ).fetchall() + self.assertEqual(len(rows), 1, "同一个 task_id 只能落一行,不是两行") + row = self._row("task-1") + self.assertEqual(row["chosen"], "openai-primary", "第二次上报不该覆盖已有的候选数据") + self.assertEqual(row["review_reason"], "命中审核规则「诊断」", "审核原因只有桌面端知道,必须生效") + + def test_blank_task_ids_never_collide(self) -> None: + """本地非网关路径目前还给不出 task_id,留空时按老行为各插一行。""" + self.db.log_model_call(self._record(task_id="", customer_text="第一条")) + self.db.log_model_call(self._record(task_id="", customer_text="第二条")) + with self.db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id=''" + ).fetchone()["n"] + self.assertEqual(n, 2) + + def test_list_model_calls_finds_a_keyword_in_either_side_of_the_exchange(self) -> None: + self.db.log_model_call(self._record(task_id="t1", customer_text="想咨询糖尿病用药")) + self.db.log_model_call(self._record(task_id="t2", customer_text="今天天气不错", reply_text="是呀")) + found = self.db.list_model_calls(days=7, q="糖尿病") + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "t1") + + def test_list_model_calls_parses_candidates_back_into_a_list(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + found = self.db.list_model_calls(days=7) + self.assertEqual(found["items"][0]["candidates"][0]["provider"], "openai-primary") + + def test_list_model_calls_respects_the_days_window(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + with self.db.connect() as con: + con.execute( + "UPDATE model_calls SET created_at = '2000-01-01 00:00:00' WHERE task_id='t1'" + ) + con.commit() + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 0) + + def test_an_old_database_with_duplicate_task_ids_is_deduped_before_the_index_goes_on(self) -> None: + """老版本拿会话指纹当 task_id,同一个会话十几轮全共享一个值——建唯一索引前必须先清干净。""" + path = self.root / "old_calls.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p1','2026-01-01 00:00:01'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p2','2026-01-01 00:00:02'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p3','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + rows = con.execute( + "SELECT task_id, chosen FROM model_calls ORDER BY id" + ).fetchall() + self.assertEqual([r["task_id"] for r in rows], ["", "", "same-session"], "只留最新一行的 task_id") + self.assertEqual(rows[-1]["chosen"], "p3", "保留的必须是最新那一行") + # 索引确实建起来了:迁移后再报一次同名 task_id 应该走合并而不是新插一行 + db.log_model_call({"task_id": "same-session", "review_reason": "新原因"}) + with db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id='same-session'" + ).fetchone()["n"] + merged = con.execute( + "SELECT chosen, review_reason FROM model_calls WHERE task_id='same-session'" + ).fetchone() + self.assertEqual(n, 1, "唯一索引必须已经生效,不能再插出第二行") + self.assertEqual(merged["chosen"], "p3", "合并不该覆盖已有数据") + self.assertEqual(merged["review_reason"], "新原因") + + def test_running_the_model_calls_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.db.log_model_call(self._record(task_id="tx")) + self.assertEqual(self._row("tx")["chosen"], "openai-primary") + + +class GuardCallsStayOutOfTheCustomerLogTest(unittest.TestCase): + """界面识别调用不能混进"客服对话"的调用日志和调用统计。 + + 这是用户真实报上来的 bug:调用记录点开一看,客户消息和模型回复全是「(空)」, + 候选里全是 `{"state":"unknown","navigation_right_ratio":0,...}`——那是机器人 + 自己在看企业微信窗口,不是在回客户。守卫每轮轮询都要问一次模型,条数远多于 + 真实对话,不分开的话这张表根本没法用来查问题,裁判分布也在给错误的东西打分。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _log(self, task_id: str, purpose: str, **extra) -> None: + record = { + "device_id": "d1", "task_id": task_id, "chosen": "p1", + "judge": {"winner": "A", "score": 0.9, "risk": "low"}, + "candidates": [{"provider": "p1", "text": "文本"}], "total_ms": 100, + "customer_text": "客户说的", "reply_text": "模型答的", + "purpose": purpose, + } + record.update(extra) + self.db.log_model_call(record) + + def test_the_default_log_shows_only_customer_chats(self) -> None: + self._log("c1", "chat") + self._log("g1", "guard") + self._log("g2", "guard") + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "c1") + + def test_guard_calls_can_still_be_listed_on_purpose(self) -> None: + """内部调用照样在花钱,需要的时候得能翻出来看,不能查无此物。""" + self._log("c1", "chat") + self._log("g1", "guard") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + self.assertEqual(self.db.list_model_calls(days=7, purpose="")["total"], 2) + + def test_stats_ignore_guard_calls(self) -> None: + """裁判分布要量的是"发给客户的回复够不够好",不是"布局认得准不准"。""" + self._log("c1", "chat", judge={"winner": "A", "score": 0.9, "risk": "low"}) + for index in range(5): + self._log( + f"g{index}", "guard", + judge={"winner": "B", "score": 0.1, "risk": "high"}, + ) + stats = self.db.model_call_stats(7) + self.assertEqual(stats["total"], 1, "5 条界面识别不该被算成客服调用") + self.assertEqual(stats["judged"], 1) + self.assertAlmostEqual(stats["avg_score"], 0.9, places=3) + self.assertNotIn("high", stats["risk"], "守卫的低分不该污染风险占比") + + def test_a_call_without_an_explicit_purpose_counts_as_a_customer_chat(self) -> None: + """桌面端补报的那条路不带 purpose——默认必须是 chat,不能凭空消失。""" + self.db.log_model_call({ + "task_id": "c9", "chosen": "p1", "judge": {}, "candidates": [], + "customer_text": "在吗", "reply_text": "在的", + }) + self.assertEqual(self.db.list_model_calls(days=7)["total"], 1) + + def test_legacy_guard_rows_are_reclassified_by_their_fingerprint(self) -> None: + """老库里的守卫调用落的是默认 chat,要按候选内容的指纹认出来并改判。 + + 指纹用的是分类器专用的 JSON 字段名(布局守卫的 navigation_right_ratio、 + 会话行分类器的 reply_capable)——正常客服回复里不可能出现这些词。 + """ + path = self.root / "legacy.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (task_id,candidates_json,created_at) VALUES + ('L1','[{"provider":"a","text":"{\\"state\\":\\"unknown\\",\\"navigation_right_ratio\\":0}"}]','2026-01-01 00:00:01'), + ('L2','[{"provider":"a","text":"{\\"kind\\":\\"customer_chat\\",\\"reply_capable\\":true}"}]','2026-01-01 00:00:02'), + ('L3','[{"provider":"a","text":"忙完了,歇会儿吧,最近还好吗"}]','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + marks = { + row["task_id"]: row["purpose"] + for row in con.execute("SELECT task_id, purpose FROM model_calls") + } + self.assertEqual(marks["L1"], "guard", "布局守卫的记录要改判") + self.assertEqual(marks["L2"], "guard", "会话行分类器的记录也要改判") + self.assertEqual(marks["L3"], "chat", "真实客服回复绝不能被误判成内部调用") + + def test_the_reclassification_never_deletes_anything(self) -> None: + self._log("g1", "chat", candidates=[ + {"provider": "a", "text": '{"navigation_right_ratio":0.1}'} + ]) + before = self.db.list_model_calls(days=7, purpose="")["total"] + with self.db.connect() as con: + admin_backend.Database._backfill_guard_purpose(con) + con.commit() + after = self.db.list_model_calls(days=7, purpose="")["total"] + self.assertEqual(before, after, "只改分类标记,一行都不能少") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + + +def setUpModule(): + # 别让测试读到开发机上的真实桌面端配置——配过模型网关的机器会让 + # `ai_chat.current_provider()` 整体改走网关分支,一大片无关测试跟着变行为。 + local_state_redirect.start() + + +def tearDownModule(): + local_state_redirect.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-remote-20260928/server-bundle/tests/test_remote_api.py b/deploy/mobile-remote-20260928/server-bundle/tests/test_remote_api.py new file mode 100644 index 000000000..23acd75d7 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/tests/test_remote_api.py @@ -0,0 +1,716 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-remote-20260928/server-bundle/tests/test_zyt_auth.py b/deploy/mobile-remote-20260928/server-bundle/tests/test_zyt_auth.py new file mode 100644 index 000000000..a5af82759 --- /dev/null +++ b/deploy/mobile-remote-20260928/server-bundle/tests/test_zyt_auth.py @@ -0,0 +1,100 @@ +"""跨服务器桌面认证:只在路由缺失时兼容,拒绝结果不能降级。""" +import io +import json +import unittest +import urllib.error +from unittest import mock + +import zyt_auth + + +class DesktopVerificationRegressionTest(unittest.TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def session(self, **updates): + data = {"user_id": "admin:9", "terminal": 7, "status": "active", + "root": False, "role_ids": [2], "permissions": ["tcm.diagnosis/lists"]} + data.update(updates) + return {"code": 1, "data": data} + + def verify(self): + return zyt_auth.verify_desktop_token("test-token", api_url="https://identity.example.test") + + def missing(self, status=404): + return urllib.error.HTTPError("https://identity.example.test", status, "test", None, None) + + def test_success_never_requests_ip_bound_web_profile(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(self.session(root=True, permissions=["*"])), + self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertTrue(identity["root"]) + self.assertEqual(identity["role_ids"], [2]) + self.assertEqual(identity["permissions"], ["*"]) + self.assertTrue(identity["permissions_known"]) + + def test_missing_permissions_do_not_trigger_profile_request(self): + payload = self.session() + del payload["data"]["permissions"] + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertEqual(identity["permissions"], []) + self.assertFalse(identity["permissions_known"]) + + def test_empty_permissions_are_known(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(self.session(permissions=[]))): + identity = self.verify() + self.assertTrue(identity["permissions_known"]) + self.assertEqual(identity["permissions"], []) + + def test_only_missing_routes_allow_legacy_verification(self): + for status in (404, 405): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(status), self.response({"code": 1, "data": { + "user": {"id": 9, "disable": 0, "role_ids": [2]}, "permissions": []}}), + ]) as opened: + identity = self.verify() + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(opened.call_count, 2) + + def test_http_rejections_do_not_fall_back(self): + for status in (401, 403, 500, 502): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=self.missing(status)) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_invalid_identity_cannot_be_accepted_through_web_fallback(self): + failures = [{"code": -1, "msg": "登录已失效"}, {"code": "bad"}, [], + {"code": 1, "data": []}, self.session(user_id=""), + self.session(terminal=1), self.session(terminal="bad"), + self.session(status="disabled")] + for payload in failures: + with self.subTest(payload=payload), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(payload), self.response({"code": 1, "data": {"user": {"id": 9}}}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_bad_json_and_network_failure_do_not_fall_back(self): + for result in (io.BytesIO(b"error"), urllib.error.URLError("offline")): + with self.subTest(result=result), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[result]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_legacy_ip_error_identifies_missing_server_deployment(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(), self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]): + with self.assertRaisesRegex(zyt_auth.ZytAuthError, "/adminapi/desktop/session"): + self.verify() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-remote-20260928/tests/test_backend.py b/deploy/mobile-remote-20260928/tests/test_backend.py new file mode 100644 index 000000000..6a460176b --- /dev/null +++ b/deploy/mobile-remote-20260928/tests/test_backend.py @@ -0,0 +1,1527 @@ +# -*- coding: utf-8 -*- +"""配置后台与桌面端同步的本地闭环测试。""" + +from __future__ import annotations + +import json +import hashlib +import shutil +import model_protocol +import socket +import sqlite3 +import tempfile +import threading +import unittest +import urllib.error +import urllib.request +from unittest import mock +from pathlib import Path + +from test_support import local_state_redirect +import admin_backend +import app_version +import backend_client +import zyt_auth + + +class BackendIntegrationTest(unittest.TestCase): + + + def test_development_diagnostics_are_cloud_controlled_and_redacted(self) -> None: + config = { + "AI_DEVELOPMENT_MODE": True, + "AI_PROVIDER_TYPE": "openai", + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_MCP_SERVERS": [ + { + "headers": {"Authorization": "Bearer hidden-auth"}, + "env": {"ACCESS_TOKEN": "hidden-token"}, + "args": [ + "--token", + "hidden-argument", + "--password=hidden-inline", + "Bearer hidden-bearer", + ], + } + ], + } + response = {"version": 8, "updated_at": "now"} + diagnostics = backend_client._config_diagnostics( + config, + response, + "https://cloud.example/api/v1/desktop/config?api_key=query-secret", + ) + rendered = "\n".join(diagnostics) + self.assertIn("https://cloud.example/api/v1/desktop/config", rendered) + self.assertIn("云端配置版本: v8", rendered) + self.assertIn('"AI_MODEL": "test-model"', rendered) + for secret in ( + "top-secret-key", + "hidden-auth", + "hidden-token", + "hidden-argument", + "hidden-inline", + "hidden-bearer", + "query-secret", + ): + self.assertNotIn(secret, rendered) + self.assertEqual( + backend_client._config_diagnostics( + {**config, "AI_DEVELOPMENT_MODE": False}, response, "https://cloud.example/config" + ), + [], + ) + + def test_model_request_diagnostics_never_print_the_api_key(self) -> None: + import ai_chat + import ai_config + + previous = { + "AI_DEVELOPMENT_MODE": getattr(ai_config, "AI_DEVELOPMENT_MODE", False), + "AI_API_BASE": ai_config.AI_API_BASE, + "AI_API_KEY": ai_config.AI_API_KEY, + "AI_MODEL": ai_config.AI_MODEL, + } + try: + ai_config.AI_DEVELOPMENT_MODE = True + ai_config.AI_API_BASE = "https://api.example/v1" + ai_config.AI_API_KEY = "never-print-this-key" + ai_config.AI_MODEL = "diagnostic-model" + with mock.patch("builtins.print") as printer: + ai_chat._log_request_diagnostics( + "https://api.example/v1/chat/completions", "OpenAI 兼容" + ) + rendered = "\n".join( + " ".join(str(item) for item in call.args) + for call in printer.call_args_list + ) + self.assertIn("https://api.example/v1/chat/completions", rendered) + self.assertIn("diagnostic-model", rendered) + self.assertNotIn("never-print-this-key", rendered) + finally: + for key, value in previous.items(): + setattr(ai_config, key, value) + + def test_model_connection_uses_unsaved_values_and_saved_key(self) -> None: + current = { + "AI_API_BASE": "https://saved.example/v1", + "AI_API_KEY": "saved-secret", + "AI_MODEL": "saved-model", + "AI_TIMEOUT": 120, + } + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://new.example/v1", + "AI_API_KEY": "", + "AI_MODEL": "new-model", + "AI_TIMEOUT": "180", + }, + current, + ) + self.assertEqual(config["endpoint"], "https://new.example/v1/chat/completions") + self.assertEqual(config["api_key"], "saved-secret") + self.assertEqual(config["model"], "new-model") + self.assertEqual(config["timeout"], 60) + + def test_model_connection_success_does_not_expose_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"choices": [{"message": {"content": "OK"}}]} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(200, response) + ) as call: + result = admin_backend.test_model_connection(config) + self.assertTrue(result["ok"]) + self.assertEqual(result["http_status"], 200) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + self.assertEqual(call.call_args.args[0], config["endpoint"]) + self.assertEqual( + call.call_args.kwargs["headers"]["Authorization"], "Bearer top-secret-key" + ) + payload = call.call_args.kwargs["payload"] + self.assertEqual(payload["model"], "test-model") + + def test_model_connection_error_redacts_key(self) -> None: + config = admin_backend.model_test_config( + { + "AI_API_BASE": "https://api.example/v1", + "AI_API_KEY": "top-secret-key", + "AI_MODEL": "test-model", + "AI_TIMEOUT": 10, + }, + {}, + ) + response = json.dumps( + {"error": {"message": "invalid top-secret-key"}} + ).encode("utf-8") + with mock.patch( + "admin_backend._perform_http_request", return_value=(401, response) + ): + result = admin_backend.test_model_connection(config) + self.assertFalse(result["ok"]) + self.assertEqual(result["http_status"], 401) + self.assertIn("API Key 无效", result["message"]) + self.assertNotIn("top-secret-key", json.dumps(result, ensure_ascii=False)) + + def test_dify_and_comfyui_use_provider_specific_endpoints(self) -> None: + dify = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "dify", + "AI_API_BASE": "https://dify.example/v1", + "AI_API_KEY": "app-secret", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(dify["endpoint"], "https://dify.example/v1/chat-messages") + self.assertEqual(dify["provider_type"], "dify") + + comfyui = admin_backend.model_test_config( + { + "AI_PROVIDER_TYPE": "comfyui", + "AI_API_BASE": "http://127.0.0.1:8188", + "AI_API_KEY": "", + "AI_MODEL": "", + "AI_TIMEOUT": 10, + }, + {}, + ) + self.assertEqual(comfyui["endpoint"], "http://127.0.0.1:8188/system_stats") + with mock.patch( + "admin_backend._perform_http_request", + return_value=(200, b'{"system": {"os": "windows"}, "devices": []}'), + ) as call: + result = admin_backend.test_model_connection(comfyui) + self.assertTrue(result["ok"]) + self.assertEqual(call.call_args.kwargs["method"], "GET") + self.assertIsNone(call.call_args.kwargs["payload"]) + + def test_desktop_ai_respects_explicit_dify_provider(self) -> None: + import ai_chat + import ai_config + + old_provider = ai_config.AI_PROVIDER_TYPE + old_base = ai_config.AI_API_BASE + try: + ai_config.AI_PROVIDER_TYPE = "dify" + ai_config.AI_API_BASE = "https://dify.example/v1" + self.assertTrue(ai_chat._is_dify_endpoint()) + self.assertEqual( + ai_chat._completions_url(), + "https://dify.example/v1/chat-messages", + ) + ai_config.AI_PROVIDER_TYPE = "openai" + self.assertFalse(ai_chat._is_dify_endpoint()) + finally: + ai_config.AI_PROVIDER_TYPE = old_provider + ai_config.AI_API_BASE = old_base + + + def test_release_status_detects_optional_and_forced_updates(self) -> None: + optional = app_version.release_status( + {"latest_version": "99.0.0", "force_upgrade": False} + ) + self.assertTrue(optional["update_available"]) + self.assertFalse(optional["force_upgrade"]) + + forced = app_version.release_status( + {"latest_version": "2.0.0", "force_upgrade": True} + ) + self.assertTrue(forced["force_upgrade"]) + + current = app_version.release_status( + {"latest_version": app_version.APP_VERSION, "force_upgrade": True} + ) + self.assertFalse(current["update_available"]) + self.assertFalse(current["force_upgrade"]) + + downgrade = app_version.release_status( + {"latest_version": "1.0.1", "force_upgrade": True} + ) + self.assertFalse(downgrade["update_available"]) + self.assertFalse(downgrade["force_upgrade"]) + + def test_release_form_validation(self) -> None: + release = admin_backend.validate_release_form( + { + "latest_version": "v1.2.3", + "download_url": "https://example.com/client.exe", + "release_notes": "修复已知问题", + "force_upgrade": "1", + } + ) + self.assertEqual(release["latest_version"], "1.2.3") + self.assertTrue(release["force_upgrade"]) + automatic = admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "package_sha256": "a" * 64, + "package_size": "1024", + "auto_install": "1", + "release_channel": "stable", + "rollout_percent": "25", + "status": "published", + } + ) + self.assertTrue(automatic["auto_install"]) + self.assertEqual(automatic["rollout_percent"], 25) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2", "force_upgrade": "1"} + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + { + "latest_version": "1.2.4", + "download_url": "https://example.com/client.exe", + "auto_install": "1", + } + ) + with self.assertRaises(ValueError): + admin_backend.validate_release_form( + {"latest_version": "1.2.3", "force_upgrade": "1"} + ) + + def test_pbkdf2_fallback_matches_standard_library(self) -> None: + password = b"FallbackPassword123" + salt = b"0123456789abcdef" + expected = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000) + native_pbkdf2 = admin_backend.hashlib.pbkdf2_hmac + try: + admin_backend.hashlib.pbkdf2_hmac = None + actual = admin_backend.pbkdf2_sha256(password, salt, 1_000) + finally: + admin_backend.hashlib.pbkdf2_hmac = native_pbkdf2 + self.assertEqual(actual, expected) + + def test_stale_runtime_file_is_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + runtime_file = root / "backend_runtime.json" + runtime_file.write_text( + json.dumps( + { + "pid": 0, + "host": "127.0.0.1", + "port": 8765, + "server_url": "http://127.0.0.1:8765", + "local_sync_token": "stale-token", + } + ), + encoding="utf-8", + ) + old_runtime_file = backend_client.RUNTIME_FILE + try: + backend_client.RUNTIME_FILE = runtime_file + self.assertEqual(backend_client.discover_local_runtime(), {}) + self.assertEqual(backend_client.discover_local_sync_token(), "") + finally: + backend_client.RUNTIME_FILE = old_runtime_file + + def test_local_agent_name_overrides_cloud_and_blank_follows_cloud(self) -> None: + import ai_config + + old_agent = ai_config.AI_AGENT_NAME + old_cloud = ai_config.AI_CLOUD_AGENT_NAME + old_local = ai_config.AI_LOCAL_AGENT_NAME + old_prompt = ai_config.AI_SYSTEM_PROMPT + try: + ai_config.AI_CLOUD_AGENT_NAME = "旧云端客服" + ai_config.AI_LOCAL_AGENT_NAME = "" + ai_config.AI_AGENT_NAME = "旧云端客服" + ai_config.set_local_agent_name_override("本机客服", persist=False) + ai_config.apply_settings({"AI_AGENT_NAME": "新云端客服"}, persist=False) + self.assertEqual(ai_config.AI_CLOUD_AGENT_NAME, "新云端客服") + self.assertEqual(ai_config.AI_AGENT_NAME, "本机客服") + ai_config.set_local_agent_name_override("", persist=False) + self.assertEqual(ai_config.AI_AGENT_NAME, "新云端客服") + finally: + ai_config.AI_AGENT_NAME = old_agent + ai_config.AI_CLOUD_AGENT_NAME = old_cloud + ai_config.AI_LOCAL_AGENT_NAME = old_local + ai_config.AI_SYSTEM_PROMPT = old_prompt + + +class DesktopConfigFallbackTest(unittest.TestCase): + """桌面端拉配置:新接口优先,老接口兜底。 + + 客户端和服务端是分批更新的,两边版本对不齐是常态而不是意外。直接把路径改 + 成 v2,那些服务端还没升级的部署会当场同步失败——而且失败得很安静,界面上 + 只是配置一直显示旧的,没人会立刻发现。 + """ + + def _fail(self, message: str): + return backend_client.BackendError(message) + + def test_startup_auth_config_is_read_from_management_and_cached(self) -> None: + calls = [] + saved = {} + + def fake(method, server, path, **kwargs): + calls.append((method, server, path)) + return 200, { + "zyt_api_url": "https://account.example.com/zyt/", + "terminal": 7, + "configured": True, + } + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", lambda: {}), + mock.patch.object(backend_client, "save_settings", saved.update), + ): + config = backend_client.fetch_desktop_auth_config("https://admin.example.com/") + + self.assertEqual( + calls, + [("GET", "https://admin.example.com", "/api/v2/desktop/auth/config")], + ) + self.assertEqual(config["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["zyt_api_url"], "https://account.example.com/zyt") + self.assertEqual(saved["server_url"], "https://admin.example.com") + + def test_startup_auth_config_rejects_an_incomplete_response(self) -> None: + with mock.patch.object( + backend_client, "_request", return_value=(200, {"terminal": 7}) + ): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client.fetch_desktop_auth_config("https://admin.example.com") + self.assertIn("登录 API", str(caught.exception)) + + def test_zyt_admin_login_uses_admin_endpoint_and_desktop_terminal(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append((method, server, path, kwargs.get("payload"))) + return 200, { + "code": 1, + "msg": "登录成功", + "data": {"token": "admin-desktop-token", "name": "管理员"}, + } + + with mock.patch.object(backend_client, "_request", fake): + token, user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(token, "admin-desktop-token") + self.assertEqual(user["name"], "管理员") + self.assertEqual(calls[0][2], "/adminapi/login/account") + self.assertEqual(calls[0][3]["terminal"], 7) + self.assertEqual(calls[0][3]["account"], "admin") + + def test_zyt_admin_login_falls_back_while_server_has_no_terminal_7(self) -> None: + terminals = [] + + def fake(method, server, path, **kwargs): + terminal = kwargs["payload"]["terminal"] + terminals.append(terminal) + if terminal == 7: + return 200, {"code": 0, "msg": "terminal必须在 1,2 范围内"} + return 200, {"code": 1, "data": {"token": "legacy-admin-token"}} + + with mock.patch.object(backend_client, "_request", fake): + token, _user = backend_client._zyt_login( + "https://admin.example.com", "admin", "secret" + ) + + self.assertEqual(terminals, [7, 1]) + self.assertEqual(token, "legacy-admin-token") + + def test_desktop_login_can_remember_password_with_dpapi_protection(self) -> None: + settings = backend_client.default_settings() + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, + "_protect_secret", + side_effect=lambda value: f"protected:{value}", + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=True, + ) + + self.assertTrue(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["zyt_account"], "admin") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:secret" + ) + self.assertNotIn("secret", json.dumps(saved[-1], ensure_ascii=False).replace( + "protected:secret", "" + )) + + def test_desktop_login_unchecked_forgets_the_saved_password(self) -> None: + settings = backend_client.default_settings() + settings["remembered_password_protected"] = "protected:old-password" + saved = [] + with ( + mock.patch.object( + backend_client, + "fetch_desktop_auth_config", + return_value={"zyt_api_url": "https://account.example.com"}, + ), + mock.patch.object( + backend_client, + "_zyt_login", + return_value=("zyt-token", {"id": 9}), + ), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "desktop-token", + "expires_in": 3600, + "account": {"id": 1, "sn": "admin"}, + }, + ), + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected:token" + ), + ): + backend_client.desktop_login( + "https://admin.example.com", + "admin", + "secret", + remember_password=False, + ) + + self.assertFalse(saved[-1]["remember_login"]) + self.assertEqual(saved[-1]["remembered_password_protected"], "") + + def test_forced_logout_keeps_explicitly_remembered_password(self) -> None: + settings = backend_client.default_settings() + settings.update( + { + "zyt_account": "admin", + "remember_login": True, + "remembered_password_protected": "protected:password", + "zyt_token_protected": "protected:zyt-token", + "desktop_token_protected": "protected:desktop-token", + } + ) + saved = [] + with ( + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + ): + backend_client._clear_desktop_credentials(settings) + + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual( + saved[-1]["remembered_password_protected"], "protected:password" + ) + + def test_session_heartbeat_never_silently_reauthenticates(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + } + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object(backend_client, "save_settings"), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object( + backend_client, + "_request", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + with self.assertRaises(backend_client.AuthenticationError): + backend_client.validate_desktop_session() + exchange.assert_not_called() + + def test_session_heartbeat_persists_the_renewed_expiration(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "desktop_expires_at": 1200, + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, + "save_settings", + side_effect=lambda value: saved.append(dict(value)), + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_request", + return_value=( + 200, + { + "id": 1, + "sn": "admin", + "expires_at": 50_000, + }, + ), + ), + ): + account = backend_client.validate_desktop_session() + + self.assertEqual(account["id"], 1) + self.assertEqual(saved[-1]["desktop_expires_at"], 50_000) + + def test_revoked_unexpired_session_requires_password_again(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-desktop", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 2000, + "zyt_account": "admin", + } + saved = [] + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object( + backend_client, "_unprotect_secret", return_value="desktop-token" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "validate_desktop_session", + side_effect=backend_client.AuthenticationError( + "桌面登录已失效", status=401 + ), + ), + mock.patch.object(backend_client, "_exchange_desktop_token") as exchange, + ): + self.assertIsNone(backend_client.restore_desktop_session()) + + exchange.assert_not_called() + self.assertEqual(saved[-1]["desktop_token_protected"], "") + self.assertEqual(saved[-1]["zyt_token_protected"], "") + self.assertEqual(saved[-1]["zyt_account"], "admin") + + def test_naturally_expired_session_can_refresh_from_zyt(self) -> None: + settings = { + "server_url": "https://admin.example.com", + "desktop_token_protected": "protected-old", + "zyt_token_protected": "protected-zyt", + "desktop_expires_at": 999, + } + saved = [] + + def unprotect(value): + return { + "protected-old": "old-token", + "protected-zyt": "zyt-token", + }.get(value, "") + + with ( + mock.patch.object(backend_client, "load_settings", return_value=settings), + mock.patch.object( + backend_client, "save_settings", side_effect=lambda value: saved.append(dict(value)) + ), + mock.patch.object(backend_client, "_unprotect_secret", side_effect=unprotect), + mock.patch.object( + backend_client, "_protect_secret", return_value="protected-new" + ), + mock.patch.object(backend_client.time, "time", return_value=1000), + mock.patch.object( + backend_client, + "_exchange_desktop_token", + return_value={ + "access_token": "new-token", + "expires_in": 1800, + "account": {"id": 1, "sn": "admin"}, + }, + ), + ): + account = backend_client.restore_desktop_session() + + self.assertEqual(account, {"id": 1, "sn": "admin"}) + self.assertEqual(saved[-1]["desktop_token_protected"], "protected-new") + self.assertEqual(saved[-1]["desktop_expires_at"], 2800) + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"config": {}, "version": 3} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + self.assertEqual(used, "/api/v2/desktop/config") + self.assertEqual(response["version"], 3) + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """服务端还是老版本时,必须照常同步,不能报错。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("HTTP 404", status=404) + return 200, {"config": {}, "version": 7} + + with mock.patch.object(backend_client, "_request", fake): + response, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config", "/api/v1/desktop/config"]) + self.assertEqual(used, "/api/v1/desktop/config") + self.assertEqual(response["version"], 7) + + def test_the_fallback_is_decided_by_status_not_by_wording(self) -> None: + """老后台对未知 GET 路径回的是"页面不存在",未知 POST 才回"接口不存在"。 + + 一个字之差。曾经这里靠文案匹配,结果对着真的老后台跑就整个失效了—— + 判据必须是状态码。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("页面不存在", status=404) + return 200, {"config": {}} + + with mock.patch.object(backend_client, "_request", fake): + _, used = backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(used, "/api/v1/desktop/config") + + def test_a_connection_failure_is_not_retried_on_the_old_path(self) -> None: + """连不上就是连不上,换个路径同样连不上。 + + 真正的危害不是多发一个请求,而是把"服务器宕了"这个明确故障,伪装成 + "接口不存在"往下走——最后一层抛出来的是老接口的错误,人会去查一个根本 + 没坏的地方。 + """ + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + # 连不上根本没拿到响应,所以没有状态码 + raise backend_client.BackendError("无法连接后台:timed out") + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError) as caught: + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + self.assertEqual(calls, ["/api/v2/desktop/config"], "不该退回去重试") + self.assertIn("无法连接", str(caught.exception)) + + def test_a_bad_sync_key_surfaces_instead_of_falling_back(self) -> None: + """401/403 说明服务在、只是凭证不对。这是要人去改配置的错误,不能吞。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.AuthenticationError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + + self.assertEqual(calls, ["/api/v2/desktop/config"]) + + def test_a_404_on_both_paths_reports_the_last_error(self) -> None: + def fake(method, server, path, **kwargs): + raise backend_client.BackendError("HTTP 404", status=404) + + with mock.patch.object(backend_client, "_request", fake): + with self.assertRaises(backend_client.BackendError): + backend_client._fetch_desktop_config("http://x", "k", 5.0) + + def test_the_legacy_constant_still_points_at_a_real_path(self) -> None: + """DESKTOP_CONFIG_PATH 可能被别处引用,不能变成空字符串。""" + self.assertIn(backend_client.DESKTOP_CONFIG_PATH, backend_client.DESKTOP_CONFIG_PATHS) + self.assertTrue(backend_client.DESKTOP_CONFIG_PATH.startswith("/api/")) + + +class ZytAdminTokenVerificationTest(unittest.TestCase): + def test_backend_verifies_the_admin_desktop_session_endpoint(self) -> None: + session_response = mock.MagicMock() + session_response.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user_id": "admin:1", + "sn": "admin", + "nickname": "管理员", + "mobile": "", + "avatar": "", + "terminal": 7, + "status": "active", + "root": False, + "role_ids": [2], + "permissions": ["tcm.diagnosis/lists"], + }, + } + ).encode("utf-8") + with mock.patch.object( + zyt_auth.urllib.request, + "urlopen", + return_value=session_response, + ) as opened: + identity = zyt_auth.verify_desktop_token( + "admin-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 1) + request = opened.call_args_list[0].args[0] + self.assertEqual( + request.full_url, + "https://admin.example.com/adminapi/desktop/session", + ) + self.assertEqual(request.get_header("Token"), "admin-token") + self.assertEqual(identity["user_id"], "admin:1") + self.assertTrue(identity["permissions_known"]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + def test_old_server_falls_back_to_current_admin_profile(self) -> None: + profile = mock.MagicMock() + profile.__enter__.return_value.read.return_value = json.dumps( + { + "code": 1, + "data": { + "user": { + "id": 9, + "account": "doctor9", + "name": "九号医助", + "phone": "13800138000", + "avatar": "https://img.example/a.png", + "disable": 0, + "root": 0, + "role_ids": [2], + } + ,"permissions": ["tcm.diagnosis/lists"] + }, + } + ).encode("utf-8") + missing = urllib.error.HTTPError( + "https://admin.example.com/adminapi/desktop/session", + 404, + "Not Found", + None, + None, + ) + + with mock.patch.object( + zyt_auth.urllib.request, "urlopen", side_effect=[missing, profile] + ) as opened: + identity = zyt_auth.verify_desktop_token( + "legacy-token", api_url="https://admin.example.com" + ) + + self.assertEqual(opened.call_count, 2) + fallback_request = opened.call_args_list[1].args[0] + self.assertEqual( + fallback_request.full_url, + "https://admin.example.com/adminapi/auth.admin/mySelf", + ) + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(identity["nickname"], "九号医助") + self.assertEqual(identity["terminal"], 7) + self.assertEqual(identity["role_ids"], [2]) + self.assertIn("tcm.diagnosis/lists", identity["permissions"]) + + +class ModelCallReportFallbackTest(unittest.TestCase): + """调用留痕上报:和拉配置同一套 v2 → v1 顺序。 + + 补这组测试是因为老后台(8765)退役前卡在这一条上:配置同步早有 v2, + 上报却只有 v1。桌面端只要改指到 8766,配置照常同步、回复照常发,唯独 + 调用记录一条都不进库——静悄悄地丢掉的正是出事后用来解释"这句话怎么来的" + 那份证据。 + """ + + def _settings(self): + return {"server_url": "http://x"} + + def test_the_new_endpoint_is_tried_first(self) -> None: + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls"], "新接口在就不该再打老的") + + def test_a_404_falls_back_to_the_legacy_endpoint(self) -> None: + """还指着老后台(8765)的客户端必须照常上报,不能悄悄丢数据。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + if path.startswith("/api/v2"): + raise backend_client.BackendError("接口不存在", status=404) + return 200, {"ok": True} + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertTrue(ok) + self.assertEqual(calls, ["/api/v2/model/calls", "/api/v1/model/calls"]) + + def test_a_401_does_not_fall_back(self) -> None: + """凭证不对换个路径重试同样不对,只会多打一个请求。""" + calls = [] + + def fake(method, server, path, **kwargs): + calls.append(path) + raise backend_client.AuthenticationError("同步凭证无效", status=401) + + with ( + mock.patch.object(backend_client, "_request", fake), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + ok = backend_client.report_model_call({"task_id": "t1"}) + + self.assertFalse(ok) + self.assertEqual(calls, ["/api/v2/model/calls"]) + + def test_a_failure_never_raises_into_the_reply_path(self) -> None: + """上报是观测。它出问题绝不能把已经生成好的回复流程带下水。""" + def boom(*_args, **_kwargs): + raise RuntimeError("断网") + + with ( + mock.patch.object(backend_client, "_request", boom), + mock.patch.object(backend_client, "load_settings", self._settings), + mock.patch.object(backend_client, "desktop_access_token", lambda: "desk-token"), + ): + self.assertFalse(backend_client.report_model_call({"task_id": "t1"})) + + +class GatewayUrlDerivationTest(unittest.TestCase): + """网关地址由后台算出来告诉客户端。 + + 这是"只改一个域名"的关键。桌面端配置里只有后台地址一项,网关在哪、模型怎么 + 编排全部由后台决定——运维换网关位置只动后台一处,不用挨个改客户端。 + """ + + def test_direct_port_access_points_at_the_local_gateway_port(self) -> None: + """本机开发:三个服务各占一个端口,网关就在同机的 8770。""" + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "127.0.0.1:8765"), + "http://127.0.0.1:8770/v1/answer", + ) + self.assertEqual( + admin_backend.derive_gateway_url("", "http", "192.168.1.20:8766"), + "http://192.168.1.20:8770/v1/answer", + ) + + def test_a_proxied_domain_gets_a_same_origin_path(self) -> None: + """反代后面只有 80/443 对外,公网根本连不到 8770。 + + 这两种推法必须分开。用一套规则套两种部署,总有一边是错的——而且错得很 + 安静:客户端拿到一个连不上的地址,表现成"模型不回复"。 + """ + self.assertEqual( + admin_backend.derive_gateway_url("", "https", "xchat.example.com"), + "https://xchat.example.com/gateway/v1/answer", + ) + + def test_an_absolute_override_is_used_as_is(self) -> None: + """网关在别的域名或别的机器上时用这个。""" + self.assertEqual( + admin_backend.derive_gateway_url( + "https://gw.example.com/v1/answer", "https", "xchat.example.com" + ), + "https://gw.example.com/v1/answer", + ) + + def test_a_path_override_hangs_off_the_backend_domain(self) -> None: + self.assertEqual( + admin_backend.derive_gateway_url( + "/model-gw/v1/answer", "https", "xchat.example.com" + ), + "https://xchat.example.com/model-gw/v1/answer", + ) + + def test_a_missing_host_still_yields_something_usable(self) -> None: + """拿不到 Host 时给本机地址,而不是空串。 + + 空串会让客户端把 gateway 当成"没配",静默回落到本机单模型——而本机 + 没有密钥,最后报的是一个看不懂的 401。 + """ + self.assertTrue( + admin_backend.derive_gateway_url("", "http", "").startswith("http://") + ) + + def test_an_override_without_a_scheme_is_refused_on_save(self) -> None: + """漏了 http:// 会被拼成 https://你的域名/gw.example.com/v1/answer。 + + 那是个没意义的地址,而且失败要等到桌面端下次发模型请求才暴露。 + """ + base = { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + } + with self.assertRaises(ValueError) as caught: + admin_backend.validate_config_form( + {**base, "AI_GATEWAY_URL": "gw.example.com/v1/answer"}, {} + ) + self.assertIn("http", str(caught.exception)) + + def test_an_empty_override_is_accepted_and_means_auto(self) -> None: + config = admin_backend.validate_config_form( + { + "AI_AGENT_NAME": "甲", + "AI_HOSPITAL_NAME": "乙", + "AI_CONTEXT_MAX_ROUNDS": "5", + "AI_MCP_MAX_ROUNDS": "5", + "AI_MCP_SERVERS": "[]", + "AI_GATEWAY_URL": "", + }, + {}, + ) + self.assertEqual(config["AI_GATEWAY_URL"], "") + + +class GatewayArrivesBySyncTest(unittest.TestCase): + """桌面端不用手工配网关——同步时后台会把地址带过来。""" + + def _settings(self, response, existing=None): + """跑一遍 _apply_config_response,返回它写下的设置。""" + saved = {} + base = backend_client.default_settings() + if existing: + base.update(existing) + + with ( + mock.patch.object(backend_client, "load_settings", lambda: dict(base)), + mock.patch.object(backend_client, "save_settings", saved.update), + mock.patch("ai_config.apply_settings", lambda cfg, persist=True: cfg), + ): + backend_client._apply_config_response(response, dict(base)) + return saved + + def test_the_synced_gateway_address_lands_in_the_settings(self) -> None: + saved = self._settings( + { + "version": 3, + "config": {}, + "gateway": {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + } + ) + self.assertEqual( + saved["gateway"], + {"enabled": True, "url": "https://x.example.com/gateway/v1/answer"}, + ) + + def test_an_old_backend_without_a_gateway_block_keeps_the_last_one(self) -> None: + """老版本后端不返回这一段。清空等于把已经能用的客户端打回"没有网关"。""" + existing = {"gateway": {"enabled": True, "url": "https://old.example.com/v1/answer"}} + saved = self._settings({"version": 4, "config": {}}, existing=existing) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + def test_a_blank_url_from_the_backend_is_ignored(self) -> None: + """后台配错回了空地址时,宁可保持原样也不要把客户端弄成没网关。""" + existing = {"gateway": {"enabled": True, "url": "https://good.example.com/v1/answer"}} + saved = self._settings( + {"version": 5, "config": {}, "gateway": {"enabled": True, "url": " "}}, + existing=existing, + ) + self.assertEqual(saved.get("gateway", existing["gateway"]), existing["gateway"]) + + +class EndpointModeTest(unittest.TestCase): + """接口地址:自动补全 vs 原样使用。 + + 加这个开关是因为 auto 那套拼接规则只覆盖得了"服务商标准形状"。自建服务的路径 + 常常不按套路,比如 `https://api.example.com/custom/llm/invoke`——auto 会把它 + 拼成 `.../invoke/chat/completions`,请求发到一个不存在的地址,报 404,而排查的 + 人会去怀疑密钥和网络。 + + 为什么用显式开关而不是"更聪明的猜测":`https://api.example.com/openai` 到底是 + 前缀还是完整端点,光看地址分不出来。猜错的两种方向都会静默地把请求发歪。 + """ + + def test_auto_completes_the_path_for_each_kind(self) -> None: + cases = [ + ("openai", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"), + ("openai", "https://api.deepseek.com", "https://api.deepseek.com/chat/completions"), + ("claude", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"), + ("dify", "https://api.dify.ai/v1", "https://api.dify.ai/v1/chat-messages"), + ] + for kind, base, expected in cases: + with self.subTest(kind=kind, base=base): + self.assertEqual(model_protocol.endpoint_url(kind, base), expected) + + def test_auto_does_not_double_append_a_complete_url(self) -> None: + """已经写全的标准地址不该再被拼一次。""" + full = "https://api.openai.com/v1/chat/completions" + self.assertEqual(model_protocol.endpoint_url("openai", full), full) + + def test_exact_uses_the_address_verbatim(self) -> None: + odd = "https://api.example.com/custom/llm/invoke" + self.assertEqual(model_protocol.endpoint_url("openai", odd), f"{odd}/chat/completions") + self.assertEqual(model_protocol.endpoint_url("openai", odd, "exact"), odd) + + def test_exact_wins_for_every_kind(self) -> None: + """exact 是"别动我的地址",不该被接口类型的规则翻掉。""" + odd = "https://gw.internal/llm" + for kind in ("openai", "claude", "dify", "comfyui"): + with self.subTest(kind=kind): + self.assertEqual(model_protocol.endpoint_url(kind, odd, "exact"), odd) + + def test_a_trailing_slash_is_trimmed_in_both_modes(self) -> None: + self.assertEqual( + model_protocol.endpoint_url("openai", "https://x.com/llm/", "exact"), + "https://x.com/llm", + ) + + def test_the_connectivity_test_uses_the_same_rule_as_the_real_call(self) -> None: + """两边规则若不一致,测试会去戳一个和实际调用不同的地址。 + + 那是最没用的一种测试:测通了照样用不了,或者反过来。 + """ + odd = "https://api.example.com/custom/llm/invoke" + for mode in ("auto", "exact"): + with self.subTest(mode=mode): + self.assertEqual( + admin_backend._model_endpoint(odd, "openai", mode), + model_protocol.endpoint_url("openai", odd, mode), + ) + + def test_dify_upload_root_follows_the_mode(self) -> None: + """Dify 传图要拼 /files/upload,根地址得跟着 exact 走。""" + self.assertEqual( + model_protocol.dify_api_root("https://dify.internal/app/chat", "exact"), + "https://dify.internal/app/chat", + ) + + +class EndpointModeStorageTest(unittest.TestCase): + """存和读。老库要能平滑升级,老记录的行为必须一个字都不变。""" + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + self.uid = self.db.authenticate("admin", "Admin@123456")["id"] + + def _save(self, **extra): + item = { + "id": "p1", "name": "出口", "kind": "openai", + "base_url": "https://api.example.com/custom/llm/invoke", + "api_key": "sk-x", "model": "m", + } + item.update(extra) + return self.db.save_model_provider(item, self.uid, "1.1.1.1") + + def test_the_mode_round_trips(self) -> None: + self._save(endpoint_mode="exact") + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "exact") + self.assertEqual(item["endpoint"], "https://api.example.com/custom/llm/invoke") + + def test_the_default_is_auto(self) -> None: + """不填就是老行为。这个默认值是整件事零回归的前提。""" + self._save() + item = next(x for x in self.db.model_providers() if x["id"] == "p1") + self.assertEqual(item["endpoint_mode"], "auto") + self.assertTrue(item["endpoint"].endswith("/chat/completions")) + + def test_an_unknown_mode_is_refused(self) -> None: + with self.assertRaises(ValueError) as caught: + self._save(endpoint_mode="随便写") + self.assertIn("地址模式", str(caught.exception)) + + def test_an_old_database_without_the_column_is_upgraded_in_place(self) -> None: + """已经在用的库不能因为加一列就要重建表——重建的每一步都可能丢数据。""" + path = self.root / "old.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_providers ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, + base_url TEXT NOT NULL, api_key_enc TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', capabilities TEXT NOT NULL DEFAULT 'text', + max_tokens INTEGER NOT NULL DEFAULT 500, temperature REAL NOT NULL DEFAULT 0.35, + timeout_ms INTEGER NOT NULL DEFAULT 30000, max_inflight INTEGER NOT NULL DEFAULT 32, + rpm_limit INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, + health TEXT NOT NULL DEFAULT 'unknown', health_checked_at TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by INTEGER); + INSERT INTO model_providers (id,name,kind,base_url,created_at,updated_at) + VALUES ('old-1','老出口','openai','https://api.openai.com/v1','x','x'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + item = next( + x for x in admin_backend.Database(path).model_providers() if x["id"] == "old-1" + ) + self.assertEqual(item["endpoint_mode"], "auto", "老记录必须还是老行为") + self.assertEqual(item["endpoint"], "https://api.openai.com/v1/chat/completions") + + def test_running_the_column_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.assertTrue(self.db.model_providers() is not None) + + +class ModelCallTraceabilityTest(unittest.TestCase): + """`model_calls` 存客户原话、模型原话、审核原因——用来"跟踪问题"。 + + 网关和桌面端各自都会往这张表报一次同一次调用(网关看得到候选和裁判, + 桌面端才知道审核规则命中原因)。两边共用 task_id,谁先落盘谁建行,后到 + 的一方只补自己独有的字段——这里测的就是这道合并逻辑不多写也不少写。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _record(self, **extra) -> dict: + record = { + "device_id": "dev-1", + "task_id": "task-1", + "roles_version": 1, + "judge_mode": "arbitrate", + "chosen": "openai-primary", + "judge": {"winner": "A", "score": 0.8, "risk": "low"}, + "candidates": [{"provider": "openai-primary", "text": "建议您注意休息"}], + "total_ms": 900, + "customer_text": "我这几天总是失眠", + "reply_text": "建议您注意休息", + "review_reason": "", + } + record.update(extra) + return record + + def _row(self, task_id: str) -> sqlite3.Row: + with self.db.connect() as con: + return con.execute( + "SELECT * FROM model_calls WHERE task_id = ?", (task_id,) + ).fetchone() + + def test_a_call_round_trips_customer_and_reply_text(self) -> None: + self.db.log_model_call(self._record()) + row = self._row("task-1") + self.assertEqual(row["customer_text"], "我这几天总是失眠") + self.assertEqual(row["reply_text"], "建议您注意休息") + self.assertEqual(row["review_reason"], "") + + def test_a_second_report_with_the_same_task_id_only_merges_review_reason(self) -> None: + """网关先落盘(候选、裁判都是权威数据),桌面端后到——不能把网关的数据覆盖掉。""" + self.db.log_model_call(self._record()) + self.db.log_model_call(self._record( + chosen="不该生效的值", + candidates=[{"provider": "不该生效的值", "text": "x"}], + total_ms=1, + review_reason="命中审核规则「诊断」", + )) + with self.db.connect() as con: + rows = con.execute( + "SELECT * FROM model_calls WHERE task_id='task-1'" + ).fetchall() + self.assertEqual(len(rows), 1, "同一个 task_id 只能落一行,不是两行") + row = self._row("task-1") + self.assertEqual(row["chosen"], "openai-primary", "第二次上报不该覆盖已有的候选数据") + self.assertEqual(row["review_reason"], "命中审核规则「诊断」", "审核原因只有桌面端知道,必须生效") + + def test_blank_task_ids_never_collide(self) -> None: + """本地非网关路径目前还给不出 task_id,留空时按老行为各插一行。""" + self.db.log_model_call(self._record(task_id="", customer_text="第一条")) + self.db.log_model_call(self._record(task_id="", customer_text="第二条")) + with self.db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id=''" + ).fetchone()["n"] + self.assertEqual(n, 2) + + def test_list_model_calls_finds_a_keyword_in_either_side_of_the_exchange(self) -> None: + self.db.log_model_call(self._record(task_id="t1", customer_text="想咨询糖尿病用药")) + self.db.log_model_call(self._record(task_id="t2", customer_text="今天天气不错", reply_text="是呀")) + found = self.db.list_model_calls(days=7, q="糖尿病") + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "t1") + + def test_list_model_calls_parses_candidates_back_into_a_list(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + found = self.db.list_model_calls(days=7) + self.assertEqual(found["items"][0]["candidates"][0]["provider"], "openai-primary") + + def test_list_model_calls_respects_the_days_window(self) -> None: + self.db.log_model_call(self._record(task_id="t1")) + with self.db.connect() as con: + con.execute( + "UPDATE model_calls SET created_at = '2000-01-01 00:00:00' WHERE task_id='t1'" + ) + con.commit() + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 0) + + def test_an_old_database_with_duplicate_task_ids_is_deduped_before_the_index_goes_on(self) -> None: + """老版本拿会话指纹当 task_id,同一个会话十几轮全共享一个值——建唯一索引前必须先清干净。""" + path = self.root / "old_calls.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p1','2026-01-01 00:00:01'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p2','2026-01-01 00:00:02'); + INSERT INTO model_calls (device_id,task_id,chosen,created_at) + VALUES ('d1','same-session','p3','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + rows = con.execute( + "SELECT task_id, chosen FROM model_calls ORDER BY id" + ).fetchall() + self.assertEqual([r["task_id"] for r in rows], ["", "", "same-session"], "只留最新一行的 task_id") + self.assertEqual(rows[-1]["chosen"], "p3", "保留的必须是最新那一行") + # 索引确实建起来了:迁移后再报一次同名 task_id 应该走合并而不是新插一行 + db.log_model_call({"task_id": "same-session", "review_reason": "新原因"}) + with db.connect() as con: + n = con.execute( + "SELECT COUNT(*) AS n FROM model_calls WHERE task_id='same-session'" + ).fetchone()["n"] + merged = con.execute( + "SELECT chosen, review_reason FROM model_calls WHERE task_id='same-session'" + ).fetchone() + self.assertEqual(n, 1, "唯一索引必须已经生效,不能再插出第二行") + self.assertEqual(merged["chosen"], "p3", "合并不该覆盖已有数据") + self.assertEqual(merged["review_reason"], "新原因") + + def test_running_the_model_calls_migration_twice_is_harmless(self) -> None: + with mock.patch("builtins.print"): + admin_backend.Database(self.root / "t.db").initialize("Admin@123456") + self.db.log_model_call(self._record(task_id="tx")) + self.assertEqual(self._row("tx")["chosen"], "openai-primary") + + +class GuardCallsStayOutOfTheCustomerLogTest(unittest.TestCase): + """界面识别调用不能混进"客服对话"的调用日志和调用统计。 + + 这是用户真实报上来的 bug:调用记录点开一看,客户消息和模型回复全是「(空)」, + 候选里全是 `{"state":"unknown","navigation_right_ratio":0,...}`——那是机器人 + 自己在看企业微信窗口,不是在回客户。守卫每轮轮询都要问一次模型,条数远多于 + 真实对话,不分开的话这张表根本没法用来查问题,裁判分布也在给错误的东西打分。 + """ + + def setUp(self) -> None: + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + self.db = admin_backend.Database(self.root / "t.db") + with mock.patch("builtins.print"): + self.db.initialize("Admin@123456") + + def _log(self, task_id: str, purpose: str, **extra) -> None: + record = { + "device_id": "d1", "task_id": task_id, "chosen": "p1", + "judge": {"winner": "A", "score": 0.9, "risk": "low"}, + "candidates": [{"provider": "p1", "text": "文本"}], "total_ms": 100, + "customer_text": "客户说的", "reply_text": "模型答的", + "purpose": purpose, + } + record.update(extra) + self.db.log_model_call(record) + + def test_the_default_log_shows_only_customer_chats(self) -> None: + self._log("c1", "chat") + self._log("g1", "guard") + self._log("g2", "guard") + found = self.db.list_model_calls(days=7) + self.assertEqual(found["total"], 1) + self.assertEqual(found["items"][0]["task_id"], "c1") + + def test_guard_calls_can_still_be_listed_on_purpose(self) -> None: + """内部调用照样在花钱,需要的时候得能翻出来看,不能查无此物。""" + self._log("c1", "chat") + self._log("g1", "guard") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + self.assertEqual(self.db.list_model_calls(days=7, purpose="")["total"], 2) + + def test_stats_ignore_guard_calls(self) -> None: + """裁判分布要量的是"发给客户的回复够不够好",不是"布局认得准不准"。""" + self._log("c1", "chat", judge={"winner": "A", "score": 0.9, "risk": "low"}) + for index in range(5): + self._log( + f"g{index}", "guard", + judge={"winner": "B", "score": 0.1, "risk": "high"}, + ) + stats = self.db.model_call_stats(7) + self.assertEqual(stats["total"], 1, "5 条界面识别不该被算成客服调用") + self.assertEqual(stats["judged"], 1) + self.assertAlmostEqual(stats["avg_score"], 0.9, places=3) + self.assertNotIn("high", stats["risk"], "守卫的低分不该污染风险占比") + + def test_a_call_without_an_explicit_purpose_counts_as_a_customer_chat(self) -> None: + """桌面端补报的那条路不带 purpose——默认必须是 chat,不能凭空消失。""" + self.db.log_model_call({ + "task_id": "c9", "chosen": "p1", "judge": {}, "candidates": [], + "customer_text": "在吗", "reply_text": "在的", + }) + self.assertEqual(self.db.list_model_calls(days=7)["total"], 1) + + def test_legacy_guard_rows_are_reclassified_by_their_fingerprint(self) -> None: + """老库里的守卫调用落的是默认 chat,要按候选内容的指纹认出来并改判。 + + 指纹用的是分类器专用的 JSON 字段名(布局守卫的 navigation_right_ratio、 + 会话行分类器的 reply_capable)——正常客服回复里不可能出现这些词。 + """ + path = self.root / "legacy.db" + con = sqlite3.connect(path) + con.executescript( + """ + CREATE TABLE model_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', + roles_version INTEGER NOT NULL DEFAULT 0, judge_mode TEXT NOT NULL DEFAULT '', + chosen TEXT NOT NULL DEFAULT '', judge_winner TEXT NOT NULL DEFAULT '', + judge_score REAL NOT NULL DEFAULT 0, judge_risk TEXT NOT NULL DEFAULT '', + candidates_json TEXT NOT NULL DEFAULT '[]', total_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ); + INSERT INTO model_calls (task_id,candidates_json,created_at) VALUES + ('L1','[{"provider":"a","text":"{\\"state\\":\\"unknown\\",\\"navigation_right_ratio\\":0}"}]','2026-01-01 00:00:01'), + ('L2','[{"provider":"a","text":"{\\"kind\\":\\"customer_chat\\",\\"reply_capable\\":true}"}]','2026-01-01 00:00:02'), + ('L3','[{"provider":"a","text":"忙完了,歇会儿吧,最近还好吗"}]','2026-01-01 00:00:03'); + """ + ) + con.commit() + con.close() + + with mock.patch("builtins.print"): + admin_backend.Database(path).initialize("Admin@123456") + + db = admin_backend.Database(path) + with db.connect() as con: + marks = { + row["task_id"]: row["purpose"] + for row in con.execute("SELECT task_id, purpose FROM model_calls") + } + self.assertEqual(marks["L1"], "guard", "布局守卫的记录要改判") + self.assertEqual(marks["L2"], "guard", "会话行分类器的记录也要改判") + self.assertEqual(marks["L3"], "chat", "真实客服回复绝不能被误判成内部调用") + + def test_the_reclassification_never_deletes_anything(self) -> None: + self._log("g1", "chat", candidates=[ + {"provider": "a", "text": '{"navigation_right_ratio":0.1}'} + ]) + before = self.db.list_model_calls(days=7, purpose="")["total"] + with self.db.connect() as con: + admin_backend.Database._backfill_guard_purpose(con) + con.commit() + after = self.db.list_model_calls(days=7, purpose="")["total"] + self.assertEqual(before, after, "只改分类标记,一行都不能少") + self.assertEqual(self.db.list_model_calls(days=7, purpose="guard")["total"], 1) + + +def setUpModule(): + # 别让测试读到开发机上的真实桌面端配置——配过模型网关的机器会让 + # `ai_chat.current_provider()` 整体改走网关分支,一大片无关测试跟着变行为。 + local_state_redirect.start() + + +def tearDownModule(): + local_state_redirect.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-remote-20260928/tests/test_remote_api.py b/deploy/mobile-remote-20260928/tests/test_remote_api.py new file mode 100644 index 000000000..23acd75d7 --- /dev/null +++ b/deploy/mobile-remote-20260928/tests/test_remote_api.py @@ -0,0 +1,716 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/deploy/mobile-remote-20260928/tests/test_zyt_auth.py b/deploy/mobile-remote-20260928/tests/test_zyt_auth.py new file mode 100644 index 000000000..a5af82759 --- /dev/null +++ b/deploy/mobile-remote-20260928/tests/test_zyt_auth.py @@ -0,0 +1,100 @@ +"""跨服务器桌面认证:只在路由缺失时兼容,拒绝结果不能降级。""" +import io +import json +import unittest +import urllib.error +from unittest import mock + +import zyt_auth + + +class DesktopVerificationRegressionTest(unittest.TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def session(self, **updates): + data = {"user_id": "admin:9", "terminal": 7, "status": "active", + "root": False, "role_ids": [2], "permissions": ["tcm.diagnosis/lists"]} + data.update(updates) + return {"code": 1, "data": data} + + def verify(self): + return zyt_auth.verify_desktop_token("test-token", api_url="https://identity.example.test") + + def missing(self, status=404): + return urllib.error.HTTPError("https://identity.example.test", status, "test", None, None) + + def test_success_never_requests_ip_bound_web_profile(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(self.session(root=True, permissions=["*"])), + self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertTrue(identity["root"]) + self.assertEqual(identity["role_ids"], [2]) + self.assertEqual(identity["permissions"], ["*"]) + self.assertTrue(identity["permissions_known"]) + + def test_missing_permissions_do_not_trigger_profile_request(self): + payload = self.session() + del payload["data"]["permissions"] + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + identity = self.verify() + self.assertEqual(opened.call_count, 1) + self.assertEqual(identity["permissions"], []) + self.assertFalse(identity["permissions_known"]) + + def test_empty_permissions_are_known(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(self.session(permissions=[]))): + identity = self.verify() + self.assertTrue(identity["permissions_known"]) + self.assertEqual(identity["permissions"], []) + + def test_only_missing_routes_allow_legacy_verification(self): + for status in (404, 405): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(status), self.response({"code": 1, "data": { + "user": {"id": 9, "disable": 0, "role_ids": [2]}, "permissions": []}}), + ]) as opened: + identity = self.verify() + self.assertEqual(identity["user_id"], "admin:9") + self.assertEqual(opened.call_count, 2) + + def test_http_rejections_do_not_fall_back(self): + for status in (401, 403, 500, 502): + with self.subTest(status=status), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=self.missing(status)) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_invalid_identity_cannot_be_accepted_through_web_fallback(self): + failures = [{"code": -1, "msg": "登录已失效"}, {"code": "bad"}, [], + {"code": 1, "data": []}, self.session(user_id=""), + self.session(terminal=1), self.session(terminal="bad"), + self.session(status="disabled")] + for payload in failures: + with self.subTest(payload=payload), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response(payload), self.response({"code": 1, "data": {"user": {"id": 9}}}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_bad_json_and_network_failure_do_not_fall_back(self): + for result in (io.BytesIO(b"error"), urllib.error.URLError("offline")): + with self.subTest(result=result), mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[result]) as opened: + with self.assertRaises(zyt_auth.ZytAuthError): + self.verify() + self.assertEqual(opened.call_count, 1) + + def test_legacy_ip_error_identifies_missing_server_deployment(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.missing(), self.response({"code": -1, "msg": "ip地址发生变化,请重新登录"}), + ]): + with self.assertRaisesRegex(zyt_auth.ZytAuthError, "/adminapi/desktop/session"): + self.verify() + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/mobile-remote-20260928/zyt/README.md b/deploy/mobile-remote-20260928/zyt/README.md new file mode 100644 index 000000000..d52d98cde --- /dev/null +++ b/deploy/mobile-remote-20260928/zyt/README.md @@ -0,0 +1,33 @@ +# ZYT 侧改动(企微客服助手 · 手机远程控制台) + +源码在 `C:/zyt`(仓库 `git1.zhenyangtang.com.cn:9595/ZYT/zyt.git`),已由 ZYT 团队提交到 `master` +(提交 `694c83dc7` 新增终端与菜单、`3cdc812f2` 把终端号从 8 改为 9),**由 ZYT 团队自行发布**。本目录只是参考副本: + +| 文件 | 内容 | +|---|---| +| `zyt-wecom-rpa-mobile-terminal.patch` | `694c83dc7^..3cdc812f2` 的代码部分:`AdminTerminalEnum::WECOM_RPA_MOBILE = 9`;`LoginValidate` 接受终端 9;`DesktopController::session` 同时接受 7 和 9,并如实返回终端号;两个契约 / 行为测试补上终端 9 的断言(并断言不等于 AI 浏览器的终端 8) | +| `add_wecom_rpa_remote_menu.sql` | `server/sql/1.9.20260928/`:隐藏目录"企微客服助手"+ 4 个按钮权限(`wecom_rpa.remote/control`、`wecom_rpa.remote/all`、`wecom_rpa.blacklist/add`、`wecom_rpa.blacklist/remove`),默认不授予任何角色 | + +为什么是 9:终端 8 已被 ZYT 的 AI 浏览器(`app\mcp\service\ConsoleService::TERMINAL`)占用,`AdminSession` +按(管理员, 终端)只保留一条,共用 8 会让手机登录和 AI 浏览器互相顶掉。 + +## 上线 + +1. 发布 ZYT 后端(含上面两个提交)。 +2. **同一次**执行 SQL(表前缀不是 `zyt_` 时先替换)。SQL 可重复执行,已在 MySQL 5.7 上连续跑两遍验证。 + 在 SQL 执行前,"拥有全部菜单"的角色会被 ZYT 返回为 `*`,客服后台会把它当作全权限。 +3. 在「权限管理 → 角色管理」给试点客服所在角色勾选 `远程启停本人电脑`、`会话加入黑名单`;主管角色再加 + `会话移出黑名单`(需要跨账号时加 `控制所有客服电脑`)。 + +"企微客服助手"目录 `is_show=0`,不出现在侧边栏;它没有页面,只用来挂这 4 个按钮权限。 + +## 测试 + +```bash +cd server +php -n -d extension_dir=/ext -d extension=mbstring -d extension=openssl -d extension=fileinfo -d extension=curl -d extension=pdo_mysql tests/AdminDesktopAuthContractTest.php +php -n -d extension_dir=/ext -d extension=mbstring -d extension=openssl -d extension=fileinfo -d extension=curl -d extension=pdo_mysql tests/AdminDesktopSessionBehaviorTest.php +``` + +终端 9 的令牌只能由管理员用自己的账号密码登录获得(由客服后台代登),`desktop/session` 仍然是免菜单权限、 +但严格校验令牌的接口;桌面端(7)和手机端(9)的令牌在客服后台互不通用。 diff --git a/deploy/mobile-remote-20260928/zyt/add_wecom_rpa_remote_menu.sql b/deploy/mobile-remote-20260928/zyt/add_wecom_rpa_remote_menu.sql new file mode 100644 index 000000000..b5bcea240 --- /dev/null +++ b/deploy/mobile-remote-20260928/zyt/add_wecom_rpa_remote_menu.sql @@ -0,0 +1,47 @@ +-- 企业微信客服助手:手机远程控制台(终端 9)使用的菜单权限。 +-- +-- 新增一个不在侧边栏显示的目录“企微客服助手”,下挂四个按钮权限。客服后台只读取 +-- /adminapi/desktop/session 返回的 permissions 做校验,ZYT 这边没有对应页面。 +-- +-- wecom_rpa.remote/control 远程启停本人电脑的监听(建议授予客服) +-- wecom_rpa.remote/all 查看并控制所有客服的电脑(仅管理员) +-- wecom_rpa.blacklist/add 把会话加入黑名单(建议授予客服) +-- wecom_rpa.blacklist/remove 把会话移出黑名单(主管 / 管理员) +-- +-- 默认不授予任何角色:先在“权限管理 → 角色管理”里给 1~2 位客服所在角色勾选试用, +-- 再逐步放开。登录手机控制台、查看本人电脑与会话不需要这些权限。 +-- 注意:拥有“全部菜单”的角色原先返回 '*',新增按钮后会改为返回明细权限(与以往 +-- 新增菜单的行为一致),需要时在角色里补勾这四项。表前缀如非 zyt_ 请调整。 + +START TRANSACTION; + +INSERT INTO `zyt_system_menu` + (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +SELECT + 0, 'M', '企微客服助手', 'el-icon-Iphone', 0, '', 'wecom_rpa', '', '', '', 0, 0, 0, + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `zyt_system_menu` WHERE `type` = 'M' AND `paths` = 'wecom_rpa' +); + +SET @wecom_rpa_menu_id = ( + SELECT `id` FROM `zyt_system_menu` + WHERE `type` = 'M' AND `paths` = 'wecom_rpa' + ORDER BY `id` ASC LIMIT 1 +); + +INSERT INTO `zyt_system_menu` + (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +SELECT @wecom_rpa_menu_id, 'A', item.`name`, '', item.`sort`, item.`perms`, '', '', '', '', 0, 1, 0, + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +FROM ( + SELECT '远程启停本人电脑' AS `name`, 40 AS `sort`, 'wecom_rpa.remote/control' AS `perms` + UNION ALL SELECT '控制所有客服电脑', 30, 'wecom_rpa.remote/all' + UNION ALL SELECT '会话加入黑名单', 20, 'wecom_rpa.blacklist/add' + UNION ALL SELECT '会话移出黑名单', 10, 'wecom_rpa.blacklist/remove' +) AS item +WHERE @wecom_rpa_menu_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM `zyt_system_menu` AS existing WHERE existing.`perms` = item.`perms`); + +COMMIT; diff --git a/deploy/mobile-remote-20260928/zyt/zyt-wecom-rpa-mobile-terminal.patch b/deploy/mobile-remote-20260928/zyt/zyt-wecom-rpa-mobile-terminal.patch new file mode 100644 index 000000000..2e503912a --- /dev/null +++ b/deploy/mobile-remote-20260928/zyt/zyt-wecom-rpa-mobile-terminal.patch @@ -0,0 +1,111 @@ +diff --git a/server/app/adminapi/controller/DesktopController.php b/server/app/adminapi/controller/DesktopController.php +index 3c2b84333..f0272a4b9 100644 +--- a/server/app/adminapi/controller/DesktopController.php ++++ b/server/app/adminapi/controller/DesktopController.php +@@ -28,7 +28,10 @@ class DesktopController extends BaseAdminController + if (empty($identity) || empty($identity['admin_id'])) { + return $this->fail('登录已失效,请重新登录'); + } +- if ((int)($identity['terminal'] ?? 0) !== AdminTerminalEnum::WECOM_RPA) { ++ // 桌面软件(7)和客服后台代登的手机控制台(9)都用这个接口换身份; ++ // 返回真实终端,客服后台据此区分两种会话,桌面令牌不能冒充手机令牌。 ++ $terminal = (int)($identity['terminal'] ?? 0); ++ if (!in_array($terminal, AdminTerminalEnum::wecomRpaTerminals(), true)) { + return $this->fail('当前登录令牌不属于企业微信客服桌面端'); + } + +@@ -59,7 +62,7 @@ class DesktopController extends BaseAdminController + 'nickname' => $name !== '' ? $name : $account, + 'mobile' => '', + 'avatar' => (string)$admin['avatar'], +- 'terminal' => AdminTerminalEnum::WECOM_RPA, ++ 'terminal' => $terminal, + 'expire_time' => (int)($identity['expire_time'] ?? 0), + 'status' => 'active', + 'account_type' => 'admin', +diff --git a/server/app/adminapi/validate/LoginValidate.php b/server/app/adminapi/validate/LoginValidate.php +index a1d18d624..734cc39a9 100755 +--- a/server/app/adminapi/validate/LoginValidate.php ++++ b/server/app/adminapi/validate/LoginValidate.php +@@ -33,7 +33,7 @@ class LoginValidate extends BaseValidate + { + protected $rule = [ + 'terminal' => 'require|in:' . AdminTerminalEnum::PC . ',' . AdminTerminalEnum::MOBILE +- . ',' . AdminTerminalEnum::WECOM_RPA, ++ . ',' . AdminTerminalEnum::WECOM_RPA . ',' . AdminTerminalEnum::WECOM_RPA_MOBILE, + 'account' => 'require', + 'password' => 'require|password', + ]; +diff --git a/server/app/common/enum/AdminTerminalEnum.php b/server/app/common/enum/AdminTerminalEnum.php +index 63585a9dd..70fd2e33a 100755 +--- a/server/app/common/enum/AdminTerminalEnum.php ++++ b/server/app/common/enum/AdminTerminalEnum.php +@@ -27,4 +27,16 @@ class AdminTerminalEnum + const MOBILE = 2; + // 企业微信客服桌面软件。与网页端分开保存会话,桌面登录不会顶掉浏览器登录。 + const WECOM_RPA = 7; ++ // 8 已被 AI 浏览器(app/mcp/service/ConsoleService::TERMINAL)占用,不要复用。 ++ // 企业微信客服手机远程控制台。由客服后台服务端代为登录,单独保存会话, ++ // 手机登录既不会顶掉桌面软件(7),也不会和网页后台(1)、AI 浏览器(8)互踢。 ++ const WECOM_RPA_MOBILE = 9; ++ ++ /** ++ * 可以调用 /adminapi/desktop/session 换取客服身份的终端。 ++ */ ++ public static function wecomRpaTerminals(): array ++ { ++ return [self::WECOM_RPA, self::WECOM_RPA_MOBILE]; ++ } + } +diff --git a/server/tests/AdminDesktopAuthContractTest.php b/server/tests/AdminDesktopAuthContractTest.php +index 3b16d8641..2f39d3b53 100644 +--- a/server/tests/AdminDesktopAuthContractTest.php ++++ b/server/tests/AdminDesktopAuthContractTest.php +@@ -15,6 +15,15 @@ function adminDesktopExpect(bool $condition, string $message): void + } + + adminDesktopExpect(AdminTerminalEnum::WECOM_RPA === 7, 'desktop admin terminal remains 7'); ++adminDesktopExpect(AdminTerminalEnum::WECOM_RPA_MOBILE === 9, 'mobile console terminal is 9'); ++adminDesktopExpect( ++ AdminTerminalEnum::WECOM_RPA_MOBILE !== \app\mcp\service\ConsoleService::TERMINAL, ++ 'mobile console must not share the AI browser console terminal' ++); ++adminDesktopExpect( ++ AdminTerminalEnum::wecomRpaTerminals() === [7, 9], ++ 'only the desktop and mobile console terminals may read the desktop session' ++); + + $validator = file_get_contents(dirname(__DIR__) . '/app/adminapi/validate/LoginValidate.php'); + $controller = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/DesktopController.php'); +@@ -23,6 +32,10 @@ adminDesktopExpect( + str_contains($validator, 'AdminTerminalEnum::WECOM_RPA'), + 'admin login accepts the dedicated desktop terminal' + ); ++adminDesktopExpect( ++ str_contains($validator, 'AdminTerminalEnum::WECOM_RPA_MOBILE'), ++ 'admin login accepts the dedicated mobile console terminal' ++); + adminDesktopExpect( + str_contains($controller, "public array \$notNeedLogin = ['session'];") + && str_contains($controller, "'user_id' => 'admin:'") +diff --git a/server/tests/AdminDesktopSessionBehaviorTest.php b/server/tests/AdminDesktopSessionBehaviorTest.php +index 9fe878c34..06d13bf20 100644 +--- a/server/tests/AdminDesktopSessionBehaviorTest.php ++++ b/server/tests/AdminDesktopSessionBehaviorTest.php +@@ -90,8 +90,16 @@ namespace { + desktopCheck($invoke($valid)['data']['root'] === true, 'root comes from current account'); + Admin::$row['root'] = 0; + ++ desktopCheck($result['data']['terminal'] === 7, 'desktop token reports the desktop terminal'); ++ $mobile = $invoke(array_replace($valid, ['terminal' => 9])); ++ desktopCheck($mobile['code'] === 1, 'mobile console token (terminal 9) is accepted'); ++ desktopCheck($mobile['data']['terminal'] === 9, 'mobile console token reports terminal 9'); ++ desktopCheck($mobile['data']['user_id'] === 'admin:9', 'mobile console maps to the same tenant identity'); ++ desktopCheck($invoke(array_replace($valid, ['terminal' => 8]))['code'] === 0, 'AI browser console token rejected'); ++ + desktopCheck($invoke([])['code'] === 0, 'missing or expired cached identity rejected'); + desktopCheck($invoke(array_replace($valid, ['terminal' => 1]))['code'] === 0, 'web token rejected'); ++ desktopCheck($invoke(array_replace($valid, ['terminal' => 2]))['code'] === 0, 'admin mobile web token rejected'); + Admin::$row['disable'] = 1; + desktopCheck($invoke($valid)['code'] === 0, 'disabled account rejected'); + Admin::$row = []; diff --git a/deploy/mobile-remote-deployed.md b/deploy/mobile-remote-deployed.md new file mode 100644 index 000000000..0568dc1d6 --- /dev/null +++ b/deploy/mobile-remote-deployed.md @@ -0,0 +1,103 @@ +# 手机远程控制台(管理端)上线记录 + +2026-09-28 15:21(CST)完成用户授权的管理端服务更新("把其他部署到线上吧" / "继续,zyt我们自己发布";内网模式, +SSH 绑定有线网卡 192.168.123.43 → 192.168.110.111)。ZYT 由 ZYT 团队自行发布,本次未动 ZYT。 + +- 服务器:`ps@192.168.110.111`,项目 `/opt/im-admin`。 +- 发布目录:`/home/ps/mobile-remote-20260928`(本地 `C:/kefu/deploy/mobile-remote-20260928/`,说明见其中 `RELEASE.md`)。 +- 当前镜像:`zyt/wecom-admin:mobile-remote-20260928`,同时标记为 `zyt/wecom-admin:current`, + `sha256:826d68999c239d9c1f4fd6dbd15c62d0f520f8caf1e764d418ea93493e3498ac`。 +- 发布包:`mobile-remote-server-bundle-r2.tar.gz`,522,202 字节,SHA-256 + `dc59f2a6278ae04da1f8a4c6193373317126a2fda184cdf3335004cfd06a751e`(r1 只差 `02-backup-build.sh` 一处测试导入修正,见下)。 +- 生产文件:`wechat_rpa/admin_api.py`、`wechat_rpa/zyt_auth.py`、`deploy/im-admin/Dockerfile`(替换), + `wechat_rpa/remote_api.py`、`wechat_rpa/remote_store.py`、`admin-web/apps/mobile-h5/dist/`(28 个文件,新增)。 +- 表结构:新增 `mobile_sessions`、`desktop_commands`、`desktop_status`、`conversation_blacklist`(纯新增,未改已有表)。 + +## 生产基线漂移(重要) + +生产上的 `admin_api.py` 不是 `C:/kefu` HEAD,而是 2026-09-23 COS 直传发布从 `C:/wechat_rpa/tmp/cos-direct-server-payload` +放上去的版本(同批还有 `archive_store.py`,两者都是 CRLF)。本次先把生产 104 个后端文件原样取回,用三方合并得到 +"生产版 + 本次改动"(无冲突,逐行核对只多出本次的改动),在"生产文件 + 本次 4 个文件"的副本上跑测试后才打包。 +`C:/kefu/wechat_rpa` 的 `admin_api.py`、`archive_store.py`、`archive_api.py` 已同步为与生产一致(后两个本地原本只是落后于生产)。 + +## 结果 + +- 预检:宿主与运行中容器内 3 个基线文件哈希一致(`admin_api.py` fcc94534…、`zyt_auth.py` 6b8ded1a…、`Dockerfile` 2bc1a6c1…), + api 运行镜像 `sha256:0861974700f0…`;网关、知识库 worker(`sha256:8299aac40787…`)记录在案。 +- 备份:`/data/backups/mobile-remote-20260928/backend.db` 6,972,891,136 字节(15:16 CST,WAL 模式在线备份), + quick_check ok,密钥 1 份。 +- 候选镜像离线(`--network=none`、只读根目录)跑 136 项测试通过:`test_remote_api`、`test_zyt_auth`、`test_backend` + (随包的新版)、`test_archive_api`(镜像自带)。第一次运行因工作目录优先导入了镜像里的旧 `test_backend.py` 而停在测试 + 阶段(线上服务未受影响),`02-backup-build.sh` 改用 `python -P` 并在重跑时沿用已完成的备份后通过。 +- 部署后:api 容器 healthy、重启 0 次;网关与 worker 未变;宿主与容器内 33 个文件哈希与清单一致; + 4 张新表存在,quick_check ok;4 个新接口匿名访问 401;`/m/` 200 并带 CSP、`X-Frame-Options: DENY`、nosniff、 + no-referrer;经 Nginx(`:8369`)访问 `/m/`、`/api/v2/health`、管理后台首页均 200;重启后 10 分钟内日志无错误、无 5xx, + 桌面端心跳(167 次 `desktop/me`)已恢复。没有做任何登录,没有读取聊天或患者数据。 + +## 备份与回滚 + +- 源码备份:`/home/ps/mobile-remote-20260928/source-before/`。 +- 旧镜像:`zyt/wecom-admin:before-mobile-remote-20260928`(`sha256:0861974700f0…`)。 +- 回滚:`REMOTE_RELEASE_APPROVED=mobile-remote-20260928 bash /home/ps/mobile-remote-20260928/rollback.sh` + (恢复 3 个文件、删除新增文件与 `/m/`、切回旧镜像,只重建 api;保留实时数据库,新表旧代码不读写)。 + +## 尚未完成(需要各自负责人) + +1. **ZYT**:ZYT 团队发布 `master` 上的 `694c83dc7`、`3cdc812f2`(终端 9),并同一次执行 + `server/sql/1.9.20260928/add_wecom_rpa_remote_menu.sql`。在此之前手机登录提示"ZYT 尚未开通企微客服手机端登录(终端 9)"。 +2. **HTTPS(可选)**:16:26 起公网也可以用 HTTP 登录(见下方"追加发布")。以后要加密传输时按 + `deploy/im-admin/im.zhenyangtang.cn.https.conf.example` 配证书 + 端口转发 + Nginx。 +3. **桌面端 1.5.1**:安装包由管理员在「版本升级」小比例灰度发布;旧版桌面端在手机上显示"版本过低"。 + 已于 15:39 构建:`C:/wechat_rpa/dist/installer/甄AI客服-安装包-v1.5.1.exe`,542,418,052 字节,SHA-256 + `CEFDF05E8059C4BCD04AC1C7A9927123A65B5CD9FA2276AA38904D74182E4CF2`(未签名:本机未配置代码签名证书,与以往一致)。 + 第一次完整构建停在打包自检:内置 OCR 的"消息导航"合成图检查在新生成文件首次冷启动时没识别出来,且失败后进程没有退出, + 被构建脚本记为超时;同一个包随后按构建脚本同样的干净 PATH 连续两次自检通过(14–15 秒),再用 + `build_installer.ps1 -SkipAppBuild` 生成安装包。 +4. **灰度授权**:ZYT 角色里给试点客服勾选 `wecom_rpa.remote/control`、`wecom_rpa.blacklist/add`,主管加 + `wecom_rpa.blacklist/remove`。 + +## 已知的旧测试问题(与本次无关) + +生产镜像自带的 `test_admin_api.ReleaseApiTest.test_package_upload_builds_a_verified_public_manifest` 仍断言旧的 +`/api/v2/desktop/update/cos-packages/` 下载地址(9-23 起 `complete` 返回 COS 签名地址),`test_backend` 的 +`test_backend_verifies_the_admin_desktop_session_endpoint` 仍模拟旧的两次请求流程;两者在未改动的生产代码上同样失败。 +`C:/kefu/wechat_rpa` 的 `test_backend` 已是新版;`test_admin_api` 那一项待更新。 + +## 追加发布:mobile-http-20260928(2026-09-28 16:26 CST) + +用户要求"不管在什么环境下都可以使用 http 登录账号,不要非固定死 https,改完后直接发布到线上环境" +(此前用 4G 访问 `http://im.zhenyangtang.cn:8369/m/` 登录被"请通过 HTTPS 地址访问手机控制台"拒绝)。 + +- 改动:`remote_api.py` 里 `MOBILE_REQUIRE_HTTPS` 默认值由 `auto` 改为 `never`(内外网都允许明文 HTTP 登录; + 以后配好 HTTPS 可设 `auto` / `always` 恢复限制);`/m/` 登录页去掉"只能在公司内网登录"的横幅。 + 其余逻辑(登录限流、ZYT 锁定处理、令牌、权限)不变。无数据库变更,本次未做整库备份。 +- 发布目录:`/home/ps/mobile-http-20260928`(本地 `C:/kefu/deploy/mobile-http-20260928/`)。 + 发布包 `mobile-http-server-bundle.tar.gz`,488,202 字节,SHA-256 + `3befdb5ead6c4f5a1f7e43d3306c7a0917620fab3f1a92ad82f04721f7d2d499`;代码差异见 `payload-diff.txt`(只有该函数的说明和默认值)。 +- 当前镜像:`zyt/wecom-admin:mobile-http-20260928` = `current`,`sha256:a633dd559af7d87260a6d32deb205f92c4ba7e2fcf92f4fe91df91c2493d20f8`; + 回滚标签 `before-mobile-http-20260928`(= mobile-remote-20260928,`sha256:826d68999c23…`)。 +- 验证:预检宿主与容器内 29 个文件(`remote_api.py` + `/m/` 28 个文件)与上一版一致;候选镜像离线 137 项测试通过 + (含新增"默认允许公网 HTTP 登录"),候选镜像 `/m/` 恰好是新版 28 个文件、无旧横幅;部署后 api healthy、重启 0 次, + 网关与 worker 未变,宿主与容器内文件与清单一致,在运行中的容器里判定"公网来源 + HTTP 登录"不再被拒绝; + 经 Nginx 访问公网域名的 `/m/` 加载的是新登录页(无横幅、控制台无报错),重启后日志无错误、无 5xx,桌面端心跳正常。 +- 注意:8369 端口的 Nginx 会缓存 `/m/assets/` 下的带哈希文件(旧文件在 Nginx 里仍可取到,API 本身已 404)。 + 入口 `index.html` 不缓存,所以重新打开或刷新页面即是新版;已经开着旧页面的手机需要刷新一次。 +- 回滚:`REMOTE_RELEASE_APPROVED=mobile-http-20260928 bash /home/ps/mobile-http-20260928/rollback.sh` + (恢复 `remote_api.py` 与原 `/m/`,切回 mobile-remote-20260928 镜像,只重建 api)。 + +## 追加发布:mobile-ignore-20260929(2026-09-29 CST) + +用户要求"把黑名单文字改成忽略,两个字,只改文字不改规则,改完后同步到线上"。 + +- 改动:`/m/` 界面文字"黑名单"全部换成"忽略"(ConversationDetail.vue、ConversationsView.vue、MeView.vue、manifest.webmanifest 描述)。 + 规则、接口、权限码(`wecom_rpa.blacklist/*`)、后端代码和返回的错误文字都没动;无数据库变更。 +- 发布目录:`/home/ps/mobile-ignore-20260929`(本地 `C:/kefu/deploy/mobile-ignore-20260929/`),只含 `/m/` 28 个文件; + 发布包 `mobile-ignore-server-bundle.tar.gz`,478,493 字节,SHA-256 `971f1e1370553a88a10bfb77d65f9c9ee3b32f14cda30c078646ef192967d395`。 +- 当前镜像:`zyt/wecom-admin:mobile-ignore-20260929` = `current`,`sha256:23c1f2088b8f…`; + 回滚标签 `before-mobile-ignore-20260929`(= mobile-http-20260928,`sha256:a633dd559af7…`)。 +- 验证:基线 28 个文件与 mobile-http 一致;候选镜像 137 项测试通过;部署后 api healthy、网关与 worker 未变、日志无错误; + 公网域名 `/m/` 返回新版资源,不再含"黑名单"。 +- 回滚:`REMOTE_RELEASE_APPROVED=mobile-ignore-20260929 bash /home/ps/mobile-ignore-20260929/rollback.sh`。 +- 桌面端:`C:/wechat_rpa` 的 console-app.js、zhenyang-ai-console.html、wechat_gui_qt.py、remote_control.py、 + conversation_blacklist.py 的界面/提示文字同步改为"忽略",需重新打包并经「版本升级」发布才会到客服电脑上。 +- 桌面端安装包已于 2026-09-29 11:33 重建:`C:/wechat_rpa/dist/installer/甄AI客服-安装包-v1.5.2.exe`,542,437,337 字节,SHA-256 `E3527C032882ABD7E72538F67B987F768F09F83B88D199A413AD67747E794D16`(未签名,未发布)。 diff --git a/wechat_rpa/admin_api.py b/wechat_rpa/admin_api.py index 99955506f..e8f8077ae 100644 --- a/wechat_rpa/admin_api.py +++ b/wechat_rpa/admin_api.py @@ -44,6 +44,7 @@ import admin_backend as backend import app_version import archive_api import knowledge_api +import remote_api import zyt_auth # 账号下拉框里「全部」的取值。用 -1 而不是 0:0 早就有含义了(旧版未归属数据), @@ -220,6 +221,7 @@ class ConfigBody(BaseModel): class ReleaseBody(BaseModel): + package_media_id: str = "" latest_version: str download_url: str = "" release_notes: str = "" @@ -320,10 +322,16 @@ def _frontend_dist() -> Path: return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "web-antd" / "dist" +def _mobile_dist() -> Path: + """手机远程控制台(admin-web/apps/mobile-h5)的构建产物,托管在 /m/。""" + return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "mobile-h5" / "dist" + + def create_app( db_path: Path | str = "backend.db", zyt_token_verifier: Callable[[str], dict[str, Any]] | None = None, zyt_patient_searcher: Callable[[str, str, int, int], dict[str, Any]] | None = None, + zyt_mobile_auth: Any = None, ) -> FastAPI: database = backend.Database(Path(db_path).resolve()) # 每个碰这个库的服务启动时都要把结构升到当前版本。只起 API、不起网页后台的 @@ -439,13 +447,13 @@ def create_app( ).strip().rstrip("/") return (configured or zyt_auth.api_base_url(), bool(configured)) - def release_payload(row: Any) -> dict[str, Any]: + def release_payload(row: Any, *, direct_download: bool = False) -> dict[str, Any]: keys = set(row.keys()) if hasattr(row, "keys") else set() def value(name: str, default: Any = "") -> Any: return row[name] if name in keys else default - return { + result = { "latest_version": str(value("latest_version")), "download_url": str(value("download_url")), "release_notes": str(value("release_notes")), @@ -464,6 +472,17 @@ def create_app( ), } + identity = app.state.archive_store.release_package_identity(result['download_url']) + result['package_media_id'] = identity[0] if identity else '' + if direct_download and identity: + try: + result['download_url'] = app.state.archive_store.release_package_download_url(*identity) + except (KeyError, ValueError, RuntimeError) as exc: + raise HTTPException(status_code=409, detail=f"COS 安装包暂不可用:{exc}") from exc + except Exception as exc: + raise HTTPException(status_code=502, detail="COS 下载地址生成失败,请重试") from exc + return result + # ── 桌面账号认证 ───────────────────────────────────────────────────── @app.get("/api/v2/desktop/auth/config") async def desktop_auth_config() -> dict: @@ -736,8 +755,12 @@ def create_app( if "desktop-account:all" in principal.permissions else database.desktop_account_ids_for_admin(principal.id) ) + accounts = database.desktop_accounts(allowed, include_counts=include_counts) + mobile_counts = app.state.remote_store.mobile_session_counts() + for account in accounts: + account["mobile_session_count"] = mobile_counts.get(int(account["id"]), 0) return { - "accounts": database.desktop_accounts(allowed, include_counts=include_counts), + "accounts": accounts, "can_view_all": "desktop-account:all" in principal.permissions, # 尚未分配任何账号的旧角色仍能查看升级前的未归属数据;一旦分配账号, # 就必须明确选择授权账号,不能再借默认值绕过范围限制。 @@ -769,6 +792,9 @@ def create_app( archive_store = getattr(app.state, "archive_store", None) if archive_store is not None: archive_store.clear_zyt_session(str(account["tenant_id"])) + app.state.remote_store.revoke_mobile_sessions_for_account( + account_id, reason="account_disabled" + ) return {"account": account} @app.post("/api/v2/desktop-accounts/{account_id}/revoke-devices") @@ -782,6 +808,10 @@ def create_app( archive_store = getattr(app.state, "archive_store", None) if archive_store is not None: archive_store.clear_zyt_session(tenant) + # 「强制下线」同时收回手机控制台的登录,否则被下线的人还能远程启停。 + app.state.remote_store.revoke_mobile_sessions_for_account( + account_id, reason="admin_revoke" + ) return {"ok": True} @app.get("/api/v2/desktop-accounts/{account_id}/admins") @@ -1031,7 +1061,7 @@ def create_app( # ── 桌面端版本升级 ─────────────────────────────────────────────────── @app.get("/api/v2/release") async def get_release(_: Principal = Depends(require("config:read"))) -> dict: - return release_payload(database.release()) + return release_payload(database.release(), direct_download=True) @app.post("/api/v2/release") async def save_release( @@ -1040,10 +1070,13 @@ def create_app( principal: Principal = Depends(require("release:write")), ) -> dict: try: + download_url = app.state.archive_store.normalize_release_download( + body.download_url, body.package_sha256, body.package_size, body.package_media_id + ) release = backend.validate_release_form( { "latest_version": body.latest_version, - "download_url": body.download_url, + "download_url": download_url, "release_notes": body.release_notes, "force_upgrade": "1" if body.force_upgrade else "", "auto_install": "1" if body.auto_install else "", @@ -1057,6 +1090,8 @@ def create_app( ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc database.save_release( release["latest_version"], release["download_url"], @@ -1072,7 +1107,7 @@ def create_app( rollout_percent=release["rollout_percent"], status=release["status"], ) - return {"ok": True, **release} + return {"ok": True, **release_payload(database.release(), direct_download=True)} @app.post("/api/v2/release/package/prepare") async def prepare_release_package( @@ -1217,7 +1252,11 @@ def create_app( eligible and app_version.is_newer_version(latest, current_version) ) download_url = release["download_url"] - if download_url.startswith("/"): + identity = app.state.archive_store.release_package_identity(download_url) + if identity: + # Each check gets a fresh COS HTTPS URL, including old release rows. + download_url = release_payload(release, direct_download=True)["download_url"] if update_available else "" + elif download_url.startswith("/"): forwarded_scheme = str( request.headers.get("x-forwarded-proto") or request.url.scheme ).split(",", 1)[0].strip() @@ -1237,6 +1276,7 @@ def create_app( return { **release, "download_url": download_url, + "download_url_expires_in": 900 if identity and update_available else 0, "update_available": update_available, "eligible": eligible, } @@ -1425,6 +1465,58 @@ def create_app( app, database, require, client_ip, resolve_admin_tenant, current_desktop, ) + remote_api.register_remote_routes( + app, + database, + current_desktop=current_desktop, + zyt_api_url=lambda: desktop_login_api_url()[0], + zyt_mobile_auth=zyt_mobile_auth, + ) + + # ── 手机远程控制台 /m/ ─────────────────────────────────────────────── + # 必须注册在管理端 SPA 的通配路由之前,否则 /m/ 会被当成管理端的前端路由。 + mobile_dist = _mobile_dist() + if (mobile_dist / "index.html").is_file(): + from starlette.responses import FileResponse as _MobileFile + + mobile_root = mobile_dist.resolve() + + @app.get("/m", include_in_schema=False) + async def mobile_entry_redirect(): + return RedirectResponse("/m/", status_code=308) + + # 远程启停只要"点按钮、点确认"两下,页面不许被别的站点嵌进 iframe 诱导点击。 + mobile_headers = { + "X-Frame-Options": "DENY", + "Content-Security-Policy": ( + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; font-src 'self' data:; connect-src 'self'; " + "manifest-src 'self'; worker-src 'self'; frame-ancestors 'none'; " + "base-uri 'self'; form-action 'self'" + ), + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + } + + @app.get("/m/{path:path}", include_in_schema=False) + async def mobile_spa(path: str): + candidate = (mobile_root / path).resolve() if path else mobile_root / "index.html" + if not candidate.is_relative_to(mobile_root) or not candidate.is_file(): + # 发版后还开着的旧页面会去拉旧的哈希文件:回 404 让前端整页刷新到 + # 新版本;回 index.html 的话浏览器会把 HTML 当脚本执行,页面直接卡死。 + if path.startswith("assets/"): + raise HTTPException(status_code=404, detail="文件不存在") + candidate = mobile_root / "index.html" + response = _MobileFile(candidate) + # 文件名带哈希的构建产物可以长缓存;入口、清单和 Service Worker 每次都 + # 要重新验证,否则发版后手机上会一直卡在旧界面。 + if candidate.parent.name == "assets" and candidate.parent.parent == mobile_root: + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "no-cache" + response.headers.update(mobile_headers) + return response + # ── 前端静态资源 ───────────────────────────────────────────────────── # 构建产物存在时由本服务一起托管,前后端同源——省掉生产环境的 CORS 配置, # 也省掉一个 Nginx location。开发时前端跑 Vite dev server,走它自己的代理。 diff --git a/wechat_rpa/admin_backend.py b/wechat_rpa/admin_backend.py index 00b7d5764..1d569a093 100644 --- a/wechat_rpa/admin_backend.py +++ b/wechat_rpa/admin_backend.py @@ -140,6 +140,19 @@ def pbkdf2_sha256( return derived.to_bytes(32, "big") +def _sql_like_escape(value: str) -> str: + return ( + str(value or "") + .replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + ) + + +def _sql_like_prefix(value: str) -> str: + return f"{_sql_like_escape(value)}%" + + def now_text() -> str: return datetime.now().astimezone().isoformat(timespec="seconds") @@ -1478,6 +1491,27 @@ class Database: ).fetchall() return [{key: row[key] for key in row.keys()} for row in rows] + def audit_entries_for_user( + self, user_id: int, action_prefix: str, *, detail_contains: str = "", limit: int = 20 + ) -> list[dict[str, Any]]: + """某个账号在某类动作上的审计记录,用于桌面端展示订单查询留痕。""" + limit = max(1, min(int(limit or 20), 200)) + prefix = str(action_prefix or "").strip() + needle = str(detail_contains or "").strip() + with self.connect() as db: + rows = db.execute( + """SELECT id, action, detail, ip_address, created_at FROM audit_log + WHERE user_id=? AND action LIKE ? ESCAPE '\\' AND detail LIKE ? ESCAPE '\\' + ORDER BY id DESC LIMIT ?""", + ( + int(user_id), + _sql_like_prefix(prefix), + f"%{_sql_like_escape(needle)}%" if needle else "%", + limit, + ), + ).fetchall() + return [{key: row[key] for key in row.keys()} for row in rows] + def model_call_stats( self, days: int = 7, tenant_id: Any = "default" ) -> dict[str, Any]: diff --git a/wechat_rpa/archive_api.py b/wechat_rpa/archive_api.py index 6b16bd650..f85130f46 100644 --- a/wechat_rpa/archive_api.py +++ b/wechat_rpa/archive_api.py @@ -8,6 +8,8 @@ from __future__ import annotations import asyncio import json +import re +from datetime import datetime from pathlib import Path from typing import Any, Callable @@ -19,9 +21,33 @@ from archive_store import ALL_TENANTS, ArchiveStore from zyt_patient_client import ( ZytPatientClient, ZytPatientError, ZytPatientSessionExpired, ZytPatientPermissionDenied, ) +from zyt_order_client import ( + ZytOrderClient, ZytOrderError, ZytOrderSessionExpired, + ZytOrderPermissionDenied, ZytOrderNotFound, ZytOrderTrackingUnavailable, + sanitize_order, sanitize_tracking, +) ZYT_PATIENT_LIST_PERMISSION = "tcm.diagnosis/lists" +# 每个订单动作在 ZYT 侧真正校验的权限。桌面代理按动作要求同样的权限,避免本地 +# 放行之后再被上游拒绝,也避免只有列表权限的账号看到物流轨迹。 +ZYT_ORDER_LIST_PERMISSIONS = frozenset({"firstvisit.mypatient/orders"}) +ZYT_ORDER_DETAIL_PERMISSIONS = frozenset({ + "firstvisit.mypatient/orderdetail", + "tcm.prescriptionorder/detail", +}) +ZYT_ORDER_TRACKING_PERMISSIONS = frozenset({"tcm.prescriptionorder/logisticstrace"}) +ZYT_ORDER_PERMISSIONS = ( + ZYT_ORDER_LIST_PERMISSIONS | ZYT_ORDER_DETAIL_PERMISSIONS | ZYT_ORDER_TRACKING_PERMISSIONS +) +ORDER_AUDIT_ACTION_PREFIX = "archive.order." +ORDER_AUDIT_LABELS = { + "archive.order.list": "查询订单列表", + "archive.order.detail": "查询订单详情", + "archive.order.tracking": "查询物流轨迹", + "archive.order.audit_status": "查询审核状态", + "archive.order.summary": "刷新订单概览", +} class StorageBody(BaseModel): @@ -117,6 +143,27 @@ def _http_error(exc: Exception) -> HTTPException: }) if isinstance(exc, ZytPatientPermissionDenied): return HTTPException(status_code=403, detail=str(exc)) + if isinstance(exc, ZytOrderSessionExpired): + return HTTPException(status_code=409, detail={ + "code": "zyt_session_expired", + "message": "ZYT 订单查询登录已过期,请重新验证账号。", + }) + if isinstance(exc, ZytOrderPermissionDenied): + return HTTPException(status_code=403, detail=str(exc)) + if isinstance(exc, ZytOrderNotFound): + return HTTPException(status_code=404, detail={ + "code": "order_not_found", + "message": str(exc) or "订单不存在或不属于当前绑定患者", + }) + if isinstance(exc, ZytOrderTrackingUnavailable): + # 没有单号或物流服务失败都不是权限问题;用明确的 code 让 MCP 和界面 + # 告诉客服“正在核实”,而不是把它当成通用 502。 + return HTTPException( + status_code=409 if exc.code == "tracking_number_missing" else 502, + detail={"code": exc.code, "message": str(exc)}, + ) + if isinstance(exc, ZytOrderError): + return HTTPException(status_code=502, detail=str(exc)) if isinstance(exc, ZytPatientError): return HTTPException(status_code=502, detail=str(exc)) if isinstance(exc, RuntimeError): @@ -274,6 +321,276 @@ def register_archive_routes( client = ZytPatientClient(zyt_api_url(), token) return await asyncio.to_thread(client.get_patient, int(patient_id)) + async def _diagnosis_visible_to_patient( + tenant: str, patient_id: int, diagnosis_id: int + ) -> dict[str, Any] | None: + """Verify the selected diagnosis belongs to the selected patient. + + ``archive_patient_shadow`` stores only the latest diagnosis, while a + patient can have several historical diagnoses. Read raw rows with an + exact patient filter through the authenticated ZYT list endpoint; the + normal keyword search only matches name/phone and deduplicates patients. + """ + try: + patient_id = int(patient_id) + diagnosis_id = int(diagnosis_id) + except (TypeError, ValueError): + return None + if patient_id <= 0 or diagnosis_id <= 0: + return None + token = await asyncio.to_thread(store.zyt_token, tenant) + if zyt_patient_searcher is not None: + result = await asyncio.to_thread( + zyt_patient_searcher, token, str(diagnosis_id), 1, 50 + ) + else: + if zyt_api_url is None: + raise RuntimeError("ZYT 患者接口地址未配置") + client = ZytPatientClient(zyt_api_url(), token) + return await asyncio.to_thread( + client.get_patient_diagnosis, patient_id, diagnosis_id + ) + if not isinstance(result, dict): + raise ZytPatientError("ZYT 患者查询结果格式不正确") + for item in list(result.get("items") or []): + if not isinstance(item, dict): + continue + try: + item_patient_id = int(item.get("patient_id") or 0) + item_diagnosis_id = int(item.get("diagnosis_id") or 0) + except (TypeError, ValueError): + continue + if item_patient_id == patient_id and item_diagnosis_id == diagnosis_id: + return dict(item) + return None + + def _desktop_order_access( + account: Any, required: frozenset[str] = ZYT_ORDER_LIST_PERMISSIONS + ) -> dict[str, Any]: + """订单查询权限只允许只读业务权限,不能由模型自行提升。 + + ``required`` 是该动作在 ZYT 侧会检查的全部权限;缺任何一个都在本地拒绝。 + """ + try: + raw_permissions = account["zyt_permissions_json"] + except (KeyError, IndexError): + raw_permissions = "[]" + try: + parsed_permissions = json.loads(str(raw_permissions or "[]")) + except (TypeError, ValueError): + parsed_permissions = [] + permissions = { + str(item).strip().lower() + for item in parsed_permissions + if str(item).strip() + } if isinstance(parsed_permissions, list) else set() + try: + is_root = bool(int(account["zyt_root"] or 0)) + except (KeyError, IndexError, TypeError, ValueError): + is_root = False + try: + known = bool(int(account["zyt_permissions_known"] or 0)) + except (KeyError, IndexError, TypeError, ValueError): + known = False + full_access = is_root or "*" in permissions + allowed = full_access or required <= permissions + reason = "" if allowed else ( + "当前登录的 ZYT 账号没有患者订单查看权限" + if known else "暂未取得当前 ZYT 账号的权限信息,请退出软件后重新登录" + ) + return { + "can_query": allowed, + "can_list": full_access or ZYT_ORDER_LIST_PERMISSIONS <= permissions, + "can_detail": full_access or ZYT_ORDER_DETAIL_PERMISSIONS <= permissions, + "can_track": full_access or ZYT_ORDER_TRACKING_PERMISSIONS <= permissions, + "is_root": is_root, + "known": known, + "required_permissions": sorted(required), + "reason": reason, + } + + def _require_desktop_order_access( + account: Any, required: frozenset[str] = ZYT_ORDER_LIST_PERMISSIONS + ) -> dict[str, Any]: + access = _desktop_order_access(account, required) + if not access["can_query"]: + raise HTTPException(status_code=403, detail=str(access["reason"])) + return access + + async def _bound_patient_for_order( + account: Any, external_account_id: str, conversation_external_id: str, person_id: str = "" + ) -> tuple[dict[str, Any], dict[str, Any]]: + """Resolve the current conversation and return its active primary binding.""" + context = await asyncio.to_thread( + store.conversation_patient_context, + str(external_account_id or "").strip(), + str(conversation_external_id or "").strip(), + str(account["tenant_id"]), + person_id=str(person_id or "").strip(), + ) + person = context.get("person") if isinstance(context, dict) else None + bindings = list((person or {}).get("patient_bindings") or []) if isinstance(person, dict) else [] + if not bindings and isinstance(context, dict): + bindings = list(context.get("patient_bindings") or []) + if not isinstance(person, dict) or not person.get("id"): + if isinstance(context, dict) and context.get("requires_person_selection"): + raise HTTPException(status_code=409, detail={ + "code": "person_selection_required", + "message": "当前会话有多个联系人,请先在桌面端选择联系人并绑定患者", + }) + raise HTTPException(status_code=409, detail={ + "code": "patient_binding_required", + "message": str((context or {}).get("reason") or "当前会话没有可绑定的联系人"), + }) + if not bindings: + raise HTTPException(status_code=409, detail={ + "code": "patient_binding_required", + "message": "当前会话尚未绑定患者,请先绑定患者", + }) + binding = dict(bindings[0]) + try: + patient_id = int(binding.get("patient_id") or 0) + except (TypeError, ValueError): + patient_id = 0 + if patient_id <= 0: + raise HTTPException(status_code=409, detail={ + "code": "patient_binding_invalid", + "message": "当前患者绑定已失效,请重新绑定", + }) + # Older bindings may not have persisted diagnosis_id. Resolve it through + # the authenticated ZYT patient list before any order query, otherwise a + # broad order list could expose another patient's data. + try: + diagnosis_id = int(binding.get("diagnosis_id") or 0) + except (TypeError, ValueError): + diagnosis_id = 0 + if diagnosis_id <= 0: + visible = await _patient_visible_to_tenant(str(account["tenant_id"]), patient_id) + try: + diagnosis_id = int((visible or {}).get("diagnosis_id") or 0) + except (TypeError, ValueError): + diagnosis_id = 0 + if diagnosis_id <= 0: + raise HTTPException(status_code=409, detail={ + "code": "patient_binding_invalid", + "message": "当前患者绑定缺少有效诊单,请重新绑定", + }) + binding["diagnosis_id"] = diagnosis_id + # Do not trust a diagnosis ID persisted by an older client or supplied + # by a caller. It must still resolve to this patient in the current + # authenticated ZYT data scope before any order request is made. + verified = await _diagnosis_visible_to_patient( + str(account["tenant_id"]), patient_id, diagnosis_id + ) + if verified is None: + raise HTTPException(status_code=409, detail={ + "code": "patient_binding_invalid", + "message": "当前患者绑定的诊单不属于该患者,请重新绑定", + }) + return context, binding + + def _order_client(tenant: str) -> ZytOrderClient: + if zyt_api_url is None: + raise RuntimeError("ZYT 订单接口地址未配置") + token = store.zyt_token(str(tenant)) + return ZytOrderClient(zyt_api_url(), token) + + @staticmethod + def _query_time() -> str: + return datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S") + + def _desktop_order_diagnosis_id(row: dict[str, Any]) -> int: + try: + return int(row.get("diagnosis_id") or row.get("diagnosisId") or 0) + except (TypeError, ValueError): + return 0 + + async def _order_for_binding( + client: ZytOrderClient, binding: dict[str, Any], order_id: int + ) -> dict[str, Any]: + order = await asyncio.to_thread(client.get_order_detail, int(order_id)) + diagnosis_id = _desktop_order_diagnosis_id(order) + bound_diagnosis = int(binding.get("diagnosis_id") or 0) + if diagnosis_id <= 0 or (bound_diagnosis > 0 and diagnosis_id != bound_diagnosis): + raise ZytOrderNotFound("订单不存在或不属于当前绑定患者") + return order + + def _audit_order_query( + account: Any, request: Request, action: str, binding: dict[str, Any], + order_id: int = 0, *, conversation_external_id: str = "", outcome: str = "ok", + ) -> None: + try: + database.audit( + int(account["id"]), + action, + f"patient={int(binding.get('patient_id') or 0)} order={int(order_id or 0)} " + f"outcome={str(outcome or 'ok')[:40]} conv={str(conversation_external_id or '')[:200]}", + client_ip(request), + ) + except Exception: + pass + + def _order_query_logs(account: Any, conversation_external_id: str, limit: int = 8) -> list[dict[str, Any]]: + """当前会话最近的订单查询留痕,给桌面端展示“MCP 查询日志”。""" + conversation = str(conversation_external_id or "").strip() + if not conversation: + return [] + try: + rows = database.audit_entries_for_user( + int(account["id"]), ORDER_AUDIT_ACTION_PREFIX, + detail_contains=f"conv={conversation}", limit=limit, + ) + except Exception: + return [] + logs = [] + for row in rows: + detail = str(row.get("detail") or "") + order_match = re.search(r"\border=(\d+)", detail) + outcome_match = re.search(r"\boutcome=(\S+)", detail) + action = str(row.get("action") or "") + logs.append({ + "time": str(row.get("created_at") or ""), + "action": action, + "label": ORDER_AUDIT_LABELS.get(action, action), + "order_id": int(order_match.group(1)) if order_match else 0, + "outcome": outcome_match.group(1) if outcome_match else "ok", + }) + return logs + + async def _tracking_for_order( + client: ZytOrderClient, order: dict[str, Any], order_id: int, queried_at: str + ) -> tuple[dict[str, Any] | None, dict[str, str]]: + """Return (latest_tracking, error). Never invents a status when upstream fails.""" + phone = str(order.get("recipient_phone") or order.get("patient_phone") or "") + try: + tracking = await asyncio.to_thread( + client.get_tracking, int(order_id), phone_tail=re.sub(r"\D", "", phone)[-4:] + ) + except ZytOrderTrackingUnavailable as exc: + return None, {"code": exc.code, "message": str(exc)} + except ZytOrderError as exc: + return None, {"code": "tracking_unavailable", "message": str(exc)} + return sanitize_tracking(tracking, queried_at=queried_at), {} + + def _order_list_hint(items: list[dict[str, Any]]) -> dict[str, Any]: + """把“单笔直接查、多笔让客户选”的规则做成数据,不靠模型自觉。""" + count = len(items) + if count == 0: + return { + "order_count": 0, "selection_required": False, "single_order_id": 0, + "hint": "该患者名下没有查到订单记录:如实告诉客户系统里暂时没有查到,说明会人工核实后回复;不要向客户索要订单号。", + } + if count == 1: + return { + "order_count": 1, "selection_required": False, + "single_order_id": int(items[0].get("order_id") or 0), + "hint": "只有一笔订单,可以直接告知该订单的状态和物流。", + } + return { + "order_count": count, "selection_required": True, "single_order_id": 0, + "hint": "查到多笔订单,只能向客户说明各笔的下单时间和状态,让客户确认是哪一笔后再查物流。", + } + @app.get("/api/v2/archive/stats") def archive_stats( account_id: int | None = None, @@ -350,7 +667,8 @@ def register_archive_routes( return { "checkpoint": store.source_checkpoint( external_account_id, source_table, str(account["tenant_id"]) - ) + ), + "capabilities": {"message_origin_updates": True} } @app.post("/api/v2/archive/desktop/checkpoint") @@ -501,6 +819,288 @@ def register_archive_routes( except Exception as exc: raise _http_error(exc) from exc + @app.get("/api/v2/archive/desktop/patient-orders") + async def desktop_patient_orders( + request: Request, + external_account_id: str, + conversation_external_id: str, + person_id: str = "", + keyword: str = "", + page_no: int = 1, + page_size: int = 20, + start_date: str = "", + end_date: str = "", + account: Any = Depends(current_desktop), + ) -> dict[str, Any]: + access = _require_desktop_order_access(account, ZYT_ORDER_LIST_PERMISSIONS) + try: + context, binding = await _bound_patient_for_order( + account, external_account_id, conversation_external_id, person_id + ) + page_no = max(1, int(page_no)) + page_size = max(1, min(int(page_size), 50)) + keyword = str(keyword or "").strip()[:80] + client = _order_client(str(account["tenant_id"])) + result = await asyncio.to_thread( + client.list_orders, + diagnosis_id=int(binding.get("diagnosis_id") or 0), + keyword=keyword, page_no=page_no, page_size=page_size, + start_date=str(start_date or "").strip(), end_date=str(end_date or "").strip(), + ) + bound_diagnosis = int(binding.get("diagnosis_id") or 0) + rows = [ + row for row in list(result.get("items") or []) + if isinstance(row, dict) + and (bound_diagnosis <= 0 or _desktop_order_diagnosis_id(row) == bound_diagnosis) + ] + queried_at = _query_time() + items = [sanitize_order(row, queried_at=queried_at) for row in rows] + hint = _order_list_hint(items) + latest_tracking = None + tracking_error: dict[str, str] = {} + # 只有一笔订单时直接把物流带回去,模型不需要再猜要不要查、查哪一笔。 + if hint["single_order_id"] and access["can_track"]: + only = items[0] + if only.get("has_tracking_number"): + latest_tracking, tracking_error = await _tracking_for_order( + client, rows[0], hint["single_order_id"], queried_at + ) + else: + tracking_error = { + "code": "tracking_number_missing", + "message": "该订单还没有快递单号,请告知客户正在核实发货情况", + } + _audit_order_query( + account, request, "archive.order.list", binding, + conversation_external_id=conversation_external_id, + outcome=f"count={len(items)}", + ) + return { + "ok": True, + "patient": { + "patient_id": int(binding.get("patient_id") or 0), + "diagnosis_id": bound_diagnosis, + }, + "items": items, + # Never expose the upstream unfiltered count. + "total": len(items), + "page_no": page_no, + "page_size": page_size, + "queried_at": queried_at, + "binding_id": str(binding.get("id") or ""), + "context_available": bool(context.get("available")), + **hint, + "latest_tracking": latest_tracking, + "tracking_error": tracking_error, + } + except Exception as exc: + raise _http_error(exc) from exc + + @app.get("/api/v2/archive/desktop/patient-orders/summary") + async def desktop_patient_order_summary( + request: Request, + external_account_id: str, + conversation_external_id: str, + person_id: str = "", + account: Any = Depends(current_desktop), + ) -> dict[str, Any]: + """桌面端会话面板用的概览:能力状态、最近订单、最新物流和查询留痕。 + + 与其它订单接口不同,这里对“未绑定 / 无权限”不抛错,而是把原因返回给界面 + 展示;真正的订单数据仍然只在绑定有效且有权限时才会查询。 + """ + access = _desktop_order_access(account, ZYT_ORDER_LIST_PERMISSIONS) + base: dict[str, Any] = { + "ok": True, + "capability": { + "enabled": False, + "bound": False, + "reason": str(access["reason"] or ""), + "can_track": bool(access["can_track"]), + "can_detail": bool(access["can_detail"]), + }, + "recent_orders": [], + "latest_tracking": None, + "tracking_error": {}, + "logs": _order_query_logs(account, conversation_external_id), + "queried_at": _query_time(), + } + if not access["can_query"]: + return base + try: + _context, binding = await _bound_patient_for_order( + account, external_account_id, conversation_external_id, person_id + ) + except HTTPException as exc: + detail = exc.detail if isinstance(exc.detail, dict) else {"message": str(exc.detail)} + base["capability"]["reason"] = str(detail.get("message") or "当前会话尚未绑定患者") + base["capability"]["code"] = str(detail.get("code") or "") + return base + except Exception as exc: + raise _http_error(exc) from exc + base["capability"].update({"enabled": True, "bound": True, "reason": ""}) + try: + client = _order_client(str(account["tenant_id"])) + result = await asyncio.to_thread( + client.list_orders, + diagnosis_id=int(binding.get("diagnosis_id") or 0), page_no=1, page_size=5, + ) + bound_diagnosis = int(binding.get("diagnosis_id") or 0) + rows = [ + row for row in list(result.get("items") or []) + if isinstance(row, dict) + and (bound_diagnosis <= 0 or _desktop_order_diagnosis_id(row) == bound_diagnosis) + ][:3] + queried_at = _query_time() + items = [sanitize_order(row, queried_at=queried_at) for row in rows] + base["recent_orders"] = items + base["queried_at"] = queried_at + shipped = next( + (index for index, item in enumerate(items) if item.get("has_tracking_number")), + None, + ) + if shipped is not None and access["can_track"]: + base["latest_tracking"], base["tracking_error"] = await _tracking_for_order( + client, rows[shipped], int(items[shipped]["order_id"]), queried_at + ) + if base["latest_tracking"] is not None: + base["latest_tracking"]["order_id"] = int(items[shipped]["order_id"]) + _audit_order_query( + account, request, "archive.order.summary", binding, + conversation_external_id=conversation_external_id, + outcome=f"count={len(items)}", + ) + base["logs"] = _order_query_logs(account, conversation_external_id) + except Exception as exc: + raise _http_error(exc) from exc + return base + + @app.get("/api/v2/archive/desktop/patient-orders/{order_id}") + async def desktop_patient_order_detail( + order_id: int, + request: Request, + external_account_id: str, + conversation_external_id: str, + person_id: str = "", + account: Any = Depends(current_desktop), + ) -> dict[str, Any]: + _require_desktop_order_access(account, ZYT_ORDER_DETAIL_PERMISSIONS) + try: + _context, binding = await _bound_patient_for_order( + account, external_account_id, conversation_external_id, person_id + ) + client = _order_client(str(account["tenant_id"])) + try: + order = await _order_for_binding(client, binding, order_id) + except ZytOrderNotFound: + _audit_order_query( + account, request, "archive.order.detail", binding, order_id, + conversation_external_id=conversation_external_id, outcome="rejected", + ) + raise + queried_at = _query_time() + _audit_order_query( + account, request, "archive.order.detail", binding, order_id, + conversation_external_id=conversation_external_id, + ) + return {"ok": True, "order": sanitize_order(order, queried_at=queried_at)} + except Exception as exc: + raise _http_error(exc) from exc + + @app.get("/api/v2/archive/desktop/patient-orders/{order_id}/tracking") + async def desktop_patient_order_tracking( + order_id: int, + request: Request, + external_account_id: str, + conversation_external_id: str, + person_id: str = "", + account: Any = Depends(current_desktop), + ) -> dict[str, Any]: + _require_desktop_order_access( + account, ZYT_ORDER_DETAIL_PERMISSIONS | ZYT_ORDER_TRACKING_PERMISSIONS + ) + try: + _context, binding = await _bound_patient_for_order( + account, external_account_id, conversation_external_id, person_id + ) + client = _order_client(str(account["tenant_id"])) + order = await _order_for_binding(client, binding, order_id) + queried_at = _query_time() + safe = sanitize_order(order, queried_at=queried_at) + if not safe.get("has_tracking_number"): + _audit_order_query( + account, request, "archive.order.tracking", binding, order_id, + conversation_external_id=conversation_external_id, + outcome="tracking_number_missing", + ) + raise ZytOrderTrackingUnavailable( + "该订单还没有快递单号,请告知客户正在核实发货情况", + code="tracking_number_missing", + ) + latest_tracking, tracking_error = await _tracking_for_order( + client, order, order_id, queried_at + ) + if latest_tracking is None: + _audit_order_query( + account, request, "archive.order.tracking", binding, order_id, + conversation_external_id=conversation_external_id, + outcome=str(tracking_error.get("code") or "tracking_unavailable"), + ) + raise ZytOrderTrackingUnavailable( + str(tracking_error.get("message") or "物流服务暂时查不到轨迹"), + code=str(tracking_error.get("code") or "tracking_unavailable"), + ) + _audit_order_query( + account, request, "archive.order.tracking", binding, order_id, + conversation_external_id=conversation_external_id, + ) + return { + "ok": True, + "order_id": int(order_id), + "order_no": safe.get("order_no", ""), + "express_company": safe.get("express_company", ""), + "tracking_number_masked": safe.get("tracking_number_masked", ""), + "fulfillment_status": safe.get("fulfillment_status", ""), + "latest_tracking": latest_tracking, + "queried_at": queried_at, + } + except Exception as exc: + raise _http_error(exc) from exc + + @app.get("/api/v2/archive/desktop/patient-orders/{order_id}/audit-status") + async def desktop_patient_order_audit_status( + order_id: int, + request: Request, + external_account_id: str, + conversation_external_id: str, + person_id: str = "", + account: Any = Depends(current_desktop), + ) -> dict[str, Any]: + _require_desktop_order_access(account, ZYT_ORDER_DETAIL_PERMISSIONS) + try: + _context, binding = await _bound_patient_for_order( + account, external_account_id, conversation_external_id, person_id + ) + client = _order_client(str(account["tenant_id"])) + order = await _order_for_binding(client, binding, order_id) + queried_at = _query_time() + safe = sanitize_order(order, queried_at=queried_at) + _audit_order_query( + account, request, "archive.order.audit_status", binding, order_id, + conversation_external_id=conversation_external_id, + ) + return { + "ok": True, + "order_id": int(order_id), + "order_no": safe.get("order_no", ""), + "prescription_audit": safe["prescription_audit"], + "payment_audit": safe["payment_audit"], + "fulfillment_status": safe["fulfillment_status"], + "queried_at": queried_at, + } + except Exception as exc: + raise _http_error(exc) from exc + @app.get("/api/v2/archive/desktop/patients/search") async def desktop_search_zyt_patients( keyword: str, @@ -531,6 +1131,15 @@ def register_archive_routes( status_code=403, detail="当前 ZYT 账号无权访问该患者,不能建立绑定", ) + if body.diagnosis_id: + verified_diagnosis = await _diagnosis_visible_to_patient( + tenant, body.patient_id, body.diagnosis_id + ) + if verified_diagnosis is None: + raise HTTPException( + status_code=403, + detail="所选诊单不属于该患者,不能建立绑定", + ) context = await asyncio.to_thread(store.conversation_patient_context, body.external_account_id, body.conversation_external_id, diff --git a/wechat_rpa/archive_store.py b/wechat_rpa/archive_store.py index 1dd604e72..f05bbcee5 100644 --- a/wechat_rpa/archive_store.py +++ b/wechat_rpa/archive_store.py @@ -25,7 +25,7 @@ import zipfile from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable, Iterator -from urllib.parse import quote, urlparse +from urllib.parse import quote, unquote, urlparse from archive_content_parser import ( decode_hex_protobuf_text, @@ -44,6 +44,7 @@ RELEASE_TENANT = "system-release" ALL_TENANTS = "*" MAX_DIRECT_UPLOAD_BYTES = 5 * 1024 * 1024 * 1024 MAX_RELEASE_PACKAGE_BYTES = 1024 * 1024 * 1024 +RELEASE_DOWNLOAD_PREFIX = "/api/v2/desktop/update/cos-packages/" MULTIPART_THRESHOLD_BYTES = 8 * 1024 * 1024 MULTIPART_PART_BYTES = 2 * 1024 * 1024 EXCEL_SHEET_DATA_ROWS = 900_000 @@ -182,6 +183,7 @@ CREATE TABLE IF NOT EXISTS archive_message ( message_type TEXT NOT NULL DEFAULT 'unknown', content TEXT NOT NULL DEFAULT '', direction TEXT NOT NULL DEFAULT 'unknown', + message_origin TEXT NOT NULL DEFAULT 'unknown', status TEXT NOT NULL DEFAULT 'normal', sent_at TEXT NOT NULL, sent_at_epoch INTEGER, @@ -730,6 +732,9 @@ class ArchiveStore: now = utc_now() with self.database.connect() as db: db.executescript(SCHEMA) + message_columns = {row[1] for row in db.execute('PRAGMA table_info(archive_message)')} + if 'message_origin' not in message_columns: + db.execute("ALTER TABLE archive_message ADD COLUMN message_origin TEXT NOT NULL DEFAULT 'unknown'") storage_columns = { row[1] for row in db.execute("PRAGMA table_info(archive_storage_config)") } @@ -1187,17 +1192,17 @@ class ArchiveStore: "required_headers": headers, } - @staticmethod - def _release_package_payload(payload: dict[str, Any]) -> dict[str, Any]: + def _release_package_payload(self, payload: dict[str, Any]) -> dict[str, Any]: media = payload.get("media") if isinstance(payload.get("media"), dict) else {} media_id = str(media.get("id") or "") filename = Path(str(media.get("original_filename") or "update.exe")).name return { **payload, "file_name": filename, - "download_url": ( - f"/api/v2/desktop/update/cos-packages/{media_id}/{quote(filename)}" - ), + "package_media_id": media_id, + "download_url": self.release_package_download_url(media_id, filename) + if media.get("status") == "ready" else "", + "download_url_expires_in": 900 if media.get("status") == "ready" else 0, "package_sha256": str(media.get("sha256") or ""), "package_size": int(media.get("size_bytes") or 0), } @@ -1249,6 +1254,50 @@ class ArchiveStore: raise KeyError("升级包不存在或尚未上传完成") return self.media_download_url(media_id, expires, RELEASE_TENANT) + @staticmethod + def release_package_identity(download_url: str) -> tuple[str, str] | None: + """Recognize the durable reference used by old release records.""" + value = str(download_url or "").strip() + if not value.startswith(RELEASE_DOWNLOAD_PREFIX): + return None + parts = value[len(RELEASE_DOWNLOAD_PREFIX):].split("/", 1) + if len(parts) != 2 or not parts[0] or not parts[1]: + raise ValueError("COS 安装包标识无效") + return parts[0], unquote(parts[1]) + + def release_package_reference(self, media_id: str, digest: str, size: int) -> str: + with self.database.connect() as db: + row = db.execute("""SELECT * FROM archive_media_object + WHERE id=? AND tenant_id=? AND status='ready'""", (media_id, RELEASE_TENANT)).fetchone() + if row is None: + raise ValueError("COS 安装包不存在或尚未上传完成,请重新上传") + if str(digest or '').lower() != row['sha256'] or int(size or 0) != row['size_bytes']: + raise ValueError("安装包 SHA256 或大小与已上传 COS 文件不一致") + return RELEASE_DOWNLOAD_PREFIX + str(row['id']) + '/' + quote(row['original_filename']) + + def normalize_release_download(self, download_url: str, digest: str, size: int, media_id: str = "") -> str: + """Persist an artifact reference, never the expiring COS signature.""" + value = str(download_url or "").strip() + identity = self.release_package_identity(value) + if media_id or identity: + reference = self.release_package_reference(media_id or identity[0], digest, size) + if identity and reference != value: + raise ValueError("COS 安装包地址与所选文件不一致") + return reference + # A page opened before this API upgrade does not send package_media_id. + # Match its signed URL by verified hash AND exact COS host/object path. + if value and digest: + with self.database.connect() as db: + row = db.execute("""SELECT * FROM archive_media_object + WHERE tenant_id=? AND sha256=? AND status='ready'""", (RELEASE_TENANT, digest.lower())).fetchone() + if row is not None: + parsed = urlparse(value) + if unquote(parsed.path).lstrip('/') == row['object_key']: + current = urlparse(self.release_package_download_url(row['id'], row['original_filename'])) + if parsed.scheme == current.scheme and parsed.netloc == current.netloc: + return self.release_package_reference(row['id'], digest, size) + return value + def _prepare_multipart_media( self, config: dict[str, Any], @@ -2048,6 +2097,20 @@ class ArchiveStore: for message in rows: try: raw = dict(message) + if raw.get('origin_only') is True: + origin = str(raw.get('message_origin') or 'unknown') + conv_id = str((raw.get('conversation') or {}).get('external_id') or '') + if origin not in {'ai', 'human', 'system'} or not conv_id: + raise ValueError('消息来源或会话身份无效') + dedup = sha256_text(f"{tenant}\x1f{account['id']}\x1f{self._source_message_key(raw)}") + changed = db.execute("""UPDATE archive_message SET message_origin=?,updated_at=? + WHERE tenant_id=? AND source_account_id=? AND dedup_key=? AND direction='outbound' + AND conversation_id IN (SELECT id FROM archive_conversation WHERE tenant_id=? AND source_account_id=? AND external_id=?) + AND (message_origin='unknown' OR ? IN ('ai','system'))""", + (origin, now, tenant, account['id'], dedup, tenant, account['id'], conv_id, origin)).rowcount + duplicates += changed + skipped += int(not changed) + continue conv_values = dict(raw.get("conversation") or {}) if not conv_values: conv_values = { @@ -2183,6 +2246,11 @@ class ArchiveStore: (new_id(), message_id, status, content, raw_id, now), ) inserted += 1 + origin = str(raw.get('message_origin') or 'unknown') + if origin in {'ai', 'human', 'system'} and raw.get('direction') == 'outbound': + # Never let a legacy/unknown re-import erase a known source. + db.execute("UPDATE archive_message SET message_origin=?,updated_at=? WHERE id=? AND (message_origin='unknown' OR ? IN ('ai','system'))", + (origin, now, message_id, origin)) parent = str( raw.get("parent_source_message_id") or raw.get("parent_message_id") or "" @@ -3150,6 +3218,7 @@ class ArchiveStore: has_attachment=bool(attachments_by_message[str(row["id"])]), ), "direction": row["direction"], + "message_origin": 'customer' if row['direction'] == 'inbound' else row['message_origin'], "status": row["status"], "sent_at": row["sent_at"], "sequence_no": row["sequence_no"], diff --git a/wechat_rpa/remote_api.py b/wechat_rpa/remote_api.py new file mode 100644 index 000000000..69ad3e384 --- /dev/null +++ b/wechat_rpa/remote_api.py @@ -0,0 +1,804 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台与桌面命令通道的 HTTP 接口。 + + /api/v2/mobile/* 手机 / 平板 H5 用,ZYT 账号密码登录(终端 9,由本服务代登) + /api/v2/desktop/commands/* 桌面端长轮询领取指令、回执结果、顺带上报状态 + /api/v2/desktop/blacklist* 桌面端同步服务端会话黑名单 + +手机令牌和桌面令牌分别由 ``mobile_sessions`` 和 ``desktop_sessions`` 签发,互相 +不能冒用:手机端拿不到归档上传之类的桌面接口,桌面端也调不了手机接口。 +""" + +from __future__ import annotations + +import asyncio +import ipaddress +import os +import threading +import time +from collections import defaultdict, deque +from typing import Any, Callable + +from fastapi import Depends, FastAPI, HTTPException, Query, Request +from pydantic import BaseModel, Field + +import zyt_auth +from remote_store import ( + COMMAND_TYPES, + MOBILE_IDENTITY_RECHECK_SECONDS, + PERM_BLACKLIST_ADD, + PERM_BLACKLIST_REMOVE, + PERM_REMOTE_ALL, + PERM_REMOTE_CONTROL, + RemoteStore, + account_label, + account_permissions, + valid_device_id, +) + +# 桌面端长轮询最长挂起时间。uvicorn 停机时会等在途请求结束,这里必须明显短于 +# compose 里 20 秒的 stop_grace_period,否则每次发版都会被强杀;指令本身靠进程内 +# 通知即时送达,挂起时长只影响"没事时多久回来报一次状态"。 +MAX_POLL_WAIT_SECONDS = 15 +# 挂起期间多久回库复查一次;进程内通知失效(多进程部署)时的兜底。 +POLL_RECHECK_SECONDS = 5 + + +def _error(status_code: int, code: str, message: str, **extra: Any) -> HTTPException: + return HTTPException(status_code=status_code, detail={"code": code, "message": message, **extra}) + + +def _bearer(request: Request) -> str: + raw = request.headers.get("authorization", "") + return raw[7:].strip() if raw.lower().startswith("bearer ") else "" + + +def trusted_client_ip(request: Request) -> str: + """做安全判断用的客户端地址。 + + X-Forwarded-For 的第一段是客户端自己填的,可以伪造;X-Real-IP 由 Nginx 按 + $remote_addr 覆盖写入,API 端口又只监听本机回环,只有 Nginx 能连进来。 + """ + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return real_ip + return request.client.host if request.client else "" + + +def _is_private_address(value: str) -> bool: + try: + address = ipaddress.ip_address(str(value or "").strip()) + except ValueError: + return False + return address.is_private or address.is_loopback or address.is_link_local + + +def request_scheme(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-proto", "").split(",")[0].strip().lower() + return forwarded or request.url.scheme.lower() + + +def https_required(request: Request) -> bool: + """手机端登录是否必须走 HTTPS。 + + MOBILE_REQUIRE_HTTPS:never(默认,内外网都允许明文 HTTP 登录,按用户要求)/ + auto(公网地址必须 HTTPS,内网可用 HTTP)/ always。配好 HTTPS 入口后建议改成 + auto:明文 HTTP 下 ZYT 密码和手机会话令牌在公网上可被截获。 + + auto 模式下没有 X-Real-IP 时按"来源不明"处理:生产环境里直连 API 的对端是 Docker + 网桥(172.x,看起来像内网),代理漏配头就会把公网请求误当内网放行。只有本机回环 + (开发时直接访问 127.0.0.1)才在没有这个头时被信任。 + """ + policy = os.environ.get("MOBILE_REQUIRE_HTTPS", "never").strip().lower() + if policy == "never" or request_scheme(request) == "https": + return False + if policy == "always": + return True + real_ip = request.headers.get("x-real-ip", "").strip() + if real_ip: + return not _is_private_address(real_ip) + peer = request.client.host if request.client else "" + try: + return not ipaddress.ip_address(peer).is_loopback + except ValueError: + return True + + +class LoginAttempt: + __slots__ = ("ip", "account", "finished") + + def __init__(self, ip: str, account: str): + self.ip, self.account, self.finished = ip, account, False + + +class MobileLoginGuard: + """手机登录限流。 + + ZYT 按请求来源 IP 计数锁定(默认 30 分钟内错 5 次锁 30 分钟),而手机端的 + 登录全部由本服务代发,在 ZYT 看来都来自服务器出口 IP——如果公司电脑也走同一 + 个出口,锁住的就是全公司的 ZYT 登录。所以这里除了按账号、按客户端 IP 限流, + 还给"真正发到 ZYT 的密码错误"设了一个全局预算,并在 ZYT 已锁定时直接短路。 + + 限流必须把"正在进行"的尝试也算进去:先检查、等 ZYT 回来再记账的话,一次并发 + 发来十几个请求会全部通过检查,一起打到 ZYT 上。`begin()` 在锁里占位, + `finish()` 按结果转成失败记录或释放。 + """ + + def __init__( + self, *, account_limit: int = 5, account_window: int = 600, + ip_limit: int = 10, ip_window: int = 600, + upstream_budget: int | None = None, upstream_window: int = 1800, + ): + if upstream_budget is None: + try: + upstream_budget = int(os.environ.get("MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET", "4")) + except ValueError: + upstream_budget = 4 + self.account_limit, self.account_window = account_limit, account_window + self.ip_limit, self.ip_window = ip_limit, ip_window + self.upstream_budget, self.upstream_window = max(0, upstream_budget), upstream_window + self._accounts: dict[str, deque[float]] = {} + self._ips: dict[str, deque[float]] = {} + self._upstream: deque[float] = deque() + self._pending_accounts: dict[str, int] = defaultdict(int) + self._pending_ips: dict[str, int] = defaultdict(int) + self._pending_upstream = 0 + self._locked_until = 0.0 + self._lock = threading.Lock() + + @staticmethod + def _recent(book: dict[str, deque[float]], key: str, window: int, now: float) -> deque[float]: + """剪掉过期记录;空了就把这个键删掉,随机账号名刷不大内存。""" + items = book.get(key) + if items is None: + return deque() + while items and items[0] <= now - window: + items.popleft() + if not items: + del book[key] + return items + + @staticmethod + def _minutes(seconds: float) -> int: + return max(1, int((seconds + 59) // 60)) + + def begin(self, ip: str, account: str) -> LoginAttempt: + now = time.time() + with self._lock: + if self._locked_until > now: + wait = self._locked_until - now + raise _error( + 429, "zyt_locked", + f"ZYT 已暂时锁定登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + failures = self._recent(self._accounts, account, self.account_window, now) + if len(failures) >= self.account_limit: + wait = failures[0] + self.account_window - now + raise _error( + 429, "rate_limited", + f"该账号登录失败次数过多,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(failures) + self._pending_accounts.get(account, 0) >= self.account_limit: + raise _error(429, "rate_limited", "该账号正在登录,请稍候几秒再试", retry_after=5) + failures = self._recent(self._ips, ip, self.ip_window, now) + if len(failures) + self._pending_ips.get(ip, 0) >= self.ip_limit: + wait = (failures[0] + self.ip_window - now) if failures else 5 + raise _error( + 429, "rate_limited", + f"登录尝试过于频繁,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + while self._upstream and self._upstream[0] <= now - self.upstream_window: + self._upstream.popleft() + if self.upstream_budget: + if len(self._upstream) >= self.upstream_budget: + wait = self._upstream[0] + self.upstream_window - now + raise _error( + 429, "upstream_budget", + "近期手机端密码错误次数较多,为避免 ZYT 锁定服务器出口 IP," + f"已暂停手机登录,请 {self._minutes(wait)} 分钟后再试", + retry_after=int(wait), + ) + if len(self._upstream) + self._pending_upstream >= self.upstream_budget: + raise _error(429, "rate_limited", "同时登录的人较多,请几秒后再试", retry_after=5) + self._pending_accounts[account] += 1 + self._pending_ips[ip] += 1 + self._pending_upstream += 1 + return LoginAttempt(ip, account) + + def finish(self, attempt: LoginAttempt, outcome: str) -> None: + """outcome:credentials(密码错)、unknown_account(账号不存在)、success(ZYT 认可了密码)、 + other(网络、终端未开通等,不记账)。""" + if attempt.finished: + return + attempt.finished = True + now = time.time() + with self._lock: + for book, key in ((self._pending_accounts, attempt.account), (self._pending_ips, attempt.ip)): + book[key] -= 1 + if book[key] <= 0: + del book[key] + self._pending_upstream = max(0, self._pending_upstream - 1) + if outcome in ("credentials", "unknown_account"): + self._accounts.setdefault(attempt.account, deque()).append(now) + self._ips.setdefault(attempt.ip, deque()).append(now) + # ZYT 只给"账号存在、密码不对"累计出口 IP 的失败次数。 + if outcome == "credentials": + self._upstream.append(now) + elif outcome == "success": + self._accounts.pop(attempt.account, None) + # 密码校验通过时 ZYT 会清零该出口 IP 的失败计数(AdminAccountSafeCache::relieve)。 + self._upstream.clear() + + def locked(self, seconds: int) -> None: + with self._lock: + self._locked_until = max(self._locked_until, time.time() + max(60, int(seconds))) + + +class ZytMobileAuth: + """默认实现:真的去调 ZYT。测试里换成假的。""" + + def __init__(self, api_url: Callable[[], str]): + self._api_url = api_url + + def login(self, account: str, password: str) -> str: + return zyt_auth.login_admin( + account, password, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def identity(self, token: str) -> dict[str, Any]: + return zyt_auth.verify_admin_session( + token, terminal=zyt_auth.WECOM_RPA_MOBILE_TERMINAL, api_url=self._api_url() + ) + + def logout(self, token: str) -> None: + zyt_auth.logout_admin(token, api_url=self._api_url()) + + +class _Waiter: + __slots__ = ("key", "loop", "event") + + def __init__(self, key: tuple[int, str]): + self.key = key + self.loop = asyncio.get_running_loop() + self.event = asyncio.Event() + + async def wait(self, timeout: float) -> None: + try: + await asyncio.wait_for(self.event.wait(), timeout=max(0.0, timeout)) + except asyncio.TimeoutError: + pass + + +class CommandNotifier: + """进程内唤醒:有新指令或黑名单变化时,立刻结束对应电脑挂着的长轮询。 + + 先登记再查库:登记和查库之间落库的指令也能把它叫醒,不会白等一个复查周期。 + 唤醒走 ``call_soon_threadsafe``,发通知的请求和挂起的长轮询不在同一个事件 + 循环(线程)里也不会漏掉。 + """ + + def __init__(self) -> None: + self._waiters: dict[tuple[int, str], set[_Waiter]] = {} + self._lock = threading.Lock() + + def register(self, account_id: int, device_id: str) -> _Waiter: + waiter = _Waiter((int(account_id), str(device_id))) + with self._lock: + self._waiters.setdefault(waiter.key, set()).add(waiter) + return waiter + + def unregister(self, waiter: _Waiter) -> None: + with self._lock: + waiters = self._waiters.get(waiter.key) + if waiters is not None: + waiters.discard(waiter) + if not waiters: + self._waiters.pop(waiter.key, None) + + def notify(self, account_id: int, device_id: str | None = None) -> None: + with self._lock: + targets = [ + waiter + for key, waiters in self._waiters.items() + if key[0] == int(account_id) and (device_id is None or key[1] == device_id) + for waiter in waiters + ] + for waiter in targets: + try: + waiter.loop.call_soon_threadsafe(waiter.event.set) + except RuntimeError: + # 事件循环已经关闭(请求刚好结束),没有人需要被叫醒。 + continue + + +class MobilePrincipal: + def __init__(self, row: dict[str, Any], token: str): + self.row = row + self.token = token + self.permissions = account_permissions(row) + + @property + def account_id(self) -> int: + return int(self.row["id"]) + + @property + def session_id(self) -> int: + return int(self.row["session_id"]) + + @property + def label(self) -> str: + return f"{account_label(self.row)}(手机)" + + def me(self) -> dict[str, Any]: + return { + "account": { + "id": self.account_id, + "nickname": str(self.row.get("nickname") or ""), + "sn": str(self.row.get("zyt_sn") or ""), + "avatar": str(self.row.get("avatar") or ""), + }, + "permissions": { + "control": self.permissions["control"], + "control_all": self.permissions["control_all"], + "blacklist_add": self.permissions["blacklist_add"], + "blacklist_remove": self.permissions["blacklist_remove"], + "root": self.permissions["root"], + "known": self.permissions["known"], + }, + "permission_codes": { + "control": PERM_REMOTE_CONTROL, + "control_all": PERM_REMOTE_ALL, + "blacklist_add": PERM_BLACKLIST_ADD, + "blacklist_remove": PERM_BLACKLIST_REMOVE, + }, + "session": { + "created_at": str(self.row.get("session_created_at") or ""), + "expires_at": int(self.row.get("session_expires_at") or 0), + }, + } + + +# ── 请求体 ─────────────────────────────────────────────────────────────────── +class MobileLoginBody(BaseModel): + account: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=1, max_length=128, repr=False) + client_name: str = Field(default="", max_length=120) + + +class CommandBody(BaseModel): + type: str = Field(min_length=1, max_length=32) + + +class BlacklistBody(BaseModel): + name: str = Field(default="", max_length=191) + account_name: str = Field(default="", max_length=191) + reason: str = Field(default="", max_length=200) + + +class StatusBody(BaseModel): + listening: bool = False + state: str = Field(default="", max_length=32) + transport: str = Field(default="", max_length=16) + wecom_connected: bool = False + pending_count: int = Field(default=0, ge=0, le=1_000_000) + app_version: str = Field(default="", max_length=64) + capabilities: list[str] = Field(default_factory=list, max_length=16) + + +class DesktopPollBody(BaseModel): + wait: int = Field(default=15, ge=0, le=60) + status: StatusBody | None = None + # 桌面端当前持有的黑名单版本;与服务端不一致时立即返回,让它去同步。 + blacklist_version: int | None = Field(default=None, ge=0) + + +class AckBody(BaseModel): + ok: bool + result: dict[str, Any] = Field(default_factory=dict) + error: str = Field(default="", max_length=500) + status: StatusBody | None = None + + +class DesktopBlacklistSyncBody(BaseModel): + add: list[dict[str, Any]] = Field(default_factory=list, max_length=500) + + +def register_remote_routes( + app: FastAPI, + database: Any, + *, + current_desktop: Callable[..., Any], + zyt_api_url: Callable[[], str], + zyt_mobile_auth: Any = None, + login_guard: MobileLoginGuard | None = None, +) -> RemoteStore: + store = RemoteStore(database) + store.initialize() + app.state.remote_store = store + notifier = CommandNotifier() + app.state.remote_notifier = notifier + guard = login_guard or MobileLoginGuard() + auth = zyt_mobile_auth or ZytMobileAuth(zyt_api_url) + identity_locks: dict[int, asyncio.Lock] = {} + + # ── 手机端鉴权 ─────────────────────────────────────────────────────── + async def _recheck_identity(row: dict[str, Any], request: Request) -> dict[str, Any] | None: + # 同一会话并发的几个请求只复核一次;用完即删,锁表不随会话数增长。 + session_id = int(row["session_id"]) + lock = identity_locks.setdefault(session_id, asyncio.Lock()) + try: + async with lock: + return await _recheck_identity_locked(session_id, request) + finally: + if not lock.locked() and identity_locks.get(session_id) is lock: + identity_locks.pop(session_id, None) + + async def _recheck_identity_locked(session_id: int, request: Request) -> dict[str, Any] | None: + fresh = await asyncio.to_thread(store.mobile_session, _bearer(request)) + if fresh is None: + return None + if time.time() - int(fresh["identity_checked_at"] or 0) < MOBILE_IDENTITY_RECHECK_SECONDS: + return fresh + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, session_id) + try: + if not zyt_token: + raise zyt_auth.ZytLoginError("ZYT 会话缺失", kind="rejected") + identity = await asyncio.to_thread(auth.identity, zyt_token) + if str(identity.get("user_id") or "") != str(fresh["zyt_user_id"]): + raise zyt_auth.ZytLoginError("ZYT 身份与手机会话不一致", kind="rejected") + except zyt_auth.ZytAuthError as exc: + if getattr(exc, "kind", "") == "unavailable": + # ZYT 暂时连不上不算下线:沿用上次同步的权限,一分钟后再试。 + await asyncio.to_thread(store.mark_identity_checked, session_id, retry_in=60) + return fresh + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="zyt_invalid", ip=trusted_client_ip(request), + ) + return None + try: + await asyncio.to_thread(store.refresh_identity, session_id, identity) + except PermissionError: + await asyncio.to_thread( + store.revoke_mobile_session, session_id=session_id, + reason="account_disabled", ip=trusted_client_ip(request), + ) + return None + return await asyncio.to_thread(store.mobile_session, _bearer(request)) + + async def current_mobile(request: Request) -> MobilePrincipal: + token = _bearer(request) + row = await asyncio.to_thread(store.mobile_session, token) if token else None + if row is not None and time.time() - int(row["identity_checked_at"] or 0) >= MOBILE_IDENTITY_RECHECK_SECONDS: + row = await _recheck_identity(row, request) + if row is None: + raise _error(401, "session_expired", "登录已失效,请重新登录") + return MobilePrincipal(row, token) + + def target_account(principal: MobilePrincipal, account_id: int | None) -> dict[str, Any]: + if account_id is None or int(account_id) == principal.account_id: + return principal.row + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", "只能查看和控制自己的电脑") + account = store.account(int(account_id)) + if account is None or str(account.get("status")) != "active": + raise _error(404, "account_not_found", "客服账号不存在或已停用") + return account + + # ── 手机端:登录 ───────────────────────────────────────────────────── + @app.post("/api/v2/mobile/auth/login") + async def mobile_login(body: MobileLoginBody, request: Request) -> dict: + ip = trusted_client_ip(request) + if https_required(request): + raise _error(403, "https_required", "请通过 HTTPS 地址访问手机控制台,明文连接不能提交密码") + account_key = body.account.strip().lower() + attempt = guard.begin(ip, account_key) + outcome = "other" + try: + zyt_token = await asyncio.to_thread(auth.login, body.account.strip(), body.password) + outcome = "success" + except zyt_auth.ZytLoginError as exc: + if exc.kind in ("credentials", "unknown_account"): + outcome = exc.kind + database.audit(None, "mobile.login.failed", f"account={account_key[:64]}", ip) + raise _error(401, "invalid_credentials", "账号或密码错误") from exc + if exc.kind == "password_change_required": + outcome = "success" + raise _error(403, "password_change_required", str(exc)) from exc + if exc.kind == "locked": + guard.locked(exc.retry_after or 1800) + database.audit(None, "mobile.login.locked", f"account={account_key[:64]}", ip) + raise _error(429, "zyt_locked", str(exc), retry_after=exc.retry_after) from exc + if exc.kind == "disabled": + raise _error(403, "account_disabled", str(exc)) from exc + if exc.kind == "terminal_unsupported": + raise _error(503, "terminal_unsupported", str(exc)) from exc + if exc.kind == "unavailable": + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + raise _error(400, "login_rejected", str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + raise _error(502, "zyt_unavailable", "ZYT 登录服务暂不可用,请稍后重试") from exc + finally: + guard.finish(attempt, outcome) + try: + identity = await asyncio.to_thread(auth.identity, zyt_token) + account = await asyncio.to_thread(store.upsert_account, identity) + except zyt_auth.ZytLoginError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + status_code = {"terminal_unsupported": 503, "disabled": 403}.get(exc.kind, 502) + code = {"terminal_unsupported": "terminal_unsupported", + "disabled": "account_disabled"}.get(exc.kind, "zyt_unavailable") + raise _error(status_code, code, str(exc)) from exc + except zyt_auth.ZytAuthError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + except PermissionError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(403, "account_disabled", str(exc)) from exc + except ValueError as exc: + await asyncio.to_thread(auth.logout, zyt_token) + raise _error(502, "zyt_unavailable", str(exc)) from exc + token, session = await asyncio.to_thread( + store.create_mobile_session, int(account["id"]), zyt_token=zyt_token, + client_name=body.client_name, user_agent=request.headers.get("user-agent", ""), ip=ip, + ) + row = await asyncio.to_thread(store.mobile_session, token) + principal = MobilePrincipal(row, token) + return {"access_token": token, "expires_at": session["expires_at"], "me": principal.me()} + + @app.post("/api/v2/mobile/auth/logout") + async def mobile_logout(request: Request, principal: MobilePrincipal = Depends(current_mobile)) -> dict: + zyt_token = await asyncio.to_thread(store.mobile_zyt_token, principal.session_id) + await asyncio.to_thread( + store.revoke_mobile_session, session_id=principal.session_id, reason="logout", + ip=trusted_client_ip(request), + ) + if zyt_token: + await asyncio.to_thread(auth.logout, zyt_token) + return {"ok": True} + + @app.get("/api/v2/mobile/me") + async def mobile_me(principal: MobilePrincipal = Depends(current_mobile)) -> dict: + return principal.me() + + # ── 手机端:电脑 ───────────────────────────────────────────────────── + @app.get("/api/v2/mobile/accounts") + async def mobile_accounts( + q: str = Query(default="", max_length=64), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + if not principal.permissions["control_all"]: + raise _error(403, "forbidden", f"查看全部客服电脑需要 ZYT 权限 {PERM_REMOTE_ALL}") + return {"items": await asyncio.to_thread(store.accounts_overview, q)} + + @app.get("/api/v2/mobile/desktops") + async def mobile_desktops( + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + items = await asyncio.to_thread(store.devices, int(account["id"])) + return { + "account": { + "id": int(account["id"]), + "nickname": str(account.get("nickname") or ""), + "sn": str(account.get("zyt_sn") or ""), + }, + "items": items, + "server_time": int(time.time()), + } + + @app.post("/api/v2/mobile/desktops/{device_id}/commands") + async def mobile_create_command( + device_id: str, body: CommandBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + if not principal.permissions["control"]: + raise _error(403, "forbidden", f"远程启停需要 ZYT 权限 {PERM_REMOTE_CONTROL}") + if body.type not in COMMAND_TYPES: + raise _error(400, "invalid_command", "不支持的指令") + try: + device_id = valid_device_id(device_id) + except ValueError as exc: + raise _error(404, "device_not_found", "电脑不存在") from exc + device = await asyncio.to_thread(store.device, int(account["id"]), device_id) + if device is None: + raise _error(404, "device_not_found", "电脑不存在或已被管理端下线") + if not device["online"]: + raise _error(409, "device_offline", "电脑离线:客服软件没有运行,无法远程拉起") + if not device["remote_capable"]: + raise _error(409, "client_outdated", "该电脑上的客服软件版本过低,请升级后再远程控制") + try: + command = await asyncio.to_thread( + store.create_command, account_id=int(account["id"]), device_id=device_id, + command_type=body.type, actor_account_id=principal.account_id, + actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "device_not_found", str(exc)) from exc + notifier.notify(int(account["id"]), device_id) + return {"command": command} + + @app.get("/api/v2/mobile/commands/{command_id}") + async def mobile_command( + command_id: str, principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + command = await asyncio.to_thread(store.command, command_id) + if command is None: + raise _error(404, "command_not_found", "指令不存在") + if command["account_id"] != principal.account_id and not principal.permissions["control_all"]: + raise _error(404, "command_not_found", "指令不存在") + command.pop("account_id", None) + return {"command": command} + + # ── 手机端:会话与黑名单 ───────────────────────────────────────────── + @app.get("/api/v2/mobile/conversations") + async def mobile_conversations( + account_id: int | None = Query(default=None, ge=1), + q: str = Query(default="", max_length=100), + scope: str = Query(default="all", pattern="^(all|blocked)$"), + cursor: str = Query(default="", max_length=200), + limit: int = Query(default=30, ge=1, le=100), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + account = target_account(principal, account_id) + try: + return await asyncio.to_thread( + store.conversations, str(account["tenant_id"]), query=q, scope=scope, + cursor=cursor, limit=limit, + ) + except ValueError as exc: + raise _error(400, "invalid_cursor", str(exc)) from exc + + @app.put("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_add( + account: str, conversation: str, body: BlacklistBody, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_add"]: + raise _error(403, "forbidden", f"加入黑名单需要 ZYT 权限 {PERM_BLACKLIST_ADD}") + try: + entry, version = await asyncio.to_thread( + store.add_blacklist, str(target["tenant_id"]), + {"account": account, "conversation": conversation, **body.model_dump()}, + source="mobile", actor_label=principal.label, + actor_account_id=principal.account_id, ip=trusted_client_ip(request), + ) + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"entry": entry, "version": version} + + @app.delete("/api/v2/mobile/blacklist/{account}/{conversation:path}") + async def mobile_blacklist_remove( + account: str, conversation: str, request: Request, + account_id: int | None = Query(default=None, ge=1), + principal: MobilePrincipal = Depends(current_mobile), + ) -> dict: + target = target_account(principal, account_id) + if not principal.permissions["blacklist_remove"]: + raise _error(403, "forbidden", f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管") + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(target["tenant_id"]), account, conversation, + source="mobile", actor_label=principal.label, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(target["id"])) + return {"ok": True, "version": version} + + # ── 桌面端:命令通道 ───────────────────────────────────────────────── + def _desktop_label(account: Any) -> str: + return f"{account_label(account)}(电脑 {str(account['device_name'] or '')[:40]})" + + async def _report(account: Any, status: StatusBody | None) -> None: + if status is None: + return + await asyncio.to_thread( + store.report_status, int(account["id"]), str(account["device_id"]), + status.model_dump(), app_version=str(account["app_version"] or ""), + ) + + @app.post("/api/v2/desktop/commands/poll") + async def desktop_poll( + body: DesktopPollBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + account_id = int(account["id"]) + device_id = str(account["device_id"]) + tenant = str(account["tenant_id"]) + await _report(account, body.status) + wait = float(min(body.wait, MAX_POLL_WAIT_SECONDS)) + deadline = time.monotonic() + wait + while True: + waiter = notifier.register(account_id, device_id) + try: + commands = await asyncio.to_thread(store.take_commands, account_id, device_id) + version = await asyncio.to_thread(store.blacklist_version, tenant) + remaining = deadline - time.monotonic() + if ( + commands + or (body.blacklist_version is not None and version != body.blacklist_version) + or remaining <= 0 + or await request.is_disconnected() + ): + break + await waiter.wait(min(remaining, POLL_RECHECK_SECONDS)) + finally: + notifier.unregister(waiter) + return {"commands": commands, "blacklist_version": version, "server_time": int(time.time())} + + @app.post("/api/v2/desktop/commands/{command_id}/ack") + async def desktop_ack( + command_id: str, body: AckBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + await _report(account, body.status) + try: + command = await asyncio.to_thread( + store.ack_command, account_id=int(account["id"]), + device_id=str(account["device_id"]), command_id=command_id, ok=body.ok, + result=body.result, error=body.error, ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "command_not_found", str(exc)) from exc + return {"ok": True, "command": command} + + @app.post("/api/v2/desktop/status") + async def desktop_status(body: StatusBody, account: Any = Depends(current_desktop)) -> dict: + await _report(account, body) + return {"ok": True} + + # ── 桌面端:黑名单 ─────────────────────────────────────────────────── + @app.get("/api/v2/desktop/blacklist") + async def desktop_blacklist(account: Any = Depends(current_desktop)) -> dict: + version, items = await asyncio.to_thread(store.blacklist, str(account["tenant_id"])) + return {"version": version, "items": items} + + @app.post("/api/v2/desktop/blacklist/sync") + async def desktop_blacklist_sync( + body: DesktopBlacklistSyncBody, request: Request, account: Any = Depends(current_desktop), + ) -> dict: + # 电脑前的人加黑名单不要求 ZYT 权限:拉黑只会让系统少发消息,是安全方向; + # 离线时加的、升级前本地就有的条目也都从这里补传。 + before = await asyncio.to_thread(store.blacklist_version, str(account["tenant_id"])) + version, items, rejected = await asyncio.to_thread( + store.sync_desktop_blacklist, str(account["tenant_id"]), body.add, + actor_label=_desktop_label(account), actor_account_id=int(account["id"]), + ip=trusted_client_ip(request), + ) + if version != before: + notifier.notify(int(account["id"])) + return {"version": version, "items": items, "rejected": rejected} + + @app.delete("/api/v2/desktop/blacklist/{account_key}/{conversation:path}") + async def desktop_blacklist_remove( + account_key: str, conversation: str, request: Request, + account: Any = Depends(current_desktop), + ) -> dict: + if not account_permissions(account)["blacklist_remove"]: + raise _error( + 403, "forbidden", + f"移出黑名单需要 ZYT 权限 {PERM_BLACKLIST_REMOVE},请联系主管在手机控制台操作", + ) + try: + version = await asyncio.to_thread( + store.remove_blacklist, str(account["tenant_id"]), account_key, conversation, + source="desktop", actor_label=_desktop_label(account), ip=trusted_client_ip(request), + ) + except LookupError as exc: + raise _error(404, "not_blocked", str(exc)) from exc + except ValueError as exc: + raise _error(400, "invalid_conversation", str(exc)) from exc + notifier.notify(int(account["id"])) + return {"ok": True, "version": version} + + return store diff --git a/wechat_rpa/remote_store.py b/wechat_rpa/remote_store.py new file mode 100644 index 000000000..350de5ed8 --- /dev/null +++ b/wechat_rpa/remote_store.py @@ -0,0 +1,1149 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台的数据层。 + +四张表,全部挂在已有的 ``desktop_accounts`` 上: + + mobile_sessions 手机端登录会话。和桌面会话分表存放:桌面会话表上有 + "每个账号只能有一个有效会话"的唯一索引,手机登录如果 + 也写进去,就会把正在值班的电脑顶下线。分表之后两种 + 令牌也天然不能互相冒用。 + desktop_commands 下发给某台电脑的一次性命令(启动 / 停止监听)。 + desktop_status 每台电脑随长轮询上报的最新运行状态。 + conversation_blacklist 服务端会话黑名单。桌面端本地库降级为缓存,协议引擎和 + 视觉流程仍只读本地库,离线时照旧生效。 + +客服电脑在内网、没有入站端口,服务端永远不主动连电脑:命令只是落库,等电脑下一次 +长轮询时带走,执行完再回执。 +""" + +from __future__ import annotations + +import json +import re +import secrets +import time +import uuid +from datetime import datetime +from typing import Any, Iterable + +from admin_backend import now_text, token_hash + +# ── ZYT 菜单权限码(在 ZYT 角色管理里勾选,客服后台只做校验)────────────── +PERM_REMOTE_CONTROL = "wecom_rpa.remote/control" +PERM_REMOTE_ALL = "wecom_rpa.remote/all" +PERM_BLACKLIST_ADD = "wecom_rpa.blacklist/add" +PERM_BLACKLIST_REMOVE = "wecom_rpa.blacklist/remove" + +COMMAND_TYPES = {"start_listening": "启动监听", "stop_listening": "停止监听"} +COMMAND_STATUS_LABELS = { + "pending": "已下发,等待电脑领取", + "delivered": "电脑已领取,正在执行", + "succeeded": "已执行", + "failed": "执行失败", + "expired": "电脑未在 10 分钟内确认,已过期", + "superseded": "已被更新的指令取代", +} +FINAL_COMMAND_STATUSES = frozenset({"succeeded", "failed", "expired", "superseded"}) +COMMAND_TTL_SECONDS = 10 * 60 +MOBILE_SESSION_LIFETIME_SECONDS = 30 * 24 * 60 * 60 +# 手机会话每 10 分钟向 ZYT 复核一次身份与权限;复核本身也会让 ZYT 顺延令牌。 +MOBILE_IDENTITY_RECHECK_SECONDS = 10 * 60 +# 桌面端每 5 秒心跳一次(/desktop/me),30 秒没有心跳就算程序没开。 +DEVICE_ONLINE_SECONDS = 30 +# 命令通道每次长轮询(最长 25 秒)都会顺带上报状态。 +STATUS_FRESH_SECONDS = 90 +CAPABILITY_COMMANDS = "commands_v1" +CAPABILITY_BLACKLIST = "blacklist_sync_v1" + +STATE_LABELS = { + "running": "监听中", + "connecting": "正在连接企业微信", + "waiting": "等待企业微信", + "verification": "等待企业微信验证", + "recovering": "自动恢复中", + "stopping": "正在停止", + "stopped": "已停止", + "error": "连接失败", +} +TRANSPORT_LABELS = {"visual": "视觉通道", "protocol": "协议通道"} +CONVERSATION_TYPE_LABELS = { + "direct_wechat": "微信联系人", + "direct_wecom": "企业微信联系人", + "group": "群聊", + "application": "应用消息", + "service": "服务通知", + "unknown": "会话", +} + +_DEVICE_ID_RE = re.compile(r"^[A-Za-z0-9._:@-]{1,128}$") +_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f]") +MAX_SYNC_ITEMS = 500 + + +def _now_epoch() -> int: + return int(time.time()) + + +def _epoch_of(text: Any) -> int | None: + value = str(text or "").strip() + if not value: + return None + try: + return int(datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()) + except ValueError: + return None + + +def _seconds_since(epoch: int | None, now: int) -> int | None: + return None if epoch is None else max(0, now - int(epoch)) + + +def valid_device_id(value: Any) -> str: + text = str(value or "").strip() + if not _DEVICE_ID_RE.fullmatch(text): + raise ValueError("设备标识不正确") + return text + + +def clean_key(value: Any, label: str, *, limit: int) -> str: + """黑名单的账号 / 会话标识:来自企业微信本地库,原样保存,只挡控制字符。""" + text = str(value or "").strip() + if not text or len(text) > limit or _CONTROL_CHARS_RE.search(text): + raise ValueError(f"{label}不正确") + return text + + +def clean_text(value: Any, *, limit: int) -> str: + return _CONTROL_CHARS_RE.sub(" ", str(value or "")).strip()[:limit] + + +def _like(value: str) -> str: + escaped = str(value).replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + return f"%{escaped}%" + + +def account_permissions(account: Any) -> dict[str, bool]: + """从 desktop_accounts 行里同步下来的 ZYT 权限推出手机端能做什么。 + + ZYT 对 root 以及"拥有全部菜单"的角色返回 ``*``。remote/all 包含了控制本人 + 电脑的能力。 + """ + try: + raw = json.loads(str(account["zyt_permissions_json"] or "[]")) + except (KeyError, IndexError, TypeError, ValueError): + raw = [] + granted = {str(item).strip().lower() for item in raw if str(item).strip()} if isinstance(raw, list) else set() + try: + root = bool(int(account["zyt_root"] or 0)) or "*" in granted + except (KeyError, IndexError, TypeError, ValueError): + root = "*" in granted + try: + known = bool(int(account["zyt_permissions_known"] or 0)) + except (KeyError, IndexError, TypeError, ValueError): + known = False + + def has(code: str) -> bool: + return root or code in granted + + return { + "root": root, + "known": known, + "control": has(PERM_REMOTE_CONTROL) or has(PERM_REMOTE_ALL), + "control_all": has(PERM_REMOTE_ALL), + "blacklist_add": has(PERM_BLACKLIST_ADD), + "blacklist_remove": has(PERM_BLACKLIST_REMOVE), + } + + +def account_label(account: Any) -> str: + try: + return str(account["nickname"] or account["zyt_sn"] or f"账号{account['id']}") + except (KeyError, IndexError): + return "未知账号" + + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS mobile_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + token_digest TEXT NOT NULL UNIQUE, + zyt_token_enc TEXT NOT NULL DEFAULT '', + client_name TEXT NOT NULL DEFAULT '', + user_agent TEXT NOT NULL DEFAULT '', + ip_address TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + last_used_at TEXT NOT NULL, + last_used_epoch INTEGER NOT NULL DEFAULT 0, + expires_at INTEGER NOT NULL, + identity_checked_at INTEGER NOT NULL DEFAULT 0, + revoked_at TEXT NOT NULL DEFAULT '', + revoke_reason TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_mobile_sessions_account + ON mobile_sessions(account_id, revoked_at, expires_at); + +CREATE TABLE IF NOT EXISTS desktop_commands ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command_id TEXT NOT NULL UNIQUE, + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + device_id TEXT NOT NULL, + type TEXT NOT NULL CHECK(type IN ('start_listening','stop_listening')), + payload_json TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'pending' + CHECK(status IN ('pending','delivered','succeeded','failed','expired','superseded')), + created_by_account_id INTEGER, + created_by TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + expires_at INTEGER NOT NULL, + delivered_at TEXT NOT NULL DEFAULT '', + delivery_count INTEGER NOT NULL DEFAULT 0, + acked_at TEXT NOT NULL DEFAULT '', + result_json TEXT NOT NULL DEFAULT '{}', + error TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_desktop_commands_target + ON desktop_commands(account_id, device_id, status, id); + +CREATE TABLE IF NOT EXISTS desktop_status ( + account_id INTEGER NOT NULL REFERENCES desktop_accounts(id) ON DELETE CASCADE, + device_id TEXT NOT NULL, + listening INTEGER NOT NULL DEFAULT 0, + state TEXT NOT NULL DEFAULT '', + transport TEXT NOT NULL DEFAULT '', + wecom_connected INTEGER NOT NULL DEFAULT 0, + pending_count INTEGER NOT NULL DEFAULT 0, + app_version TEXT NOT NULL DEFAULT '', + capabilities_json TEXT NOT NULL DEFAULT '[]', + reported_at TEXT NOT NULL, + reported_epoch INTEGER NOT NULL, + PRIMARY KEY(account_id, device_id) +); + +CREATE TABLE IF NOT EXISTS conversation_blacklist ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id TEXT NOT NULL, + account TEXT NOT NULL, + conversation TEXT NOT NULL, + display_name TEXT NOT NULL DEFAULT '', + account_name TEXT NOT NULL DEFAULT '', + reason TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT 'desktop', + created_by TEXT NOT NULL DEFAULT '', + created_by_account_id INTEGER, + created_at TEXT NOT NULL, + removed_at TEXT NOT NULL DEFAULT '', + removed_by TEXT NOT NULL DEFAULT '', + removed_source TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 0, + UNIQUE(tenant_id, account, conversation) +); +CREATE INDEX IF NOT EXISTS idx_conversation_blacklist_active + ON conversation_blacklist(tenant_id, removed_at, id DESC); +CREATE INDEX IF NOT EXISTS idx_conversation_blacklist_version + ON conversation_blacklist(tenant_id, version); +""" + + +class RemoteStore: + def __init__(self, database: Any): + self.database = database + + def initialize(self) -> None: + with self.database.connect() as db: + db.executescript(SCHEMA) + db.commit() + + # ── 账号 ───────────────────────────────────────────────────────────── + def upsert_account(self, identity: dict[str, Any]) -> dict[str, Any]: + """按 ZYT 身份落到同一条 desktop_accounts,并同步权限;不碰设备与桌面会话。 + + 没用过桌面端的主管第一次登录手机端时会新建一行,这一行在管理端的 + 「客户端账号」里显示为没有设备。 + """ + zyt_user_id = str(identity.get("user_id") or "").strip() + if not zyt_user_id: + raise ValueError("ZYT 用户标识不能为空") + permissions_value = identity.get("permissions") + permissions = sorted( + {str(item).strip() for item in permissions_value if str(item).strip()} + ) if isinstance(permissions_value, list) else [] + role_ids: list[int] = [] + for item in identity.get("role_ids") if isinstance(identity.get("role_ids"), list) else []: + try: + role_ids.append(int(item)) + except (TypeError, ValueError): + continue + root = int(bool(identity.get("root")) or "*" in permissions) + known = int(bool(identity.get("permissions_known", isinstance(permissions_value, list)))) + mobile = str(identity.get("mobile") or "") + if len(mobile) >= 7: + mobile = f"{mobile[:3]}****{mobile[-4:]}" + stamp = now_text() + tenant_id = self.database._desktop_tenant_id(zyt_user_id) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + db.execute( + """INSERT INTO desktop_accounts + (zyt_user_id,tenant_id,zyt_sn,nickname,mobile_masked,avatar, + zyt_root,zyt_role_ids_json,zyt_permissions_json,zyt_permissions_known, + status,created_at,updated_at,last_login_at,last_online_at) + VALUES (?,?,?,?,?,?,?,?,?,?,'active',?,?,'','') + ON CONFLICT(zyt_user_id) DO UPDATE SET + zyt_sn=excluded.zyt_sn,nickname=excluded.nickname, + avatar=excluded.avatar,zyt_root=excluded.zyt_root, + zyt_role_ids_json=excluded.zyt_role_ids_json, + zyt_permissions_json=excluded.zyt_permissions_json, + zyt_permissions_known=excluded.zyt_permissions_known, + updated_at=excluded.updated_at""", + ( + zyt_user_id, tenant_id, + str(identity.get("sn") or "")[:128], + str(identity.get("nickname") or "")[:191], + mobile[:32], + str(identity.get("avatar") or "")[:1024], + root, + json.dumps(sorted(set(role_ids))), + json.dumps(permissions, ensure_ascii=False), + known, stamp, stamp, + ), + ) + account = db.execute( + "SELECT * FROM desktop_accounts WHERE zyt_user_id=?", (zyt_user_id,) + ).fetchone() + if account is None or str(account["status"]) != "active": + db.rollback() + raise PermissionError("该账号已被管理端禁用,不能使用手机控制台") + db.commit() + return dict(account) + + def account(self, account_id: int) -> dict[str, Any] | None: + row = self.database.desktop_account(int(account_id)) + return dict(row) if row is not None else None + + # ── 手机会话 ───────────────────────────────────────────────────────── + def create_mobile_session( + self, account_id: int, *, zyt_token: str, client_name: str = "", + user_agent: str = "", ip: str = "", + lifetime: int = MOBILE_SESSION_LIFETIME_SECONDS, + ) -> tuple[str, dict[str, Any]]: + import secret_box + + token = secrets.token_urlsafe(48) + now = _now_epoch() + stamp = now_text() + expires_at = now + max(300, min(int(lifetime), MOBILE_SESSION_LIFETIME_SECONDS)) + encrypted = secret_box.encrypt(str(zyt_token or ""), self.database._secret_key()) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + account = db.execute( + "SELECT * FROM desktop_accounts WHERE id=? AND status='active'", (int(account_id),) + ).fetchone() + if account is None: + db.rollback() + raise PermissionError("该账号已被管理端禁用,不能使用手机控制台") + cursor = db.execute( + """INSERT INTO mobile_sessions + (account_id,token_digest,zyt_token_enc,client_name,user_agent,ip_address, + created_at,last_used_at,last_used_epoch,expires_at,identity_checked_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?)""", + ( + int(account_id), token_hash(token), encrypted, + clean_text(client_name, limit=120), clean_text(user_agent, limit=255), + str(ip or "")[:80], stamp, stamp, now, expires_at, now, + ), + ) + db.execute( + "DELETE FROM mobile_sessions WHERE expires_at'' AND revoked_at dict[str, Any] | None: + if not token: + return None + now = _now_epoch() + with self.database.connect() as db: + row = db.execute( + """SELECT a.*,s.id AS session_id,s.expires_at AS session_expires_at, + s.created_at AS session_created_at,s.identity_checked_at, + s.last_used_epoch + FROM mobile_sessions s JOIN desktop_accounts a ON a.id=s.account_id + WHERE s.token_digest=? AND s.revoked_at='' AND s.expires_at>=? + AND a.status='active'""", + (token_hash(token), now), + ).fetchone() + if row is None: + return None + if now - int(row["last_used_epoch"] or 0) >= 60: + db.execute( + "UPDATE mobile_sessions SET last_used_at=?,last_used_epoch=? WHERE id=?", + (now_text(), now, int(row["session_id"])), + ) + db.commit() + return dict(row) + + def mobile_zyt_token(self, session_id: int) -> str: + import secret_box + + with self.database.connect() as db: + row = db.execute( + "SELECT zyt_token_enc FROM mobile_sessions WHERE id=?", (int(session_id),) + ).fetchone() + if row is None or not str(row["zyt_token_enc"] or ""): + return "" + try: + return secret_box.decrypt(str(row["zyt_token_enc"]), self.database._secret_key()) + except Exception: + return "" + + def mark_identity_checked(self, session_id: int, *, retry_in: int | None = None) -> None: + """记录复核时间。ZYT 暂时连不上时用 retry_in 让下一次复核提前。""" + checked = _now_epoch() + if retry_in is not None: + checked = checked - MOBILE_IDENTITY_RECHECK_SECONDS + max(10, int(retry_in)) + with self.database.connect() as db: + db.execute( + "UPDATE mobile_sessions SET identity_checked_at=? WHERE id=?", + (checked, int(session_id)), + ) + db.commit() + + def refresh_identity(self, session_id: int, identity: dict[str, Any]) -> None: + account = self.upsert_account(identity) + with self.database.connect() as db: + db.execute( + "UPDATE mobile_sessions SET identity_checked_at=? WHERE id=? AND account_id=?", + (_now_epoch(), int(session_id), int(account["id"])), + ) + db.commit() + + def revoke_mobile_session( + self, *, token: str = "", session_id: int | None = None, reason: str = "logout", + ip: str = "", + ) -> None: + with self.database.connect() as db: + if token: + row = db.execute( + "SELECT id,account_id FROM mobile_sessions WHERE token_digest=?", + (token_hash(token),), + ).fetchone() + else: + row = db.execute( + "SELECT id,account_id FROM mobile_sessions WHERE id=?", (int(session_id or 0),) + ).fetchone() + if row is None: + return + db.execute( + """UPDATE mobile_sessions SET revoked_at=?,revoke_reason=?,zyt_token_enc='' + WHERE id=? AND revoked_at=''""", + (now_text(), str(reason)[:40], int(row["id"])), + ) + self.database._audit( + db, None, "mobile.logout" if reason == "logout" else "mobile.session.revoke", + f"account={row['account_id']} session={row['id']} reason={reason}", str(ip or ""), + ) + db.commit() + + def revoke_mobile_sessions_for_account(self, account_id: int, *, reason: str) -> int: + with self.database.connect() as db: + cursor = db.execute( + """UPDATE mobile_sessions SET revoked_at=?,revoke_reason=?,zyt_token_enc='' + WHERE account_id=? AND revoked_at=''""", + (now_text(), str(reason)[:40], int(account_id)), + ) + db.commit() + return int(cursor.rowcount or 0) + + def mobile_session_counts(self) -> dict[int, int]: + with self.database.connect() as db: + rows = db.execute( + """SELECT account_id,COUNT(*) FROM mobile_sessions + WHERE revoked_at='' AND expires_at>=? GROUP BY account_id""", + (_now_epoch(),), + ).fetchall() + return {int(row[0]): int(row[1]) for row in rows} + + # ── 设备与状态 ─────────────────────────────────────────────────────── + def report_status( + self, account_id: int, device_id: str, status: dict[str, Any], *, app_version: str = "" + ) -> None: + capabilities = status.get("capabilities") + capabilities = sorted({ + clean_text(item, limit=32) for item in capabilities if clean_text(item, limit=32) + })[:16] if isinstance(capabilities, list) else [] + try: + pending = max(0, min(int(status.get("pending_count") or 0), 1_000_000)) + except (TypeError, ValueError): + pending = 0 + now = _now_epoch() + with self.database.connect() as db: + db.execute( + """INSERT INTO desktop_status + (account_id,device_id,listening,state,transport,wecom_connected,pending_count, + app_version,capabilities_json,reported_at,reported_epoch) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(account_id,device_id) DO UPDATE SET + listening=excluded.listening,state=excluded.state, + transport=excluded.transport,wecom_connected=excluded.wecom_connected, + pending_count=excluded.pending_count,app_version=excluded.app_version, + capabilities_json=excluded.capabilities_json, + reported_at=excluded.reported_at,reported_epoch=excluded.reported_epoch""", + ( + int(account_id), valid_device_id(device_id), + int(bool(status.get("listening"))), + clean_text(status.get("state"), limit=32), + clean_text(status.get("transport"), limit=16), + int(bool(status.get("wecom_connected"))), + pending, + clean_text(status.get("app_version") or app_version, limit=64), + json.dumps(capabilities), now_text(), now, + ), + ) + db.commit() + + def devices(self, account_id: int, *, device_id: str = "") -> list[dict[str, Any]]: + now = _now_epoch() + params: list[Any] = [now, int(account_id)] + extra = "" + if device_id: + extra = " AND d.device_id=?" + params.append(valid_device_id(device_id)) + with self.database.connect() as db: + rows = db.execute( + f"""SELECT d.id AS device_row_id,d.device_id,d.device_name,d.app_version, + d.last_seen_at, + (SELECT COUNT(*) FROM desktop_sessions s + WHERE s.device_row_id=d.id AND s.revoked_at='' + AND s.expires_at>=?) AS active_sessions, + st.listening,st.state,st.transport,st.wecom_connected, + st.pending_count,st.app_version AS status_app_version, + st.capabilities_json,st.reported_at,st.reported_epoch + FROM desktop_devices d + LEFT JOIN desktop_status st + ON st.account_id=d.account_id AND st.device_id=d.device_id + WHERE d.account_id=? AND d.revoked_at=''{extra} + ORDER BY d.last_seen_at DESC,d.id DESC""", + tuple(params), + ).fetchall() + commands = {} + for row in rows: + latest = db.execute( + """SELECT * FROM desktop_commands WHERE account_id=? AND device_id=? + ORDER BY id DESC LIMIT 1""", + (int(account_id), str(row["device_id"])), + ).fetchone() + if latest is not None: + commands[str(row["device_id"])] = self._public_command(latest, now) + return [self._public_device(row, commands.get(str(row["device_id"])), now) for row in rows] + + def device(self, account_id: int, device_id: str) -> dict[str, Any] | None: + items = self.devices(account_id, device_id=device_id) + return items[0] if items else None + + @staticmethod + def _public_device(row: Any, command: dict[str, Any] | None, now: int) -> dict[str, Any]: + seen_epoch = _epoch_of(row["last_seen_at"]) + seen_seconds = _seconds_since(seen_epoch, now) + session_active = int(row["active_sessions"] or 0) > 0 + online = bool(session_active and seen_seconds is not None and seen_seconds <= DEVICE_ONLINE_SECONDS) + status = None + remote_capable = False + blacklist_sync = False + if row["reported_epoch"] is not None: + try: + capabilities = set(json.loads(str(row["capabilities_json"] or "[]"))) + except (TypeError, ValueError): + capabilities = set() + reported_seconds = _seconds_since(int(row["reported_epoch"]), now) + fresh = reported_seconds is not None and reported_seconds <= STATUS_FRESH_SECONDS + same_build = str(row["status_app_version"] or "") == str(row["app_version"] or "") + # 同一台电脑降级回旧版本时,旧的能力声明不能继续算数。 + remote_capable = CAPABILITY_COMMANDS in capabilities and (fresh or same_build) + blacklist_sync = CAPABILITY_BLACKLIST in capabilities and (fresh or same_build) + state = str(row["state"] or "") + transport = str(row["transport"] or "") + status = { + "listening": bool(row["listening"]), + "state": state, + "state_label": STATE_LABELS.get(state, state or "未知"), + "transport": transport, + "transport_label": TRANSPORT_LABELS.get(transport, transport or "未知"), + "wecom_connected": bool(row["wecom_connected"]), + "pending_count": int(row["pending_count"] or 0), + "reported_at": str(row["reported_at"] or ""), + "reported_seconds": reported_seconds, + "fresh": fresh, + } + return { + "device_id": str(row["device_id"]), + "device_name": str(row["device_name"] or ""), + "app_version": str(row["app_version"] or ""), + "online": online, + "session_active": session_active, + "last_seen_at": str(row["last_seen_at"] or ""), + "last_seen_seconds": seen_seconds, + "remote_capable": remote_capable, + "blacklist_sync": blacklist_sync, + "status": status, + "last_command": command, + } + + def accounts_overview(self, query: str = "", limit: int = 100) -> list[dict[str, Any]]: + """全部客服电脑的概况,只给有 remote/all 权限的人看。""" + params: list[Any] = [] + where = "WHERE a.status='active'" + needle = str(query or "").strip() + if needle: + where += " AND (a.nickname LIKE ? ESCAPE '\\' OR a.zyt_sn LIKE ? ESCAPE '\\')" + params.extend([_like(needle), _like(needle)]) + params.append(max(1, min(int(limit or 100), 300))) + with self.database.connect() as db: + rows = db.execute( + f"""SELECT a.id,a.nickname,a.zyt_sn,a.last_online_at, + (SELECT COUNT(*) FROM desktop_devices d + WHERE d.account_id=a.id AND d.revoked_at='') AS device_count + FROM desktop_accounts a {where} + ORDER BY a.last_online_at DESC,a.id DESC LIMIT ?""", + tuple(params), + ).fetchall() + result = [] + for row in rows: + devices = self.devices(int(row["id"])) if int(row["device_count"] or 0) else [] + result.append({ + "id": int(row["id"]), + "nickname": str(row["nickname"] or ""), + "sn": str(row["zyt_sn"] or ""), + "device_count": len(devices), + "online": any(item["online"] for item in devices), + "listening": any( + item["online"] and item["status"] and item["status"]["listening"] + for item in devices + ), + "last_online_at": str(row["last_online_at"] or ""), + }) + return result + + # ── 命令 ───────────────────────────────────────────────────────────── + @staticmethod + def _public_command(row: Any, now: int | None = None) -> dict[str, Any]: + now = _now_epoch() if now is None else now + status = str(row["status"]) + if status in {"pending", "delivered"} and int(row["expires_at"]) < now: + status = "expired" + try: + result = json.loads(str(row["result_json"] or "{}")) + except (TypeError, ValueError): + result = {} + return { + "id": str(row["command_id"]), + "type": str(row["type"]), + "type_label": COMMAND_TYPES.get(str(row["type"]), str(row["type"])), + "device_id": str(row["device_id"]), + "status": status, + "status_label": COMMAND_STATUS_LABELS.get(status, status), + "final": status in FINAL_COMMAND_STATUSES, + "created_at": str(row["created_at"]), + "created_by": str(row["created_by"] or ""), + "delivered_at": str(row["delivered_at"] or ""), + "acked_at": str(row["acked_at"] or ""), + "expires_at": int(row["expires_at"]), + "result": result if isinstance(result, dict) else {}, + "error": str(row["error"] or ""), + } + + def create_command( + self, *, account_id: int, device_id: str, command_type: str, + actor_account_id: int, actor_label: str, ip: str, + ) -> dict[str, Any]: + if command_type not in COMMAND_TYPES: + raise ValueError("不支持的指令") + device_id = valid_device_id(device_id) + now = _now_epoch() + stamp = now_text() + command_id = uuid.uuid4().hex + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + if db.execute( + """SELECT 1 FROM desktop_devices + WHERE account_id=? AND device_id=? AND revoked_at=''""", + (int(account_id), device_id), + ).fetchone() is None: + db.rollback() + raise LookupError("电脑不存在或已被管理端下线") + # 还没被电脑领走的旧指令直接作废:连点"停止、启动"时,电脑只该执行最后一次。 + # 已经领走的正在执行,不能撤回,交给电脑按顺序执行。 + db.execute( + """UPDATE desktop_commands SET status='superseded',acked_at=? + WHERE account_id=? AND device_id=? AND status='pending'""", + (stamp, int(account_id), device_id), + ) + db.execute( + """INSERT INTO desktop_commands + (command_id,account_id,device_id,type,payload_json,status, + created_by_account_id,created_by,created_at,expires_at) + VALUES (?,?,?,?,?,'pending',?,?,?,?)""", + ( + command_id, int(account_id), device_id, command_type, "{}", + int(actor_account_id), clean_text(actor_label, limit=120), stamp, + now + COMMAND_TTL_SECONDS, + ), + ) + self.database._audit( + db, None, "remote.command.create", + f"command={command_id} account={int(account_id)} device={device_id[:64]} " + f"type={command_type} by_account={int(actor_account_id)}", + str(ip or ""), + ) + db.commit() + row = db.execute( + "SELECT * FROM desktop_commands WHERE command_id=?", (command_id,) + ).fetchone() + return self._public_command(row, now) + + def command(self, command_id: str) -> dict[str, Any] | None: + with self.database.connect() as db: + row = db.execute( + "SELECT * FROM desktop_commands WHERE command_id=?", (str(command_id or ""),) + ).fetchone() + if row is None: + return None + payload = self._public_command(row) + payload["account_id"] = int(row["account_id"]) + return payload + + def has_open_commands(self, account_id: int, device_id: str) -> bool: + with self.database.connect() as db: + return db.execute( + """SELECT 1 FROM desktop_commands + WHERE account_id=? AND device_id=? AND status IN ('pending','delivered') + LIMIT 1""", + (int(account_id), str(device_id)), + ).fetchone() is not None + + def take_commands(self, account_id: int, device_id: str) -> list[dict[str, Any]]: + """把待执行指令交给电脑。 + + 领取不等于完成:电脑回执之前每次轮询都会再拿到一次,由电脑按指令 id 去重。 + 这样长轮询的响应在路上丢了,指令也不会跟着丢。 + """ + if not self.has_open_commands(account_id, device_id): + return [] + now = _now_epoch() + stamp = now_text() + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + db.execute( + """UPDATE desktop_commands SET status='expired' + WHERE account_id=? AND device_id=? AND status IN ('pending','delivered') + AND expires_at dict[str, Any]: + stamp = now_text() + now = _now_epoch() + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + """SELECT * FROM desktop_commands + WHERE command_id=? AND account_id=? AND device_id=?""", + (str(command_id or ""), int(account_id), str(device_id)), + ).fetchone() + if row is None: + db.rollback() + raise LookupError("指令不存在") + status = str(row["status"]) + if status in {"succeeded", "failed"}: + db.rollback() + return self._public_command(row, now) + if status in {"pending", "delivered"} and int(row["expires_at"]) >= now: + status = "succeeded" if ok else "failed" + elif status in {"pending", "delivered"}: + status = "expired" + safe_result = result if isinstance(result, dict) else {} + encoded = json.dumps(safe_result, ensure_ascii=False)[:4000] + db.execute( + """UPDATE desktop_commands SET status=?,acked_at=?,result_json=?,error=? + WHERE id=?""", + (status, stamp, encoded, clean_text(error, limit=500), int(row["id"])), + ) + self.database._audit( + db, None, "remote.command.ack", + f"command={row['command_id']} account={int(account_id)} status={status}", + str(ip or ""), + ) + db.commit() + row = db.execute("SELECT * FROM desktop_commands WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_command(row, now) + + # ── 黑名单 ─────────────────────────────────────────────────────────── + @staticmethod + def _public_entry(row: Any) -> dict[str, Any]: + return { + "account": str(row["account"]), + "account_name": str(row["account_name"] or ""), + "conversation": str(row["conversation"]), + "name": str(row["display_name"] or ""), + "reason": str(row["reason"] or ""), + "source": str(row["source"] or ""), + "created_by": str(row["created_by"] or ""), + "created_at": str(row["created_at"] or ""), + } + + @staticmethod + def _version(db: Any, tenant_id: str) -> int: + return int(db.execute( + "SELECT COALESCE(MAX(version),0) FROM conversation_blacklist WHERE tenant_id=?", + (tenant_id,), + ).fetchone()[0]) + + def blacklist_version(self, tenant_id: str) -> int: + with self.database.connect() as db: + return self._version(db, str(tenant_id)) + + def blacklist(self, tenant_id: str) -> tuple[int, list[dict[str, Any]]]: + with self.database.connect() as db: + rows = db.execute( + """SELECT * FROM conversation_blacklist WHERE tenant_id=? AND removed_at='' + ORDER BY account,conversation""", + (str(tenant_id),), + ).fetchall() + version = self._version(db, str(tenant_id)) + return version, [self._public_entry(row) for row in rows] + + @staticmethod + def _archive_names(db: Any, tenant_id: str, account: str, conversation: str) -> tuple[str, str]: + """补上会话名和企微账号名;归档表不存在或没有这条会话时返回空串。""" + try: + row = db.execute( + """SELECT c.name,a.display_name FROM archive_conversation c + JOIN archive_source_account a ON a.id=c.source_account_id + WHERE c.tenant_id=? AND a.external_account_id=? AND c.external_id=? + LIMIT 1""", + (tenant_id, account, conversation), + ).fetchone() + except Exception: + return "", "" + if row is None: + return "", "" + return str(row[0] or ""), str(row[1] or "") + + def _add_entry( + self, db: Any, tenant_id: str, item: dict[str, Any], *, source: str, + actor_label: str, actor_account_id: int | None, version: int, + ) -> tuple[dict[str, Any], bool]: + account = clean_key(item.get("account"), "企业微信账号", limit=191) + conversation = clean_key(item.get("conversation"), "会话", limit=512) + name = clean_text(item.get("name"), limit=191) + account_name = clean_text(item.get("account_name"), limit=191) + reason = clean_text(item.get("reason"), limit=200) + if not name or not account_name: + archive_name, archive_account = self._archive_names(db, tenant_id, account, conversation) + name = name or archive_name + account_name = account_name or archive_account + stamp = now_text() + row = db.execute( + """SELECT * FROM conversation_blacklist + WHERE tenant_id=? AND account=? AND conversation=?""", + (tenant_id, account, conversation), + ).fetchone() + if row is not None and not str(row["removed_at"] or ""): + changed = ( + (name and name != row["display_name"]) + or (account_name and account_name != row["account_name"]) + or (reason and reason != row["reason"]) + ) + if changed: + db.execute( + """UPDATE conversation_blacklist SET + display_name=CASE WHEN ?<>'' THEN ? ELSE display_name END, + account_name=CASE WHEN ?<>'' THEN ? ELSE account_name END, + reason=CASE WHEN ?<>'' THEN ? ELSE reason END, + updated_at=?,version=? WHERE id=?""", + (name, name, account_name, account_name, reason, reason, stamp, version, + int(row["id"])), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_entry(fresh), bool(changed) + if row is not None: + db.execute( + """UPDATE conversation_blacklist SET display_name=?,account_name=?,reason=?, + source=?,created_by=?,created_by_account_id=?,created_at=?, + removed_at='',removed_by='',removed_source='',updated_at=?,version=? + WHERE id=?""", + (name or row["display_name"], account_name or row["account_name"], reason, + source, clean_text(actor_label, limit=120), actor_account_id, stamp, stamp, + version, int(row["id"])), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(row["id"]),)).fetchone() + return self._public_entry(fresh), True + cursor = db.execute( + """INSERT INTO conversation_blacklist + (tenant_id,account,conversation,display_name,account_name,reason,source, + created_by,created_by_account_id,created_at,updated_at,version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + (tenant_id, account, conversation, name, account_name, reason, source, + clean_text(actor_label, limit=120), actor_account_id, stamp, stamp, version), + ) + fresh = db.execute("SELECT * FROM conversation_blacklist WHERE id=?", (int(cursor.lastrowid),)).fetchone() + return self._public_entry(fresh), True + + def add_blacklist( + self, tenant_id: str, item: dict[str, Any], *, source: str, actor_label: str, + actor_account_id: int | None, ip: str, + ) -> tuple[dict[str, Any], int]: + tenant_id = str(tenant_id) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + version = self._version(db, tenant_id) + 1 + entry, changed = self._add_entry( + db, tenant_id, item, source=source, actor_label=actor_label, + actor_account_id=actor_account_id, version=version, + ) + if changed: + self.database._audit( + db, None, "blacklist.add", + f"tenant={tenant_id} account={entry['account'][:64]} " + f"conversation={entry['conversation'][:120]} source={source} " + f"by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + else: + db.rollback() + current = self._version(db, tenant_id) + return entry, current + + def remove_blacklist( + self, tenant_id: str, account: str, conversation: str, *, source: str, + actor_label: str, ip: str, + ) -> int: + tenant_id = str(tenant_id) + account = clean_key(account, "企业微信账号", limit=191) + conversation = clean_key(conversation, "会话", limit=512) + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + """SELECT id FROM conversation_blacklist + WHERE tenant_id=? AND account=? AND conversation=? AND removed_at=''""", + (tenant_id, account, conversation), + ).fetchone() + if row is None: + db.rollback() + raise LookupError("该会话不在黑名单中") + version = self._version(db, tenant_id) + 1 + stamp = now_text() + db.execute( + """UPDATE conversation_blacklist SET removed_at=?,removed_by=?,removed_source=?, + updated_at=?,version=? WHERE id=?""", + (stamp, clean_text(actor_label, limit=120), source, stamp, version, int(row["id"])), + ) + self.database._audit( + db, None, "blacklist.remove", + f"tenant={tenant_id} account={account[:64]} conversation={conversation[:120]} " + f"source={source} by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + return version + + def sync_desktop_blacklist( + self, tenant_id: str, adds: Iterable[dict[str, Any]], *, actor_label: str, + actor_account_id: int | None, ip: str, + ) -> tuple[int, list[dict[str, Any]], list[dict[str, Any]]]: + """桌面端补传本地新增(含升级前的历史条目),然后拿回全量。""" + tenant_id = str(tenant_id) + items = list(adds or [])[:MAX_SYNC_ITEMS] + rejected: list[dict[str, Any]] = [] + added = 0 + with self.database.connect() as db: + db.execute("BEGIN IMMEDIATE") + version = self._version(db, tenant_id) + 1 + for item in items: + if not isinstance(item, dict): + continue + try: + _entry, changed = self._add_entry( + db, tenant_id, item, source="desktop", actor_label=actor_label, + actor_account_id=actor_account_id, version=version, + ) + except ValueError as exc: + rejected.append({ + "account": str(item.get("account") or "")[:191], + "conversation": str(item.get("conversation") or "")[:512], + "error": str(exc), + }) + continue + added += int(changed) + if added: + self.database._audit( + db, None, "blacklist.sync", + f"tenant={tenant_id} added={added} by={clean_text(actor_label, limit=60)}", + str(ip or ""), + ) + db.commit() + else: + db.rollback() + version, entries = self.blacklist(tenant_id) + return version, entries, rejected + + # ── 会话 ───────────────────────────────────────────────────────────── + def conversations( + self, tenant_id: str, *, query: str = "", scope: str = "all", cursor: str = "", + limit: int = 30, + ) -> dict[str, Any]: + """手机端会话列表。只给名称、类型、最近时间和黑名单状态,不出聊天正文。""" + tenant_id = str(tenant_id) + limit = max(1, min(int(limit or 30), 100)) + needle = str(query or "").strip()[:100] + with self.database.connect() as db: + blocked_total = int(db.execute( + "SELECT COUNT(*) FROM conversation_blacklist WHERE tenant_id=? AND removed_at=''", + (tenant_id,), + ).fetchone()[0]) + if scope == "blocked": + params: list[Any] = [tenant_id] + where = "b.tenant_id=? AND b.removed_at=''" + if needle: + where += (" AND (b.display_name LIKE ? ESCAPE '\\' OR b.conversation LIKE ? ESCAPE '\\'" + " OR b.account_name LIKE ? ESCAPE '\\')") + params.extend([_like(needle)] * 3) + if cursor: + try: + where += " AND b.id limit + rows = rows[:limit] + items = [] + for row in rows: + archive_name, archive_account = ("", "") + if not row["display_name"] or not row["account_name"]: + archive_name, archive_account = self._archive_names( + db, tenant_id, str(row["account"]), str(row["conversation"]) + ) + conversation = str(row["conversation"]) + kind = _conversation_kind(conversation) + items.append({ + "account": str(row["account"]), + "account_name": str(row["account_name"] or archive_account or ""), + "conversation": conversation, + "name": str(row["display_name"] or archive_name or conversation), + "type": kind, + "type_label": CONVERSATION_TYPE_LABELS.get(kind, "会话"), + "last_message_at": "", + "blocked": True, + "blacklist": self._public_entry(row), + }) + next_cursor = str(rows[-1]["id"]) if has_more and rows else "" + return {"items": items, "next_cursor": next_cursor, "has_more": has_more, + "blocked_total": blocked_total} + + params = [tenant_id] + where = "c.tenant_id=?" + if needle: + where += " AND (c.name LIKE ? ESCAPE '\\' OR c.external_id LIKE ? ESCAPE '\\')" + params.extend([_like(needle), _like(needle)]) + if cursor: + try: + cursor_time, cursor_id = cursor.split("|", 1) + except ValueError as exc: + raise ValueError("分页游标不正确") from exc + where += " AND (c.last_message_at,c.id)<(?,?)" + params.extend([cursor_time, cursor_id]) + params.append(limit + 1) + try: + rows = db.execute( + f"""SELECT c.id,c.external_id,c.name,c.conversation_type,c.last_message_at, + a.external_account_id,a.display_name AS account_display_name, + b.id AS blacklist_id,b.account,b.conversation,b.display_name, + b.account_name,b.reason,b.source,b.created_by,b.created_at + FROM archive_conversation c + JOIN archive_source_account a ON a.id=c.source_account_id + LEFT JOIN conversation_blacklist b + ON b.tenant_id=c.tenant_id AND b.account=a.external_account_id + AND b.conversation=c.external_id AND b.removed_at='' + WHERE {where} + ORDER BY c.last_message_at DESC,c.id DESC LIMIT ?""", + tuple(params), + ).fetchall() + except Exception as exc: + if "no such table" in str(exc): + rows = [] + else: + raise + has_more = len(rows) > limit + rows = rows[:limit] + items = [] + for row in rows: + kind = str(row["conversation_type"] or "unknown") + items.append({ + "account": str(row["external_account_id"]), + "account_name": str(row["account_display_name"] or ""), + "conversation": str(row["external_id"]), + "name": str(row["name"] or row["external_id"]), + "type": kind, + "type_label": CONVERSATION_TYPE_LABELS.get(kind, "会话"), + "last_message_at": str(row["last_message_at"] or ""), + "blocked": row["blacklist_id"] is not None, + "blacklist": self._public_entry(row) if row["blacklist_id"] is not None else None, + }) + next_cursor = f"{rows[-1]['last_message_at']}|{rows[-1]['id']}" if has_more and rows else "" + return {"items": items, "next_cursor": next_cursor, "has_more": has_more, + "blocked_total": blocked_total} + + +def _conversation_kind(external_id: str) -> str: + prefix = str(external_id or "").split(":", 1)[0].upper() + return { + "M": "direct_wechat", "S": "direct_wecom", "R": "group", + "Y": "application", "O": "service", + }.get(prefix, "unknown") + + +def now_text_days_ago(days: int) -> str: + from datetime import timedelta + + return (datetime.now().astimezone() - timedelta(days=int(days))).isoformat(timespec="seconds") diff --git a/wechat_rpa/test_remote_api.py b/wechat_rpa/test_remote_api.py new file mode 100644 index 000000000..120d628b0 --- /dev/null +++ b/wechat_rpa/test_remote_api.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- +"""手机远程控制台、桌面命令通道与服务端黑名单。 + +重点是边界:手机令牌和桌面令牌互不通用,没有 ZYT 权限的人拦得住,只能动自己 +名下的电脑,命令只会被目标电脑领走,手机登录不会把值班电脑顶下线。 +""" + +import io +import json +import os +import shutil +import tempfile +import threading +import time +import urllib.error +from pathlib import Path +from unittest import TestCase, main, mock + +from fastapi.testclient import TestClient + +import admin_api +import admin_backend as ab +import remote_store +import zyt_auth + +STRONG = "Zhen@2026Ops" +LAN = {"X-Real-IP": "192.168.1.20"} +CONTROL = remote_store.PERM_REMOTE_CONTROL +ALL = remote_store.PERM_REMOTE_ALL +ADD = remote_store.PERM_BLACKLIST_ADD +REMOVE = remote_store.PERM_BLACKLIST_REMOVE + + +class FakeZyt: + """ZYT 的替身:账号密码、终端 9 令牌、/adminapi/desktop/session。""" + + def __init__(self): + self.users = { + "kefu1": {"id": 101, "password": "pw-kefu1", "name": "客服一", "root": False, + "permissions": [CONTROL, ADD]}, + "kefu2": {"id": 202, "password": "pw-kefu2", "name": "客服二", "root": False, + "permissions": []}, + "boss": {"id": 303, "password": "pw-boss", "name": "主管", "root": False, + "permissions": [ALL, ADD, REMOVE]}, + } + self.tokens: dict[str, str] = {} + self.login_calls = 0 + self.login_delay = 0.0 + self.login_error: Exception | None = None + self.identity_error: Exception | None = None + self.logged_out: list[str] = [] + self._count_lock = threading.Lock() + + def login(self, account, password): + with self._count_lock: + self.login_calls += 1 + number = self.login_calls + if self.login_delay: + time.sleep(self.login_delay) + if self.login_error is not None: + raise self.login_error + user = self.users.get(account) + if user is None: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="unknown_account") + if user["password"] != password: + raise zyt_auth.ZytLoginError("账号或密码错误", kind="credentials") + if user.get("is_paw") == 0: + raise zyt_auth.ZytLoginError("该账号仍在使用初始密码", kind="password_change_required") + token = f"zyt-mobile-{account}-{number}" + self.tokens[token] = account + return token + + def identity_for(self, account, terminal): + user = self.users[account] + return { + "user_id": f"admin:{user['id']}", "sn": account, "nickname": user["name"], + "mobile": "", "avatar": "", "terminal": terminal, "status": "active", + "root": user["root"], "role_ids": [2], "permissions": list(user["permissions"]), + "permissions_known": True, + } + + def identity(self, token): + if self.identity_error is not None: + raise self.identity_error + account = self.tokens.get(token) + if account is None: + raise zyt_auth.ZytLoginError("登录已失效,请重新登录", kind="rejected") + return self.identity_for(account, 9) + + def logout(self, token): + self.logged_out.append(token) + self.tokens.pop(token, None) + + +class _Base(TestCase): + budget = "0" + + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.db = ab.Database(self.root / "t.db") + self.db.initialize("Admin@123456") + uid = self.db.authenticate("admin", "Admin@123456")["id"] + self.db.change_password(uid, "Admin@123456", STRONG, "1.1.1.1") + self.zyt = FakeZyt() + env = mock.patch.dict(os.environ, { + "MOBILE_LOGIN_UPSTREAM_FAILURE_BUDGET": self.budget, "MOBILE_REQUIRE_HTTPS": "auto", + }) + env.start() + self.addCleanup(env.stop) + self.client = TestClient(admin_api.create_app( + self.root / "t.db", zyt_token_verifier=self._verify_desktop, zyt_mobile_auth=self.zyt, + )) + self.addCleanup(self.client.close) + + def _verify_desktop(self, token): + account = token.removeprefix("zyt-desktop-") + if account not in self.zyt.users: + raise zyt_auth.ZytAuthError("登录已失效") + return self.zyt.identity_for(account, 7) + + # ── 登录助手 ── + def desktop_login(self, account="kefu1", device_id="dev-0000000000000001"): + response = self.client.post( + "/api/v2/desktop/auth/exchange", + headers={"Authorization": f"Bearer zyt-desktop-{account}"}, + json={"device_id": device_id, "device_name": f"PC-{account}", "app_version": "1.5.0", + "interactive_login": True}, + ) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"}, response.json()["account"] + + def mobile_login(self, account="kefu1", password=None, headers=None, expect=200): + response = self.client.post( + "/api/v2/mobile/auth/login", + headers=headers if headers is not None else LAN, + json={"account": account, "password": password or self.zyt.users[account]["password"]}, + ) + self.assertEqual(response.status_code, expect, response.text) + if expect != 200: + return response + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def admin_login(self): + response = self.client.post("/api/v2/auth/login", json={"username": "admin", "password": STRONG}) + self.assertEqual(response.status_code, 200, response.text) + return {"Authorization": f"Bearer {response.json()['access_token']}"} + + def poll(self, desktop, *, status=None, wait=0, blacklist_version=None): + body = {"wait": wait, "status": status if status is not None else { + "listening": False, "state": "stopped", "transport": "visual", "wecom_connected": True, + "pending_count": 2, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }} + if blacklist_version is not None: + body["blacklist_version"] = blacklist_version + response = self.client.post("/api/v2/desktop/commands/poll", headers=desktop, json=body) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def detail(self, response): + return response.json()["detail"] + + +class MobileAuthTest(_Base): + def test_mobile_login_does_not_kick_the_desktop_and_tokens_are_not_interchangeable(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=desktop).status_code, 200) + me = self.client.get("/api/v2/mobile/me", headers=mobile) + self.assertEqual(me.status_code, 200, me.text) + self.assertEqual(me.json()["account"]["nickname"], "客服一") + self.assertTrue(me.json()["permissions"]["control"]) + self.assertFalse(me.json()["permissions"]["blacklist_remove"]) + self.assertEqual(self.client.get("/api/v2/desktop/me", headers=mobile).status_code, 401) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=desktop).status_code, 401) + self.assertEqual( + self.client.post("/api/v2/desktop/commands/poll", headers=mobile, json={"wait": 0}).status_code, + 401, + ) + + def test_mobile_and_desktop_land_on_the_same_account(self): + _desktop, desktop_account = self.desktop_login() + mobile = self.mobile_login() + self.assertEqual( + self.client.get("/api/v2/mobile/me", headers=mobile).json()["account"]["id"], + desktop_account["id"], + ) + + def test_wrong_password_is_generic_and_limited_per_account(self): + for _ in range(5): + response = self.mobile_login(password="wrong", expect=401) + self.assertEqual(self.detail(response)["code"], "invalid_credentials") + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + calls = self.zyt.login_calls + blocked = self.mobile_login(expect=429) + self.assertEqual(self.detail(blocked)["code"], "rate_limited") + self.assertEqual(self.zyt.login_calls, calls, "被限流的尝试不能再发到 ZYT") + # 其他账号不受影响 + self.mobile_login("kefu2") + + def test_public_plain_http_login_is_refused(self): + public = {"X-Real-IP": "114.114.114.114"} + response = self.mobile_login(headers=public, expect=403) + self.assertEqual(self.detail(response)["code"], "https_required") + self.assertEqual(self.zyt.login_calls, 0) + self.mobile_login(headers={**public, "X-Forwarded-Proto": "https"}) + # 伪造 X-Forwarded-For 不能冒充内网 + spoofed = {"X-Real-IP": "114.114.114.114", "X-Forwarded-For": "10.0.0.8"} + self.assertEqual(self.detail(self.mobile_login(headers=spoofed, expect=403))["code"], "https_required") + # 代理漏配 X-Real-IP 时不放行明文登录(对端不是本机回环) + self.assertEqual(self.detail(self.mobile_login(headers={}, expect=403))["code"], "https_required") + + def test_plain_http_login_is_allowed_by_default(self): + with mock.patch.dict(os.environ): + os.environ.pop("MOBILE_REQUIRE_HTTPS") + self.mobile_login(headers={"X-Real-IP": "114.114.114.114"}) + self.mobile_login("kefu2", headers={}) + self.assertEqual(self.zyt.login_calls, 2) + + def test_zyt_lockout_is_remembered_and_not_retried(self): + self.zyt.login_error = zyt_auth.ZytLoginError( + "密码连续5次输入错误,请30分钟后重试", kind="locked", retry_after=1800) + response = self.mobile_login(expect=429) + self.assertEqual(self.detail(response)["code"], "zyt_locked") + self.zyt.login_error = None + calls = self.zyt.login_calls + again = self.mobile_login("kefu2", expect=429) + self.assertEqual(self.detail(again)["code"], "zyt_locked") + self.assertEqual(self.zyt.login_calls, calls) + + def test_unsupported_terminal_and_outage_are_reported_clearly(self): + self.zyt.login_error = zyt_auth.ZytLoginError("升级", kind="terminal_unsupported") + self.assertEqual(self.detail(self.mobile_login(expect=503))["code"], "terminal_unsupported") + self.zyt.login_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + self.assertEqual(self.detail(self.mobile_login(expect=502))["code"], "zyt_unavailable") + + def test_logout_revokes_the_session_and_the_zyt_token(self): + mobile = self.mobile_login() + self.assertEqual(self.client.post("/api/v2/mobile/auth/logout", headers=mobile).status_code, 200) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(len(self.zyt.logged_out), 1) + + def test_disabled_account_and_admin_revoke_end_mobile_sessions(self): + _desktop, account = self.desktop_login() + mobile = self.mobile_login() + admin = self.admin_login() + listing = self.client.get("/api/v2/desktop-accounts", headers=admin).json()["accounts"] + self.assertEqual(listing[0]["mobile_session_count"], 1) + revoke = self.client.post(f"/api/v2/desktop-accounts/{account['id']}/revoke-devices", headers=admin) + self.assertEqual(revoke.status_code, 200, revoke.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + mobile = self.mobile_login() + disabled = self.client.put( + f"/api/v2/desktop-accounts/{account['id']}", headers=admin, json={"status": "disabled"}) + self.assertEqual(disabled.status_code, 200, disabled.text) + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + self.assertEqual(self.detail(self.mobile_login(expect=403))["code"], "account_disabled") + + def test_identity_is_rechecked_and_follows_zyt(self): + mobile = self.mobile_login() + self.zyt.users["kefu1"]["permissions"] = [CONTROL, ADD, REMOVE] + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + me = self.client.get("/api/v2/mobile/me", headers=mobile).json() + self.assertTrue(me["permissions"]["blacklist_remove"]) + + # ZYT 暂时不可用:不下线,沿用上次的权限 + self.zyt.identity_error = zyt_auth.ZytLoginError("超时", kind="unavailable") + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 200) + + # ZYT 会话真的失效:手机端下线 + self.zyt.identity_error = None + self.zyt.tokens.clear() + with self.db.connect() as db: + db.execute("UPDATE mobile_sessions SET identity_checked_at=0") + db.commit() + self.assertEqual(self.client.get("/api/v2/mobile/me", headers=mobile).status_code, 401) + + +class UpstreamBudgetTest(_Base): + budget = "2" + + def test_password_failures_across_accounts_pause_mobile_login(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + response = self.mobile_login("boss", expect=429) + self.assertEqual(self.detail(response)["code"], "upstream_budget") + self.assertEqual(self.zyt.login_calls, 2) + + def test_unknown_accounts_do_not_spend_the_zyt_budget(self): + # ZYT 不给"账号不存在"计数,随手乱填的账号名不应该把全体手机登录停掉。 + for name in ("nobody-1", "nobody-2", "nobody-3"): + response = self.mobile_login(name, password="x", expect=401) + self.assertEqual(self.detail(response)["message"], "账号或密码错误") + self.mobile_login("kefu1") + + def test_a_successful_login_resets_the_budget_like_zyt_does(self): + self.mobile_login("kefu1", password="bad", expect=401) + self.mobile_login("kefu1") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("boss") + self.mobile_login("kefu2", password="bad", expect=401) + self.mobile_login("kefu2", password="bad", expect=401) + self.assertEqual(self.detail(self.mobile_login("boss", expect=429))["code"], "upstream_budget") + + +class ConcurrentLoginTest(_Base): + budget = "4" + + def test_a_parallel_burst_cannot_outrun_the_limits(self): + """并发的错误密码不能一起穿过限流打到 ZYT(ZYT 按出口 IP 5 次就锁)。""" + self.zyt.login_delay = 0.4 + results = [] + + def attempt(index): + response = self.client.post( + "/api/v2/mobile/auth/login", headers={"X-Real-IP": f"192.168.1.{index + 10}"}, + json={"account": "kefu1" if index % 2 else "kefu2", "password": "wrong"}) + results.append(response.status_code) + + workers = [threading.Thread(target=attempt, args=(index,)) for index in range(12)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(30) + self.assertLessEqual(self.zyt.login_calls, 4) + self.assertEqual(results.count(401), self.zyt.login_calls) + self.assertEqual(results.count(429), 12 - self.zyt.login_calls) + + def test_initial_password_accounts_are_refused_without_counting_as_failures(self): + self.zyt.users["kefu1"]["is_paw"] = 0 + for _ in range(6): + response = self.mobile_login("kefu1", expect=403) + self.assertEqual(self.detail(response)["code"], "password_change_required") + self.assertEqual(self.zyt.login_calls, 6) + + +class RemoteControlTest(_Base): + def test_device_list_shows_live_status_from_the_command_channel(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop, status={ + "listening": True, "state": "running", "transport": "protocol", "wecom_connected": True, + "pending_count": 3, "app_version": "1.5.0", "capabilities": ["commands_v1", "blacklist_sync_v1"], + }) + items = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"] + self.assertEqual(len(items), 1) + device = items[0] + self.assertTrue(device["online"]) + self.assertTrue(device["remote_capable"]) + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["status"]["state_label"], "监听中") + self.assertEqual(device["status"]["transport_label"], "协议通道") + self.assertEqual(device["status"]["pending_count"], 3) + + def test_command_round_trip(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "start_listening"}) + self.assertEqual(created.status_code, 200, created.text) + command = created.json()["command"] + self.assertEqual(command["status"], "pending") + + delivered = self.poll(desktop)["commands"] + self.assertEqual([item["id"] for item in delivered], [command["id"]]) + self.assertEqual(delivered[0]["type"], "start_listening") + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "delivered") + + ack = self.client.post( + f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True, "result": {"listening": True}, "status": { + "listening": True, "state": "running", "transport": "visual", "wecom_connected": True, + "pending_count": 0, "app_version": "1.5.0", "capabilities": ["commands_v1"]}}) + self.assertEqual(ack.status_code, 200, ack.text) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "succeeded") + self.assertTrue(state["result"]["listening"]) + # 回执里带的状态立刻反映到手机端,不用等下一次长轮询 + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["status"]["listening"]) + self.assertEqual(device["last_command"]["status"], "succeeded") + self.assertEqual(self.poll(desktop)["commands"], []) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("remote.command.create", actions) + self.assertIn("remote.command.ack", actions) + + def test_unacknowledged_commands_are_redelivered_and_stale_ones_superseded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + first = self.client.post(url, headers=mobile, json={"type": "stop_listening"}).json()["command"] + second = self.client.post(url, headers=mobile, json={"type": "start_listening"}).json()["command"] + state = self.client.get(f"/api/v2/mobile/commands/{first['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "superseded") + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + # 没回执就再给一次,由电脑按 id 去重 + self.assertEqual([item["id"] for item in self.poll(desktop)["commands"]], [second["id"]]) + + def test_commands_expire_after_ten_minutes(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + command = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}).json()["command"] + self.assertGreaterEqual(command["expires_at"] - time.time(), 590) + with self.db.connect() as db: + db.execute("UPDATE desktop_commands SET expires_at=?", (int(time.time()) - 1,)) + db.commit() + self.assertEqual(self.poll(desktop)["commands"], []) + state = self.client.get(f"/api/v2/mobile/commands/{command['id']}", headers=mobile).json()["command"] + self.assertEqual(state["status"], "expired") + late = self.client.post(f"/api/v2/desktop/commands/{command['id']}/ack", headers=desktop, + json={"ok": True}) + self.assertEqual(late.json()["command"]["status"], "expired") + + def test_long_poll_wakes_up_as_soon_as_a_command_arrives(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + self.poll(desktop) + result = {} + + def wait_for_command(): + started = time.monotonic() + result["body"] = self.poll(desktop, wait=20) + result["elapsed"] = time.monotonic() - started + + worker = threading.Thread(target=wait_for_command) + worker.start() + time.sleep(0.6) + created = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=mobile, + json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + worker.join(15) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(result["body"]["commands"]), 1) + self.assertLess(result["elapsed"], 5) + + def test_offline_and_outdated_clients_cannot_be_commanded(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + url = "/api/v2/mobile/desktops/dev-0000000000000001/commands" + # 旧版客户端:有心跳,没有命令通道 + self.client.get("/api/v2/desktop/me", headers=desktop) + device = self.client.get("/api/v2/mobile/desktops", headers=mobile).json()["items"][0] + self.assertTrue(device["online"]) + self.assertFalse(device["remote_capable"]) + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "client_outdated") + # 程序没开:心跳停了 + self.poll(desktop) + with self.db.connect() as db: + db.execute("UPDATE desktop_devices SET last_seen_at='2020-01-01T00:00:00+08:00'") + db.commit() + response = self.client.post(url, headers=mobile, json={"type": "start_listening"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(self.detail(response)["code"], "device_offline") + self.assertEqual( + self.client.post(url.replace("0001", "9999"), headers=mobile, + json={"type": "start_listening"}).status_code, 404) + + def test_permissions_and_account_scope(self): + desktop1, account1 = self.desktop_login("kefu1", "dev-0000000000000001") + self.poll(desktop1) + _desktop2, account2 = self.desktop_login("kefu2", "dev-0000000000000002") + kefu2 = self.mobile_login("kefu2") + # 没有 remote/control:能看不能控 + self.assertEqual(self.client.get("/api/v2/mobile/desktops", headers=kefu2).status_code, 200) + response = self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu2, + json={"type": "start_listening"}) + self.assertEqual(response.status_code, 403) + # 不能看别人的电脑,也不能冒用别人的设备 id + self.assertEqual(self.client.get( + f"/api/v2/mobile/desktops?account_id={account1['id']}", headers=kefu2).status_code, 403) + kefu1 = self.mobile_login("kefu1") + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000002/commands", headers=kefu1, + json={"type": "start_listening"}).status_code, 404) + self.assertEqual(self.client.get("/api/v2/mobile/accounts", headers=kefu1).status_code, 403) + # 主管(remote/all):能看全部、能控别人 + boss = self.mobile_login("boss") + accounts = self.client.get("/api/v2/mobile/accounts", headers=boss).json()["items"] + self.assertEqual({item["id"] for item in accounts} >= {account1["id"], account2["id"]}, True) + created = self.client.post( + f"/api/v2/mobile/desktops/dev-0000000000000001/commands?account_id={account1['id']}", + headers=boss, json={"type": "stop_listening"}) + self.assertEqual(created.status_code, 200, created.text) + self.assertEqual(self.poll(desktop1)["commands"][0]["created_by"], "主管(手机)") + # 未知指令 + self.assertEqual(self.client.post( + "/api/v2/mobile/desktops/dev-0000000000000001/commands", headers=kefu1, + json={"type": "shutdown"}).status_code, 400) + + +class BlacklistTest(_Base): + def seed_archive(self, desktop): + payload = { + "source_account": {"external_account_id": "1688850000000001", "display_name": "客服一企微"}, + "source_table": "message_table", + "checkpoint": {"rowid": 3}, + "messages": [ + { + "source_message_id": f"m-{index}", "server_id": f"s-{index}", + "conversation": {"external_id": external_id, "name": name}, + "sender": {"external_id": "7881000000000001", "display_name": name, + "identity_type": "wecom_local_uid", "scope_id": "1688850000000001"}, + "sent_at": 1_758_000_000_000 + index * 1000, + "message_type": "text", "direction": "inbound", "content": "正文不应出现在手机端", + } + for index, (external_id, name) in enumerate( + (("S:1688850000000001_7881000000000001", "客户张三"), + ("R:10696052955016166", "售后群"), + ("S:1688850000000001_7881000000000002", "客户李四")), start=1) + ], + } + response = self.client.post("/api/v2/archive/desktop/imports/messages", headers=desktop, json=payload) + self.assertEqual(response.status_code, 200, response.text) + + def test_conversations_carry_blacklist_state_and_never_message_bodies(self): + desktop, _account = self.desktop_login() + self.seed_archive(desktop) + mobile = self.mobile_login() + listing = self.client.get("/api/v2/mobile/conversations", headers=mobile) + self.assertEqual(listing.status_code, 200, listing.text) + items = listing.json()["items"] + self.assertEqual([item["name"] for item in items], ["客户李四", "售后群", "客户张三"]) + self.assertNotIn("正文不应出现在手机端", listing.text) + self.assertEqual(items[1]["type_label"], "群聊") + + target = items[0] + added = self.client.put( + f"/api/v2/mobile/blacklist/{target['account']}/{target['conversation']}", + headers=mobile, json={"reason": "客户投诉"}) + self.assertEqual(added.status_code, 200, added.text) + self.assertEqual(added.json()["entry"]["name"], "客户李四") + self.assertEqual(added.json()["entry"]["account_name"], "客服一企微") + self.assertEqual(added.json()["entry"]["created_by"], "客服一(手机)") + + searched = self.client.get("/api/v2/mobile/conversations?q=李四", headers=mobile).json() + self.assertEqual(len(searched["items"]), 1) + self.assertTrue(searched["items"][0]["blocked"]) + self.assertEqual(searched["items"][0]["blacklist"]["reason"], "客户投诉") + blocked = self.client.get("/api/v2/mobile/conversations?scope=blocked", headers=mobile).json() + self.assertEqual(blocked["blocked_total"], 1) + self.assertEqual(blocked["items"][0]["conversation"], target["conversation"]) + + # 其他客服看不到这个租户的会话 + other = self.mobile_login("kefu2") + self.assertEqual(self.client.get("/api/v2/mobile/conversations", headers=other).json()["items"], []) + + def test_blacklist_permissions(self): + _desktop, account = self.desktop_login() + kefu2 = self.mobile_login("kefu2") + path = "/api/v2/mobile/blacklist/1688850000000001/S:1688850000000001_7881000000000009" + self.assertEqual(self.client.put(path, headers=kefu2, json={}).status_code, 403) + kefu1 = self.mobile_login("kefu1") + first = self.client.put(path, headers=kefu1, json={"name": "客户王五"}) + self.assertEqual(first.status_code, 200, first.text) + removed = self.client.delete(path, headers=kefu1) + self.assertEqual(removed.status_code, 403) + self.assertIn(REMOVE, self.detail(removed)["message"]) + boss = self.mobile_login("boss") + by_boss = self.client.delete(f"{path}?account_id={account['id']}", headers=boss) + self.assertEqual(by_boss.status_code, 200, by_boss.text) + self.assertGreater(by_boss.json()["version"], first.json()["version"]) + self.assertEqual(self.client.delete(f"{path}?account_id={account['id']}", headers=boss).status_code, 404) + actions = [row["action"] for row in self.db.audit_entries(50)] + self.assertIn("blacklist.add", actions) + self.assertIn("blacklist.remove", actions) + + def test_desktop_sync_is_the_write_through_path_and_polls_see_the_version(self): + desktop, _account = self.desktop_login() + mobile = self.mobile_login() + start = self.poll(desktop)["blacklist_version"] + synced = self.client.post("/api/v2/desktop/blacklist/sync", headers=desktop, json={"add": [ + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户", "account_name": "企微"}, + {"account": "1688850000000001", "conversation": "S:legacy-1", "name": "老客户"}, + {"account": "", "conversation": "S:bad"}, + ]}) + self.assertEqual(synced.status_code, 200, synced.text) + body = synced.json() + self.assertEqual([item["conversation"] for item in body["items"]], ["S:legacy-1"]) + self.assertEqual(len(body["rejected"]), 1) + self.assertGreater(body["version"], start) + # 版本不一致的长轮询立即返回,让电脑去同步 + began = time.monotonic() + self.assertEqual(self.poll(desktop, wait=10, blacklist_version=start)["blacklist_version"], body["version"]) + self.assertLess(time.monotonic() - began, 3) + pulled = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json() + self.assertEqual(pulled["version"], body["version"]) + # 桌面端没有 remove 权限:移出被拒;主管有 + denied = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(denied.status_code, 403) + self.zyt.users["kefu1"]["permissions"].append(REMOVE) + desktop, _account = self.desktop_login() + allowed = self.client.delete("/api/v2/desktop/blacklist/1688850000000001/S:legacy-1", headers=desktop) + self.assertEqual(allowed.status_code, 200, allowed.text) + self.assertEqual(self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"], []) + # 手机加的黑名单,电脑下一次长轮询就能看到版本变化 + version = self.poll(desktop)["blacklist_version"] + self.client.put("/api/v2/mobile/blacklist/1688850000000001/S:from-phone", headers=mobile, json={}) + self.assertGreater(self.poll(desktop, blacklist_version=version)["blacklist_version"], version) + + def test_re_adding_a_removed_entry_reactivates_it(self): + desktop, account = self.desktop_login() + boss = self.mobile_login("boss") + path = f"/api/v2/mobile/blacklist/1688850000000001/S:again?account_id={account['id']}" + self.assertEqual(self.client.put(path, headers=boss, json={"reason": "一"}).status_code, 200) + self.assertEqual(self.client.delete(path, headers=boss).status_code, 200) + again = self.client.put(path, headers=boss, json={"reason": "二"}) + self.assertEqual(again.status_code, 200) + items = self.client.get("/api/v2/desktop/blacklist", headers=desktop).json()["items"] + self.assertEqual([(item["conversation"], item["reason"]) for item in items], [("S:again", "二")]) + + +class MobileHostingTest(_Base): + def test_mobile_bundle_is_served_under_m_with_safe_fallback(self): + dist = self.root / "mobile-dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("mobile", encoding="utf-8") + (dist / "assets" / "app-abc123.js").write_text("console.log(1)", encoding="utf-8") + (self.root / "secret.txt").write_text("secret", encoding="utf-8") + with mock.patch.object(admin_api, "_mobile_dist", return_value=dist): + client = TestClient(admin_api.create_app(self.root / "t.db", zyt_mobile_auth=self.zyt)) + with client: + entry = client.get("/m/") + self.assertIn("mobile", entry.text) + self.assertEqual(entry.headers["cache-control"], "no-cache") + self.assertEqual(entry.headers["x-frame-options"], "DENY") + self.assertIn("frame-ancestors 'none'", entry.headers["content-security-policy"]) + asset = client.get("/m/assets/app-abc123.js") + self.assertEqual(asset.status_code, 200) + self.assertIn("immutable", asset.headers["cache-control"]) + # 发版后旧页面请求旧 chunk:必须是 404,不能回 index.html + self.assertEqual(client.get("/m/assets/app-old999.js").status_code, 404) + self.assertIn("mobile", client.get("/m/devices/abc").text) + self.assertNotIn("secret", client.get("/m/..%2Fsecret.txt").text) + self.assertEqual(client.get("/m", follow_redirects=False).status_code, 308) + self.assertEqual(client.get("/api/v2/health").status_code, 200) + + +class ZytMobileClientTest(TestCase): + def response(self, payload): + return io.BytesIO(json.dumps(payload).encode("utf-8")) + + def login(self, payload): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(payload)) as opened: + token = zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + request = opened.call_args.args[0] + self.assertEqual(json.loads(request.data)["terminal"], 9) + return token + + def failure(self, message): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + self.login({"code": 0, "msg": message, "data": []}) + return caught.exception + + def test_login_returns_the_terminal_9_token(self): + self.assertEqual(self.login({"code": 1, "data": {"token": "abc"}}), "abc") + + def test_initial_password_accounts_are_refused_and_logged_out(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", side_effect=[ + self.response({"code": 1, "data": {"token": "fresh", "is_paw": 0}}), + self.response({"code": 1, "msg": ""}), + ]) as opened: + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "password_change_required") + logout = opened.call_args_list[1].args[0] + self.assertTrue(logout.full_url.endswith("/adminapi/login/logout")) + self.assertEqual(logout.headers.get("Token"), "fresh") + + def test_login_failures_are_classified(self): + self.assertEqual(self.failure("密码错误").kind, "credentials") + self.assertEqual(self.failure("账号不存在").kind, "unknown_account") + self.assertEqual(str(self.failure("账号不存在")), "账号或密码错误") + locked = self.failure("密码连续5次输入错误,请30分钟后重试") + self.assertEqual((locked.kind, locked.retry_after), ("locked", 1800)) + self.assertEqual(self.failure("terminal必须在 1,2,7 范围内").kind, "terminal_unsupported") + self.assertEqual(self.failure("账号已禁用").kind, "disabled") + + def test_transport_errors_are_unavailable(self): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", + side_effect=urllib.error.URLError("timed out")): + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + zyt_auth.login_admin("kefu1", "secret", api_url="https://zyt.example.test") + self.assertEqual(caught.exception.kind, "unavailable") + + def test_session_must_be_the_mobile_terminal(self): + def verify(data): + with mock.patch.object(zyt_auth.urllib.request, "urlopen", return_value=self.response(data)): + return zyt_auth.verify_admin_session("tok", api_url="https://zyt.example.test") + + identity = verify({"code": 1, "data": {"user_id": "admin:9", "terminal": 9, "status": "active", + "root": 0, "permissions": [CONTROL]}}) + self.assertEqual(identity["permissions"], [CONTROL]) + for other in (7, 8): # 桌面端令牌、ZYT 的 AI 浏览器令牌都不能当手机令牌用 + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 1, "data": {"user_id": "admin:9", "terminal": other, "status": "active"}}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "当前登录令牌不属于企业微信客服桌面端"}) + self.assertEqual(caught.exception.kind, "terminal_unsupported") + with self.assertRaises(zyt_auth.ZytLoginError) as caught: + verify({"code": 0, "msg": "登录已失效,请重新登录"}) + self.assertEqual(caught.exception.kind, "rejected") + + +if __name__ == "__main__": + main() diff --git a/wechat_rpa/test_zyt_order_api.py b/wechat_rpa/test_zyt_order_api.py new file mode 100644 index 000000000..7c998a853 --- /dev/null +++ b/wechat_rpa/test_zyt_order_api.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +"""Patient-order proxy security checks using persisted conversation bindings.""" + +from unittest import TestCase, main, mock + +import test_archive_api as archive_tests + + +class ZytOrderApiTest(TestCase): + # Reuse fixture helpers without inheriting and re-running the archive suite. + setUp = archive_tests.ArchiveApiTest.setUp + tearDown = archive_tests.ArchiveApiTest.tearDown + login = archive_tests.ArchiveApiTest.login + desktop_login = archive_tests.ArchiveApiTest.desktop_login + sample_payload = staticmethod(archive_tests.ArchiveApiTest.sample_payload) + search_zyt_patients = staticmethod(archive_tests.ArchiveApiTest.search_zyt_patients) + + @staticmethod + def verify_zyt(token): + identity = archive_tests.ArchiveApiTest.verify_zyt(token) + if token == "zyt-user-list-only": + identity["permissions"].append("firstvisit.myPatient/orders") + elif token != "zyt-no-patient": + identity["permissions"] += [ + "firstvisit.myPatient/orders", "firstvisit.myPatient/orderDetail", + "tcm.prescriptionOrder/detail", "tcm.prescriptionOrder/logisticsTrace", + ] + return identity + + def prepare_conversation(self, *, bind=True): + session = self.desktop_login() + payload = self.sample_payload() + imported = self.client.post( + "/api/v2/archive/desktop/imports/messages", + headers=session["headers"], json=payload, + ) + self.assertEqual(imported.status_code, 200, imported.text) + params = { + "external_account_id": payload["source_account"]["external_account_id"], + "conversation_external_id": payload["messages"][0]["conversation"]["external_id"], + } + context = self.client.get( + "/api/v2/archive/desktop/patient-context", + headers=session["headers"], params=params, + ) + self.assertEqual(context.status_code, 200, context.text) + person_id = context.json()["person"]["id"] + params["person_id"] = person_id + binding = None + if bind: + searched = self.client.get( + "/api/v2/archive/desktop/patients/search", + headers=session["headers"], params={"keyword": "张三"}, + ) + self.assertEqual(searched.status_code, 200, searched.text) + response = self.client.put( + "/api/v2/archive/desktop/patient-binding", + headers=session["headers"], + json={**params, "patient_id": 9001, "diagnosis_id": 9101}, + ) + self.assertEqual(response.status_code, 200, response.text) + binding = response.json()["binding"] + return session, params, binding + + @staticmethod + def sample_order(**overrides): + return { + "id": 321, "order_no": "ZYT202609220321", "diagnosis_id": 9101, + "create_time": "2026-09-22 10:20:00", "fulfillment_status": 5, + "prescription_audit_status": 1, "payment_slip_audit_status": 1, + "express_company": "顺丰", "tracking_number": "SF1234567899", + "recipient_name": "张三", "recipient_phone": "13800138000", + "recipient_address": "不应向模型公开的住址", "prescription": {"private": True}, + **overrides, + } + + def order_paths(self): + return ( + "/api/v2/archive/desktop/patient-orders", + "/api/v2/archive/desktop/patient-orders/321", + "/api/v2/archive/desktop/patient-orders/321/tracking", + "/api/v2/archive/desktop/patient-orders/321/audit-status", + ) + + def test_unbound_conversation_never_queries_orders(self): + session, params, _ = self.prepare_conversation(bind=False) + with mock.patch("archive_api.ZytOrderClient") as client: + for path in self.order_paths(): + with self.subTest(path=path): + response = self.client.get(path, headers=session["headers"], params=params) + self.assertEqual(response.status_code, 409, response.text) + self.assertEqual(response.json()["detail"]["code"], "patient_binding_required") + client.assert_not_called() + + def test_order_from_other_diagnosis_is_rejected_before_tracking(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.get_order_detail.return_value = self.sample_order(diagnosis_id=9999) + for path in self.order_paths()[1:]: + with self.subTest(path=path): + response = self.client.get(path, headers=session["headers"], params=params) + self.assertEqual(response.status_code, 404, response.text) + self.assertNotIn("ZYT202609220321", response.text) + client.return_value.get_tracking.assert_not_called() + + def test_persisted_forged_diagnosis_binding_never_queries_orders(self): + session, params, binding = self.prepare_conversation() + with self.database.connect() as db: + db.execute( + "UPDATE archive_patient_binding SET diagnosis_id=? WHERE id=?", + (9999, binding["id"]), + ) + db.commit() + with mock.patch("archive_api.ZytOrderClient") as client: + for path in self.order_paths(): + with self.subTest(path=path): + response = self.client.get(path, headers=session["headers"], params=params) + self.assertEqual(response.status_code, 409, response.text) + self.assertEqual(response.json()["detail"]["code"], "patient_binding_invalid") + client.assert_not_called() + + def test_other_tenant_cannot_reuse_conversation_or_person_binding(self): + _, params, _ = self.prepare_conversation() + other = self.desktop_login("zyt-user-2", "desktop-device-02") + with mock.patch("archive_api.ZytOrderClient") as client: + for path in self.order_paths(): + with self.subTest(path=path): + response = self.client.get(path, headers=other["headers"], params=params) + self.assertEqual(response.status_code, 409, response.text) + self.assertEqual(response.json()["detail"]["code"], "patient_binding_required") + client.assert_not_called() + + def test_missing_order_permission_blocks_before_upstream_query(self): + session = self.desktop_login("zyt-no-patient") + with mock.patch("archive_api.ZytOrderClient") as client: + for path in self.order_paths(): + with self.subTest(path=path): + response = self.client.get(path, headers=session["headers"], params={ + "external_account_id": "wxwork-account-01", + "conversation_external_id": "S:external-user-01", + }) + self.assertEqual(response.status_code, 403, response.text) + client.assert_not_called() + + def test_list_filters_other_patient_and_masks_private_fields(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.list_orders.return_value = { + "items": [self.sample_order(), self.sample_order(id=322, diagnosis_id=9999)], + "total": 2, + } + response = self.client.get(self.order_paths()[0], headers=session["headers"], params=params) + self.assertEqual(response.status_code, 200, response.text) + result = response.json() + self.assertEqual([item["order_id"] for item in result["items"]], [321]) + self.assertEqual(result["total"], 1) + self.assertEqual(result["items"][0]["tracking_number_masked"], "SF****7899") + self.assertEqual(client.return_value.list_orders.call_args.kwargs["diagnosis_id"], 9101) + self.assertNotIn("13800138000", response.text) + self.assertNotIn("SF1234567899", response.text) + self.assertNotIn("recipient_address", response.text) + self.assertNotIn("private", response.text) + + def test_allowed_detail_contains_only_sanitized_order_facts(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.get_order_detail.return_value = self.sample_order() + response = self.client.get(self.order_paths()[1], headers=session["headers"], params=params) + self.assertEqual(response.status_code, 200, response.text) + order = response.json()["order"] + self.assertEqual(order["order_id"], 321) + self.assertEqual(order["tracking_number_masked"], "SF****7899") + self.assertEqual(order["fulfillment_status"], "已发货") + self.assertNotIn("SF1234567899", response.text) + self.assertNotIn("13800138000", response.text) + self.assertNotIn("recipient_address", response.text) + client.return_value.get_order_detail.assert_called_once_with(321) + + def test_single_order_list_brings_tracking_inline(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.list_orders.return_value = {"items": [self.sample_order()], "total": 1} + client.return_value.get_tracking.return_value = { + "state_text": "运输中", + "traces": [{"accept_time": "2026-09-22 09:42:00", "remark": "快件已到达中转站"}], + } + response = self.client.get(self.order_paths()[0], headers=session["headers"], params=params) + self.assertEqual(response.status_code, 200, response.text) + result = response.json() + self.assertEqual(result["order_count"], 1) + self.assertFalse(result["selection_required"]) + self.assertEqual(result["single_order_id"], 321) + self.assertEqual(result["latest_tracking"]["status"], "运输中") + self.assertEqual(result["latest_tracking"]["description"], "快件已到达中转站") + self.assertEqual(client.return_value.get_tracking.call_args.kwargs["phone_tail"], "8000") + + def test_multiple_orders_require_customer_selection_and_skip_tracking(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.list_orders.return_value = { + "items": [self.sample_order(), self.sample_order(id=322, order_no="ZYT202609100322")], + "total": 2, + } + response = self.client.get(self.order_paths()[0], headers=session["headers"], params=params) + self.assertEqual(response.status_code, 200, response.text) + result = response.json() + self.assertEqual(result["order_count"], 2) + self.assertTrue(result["selection_required"]) + self.assertEqual(result["single_order_id"], 0) + self.assertIsNone(result["latest_tracking"]) + self.assertIn("确认", result["hint"]) + client.return_value.get_tracking.assert_not_called() + + def test_single_order_without_tracking_number_reports_missing(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.list_orders.return_value = { + "items": [self.sample_order(tracking_number="", fulfillment_status=2)], "total": 1, + } + client.return_value.get_order_detail.return_value = self.sample_order(tracking_number="", fulfillment_status=2) + listed = self.client.get(self.order_paths()[0], headers=session["headers"], params=params) + self.assertEqual(listed.status_code, 200, listed.text) + self.assertIsNone(listed.json()["latest_tracking"]) + self.assertEqual(listed.json()["tracking_error"]["code"], "tracking_number_missing") + tracked = self.client.get(self.order_paths()[2], headers=session["headers"], params=params) + self.assertEqual(tracked.status_code, 409, tracked.text) + self.assertEqual(tracked.json()["detail"]["code"], "tracking_number_missing") + client.return_value.get_tracking.assert_not_called() + + def test_tracking_provider_failure_never_returns_a_status(self): + from zyt_order_client import ZytOrderError + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.get_order_detail.return_value = self.sample_order() + client.return_value.get_tracking.side_effect = ZytOrderError("快递100 暂不可用") + response = self.client.get(self.order_paths()[2], headers=session["headers"], params=params) + self.assertEqual(response.status_code, 502, response.text) + self.assertEqual(response.json()["detail"]["code"], "tracking_unavailable") + self.assertNotIn("运输中", response.text) + self.assertNotIn("SF1234567899", response.text) + + def test_missing_order_returns_not_found_code(self): + from zyt_order_client import ZytOrderNotFound + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.get_order_detail.side_effect = ZytOrderNotFound("订单不存在") + for path in self.order_paths()[1:]: + with self.subTest(path=path): + response = self.client.get(path, headers=session["headers"], params=params) + self.assertEqual(response.status_code, 404, response.text) + self.assertEqual(response.json()["detail"]["code"], "order_not_found") + + def test_list_only_permission_cannot_read_detail_or_tracking(self): + _, params, _ = self.prepare_conversation() + limited = self.desktop_login("zyt-user-list-only") + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.list_orders.return_value = {"items": [self.sample_order()], "total": 1} + listed = self.client.get(self.order_paths()[0], headers=limited["headers"], params=params) + self.assertEqual(listed.status_code, 200, listed.text) + # Without the ZYT logistics permission the single-order shortcut must not query tracking. + self.assertIsNone(listed.json()["latest_tracking"]) + client.return_value.get_tracking.assert_not_called() + for path in self.order_paths()[1:]: + with self.subTest(path=path): + response = self.client.get(path, headers=limited["headers"], params=params) + self.assertEqual(response.status_code, 403, response.text) + client.return_value.get_order_detail.assert_not_called() + + def test_summary_for_unbound_conversation_explains_without_querying(self): + session, params, _ = self.prepare_conversation(bind=False) + with mock.patch("archive_api.ZytOrderClient") as client: + response = self.client.get( + "/api/v2/archive/desktop/patient-orders/summary", headers=session["headers"], params=params, + ) + self.assertEqual(response.status_code, 200, response.text) + summary = response.json() + self.assertFalse(summary["capability"]["enabled"]) + self.assertFalse(summary["capability"]["bound"]) + self.assertEqual(summary["capability"]["code"], "patient_binding_required") + self.assertEqual(summary["recent_orders"], []) + client.assert_not_called() + + def test_summary_for_bound_conversation_returns_orders_tracking_and_logs(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.list_orders.return_value = { + "items": [ + self.sample_order(id=322, order_no="ZYT202609200322", tracking_number="", fulfillment_status=2), + self.sample_order(), + self.sample_order(id=300, diagnosis_id=9999), + ], + "total": 3, + } + client.return_value.get_order_detail.return_value = self.sample_order() + client.return_value.get_tracking.return_value = {"state_text": "派送中", "traces": []} + detail = self.client.get(self.order_paths()[1], headers=session["headers"], params=params) + self.assertEqual(detail.status_code, 200, detail.text) + response = self.client.get( + "/api/v2/archive/desktop/patient-orders/summary", headers=session["headers"], params=params, + ) + self.assertEqual(response.status_code, 200, response.text) + summary = response.json() + self.assertTrue(summary["capability"]["enabled"]) + self.assertEqual([item["order_id"] for item in summary["recent_orders"]], [322, 321]) + self.assertEqual(summary["latest_tracking"]["status"], "派送中") + self.assertEqual(summary["latest_tracking"]["order_id"], 321) + self.assertNotIn("13800138000", response.text) + self.assertNotIn("SF1234567899", response.text) + labels = [entry["label"] for entry in summary["logs"]] + self.assertIn("查询订单详情", labels) + self.assertIn("刷新订单概览", labels) + self.assertEqual(client.return_value.get_tracking.call_args.args[0], 321) + + def test_summary_logs_are_scoped_to_the_conversation(self): + session, params, _ = self.prepare_conversation() + with mock.patch("archive_api.ZytOrderClient") as client: + client.return_value.get_order_detail.return_value = self.sample_order() + self.client.get(self.order_paths()[1], headers=session["headers"], params=params) + other = dict(params, conversation_external_id="S:someone-else") + response = self.client.get( + "/api/v2/archive/desktop/patient-orders/summary", headers=session["headers"], params=other, + ) + self.assertEqual(response.status_code, 200, response.text) + self.assertEqual(response.json()["logs"], []) + + +if __name__ == "__main__": + main() diff --git a/wechat_rpa/test_zyt_patient_client.py b/wechat_rpa/test_zyt_patient_client.py index 167a12622..7846351f9 100644 --- a/wechat_rpa/test_zyt_patient_client.py +++ b/wechat_rpa/test_zyt_patient_client.py @@ -3,7 +3,7 @@ import json from unittest import TestCase, main, mock -from zyt_patient_client import ZytPatientClient +from zyt_patient_client import ZytPatientClient, ZytPatientError class _Response: @@ -82,5 +82,45 @@ class ZytPatientClientTest(TestCase): self.assertIn("patient_id=8", opened.call_args.args[0].full_url) + def test_diagnosis_validation_keeps_historical_rows_for_the_same_patient(self): + client = ZytPatientClient("https://admin.example.com", "secret-token") + with mock.patch.object(client, "_get", return_value={ + "lists": [ + {"id": 12, "patient_id": 8, "patient_name": "张三"}, + {"id": 10, "patient_id": 8, "patient_name": "张三"}, + ], "count": 2, + }) as called: + diagnosis = client.get_patient_diagnosis(8, 10) + self.assertEqual(diagnosis["diagnosis_id"], 10) + self.assertEqual(diagnosis["patient_id"], 8) + self.assertEqual(called.call_args.args[1], {"patient_id": 8, "page_no": 1, "page_size": 50}) + + def test_diagnosis_validation_rejects_wrong_patient_even_if_filter_ignored(self): + client = ZytPatientClient("https://admin.example.com", "secret-token") + with mock.patch.object(client, "_get", return_value={ + "lists": [{"id": 10, "patient_id": 9}], "count": 1, + }): + self.assertIsNone(client.get_patient_diagnosis(8, 10)) + + def test_diagnosis_validation_reads_later_pages(self): + client = ZytPatientClient("https://admin.example.com", "secret-token") + pages = [ + {"lists": [{"id": n, "patient_id": 8} for n in range(100, 150)], "count": 51}, + {"lists": [{"id": 10, "patient_id": 8}], "count": 51}, + ] + with mock.patch.object(client, "_get", side_effect=pages) as called: + self.assertEqual(client.get_patient_diagnosis(8, 10)["diagnosis_id"], 10) + self.assertEqual(called.call_args.args[1]["page_no"], 2) + + def test_diagnosis_validation_fails_explicitly_when_bounded_scan_is_truncated(self): + client = ZytPatientClient("https://admin.example.com", "secret-token") + with mock.patch.object(client, "_get", return_value={ + "lists": [{"id": 12, "patient_id": 8}], "count": 1001, + }) as called: + with self.assertRaisesRegex(ZytPatientError, "超过核验范围"): + client.get_patient_diagnosis(8, 10) + self.assertEqual(called.call_count, 20) + + if __name__ == "__main__": main() diff --git a/wechat_rpa/updates/1.5.0/ZhenAI-Update-v1.5.0.exe.part b/wechat_rpa/updates/1.5.0/ZhenAI-Update-v1.5.0.exe.part new file mode 100644 index 000000000..123aca8f4 Binary files /dev/null and b/wechat_rpa/updates/1.5.0/ZhenAI-Update-v1.5.0.exe.part differ diff --git a/wechat_rpa/zyt_auth.py b/wechat_rpa/zyt_auth.py index fdfeb0f4d..6fad0c3b9 100644 --- a/wechat_rpa/zyt_auth.py +++ b/wechat_rpa/zyt_auth.py @@ -8,6 +8,7 @@ from __future__ import annotations import json import os +import re import socket import urllib.error import urllib.parse @@ -16,6 +17,10 @@ from typing import Any WECOM_RPA_TERMINAL = 7 +# 手机远程控制台。和桌面端(7)、网页后台(1)分开保存 ZYT 会话,互不顶号。 +# 8 已被 ZYT 的 AI 浏览器(app/mcp/service/ConsoleService)占用,不能复用。 +# 这个终端的登录只由客服后台代为发起,密码不经过任何第三方页面。 +WECOM_RPA_MOBILE_TERMINAL = 9 DEFAULT_ZYT_API_BASE_URL = os.environ.get( "ZYT_API_BASE_URL", "http://127.0.0.1" ).strip().rstrip("/") @@ -25,6 +30,26 @@ class ZytAuthError(RuntimeError): pass +class ZytLoginError(ZytAuthError): + """ZYT 登录失败,并带上可供界面区分的原因。 + + kind 取值: + credentials 密码错误(ZYT 会按出口 IP 计入锁定次数) + unknown_account 账号不存在(ZYT 不计入锁定次数;对外提示与密码错误相同) + password_change_required 密码正确,但账号仍在用初始密码,须先到 ZYT 网页后台修改 + locked ZYT 已按出口 IP 锁定登录,retry_after 为剩余秒数 + disabled 管理员账号已停用 + terminal_unsupported 线上 ZYT 还不认识手机终端 9 + unavailable 网络、超时或 ZYT 返回了非预期数据 + rejected 其他明确的拒绝(原样展示 ZYT 的提示) + """ + + def __init__(self, message: str, *, kind: str = "rejected", retry_after: int = 0): + super().__init__(message) + self.kind = kind + self.retry_after = max(0, int(retry_after or 0)) + + def api_base_url() -> str: value = DEFAULT_ZYT_API_BASE_URL parsed = urllib.parse.urlparse(value) @@ -160,3 +185,173 @@ def verify_desktop_token( return identity raise ZytAuthError(last_error) + + +def _checked_base_url(api_url: str | None) -> str: + base_url = str(api_url or api_base_url()).strip().rstrip("/") + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytLoginError("ZYT 登录 API 域名配置不正确", kind="unavailable") + return base_url + + +def _zyt_json( + method: str, url: str, *, payload: dict[str, Any] | None = None, + token: str = "", timeout: float, +) -> dict[str, Any]: + headers = {"Accept": "application/json", "User-Agent": "WeCom-RPA-Backend/1.0"} + body = None + if payload is not None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + headers["Content-Type"] = "application/json; charset=utf-8" + if token: + headers["token"] = token + request = urllib.request.Request(url, data=body, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + payload_out = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + raise ZytLoginError( + f"ZYT 身份服务拒绝请求(HTTP {exc.code})", kind="unavailable" + ) from exc + except (urllib.error.URLError, TimeoutError, socket.timeout, OSError) as exc: + raise ZytLoginError( + f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}", kind="unavailable" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytLoginError("ZYT 身份服务返回了无效数据", kind="unavailable") from exc + if not isinstance(payload_out, dict): + raise ZytLoginError("ZYT 身份服务返回了无效数据", kind="unavailable") + return payload_out + + +_LOCK_MINUTES_RE = re.compile(r"(\d+)\s*分钟") + + +def _login_failure(message: str) -> ZytLoginError: + """把 ZYT 登录接口的中文提示归类。 + + 账号不存在和密码错误合并成同一句,免得手机端被拿来探测哪些账号存在。 + """ + text = str(message or "").strip() or "ZYT 登录失败" + if "连续" in text and "分钟" in text: + match = _LOCK_MINUTES_RE.search(text.split("请", 1)[-1]) + minutes = int(match.group(1)) if match else 30 + return ZytLoginError(text, kind="locked", retry_after=max(1, minutes) * 60) + # ZYT 只在"账号存在、密码不对"时累计出口 IP 的失败次数(LoginValidate::password), + # 账号不存在直接返回、不计数。两者对外同一句话,对内要分开:限流预算只该为前者消耗。 + if "密码错误" in text: + return ZytLoginError("账号或密码错误", kind="credentials") + if text == "账号不存在": + return ZytLoginError("账号或密码错误", kind="unknown_account") + if "禁用" in text or "停用" in text: + return ZytLoginError("该 ZYT 账号已停用,请联系管理员", kind="disabled") + if "terminal" in text.lower() or "终端" in text: + return ZytLoginError( + "ZYT 尚未开通企微客服手机端登录(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + return ZytLoginError(text, kind="rejected") + + +def login_admin( + account: str, password: str, *, terminal: int = WECOM_RPA_MOBILE_TERMINAL, + api_url: str | None = None, timeout: float = 10.0, +) -> str: + """用 ZYT 管理员账号密码换取指定终端的 token。只在服务端调用。""" + + account = str(account or "").strip() + if not account or not password: + raise ZytLoginError("请输入账号和密码", kind="rejected") + base_url = _checked_base_url(api_url) + result = _zyt_json( + "POST", f"{base_url}/adminapi/login/account", + payload={"account": account, "password": str(password), "terminal": int(terminal)}, + timeout=timeout, + ) + if str(result.get("code")) != "1": + raise _login_failure(str(result.get("msg") or "")) + data = result.get("data") + token = str(data.get("token") or "").strip() if isinstance(data, dict) else "" + if not token: + raise ZytLoginError("ZYT 没有返回登录令牌", kind="unavailable") + # is_paw=0:管理员发放的初始密码还没改过。网页后台会强制先改密;这里是面向公网的 + # 长期会话,同样不放行,并立即注销刚拿到的 ZYT 令牌。 + if str(data.get("is_paw", 1)).strip() == "0": + logout_admin(token, api_url=base_url, timeout=timeout) + raise ZytLoginError( + "该账号仍在使用初始密码,请先登录 ZYT 网页后台修改密码,再使用手机控制台", + kind="password_change_required", + ) + return token + + +def verify_admin_session( + token: str, *, terminal: int = WECOM_RPA_MOBILE_TERMINAL, + api_url: str | None = None, timeout: float = 8.0, +) -> dict[str, Any]: + """通过 /adminapi/desktop/session 读取当前身份与权限,并核对终端类型。 + + 与 ``verify_desktop_token`` 不同,这里没有旧版 mySelf 兜底:手机终端本身 + 就要求 ZYT 已升级,旧接口也不会返回终端 9。 + """ + + token = str(token or "").strip() + if not token: + raise ZytLoginError("缺少 ZYT 登录令牌", kind="rejected") + base_url = _checked_base_url(api_url) + try: + result = _zyt_json( + "GET", f"{base_url}/adminapi/desktop/session", token=token, timeout=timeout + ) + except ZytLoginError as exc: + cause = exc.__cause__ + if isinstance(cause, urllib.error.HTTPError) and cause.code in {404, 405}: + raise ZytLoginError( + "ZYT 尚未部署桌面身份接口,暂不能使用手机端", kind="terminal_unsupported" + ) from exc + raise + if str(result.get("code")) != "1": + message = str(result.get("msg") or "").strip() or "ZYT 登录已失效" + if "不属于" in message and "桌面" in message: + raise ZytLoginError( + "ZYT 尚未开通企微客服手机端身份校验(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + if "禁用" in message or "不存在" in message: + raise ZytLoginError("ZYT 管理员账号不存在或已停用", kind="disabled") + raise ZytLoginError(message, kind="rejected") + data = result.get("data") + if not isinstance(data, dict) or not data.get("user_id"): + raise ZytLoginError("ZYT 身份信息不完整", kind="unavailable") + identity = dict(data) + if str(identity.get("terminal")) != str(int(terminal)): + raise ZytLoginError( + "ZYT 尚未开通企微客服手机端身份校验(终端 9),请联系管理员升级 ZYT", + kind="terminal_unsupported", + ) + if str(identity.get("status") or "") != "active": + raise ZytLoginError("ZYT 管理员账号已停用", kind="disabled") + permissions_value = identity.get("permissions") + identity["permissions"] = ( + sorted({str(item).strip() for item in permissions_value if str(item).strip()}) + if isinstance(permissions_value, list) else [] + ) + identity["permissions_known"] = isinstance(permissions_value, list) + identity["root"] = bool(int(identity.get("root") or 0)) + return identity + + +def logout_admin(token: str, *, api_url: str | None = None, timeout: float = 5.0) -> None: + """尽力注销 ZYT 侧会话;失败不影响本地登出。""" + + token = str(token or "").strip() + if not token: + return + try: + _zyt_json( + "POST", f"{_checked_base_url(api_url)}/adminapi/login/logout", + payload={}, token=token, timeout=timeout, + ) + except ZytAuthError: + return diff --git a/wechat_rpa/zyt_order_client.py b/wechat_rpa/zyt_order_client.py new file mode 100644 index 000000000..5cd7f6f67 --- /dev/null +++ b/wechat_rpa/zyt_order_client.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +"""ZYT 患者订单只读适配器。 + +订单查询必须经过 ZYT 的管理端接口,桌面端不直接打开 ``C:\\zyt`` 的源码、 +数据库或配置文件。本模块只负责 HTTP、响应解析和脱敏;患者、租户和会话归属 +由 ``archive_api`` 的桌面代理再次校验。 +""" + +from __future__ import annotations + +import json +import re +import socket +import urllib.error +import urllib.parse +import urllib.request +from datetime import datetime, timezone +from typing import Any + + +class ZytOrderError(RuntimeError): + """ZYT 订单查询失败。""" + + +class ZytOrderSessionExpired(ZytOrderError): + """ZYT 管理端会话已过期。""" + + +class ZytOrderPermissionDenied(ZytOrderError): + """当前 ZYT 账号没有订单查询权限。""" + + +class ZytOrderNotFound(ZytOrderError): + """订单不存在或不在当前账号数据范围内。""" + + +class ZytOrderTrackingUnavailable(ZytOrderError): + """订单还没有快递单号,或物流服务暂时查不到真实轨迹。""" + + def __init__(self, message: str, *, code: str = "tracking_unavailable"): + super().__init__(message) + self.code = code + + +def _expired_message(message: str) -> bool: + text = str(message or "").lower().replace(" ", "") + return any(token in text for token in ( + "登录超时", "登录已失效", "登录失效", "登录已过期", "登录过期", + "请重新登录", "请先登录", "token过期", "token失效", "unauthorized", + )) + + +def mask_tracking_number(value: Any) -> str: + """只保留首两位和末四位,避免把完整运单号交给模型。""" + + number = re.sub(r"\s+", "", str(value or "")) + if not number: + return "" + if len(number) <= 6: + return "*" * len(number) + return f"{number[:2]}****{number[-4:]}" + + +def _date_text(value: Any) -> str: + if value in (None, ""): + return "" + if isinstance(value, (int, float)): + try: + return datetime.fromtimestamp(float(value), tz=timezone.utc).astimezone().strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (OverflowError, OSError, ValueError): + return "" + return str(value).strip()[:40] + + +def _first(row: dict[str, Any], *names: str) -> Any: + for name in names: + value = row.get(name) + if value not in (None, ""): + return value + return "" + + +def _audit_text(value: Any) -> str: + if isinstance(value, str) and value.strip() and not value.strip().isdigit(): + return value.strip() + try: + return {0: "待审核", 1: "已通过", 2: "已驳回"}.get(int(value), "未知") + except (TypeError, ValueError): + return str(value or "") + + +def _fulfillment_text(value: Any) -> str: + if isinstance(value, str) and value.strip() and not value.strip().isdigit(): + return value.strip() + try: + return { + 1: "待双审通过", 2: "待发货", 3: "已完成", 4: "已取消", + 5: "已发货", 6: "已签收", 7: "进行中", 8: "暂不制药", + 9: "拒收", 10: "退款", 11: "保留药方", 12: "制药缓发", + }.get(int(value), "未知") + except (TypeError, ValueError): + return str(value or "") + + +class ZytOrderClient: + """调用 ZYT 已存在的管理端订单、详情和物流只读接口。""" + + ORDER_LIST_PATH = "/adminapi/firstvisit.myPatient/orders" + ORDER_DETAIL_PATH = "/adminapi/firstvisit.myPatient/orderDetail" + TRACKING_PATH = "/adminapi/tcm.prescriptionOrder/logisticsTrace" + + def __init__(self, api_url: str, token: str, *, timeout: float = 12.0): + self.api_url = str(api_url or "").strip().rstrip("/") + self.token = str(token or "").strip() + self.timeout = max(2.0, min(float(timeout), 30.0)) + parsed = urllib.parse.urlparse(self.api_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: + raise ZytOrderError("ZYT 订单接口地址配置不正确") + if not self.token: + raise ZytOrderSessionExpired("ZYT 订单查询会话缺失,请重新验证账号") + + def _get(self, path: str, params: dict[str, Any]) -> Any: + query = urllib.parse.urlencode( + {key: value for key, value in params.items() if value not in (None, "")} + ) + url = f"{self.api_url}{path}" + (f"?{query}" if query else "") + request = urllib.request.Request( + url, + headers={ + "Accept": "application/json", + "User-Agent": "WeCom-RPA-Order-Bridge/1.0", + # ZYT 管理端中间件使用 token 头,不使用 Bearer。 + "token": self.token, + }, + method="GET", + ) + try: + with urllib.request.urlopen(request, timeout=self.timeout) as response: + payload = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + if exc.code == 401: + raise ZytOrderSessionExpired("ZYT 订单查询登录已过期,请重新验证账号") from exc + if exc.code == 403: + raise ZytOrderPermissionDenied("当前 ZYT 账号没有患者订单查询权限") from exc + raise ZytOrderError(f"ZYT 订单接口请求失败(HTTP {exc.code})") from exc + except (urllib.error.URLError, TimeoutError, socket.timeout) as exc: + raise ZytOrderError( + f"无法连接 ZYT 订单接口:{getattr(exc, 'reason', exc)}" + ) from exc + except (ValueError, TypeError, UnicodeDecodeError) as exc: + raise ZytOrderError("ZYT 订单接口返回了无效数据") from exc + + if not isinstance(payload, dict): + raise ZytOrderError("ZYT 订单接口返回了无效数据") + if str(payload.get("code")) != "1": + message = str(payload.get("msg") or payload.get("message") or "").strip() if isinstance(payload, dict) else "" + if "权限" in message or "无权" in message or "禁止" in message: + raise ZytOrderPermissionDenied(message) + if str(payload.get("code")) in {"-1", "401", "403"} or _expired_message(message): + raise ZytOrderSessionExpired(message or "ZYT 订单查询会话已失效") + if "订单不存在" in message or "无权操作" in message: + raise ZytOrderNotFound(message) + if "未填写快递单号" in message or "暂无快递单号" in message: + raise ZytOrderTrackingUnavailable(message, code="tracking_number_missing") + if path == self.TRACKING_PATH: + raise ZytOrderTrackingUnavailable(message or "物流服务暂时查不到轨迹") + raise ZytOrderError(message or "ZYT 订单查询失败") + return payload.get("data") + + @staticmethod + def _list_payload(data: Any) -> tuple[list[dict[str, Any]], int]: + if isinstance(data, list): + return [dict(row) for row in data if isinstance(row, dict)], len(data) + if not isinstance(data, dict): + return [], 0 + raw = data.get("lists") + if not isinstance(raw, list): + raw = data.get("items") + if not isinstance(raw, list): + raw = data.get("data") + rows = [dict(row) for row in raw if isinstance(row, dict)] if isinstance(raw, list) else [] + try: + total = max(0, int(data.get("count", data.get("total", len(rows))))) + except (TypeError, ValueError): + total = len(rows) + return rows, total + + def list_orders( + self, + *, + diagnosis_id: int = 0, + keyword: str = "", + page_no: int = 1, + page_size: int = 20, + start_date: str = "", + end_date: str = "", + ) -> dict[str, Any]: + page_no = max(1, int(page_no)) + page_size = max(1, min(int(page_size), 50)) + keyword = str(keyword or "").strip()[:80] + # MyPatientOrderLists 的数字 keyword 可按 diagnosis_id 精确命中;绑定 + # 没有诊单号时才使用调用方给出的关键词。 + if not keyword and int(diagnosis_id or 0) > 0: + keyword = str(int(diagnosis_id)) + data = self._get( + self.ORDER_LIST_PATH, + { + "keyword": keyword, + "page_no": page_no, + "page_size": page_size, + "start_date": str(start_date or "").strip(), + "end_date": str(end_date or "").strip(), + }, + ) + rows, total = self._list_payload(data) + return {"items": rows, "total": total, "page_no": page_no, "page_size": page_size} + + def get_order_detail(self, order_id: int) -> dict[str, Any]: + order_id = int(order_id) + if order_id <= 0: + raise ValueError("订单编号不正确") + data = self._get(self.ORDER_DETAIL_PATH, {"id": order_id}) + if not isinstance(data, dict): + raise ZytOrderNotFound("订单不存在或无权查看") + return dict(data) + + def get_tracking(self, order_id: int, *, phone_tail: str = "") -> dict[str, Any]: + order_id = int(order_id) + if order_id <= 0: + raise ValueError("订单编号不正确") + data = self._get( + self.TRACKING_PATH, + {"id": order_id, "phone_tail": re.sub(r"\D", "", str(phone_tail or ""))[-4:]}, + ) + return dict(data) if isinstance(data, dict) else {} + + +def sanitize_order(row: dict[str, Any], *, queried_at: str = "") -> dict[str, Any]: + """仅保留客服回答所需事实,并移除姓名、地址、金额和处方明细。""" + + row = dict(row or {}) + tracking_number = str(_first(row, "tracking_number", "trackingNumber") or "").strip() + return { + "order_id": int(_first(row, "id", "order_id") or 0), + "order_no": str(_first(row, "order_no", "orderNo") or "")[:80], + "diagnosis_id": int(_first(row, "diagnosis_id", "diagnosisId") or 0), + "order_time": _date_text(_first(row, "create_time", "order_time", "createTime")), + "prescription_audit": _audit_text(_first(row, "prescription_audit_text", "prescription_audit_status")), + "payment_audit": _audit_text(_first(row, "payment_slip_audit_text", "payment_audit_text", "payment_slip_audit_status")), + "fulfillment_status": _fulfillment_text(_first(row, "fulfillment_text", "fulfillment_status")), + "express_company": str(_first(row, "express_company_name", "express_company") or "")[:40], + "tracking_number_masked": mask_tracking_number(tracking_number), + "has_tracking_number": bool(tracking_number), + "queried_at": queried_at, + } + + +def sanitize_tracking(payload: dict[str, Any], *, queried_at: str = "") -> dict[str, Any]: + """把物流接口的多种字段形状归一为一条可审计的最新轨迹。""" + + data = dict(payload or {}) + traces = data.get("traces") or data.get("trace") or data.get("list") or [] + if not isinstance(traces, list): + traces = [] + normalized = [] + for trace in traces[:50]: + if not isinstance(trace, dict): + continue + normalized.append({ + "time": _date_text(_first(trace, "trace_time", "accept_time", "time", "created_at")), + "description": str(_first(trace, "trace_context", "remark", "context", "description") or "")[:500], + }) + normalized.sort(key=lambda item: item.get("time") or "", reverse=True) + latest = normalized[0] if normalized else {} + return { + "status": str(_first(data, "state_text", "current_state_text", "status", "state") or ""), + "time": str(latest.get("time") or _first(data, "latest_trace_time", "last_query_time") or ""), + "description": str(latest.get("description") or _first(data, "latest_trace_context", "hint") or "")[:500], + "traces": normalized[:20], + "queried_at": queried_at, + } + diff --git a/wechat_rpa/zyt_patient_client.py b/wechat_rpa/zyt_patient_client.py index 5d2cdacb7..4f84c8969 100644 --- a/wechat_rpa/zyt_patient_client.py +++ b/wechat_rpa/zyt_patient_client.py @@ -191,3 +191,47 @@ class ZytPatientClient: "source_update_time": str(row.get("update_time") or ""), } return None + + def get_patient_diagnosis( + self, patient_id: int, diagnosis_id: int, *, max_pages: int = 20 + ) -> dict[str, Any] | None: + """Validate one diagnosis within an exact, scoped patient lookup. + + The normal keyword search only matches names/phones and deduplicates + each patient to the latest diagnosis. Neither behavior is suitable for + validating a historical binding, so inspect the raw filtered rows. + """ + patient_id = int(patient_id) + diagnosis_id = int(diagnosis_id) + if patient_id <= 0 or diagnosis_id <= 0: + raise ValueError("患者或诊单编号不正确") + page_size = 50 + page_limit = max(1, min(int(max_pages), 20)) + for page_no in range(1, page_limit + 1): + data = self._get( + "/adminapi/tcm.diagnosis/lists", + {"patient_id": patient_id, "page_no": page_no, "page_size": page_size}, + ) + rows, total = self._list_payload(data) + for row in rows: + try: + row_diagnosis_id = int(row.get("id") or 0) + row_patient_id = int(row.get("patient_id") or row_diagnosis_id) + except (TypeError, ValueError): + continue + if row_diagnosis_id != diagnosis_id or row_patient_id != patient_id: + continue + return { + "patient_id": row_patient_id, + "diagnosis_id": row_diagnosis_id, + "patient_name": str(row.get("patient_name") or "").strip(), + "phone_masked": mask_mobile(row.get("phone")), + "gender": row.get("gender"), + "age": row.get("age"), + "source_update_time": str(row.get("update_time") or ""), + } + if page_no * page_size >= total: + return None + if not rows: + raise ZytPatientError("ZYT 患者诊单列表未完整返回,无法核验绑定") + raise ZytPatientError("ZYT 患者诊单超过核验范围,无法确认绑定,请人工核对")