{
  "config": {
    "argv": [
      "/opt/hostedtoolcache/node/20.20.2/x64/bin/node",
      "/home/runner/work/dr-card-web/dr-card-web/node_modules/.bin/playwright",
      "test",
      "tests/e2e/test-gate.spec.ts"
    ],
    "configFile": "/home/runner/work/dr-card-web/dr-card-web/playwright.config.ts",
    "rootDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
    "failOnFlakyTests": false,
    "forbidOnly": true,
    "fullyParallel": true,
    "globalSetup": null,
    "globalTeardown": null,
    "globalTimeout": 0,
    "grep": {},
    "grepInvert": null,
    "maxFailures": 0,
    "metadata": {
      "ci": {
        "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/bb1948c3c0a910c3a735d6901ae22dc538c42c62",
        "commitHash": "bb1948c3c0a910c3a735d6901ae22dc538c42c62",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/279",
        "prTitle": "promote(dev->test): verified H5 changes and Buddy QR origin",
        "prBaseHash": "b1d51bc34b8045e96ec4cc54777bd159e601a0c8",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/41609532737"
      },
      "gitCommit": {
        "shortHash": "bb1948c",
        "hash": "bb1948c3c0a910c3a735d6901ae22dc538c42c62",
        "subject": "Merge 885ec7a2fd25f9baae818d20e71ecbe74a362436 into b1d51bc34b8045e96ec4cc54777bd159e601a0c8",
        "body": "Merge 885ec7a2fd25f9baae818d20e71ecbe74a362436 into b1d51bc34b8045e96ec4cc54777bd159e601a0c8\n",
        "author": {
          "name": "SINGING-AKIBA",
          "email": "980325684@qq.com",
          "time": 1793697318000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1793697318000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/.env.example b/.env.example\nindex 74fad26..ba28cd8 100644\n--- a/.env.example\n+++ b/.env.example\n@@ -38,5 +38,5 @@ VITE_BRIDGE_MODE=disabled\n # VITE_BUILD_ID=local\n # VITE_SOURCE_BRANCH=local\n \n-# 正式洗头搭子邀请二维码公开站点；test/prod 请分别配置，缺失时显式失败\n-# VITE_BUDDY_PUBLIC_ORIGIN=https://test.dr-card-ui.pages.dev\n+# 正式洗头搭子邀请二维码公开站点；test 默认值维护在 .env.test，prod 由生产环境单独配置。\n+# VITE_BUDDY_PUBLIC_ORIGIN=https://kbs.3cgroup.cn\ndiff --git a/.env.test b/.env.test\nindex b08347f..e9851ae 100644\n--- a/.env.test\n+++ b/.env.test\n@@ -1,4 +1,6 @@\n # Public build-time configuration for the formal Test H5.\n-# Not a secret. Loaded by scripts/build-h5.mjs through Vite's \"test\" mode.\n-# The backend API remains configured outside this file.\n-VITE_BUDDY_PUBLIC_ORIGIN=https://test.dr-card-ui.pages.dev\n+# Not a secret. This is the official origin used by Buddy invitation QR URLs.\n+# Keep this as the single default source for test builds; workflows must not\n+# override it with an empty GitHub Actions variable.\n+# The backend API remains configured by GitHub Actions / deployment settings.\n+VITE_BUDDY_PUBLIC_ORIGIN=https://kbs.3cgroup.cn\ndiff --git a/.github/workflows/build.yml b/.github/workflows/build.yml\nindex caaa5de..e8bcab9 100644\n--- a/.github/workflows/build.yml\n+++ b/.github/workflows/build.yml\n@@ -111,7 +111,7 @@ jobs:\n           grep -F '\"dataMode\":\"mock\"' /tmp/cf-auto-build.txt\n           grep -F '\"apiBaseConfigured\":false' /tmp/cf-auto-build.txt\n \n-          CF_PAGES_BRANCH=test CF_PAGES_COMMIT_SHA=\"$GITHUB_SHA\" VITE_API_BASE_URL=http://example.invalid VITE_BUDDY_PUBLIC_ORIGIN=https://test.dr-card-ui.pages.dev node scripts/build-h5.mjs cf --dry-run | tee /tmp/cf-test-build.txt\n+          CF_PAGES_BRANCH=test CF_PAGES_COMMIT_SHA=\"$GITHUB_SHA\" VITE_API_BASE_URL=http://example.invalid VITE_BUDDY_PUBLIC_ORIGIN=https://kbs.3cgroup.cn node scripts/build-h5.mjs cf --dry-run | tee /tmp/cf-test-build.txt\n           grep -F '\"appEnvironment\":\"test\"' /tmp/cf-test-build.txt\n           grep -F '\"dataMode\":\"api\"' /tmp/cf-test-build.txt\n           grep -F '\"bridgeMode\":\"native\"' /tmp/cf-test-build.txt\n@@ -194,13 +194,12 @@ jobs:\n       - name: Build test bundle\n         env:\n           VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}\n-          # CI build fixture; actual Pages deployments must configure their own official origin.\n-          VITE_BUDDY_PUBLIC_ORIGIN: https://test.dr-card-ui.pages.dev\n         run: npm run build:test\n \n       - name: Verify test build identity\n         run: |\n-          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true||m.buddyPublicOriginConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const files=fs.readdirSync('dist/assets').filter(x=>x.endsWith('.js'));if(!files.some(x=>fs.readFileSync('dist/assets/'+x,'utf8').includes('https://kbs.3cgroup.cn')))process.exit(1)\"\n           test ! -f dist/mockServiceWorker.js\n \n       - name: Reject Mock in production-like builds\n@@ -281,7 +280,6 @@ jobs:\n       VITE_APP_ENV: test\n       VITE_DATA_MODE: api\n       VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}\n-      VITE_BUDDY_PUBLIC_ORIGIN: https://test.dr-card-ui.pages.dev\n     steps:\n       - name: Checkout\n         uses: actions/checkout@v4\n@@ -308,7 +306,8 @@ jobs:\n \n       - name: Verify test bundle identity\n         run: |\n-          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true||m.buddyPublicOriginConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const files=fs.readdirSync('dist/assets').filter(x=>x.endsWith('.js'));if(!files.some(x=>fs.readFileSync('dist/assets/'+x,'utf8').includes('https://kbs.3cgroup.cn')))process.exit(1)\"\n \n       - name: Verify test bundle has no Mock Service Worker asset\n         shell: bash\ndiff --git a/.github/workflows/oss-web-nonprod.yml b/.github/workflows/oss-web-nonprod.yml\nindex 5433b76..cc1abc7 100644\n--- a/.github/workflows/oss-web-nonprod.yml\n+++ b/.github/workflows/oss-web-nonprod.yml\n@@ -88,10 +88,9 @@ jobs:\n       OSS_ENDPOINT: https://oss-cn-guangzhou.aliyuncs.com\n       OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}\n       OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}\n-      # Vite must receive the test Environment Variables at package build time.\n-      # Missing values still allow artifact upload but are not API-ready.\n+      # The test Buddy QR origin is read from the checked-in .env.test file.\n+      # Do not inject an empty OSS_PUBLIC_ORIGIN variable over that mode config.\n       VITE_API_BASE_URL: ${{ vars.OSS_API_BASE_URL }}\n-      VITE_BUDDY_PUBLIC_ORIGIN: ${{ vars.OSS_PUBLIC_ORIGIN }}\n       OSS_WEB_TARGET: ${{ github.ref_name == 'preview' && 'ui' || github.ref_name }}\n       H5_OSS_ARTIFACT: '1'\n     steps:\n@@ -119,6 +118,9 @@ jobs:\n           if [[ \"$mode\" == \"ui\" ]]; then mode=preview; fi\n           node scripts/build-h5.mjs \"$mode\"\n           node scripts/verify-oss-web-package.mjs \"$OSS_WEB_TARGET\"\n+          if [[ \"$OSS_WEB_TARGET\" == \"test\" ]]; then\n+            node -e \"const fs=require('fs');const files=fs.readdirSync('dist/assets').filter(x=>x.endsWith('.js'));if(!files.some(x=>fs.readFileSync('dist/assets/'+x,'utf8').includes('https://kbs.3cgroup.cn')))process.exit(1)\"\n+          fi\n           test ! \"$OSS_WEB_TARGET\" = \"prod\"\n       - name: Install official ossutil (pinned SHA256)\n         shell: bash\ndiff --git a/docs/api/chat-service-contract-20261009.md b/docs/api/chat-service-contract-20261009.md\nindex 4eab1bb..6a839a9 100644\n--- a/docs/api/chat-service-contract-20261009.md\n+++ b/docs/api/chat-service-contract-20261009.md\n@@ -5,7 +5,7 @@\n ## 1. 通用认证与身份安全\n \n - 所有以下 HTTP 接口均使用 `Authorization: Bearer <accessToken>`。\n-- Socket.IO 握手通过 `auth: { token: accessToken }` 传递 token；不要把 token 放在 URL 或 query string。\n+- Socket.IO 握手通过 `auth: { token: 'Bearer ' + accessToken }` 传递 token，格式与 HTTP API 的 `Authorization: Bearer <accessToken>` 一致；不要把 token 放在 URL 或 query string。\n - H5 不传 `user_id` 或 `sender_id`，用户身份由服务端根据 token 确定。\n - Socket 鉴权失败时，服务端可能先发送 `error` 再断开；前端应进入未授权状态并停止无限重连。token 更新后，使用新 token 重建 socket。\n \n@@ -59,7 +59,7 @@ Content-Type: application/json\n \n ```ts\n io('https://api.example.com/socket.io', {\n-  auth: { token: accessToken },\n+  auth: { token: `Bearer ${accessToken}` },\n   // Engine.IO transport path；它与上面的 namespace URL 不同\n   path: '/socket.io/',\n })\ndiff --git a/docs/deploy/oss-web-fixed-nonprod.md b/docs/deploy/oss-web-fixed-nonprod.md\nindex fa2c0a4..16ec9f5 100644\n--- a/docs/deploy/oss-web-fixed-nonprod.md\n+++ b/docs/deploy/oss-web-fixed-nonprod.md\n@@ -36,7 +36,7 @@ Bucket `kbs-sdl`（`cn-guangzhou`）：\n | `OSS_ACCESS_KEY_ID` | Repository 或 Environment Secret | OSS 写/读凭据；Environment 同名值可覆盖仓库级值 |\n | `OSS_ACCESS_KEY_SECRET` | Repository 或 Environment Secret | 对应密钥，不进入源代码、`VITE_*` 或公开日志 |\n \n-缺少 OSS 凭据时部署作业**明确失败**。测试 API / 邀请 Origin 不再作为静态包上传前置：固定目录可以先落 OSS 并完成人工页面验证，`build-meta.json` 会如实记录 API / Origin 是否配���；真实业务连通性继续在 #171 单独验收。项目原有 `Build` 与 CF 部署工作流继续保留。\n+缺少 OSS 凭据时部署作业**明确失败**。测试 API 根地址与真实业务连通性仍是单独验收项；洗头搭子二维码 Origin 则是 test 包的必需配置，单一默认值维护在 `.env.test`：`https://kbs.3cgroup.cn`。部署工作流不得用空的 `OSS_PUBLIC_ORIGIN` 覆盖它；测试构建与 OSS 包校验会确认最终 JS 资源包含该官方域名。项目原有 `Build` 与 CF 部署工作流继续保留。\n \n ## CI / 发布流程\n \n@@ -51,6 +51,8 @@ Bucket `kbs-sdl`（`cn-guangzhou`）：\n \n ## 边界与待确认项目\n \n+- 洗头搭子二维码编码根路径 `/buddy/invite/scan?code=...`。静态包位于 `/kbs-web/test/`，两者并非同一路径；公司网关仍须把根路径邀请入口映射到测试 H5 引导页并保留 `code` 查询参数。此公网路由映射需独立验收，本次只修正构建 Origin，不代表扫码链路已经可用。\n+\n - **本卡未重写生产流程**：现有 `oss-production.yml` 仍写旧不可变版本目录；#169/#170 联合接入并完成安全门禁前不得切换 `prod/last`。\n - 现有 Cloudflare 构建及站点仍按原配置运行。\n - OSS 上原 `h5/releases/`、`_ci-smoke/` 等文件不动，无任何批量删除。\ndiff --git a/scripts/verify-oss-web-package.mjs b/scripts/verify-oss-web-package.mjs\nindex bc922e2..47aa0dd 100644\n--- a/scripts/verify-oss-web-package.mjs\n+++ b/scripts/verify-oss-web-package.mjs\n@@ -19,11 +19,12 @@ if (meta.appEnvironment !== allowed[target] ||\n   throw new Error(`OSS package build identity mismatch for ${target}`)\n }\n if (target === 'test' || target === 'prod') {\n-  // API/origin readiness is business acceptance, not a prerequisite for\n-  // uploading a fixed-path test/prod bundle for human verification.\n+  // API connectivity is a separate acceptance item; the test QR origin is\n+  // required because the test package must generate official invitation URLs.\n   if (meta.dataMode !== 'api' || meta.bridgeMode !== 'native' ||\n+      (target === 'test' && !meta.buddyPublicOriginConfigured) ||\n       existsSync(join(root, 'mockServiceWorker.js'))) {\n-    throw new Error('Formal H5 cannot ship Mock or invalid Native bridge')\n+    throw new Error('Formal H5 requires API mode, Native bridge, and a configured test Buddy QR origin')\n   }\n } else if (meta.dataMode !== 'mock' || !existsSync(join(root, 'mockServiceWorker.js'))) {\n   throw new Error('UI/development fixture bundle must preserve its Mock worker')\ndiff --git a/src/App.tsx b/src/App.tsx\nindex fab6c4f..661efab 100644\n--- a/src/App.tsx\n+++ b/src/App.tsx\n@@ -8,8 +8,13 @@ import { getAuthSession, type NewUserPopup } from './services/auth/session'\n export default function App() {\n   const [newUserPopup, setNewUserPopup] = useState<NewUserPopup | null>(null)\n   const popupHandledThisDocument = useRef(false)\n-  // Hide the new-user popup only until the initial router replace is resolved.\n-  const routerInitialized = useSyncExternalStore(router.subscribe, () => router.state.initialized)\n+\n+  // Hide the new-user dialog only while React Router resolves the initial\n+  // deep-link loader. Once resolved, preserve its existing global behavior.\n+  const routerInitialized = useSyncExternalStore(\n+    router.subscribe,\n+    () => router.state.initialized,\n+  )\n   const entryRedirectPending = initialEntryRedirect !== null && !routerInitialized\n \n   useEffect(() => {\ndiff --git a/src/app/router/index.tsx b/src/app/router/index.tsx\nindex 6d5e56e..8fa14ea 100644\n--- a/src/app/router/index.tsx\n+++ b/src/app/router/index.tsx\n@@ -280,7 +280,7 @@ export const router = createBrowserRouter([\n         else if (route.boundary === 'webview') element = <WebViewBoundary />\n         return { path: route.path, ...(route.path === '/' ? { loader: entryRedirectLoader } : {}), element }\n       }),\n-      // OSS static index.html is a valid document URL and shares the Home route.\n+      // Explicit OSS index.html is a real object URL; keep the home route identical without a redirect parameter.\n       { path: '/index.html', loader: entryRedirectLoader, element: customPages['/'] },\n       /* T002 品牌 Token 展示页（工程参照页，不属于业务节点，不进入业务导航） */\n       { path: '/tokens', element: <Tokens /> },\ndiff --git a/src/app/router/routes.ts b/src/app/router/routes.ts\nindex 818587c..80f41f6 100644\n--- a/src/app/router/routes.ts\n+++ b/src/app/router/routes.ts\n@@ -1199,6 +1199,7 @@ export function isTabPath(pathname: string): boolean {\n \n /** 精确匹配；无则尝试 `:param` 动态路由匹配 */\n export function findRouteByPathname(pathname: string): RouteMeta | undefined {\n+  // OSS's explicit static document shares the same route metadata as the home entry.\n   const normalizedPath = pathname === '/index.html' ? '/' : pathname\n   const exact = ROUTES.find((route) => route.path === normalizedPath)\n   if (exact) return exact\ndiff --git a/src/services/chatHuman.test.ts b/src/services/chatHuman.test.ts\nindex 6ea51fc..4100475 100644\n--- a/src/services/chatHuman.test.ts\n+++ b/src/services/chatHuman.test.ts\n@@ -48,7 +48,7 @@ describe('#138 Socket.IO client contract', () => {\n     const status = vi.fn(), onMessage = vi.fn(), onConnected = vi.fn()\n     const handle = connectChatSocket('secret-token', { onStatus: status, onMessage, onConnected })\n     expect(mocks.io).toHaveBeenCalledWith('https://api.example.test/socket.io', expect.objectContaining({\n-      auth: { token: 'secret-token' }, path: '/socket.io/', autoConnect: false, reconnection: true,\n+      auth: { token: 'Bearer secret-token' }, path: '/socket.io/', autoConnect: false, reconnection: true,\n     }))\n     expect(mocks.socket.connect).toHaveBeenCalledOnce()\n     mocks.socket.connected = true\n@@ -77,7 +77,7 @@ describe('#138 Socket.IO client contract', () => {\n     connectChatSocket('secret-token', { onStatus: vi.fn(), onMessage: vi.fn(), onConnected: vi.fn() })\n     expect(mocks.io).toHaveBeenCalledWith('https://socket.example.test/socket.io', expect.objectContaining({\n       path: '/realtime/socket.io/',\n-      auth: { token: 'secret-token' },\n+      auth: { token: 'Bearer secret-token' },\n     }))\n   })\n \ndiff --git a/src/services/chatHuman.ts b/src/services/chatHuman.ts\nindex f6f108a..bca529c 100644\n--- a/src/services/chatHuman.ts\n+++ b/src/services/chatHuman.ts\n@@ -27,13 +27,13 @@ export function connectChatSocket(token: string, callbacks: ChatSocketEvents): C\n   // Keep it separate from path, which configures the Engine.IO transport endpoint.\n   const socket: Socket = io(`${runtimePolicy.socketBaseUrl ?? new URL(base).origin}${CHAT_SOCKET_NAMESPACE}`, {\n     path: runtimePolicy.socketPath ?? '/socket.io',\n-    auth: { token },\n+    auth: { token: `Bearer ${token}` },\n     autoConnect: false,\n     reconnection: true,\n     reconnectionDelay: 1000,\n     reconnectionDelayMax: 10_000,\n     timeout: 8000,\n-    // No token in URL/query; backend authenticates socket.handshake.auth.token\n+    // No token in URL/query; backend authenticates socket.handshake.auth.token; value uses the same Bearer scheme as HTTP APIs\n   })\n   let ended = false\n   let connectedBefore = false",
      "actualWorkers": 2
    },
    "preserveOutput": "always",
    "projects": [
      {
        "outputDir": "/home/runner/work/dr-card-web/dr-card-web/test-results/artifacts",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "ci": {
            "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/bb1948c3c0a910c3a735d6901ae22dc538c42c62",
            "commitHash": "bb1948c3c0a910c3a735d6901ae22dc538c42c62",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/279",
            "prTitle": "promote(dev->test): verified H5 changes and Buddy QR origin",
            "prBaseHash": "b1d51bc34b8045e96ec4cc54777bd159e601a0c8",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/41609532737"
          },
          "gitCommit": {
            "shortHash": "bb1948c",
            "hash": "bb1948c3c0a910c3a735d6901ae22dc538c42c62",
            "subject": "Merge 885ec7a2fd25f9baae818d20e71ecbe74a362436 into b1d51bc34b8045e96ec4cc54777bd159e601a0c8",
            "body": "Merge 885ec7a2fd25f9baae818d20e71ecbe74a362436 into b1d51bc34b8045e96ec4cc54777bd159e601a0c8\n",
            "author": {
              "name": "SINGING-AKIBA",
              "email": "980325684@qq.com",
              "time": 1793697318000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1793697318000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/.env.example b/.env.example\nindex 74fad26..ba28cd8 100644\n--- a/.env.example\n+++ b/.env.example\n@@ -38,5 +38,5 @@ VITE_BRIDGE_MODE=disabled\n # VITE_BUILD_ID=local\n # VITE_SOURCE_BRANCH=local\n \n-# 正式洗头搭子邀请二维码公开站点；test/prod 请分别配置，缺失时显式失败\n-# VITE_BUDDY_PUBLIC_ORIGIN=https://test.dr-card-ui.pages.dev\n+# 正式洗头搭子邀请二维码公开站点；test 默认值维护在 .env.test，prod 由生产环境单独配置。\n+# VITE_BUDDY_PUBLIC_ORIGIN=https://kbs.3cgroup.cn\ndiff --git a/.env.test b/.env.test\nindex b08347f..e9851ae 100644\n--- a/.env.test\n+++ b/.env.test\n@@ -1,4 +1,6 @@\n # Public build-time configuration for the formal Test H5.\n-# Not a secret. Loaded by scripts/build-h5.mjs through Vite's \"test\" mode.\n-# The backend API remains configured outside this file.\n-VITE_BUDDY_PUBLIC_ORIGIN=https://test.dr-card-ui.pages.dev\n+# Not a secret. This is the official origin used by Buddy invitation QR URLs.\n+# Keep this as the single default source for test builds; workflows must not\n+# override it with an empty GitHub Actions variable.\n+# The backend API remains configured by GitHub Actions / deployment settings.\n+VITE_BUDDY_PUBLIC_ORIGIN=https://kbs.3cgroup.cn\ndiff --git a/.github/workflows/build.yml b/.github/workflows/build.yml\nindex caaa5de..e8bcab9 100644\n--- a/.github/workflows/build.yml\n+++ b/.github/workflows/build.yml\n@@ -111,7 +111,7 @@ jobs:\n           grep -F '\"dataMode\":\"mock\"' /tmp/cf-auto-build.txt\n           grep -F '\"apiBaseConfigured\":false' /tmp/cf-auto-build.txt\n \n-          CF_PAGES_BRANCH=test CF_PAGES_COMMIT_SHA=\"$GITHUB_SHA\" VITE_API_BASE_URL=http://example.invalid VITE_BUDDY_PUBLIC_ORIGIN=https://test.dr-card-ui.pages.dev node scripts/build-h5.mjs cf --dry-run | tee /tmp/cf-test-build.txt\n+          CF_PAGES_BRANCH=test CF_PAGES_COMMIT_SHA=\"$GITHUB_SHA\" VITE_API_BASE_URL=http://example.invalid VITE_BUDDY_PUBLIC_ORIGIN=https://kbs.3cgroup.cn node scripts/build-h5.mjs cf --dry-run | tee /tmp/cf-test-build.txt\n           grep -F '\"appEnvironment\":\"test\"' /tmp/cf-test-build.txt\n           grep -F '\"dataMode\":\"api\"' /tmp/cf-test-build.txt\n           grep -F '\"bridgeMode\":\"native\"' /tmp/cf-test-build.txt\n@@ -194,13 +194,12 @@ jobs:\n       - name: Build test bundle\n         env:\n           VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}\n-          # CI build fixture; actual Pages deployments must configure their own official origin.\n-          VITE_BUDDY_PUBLIC_ORIGIN: https://test.dr-card-ui.pages.dev\n         run: npm run build:test\n \n       - name: Verify test build identity\n         run: |\n-          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true||m.buddyPublicOriginConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const files=fs.readdirSync('dist/assets').filter(x=>x.endsWith('.js'));if(!files.some(x=>fs.readFileSync('dist/assets/'+x,'utf8').includes('https://kbs.3cgroup.cn')))process.exit(1)\"\n           test ! -f dist/mockServiceWorker.js\n \n       - name: Reject Mock in production-like builds\n@@ -281,7 +280,6 @@ jobs:\n       VITE_APP_ENV: test\n       VITE_DATA_MODE: api\n       VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}\n-      VITE_BUDDY_PUBLIC_ORIGIN: https://test.dr-card-ui.pages.dev\n     steps:\n       - name: Checkout\n         uses: actions/checkout@v4\n@@ -308,7 +306,8 @@ jobs:\n \n       - name: Verify test bundle identity\n         run: |\n-          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dist/build-meta.json','utf8'));if(m.appEnvironment!=='test'||m.dataMode!=='api'||m.bridgeMode!=='native'||m.apiBaseConfigured!==true||m.buddyPublicOriginConfigured!==true)process.exit(1)\"\n+          node -e \"const fs=require('fs');const files=fs.readdirSync('dist/assets').filter(x=>x.endsWith('.js'));if(!files.some(x=>fs.readFileSync('dist/assets/'+x,'utf8').includes('https://kbs.3cgroup.cn')))process.exit(1)\"\n \n       - name: Verify test bundle has no Mock Service Worker asset\n         shell: bash\ndiff --git a/.github/workflows/oss-web-nonprod.yml b/.github/workflows/oss-web-nonprod.yml\nindex 5433b76..cc1abc7 100644\n--- a/.github/workflows/oss-web-nonprod.yml\n+++ b/.github/workflows/oss-web-nonprod.yml\n@@ -88,10 +88,9 @@ jobs:\n       OSS_ENDPOINT: https://oss-cn-guangzhou.aliyuncs.com\n       OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}\n       OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}\n-      # Vite must receive the test Environment Variables at package build time.\n-      # Missing values still allow artifact upload but are not API-ready.\n+      # The test Buddy QR origin is read from the checked-in .env.test file.\n+      # Do not inject an empty OSS_PUBLIC_ORIGIN variable over that mode config.\n       VITE_API_BASE_URL: ${{ vars.OSS_API_BASE_URL }}\n-      VITE_BUDDY_PUBLIC_ORIGIN: ${{ vars.OSS_PUBLIC_ORIGIN }}\n       OSS_WEB_TARGET: ${{ github.ref_name == 'preview' && 'ui' || github.ref_name }}\n       H5_OSS_ARTIFACT: '1'\n     steps:\n@@ -119,6 +118,9 @@ jobs:\n           if [[ \"$mode\" == \"ui\" ]]; then mode=preview; fi\n           node scripts/build-h5.mjs \"$mode\"\n           node scripts/verify-oss-web-package.mjs \"$OSS_WEB_TARGET\"\n+          if [[ \"$OSS_WEB_TARGET\" == \"test\" ]]; then\n+            node -e \"const fs=require('fs');const files=fs.readdirSync('dist/assets').filter(x=>x.endsWith('.js'));if(!files.some(x=>fs.readFileSync('dist/assets/'+x,'utf8').includes('https://kbs.3cgroup.cn')))process.exit(1)\"\n+          fi\n           test ! \"$OSS_WEB_TARGET\" = \"prod\"\n       - name: Install official ossutil (pinned SHA256)\n         shell: bash\ndiff --git a/docs/api/chat-service-contract-20261009.md b/docs/api/chat-service-contract-20261009.md\nindex 4eab1bb..6a839a9 100644\n--- a/docs/api/chat-service-contract-20261009.md\n+++ b/docs/api/chat-service-contract-20261009.md\n@@ -5,7 +5,7 @@\n ## 1. 通用认证与身份安全\n \n - 所有以下 HTTP 接口均使用 `Authorization: Bearer <accessToken>`。\n-- Socket.IO 握手通过 `auth: { token: accessToken }` 传递 token；不要把 token 放在 URL 或 query string。\n+- Socket.IO 握手通过 `auth: { token: 'Bearer ' + accessToken }` 传递 token，格式与 HTTP API 的 `Authorization: Bearer <accessToken>` 一致；不要把 token 放在 URL 或 query string。\n - H5 不传 `user_id` 或 `sender_id`，用户身份由服务端根据 token 确定。\n - Socket 鉴权失败时，服务端可能先发送 `error` 再断开；前端应进入未授权状态并停止无限重连。token 更新后，使用新 token 重建 socket。\n \n@@ -59,7 +59,7 @@ Content-Type: application/json\n \n ```ts\n io('https://api.example.com/socket.io', {\n-  auth: { token: accessToken },\n+  auth: { token: `Bearer ${accessToken}` },\n   // Engine.IO transport path；它与上面的 namespace URL 不同\n   path: '/socket.io/',\n })\ndiff --git a/docs/deploy/oss-web-fixed-nonprod.md b/docs/deploy/oss-web-fixed-nonprod.md\nindex fa2c0a4..16ec9f5 100644\n--- a/docs/deploy/oss-web-fixed-nonprod.md\n+++ b/docs/deploy/oss-web-fixed-nonprod.md\n@@ -36,7 +36,7 @@ Bucket `kbs-sdl`（`cn-guangzhou`）：\n | `OSS_ACCESS_KEY_ID` | Repository 或 Environment Secret | OSS 写/读凭据；Environment 同名值可覆盖仓库级值 |\n | `OSS_ACCESS_KEY_SECRET` | Repository 或 Environment Secret | 对应密钥，不进入源代码、`VITE_*` 或公开日志 |\n \n-缺少 OSS 凭据时部署作业**明确失败**。测试 API / 邀请 Origin 不再作为静态包上传前置：固定目录可以先落 OSS 并完成人工页面验证，`build-meta.json` 会如实记录 API / Origin 是否配���；真实业务连通性继续在 #171 单独验收。项目原有 `Build` 与 CF 部署工作流继续保留。\n+缺少 OSS 凭据时部署作业**明确失败**。测试 API 根地址与真实业务连通性仍是单独验收项；洗头搭子二维码 Origin 则是 test 包的必需配置，单一默认值维护在 `.env.test`：`https://kbs.3cgroup.cn`。部署工作流不得用空的 `OSS_PUBLIC_ORIGIN` 覆盖它；测试构建与 OSS 包校验会确认最终 JS 资源包含该官方域名。项目原有 `Build` 与 CF 部署工作流继续保留。\n \n ## CI / 发布流程\n \n@@ -51,6 +51,8 @@ Bucket `kbs-sdl`（`cn-guangzhou`）：\n \n ## 边界与待确认项目\n \n+- 洗头搭子二维码编码根路径 `/buddy/invite/scan?code=...`。静态包位于 `/kbs-web/test/`，两者并非同一路径；公司网关仍须把根路径邀请入口映射到测试 H5 引导页并保留 `code` 查询参数。此公网路由映射需独立验收，本次只修正构建 Origin，不代表扫码链路已经可用。\n+\n - **本卡未重写生产流程**：现有 `oss-production.yml` 仍写旧不可变版本目录；#169/#170 联合接入并完成安全门禁前不得切换 `prod/last`。\n - 现有 Cloudflare 构建及站点仍按原配置运行。\n - OSS 上原 `h5/releases/`、`_ci-smoke/` 等文件不动，无任何批量删除。\ndiff --git a/scripts/verify-oss-web-package.mjs b/scripts/verify-oss-web-package.mjs\nindex bc922e2..47aa0dd 100644\n--- a/scripts/verify-oss-web-package.mjs\n+++ b/scripts/verify-oss-web-package.mjs\n@@ -19,11 +19,12 @@ if (meta.appEnvironment !== allowed[target] ||\n   throw new Error(`OSS package build identity mismatch for ${target}`)\n }\n if (target === 'test' || target === 'prod') {\n-  // API/origin readiness is business acceptance, not a prerequisite for\n-  // uploading a fixed-path test/prod bundle for human verification.\n+  // API connectivity is a separate acceptance item; the test QR origin is\n+  // required because the test package must generate official invitation URLs.\n   if (meta.dataMode !== 'api' || meta.bridgeMode !== 'native' ||\n+      (target === 'test' && !meta.buddyPublicOriginConfigured) ||\n       existsSync(join(root, 'mockServiceWorker.js'))) {\n-    throw new Error('Formal H5 cannot ship Mock or invalid Native bridge')\n+    throw new Error('Formal H5 requires API mode, Native bridge, and a configured test Buddy QR origin')\n   }\n } else if (meta.dataMode !== 'mock' || !existsSync(join(root, 'mockServiceWorker.js'))) {\n   throw new Error('UI/development fixture bundle must preserve its Mock worker')\ndiff --git a/src/App.tsx b/src/App.tsx\nindex fab6c4f..661efab 100644\n--- a/src/App.tsx\n+++ b/src/App.tsx\n@@ -8,8 +8,13 @@ import { getAuthSession, type NewUserPopup } from './services/auth/session'\n export default function App() {\n   const [newUserPopup, setNewUserPopup] = useState<NewUserPopup | null>(null)\n   const popupHandledThisDocument = useRef(false)\n-  // Hide the new-user popup only until the initial router replace is resolved.\n-  const routerInitialized = useSyncExternalStore(router.subscribe, () => router.state.initialized)\n+\n+  // Hide the new-user dialog only while React Router resolves the initial\n+  // deep-link loader. Once resolved, preserve its existing global behavior.\n+  const routerInitialized = useSyncExternalStore(\n+    router.subscribe,\n+    () => router.state.initialized,\n+  )\n   const entryRedirectPending = initialEntryRedirect !== null && !routerInitialized\n \n   useEffect(() => {\ndiff --git a/src/app/router/index.tsx b/src/app/router/index.tsx\nindex 6d5e56e..8fa14ea 100644\n--- a/src/app/router/index.tsx\n+++ b/src/app/router/index.tsx\n@@ -280,7 +280,7 @@ export const router = createBrowserRouter([\n         else if (route.boundary === 'webview') element = <WebViewBoundary />\n         return { path: route.path, ...(route.path === '/' ? { loader: entryRedirectLoader } : {}), element }\n       }),\n-      // OSS static index.html is a valid document URL and shares the Home route.\n+      // Explicit OSS index.html is a real object URL; keep the home route identical without a redirect parameter.\n       { path: '/index.html', loader: entryRedirectLoader, element: customPages['/'] },\n       /* T002 品牌 Token 展示页（工程参照页，不属于业务节点，不进入业务导航） */\n       { path: '/tokens', element: <Tokens /> },\ndiff --git a/src/app/router/routes.ts b/src/app/router/routes.ts\nindex 818587c..80f41f6 100644\n--- a/src/app/router/routes.ts\n+++ b/src/app/router/routes.ts\n@@ -1199,6 +1199,7 @@ export function isTabPath(pathname: string): boolean {\n \n /** 精确匹配；无则尝试 `:param` 动态路由匹配 */\n export function findRouteByPathname(pathname: string): RouteMeta | undefined {\n+  // OSS's explicit static document shares the same route metadata as the home entry.\n   const normalizedPath = pathname === '/index.html' ? '/' : pathname\n   const exact = ROUTES.find((route) => route.path === normalizedPath)\n   if (exact) return exact\ndiff --git a/src/services/chatHuman.test.ts b/src/services/chatHuman.test.ts\nindex 6ea51fc..4100475 100644\n--- a/src/services/chatHuman.test.ts\n+++ b/src/services/chatHuman.test.ts\n@@ -48,7 +48,7 @@ describe('#138 Socket.IO client contract', () => {\n     const status = vi.fn(), onMessage = vi.fn(), onConnected = vi.fn()\n     const handle = connectChatSocket('secret-token', { onStatus: status, onMessage, onConnected })\n     expect(mocks.io).toHaveBeenCalledWith('https://api.example.test/socket.io', expect.objectContaining({\n-      auth: { token: 'secret-token' }, path: '/socket.io/', autoConnect: false, reconnection: true,\n+      auth: { token: 'Bearer secret-token' }, path: '/socket.io/', autoConnect: false, reconnection: true,\n     }))\n     expect(mocks.socket.connect).toHaveBeenCalledOnce()\n     mocks.socket.connected = true\n@@ -77,7 +77,7 @@ describe('#138 Socket.IO client contract', () => {\n     connectChatSocket('secret-token', { onStatus: vi.fn(), onMessage: vi.fn(), onConnected: vi.fn() })\n     expect(mocks.io).toHaveBeenCalledWith('https://socket.example.test/socket.io', expect.objectContaining({\n       path: '/realtime/socket.io/',\n-      auth: { token: 'secret-token' },\n+      auth: { token: 'Bearer secret-token' },\n     }))\n   })\n \ndiff --git a/src/services/chatHuman.ts b/src/services/chatHuman.ts\nindex f6f108a..bca529c 100644\n--- a/src/services/chatHuman.ts\n+++ b/src/services/chatHuman.ts\n@@ -27,13 +27,13 @@ export function connectChatSocket(token: string, callbacks: ChatSocketEvents): C\n   // Keep it separate from path, which configures the Engine.IO transport endpoint.\n   const socket: Socket = io(`${runtimePolicy.socketBaseUrl ?? new URL(base).origin}${CHAT_SOCKET_NAMESPACE}`, {\n     path: runtimePolicy.socketPath ?? '/socket.io',\n-    auth: { token },\n+    auth: { token: `Bearer ${token}` },\n     autoConnect: false,\n     reconnection: true,\n     reconnectionDelay: 1000,\n     reconnectionDelayMax: 10_000,\n     timeout: 8000,\n-    // No token in URL/query; backend authenticates socket.handshake.auth.token\n+    // No token in URL/query; backend authenticates socket.handshake.auth.token; value uses the same Bearer scheme as HTTP APIs\n   })\n   let ended = false\n   let connectedBefore = false",
          "actualWorkers": 2
        },
        "id": "chromium-mobile",
        "name": "chromium-mobile",
        "testDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "line",
        null
      ],
      [
        "html",
        {
          "outputFolder": "playwright-report",
          "open": "never"
        }
      ],
      [
        "json",
        {
          "outputFile": "test-results/playwright-results.json"
        }
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.63.0",
    "workers": 2,
    "webServer": null
  },
  "suites": [
    {
      "title": "test-gate.spec.ts",
      "file": "test-gate.spec.ts",
      "column": 0,
      "line": 0,
      "specs": [
        {
          "title": "@test-gate build identity matches the production-like test policy",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 60,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:43.309Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-c3b6e0ce5d8c5d8734e5",
          "file": "test-gate.spec.ts",
          "line": 31,
          "column": 1
        },
        {
          "title": "@test-gate browser entry does not start the app router",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1257,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:43.271Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8ed37c8041402a2a6b34",
          "file": "test-gate.spec.ts",
          "line": 40,
          "column": 1
        },
        {
          "title": "@test-gate / shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1287,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:43.455Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-b030d57053896c93a550",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /profile shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1054,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:44.925Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8605535a2807e17fcccc",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /settings shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1061,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:44.948Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-4a2fdc9884c1725de4f8",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate gated browser entry issues no business API request",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1050,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:46.000Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-09f38a2b8ed735e192d8",
          "file": "test-gate.spec.ts",
          "line": 65,
          "column": 1
        },
        {
          "title": "@test-gate injected native host bypasses the notice and starts the app",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1097,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T09:16:46.042Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-03T09:16:42.241Z",
    "duration": 4957.062,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}