diff --git a/.github/agents/comparer_of_haptest.agent.md b/.github/agents/comparer_of_haptest.agent.md new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 50f07a0..1456340 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist/ lib/ node_modules/ +package.json package-lock.json *.log *.log.* diff --git a/config.json b/config.json index da7fc57..91fd20b 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,8 @@ { "GPT_CONFIG":{ - "baseURL": "", - "apiKey": "" + "baseURL": "${YOUR_BASE_URL}", + "apiKey": "${YOUR_API_KEY}", + "siteURL": "https://github.com/SMAT-Lab/HapTest", + "appName": "HapTest" } - } \ No newline at end of file diff --git a/output.png b/output.png new file mode 100644 index 0000000..1c6b038 Binary files /dev/null and b/output.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5eac55c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4017 @@ +{ + "name": "haptest", + "version": "0.2.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "haptest", + "version": "0.2.1", + "license": "Apache License, Version 2.0", + "dependencies": { + "@ts-graphviz/adapter": "^2.0.3", + "@types/ws": "^8.5.12", + "adm-zip": "^0.5.15", + "bjc": "^1.0.22", + "class-transformer": "^0.5.1", + "commander": "^12.1.0", + "express": "^4.19.2", + "graphology": "^0.25.4", + "graphology-shortest-path": "^2.1.0", + "haptest": "^0.1.9", + "image-hash": "^7.0.1", + "log4js": "^6.9.1", + "moment": "^2.30.1", + "openai": "^5.12.2", + "promise-socket": "7.0.0", + "ts-graphviz": "^2.1.2", + "ws": "^8.18.0" + }, + "bin": { + "haptest": "bin/haptest" + }, + "devDependencies": { + "@types/adm-zip": "^0.5.5", + "@types/express": "^4.17.21", + "@types/node": "^20.14.9", + "ts-node": "^1.7.1", + "vitest": "^3.2.4" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@canvas/image-data": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.1.0.tgz", + "integrity": "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==", + "license": "MIT" + }, + "node_modules/@cwasm/webp": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@cwasm/webp/-/webp-0.1.5.tgz", + "integrity": "sha512-ceIZQkyxK+s7mmItNcWqqHdOBiJAxYxTnrnPNgUNjldB1M9j+Bp/3eVIVwC8rUFyN/zoFwuT0331pyY3ackaNA==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", + "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", + "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", + "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", + "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", + "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", + "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", + "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", + "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", + "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", + "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", + "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", + "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", + "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", + "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", + "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", + "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", + "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", + "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", + "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", + "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@ts-graphviz/adapter": { + "version": "2.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@ts-graphviz/adapter/-/adapter-2.0.3.tgz", + "integrity": "sha512-wHSN23UdLz4vuYUBZCzq2/tfLicwStSo3cUWnzvMNxG2ngcuYauQCQInv4CI5IObq+PFol28RVrG9Ffa9BuIRA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/common": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/ast": { + "version": "2.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@ts-graphviz/ast/-/ast-2.0.3.tgz", + "integrity": "sha512-NhOgJdOHGSn5h5ydsFreLIKFBwQ59drzZ6y0B98+KeEMqduv5hXxcQoDabw8yzeNe9B92AfR5OpUYthcdAsYgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/common": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/common": { + "version": "2.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@ts-graphviz/common/-/common-2.1.2.tgz", + "integrity": "sha512-Wyh5fOZNYyNP1mymbcHg/9atWR33NhHWIDrNa4hfbel3v340YQ+q+LMwAuIPuPt1qXINvOEhkowO5dvJWqfnPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/core": { + "version": "2.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@ts-graphviz/core/-/core-2.0.3.tgz", + "integrity": "sha512-EZ+XlSwjdLtscoBOnA/Ba6QBrmoxAR73tJFjnWxaJQsZxWBQv6bLUrDgZUdXkXRAOSkRHn0uXY6Wq/3SsV2WtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/ast": "^2.0.3", + "@ts-graphviz/common": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/adm-zip": { + "version": "0.5.5", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/adm-zip/-/adm-zip-0.5.5.tgz", + "integrity": "sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.24.tgz", + "integrity": "sha512-Mbrt4SRlXSTWryOnHAh2d4UQ/E7n9lZyGSi6KgX+4hkuL9soYbLOVXVhnk/ODp12YsGc95f4pOvqywJ6kngUwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-escaper": { + "version": "3.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/html-escaper/-/html-escaper-3.0.2.tgz", + "integrity": "sha512-A8vk09eyYzk8J/lFO4OUMKCmRN0rRzfZf4n3Olwapgox/PtTiU8zPYlL1UEkJ/WeHvV6v9Xnj3o/705PKz9r4Q==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "2.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==", + "deprecated": "This is a stub types definition. json5 provides its own type definitions, so you do not need this installed.", + "dependencies": { + "json5": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@yomguithereal/helpers": { + "version": "1.1.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/@yomguithereal/helpers/-/helpers-1.1.1.tgz", + "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/adm-zip": { + "version": "0.5.15", + "resolved": "https://repo.huaweicloud.com/repository/npm/adm-zip/-/adm-zip-0.5.15.tgz", + "integrity": "sha512-jYPWSeOA8EFoZnucrKCNihqBjoEGQSU4HKgHYQgKNEQ0pQF9a/DYuo/+fAxY76k4qe75LUlLWpAM1QWcBMTOKw==", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/bjc": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/bjc/-/bjc-1.0.22.tgz", + "integrity": "sha512-Vefbjbmb1vQaw4U4cBTdDVYhhKGRUemUgzLoOLeQblP4m6n5eJtgl/Bh0bhnY+Mepb08ZMudhdxGDFUp9YZ+Sg==", + "dependencies": { + "@types/html-escaper": "^3.0.2", + "@types/json5": "^2.2.0", + "commander": "^12.0.0", + "html-escaper": "^3.0.3", + "json5": "^2.2.3", + "log4js": "^6.7.1", + "typescript": "^5.1.6" + }, + "bin": { + "bjc": "bin/bjc" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://repo.huaweicloud.com/repository/npm/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-type": { + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", + "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://repo.huaweicloud.com/repository/npm/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphology": { + "version": "0.25.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/graphology/-/graphology-0.25.4.tgz", + "integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==", + "dependencies": { + "events": "^3.3.0", + "obliterator": "^2.0.2" + }, + "peerDependencies": { + "graphology-types": ">=0.24.0" + } + }, + "node_modules/graphology-indices": { + "version": "0.17.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/graphology-indices/-/graphology-indices-0.17.0.tgz", + "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==", + "dependencies": { + "graphology-utils": "^2.4.2", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-shortest-path": { + "version": "2.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/graphology-shortest-path/-/graphology-shortest-path-2.1.0.tgz", + "integrity": "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==", + "dependencies": { + "@yomguithereal/helpers": "^1.1.1", + "graphology-indices": "^0.17.0", + "graphology-utils": "^2.4.3", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-types": { + "version": "0.24.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/graphology-types/-/graphology-types-0.24.7.tgz", + "integrity": "sha512-tdcqOOpwArNjEr0gNQKCXwaNCWnQJrog14nJNQPeemcLnXQUUGrsCWpWkVKt46zLjcS6/KGoayeJfHHyPDlvwA==", + "peer": true + }, + "node_modules/graphology-utils": { + "version": "2.5.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/graphology-utils/-/graphology-utils-2.5.2.tgz", + "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", + "peerDependencies": { + "graphology-types": ">=0.23.0" + } + }, + "node_modules/haptest": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/haptest/-/haptest-0.1.9.tgz", + "integrity": "sha512-AXlGKsRhAOiSy6WzAya3H10gCqwVZbiVx/B1XVsTqjD/ECpDXF2Tzr+MxuGGz0WlBNKWesWalKi+0Oof6JDNcA==", + "license": "Apache License, Version 2.0", + "dependencies": { + "@ts-graphviz/adapter": "^2.0.3", + "@types/ws": "^8.5.12", + "adm-zip": "^0.5.15", + "bjc": "^1.0.18", + "class-transformer": "^0.5.1", + "commander": "^12.1.0", + "graphology": "^0.25.4", + "graphology-shortest-path": "^2.1.0", + "log4js": "^6.9.1", + "moment": "^2.30.1", + "promise-socket": "7.0.0", + "ts-graphviz": "^2.1.2", + "ws": "^8.18.0" + }, + "bin": { + "haptest": "bin/haptest" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-hash": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/image-hash/-/image-hash-7.0.1.tgz", + "integrity": "sha512-UFd/RfmacH3c1MISLm1k1AFOtG1py2gy46qNE8aK7UwmAEHAZGzTfsBsF6VbLxirD30p6t2GgULzSZh1XmsWQA==", + "license": "MIT", + "dependencies": { + "@cwasm/webp": "^0.1.5", + "file-type": "^21.0.0", + "jpeg-js": "^0.4.0", + "pngjs": "^7.0.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loupe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://repo.huaweicloud.com/repository/npm/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openai": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", + "integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/promise-duplex": { + "version": "6.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/promise-duplex/-/promise-duplex-6.0.0.tgz", + "integrity": "sha512-ZL7rquzjTFzInDBeWYcsT+qddolNvzigahk6MI6qLSbQvlyRRCJkU3JztgaVunzvkH28smRa2Qu/cY9RXtSkgA==", + "license": "MIT", + "dependencies": { + "core-js": "^3.6.5", + "promise-readable": "^6.0.0", + "promise-writable": "^6.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/promise-readable": { + "version": "6.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/promise-readable/-/promise-readable-6.0.0.tgz", + "integrity": "sha512-5NxtmUswijvX5cAM0zPSy6yiCXH/eKBpiiBq6JfAUrmngMquMbzcBhF2qA+ocs4rYYKdvAfv3cOvZxADLtL1CA==", + "license": "MIT", + "dependencies": { + "core-js": "^3.6.5" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/promise-socket": { + "version": "7.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/promise-socket/-/promise-socket-7.0.0.tgz", + "integrity": "sha512-Oic9BrxmcHOPEnzKp2Js+ehFyvsbd0WxsE5khweCTHuRvdzbXjHUZmSDT6F9TW8SIkAJ0lCzoHjMYnb0WQJPiw==", + "license": "MIT", + "dependencies": { + "promise-duplex": "^6.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/promise-writable": { + "version": "6.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/promise-writable/-/promise-writable-6.0.0.tgz", + "integrity": "sha512-b81zre/itgJFS7dwWzIdKNVVqvLiUxYRS/wolUB0H1YY/tAaS146XGKa4Q/5wCbsnXLyn0MCeV6f8HHe4iUHLg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/rollup": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", + "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.1", + "@rollup/rollup-android-arm64": "4.44.1", + "@rollup/rollup-darwin-arm64": "4.44.1", + "@rollup/rollup-darwin-x64": "4.44.1", + "@rollup/rollup-freebsd-arm64": "4.44.1", + "@rollup/rollup-freebsd-x64": "4.44.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", + "@rollup/rollup-linux-arm-musleabihf": "4.44.1", + "@rollup/rollup-linux-arm64-gnu": "4.44.1", + "@rollup/rollup-linux-arm64-musl": "4.44.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-musl": "4.44.1", + "@rollup/rollup-linux-s390x-gnu": "4.44.1", + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1", + "@rollup/rollup-win32-arm64-msvc": "4.44.1", + "@rollup/rollup-win32-ia32-msvc": "4.44.1", + "@rollup/rollup-win32-x64-msvc": "4.44.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://repo.huaweicloud.com/repository/npm/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-graphviz": { + "version": "2.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/ts-graphviz/-/ts-graphviz-2.1.2.tgz", + "integrity": "sha512-9GnOA3yiFaqZeHBEZXWa6kqc61FVhAhxQU5g3KLyGrhRr7OsDGRzs+1z35ctvD+hTTEhrBza6D41+qz+3qs7Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/adapter": "^2.0.3", + "@ts-graphviz/ast": "^2.0.3", + "@ts-graphviz/common": "^2.1.2", + "@ts-graphviz/core": "^2.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-node": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-1.7.1.tgz", + "integrity": "sha512-2b6YmKQ0052pEP5Y+KnBce0NkyjuQRBTLKd5XV0O/+WHLbj3CJ5rUrhB2Co1a5IaBiaC2DhynS/v4UfWtHy+2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.0", + "chalk": "^1.1.1", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "pinkie": "^2.0.4", + "source-map-support": "^0.4.0", + "tsconfig": "^5.0.2", + "v8flags": "^2.0.11", + "xtend": "^4.0.0", + "yn": "^1.2.0" + }, + "bin": { + "ts-node": "dist/bin.js" + } + }, + "node_modules/tsconfig": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-5.0.3.tgz", + "integrity": "sha512-Cq65A3kVp6BbsUgg9DRHafaGmbMb9EhAc7fjWvudNWKjkbWrt43FnrtZt6awshH1R0ocfF2Z0uxock3lVqEgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.3.0", + "parse-json": "^2.2.0", + "strip-bom": "^2.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://repo.huaweicloud.com/repository/npm/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha512-aggiKfEEubv3UwRNqTzLInZpAOmKzwdHqEBmW/hBA/mt99eg+b4VrX6i+IRLxU8+WJYfa33rGwRseg4eElUgsQ==", + "dev": true, + "license": "MIT", + "bin": { + "user-home": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha512-SKfhk/LlaXzvtowJabLZwD4K6SGRYeoxA7KJeISlUMAB/NT4CBkZjMq3WceX2Ckm4llwqYVo8TICgsDYCBU2tA==", + "dev": true, + "dependencies": { + "user-home": "^1.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/vite-node/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.1.tgz", + "integrity": "sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q==", + "dev": true, + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-1.3.0.tgz", + "integrity": "sha512-cUr+6jz1CH+E9wIGgFW5lyMMOHLbCe/UCOVqV/TTnf5XMe0NBC3TS7pR9ZpDsb84iCWKBd6ETPRBqQjssDKsIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json index a68b410..d45e8b8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "express": "^4.19.2", "graphology": "^0.25.4", "graphology-shortest-path": "^2.1.0", - "image-hash": "^5.3.2", + "haptest": "^0.1.9", + "image-hash": "^7.0.1", "log4js": "^6.9.1", "moment": "^2.30.1", "openai": "^5.12.2", @@ -37,7 +38,7 @@ "@types/adm-zip": "^0.5.5", "@types/express": "^4.17.21", "@types/node": "^20.14.9", - "ts-node": "^10.9.2", + "ts-node": "^1.7.1", "vitest": "^3.2.4" }, "files": [ diff --git a/scripts/extract_viewtree.js b/scripts/extract_viewtree.js new file mode 100644 index 0000000..3f1172f --- /dev/null +++ b/scripts/extract_viewtree.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +function fmtBounds(bounds) { + if (!bounds || !Array.isArray(bounds) || bounds.length < 2) return ''; + const a = bounds[0]; + const b = bounds[1]; + return `bounds=${a.x},${a.y}-${b.x},${b.y}`; +} + +function nodeSummary(node) { + if (!node || typeof node !== 'object') return ''; + const parts = []; + if (node.type) parts.push(node.type); + const b = fmtBounds(node.bounds || node.origBounds); + if (b) parts.push(b); + if (node.id) parts.push(`id=${node.id}`); + if (node.key) parts.push(`key=${node.key}`); + if (node.text) parts.push(`text=${String(node.text).replace(/\s+/g, ' ').slice(0,80)}`); + return parts.join(' | '); +} + +function walk(node, indent, lines) { + if (!node || typeof node !== 'object') return; + const summary = nodeSummary(node) || '(no-type)'; + lines.push(`${' '.repeat(indent)}- ${summary}`); + const children = node.children; + if (Array.isArray(children) && children.length > 0) { + for (const c of children) walk(c, indent + 1, lines); + } +} + +if (process.argv.length < 3) { + console.error('Usage: node scripts/extract_viewtree.js '); + process.exit(2); +} + +const infile = process.argv[2]; +if (!fs.existsSync(infile)) { + console.error('File not found:', infile); + process.exit(2); +} + +let obj; +try { + obj = JSON.parse(fs.readFileSync(infile, 'utf8')); +} catch (e) { + console.error('JSON parse error:', e.message); + process.exit(2); +} + +const outPath = infile.replace(/\.json$/i, '_viewtree.txt'); +const lines = []; + +function extractSide(sideName) { + const root = obj[sideName] && obj[sideName].viewTree && obj[sideName].viewTree.root; + lines.push(`${sideName.toUpperCase()} VIEWTREE:`); + if (!root) { + lines.push(' (no viewTree.root)'); + lines.push(''); + return; + } + walk(root, 0, lines); + lines.push(''); +} + +extractSide('from'); +extractSide('to'); + +fs.writeFileSync(outPath, lines.join('\n'), 'utf8'); +console.log('Wrote:', outPath); diff --git a/scripts/format_and_analyze.js b/scripts/format_and_analyze.js new file mode 100644 index 0000000..fc67c69 --- /dev/null +++ b/scripts/format_and_analyze.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +function maxDepth(obj) { + if (obj === null || typeof obj !== 'object') return 0; + if (Array.isArray(obj)) { + let m = 0; + for (const v of obj) m = Math.max(m, maxDepth(v)); + return 1 + m; + } + let m = 0; + for (const k of Object.keys(obj)) m = Math.max(m, maxDepth(obj[k])); + return 1 + m; +} + +function countViewNodes(node) { + if (!node || typeof node !== 'object') return 0; + let count = 0; + if (node.type) count = 1; + const children = node.children || []; + for (const c of children) count += countViewNodes(c); + return count; +} + +function safeGet(o, pathArr) { + try { + let cur = o; + for (const p of pathArr) { + if (cur == null) return undefined; + cur = cur[p]; + } + return cur; + } catch (e) { + return undefined; + } +} + +if (process.argv.length < 3) { + console.error('Usage: node scripts/format_and_analyze.js '); + process.exit(2); +} + +const infile = process.argv[2]; +if (!fs.existsSync(infile)) { + console.error('File not found:', infile); + process.exit(2); +} +const raw = fs.readFileSync(infile, 'utf8'); +let obj; +try { + obj = JSON.parse(raw); +} catch (e) { + console.error('JSON parse error:', e.message); + process.exit(2); +} + +const pretty = JSON.stringify(obj, null, 2); +const outPretty = infile.replace(/\.json$/i, '_pretty.json'); +fs.writeFileSync(outPretty, pretty, 'utf8'); + +// Analysis +const stats = fs.statSync(infile); +const analysis = []; +analysis.push('File: ' + infile); +analysis.push('Size: ' + stats.size + ' bytes'); +analysis.push('Pretty JSON: ' + outPretty); + +const topKeys = Object.keys(obj); +analysis.push('Top-level keys: ' + topKeys.join(', ')); +analysis.push('Top-level key count: ' + topKeys.length); +analysis.push('Max object depth: ' + maxDepth(obj)); + +// Try to detect viewTree nodes +let totalViewNodes = 0; +const fromViewRoot = safeGet(obj, ['from','viewTree','root']); +const toViewRoot = safeGet(obj, ['to','viewTree','root']); +if (fromViewRoot) totalViewNodes += countViewNodes(fromViewRoot); +if (toViewRoot) totalViewNodes += countViewNodes(toViewRoot); +if (totalViewNodes > 0) analysis.push('Estimated total view nodes (from+to): ' + totalViewNodes); + +const eventType = safeGet(obj, ['event','type']) || safeGet(obj, ['type']); +if (eventType) analysis.push('Event type: ' + eventType); + +const fromAbility = safeGet(obj, ['from','abilityName']) || safeGet(obj, ['from','ability']); +const toAbility = safeGet(obj, ['to','abilityName']) || safeGet(obj, ['to','ability']); +if (fromAbility) analysis.push('From ability: ' + fromAbility); +if (toAbility) analysis.push('To ability: ' + toAbility); + +const fromPage = safeGet(obj, ['from','pagePath']); +const toPage = safeGet(obj, ['to','pagePath']); +if (fromPage) analysis.push('From pagePath: ' + fromPage); +if (toPage) analysis.push('To pagePath: ' + toPage); + +const fromCap = safeGet(obj, ['from','snapshot','screenCapPath']); +const toCap = safeGet(obj, ['to','snapshot','screenCapPath']); +if (fromCap) analysis.push('From screenCap: ' + fromCap); +if (toCap) analysis.push('To screenCap: ' + toCap); + +// Summarize first-level differences between from and to (common keys) +const diffs = []; +for (const k of topKeys) { + if (k === 'from' || k === 'to') continue; +} + +// Provide a short sample of 'to' top-level structure (first 200 chars) +try { + const sample = JSON.stringify(obj.to || obj, null, 2).slice(0, 2000); + analysis.push('Sample of `to` (truncated):'); + analysis.push(sample); +} catch (e) {} + +const outAnalysis = infile.replace(/\.json$/i, '.analysis.txt'); +fs.writeFileSync(outAnalysis, analysis.join('\n'), 'utf8'); + +console.log('Wrote pretty JSON to:', outPretty); +console.log('Wrote analysis to:', outAnalysis); +console.log('Summary:'); +console.log(analysis.join('\n')); diff --git a/scripts/run_haptests.sh b/scripts/run_haptests.sh new file mode 100755 index 0000000..445c1a8 --- /dev/null +++ b/scripts/run_haptests.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +set -uo pipefail + +# 超时时间(秒),可通过环境变量覆盖,例如: +# TIMEOUT_SECS=300 ./run_haptests.sh +TIMEOUT_SECS="${TIMEOUT_SECS:-600}" + +# 在下面的数组中按顺序添加要执行的命令(每条一行)。 +# 保持格式为: node bin/haptest -i com.huawei.hmos.* -o out/* + +COMMANDS=( + #"node bin/haptest -i cn.wps.office.hap -o out/2in1/cn_wps_office_hap" + "node bin/haptest -i cn.damai.hongmeng -o out/2in1/cn_damai_hongmeng" + "node bin/haptest -i cn.gov.tax.its.hm -o out/2in1/cn_gov_tax_its_hm" + # "node bin/haptest -i cn.icheny.wechat -o out/2in1/cn_icheny_wechat" + "node bin/haptest -i cn.wps.2in1office.hap -o out/2in1/cn_wps_2in1office_hap" + # "node bin/haptest -i com.100mi.ddmc -o out/2in1/com_100mi_ddmc" + # "node bin/haptest -i com.airchina.harmonynext -o out/2in1/com_airchina_harmonynext" + "node bin/haptest -i com.alibaba.wireless_hmos -o out/2in1/com_alibaba_wireless_hmos" + # "node bin/haptest -i com.alipay.2in1.client -o out/2in1/com_alipay_2in1_client" + # "node bin/haptest -i com.amap.hmapp -o out/2in1/com_amap_hmapp" + "node bin/haptest -i com.anjuke.home -o out/2in1/com_anjuke_home" + # "node bin/haptest -i com.app.xt.retouch -o out/2in1/com_app_xt_retouch" + # "node bin/haptest -i com.autohome.main -o out/2in1/com_autohome_main" + "node bin/haptest -i com.baicizhan.bcz.hm -o out/2in1/com_baicizhan_bcz_hm" + "node bin/haptest -i com.baidu.baiduapp -o out/2in1/com_baidu_baiduapp" + # "node bin/haptest -i com.baidu.hmmap -o out/2in1/com_baidu_hmmap" + # "node bin/haptest -i com.baidu.netdisk.hmos -o out/2in1/com_baidu_netdisk_hmos" + # "node bin/haptest -i com.bankabc.openharmonyapp.release -o out/2in1/com_bankabc_openharmonyapp_release" + # "node bin/haptest -i com.beike.hongmeng -o out/2in1/com_beike_hongmeng" + # "node bin/haptest -i com.cainiao.cainiao4hmos -o out/2in1/com_cainiao_cainiao4hmos" + # "node bin/haptest -i com.ccb.2in1bank.hm -o out/2in1/com_ccb_2in1bank_hm" + # "node bin/haptest -i com.cctv.yangshipin.app.harmonyp -o out/2in1/com_cctv_yangshipin_app_harmonyp" + # "node bin/haptest -i com.china2in1.cmcc -o out/2in1/com_china2in1_cmcc" + # "node bin/haptest -i com.chinarailway.ticketingHM -o out/2in1/com_chinarailway_ticketingHM" + # "node bin/haptest -i com.cmbchina.harmony -o out/2in1/com_cmbchina_harmony" + # "node bin/haptest -i com.cmcc.DigitalHome -o out/2in1/com_cmcc_DigitalHome" + # "node bin/haptest -i com.cmcc.cmvideohm -o out/2in1/com_cmcc_cmvideohm" + # "node bin/haptest -i com.ctrip.harmonynext -o out/2in1/com_ctrip_harmonynext" + # "node bin/haptest -i com.dewu.hos -o out/2in1/com_dewu_hos" + # "node bin/haptest -i com.dingtalk.hmos -o out/2in1/com_dingtalk_hmos" + # "node bin/haptest -i com.douban.frodo.hap -o out/2in1/com_douban_frodo_hap" + # "node bin/haptest -i com.dragon.read.next -o out/2in1/com_dragon_read_next" + # "node bin/haptest -i com.droi.tong -o out/2in1/com_droi_tong" + # "node bin/haptest -i com.eastmoney.hmn.berlin -o out/2in1/com_eastmoney_hmn_berlin" + # "node bin/haptest -i com.easy.hmos.abroad -o out/2in1/com_easy_hmos_abroad" + # "node bin/haptest -i com.eternaljust.msea.huawei -o out/2in1/com_eternaljust_msea_huawei" + # "node bin/haptest -i com.example.cameran2 -o out/2in1/com_example_cameran2" + # "node bin/haptest -i com.example.deephierarchy -o out/2in1/com_example_deephierarchy" + # "node bin/haptest -i com.fliggy.hmos -o out/2in1/com_fliggy_hmos" + # "node bin/haptest -i com.hexin.hmn.sjcg -o out/2in1/com_hexin_hmn_sjcg" + # "node bin/haptest -i com.hm.cat.readall -o out/2in1/com_hm_cat_readall" + # "node bin/haptest -i com.hm.youdao -o out/2in1/com_hm_youdao" + "node bin/haptest -i com.hos.moonshot.kimichat -o out/2in1/com_hos_moonshot_kimichat" + # "node bin/haptest -i com.htinns.application -o out/2in1/com_htinns_application" + # "node bin/haptest -i com.hupu.heroes -o out/2in1/com_hupu_heroes" + # "node bin/haptest -i com.icbc.harmonyclient -o out/2in1/com_icbc_harmonyclient" + # "node bin/haptest -i com.jd.hm.mall -o out/2in1/com_jd_hm_mall" + # "node bin/haptest -i com.jiaxiao.driveharmony -o out/2in1/com_jiaxiao_driveharmony" + # "node bin/haptest -i com.jinrishuiyinxiangji.camera -o out/2in1/com_jinrishuiyinxiangji_camera" + # "node bin/haptest -i com.kanyun.hos.leo -o out/2in1/com_kanyun_hos_leo" + # "node bin/haptest -i com.kanyun.hos.solar -o out/2in1/com_kanyun_hos_solar" + # "node bin/haptest -i com.kuaishou.hmapp -o out/2in1/com_kuaishou_hmapp" + # "node bin/haptest -i com.kugou.hmmusic -o out/2in1/com_kugou_hmmusic" + # "node bin/haptest -i com.legado.app -o out/2in1/com_legado_app" + # "node bin/haptest -i com.lfr.accessibility -o out/2in1/com_lfr_accessibility" + # "node bin/haptest -i com.lfr.uitest -o out/2in1/com_lfr_uitest" + # "node bin/haptest -i com.lianjia.hongmeng -o out/2in1/com_lianjia_hongmeng" + # "node bin/haptest -i com.liuzh.deviceinfo.hmos -o out/2in1/com_liuzh_deviceinfo_hmos" + # "node bin/haptest -i com.lucky.luckincoffee -o out/2in1/com_lucky_luckincoffee" + # "node bin/haptest -i com.luna.hm.music -o out/2in1/com_luna_hm_music" + # "node bin/haptest -i com.meitu.beautycam -o out/2in1/com_meitu_beautycam" + "node bin/haptest -i com.meitu.meitupic -o out/2in1/com_meitu_meitupic" + # "node bin/haptest -i com.meituan.takeaway -o out/2in1/com_meituan_takeaway" + # "node bin/haptest -i com.mgtv.phone -o out/2in1/com_mgtv_phone" + # "node bin/haptest -i com.ohos.FusionSearch -o out/2in1/com_ohos_FusionSearch" + # "node bin/haptest -i com.ohos.UserFile.ExternalFileManager -o out/2in1/com_ohos_UserFile_ExternalFileManager" + # "node bin/haptest -i com.ohos.amsdialog -o out/2in1/com_ohos_amsdialog" + # "node bin/haptest -i com.ohos.backgroundtaskmgr.resources -o out/2in1/com_ohos_backgroundtaskmgr_resources" + # "node bin/haptest -i com.ohos.callui -o out/2in1/com_ohos_callui" + # "node bin/haptest -i com.ohos.certmanager -o out/2in1/com_ohos_certmanager" + # "node bin/haptest -i com.ohos.commondialog -o out/2in1/com_ohos_commondialog" + # "node bin/haptest -i com.ohos.contacts -o out/2in1/com_ohos_contacts" + # "node bin/haptest -i com.ohos.contactsdataability -o out/2in1/com_ohos_contactsdataability" + # "node bin/haptest -i com.ohos.devicemanagerui -o out/2in1/com_ohos_devicemanagerui" + # "node bin/haptest -i com.ohos.dhardwareui -o out/2in1/com_ohos_dhardwareui" + # "node bin/haptest -i com.ohos.dlpmanager -o out/2in1/com_ohos_dlpmanager" + # "node bin/haptest -i com.ohos.formrenderservice -o out/2in1/com_ohos_formrenderservice" + # "node bin/haptest -i com.ohos.inputmethodchoosedialog -o out/2in1/com_ohos_inputmethodchoosedialog" + # "node bin/haptest -i com.ohos.locationdialog -o out/2in1/com_ohos_locationdialog" + # "node bin/haptest -i com.ohos.medialibrary.medialibrarydata -o out/2in1/com_ohos_medialibrary_medialibrarydata" + # "node bin/haptest -i com.ohos.mms -o out/2in1/com_ohos_mms" + # "node bin/haptest -i com.ohos.notificationdialog -o out/2in1/com_ohos_notificationdialog" + # "node bin/haptest -i com.ohos.pasteboarddialog -o out/2in1/com_ohos_pasteboarddialog" + # "node bin/haptest -i com.ohos.permissionmanager -o out/2in1/com_ohos_permissionmanager" + # "node bin/haptest -i com.ohos.powerdialog -o out/2in1/com_ohos_powerdialog" + # "node bin/haptest -i com.ohos.ringtonelibrary.ringtonelibrarydata -o out/2in1/com_ohos_ringtonelibrary_ringtonelibrarydata" + # "node bin/haptest -i com.ohos.sceneboard -o out/2in1/com_ohos_sceneboard" + # "node bin/haptest -i com.ohos.settingsdata -o out/2in1/com_ohos_settingsdata" + # "node bin/haptest -i com.ohos.telephonydataability -o out/2in1/com_ohos_telephonydataability" + # "node bin/haptest -i com.ohos.useriam.authwidget -o out/2in1/com_ohos_useriam_authwidget" + # "node bin/haptest -i com.psbc.mbank.hm -o out/2in1/com_psbc_mbank_hm" + # "node bin/haptest -i com.qihoo.hms.browser -o out/2in1/com_qihoo_hms_browser" + # "node bin/haptest -i com.qimao.novel -o out/2in1/com_qimao_novel" + # "node bin/haptest -i com.qiyi.video.hmy -o out/2in1/com_qiyi_video_hmy" + # "node bin/haptest -i com.quark.ohosbrowser -o out/2in1/com_quark_ohosbrowser" + # "node bin/haptest -i com.qunar.hos -o out/2in1/com_qunar_hos" + # "node bin/haptest -i com.sankuai.dianping -o out/2in1/com_sankuai_dianping" + "node bin/haptest -i com.sankuai.hmeituan -o out/2in1/com_sankuai_hmeituan" + # "node bin/haptest -i com.sdu.didi.hmos.psnger -o out/2in1/com_sdu_didi_hmos_psnger" + # "node bin/haptest -i com.sina.news.hm.next -o out/2in1/com_sina_news_hm_next" + "node bin/haptest -i com.sina.weibo.stage -o out/2in1/com_sina_weibo_stage" + # "node bin/haptest -i com.sinovatech.unicom.ha -o out/2in1/com_sinovatech_unicom_ha" + # "node bin/haptest -i com.sogou.input -o out/2in1/com_sogou_input" + # "node bin/haptest -i com.ss.dcar.auto -o out/2in1/com_ss_dcar_auto" + # "node bin/haptest -i com.ss.feishu -o out/2in1/com_ss_feishu" + # "node bin/haptest -i com.ss.hm.article.news -o out/2in1/com_ss_hm_article_news" + # "node bin/haptest -i com.ss.hm.article.video -o out/2in1/com_ss_hm_article_video" + "node bin/haptest -i com.ss.hm.ugc.aweme -o out/2in1/com_ss_hm_ugc_aweme" + "node bin/haptest -i com.taobao.idlefish4ohos -o out/2in1/com_taobao_idlefish4ohos" + # "node bin/haptest -i com.taobao.movie.hongmeng -o out/2in1/com_taobao_movie_hongmeng" + # "node bin/haptest -i com.taobao.taobao4hmos -o out/2in1/com_taobao_taobao4hmos" + # "node bin/haptest -i com.taobao.taobaolive4hmos -o out/2in1/com_taobao_taobaolive4hmos" + # "node bin/haptest -i com.tencent.docsohos -o out/2in1/com_tencent_docsohos" + # "node bin/haptest -i com.tencent.hm.news -o out/2in1/com_tencent_hm_news" + # "node bin/haptest -i com.tencent.hm.qqmusic -o out/2in1/com_tencent_hm_qqmusic" + # "node bin/haptest -i com.tencent.meeting.app -o out/2in1/com_tencent_meeting_app" + # "node bin/haptest -i com.tencent.mqq -o out/2in1/com_tencent_mqq" + # "node bin/haptest -i com.tencent.mtthm -o out/2in1/com_tencent_mtthm" + # "node bin/haptest -i com.tencent.videohm -o out/2in1/com_tencent_videohm" + "node bin/haptest -i com.tencent.wechat -o out/2in1/com_tencent_wechat" + # "node bin/haptest -i com.tencent.wework.hmos -o out/2in1/com_tencent_wework_hmos" + # "node bin/haptest -i com.tianyancha.skyeye.hm -o out/2in1/com_tianyancha_skyeye_hm" + # "node bin/haptest -i com.tmall.tmall4hmos -o out/2in1/com_tmall_tmall4hmos" + # "node bin/haptest -i com.tmri.app.harmony12123 -o out/2in1/com_tmri_app_harmony12123" + # "node bin/haptest -i com.tongcheng.hmos -o out/2in1/com_tongcheng_hmos" + # "node bin/haptest -i com.uc.2in1 -o out/2in1/com_uc_2in1" + # "node bin/haptest -i com.umetrip.hm.app -o out/2in1/com_umetrip_hm_app" + # "node bin/haptest -i com.unionpay.hmos.wallet -o out/2in1/com_unionpay_hmos_wallet" + # "node bin/haptest -i com.usb.right -o out/2in1/com_usb_right" + # "node bin/haptest -i com.vip.hosapp -o out/2in1/com_vip_hosapp" + # "node bin/haptest -i com.wifi.hm -o out/2in1/com_wifi_hm" + # "node bin/haptest -i com.wifiservice.portallogin -o out/2in1/com_wifiservice_portallogin" + # "node bin/haptest -i com.wuba.life -o out/2in1/com_wuba_life" + # "node bin/haptest -i com.wudaokou.hippo_hmos -o out/2in1/com_wudaokou_hippo_hmos" + # "node bin/haptest -i com.ximalaya.ting.xmharmony -o out/2in1/com_ximalaya_ting_xmharmony" + # "node bin/haptest -i com.xingin.xhs_hos -o out/2in1/com_xingin_xhs_hos" + # "node bin/haptest -i com.xs.fm.next -o out/2in1/com_xs_fm_next" + # "node bin/haptest -i com.xunlei.thunder -o out/2in1/com_xunlei_thunder" + # "node bin/haptest -i com.xunmeng.pinduoduo.hos -o out/2in1/com_xunmeng_pinduoduo_hos" + # "node bin/haptest -i com.yiche.autoeasyh -o out/2in1/com_yiche_autoeasyh" + "node bin/haptest -i com.youku.next -o out/2in1/com_youku_next" + # "node bin/haptest -i com.yumc.kfc.superapp -o out/2in1/com_yumc_kfc_superapp" + # "node bin/haptest -i com.zhibo8.hmclient -o out/2in1/com_zhibo8_hmclient" + # "node bin/haptest -i com.zhihu.hmos -o out/2in1/com_zhihu_hmos" + "node bin/haptest -i com.zhuanzhuan.hmoszz -o out/2in1/com_zhuanzhuan_hmoszz" + "node bin/haptest -i com.zuoyebang.homework -o out/2in1/com_zuoyebang_homework" + # "node bin/haptest -i me.ele.eleme -o out/2in1/me_ele_eleme" + # "node bin/haptest -i ohos.global.systemres -o out/2in1/ohos_global_systemres" + # "node bin/haptest -i yylx.bilibili.comic -o out/2in1/yylx_bilibili_comic" + "node bin/haptest -i yylx.danmaku.bili -o out/2in1/yylx_danmaku_bili" +) + + +run_with_timeout() { + local cmd="$1" + + # Prefer coreutils `timeout` if available + if command -v timeout >/dev/null 2>&1; then + timeout "${TIMEOUT_SECS}" bash -c "$cmd" + return $? + fi + + # Fallback implementation using background process and manual kill + bash -c "$cmd" & + local pid=$! + local start_ts + start_ts=$(date +%s) + + while kill -0 "$pid" >/dev/null 2>&1; do + sleep 1 + local now + now=$(date +%s) + local elapsed=$((now - start_ts)) + if [ "$elapsed" -ge "$TIMEOUT_SECS" ]; then + echo "Timeout (${TIMEOUT_SECS}s) reached for PID $pid, terminating..." + kill -TERM "$pid" >/dev/null 2>&1 || true + sleep 2 + kill -KILL "$pid" >/dev/null 2>&1 || true + wait "$pid" 2>/dev/null || true + return 124 + fi + done + + wait "$pid" + return $? +} + +for cmd in "${COMMANDS[@]}"; do + if [[ -z "$cmd" ]]; then + continue + fi + + echo "Running: $cmd (timeout ${TIMEOUT_SECS}s)" + run_with_timeout "$cmd" + rc=$? + + if [ $rc -eq 0 ]; then + echo "Command finished successfully." + elif [ $rc -eq 124 ]; then + echo "Command timed out after ${TIMEOUT_SECS}s — retrying once..." + # retry once + run_with_timeout "$cmd" + rc2=$? + if [ $rc2 -eq 0 ]; then + echo "Retry succeeded." + elif [ $rc2 -eq 124 ]; then + echo "Retry also timed out after ${TIMEOUT_SECS}s — skipping to next." + else + echo "Retry exited with status $rc2 — continuing to next." + fi + else + echo "Command exited with status $rc — continuing to next." + fi +done + +# 处理 JSON 文件并生成结构化输出 +for json_file in events/*.json; do + if [[ -f "$json_file" ]]; then + node scripts/format_and_analyze.js "$json_file" + fi +done + +echo "All commands finished." diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 44e61da..c57f112 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -22,6 +22,8 @@ import { FuzzOptions } from '../runner/fuzz_options'; import { EnvChecker } from './env_checker'; import { HapTestLogger, LOG_LEVEL } from '../utils/logger'; import { startUIViewerServer } from '../ui/ui_viewer_server'; +import { CompareDetector } from '../compare/types'; +import { compareDynamicLogs } from '../utils/dynamic_compare'; const logger = getLogger(); @@ -37,6 +39,37 @@ function resolveLogLevel(opts: BaseOptions): LOG_LEVEL { return opts.debug ? LOG_LEVEL.DEBUG : LOG_LEVEL.INFO; } +function formatTimestamp(date: Date): string { + const yyyy = date.getFullYear().toString(); + const mm = (date.getMonth() + 1).toString().padStart(2, '0'); + const dd = date.getDate().toString().padStart(2, '0'); + const hh = date.getHours().toString().padStart(2, '0'); + const min = date.getMinutes().toString().padStart(2, '0'); + const ss = date.getSeconds().toString().padStart(2, '0'); + return `${yyyy}${mm}${dd}${hh}${min}${ss}`; +} + +function resolveCompareReportPath( + outputOption: string | undefined, + dataRoot: string, + appFolder: string, + detector: CompareDetector +): string { + const defaultName = `${detector}_${formatTimestamp(new Date())}`; + const defaultDir = path.join(dataRoot, 'compare', appFolder); + if (!outputOption) { + return path.join(defaultDir, defaultName); + } + + const resolvedOutput = path.resolve(outputOption); + const looksLikeDirectory = + outputOption.endsWith('/') || + outputOption.endsWith('\\') || + (path.extname(outputOption) === '' && !outputOption.endsWith('.json')); + + return looksLikeDirectory ? path.join(resolvedOutput, defaultName) : resolvedOutput; +} + async function runFuzzCommand(options: any): Promise { const outputDir = path.resolve(options.output ?? 'out'); const logLevel = resolveLogLevel(options); @@ -97,6 +130,38 @@ async function runUIViewerCommand(options: any, version: string): Promise }); } +async function runCompareCommand(options: any): Promise { + const outputOption = options.output; + const dataRoot = path.resolve(options.dataRoot ?? 'out'); + const detector = (options.detector ?? 'all') as CompareDetector; + const reportPath = resolveCompareReportPath(outputOption, dataRoot, options.app, detector); + const reportDir = path.dirname(reportPath); + const logLevel = resolveLogLevel(options); + fs.mkdirSync(reportDir, { recursive: true }); + HapTestLogger.configure(path.join(reportDir, 'haptest.log'), logLevel); + + if (!['all', 'full-width', 'ratio', 'scene', 'diff'].includes(detector)) { + throw new Error(`Invalid detector: ${options.detector}. Use one of: all, full-width, ratio, scene, diff.`); + } + + await compareDynamicLogs({ + outputRoot: dataRoot, + appFolder: options.app, + mobileDir: options.mobile, + twoInOneDir: options.twoInOne, + reportPath, + fullWidthTolerance: Number(options.tolerance), + aspectRatioTolerance: Number(options.ratioTolerance), + sceneSimilarityThreshold: Number(options.sceneSimilarityThreshold), + detector, + aiComponentMatch: options.aiComponentMatch, + aiComponentModel: options.aiComponentModel, + aiComponentThreshold: Number(options.aiComponentThreshold), + aiComponentMaxCalls: Number(options.aiComponentMaxCalls), + aiComponentConfigPath: options.aiComponentConfig, + }); +} + (async function (): Promise { const packageCfg = parsePackageConfig(); @@ -118,6 +183,33 @@ async function runUIViewerCommand(options: any, version: string): Promise } }); + program + .command('compare') + .description('Compare mobile and 2in1 dynamic logs for the same app') + .requiredOption('-a, --app ', 'app log folder name under each device directory') + .option('-o, --output ', 'report output file or directory path') + .option('--dataRoot ', 'dynamic logs root directory containing mobile/2in1', 'out') + .option('--mobile ', 'mobile device folder name', 'mobile') + .option('--twoInOne ', '2in1 device folder name', '2in1') + .option('--tolerance ', 'full width tolerance in px', '1') + .option('--ratioTolerance ', 'aspect ratio tolerance', '0.01') + .option('--sceneSimilarityThreshold ', 'business scene similarity threshold', '0.35') + .option('--detector ', 'detector to run: all | full-width | ratio | scene | diff', 'all') + .option('--aiComponentMatch', 'enable AI fallback to judge whether two components are the same', false) + .option('--aiComponentModel ', 'AI model used for component matching', 'openrouter/free') + .option('--aiComponentThreshold ', 'minimum AI confidence to accept a matched component pair', '0.6') + .option('--aiComponentMaxCalls ', 'maximum number of AI matching calls in one compare task', '200') + .option('--aiComponentConfig ', 'config file path containing GPT_CONFIG', 'config.json') + .option('--debug', 'debug log level', false) + .action(async (cmdOptions) => { + try { + await runCompareCommand(cmdOptions); + } catch (err) { + logger.error('haptest compare command failed.', err); + process.exit(1); + } + }); + program .description('HapTest fuzz runner') .option('-i, --hap ', 'HAP bundle name or HAP file path or HAP project source root (can specify multiple)') diff --git a/src/compare/README.md b/src/compare/README.md new file mode 100644 index 0000000..e22140e --- /dev/null +++ b/src/compare/README.md @@ -0,0 +1,283 @@ +# Dynamic Compare 使用说明 + +本目录提供 `haptest compare` 动态日志对比能力,主要用于对比 `mobile` 与 `2in1` 两端执行同一应用后的 UI 一致性问题。 + +当前可检测 4 类问题: + +- full-width:检测同一组件是否都为横向铺满 +- ratio:检测同一组件宽高比是否明显变化 +- scene:检测同一事件前后业务场景是否发生偏移 +- diff:对已匹配到的同一组件逐字段比对,任何字段差异都会上报 + +并支持可选 AI 组件匹配:开启后组件匹配阶段仅使用 AI 判定,不再执行 `type + key/id` 精确匹配。 + +--- + +## 1. 快速开始 + +```bash +haptest compare -a +``` + +最小示例: + +```bash +haptest compare -a com.demo.app +``` + +指定数据根目录与输出文件: + +```bash +haptest compare -a com.demo.app \ + --dataRoot out \ + --output out/reports/compare_all_com.demo.app.json +``` + +只跑某一个检测器: + +```bash +haptest compare -a com.demo.app --detector ratio +``` + +启用 AI 组件匹配: + +```bash +haptest compare -a com.demo.app \ + --aiComponentMatch \ + --aiComponentModel openrouter/free \ + --aiComponentThreshold 0.6 \ + --aiComponentMaxCalls 200 \ + --aiComponentConfig config.json +``` + +--- + +## 2. 输入目录约定 + +默认从以下结构读取数据(可通过参数覆盖目录名): + +```text +/ + mobile/ + / + / + events/ + temp/ + 2in1/ + / + / + events/ + temp/ +``` + +- `events/`:事件与页面快照 JSON +- `temp/`:截图文件(png/jpg/jpeg) + +如果 `` 下有多个 `runDir`,会按目录名排序后选择最新一个。 + +--- + +## 3. 按可检测功能分章 + +### 3.1 all(一次跑完全部检测) + +用途:一次执行 `full-width + ratio + scene + diff` 四类检测。 + +运行方式: + +```bash +haptest compare -a com.demo.app --detector all +``` + +说明: + +- `--detector ` 默认值为 `all`。 +- 建议先跑 `all` 获取全量问题,再按单个 detector 精确定位。 + +--- + +### 3.2 diff(组件字段差异全量检测) + +用途:针对已匹配的同一组件,按字段逐项比较并上报所有差异。 + +运行方式: + +```bash +haptest compare -a com.demo.app --detector diff +``` + +说明: + +- 该检测器不设置容差,属于“有差异即报”。 +- 报告会给出每个字段在 `mobile` 与 `2in1` 两端的值。 +- 报告中的 `diffs` 字段会直接按三组输出:结构差异(`structuralDiffs`)、状态差异(`statusDiffs`)、文本差异(`textDiffs`)。 +- 当前会比较:`type/id/key/name/text/hint`、交互状态字段(如 `clickable`、`enabled`、`visible` 等)、`bounds` 与 `origBounds`。 + +--- + +### 3.3 full-width(横向铺满一致性) + +用途:检测同一组件在两端是否都表现为“横向铺满容器”。 + +运行方式: + +```bash +haptest compare -a com.demo.app --detector full-width +``` + +相关数值参数: + +- `--tolerance `(默认 `1`) + - 含义:像素容差,允许两端在“是否铺满”判定时有小范围误差。 + - 可以理解为:当组件宽度与容器宽度的差值在该阈值内时,仍可视为铺满。 + - 调参建议: + - 值更小(如 `0`):更严格,容易报出更多问题。 + - 值更大(如 `2`、`3`):更宽松,可减少因取整/缩放导致的误报。 + +--- + +### 3.4 ratio(宽高比一致性) + +用途:检测同一组件在两端是否出现明显形变(宽高比变化)。 + +运行方式: + +```bash +haptest compare -a com.demo.app --detector ratio +``` + +相关数值参数: + +- `--ratioTolerance `(默认 `0.01`) + - 含义:宽高比差异容差。 + - 判定思路可理解为:比较两端组件的宽高比差值,若超过该阈值则记为问题。 + - 例如:`0.01` 约等于允许 $1\%$ 的比例偏差量级(用于过滤轻微浮动)。 + - 调参建议: + - 值更小(如 `0.005`):更敏感,更容易发现细微变形。 + - 值更大(如 `0.02`):更宽松,减少轻微差异告警。 + +--- + +### 3.5 scene(场景偏移检测) + +用途:检测同一事件前后,两端是否进入了不同业务场景。 + +运行方式: + +```bash +haptest compare -a com.demo.app --detector scene +``` + +相关数值参数: + +- `--sceneSimilarityThreshold `(默认 `0.35`) + - 含义:场景相似度阈值。 + - 相似度通常在 `[0, 1]` 区间,值越大表示越相似。 + - 判定逻辑可理解为:相似度低于该阈值时,认为发生场景偏移。 + - 调参建议: + - 值更高(如 `0.5`):更严格,更容易判定为场景偏移。 + - 值更低(如 `0.25`):更宽松,只关注明显偏移。 + +--- + +## 4. 通用参数与数值参数说明 + +| 参数 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| `-a, --app ` | string | 无(必填) | 设备目录下的应用日志目录名 | +| `-o, --output ` | string | 自动生成 | 报告输出文件或目录 | +| `--dataRoot ` | string | `out` | 动态日志根目录 | +| `--mobile ` | string | `mobile` | 手机侧目录名 | +| `--twoInOne ` | string | `2in1` | 2in1 侧目录名 | +| `--tolerance ` | number | `1` | full-width 检测像素容差 | +| `--ratioTolerance ` | number | `0.01` | ratio 检测宽高比容差 | +| `--sceneSimilarityThreshold ` | number | `0.35` | scene 检测场景相似度阈值 | +| `--detector ` | enum | `all` | 运行检测器:`all \| full-width \| ratio \| scene \| diff` | +| `--aiComponentMatch` | boolean | `false` | 开启 AI 组件匹配(不再做精确匹配) | +| `--aiComponentModel ` | string | `openrouter/free` | AI 匹配模型名 | +| `--aiComponentThreshold ` | number | `0.6` | AI 判定为同一组件的最小置信度 | +| `--aiComponentMaxCalls ` | number | `200` | 单次 compare 最大 AI 调用次数 | +| `--aiComponentConfig ` | string | `config.json` | 包含 `GPT_CONFIG` 的配置文件路径 | +| `--debug` | boolean | `false` | 使用 debug 日志级别 | + +### 4.1 需要填写数值的参数(速查) + +- `--tolerance` + - 单位:像素。 + - 含义:full-width 判定时允许的宽度误差。 +- `--ratioTolerance` + - 单位:比例差(无单位)。 + - 含义:ratio 判定时允许的宽高比差值。 +- `--sceneSimilarityThreshold` + - 单位:相似度分数(通常 `0~1`)。 + - 含义:scene 判定为“同场景”的最低相似度门槛。 +- `--aiComponentThreshold` + - 单位:置信度分数(通常 `0~1`)。 + - 含义:AI 认为两组件可匹配的最小置信度。 + - 取值越高,匹配越保守;取值越低,匹配越激进。 +- `--aiComponentMaxCalls` + - 单位:次数。 + - 含义:一次 compare 过程中允许的最大 AI 调用次数上限。 + - 值越大,覆盖更多候选匹配,但耗时/成本也可能增加。 + +### output 参数行为 + +- 未指定 `--output`:自动写入 `dataRoot/compare___mobile_2in1.json` +- 指定为目录:自动在目录下生成上述文件名 +- 指定为 `.json` 文件:直接写入该文件 + +--- + +## 5. AI 匹配配置 + +当启用 `--aiComponentMatch` 时,会读取配置文件中的 `GPT_CONFIG`: + +```json +{ + "GPT_CONFIG": { + "baseURL": "https://openrouter.ai/api/v1", + "apiKey": "", + "siteURL": "https://github.com/SMAT-Lab/HapTest", + "appName": "HapTest" + } +} +``` + +注意: + +- `apiKey` 为空时会跳过 AI 调用;由于已开启仅 AI 匹配,组件匹配结果会显著减少 +- 开启 `--aiComponentMatch` 后,组件匹配阶段仅由 AI 决定 +- OpenRouter 下会自动附带 `HTTP-Referer` 与 `X-Title` 请求头,并在连接错误时自动重试 +- 若 `openrouter/free` 连接不稳定,会自动回退尝试其他免费路由模型 + +### 5.1 OpenRouter `Connection error` 排查 + +若日志出现 `LLM call failed: Error: Connection error`,建议确认: + +- `GPT_CONFIG.baseURL` 是否为 `https://openrouter.ai/api/v1` +- 网络是否可访问 OpenRouter(公司网络/代理可能拦截) +- `apiKey` 是否有效且账户可用 + +可先用最简命令验证: + +```bash +haptest compare -a com.demo.app --aiComponentMatch --aiComponentModel openrouter/free --aiComponentConfig config.json +``` + +--- + +## 6. 输出报告结构 + +核心字段如下: + +- `issues`:full-width 问题列表 +- `aspectRatioIssues`:ratio 问题列表 +- `sceneIssues`:scene 问题列表 +- `componentDiffIssues`:diff 问题列表(字段级差异) +- `pageCount`:页面对比数量 +- `transitionCount`:转移对比数量 +- `mobilePages` / `twoInOnePages`:两端页面总数 +- `mobileTransitions` / `twoInOneTransitions`:两端转移总数 +- `mobileScreenshots` / `twoInOneScreenshots`:两端截图数量 + +建议先使用 `--detector all` 观察全量结果,再按问题类型缩小到单个 detector 做定位。 diff --git a/src/compare/ai_component_matcher.ts b/src/compare/ai_component_matcher.ts new file mode 100644 index 0000000..32f546e --- /dev/null +++ b/src/compare/ai_component_matcher.ts @@ -0,0 +1,417 @@ +import fs from 'fs'; +import path from 'path'; +import OpenAI from 'openai'; +import { Component } from '../model/component'; +import { HapTestLogger } from '../utils/logger'; +import { AiComponentMatchContext, AiComponentMatcher } from './component_matcher'; + +interface GptConfig { + baseURL?: string; + apiKey?: string; + siteURL?: string; + appName?: string; +} + +interface AiCompareResponse { + same?: boolean; + confidence?: number; + reason?: string; +} + +export interface OpenAiComponentMatcherOptions { + configPath?: string; + model?: string; + threshold?: number; + maxCalls?: number; +} + +const DEFAULT_MODEL = 'openrouter/free'; +const DEFAULT_THRESHOLD = 0.6; +const DEFAULT_MAX_CALLS = 200; +const DEFAULT_TIMEOUT_MS = 45000; +const RETRY_ATTEMPTS = 3; +const STRUCTURE_PARENT_DEPTH = 4; +const STRUCTURE_CHILD_PREVIEW = 6; +const STRUCTURE_DESCENDANT_DEPTH = 2; +const STRUCTURE_DESCENDANT_SAMPLE_LIMIT = 24; +const STRUCTURE_SIBLING_WINDOW = 2; + +const OPENROUTER_FREE_MODEL_FALLBACKS = [ + 'openrouter/free', + 'meta-llama/llama-3.1-8b-instruct:free', + 'mistralai/mistral-7b-instruct:free', +]; + +const logger = HapTestLogger.getLogger(); + +export class OpenAiComponentMatcher implements AiComponentMatcher { + private readonly openai: OpenAI; + private readonly model: string; + private readonly baseURL?: string; + private readonly threshold: number; + private readonly maxCalls: number; + private readonly cache: Map = new Map(); + private calls = 0; + + constructor(openai: OpenAI, model: string, threshold: number, maxCalls: number, baseURL?: string) { + this.openai = openai; + this.model = model; + this.threshold = threshold; + this.maxCalls = maxCalls; + this.baseURL = baseURL; + } + + static createFromConfig(options: OpenAiComponentMatcherOptions = {}): OpenAiComponentMatcher | undefined { + const configPath = path.resolve(options.configPath ?? path.join(__dirname, '../../config.json')); + let gptConfig: GptConfig | undefined; + try { + const raw = fs.readFileSync(configPath, { encoding: 'utf-8' }); + const json = JSON.parse(raw) as { GPT_CONFIG?: GptConfig }; + gptConfig = json.GPT_CONFIG; + } catch (error) { + logger.warn(`[ai-match] Skip AI component match because config is unreadable: ${String(error)}`); + return undefined; + } + + const apiKey = gptConfig?.apiKey?.trim(); + if (!apiKey) { + logger.warn('[ai-match] Skip AI component match because GPT_CONFIG.apiKey is empty.'); + return undefined; + } + + const baseURL = gptConfig?.baseURL?.trim() || undefined; + const openRouterHeaders = buildOpenRouterHeaders(baseURL, gptConfig); + + const openai = new OpenAI({ + apiKey, + baseURL, + timeout: DEFAULT_TIMEOUT_MS, + maxRetries: 0, + defaultHeaders: openRouterHeaders, + }); + + const model = options.model?.trim() || DEFAULT_MODEL; + const threshold = Number.isFinite(options.threshold) ? options.threshold! : DEFAULT_THRESHOLD; + const maxCalls = Number.isFinite(options.maxCalls) ? options.maxCalls! : DEFAULT_MAX_CALLS; + logger.info(`[ai-match] Enabled AI component matcher (model=${model}, threshold=${threshold}, maxCalls=${maxCalls})`); + return new OpenAiComponentMatcher(openai, model, threshold, maxCalls, baseURL); + } + + async isSameComponent( + mobileComponent: Component, + twoInOneComponent: Component, + context: AiComponentMatchContext + ): Promise { + const cacheKey = this.buildCacheKey(mobileComponent, twoInOneComponent, context); + const cached = this.cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + + if (this.calls >= this.maxCalls) { + this.cache.set(cacheKey, false); + return false; + } + + this.calls += 1; + + const modelCandidates = this.resolveModelCandidates(); + + for (const model of modelCandidates) { + let lastError: unknown; + for (let attempt = 1; attempt <= RETRY_ATTEMPTS; attempt += 1) { + try { + const response = await this.openai.chat.completions.create({ + model, + temperature: 0, + messages: [ + { + role: 'system', + content: + 'You are a strict GUI component matcher. Decide if two components represent the same business UI element across devices. Structural context (ancestor chain, sibling neighborhood, child/descendant patterns) is high-priority evidence when identity fields are missing. Respond with compact JSON only: {"same": boolean, "confidence": number, "reason": string}.', + }, + { + role: 'user', + content: this.buildPrompt(mobileComponent, twoInOneComponent, context), + }, + ], + }); + + const content = response.choices[0]?.message?.content?.trim() ?? ''; + const parsed = this.parseResponse(content); + const sameByFlag = parsed.same === true; + const confidence = Number.isFinite(parsed.confidence) ? parsed.confidence! : 0.5; + const decision = sameByFlag && confidence >= this.threshold; + this.cache.set(cacheKey, decision); + return decision; + } catch (error) { + lastError = error; + const shouldRetry = isConnectionLikeError(error) && attempt < RETRY_ATTEMPTS; + const modelInfo = model === this.model ? model : `${this.model}->${model}`; + logger.warn( + `[ai-match] LLM call failed (model=${modelInfo}, attempt=${attempt}/${RETRY_ATTEMPTS}): ${String(error)}` + ); + if (!shouldRetry) { + break; + } + await sleep(attempt * 500); + } + } + + if (!isConnectionLikeError(lastError)) { + break; + } + } + + this.cache.set(cacheKey, false); + return false; + } + + private resolveModelCandidates(): string[] { + if (!isOpenRouterBaseURL(this.baseURL)) { + return [this.model]; + } + + if (this.model !== 'openrouter/free') { + return [this.model]; + } + + return OPENROUTER_FREE_MODEL_FALLBACKS; + } + + private buildPrompt(mobileComponent: Component, twoInOneComponent: Component, context: AiComponentMatchContext): string { + const payload = { + mode: context.mode, + mobileKey: context.mobileKey, + twoInOneKey: context.twoInOneKey, + mobile: this.summarizeComponent(mobileComponent), + twoInOne: this.summarizeComponent(twoInOneComponent), + rules: [ + 'Type must be semantically compatible.', + 'Prefer key/id/name/text/hint consistency.', + 'Treat parent-child nesting and nearby siblings as strong evidence, especially when text/id/key are empty.', + 'Use bounds only as weak evidence because resolution differs across devices.', + 'If uncertain, return same=false.', + ], + }; + return JSON.stringify(payload); + } + + private summarizeComponent(component: Component): Record { + const bounds = component.bounds ?? component.origBounds; + const left = bounds?.[0]?.x; + const top = bounds?.[0]?.y; + const right = bounds?.[1]?.x; + const bottom = bounds?.[1]?.y; + return { + type: component.type?.trim() ?? '', + id: component.id?.trim() ?? '', + key: component.key?.trim() ?? '', + name: component.name?.trim() ?? '', + text: component.text?.trim() ?? '', + hint: component.hint?.trim() ?? '', + width: Number.isFinite(left) && Number.isFinite(right) ? Math.abs((right as number) - (left as number)) : null, + height: Number.isFinite(top) && Number.isFinite(bottom) ? Math.abs((bottom as number) - (top as number)) : null, + structure: this.summarizeStructure(component), + }; + } + + private summarizeStructure(component: Component): Record { + const parentChain = this.collectParentChain(component, STRUCTURE_PARENT_DEPTH).map((item) => + this.summarizeNodeIdentity(item) + ); + const siblingContext = this.collectSiblingContext(component); + const childPreview = (component.children ?? []) + .slice(0, STRUCTURE_CHILD_PREVIEW) + .map((child) => ({ + ...this.summarizeNodeIdentity(child), + childCount: child.children?.length ?? 0, + })); + const descendantTypeHistogram = this.collectDescendantTypeHistogram( + component, + STRUCTURE_DESCENDANT_DEPTH, + STRUCTURE_DESCENDANT_SAMPLE_LIMIT + ); + + return { + parentChain, + siblingContext, + childCount: component.children?.length ?? 0, + childPreview, + descendantTypeHistogram, + }; + } + + private collectParentChain(component: Component, depth: number): Component[] { + const chain: Component[] = []; + let current = component.parent ?? null; + let remaining = depth; + while (current && remaining > 0) { + chain.push(current); + current = current.parent ?? null; + remaining -= 1; + } + return chain; + } + + private collectSiblingContext(component: Component): Record { + const siblings = component.parent?.children ?? []; + const currentIndex = siblings.indexOf(component); + if (currentIndex < 0) { + return { + index: null, + total: siblings.length, + nearby: [], + }; + } + + const start = Math.max(0, currentIndex - STRUCTURE_SIBLING_WINDOW); + const end = Math.min(siblings.length, currentIndex + STRUCTURE_SIBLING_WINDOW + 1); + const nearby = siblings.slice(start, end).map((sibling, offsetIndex) => { + const absoluteIndex = start + offsetIndex; + return { + ...this.summarizeNodeIdentity(sibling), + relativeIndex: absoluteIndex - currentIndex, + }; + }); + + return { + index: currentIndex, + total: siblings.length, + nearby, + }; + } + + private collectDescendantTypeHistogram(component: Component, maxDepth: number, sampleLimit: number): Record { + if (maxDepth <= 0 || sampleLimit <= 0) { + return {}; + } + + const histogram = new Map(); + const queue: Array<{ node: Component; depth: number }> = []; + for (const child of component.children ?? []) { + queue.push({ node: child, depth: 1 }); + } + + let sampled = 0; + while (queue.length > 0 && sampled < sampleLimit) { + const item = queue.shift(); + if (!item) { + break; + } + + const type = item.node.type?.trim() || 'UNKNOWN'; + histogram.set(type, (histogram.get(type) ?? 0) + 1); + sampled += 1; + + if (item.depth >= maxDepth) { + continue; + } + for (const child of item.node.children ?? []) { + queue.push({ node: child, depth: item.depth + 1 }); + } + } + + return Object.fromEntries( + [...histogram.entries()].sort((left, right) => { + if (right[1] !== left[1]) { + return right[1] - left[1]; + } + return left[0].localeCompare(right[0]); + }) + ); + } + + private summarizeNodeIdentity(component: Component): Record { + return { + type: component.type?.trim() ?? '', + id: component.id?.trim() ?? '', + key: component.key?.trim() ?? '', + name: component.name?.trim() ?? '', + text: component.text?.trim() ?? '', + hint: component.hint?.trim() ?? '', + }; + } + + private parseResponse(content: string): AiCompareResponse { + if (!content) { + return {}; + } + + const jsonLike = content.match(/\{[\s\S]*\}/)?.[0] ?? content; + try { + const parsed = JSON.parse(jsonLike) as AiCompareResponse; + return parsed; + } catch { + const lowered = content.toLowerCase(); + if (lowered.includes('true')) { + return { same: true, confidence: 0.5, reason: 'fallback true parse' }; + } + if (lowered.includes('false')) { + return { same: false, confidence: 0.5, reason: 'fallback false parse' }; + } + return {}; + } + } + + private buildCacheKey( + mobileComponent: Component, + twoInOneComponent: Component, + context: AiComponentMatchContext + ): string { + return [ + context.mode, + context.mobileKey, + context.twoInOneKey, + this.componentFingerprint(mobileComponent), + this.componentFingerprint(twoInOneComponent), + ].join('||'); + } + + private componentFingerprint(component: Component): string { + const structure = this.summarizeStructure(component); + return [ + component.type?.trim() ?? '', + component.key?.trim() ?? '', + component.id?.trim() ?? '', + component.name?.trim() ?? '', + component.text?.trim() ?? '', + component.hint?.trim() ?? '', + JSON.stringify(structure), + ].join('::'); + } +} + +function buildOpenRouterHeaders(baseURL?: string, config?: GptConfig): Record | undefined { + if (!isOpenRouterBaseURL(baseURL)) { + return undefined; + } + + const siteURL = config?.siteURL?.trim() || 'https://github.com/SMAT-Lab/HapTest'; + const appName = config?.appName?.trim() || 'HapTest'; + return { + 'HTTP-Referer': siteURL, + 'X-Title': appName, + }; +} + +function isOpenRouterBaseURL(baseURL?: string): boolean { + return (baseURL ?? '').toLowerCase().includes('openrouter.ai'); +} + +function isConnectionLikeError(error: unknown): boolean { + const content = String(error ?? '').toLowerCase(); + return ( + content.includes('connection error') || + content.includes('fetch failed') || + content.includes('network') || + content.includes('econnreset') || + content.includes('etimedout') || + content.includes('enotfound') || + content.includes('socket hang up') + ); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/compare/component_matcher.ts b/src/compare/component_matcher.ts new file mode 100644 index 0000000..e4f2fbd --- /dev/null +++ b/src/compare/component_matcher.ts @@ -0,0 +1,327 @@ +import { Component } from '../model/component'; +import { Page } from '../model/page'; + +export interface ComponentWithParent { + component: Component; + componentName: string; + parentName: string; +} + +export interface AiComponentMatchContext { + mode: 'type-and-identity' | 'parent-and-identity'; + mobileKey: string; + twoInOneKey: string; +} + +export interface AiComponentMatcher { + isSameComponent( + mobileComponent: Component, + twoInOneComponent: Component, + context: AiComponentMatchContext + ): Promise; +} + +export interface MatchedNameComponents { + mobileKey: string; + twoInOneKey: string; + matchedName: string; + mobileComponents: Component[]; + twoInOneComponents: Component[]; + aiMatched: boolean; +} + +export interface MatchedParentComponents { + mobileKey: string; + twoInOneKey: string; + matchedName: string; + mobileComponents: ComponentWithParent[]; + twoInOneComponents: ComponentWithParent[]; + aiMatched: boolean; +} + +export interface ComponentMatchOptions { + aiOnly?: boolean; +} + +export function buildComponentNameMap(page: Page): Map { + const map = new Map(); + for (const component of page.getComponents()) { + const matchKey = buildTypeAndIdentityMatchKey(component); + if (!matchKey) { + continue; + } + const list = map.get(matchKey); + if (list) { + list.push(component); + } else { + map.set(matchKey, [component]); + } + } + return map; +} + +export function buildComponentParentMap(page: Page): Map { + const map = new Map(); + for (const component of page.getComponents()) { + const key = buildParentIdentityMatchKey(component); + if (!key) { + continue; + } + const componentName = getIdentityLabel(component); + const parentName = component.parent ? getIdentityLabel(component.parent) : 'ROOT'; + if (!componentName || !parentName) { + continue; + } + const item: ComponentWithParent = { + component, + componentName, + parentName, + }; + const list = map.get(key); + if (list) { + list.push(item); + } else { + map.set(key, [item]); + } + } + return map; +} + +export async function matchComponentNameMaps( + mobileMap: Map, + twoInOneMap: Map, + aiMatcher?: AiComponentMatcher, + options: ComponentMatchOptions = {} +): Promise { + const aiOnly = options.aiOnly === true; + const matches: MatchedNameComponents[] = []; + const usedTwoInOneKeys = new Set(); + + if (!aiOnly) { + for (const mobileKey of mobileMap.keys()) { + if (!twoInOneMap.has(mobileKey)) { + continue; + } + usedTwoInOneKeys.add(mobileKey); + matches.push({ + mobileKey, + twoInOneKey: mobileKey, + matchedName: mobileKey, + mobileComponents: mobileMap.get(mobileKey)!, + twoInOneComponents: twoInOneMap.get(mobileKey)!, + aiMatched: false, + }); + } + } + + if (!aiMatcher) { + return matches; + } + + const unresolvedMobileKeys = aiOnly + ? [...mobileMap.keys()] + : [...mobileMap.keys()].filter((mobileKey) => !twoInOneMap.has(mobileKey)); + const unresolvedTwoInOneKeys = aiOnly + ? [...twoInOneMap.keys()] + : [...twoInOneMap.keys()].filter((twoInOneKey) => !usedTwoInOneKeys.has(twoInOneKey)); + + for (const mobileKey of unresolvedMobileKeys) { + const mobileComponents = mobileMap.get(mobileKey); + if (!mobileComponents || mobileComponents.length === 0) { + continue; + } + const representativeMobile = pickRepresentativeComponent(mobileComponents); + if (!representativeMobile) { + continue; + } + + let matchedTwoInOneKey: string | undefined; + for (const twoInOneKey of unresolvedTwoInOneKeys) { + if (usedTwoInOneKeys.has(twoInOneKey)) { + continue; + } + const twoInOneComponents = twoInOneMap.get(twoInOneKey); + if (!twoInOneComponents || twoInOneComponents.length === 0) { + continue; + } + const representativeTwoInOne = pickRepresentativeComponent(twoInOneComponents); + if (!representativeTwoInOne) { + continue; + } + if (!isSameType(representativeMobile, representativeTwoInOne)) { + continue; + } + + const isSame = await aiMatcher.isSameComponent(representativeMobile, representativeTwoInOne, { + mode: 'type-and-identity', + mobileKey, + twoInOneKey, + }); + if (!isSame) { + continue; + } + + matchedTwoInOneKey = twoInOneKey; + break; + } + + if (!matchedTwoInOneKey) { + continue; + } + + usedTwoInOneKeys.add(matchedTwoInOneKey); + matches.push({ + mobileKey, + twoInOneKey: matchedTwoInOneKey, + matchedName: `${mobileKey}~${matchedTwoInOneKey}`, + mobileComponents: mobileComponents, + twoInOneComponents: twoInOneMap.get(matchedTwoInOneKey)!, + aiMatched: true, + }); + } + + return matches; +} + +export async function matchComponentParentMaps( + mobileMap: Map, + twoInOneMap: Map, + aiMatcher?: AiComponentMatcher, + options: ComponentMatchOptions = {} +): Promise { + const aiOnly = options.aiOnly === true; + const matches: MatchedParentComponents[] = []; + const usedTwoInOneKeys = new Set(); + + if (!aiOnly) { + for (const mobileKey of mobileMap.keys()) { + if (!twoInOneMap.has(mobileKey)) { + continue; + } + usedTwoInOneKeys.add(mobileKey); + matches.push({ + mobileKey, + twoInOneKey: mobileKey, + matchedName: mobileKey, + mobileComponents: mobileMap.get(mobileKey)!, + twoInOneComponents: twoInOneMap.get(mobileKey)!, + aiMatched: false, + }); + } + } + + if (!aiMatcher) { + return matches; + } + + const unresolvedMobileKeys = aiOnly + ? [...mobileMap.keys()] + : [...mobileMap.keys()].filter((mobileKey) => !twoInOneMap.has(mobileKey)); + const unresolvedTwoInOneKeys = aiOnly + ? [...twoInOneMap.keys()] + : [...twoInOneMap.keys()].filter((twoInOneKey) => !usedTwoInOneKeys.has(twoInOneKey)); + + for (const mobileKey of unresolvedMobileKeys) { + const mobileComponents = mobileMap.get(mobileKey); + if (!mobileComponents || mobileComponents.length === 0) { + continue; + } + const representativeMobile = pickRepresentativeWithParent(mobileComponents); + if (!representativeMobile) { + continue; + } + + let matchedTwoInOneKey: string | undefined; + for (const twoInOneKey of unresolvedTwoInOneKeys) { + if (usedTwoInOneKeys.has(twoInOneKey)) { + continue; + } + const twoInOneComponents = twoInOneMap.get(twoInOneKey); + if (!twoInOneComponents || twoInOneComponents.length === 0) { + continue; + } + const representativeTwoInOne = pickRepresentativeWithParent(twoInOneComponents); + if (!representativeTwoInOne) { + continue; + } + if (!isSameType(representativeMobile.component, representativeTwoInOne.component)) { + continue; + } + + const isSame = await aiMatcher.isSameComponent( + representativeMobile.component, + representativeTwoInOne.component, + { + mode: 'parent-and-identity', + mobileKey, + twoInOneKey, + } + ); + if (!isSame) { + continue; + } + + matchedTwoInOneKey = twoInOneKey; + break; + } + + if (!matchedTwoInOneKey) { + continue; + } + + usedTwoInOneKeys.add(matchedTwoInOneKey); + matches.push({ + mobileKey, + twoInOneKey: matchedTwoInOneKey, + matchedName: `${mobileKey}~${matchedTwoInOneKey}`, + mobileComponents: mobileComponents, + twoInOneComponents: twoInOneMap.get(matchedTwoInOneKey)!, + aiMatched: true, + }); + } + + return matches; +} + +function buildTypeAndIdentityMatchKey(component: Component): string | undefined { + const type = component.type?.trim(); + if (!type) { + return undefined; + } + const keyOrId = getIdentityLabel(component); + if (!keyOrId) { + return undefined; + } + return `${type}::${keyOrId}`; +} + +function buildParentIdentityMatchKey(component: Component): string | undefined { + const componentIdentity = getIdentityLabel(component); + const parentIdentity = component.parent ? getIdentityLabel(component.parent) : 'ROOT'; + if (!componentIdentity || !parentIdentity) { + return undefined; + } + return `${parentIdentity}>>${componentIdentity}`; +} + +function getIdentityLabel(component: Component): string | undefined { + const identity = (component.key ?? component.id ?? '').trim(); + if (!identity) { + return undefined; + } + return identity; +} + +function pickRepresentativeComponent(components: Component[]): Component | undefined { + return components.find((component) => !!component.type?.trim()) ?? components[0]; +} + +function pickRepresentativeWithParent(components: ComponentWithParent[]): ComponentWithParent | undefined { + return components.find((item) => !!item.component.type?.trim()) ?? components[0]; +} + +function isSameType(left: Component, right: Component): boolean { + const leftType = left.type?.trim(); + const rightType = right.type?.trim(); + return !!leftType && leftType === rightType; +} diff --git a/src/compare/detectors/diff_detector.ts b/src/compare/detectors/diff_detector.ts new file mode 100644 index 0000000..a925bf4 --- /dev/null +++ b/src/compare/detectors/diff_detector.ts @@ -0,0 +1,225 @@ +import { Page } from '../../model/page'; +import { Point } from '../../model/point'; +import { + AiComponentMatcher, + buildComponentParentMap, + ComponentWithParent, + matchComponentParentMaps, +} from '../component_matcher'; +import { CompareComponentDiffGroups, CompareComponentDiffIssue, CompareComponentFieldDiff } from '../types'; +import { HapTestLogger } from '../../utils/logger'; + +const logger = HapTestLogger.getLogger(); + +const COMPARABLE_FIELDS: Array = [ + 'type', + 'id', + 'key', + 'name', + 'text', + 'hint', + 'checkable', + 'checked', + 'clickable', + 'enabled', + 'focused', + 'longClickable', + 'scrollable', + 'selected', + 'visible', + 'debugLine', + 'bounds', + 'origBounds', +]; + +interface ComparableComponentFields { + type?: string; + id?: string; + key?: string; + name?: string; + text?: string; + hint?: string; + checkable?: boolean; + checked?: boolean; + clickable?: boolean; + enabled?: boolean; + focused?: boolean; + longClickable?: boolean; + scrollable?: boolean; + selected?: boolean; + visible?: boolean; + debugLine?: string; + bounds?: string; + origBounds?: string; +} + +export async function detectComponentDiffIssues( + pageIndex: number, + mobilePage: Page, + twoInOnePage: Page, + mobileScreenshot: string, + twoInOneScreenshot: string, + aiMatcher?: AiComponentMatcher, + aiOnlyMatch = false +): Promise { + const mobileParentMap = buildComponentParentMap(mobilePage); + const twoInOneParentMap = buildComponentParentMap(twoInOnePage); + const matchedGroups = await matchComponentParentMaps(mobileParentMap, twoInOneParentMap, aiMatcher, { aiOnly: aiOnlyMatch }); + + let matchedComponentCount = 0; + let aiMatchedCount = 0; + for (const group of matchedGroups) { + const mobileComponents = group.mobileComponents; + const twoInOneComponents = group.twoInOneComponents; + matchedComponentCount += Math.min(mobileComponents.length, twoInOneComponents.length); + if (group.aiMatched) { + aiMatchedCount += 1; + } + } + const aiLog = aiMatcher ? ` (ai-matched: ${aiMatchedCount})` : ''; + logger.info(`[diff] Matched components: ${matchedComponentCount}${aiLog} (pageIndex=${pageIndex})`); + + const issues: CompareComponentDiffIssue[] = []; + for (const group of matchedGroups) { + const mobileComponents = group.mobileComponents; + const twoInOneComponents = group.twoInOneComponents; + const pairCount = Math.min(mobileComponents.length, twoInOneComponents.length); + for (let index = 0; index < pairCount; index += 1) { + const mobileComponent = mobileComponents[index]; + const twoInOneComponent = twoInOneComponents[index]; + const fieldDiffs = collectFieldDiffs(mobileComponent, twoInOneComponent); + if (fieldDiffs.length === 0) { + continue; + } + issues.push({ + pageIndex, + componentName: mobileComponent.componentName, + parentName: mobileComponent.parentName, + mobileScreenshot, + twoInOneScreenshot, + diffs: groupFieldDiffs(fieldDiffs), + }); + } + } + return issues; +} + +function groupFieldDiffs(fieldDiffs: CompareComponentFieldDiff[]): CompareComponentDiffGroups { + const grouped: CompareComponentDiffGroups = { + structuralDiffs: [], + statusDiffs: [], + textDiffs: [], + }; + + for (const diff of fieldDiffs) { + const group = classifyDiffField(diff.field); + if (group === 'structural') { + grouped.structuralDiffs.push(diff); + continue; + } + if (group === 'status') { + grouped.statusDiffs.push(diff); + continue; + } + grouped.textDiffs.push(diff); + } + + return grouped; +} + +function classifyDiffField(field: string): 'structural' | 'status' | 'text' { + if (STATUS_FIELDS.has(field)) { + return 'status'; + } + if (TEXT_FIELDS.has(field)) { + return 'text'; + } + return 'structural'; +} + +const STATUS_FIELDS = new Set([ + 'checkable', + 'checked', + 'clickable', + 'enabled', + 'focused', + 'longClickable', + 'scrollable', + 'selected', + 'visible', +]); + +const TEXT_FIELDS = new Set([ + 'text', + 'hint', +]); + +function collectFieldDiffs( + mobileComponent: ComponentWithParent, + twoInOneComponent: ComponentWithParent +): CompareComponentFieldDiff[] { + const mobileSnapshot = snapshotComparableFields(mobileComponent); + const twoInOneSnapshot = snapshotComparableFields(twoInOneComponent); + const diffs: CompareComponentFieldDiff[] = []; + + for (const field of COMPARABLE_FIELDS) { + const mobileValue = mobileSnapshot[field]; + const twoInOneValue = twoInOneSnapshot[field]; + if (mobileValue === twoInOneValue) { + continue; + } + diffs.push({ + field, + mobileValue: stringifyValue(mobileValue), + twoInOneValue: stringifyValue(twoInOneValue), + }); + } + + return diffs; +} + +function snapshotComparableFields(componentWithParent: ComponentWithParent): ComparableComponentFields { + const component = componentWithParent.component; + return { + type: normalizeString(component.type), + id: normalizeString(component.id), + key: normalizeString(component.key), + name: normalizeString(component.name), + text: normalizeString(component.text), + hint: normalizeString(component.hint), + checkable: component.checkable, + checked: component.checked, + clickable: component.clickable, + enabled: component.enabled, + focused: component.focused, + longClickable: component.longClickable, + scrollable: component.scrollable, + selected: component.selected, + visible: component.visible, + debugLine: normalizeString(component.debugLine), + bounds: normalizeBounds(component.bounds), + origBounds: normalizeBounds(component.origBounds), + }; +} + +function normalizeString(value?: string): string | undefined { + const trimmed = value?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : undefined; +} + +function normalizeBounds(bounds?: Point[]): string | undefined { + if (!bounds || bounds.length === 0) { + return undefined; + } + return bounds.map((point) => `${point.x},${point.y}`).join('|'); +} + +function stringifyValue(value: unknown): string { + if (value === undefined) { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + return String(value); +} diff --git a/src/compare/detectors/full_width_detector.ts b/src/compare/detectors/full_width_detector.ts new file mode 100644 index 0000000..341a089 --- /dev/null +++ b/src/compare/detectors/full_width_detector.ts @@ -0,0 +1,72 @@ +import { Page } from '../../model/page'; +import { Component } from '../../model/component'; +import { AiComponentMatcher, buildComponentNameMap, matchComponentNameMaps } from '../component_matcher'; +import { getBoundsRect, getScreenRect } from '../geometry'; +import { CompareIssue, ScreenRect } from '../types'; +import { HapTestLogger } from '../../utils/logger'; + +const logger = HapTestLogger.getLogger(); + +export async function detectFullWidthIssues( + pageIndex: number, + mobilePage: Page, + twoInOnePage: Page, + mobileScreenshot: string, + twoInOneScreenshot: string, + tolerance: number, + aiMatcher?: AiComponentMatcher, + aiOnlyMatch = false +): Promise { + const mobileRect = getScreenRect(mobilePage); + const twoInOneRect = getScreenRect(twoInOnePage); + if (!mobileRect || !twoInOneRect) { + return []; + } + + const mobileMap = buildComponentNameMap(mobilePage); + const twoInOneMap = buildComponentNameMap(twoInOnePage); + const matchedGroups = await matchComponentNameMaps(mobileMap, twoInOneMap, aiMatcher, { aiOnly: aiOnlyMatch }); + + let matchedComponentCount = 0; + let aiMatchedCount = 0; + for (const group of matchedGroups) { + const mobileComponents = group.mobileComponents; + const twoInOneComponents = group.twoInOneComponents; + matchedComponentCount += Math.min(mobileComponents.length, twoInOneComponents.length); + if (group.aiMatched) { + aiMatchedCount += 1; + } + } + const aiLog = aiMatcher ? ` (ai-matched: ${aiMatchedCount})` : ''; + logger.info(`[full-width] Matched components: ${matchedComponentCount}${aiLog} (pageIndex=${pageIndex})`); + + const issues: CompareIssue[] = []; + for (const group of matchedGroups) { + const mobileComponents = group.mobileComponents; + const twoInOneComponents = group.twoInOneComponents; + if ( + hasFullWidthComponent(mobileComponents, mobileRect, tolerance) && + hasFullWidthComponent(twoInOneComponents, twoInOneRect, tolerance) + ) { + issues.push({ + pageIndex, + componentName: group.matchedName, + mobileScreenshot, + twoInOneScreenshot, + }); + } + } + return issues; +} + +function hasFullWidthComponent(components: Component[], screenRect: ScreenRect, tolerance: number): boolean { + return components.some((component) => isFullWidth(component, screenRect, tolerance)); +} + +function isFullWidth(component: Component, screenRect: ScreenRect, tolerance: number): boolean { + const rect = getBoundsRect(component.bounds ?? component.origBounds); + if (!rect) { + return false; + } + return rect.left <= screenRect.left + tolerance && rect.right >= screenRect.right - tolerance; +} diff --git a/src/compare/detectors/ratio_detector.ts b/src/compare/detectors/ratio_detector.ts new file mode 100644 index 0000000..764460c --- /dev/null +++ b/src/compare/detectors/ratio_detector.ts @@ -0,0 +1,64 @@ +import { Page } from '../../model/page'; +import { AiComponentMatcher, buildComponentParentMap, matchComponentParentMaps } from '../component_matcher'; +import { getAspectRatio } from '../geometry'; +import { CompareAspectRatioIssue } from '../types'; +import { HapTestLogger } from '../../utils/logger'; + +const logger = HapTestLogger.getLogger(); + +export async function detectAspectRatioIssues( + pageIndex: number, + mobilePage: Page, + twoInOnePage: Page, + mobileScreenshot: string, + twoInOneScreenshot: string, + tolerance: number, + aiMatcher?: AiComponentMatcher, + aiOnlyMatch = false +): Promise { + const mobileParentMap = buildComponentParentMap(mobilePage); + const twoInOneParentMap = buildComponentParentMap(twoInOnePage); + const matchedGroups = await matchComponentParentMaps(mobileParentMap, twoInOneParentMap, aiMatcher, { aiOnly: aiOnlyMatch }); + + let matchedComponentCount = 0; + let aiMatchedCount = 0; + for (const group of matchedGroups) { + const mobileComponents = group.mobileComponents; + const twoInOneComponents = group.twoInOneComponents; + matchedComponentCount += Math.min(mobileComponents.length, twoInOneComponents.length); + if (group.aiMatched) { + aiMatchedCount += 1; + } + } + const aiLog = aiMatcher ? ` (ai-matched: ${aiMatchedCount})` : ''; + logger.info(`[ratio] Matched components: ${matchedComponentCount}${aiLog} (pageIndex=${pageIndex})`); + + const issues: CompareAspectRatioIssue[] = []; + for (const group of matchedGroups) { + const mobileComponents = group.mobileComponents; + const twoInOneComponents = group.twoInOneComponents; + const pairCount = Math.min(mobileComponents.length, twoInOneComponents.length); + for (let index = 0; index < pairCount; index += 1) { + const mobileComponent = mobileComponents[index]; + const twoInOneComponent = twoInOneComponents[index]; + const mobileRatio = getAspectRatio(mobileComponent.component); + const twoInOneRatio = getAspectRatio(twoInOneComponent.component); + if (mobileRatio === undefined || twoInOneRatio === undefined) { + continue; + } + if (Math.abs(mobileRatio - twoInOneRatio) > tolerance) { + issues.push({ + pageIndex, + componentName: mobileComponent.componentName, + parentName: mobileComponent.parentName, + mobileAspectRatio: mobileRatio, + twoInOneAspectRatio: twoInOneRatio, + mobileScreenshot, + twoInOneScreenshot, + }); + } + } + } + + return issues; +} diff --git a/src/compare/detectors/scene_detector.ts b/src/compare/detectors/scene_detector.ts new file mode 100644 index 0000000..cfb079c --- /dev/null +++ b/src/compare/detectors/scene_detector.ts @@ -0,0 +1,192 @@ +import { Page } from '../../model/page'; +import { Component } from '../../model/component'; +import { CompareSceneIssue, TransitionRecord } from '../types'; +import { HapTestLogger } from '../../utils/logger'; + +const logger = HapTestLogger.getLogger(); + +const MAX_ANCHOR_TEXT_LENGTH = 12; + +export function detectSceneIssues( + transitionIndex: number, + mobileTransition: TransitionRecord, + twoInOneTransition: TransitionRecord, + mobileScreenshot: string, + twoInOneScreenshot: string, + similarityThreshold: number +): CompareSceneIssue[] { + const mobileEventType = getEventType(mobileTransition); + const twoInOneEventType = getEventType(twoInOneTransition); + if (!mobileEventType || !twoInOneEventType || mobileEventType !== twoInOneEventType) { + return []; + } + + if (!isLooseSameScene(mobileTransition.from, twoInOneTransition.from, similarityThreshold)) { + return []; + } + + const similarity = getBusinessSceneSimilarity(mobileTransition.to, twoInOneTransition.to); + const sameSceneClass = isSameBusinessSceneClass( + mobileTransition.to, + twoInOneTransition.to, + similarityThreshold, + similarity + ); + + logger.info( + `[scene] transition=${transitionIndex} event=${mobileEventType} fromMatch=true toSimilarity=${similarity.toFixed(3)}` + ); + + if (sameSceneClass) { + return []; + } + + return [ + { + transitionIndex, + eventType: mobileEventType, + reason: `destination business scene mismatch, similarity=${similarity.toFixed(3)}`, + similarity, + mobileFromPagePath: mobileTransition.from.getPagePath(), + twoInOneFromPagePath: twoInOneTransition.from.getPagePath(), + mobileToPagePath: mobileTransition.to.getPagePath(), + twoInOneToPagePath: twoInOneTransition.to.getPagePath(), + mobileToSceneKey: buildBusinessSceneKey(mobileTransition.to), + twoInOneToSceneKey: buildBusinessSceneKey(twoInOneTransition.to), + mobileScreenshot, + twoInOneScreenshot, + }, + ]; +} + +function getEventType(transition: TransitionRecord): string | undefined { + const json = transition.event?.toJson(); + const type = json?.type; + return typeof type === 'string' && type.trim().length > 0 ? type.trim() : undefined; +} + +function isLooseSameScene(left: Page, right: Page, similarityThreshold: number): boolean { + if (!isSameAbility(left, right)) { + return false; + } + + if (hasSamePagePath(left, right)) { + return true; + } + + if (left.getStructualSig() === right.getStructualSig()) { + return true; + } + + return getBusinessSceneSimilarity(left, right) >= similarityThreshold; +} + +function isSameBusinessSceneClass( + left: Page, + right: Page, + similarityThreshold: number, + similarity?: number +): boolean { + if (!isSameAbility(left, right)) { + return false; + } + + if (hasSamePagePath(left, right)) { + return true; + } + + if (left.getStructualSig() === right.getStructualSig()) { + return true; + } + + const sceneSimilarity = similarity ?? getBusinessSceneSimilarity(left, right); + return sceneSimilarity >= similarityThreshold; +} + +function isSameAbility(left: Page, right: Page): boolean { + return ( + left.getBundleName() === right.getBundleName() && + left.getAbilityName() === right.getAbilityName() + ); +} + +function hasSamePagePath(left: Page, right: Page): boolean { + const leftPath = left.getPagePath().trim(); + const rightPath = right.getPagePath().trim(); + return leftPath.length > 0 && leftPath === rightPath; +} + +function getBusinessSceneSimilarity(left: Page, right: Page): number { + const leftAnchors = collectBusinessAnchors(left); + const rightAnchors = collectBusinessAnchors(right); + if (leftAnchors.size === 0 || rightAnchors.size === 0) { + return 0; + } + + let intersection = 0; + for (const anchor of leftAnchors) { + if (rightAnchors.has(anchor)) { + intersection += 1; + } + } + + const union = new Set([...leftAnchors, ...rightAnchors]).size; + return union === 0 ? 0 : intersection / union; +} + +function buildBusinessSceneKey(page: Page): string { + const anchors = [...collectBusinessAnchors(page)].sort().slice(0, 8); + return [page.getBundleName(), page.getAbilityName(), page.getPagePath(), anchors.join('|')].join('::'); +} + +function collectBusinessAnchors(page: Page): Set { + const anchors = new Set(); + for (const component of page.getComponents()) { + appendComponentAnchor(anchors, 'id', component.id); + appendComponentAnchor(anchors, 'key', component.key); + appendComponentTextAnchor(anchors, component); + } + return anchors; +} + +function appendComponentAnchor(anchors: Set, prefix: string, value?: string): void { + const normalized = normalizeAnchorValue(value); + if (!normalized) { + return; + } + anchors.add(`${prefix}:${normalized}`); +} + +function appendComponentTextAnchor(anchors: Set, component: Component): void { + const text = normalizeTextAnchor(component.text); + const type = component.type?.trim(); + if (!text || !type) { + return; + } + anchors.add(`text:${type}:${text}`); +} + +function normalizeAnchorValue(value?: string): string | undefined { + const trimmed = value?.trim(); + if (!trimmed) { + return undefined; + } + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { + return undefined; + } + return trimmed; +} + +function normalizeTextAnchor(text?: string): string | undefined { + const trimmed = text?.trim(); + if (!trimmed) { + return undefined; + } + if (trimmed.length > MAX_ANCHOR_TEXT_LENGTH) { + return undefined; + } + if (/^\d+$/.test(trimmed)) { + return undefined; + } + return trimmed; +} \ No newline at end of file diff --git a/src/compare/geometry.ts b/src/compare/geometry.ts new file mode 100644 index 0000000..d103200 --- /dev/null +++ b/src/compare/geometry.ts @@ -0,0 +1,64 @@ +import { Component } from '../model/component'; +import { Page } from '../model/page'; +import { Point } from '../model/point'; +import { BoundsBox, ScreenRect } from './types'; + +export function getScreenRect(page: Page): ScreenRect | undefined { + const root = page.getRoot(); + const rootRect = getBoundsRect(root.bounds ?? root.origBounds); + if (rootRect && rootRect.right > rootRect.left) { + return rootRect; + } + + let minX = Number.POSITIVE_INFINITY; + let maxX = Number.NEGATIVE_INFINITY; + for (const component of page.getComponents()) { + const rect = getBoundsRect(component.bounds ?? component.origBounds); + if (!rect) { + continue; + } + minX = Math.min(minX, rect.left); + maxX = Math.max(maxX, rect.right); + } + + if (!Number.isFinite(minX) || !Number.isFinite(maxX) || maxX <= minX) { + return undefined; + } + + return { left: minX, right: maxX }; +} + +export function getBoundsRect(bounds?: Point[]): ScreenRect | undefined { + const box = getBoundsBox(bounds); + if (!box) { + return undefined; + } + return { left: box.left, right: box.right }; +} + +export function getBoundsBox(bounds?: Point[]): BoundsBox | undefined { + if (!bounds || bounds.length < 2) { + return undefined; + } + const xs = bounds.map((point) => point.x); + const ys = bounds.map((point) => point.y); + return { + left: Math.min(...xs), + right: Math.max(...xs), + top: Math.min(...ys), + bottom: Math.max(...ys), + }; +} + +export function getAspectRatio(component: Component): number | undefined { + const rect = getBoundsBox(component.bounds ?? component.origBounds); + if (!rect) { + return undefined; + } + const width = rect.right - rect.left; + const height = rect.bottom - rect.top; + if (width <= 0 || height <= 0) { + return undefined; + } + return width / height; +} diff --git a/src/compare/page_loader.ts b/src/compare/page_loader.ts new file mode 100644 index 0000000..d8d92f0 --- /dev/null +++ b/src/compare/page_loader.ts @@ -0,0 +1,180 @@ +import fs from 'fs'; +import path from 'path'; +import { EventBuilder } from '../event/event_builder'; +import { Component } from '../model/component'; +import { Page } from '../model/page'; +import { Point } from '../model/point'; +import { ViewTree } from '../model/viewtree'; +import { HapTestLogger } from '../utils/logger'; +import { SCREENSHOT_EXTENSIONS, TransitionRecord } from './types'; + +const logger = HapTestLogger.getLogger(); + +export function resolveRunDirectories( + outputRoot: string, + deviceDir: string, + appFolder: string, + label: string +): { eventsDir: string; tempDir: string } { + const deviceRoot = resolveDeviceRoot(outputRoot, deviceDir, label); + const appRoot = path.join(deviceRoot, appFolder); + ensureDirectory(appRoot, `${label} app folder`); + + const runRoot = resolveRunRoot(appRoot, label); + const eventsDir = path.join(runRoot, 'events'); + const tempDir = path.join(runRoot, 'temp'); + ensureDirectory(eventsDir, `${label} events`); + ensureDirectory(tempDir, `${label} temp`); + return { eventsDir, tempDir }; +} + +export function loadTransitions(eventsDir: string): TransitionRecord[] { + const files = fs + .readdirSync(eventsDir) + .filter((file) => file.endsWith('.json')) + .sort(); + + return files.map((file) => { + const fullPath = path.join(eventsDir, file); + const raw = fs.readFileSync(fullPath, { encoding: 'utf-8' }); + const parsed = JSON.parse(raw) as { from?: unknown; event?: unknown; to?: unknown }; + if (!parsed.from || !parsed.to) { + throw new Error(`Invalid transition file: ${fullPath}`); + } + return { + from: revivePage(parsed.from), + event: parsed.event ? EventBuilder.createEventFromJson(parsed.event) : undefined, + to: revivePage(parsed.to), + }; + }); +} + +export function buildPageSequence(transitions: TransitionRecord[]): Page[] { + if (transitions.length === 0) { + return []; + } + const pages: Page[] = []; + pages.push(transitions[0].from); + for (const transition of transitions) { + pages.push(transition.to); + } + return pages; +} + +export function listScreenshots(tempDir: string): string[] { + return fs + .readdirSync(tempDir) + .filter((file) => SCREENSHOT_EXTENSIONS.has(path.extname(file).toLowerCase())) + .sort() + .map((file) => path.join(tempDir, file)); +} + +function resolveDeviceRoot(outputRoot: string, deviceDir: string, label: string): string { + const exact = path.join(outputRoot, deviceDir); + if (fs.existsSync(exact)) { + return exact; + } + + const entries = fs.readdirSync(outputRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()); + const trimmedMatch = entries.find((entry) => entry.name.trim() === deviceDir); + if (trimmedMatch) { + const resolved = path.join(outputRoot, trimmedMatch.name); + logger.warn(`Resolved ${label} device dir "${deviceDir}" -> "${trimmedMatch.name}"`); + return resolved; + } + + throw new Error(`Missing ${label} device directory: ${exact}`); +} + +function resolveRunRoot(appRoot: string, label: string): string { + const directEvents = path.join(appRoot, 'events'); + const directTemp = path.join(appRoot, 'temp'); + if (fs.existsSync(directEvents) && fs.existsSync(directTemp)) { + return appRoot; + } + + const runDirs = fs + .readdirSync(appRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((name) => { + const runRoot = path.join(appRoot, name); + return fs.existsSync(path.join(runRoot, 'events')) && fs.existsSync(path.join(runRoot, 'temp')); + }) + .sort() + .reverse(); + + if (runDirs.length === 0) { + throw new Error(`No run directory with events/temp found under ${label} app folder: ${appRoot}`); + } + + if (runDirs.length > 1) { + logger.warn(`Multiple ${label} runs found. Using latest: ${runDirs[0]}`); + } + + return path.join(appRoot, runDirs[0]); +} + +function ensureDirectory(dirPath: string, label: string): void { + if (!fs.existsSync(dirPath)) { + throw new Error(`Missing ${label} directory: ${dirPath}`); + } +} + +function revivePage(raw: any): Page { + const abilityName = raw?.abilityName ?? ''; + const bundleName = raw?.bundleName ?? ''; + const pagePath = raw?.pagePath ?? ''; + const viewTreeRaw = raw?.viewTree ?? raw?.viewtree ?? raw?.root ?? raw; + const rootRaw = viewTreeRaw?.root ?? viewTreeRaw; + if (!rootRaw) { + throw new Error('Invalid page data: missing viewTree root'); + } + const root = reviveComponent(rootRaw); + const viewTree = new ViewTree(root); + return new Page(viewTree, abilityName, bundleName, pagePath); +} + +function reviveComponent(raw: any): Component { + const component = Object.assign(new Component(), raw); + component.bounds = parseBounds(raw?.bounds ?? component.bounds); + component.origBounds = parseBounds(raw?.origBounds ?? component.origBounds); + const children = Array.isArray(raw?.children) ? raw.children : []; + component.children = children.map((child: any) => { + const revived = reviveComponent(child); + revived.parent = component; + return revived; + }); + return component; +} + +function parseBounds(bounds: any): Point[] | undefined { + if (!bounds) { + return undefined; + } + if (typeof bounds === 'string') { + const regex = /\[(\d+),(\d+)\]/g; + const points: Point[] = []; + let match; + while ((match = regex.exec(bounds)) !== null) { + points.push({ x: parseInt(match[1], 10), y: parseInt(match[2], 10) }); + } + return points.length ? points : undefined; + } + if (Array.isArray(bounds)) { + return bounds + .map((item) => { + if (!item || typeof item !== 'object') { + return undefined; + } + const x = Number((item as any).x); + const y = Number((item as any).y); + if (Number.isFinite(x) && Number.isFinite(y)) { + return { x, y } as Point; + } + return undefined; + }) + .filter((item): item is Point => Boolean(item)); + } + return undefined; +} diff --git a/src/compare/types.ts b/src/compare/types.ts new file mode 100644 index 0000000..4ee88a1 --- /dev/null +++ b/src/compare/types.ts @@ -0,0 +1,112 @@ +import { Event } from '../event/event'; +import { Page } from '../model/page'; + +export type CompareDetector = 'all' | 'full-width' | 'ratio' | 'scene' | 'diff'; + +export interface CompareOptions { + outputRoot: string; + appFolder: string; + mobileDir?: string; + twoInOneDir?: string; + reportPath?: string; + fullWidthTolerance?: number; + aspectRatioTolerance?: number; + sceneSimilarityThreshold?: number; + detector?: CompareDetector; + aiComponentMatch?: boolean; + aiComponentModel?: string; + aiComponentThreshold?: number; + aiComponentMaxCalls?: number; + aiComponentConfigPath?: string; +} + +export interface CompareIssue { + pageIndex: number; + componentName: string; + mobileScreenshot: string; + twoInOneScreenshot: string; +} + +export interface CompareAspectRatioIssue { + pageIndex: number; + componentName: string; + parentName: string; + mobileAspectRatio: number; + twoInOneAspectRatio: number; + mobileScreenshot: string; + twoInOneScreenshot: string; +} + +export interface CompareSceneIssue { + transitionIndex: number; + eventType: string; + reason: string; + similarity: number; + mobileFromPagePath: string; + twoInOneFromPagePath: string; + mobileToPagePath: string; + twoInOneToPagePath: string; + mobileToSceneKey: string; + twoInOneToSceneKey: string; + mobileScreenshot: string; + twoInOneScreenshot: string; +} + +export interface CompareComponentFieldDiff { + field: string; + mobileValue: string; + twoInOneValue: string; +} + +export interface CompareComponentDiffGroups { + structuralDiffs: CompareComponentFieldDiff[]; + statusDiffs: CompareComponentFieldDiff[]; + textDiffs: CompareComponentFieldDiff[]; +} + +export interface CompareComponentDiffIssue { + pageIndex: number; + componentName: string; + parentName: string; + mobileScreenshot: string; + twoInOneScreenshot: string; + diffs: CompareComponentDiffGroups; +} + +export interface CompareResult { + issues: CompareIssue[]; + aspectRatioIssues: CompareAspectRatioIssue[]; + sceneIssues: CompareSceneIssue[]; + componentDiffIssues: CompareComponentDiffIssue[]; + pageCount: number; + transitionCount: number; + mobilePages: number; + twoInOnePages: number; + mobileTransitions: number; + twoInOneTransitions: number; + mobileScreenshots: number; + twoInOneScreenshots: number; +} + +export interface TransitionRecord { + from: Page; + event?: Event; + to: Page; +} + +export interface ScreenRect { + left: number; + right: number; +} + +export interface BoundsBox { + left: number; + right: number; + top: number; + bottom: number; +} + +export const DEFAULT_TOLERANCE = 1; +export const DEFAULT_RATIO_TOLERANCE = 0.01; +export const DEFAULT_SCENE_SIMILARITY_THRESHOLD = 0.35; +export const SCREENSHOT_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg']); diff --git a/src/device/device.ts b/src/device/device.ts index 6a68934..521dd27 100644 --- a/src/device/device.ts +++ b/src/device/device.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 +* http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -49,12 +49,15 @@ export class Device implements EventSimulator { private displaySize?: Point; private udid: string; private options: FuzzOptions; + private readonly is2in1Device: boolean; private arkuiInspector: ArkUIInspector; private lastFaultlogs: Set; private driverCtx?: DriverContext; constructor(options: FuzzOptions) { this.options = options; + const rawDeviceTypeOpt = options && options.deviceType ? String(options.deviceType) : ''; + this.is2in1Device = rawDeviceTypeOpt.trim().toLowerCase() === '2in1'; this.hdc = new Hdc(options.connectkey); this.output = path.join(path.resolve(options.output), moment().format('YYYY-MM-DD-HH-mm-ss')); this.udid = this.hdc.getDeviceUdid(); @@ -193,6 +196,133 @@ export class Device implements EventSimulator { this.hdc.forceStop(bundleName); } + /** + * Ensure the application is in fullscreen mode. + * If not, find and click the EnhanceMaximizeBtn to maximize the window. + * @param hap The HAP instance to check + * @returns true if already fullscreen or successfully maximized, false otherwise + */ + async ensureFullscreen(hap: Hap): Promise { + try { + // Wait for the app to fully stabilize after launch + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Get all pages from dumpViewTree, not just the "current" one + // because the app window might not be the largest page yet + let layout = await this.driverCtx!.driver.dumpLayout(); + let pages = PageBuilder.buildPagesFromLayout(layout); + + // Try to find the page that matches the target bundle name + let targetPage: Page | undefined; + for (const p of pages) { + if (p.getBundleName() === hap.bundleName && !p.isStop() && !p.isBackground()) { + targetPage = p; + break; + } + } + + // If not found, use the largest page (similar to dumpViewTree logic) + if (!targetPage && pages.length > 0) { + pages.sort((a: Page, b: Page) => { + return b.getRoot().getHeight() - a.getRoot().getHeight(); + }); + targetPage = pages[0]; + } + + if (!targetPage) { + logger.warn('No page found for fullscreen check'); + return false; + } + + let page = targetPage; + + const focusedInput = page.getComponents().find((component) => { + return component.focused === true && component.inputable; + }); + + if (focusedInput) { + logger.info( + `Focused input detected before fullscreen check, id=${focusedInput.id}, key=${focusedInput.key}, type=${focusedInput.type}. Sending Escape to dismiss focus.` + ); + try { + await this.inputKey(KeyCode.KEYCODE_ESCAPE); + await new Promise((resolve) => setTimeout(resolve, 300)); + page = await this.getCurrentPage(hap); + } catch (err) { + logger.warn('Failed to dismiss focused input before fullscreen check', err); + } + } + + // Check if the app is stopped or in background + if (page.isStop() || page.isBackground()) { + logger.debug('App is stopped or in background, skipping fullscreen check'); + return false; + } + + const root = page.getRoot(); + const windowWidth = root.getWidth(); + const windowHeight = root.getHeight(); + const screenWidth = this.getWidth(); + const screenHeight = this.getHeight(); + + // Check if window size matches screen size (with small tolerance for rounding) + const widthMatch = Math.abs(windowWidth - screenWidth) <= 10; + const heightMatch = Math.abs(windowHeight - screenHeight) <= 100; + + if (widthMatch && heightMatch) { + logger.debug('Application is already in fullscreen mode'); + return true; + } + + logger.info( + `Application is not fullscreen. Window: ${windowWidth}x${windowHeight}, Screen: ${screenWidth}x${screenHeight}. Attempting to maximize.` + ); + + const pageBundleName = page.getBundleName(); + const components = page.getComponents(); + logger.debug( + `ensureFullscreen: page bundleName=${pageBundleName}, hap bundleName=${hap.bundleName}, component count=${components.length}` + ); + + const maximizeBtn = components.find((component) => { + return component.id === 'EnhanceMaximizeBtn' || component.key === 'EnhanceMaximizeBtn'; + }); + + if (!maximizeBtn) { + logger.warn(`EnhanceMaximizeBtn not found. Searched ${components.length} components in page with bundleName=${pageBundleName}`); + return false; + } + + logger.info( + `Found EnhanceMaximizeBtn, id=${maximizeBtn.id}, key=${maximizeBtn.key}, bounds=${JSON.stringify(maximizeBtn.bounds)}, clicking to maximize` + ); + await this.sendEvent(new TouchEvent(maximizeBtn)); + + // Wait for the window to maximize + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Verify the window is now fullscreen + const updatedPage = await this.getCurrentPage(hap); + const updatedRoot = updatedPage.getRoot(); + const newWidth = updatedRoot.getWidth(); + const newHeight = updatedRoot.getHeight(); + + const newWidthMatch = Math.abs(newWidth - screenWidth) <= 2; + const newHeightMatch = Math.abs(newHeight - screenHeight) <= 2; + + if (newWidthMatch && newHeightMatch) { + logger.info('Successfully maximized application to fullscreen'); + return true; + } else { + logger.warn(`Maximize button clicked but window size still not fullscreen. New size: ${newWidth}x${newHeight}`); + return false; + } + } catch (error) { + logger.error('Error during fullscreen check/maximize operation', error); + return false; + } + } + /** * Dump UI component view tree * @returns @@ -207,8 +337,20 @@ export class Device implements EventSimulator { logger.debug( `dumpViewTree attempt=${attempt} layoutType=${layout ? typeof layout : 'undefined'} pages=${pages.length}` ); - // if exist keyboard then close and dump again. - if (this.closeKeyboard(pages)) { + const hasKeyboardPage = this.hasKeyboardPage(pages); + // 2in1: 仅在检测到输入法页面时发送回车确认,且不尝试收起键盘 + if (this.is2in1Device) { + if (hasKeyboardPage) { + logger.info('2in1 设备检测到输入法页面:发送回车以确认输入,跳过收起键盘逻辑'); + try { + await this.inputKey(KeyCode.KEYCODE_ENTER); + } catch (err) { + logger.warn('2in1 设备发送回车键失败', err); + } + } else { + logger.debug('2in1 设备:未检测到输入法页面,跳过键盘处理'); + } + } else if (this.closeKeyboard(pages)) { logger.info('Keyboard detected during dumpViewTree, sending hide event and retrying.'); // for sleep this.hdc.getDeviceUdid(); @@ -257,6 +399,15 @@ export class Device implements EventSimulator { return false; } + private hasKeyboardPage(pages: Page[]): boolean { + for (const page of pages) { + if (page.getBundleName() === 'com.huawei.hmos.inputmethod') { + return true; + } + } + return false; + } + /** * Dump inspector layout and snapshot * @param bundleName @@ -266,11 +417,64 @@ export class Device implements EventSimulator { return this.arkuiInspector.dump(bundleName, this.options.connectkey); } + /** + * Check if a point is inside a component's bounds + */ + private isPointInComponent(point: Point, component: Component): boolean { + if (!component.bounds || component.bounds.length < 2) { + return false; + } + const left = Math.min(component.bounds[0].x, component.bounds[1].x); + const right = Math.max(component.bounds[0].x, component.bounds[1].x); + const top = Math.min(component.bounds[0].y, component.bounds[1].y); + const bottom = Math.max(component.bounds[0].y, component.bounds[1].y); + + return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom; + } + + /** + * Check if a component is a control button that should be skipped + */ + private isControlButton(component: Component): boolean { + const controlButtonIds = ['EnhanceMaximizeBtn', 'EnhanceMinimizeBtn', 'EnhanceCloseBtn']; + const id = component.id || ''; + const key = component.key || ''; + + return controlButtonIds.includes(id) || controlButtonIds.includes(key); + } + + /** + * Only inspect the window control region for 2in1 clicks. + */ + private shouldInspectControlButtons(point: Point): boolean { + const screenWidth = this.getWidth(); + const screenHeight = this.getHeight(); + + return point.x >= Math.floor(screenWidth * 0.75) && point.y <= Math.floor(screenHeight * 0.25); + } + /** * Simulate a single click * @param point */ async click(point: Point): Promise { + const deviceType = this.getDeviceType().trim().toLowerCase(); + + if (deviceType === '2in1' && this.shouldInspectControlButtons(point)) { + try { + const page = await this.dumpViewTree(); + const components = page.getComponents(); + for (const comp of components) { + if (this.isPointInComponent(point, comp) && this.isControlButton(comp)) { + logger.info(`2in1 device: skipping click on control button ${comp.id || comp.key}`); + return; + } + } + } catch (err) { + logger.warn('Error detecting component at click point', err); + } + } + await this.driverCtx?.driver?.click(point.x, point.y); } @@ -279,6 +483,23 @@ export class Device implements EventSimulator { * @param point */ async doubleClick(point: Point): Promise { + const deviceType = this.getDeviceType().trim().toLowerCase(); + + if (deviceType === '2in1' && this.shouldInspectControlButtons(point)) { + try { + const page = await this.dumpViewTree(); + const components = page.getComponents(); + for (const comp of components) { + if (this.isPointInComponent(point, comp) && this.isControlButton(comp)) { + logger.info(`2in1 device: skipping double click on control button ${comp.id || comp.key}`); + return; + } + } + } catch (err) { + logger.warn('Error detecting component at double click point', err); + } + } + await this.driverCtx?.driver?.doubleClick(point.x, point.y); } @@ -287,6 +508,23 @@ export class Device implements EventSimulator { * @param point */ async longClick(point: Point): Promise { + const deviceType = this.getDeviceType().trim().toLowerCase(); + + if (deviceType === '2in1' && this.shouldInspectControlButtons(point)) { + try { + const page = await this.dumpViewTree(); + const components = page.getComponents(); + for (const comp of components) { + if (this.isPointInComponent(point, comp) && this.isControlButton(comp)) { + logger.info(`2in1 device: skipping long click on control button ${comp.id || comp.key}`); + return; + } + } + } catch (err) { + logger.warn('Error detecting component at long click point', err); + } + } + await this.driverCtx?.driver?.longClick(point.x, point.y); } @@ -375,11 +613,47 @@ export class Device implements EventSimulator { * @returns */ async getCurrentPage(hap: Hap): Promise { - let page = await this.dumpViewTree(); + // Get all pages from dumpLayout to find the correct one + let layout = await this.driverCtx!.driver.dumpLayout(); + let pages = PageBuilder.buildPagesFromLayout(layout); + + // If we have a target bundleName, try to find matching page first + let page: Page | undefined; + if (hap.bundleName) { + for (const p of pages) { + if (p.getBundleName() === hap.bundleName && !p.isStop() && !p.isBackground()) { + page = p; + logger.debug(`getCurrentPage: Found matching page with bundleName=${hap.bundleName}`); + break; + } + } + } + + // If no matching page found, use dumpViewTree logic (sort by height) + if (!page) { + pages.sort((a: Page, b: Page) => { + return b.getRoot().getHeight() - a.getRoot().getHeight(); + }); + if (pages.length > 0) { + page = pages[0]; + const pageBundleName = page.getBundleName(); + if (!hap.bundleName) { + hap.bundleName = pageBundleName; + } + logger.debug(`getCurrentPage: Using largest page with bundleName=${pageBundleName}`); + } + } + + if (!page) { + logger.warn('getCurrentPage: No page found, returning fallback'); + return this.createFallbackPage(); + } + const pageBundleName = page.getBundleName(); if (!hap.bundleName) { hap.bundleName = pageBundleName; } + if (this.options.sourceRoot) { let inspector = await this.dumpInspector(hap.bundleName); page.mergeInspector(inspector.layout); @@ -594,4 +868,4 @@ export class Device implements EventSimulator { fs.renameSync(localPath, targetPath); } } -} +} \ No newline at end of file diff --git a/src/device/uidriver/hypium_rpc.ts b/src/device/uidriver/hypium_rpc.ts index 64728ed..0f6e298 100644 --- a/src/device/uidriver/hypium_rpc.ts +++ b/src/device/uidriver/hypium_rpc.ts @@ -14,52 +14,83 @@ */ import moment from 'moment'; +import { getLogger } from 'log4js'; import { ClientSocket } from '../../utils/net_utils'; +const logger = getLogger(); + export class HypiumRpc { private socket: ClientSocket; private timeout: number; private connected: boolean; + private hostPort?: number; + private hostAddress: string; constructor(timeout: number = 10000) { this.socket = new ClientSocket(); this.timeout = timeout; this.connected = false; + this.hostAddress = '127.0.0.1'; } - async connect(port: number, address: string = '127.0.0.1'): Promise { - this.socket.setTimeout(this.timeout); - await this.socket.connect(port, address); - this.socket.setTimeout(0); - this.connected = true; - return this.connected; - } - - async close() { - if (this.connected) { - await this.socket.close(); - this.connected = false; - } - } + async connect(port: number, address: string = '127.0.0.1'): Promise { + this.hostPort = port; + this.hostAddress = address; + this.socket = new ClientSocket(); + this.socket.setTimeout(this.timeout); + await this.socket.connect(port, address); + this.socket.setTimeout(0); + this.connected = true; + return this.connected; + } + + async close() { + if (this.connected) { + await this.socket.close(); + this.connected = false; + } + } async request(method: string, params: any): Promise { - // if (!this.connected) { - // throw new Error('Socket not connected.'); - // } - let data = { + const data = { module: 'com.ohos.devicetest.hypiumApiHelper', method: method, params: params, request_id: moment().format('YYYYMMDDHHmmssSSSSSS'), client: '127.0.0.1', }; - this.socket.setTimeout(this.timeout); - await this.socket.write(JSON.stringify(data) + '\n'); - let response = await this.socket.read(); - this.socket.setTimeout(0); - if (response) { - response = JSON.parse(response).result; + + try { + if (!this.connected && this.hostPort !== undefined) { + await this.connect(this.hostPort, this.hostAddress); + } + + this.socket.setTimeout(this.timeout); + await this.socket.write(JSON.stringify(data) + '\n'); + let response = await this.socket.read(); + this.socket.setTimeout(0); + if (response) { + response = JSON.parse(response).result; + } + return response; + } catch (error) { + logger.warn(`HypiumRpc request failed for ${method}, reconnecting and continuing`, error); + this.connected = false; + try { + await this.socket.close(); + } catch { + // ignore close errors + } + + if (this.hostPort !== undefined) { + try { + await this.connect(this.hostPort, this.hostAddress); + } catch (reconnectError) { + logger.warn(`HypiumRpc reconnect failed for ${method}`, reconnectError); + } + } + + return undefined; } - return response; } } diff --git a/src/runner/event_action.ts b/src/runner/event_action.ts index 201e228..4a3f605 100644 --- a/src/runner/event_action.ts +++ b/src/runner/event_action.ts @@ -18,22 +18,25 @@ import fs from 'fs'; import moment from 'moment'; import { Device } from '../device/device'; import { Event } from '../event/event'; +import { AbilityEvent } from '../event/system_event'; import { Hap } from '../model/hap'; import { SerializeUtils } from '../utils/serialize_utils'; import { Page } from '../model/page'; -import { getLogger } from 'log4js'; +import { HapTestLogger } from '../utils/logger'; import { Transition } from '../model/ptg'; -const logger = getLogger(); +const logger = HapTestLogger.getLogger(); export class EventAction { device: Device; hap: Hap; + deviceType?: string; transition: Transition; output: string; - constructor(device: Device, hap: Hap, page: Page, event: Event) { + constructor(device: Device, hap: Hap, page: Page, event: Event, deviceType?: string) { this.device = device; this.hap = hap; + this.deviceType = deviceType; this.transition = { from: page, event: event, to: page }; this.output = path.join(device.getOutput(), 'events'); if (!fs.existsSync(this.output)) { @@ -48,6 +51,13 @@ export class EventAction { async stop() { this.transition.to = await this.device.getCurrentPage(this.hap); + + // If this was an AbilityEvent (app launch), ensure the app is in fullscreen mode + if (this.transition.event instanceof AbilityEvent) { + logger.info('AbilityEvent detected, checking and ensuring fullscreen mode'); + await this.device.ensureFullscreen(this.hap); + } + logger.info(`EventAction->stop`); this.save(); } diff --git a/src/runner/fuzz.ts b/src/runner/fuzz.ts index 0b2fc9a..8ba5256 100644 --- a/src/runner/fuzz.ts +++ b/src/runner/fuzz.ts @@ -17,6 +17,9 @@ import { Device } from '../device/device'; import { GlobMatch } from '../utils/glob_match'; import { FuzzOptions } from './fuzz_options'; import { RunnerManager } from './runner_manager'; +import { HapTestLogger } from '../utils/logger'; + +const logger = HapTestLogger.getLogger(); /** * Fuzz test entrance @@ -31,6 +34,15 @@ export class Fuzz { } async start() { + // 在开始执行时,先检测设备类型并保存到 options 中 + try { + const deviceType = this.device.getDeviceType(); + this.options.deviceType = deviceType; + logger.info(`Detected device type: ${deviceType}`); + console.log(`设备类型: ${deviceType}`); + } catch (err) { + logger.error('Failed to detect device type.', err); + } if (this.options.bundleName !== 'ALL') { await this.startOneBundle(this.options.bundleName); return; diff --git a/src/runner/fuzz_options.ts b/src/runner/fuzz_options.ts index 1cc57f7..801fbd2 100644 --- a/src/runner/fuzz_options.ts +++ b/src/runner/fuzz_options.ts @@ -24,6 +24,8 @@ export interface FuzzOptions { hapFile?: string; reportRoot?: string; excludes?: string[]; + // 设备类型,例如: phone, tablet, wearable, car, tv, 2in1 + deviceType?: string; // xmq: add cli option llm?: boolean; simK: number; diff --git a/src/runner/runner_manager.ts b/src/runner/runner_manager.ts index e900ae4..dd9ba01 100644 --- a/src/runner/runner_manager.ts +++ b/src/runner/runner_manager.ts @@ -117,7 +117,7 @@ export class RunnerManager { } protected async addEvent(page: Page, event: Event): Promise { - let eventExcute = new EventAction(this.device, this.hap, page, event); + let eventExcute = new EventAction(this.device, this.hap, page, event, this.options.deviceType); await eventExcute.start(); // sleep interval await sleep(EVENT_INTERVAL); diff --git a/src/utils/dynamic_compare.ts b/src/utils/dynamic_compare.ts new file mode 100644 index 0000000..b9c31da --- /dev/null +++ b/src/utils/dynamic_compare.ts @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import { HapTestLogger } from './logger'; +import { detectAspectRatioIssues } from '../compare/detectors/ratio_detector'; +import { detectFullWidthIssues } from '../compare/detectors/full_width_detector'; +import { detectSceneIssues } from '../compare/detectors/scene_detector'; +import { detectComponentDiffIssues } from '../compare/detectors/diff_detector'; +import { OpenAiComponentMatcher } from '../compare/ai_component_matcher'; +import { + buildPageSequence, + listScreenshots, + loadTransitions, + resolveRunDirectories, +} from '../compare/page_loader'; +import { + CompareAspectRatioIssue, + CompareComponentDiffIssue, + CompareDetector, + CompareIssue, + CompareOptions, + CompareResult, + CompareSceneIssue, + DEFAULT_RATIO_TOLERANCE, + DEFAULT_SCENE_SIMILARITY_THRESHOLD, + DEFAULT_TOLERANCE, +} from '../compare/types'; + +const logger = HapTestLogger.getLogger(); + +export type { + CompareAspectRatioIssue, + CompareComponentDiffIssue, + CompareDetector, + CompareIssue, + CompareOptions, + CompareResult, + CompareSceneIssue, +}; + +export async function compareDynamicLogs(options: CompareOptions): Promise { + const detector: CompareDetector = options.detector ?? 'all'; + const runFullWidth = detector === 'all' || detector === 'full-width'; + const runAspectRatio = detector === 'all' || detector === 'ratio'; + const runScene = detector === 'all' || detector === 'scene'; + const runDiff = detector === 'all' || detector === 'diff'; + const mobileDir = options.mobileDir ?? 'mobile'; + const twoInOneDir = options.twoInOneDir ?? '2in1'; + const tolerance = Number.isFinite(options.fullWidthTolerance) ? options.fullWidthTolerance! : DEFAULT_TOLERANCE; + const aspectRatioTolerance = Number.isFinite(options.aspectRatioTolerance) + ? options.aspectRatioTolerance! + : DEFAULT_RATIO_TOLERANCE; + const sceneSimilarityThreshold = Number.isFinite(options.sceneSimilarityThreshold) + ? options.sceneSimilarityThreshold! + : DEFAULT_SCENE_SIMILARITY_THRESHOLD; + const aiComponentMatcher = options.aiComponentMatch + ? OpenAiComponentMatcher.createFromConfig({ + configPath: options.aiComponentConfigPath, + model: options.aiComponentModel, + threshold: options.aiComponentThreshold, + maxCalls: options.aiComponentMaxCalls, + }) + : undefined; + const aiOnlyMatch = options.aiComponentMatch === true; + + const mobileResolved = resolveRunDirectories(options.outputRoot, mobileDir, options.appFolder, 'mobile'); + const twoInOneResolved = resolveRunDirectories(options.outputRoot, twoInOneDir, options.appFolder, '2in1'); + + const mobileTransitions = loadTransitions(mobileResolved.eventsDir); + const twoInOneTransitions = loadTransitions(twoInOneResolved.eventsDir); + + const mobilePages = buildPageSequence(mobileTransitions); + const twoInOnePages = buildPageSequence(twoInOneTransitions); + + const mobileScreenshots = listScreenshots(mobileResolved.tempDir); + const twoInOneScreenshots = listScreenshots(twoInOneResolved.tempDir); + + const pageCount = Math.min(mobilePages.length, twoInOnePages.length, mobileScreenshots.length, twoInOneScreenshots.length); + const transitionCount = Math.min(mobileTransitions.length, twoInOneTransitions.length); + if (mobilePages.length !== twoInOnePages.length) { + logger.warn(`Page count mismatch: mobile=${mobilePages.length}, 2in1=${twoInOnePages.length}. Using min=${pageCount}.`); + } + if (mobileScreenshots.length !== twoInOneScreenshots.length) { + logger.warn( + `Screenshot count mismatch: mobile=${mobileScreenshots.length}, 2in1=${twoInOneScreenshots.length}. Using min=${pageCount}.` + ); + } + + const issues: CompareIssue[] = []; + const aspectRatioIssues: CompareAspectRatioIssue[] = []; + const sceneIssues: CompareSceneIssue[] = []; + const componentDiffIssues: CompareComponentDiffIssue[] = []; + for (let i = 0; i < pageCount; i += 1) { + const mobilePage = mobilePages[i]; + const twoInOnePage = twoInOnePages[i]; + const mobileScreen = mobileScreenshots[i]; + const twoInOneScreen = twoInOneScreenshots[i]; + + if (runFullWidth) { + const fullWidthFindings = await detectFullWidthIssues( + i, + mobilePage, + twoInOnePage, + mobileScreen, + twoInOneScreen, + tolerance, + aiComponentMatcher, + aiOnlyMatch + ); + issues.push(...fullWidthFindings); + } + + if (runAspectRatio) { + const ratioFindings = await detectAspectRatioIssues( + i, + mobilePage, + twoInOnePage, + mobileScreen, + twoInOneScreen, + aspectRatioTolerance, + aiComponentMatcher, + aiOnlyMatch + ); + aspectRatioIssues.push(...ratioFindings); + } + + if (runDiff) { + const diffFindings = await detectComponentDiffIssues( + i, + mobilePage, + twoInOnePage, + mobileScreen, + twoInOneScreen, + aiComponentMatcher, + aiOnlyMatch + ); + componentDiffIssues.push(...diffFindings); + } + } + + if (runScene) { + for (let i = 0; i < transitionCount; i += 1) { + const mobileTransition = mobileTransitions[i]; + const twoInOneTransition = twoInOneTransitions[i]; + const mobileScreen = getTransitionScreenshot(mobileScreenshots, i); + const twoInOneScreen = getTransitionScreenshot(twoInOneScreenshots, i); + const findings = detectSceneIssues( + i, + mobileTransition, + twoInOneTransition, + mobileScreen, + twoInOneScreen, + sceneSimilarityThreshold + ); + sceneIssues.push(...findings); + } + } + + const result: CompareResult = { + issues, + aspectRatioIssues, + sceneIssues, + componentDiffIssues, + pageCount, + transitionCount, + mobilePages: mobilePages.length, + twoInOnePages: twoInOnePages.length, + mobileTransitions: mobileTransitions.length, + twoInOneTransitions: twoInOneTransitions.length, + mobileScreenshots: mobileScreenshots.length, + twoInOneScreenshots: twoInOneScreenshots.length, + }; + + if (options.reportPath) { + fs.mkdirSync(path.dirname(options.reportPath), { recursive: true }); + fs.writeFileSync(options.reportPath, JSON.stringify(result, null, 2), { encoding: 'utf-8' }); + logger.info(`Dynamic compare report saved: ${options.reportPath}`); + } + + const issueCounters: string[] = []; + const fullWidthIssueCount = issues.length; + const aspectRatioIssueCount = aspectRatioIssues.length; + const sceneIssueCount = sceneIssues.length; + const componentDiffIssueCount = componentDiffIssues.length; + + if (detector === 'all') { + const totalIssues = fullWidthIssueCount + aspectRatioIssueCount + sceneIssueCount + componentDiffIssueCount; + issueCounters.push(`Issues=${totalIssues}`); + issueCounters.push(`FullWidthIssues=${fullWidthIssueCount}`); + issueCounters.push(`AspectRatioIssues=${aspectRatioIssueCount}`); + issueCounters.push(`SceneIssues=${sceneIssueCount}`); + issueCounters.push(`ComponentDiffIssues=${componentDiffIssueCount}`); + } else { + if (runFullWidth) { + issueCounters.push(`FullWidthIssues=${fullWidthIssueCount}`); + } + if (runAspectRatio) { + issueCounters.push(`AspectRatioIssues=${aspectRatioIssueCount}`); + } + if (runScene) { + issueCounters.push(`SceneIssues=${sceneIssueCount}`); + } + if (runDiff) { + issueCounters.push(`ComponentDiffIssues=${componentDiffIssueCount}`); + } + } + const detectorSummary = issueCounters.join(', '); + + logger.info( + `Dynamic compare finished. Detector=${detector}, ${detectorSummary}, PagesCompared=${pageCount}, TransitionsCompared=${transitionCount}` + ); + return result; +} + +function getTransitionScreenshot(screenshots: string[], transitionIndex: number): string { + if (screenshots.length === 0) { + return ''; + } + return screenshots[Math.min(transitionIndex + 1, screenshots.length - 1)]; +} diff --git a/test/unit/component_matcher_ai.test.ts b/test/unit/component_matcher_ai.test.ts new file mode 100644 index 0000000..bed4fdd --- /dev/null +++ b/test/unit/component_matcher_ai.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; +import { Component } from '../../src/model/component'; +import { Page } from '../../src/model/page'; +import { ViewTree } from '../../src/model/viewtree'; +import { + AiComponentMatchContext, + AiComponentMatcher, + buildComponentNameMap, + buildComponentParentMap, + matchComponentNameMaps, + matchComponentParentMaps, +} from '../../src/compare/component_matcher'; + +function createComponent( + type: string, + options: { + id?: string; + key?: string; + text?: string; + children?: Component[]; + } = {} +): Component { + const component = new Component(); + component.type = type; + component.id = options.id; + component.key = options.key; + component.text = options.text; + component.bounds = [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ]; + component.children = options.children ?? []; + for (const child of component.children) { + child.parent = component; + } + return component; +} + +function createPage(children: Component[]): Page { + const root = createComponent('Root', { id: 'root', children }); + return new Page(new ViewTree(root), 'Ability', 'com.example.demo', 'pages/index'); +} + +class MockAiMatcher implements AiComponentMatcher { + async isSameComponent( + mobileComponent: Component, + twoInOneComponent: Component, + _context: AiComponentMatchContext + ): Promise { + const mobileText = mobileComponent.text?.trim(); + const twoInOneText = twoInOneComponent.text?.trim(); + return !!mobileText && mobileText === twoInOneText; + } +} + +describe('component_matcher ai fallback', () => { + it('uses ai fallback for name map when identity differs', async () => { + const mobilePage = createPage([ + createComponent('Button', { key: 'btn_login_mobile', text: '登录' }), + ]); + const twoInOnePage = createPage([ + createComponent('Button', { key: 'btn_login_2in1', text: '登录' }), + ]); + + const mobileMap = buildComponentNameMap(mobilePage); + const twoInOneMap = buildComponentNameMap(twoInOnePage); + + const exactMatches = await matchComponentNameMaps(mobileMap, twoInOneMap); + expect(exactMatches).toHaveLength(1); + + const aiMatches = await matchComponentNameMaps(mobileMap, twoInOneMap, new MockAiMatcher()); + expect(aiMatches).toHaveLength(2); + expect(aiMatches[1].aiMatched).toBe(true); + }); + + it('uses ai-only mode for name map and skips exact pre-match', async () => { + const mobilePage = createPage([ + createComponent('Button', { key: 'btn_same_identity', text: '登录' }), + ]); + const twoInOnePage = createPage([ + createComponent('Button', { key: 'btn_same_identity', text: '注册' }), + ]); + + const mobileMap = buildComponentNameMap(mobilePage); + const twoInOneMap = buildComponentNameMap(twoInOnePage); + const targetKey = 'Button::btn_same_identity'; + + const exactMatches = await matchComponentNameMaps(mobileMap, twoInOneMap); + expect(exactMatches.some((match) => match.mobileKey === targetKey && match.twoInOneKey === targetKey)).toBe(true); + expect(exactMatches.some((match) => match.aiMatched)).toBe(false); + + const aiOnlyMatches = await matchComponentNameMaps(mobileMap, twoInOneMap, new MockAiMatcher(), { + aiOnly: true, + }); + expect(aiOnlyMatches.some((match) => match.mobileKey === targetKey && match.twoInOneKey === targetKey)).toBe(false); + }); + + it('uses ai fallback for parent map when parent-child identity differs', async () => { + const mobilePage = createPage([ + createComponent('Column', { + key: 'container_mobile', + children: [createComponent('Image', { key: 'cover_mobile', text: '海报' })], + }), + ]); + const twoInOnePage = createPage([ + createComponent('Column', { + key: 'container_2in1', + children: [createComponent('Image', { key: 'cover_2in1', text: '海报' })], + }), + ]); + + const mobileMap = buildComponentParentMap(mobilePage); + const twoInOneMap = buildComponentParentMap(twoInOnePage); + + const exactMatches = await matchComponentParentMaps(mobileMap, twoInOneMap); + expect(exactMatches).toHaveLength(1); + + const aiMatches = await matchComponentParentMaps(mobileMap, twoInOneMap, new MockAiMatcher()); + expect(aiMatches).toHaveLength(2); + expect(aiMatches[1].aiMatched).toBe(true); + }); + + it('uses ai-only mode for parent map and skips exact pre-match', async () => { + const mobilePage = createPage([ + createComponent('Column', { + key: 'container', + children: [createComponent('Image', { key: 'cover', text: '海报A' })], + }), + ]); + const twoInOnePage = createPage([ + createComponent('Column', { + key: 'container', + children: [createComponent('Image', { key: 'cover', text: '海报B' })], + }), + ]); + + const mobileMap = buildComponentParentMap(mobilePage); + const twoInOneMap = buildComponentParentMap(twoInOnePage); + const targetKey = 'container>>cover'; + + const exactMatches = await matchComponentParentMaps(mobileMap, twoInOneMap); + expect(exactMatches.some((match) => match.mobileKey === targetKey && match.twoInOneKey === targetKey)).toBe(true); + expect(exactMatches.some((match) => match.aiMatched)).toBe(false); + + const aiOnlyMatches = await matchComponentParentMaps(mobileMap, twoInOneMap, new MockAiMatcher(), { + aiOnly: true, + }); + expect(aiOnlyMatches.some((match) => match.mobileKey === targetKey && match.twoInOneKey === targetKey)).toBe(false); + }); +}); diff --git a/test/unit/diff_detector.test.ts b/test/unit/diff_detector.test.ts new file mode 100644 index 0000000..5cb450b --- /dev/null +++ b/test/unit/diff_detector.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest'; +import { Component } from '../../src/model/component'; +import { Page } from '../../src/model/page'; +import { ViewTree } from '../../src/model/viewtree'; +import { detectComponentDiffIssues } from '../../src/compare/detectors/diff_detector'; + +function createComponent( + type: string, + options: { + id?: string; + key?: string; + text?: string; + visible?: boolean; + children?: Component[]; + bounds?: Array<{ x: number; y: number }>; + } = {} +): Component { + const component = new Component(); + component.type = type; + component.id = options.id; + component.key = options.key; + component.text = options.text; + component.visible = options.visible; + component.bounds = options.bounds ?? [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ]; + component.children = options.children ?? []; + for (const child of component.children) { + child.parent = component; + } + return component; +} + +function createPage(children: Component[]): Page { + const root = createComponent('Root', { id: 'root', children }); + return new Page(new ViewTree(root), 'Ability', 'com.example.demo', 'pages/index'); +} + +describe('detectComponentDiffIssues', () => { + it('returns empty when matched components are identical', async () => { + const mobilePage = createPage([ + createComponent('Column', { + key: 'container', + children: [ + createComponent('Button', { + key: 'btn_submit', + text: '提交', + visible: true, + }), + ], + }), + ]); + + const twoInOnePage = createPage([ + createComponent('Column', { + key: 'container', + children: [ + createComponent('Button', { + key: 'btn_submit', + text: '提交', + visible: true, + }), + ], + }), + ]); + + const issues = await detectComponentDiffIssues(0, mobilePage, twoInOnePage, 'mobile.png', '2in1.png'); + expect(issues).toHaveLength(0); + }); + + it('reports every field-level difference for matched components', async () => { + const mobilePage = createPage([ + createComponent('Column', { + key: 'container', + children: [ + createComponent('Button', { + key: 'btn_submit', + text: '提交', + visible: true, + bounds: [ + { x: 10, y: 20 }, + { x: 110, y: 80 }, + ], + }), + ], + }), + ]); + + const twoInOnePage = createPage([ + createComponent('Column', { + key: 'container', + children: [ + createComponent('Button', { + key: 'btn_submit', + text: '确认', + visible: false, + bounds: [ + { x: 12, y: 20 }, + { x: 140, y: 86 }, + ], + }), + ], + }), + ]); + + const issues = await detectComponentDiffIssues(1, mobilePage, twoInOnePage, 'mobile.png', '2in1.png'); + expect(issues).toHaveLength(1); + expect(issues[0].componentName).toBe('btn_submit'); + + const structuralFields = issues[0].diffs.structuralDiffs.map((item) => item.field); + const statusFields = issues[0].diffs.statusDiffs.map((item) => item.field); + const textFields = issues[0].diffs.textDiffs.map((item) => item.field); + + expect(structuralFields).toContain('bounds'); + expect(statusFields).toContain('visible'); + expect(textFields).toContain('text'); + }); +}); diff --git a/test/unit/scene_detector.test.ts b/test/unit/scene_detector.test.ts new file mode 100644 index 0000000..30f23a9 --- /dev/null +++ b/test/unit/scene_detector.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest'; +import { Direct } from '../../src/device/event_simulator'; +import { Event } from '../../src/event/event'; +import { ScrollEvent, TouchEvent } from '../../src/event/ui_event'; +import { Component } from '../../src/model/component'; +import { Page } from '../../src/model/page'; +import { ViewTree } from '../../src/model/viewtree'; +import { detectSceneIssues } from '../../src/compare/detectors/scene_detector'; +import { TransitionRecord } from '../../src/compare/types'; + +function createComponent( + type: string, + options: { + text?: string; + id?: string; + key?: string; + children?: Component[]; + } = {} +): Component { + const component = new Component(); + component.type = type; + component.text = options.text; + component.id = options.id; + component.key = options.key; + component.bounds = [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ]; + component.children = options.children ?? []; + for (const child of component.children) { + child.parent = component; + } + return component; +} + +function createPage(pagePath: string, children: Component[]): Page { + const root = createComponent('root', { children }); + return new Page(new ViewTree(root), 'PhoneAbility', 'com.example.demo', pagePath); +} + +function createTransition(from: Page, to: Page, event: Event = new TouchEvent({ x: 10, y: 10 })): TransitionRecord { + return { from, event, to }; +} + +describe('detectSceneIssues', () => { + it('skips when destination pages belong to the same business scene class', () => { + const mobileFrom = createPage('pages/Home', [ + createComponent('Tabs', { key: 'home_tabs' }), + createComponent('Text', { text: '首页' }), + ]); + const twoInOneFrom = createPage('pages/Home', [ + createComponent('Tabs', { key: 'home_tabs' }), + createComponent('Text', { text: '首页' }), + createComponent('Blank'), + ]); + + const mobileTo = createPage('pages/Detail', [ + createComponent('Text', { text: '评论' }), + createComponent('Button', { key: 'collect' }), + ]); + const twoInOneTo = createPage('pages/Detail', [ + createComponent('Button', { key: 'collect' }), + createComponent('Text', { text: '评论' }), + createComponent('Column'), + ]); + + const issues = detectSceneIssues( + 0, + createTransition(mobileFrom, mobileTo), + createTransition(twoInOneFrom, twoInOneTo), + 'mobile.png', + '2in1.png', + 0.35 + ); + + expect(issues).toHaveLength(0); + }); + + it('reports when the event type matches but destination business scenes diverge', () => { + const sharedFromMobile = createPage('pages/Home', [ + createComponent('Tabs', { key: 'home_tabs' }), + createComponent('Text', { text: '首页' }), + ]); + const sharedFromTwoInOne = createPage('pages/Home', [ + createComponent('Tabs', { key: 'home_tabs' }), + createComponent('Text', { text: '首页' }), + ]); + + const mobileTo = createPage('pages/Detail', [ + createComponent('Button', { key: 'comment' }), + createComponent('Text', { text: '评论' }), + ]); + const twoInOneTo = createPage('pages/Profile', [ + createComponent('Button', { key: 'setting' }), + createComponent('Text', { text: '设置' }), + ]); + + const issues = detectSceneIssues( + 1, + createTransition(sharedFromMobile, mobileTo), + createTransition(sharedFromTwoInOne, twoInOneTo), + 'mobile_detail.png', + '2in1_profile.png', + 0.35 + ); + + expect(issues).toHaveLength(1); + expect(issues[0].eventType).toBe('TouchEvent'); + expect(issues[0].mobileToPagePath).toBe('pages/Detail'); + expect(issues[0].twoInOneToPagePath).toBe('pages/Profile'); + }); + + it('skips when event types do not match', () => { + const from = createPage('pages/Home', [createComponent('Text', { text: '首页' })]); + const to = createPage('pages/Detail', [createComponent('Text', { text: '详情' })]); + const mobileTransition = createTransition(from, to, new TouchEvent({ x: 10, y: 10 })); + const twoInOneTransition = createTransition(from, to, new ScrollEvent({ x: 10, y: 10 }, Direct.DOWN)); + + const issues = detectSceneIssues(2, mobileTransition, twoInOneTransition, 'a.png', 'b.png', 0.35); + + expect(issues).toHaveLength(0); + }); +}); \ No newline at end of file