继续完善规则引擎

This commit is contained in:
mtvpls
2026-05-19 22:36:56 +08:00
parent 11f603a10c
commit 8c86243d32
5 changed files with 487 additions and 552 deletions
+2
View File
@@ -36,6 +36,7 @@
"@upstash/redis": "^1.25.0",
"@vercel/postgres": "^0.10.0",
"@vidstack/react": "^1.12.13",
"@xmldom/xmldom": "^0.9.10",
"anime4k-webgpu": "^1.0.0",
"artplayer": "^5.3.0",
"artplayer-plugin-danmuku": "^5.2.0",
@@ -75,6 +76,7 @@
"tailwind-merge": "^2.6.0",
"vidstack": "^0.6.15",
"xml2js": "^0.6.2",
"xpath": "^0.0.34",
"zod": "^3.24.1"
},
"devDependencies": {
+21 -504
View File
@@ -41,6 +41,9 @@ importers:
'@vidstack/react':
specifier: ^1.12.13
version: 1.12.13(@types/[email protected])([email protected])
'@xmldom/xmldom':
specifier: ^0.9.10
version: 0.9.10
anime4k-webgpu:
specifier: ^1.0.0
version: 1.0.0(@webgpu/[email protected])
@@ -158,6 +161,9 @@ importers:
xml2js:
specifier: ^0.6.2
version: 0.6.2
xpath:
specifier: ^0.0.34
version: 0.0.34
zod:
specifier: ^3.24.1
version: 3.25.76
@@ -3141,9 +3147,6 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/[email protected]':
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
'@types/[email protected]':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -3549,6 +3552,10 @@ packages:
'@webgpu/[email protected]':
resolution: {integrity: sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==}
'@xmldom/[email protected]':
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
engines: {node: '>=14.6'}
'@xtuc/[email protected]':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -4437,10 +4444,6 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
[email protected]:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
[email protected]:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -4658,10 +4661,6 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
engines: {node: '>=6.0'}
@@ -5935,9 +5934,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
[email protected]:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
[email protected]:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -5949,33 +5945,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
[email protected]:
resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==}
[email protected]:
resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==}
[email protected]:
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
[email protected]:
resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==}
[email protected]:
resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==}
[email protected]:
resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==}
[email protected]:
resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==}
[email protected]:
resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==}
[email protected]:
resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==}
[email protected]:
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
@@ -5985,24 +5957,15 @@ packages:
[email protected]:
resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
[email protected]:
resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==}
[email protected]:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
[email protected]:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
[email protected]:
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
[email protected]:
resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
[email protected]:
resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==}
[email protected]:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
@@ -6044,150 +6007,66 @@ packages:
engines: {node: '>= 8.0.0'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==}
[email protected]:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
[email protected]:
resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==}
[email protected]:
resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==}
[email protected]:
resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==}
[email protected]:
resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==}
[email protected]:
resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==}
[email protected]:
resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==}
[email protected]:
resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==}
[email protected]:
resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==}
[email protected]:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
[email protected]:
resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==}
[email protected]:
resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
[email protected]:
resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==}
[email protected]:
resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
[email protected]:
resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==}
[email protected]:
resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
[email protected]:
resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==}
[email protected]:
resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
[email protected]:
resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==}
[email protected]:
resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
[email protected]:
resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==}
[email protected]:
resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
[email protected]:
resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==}
[email protected]:
resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
[email protected]:
resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==}
[email protected]:
resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
[email protected]:
resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==}
[email protected]:
resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
[email protected]:
resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==}
[email protected]:
resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
[email protected]:
resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==}
[email protected]:
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
[email protected]:
resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==}
[email protected]:
resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
[email protected]:
resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==}
[email protected]:
resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
[email protected]:
resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==}
[email protected]:
resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
[email protected]:
resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==}
[email protected]:
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
[email protected]:
resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==}
[email protected]:
resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
[email protected]:
resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==}
[email protected]:
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
[email protected]:
resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==}
[email protected]:
resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
[email protected]:
resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==}
[email protected]:
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
@@ -6837,6 +6716,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
[email protected]:
@@ -7064,7 +6944,6 @@ packages:
resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
@@ -7151,10 +7030,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
[email protected]:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
@@ -7873,9 +7748,6 @@ packages:
resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
engines: {node: '>=4'}
[email protected]:
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
[email protected]:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -7883,30 +7755,18 @@ packages:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==}
[email protected]:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
[email protected]:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
[email protected]:
resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==}
[email protected]:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
[email protected]:
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
[email protected]:
resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
[email protected]:
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
[email protected]:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
@@ -7963,11 +7823,6 @@ packages:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==}
engines: {node: '>=8'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@@ -7988,15 +7843,9 @@ packages:
engines: {node: '>= 18'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==}
[email protected]:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
[email protected]:
resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==}
[email protected]:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
@@ -8068,10 +7917,12 @@ packages:
[email protected]:
resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
[email protected]:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
[email protected]:
resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==}
@@ -8278,6 +8129,10 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
[email protected]:
resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==}
engines: {node: '>=0.6.0'}
[email protected]:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -12563,10 +12418,6 @@ snapshots:
'@types/[email protected]': {}
'@types/[email protected]':
dependencies:
'@types/unist': 2.0.11
'@types/[email protected]':
dependencies:
'@types/unist': 3.0.3
@@ -13187,6 +13038,8 @@ snapshots:
'@webgpu/[email protected]': {}
'@xmldom/[email protected]': {}
'@xtuc/[email protected]': {}
'@xtuc/[email protected]': {}
@@ -14075,8 +13928,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
path-type: 4.0.0
@@ -14430,8 +14281,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
esprima: 4.0.1
@@ -16128,8 +15977,6 @@ snapshots:
dependencies:
tmpl: 1.0.5
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -16143,30 +15990,6 @@ snapshots:
crypt: 0.0.2
is-buffer: 1.1.6
[email protected]:
dependencies:
'@types/mdast': 3.0.15
escape-string-regexp: 5.0.0
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
[email protected]:
dependencies:
'@types/mdast': 3.0.15
'@types/unist': 2.0.11
decode-named-character-reference: 1.2.0
mdast-util-to-string: 3.2.0
micromark: 3.2.0
micromark-util-decode-numeric-character-reference: 1.1.0
micromark-util-decode-string: 1.1.0
micromark-util-normalize-identifier: 1.1.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
unist-util-stringify-position: 3.0.3
uvu: 0.5.6
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
'@types/mdast': 4.0.4
@@ -16184,50 +16007,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
'@types/mdast': 3.0.15
ccount: 2.0.1
mdast-util-find-and-replace: 2.2.2
micromark-util-character: 1.2.0
[email protected]:
dependencies:
'@types/mdast': 3.0.15
mdast-util-to-markdown: 1.5.0
micromark-util-normalize-identifier: 1.1.0
[email protected]:
dependencies:
'@types/mdast': 3.0.15
mdast-util-to-markdown: 1.5.0
[email protected]:
dependencies:
'@types/mdast': 3.0.15
markdown-table: 3.0.4
mdast-util-from-markdown: 1.3.1
mdast-util-to-markdown: 1.5.0
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
'@types/mdast': 3.0.15
mdast-util-to-markdown: 1.5.0
[email protected]:
dependencies:
mdast-util-from-markdown: 1.3.1
mdast-util-gfm-autolink-literal: 1.0.3
mdast-util-gfm-footnote: 1.0.2
mdast-util-gfm-strikethrough: 1.0.3
mdast-util-gfm-table: 1.0.7
mdast-util-gfm-task-list-item: 1.0.2
mdast-util-to-markdown: 1.5.0
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -16267,11 +16046,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
'@types/mdast': 3.0.15
unist-util-is: 5.2.1
[email protected]:
dependencies:
'@types/mdast': 4.0.4
@@ -16289,17 +16063,6 @@ snapshots:
unist-util-visit: 5.0.0
vfile: 6.0.3
[email protected]:
dependencies:
'@types/mdast': 3.0.15
'@types/unist': 2.0.11
longest-streak: 3.1.0
mdast-util-phrasing: 3.0.1
mdast-util-to-string: 3.2.0
micromark-util-decode-string: 1.1.0
unist-util-visit: 4.1.2
zwitch: 2.0.4
[email protected]:
dependencies:
'@types/mdast': 4.0.4
@@ -16312,10 +16075,6 @@ snapshots:
unist-util-visit: 5.0.0
zwitch: 2.0.4
[email protected]:
dependencies:
'@types/mdast': 3.0.15
[email protected]:
dependencies:
'@types/mdast': 4.0.4
@@ -16344,25 +16103,6 @@ snapshots:
content-type: 1.0.4
raw-body: 2.4.1
[email protected]:
dependencies:
decode-named-character-reference: 1.2.0
micromark-factory-destination: 1.1.0
micromark-factory-label: 1.1.0
micromark-factory-space: 1.1.0
micromark-factory-title: 1.1.0
micromark-factory-whitespace: 1.1.0
micromark-util-character: 1.2.0
micromark-util-chunked: 1.1.0
micromark-util-classify-character: 1.1.0
micromark-util-html-tag-name: 1.2.0
micromark-util-normalize-identifier: 1.1.0
micromark-util-resolve-all: 1.1.0
micromark-util-subtokenize: 1.1.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
decode-named-character-reference: 1.2.0
@@ -16382,83 +16122,12 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-character: 1.2.0
micromark-util-sanitize-uri: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-core-commonmark: 1.1.0
micromark-factory-space: 1.1.0
micromark-util-character: 1.2.0
micromark-util-normalize-identifier: 1.1.0
micromark-util-sanitize-uri: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
micromark-util-chunked: 1.1.0
micromark-util-classify-character: 1.1.0
micromark-util-resolve-all: 1.1.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
micromark-factory-space: 1.1.0
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-factory-space: 1.1.0
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
micromark-extension-gfm-autolink-literal: 1.0.5
micromark-extension-gfm-footnote: 1.1.2
micromark-extension-gfm-strikethrough: 1.0.7
micromark-extension-gfm-table: 1.0.7
micromark-extension-gfm-tagfilter: 1.0.2
micromark-extension-gfm-task-list-item: 1.0.5
micromark-util-combine-extensions: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
devlop: 1.1.0
@@ -16466,23 +16135,11 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-character: 1.2.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-character: 2.1.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-factory-space: 1.1.0
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-factory-space: 2.0.1
@@ -16490,13 +16147,6 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-factory-space: 1.1.0
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-factory-space: 2.0.1
@@ -16504,61 +16154,30 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-symbol: 1.1.0
[email protected]:
dependencies:
micromark-util-symbol: 2.0.1
[email protected]:
dependencies:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-chunked: 1.1.0
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-chunked: 2.0.1
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-symbol: 1.1.0
[email protected]:
dependencies:
micromark-util-symbol: 2.0.1
[email protected]:
dependencies:
decode-named-character-reference: 1.2.0
micromark-util-character: 1.2.0
micromark-util-decode-numeric-character-reference: 1.1.0
micromark-util-symbol: 1.1.0
[email protected]:
dependencies:
decode-named-character-reference: 1.2.0
@@ -16566,49 +16185,24 @@ snapshots:
micromark-util-decode-numeric-character-reference: 2.0.2
micromark-util-symbol: 2.0.1
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
micromark-util-symbol: 1.1.0
[email protected]:
dependencies:
micromark-util-symbol: 2.0.1
[email protected]:
dependencies:
micromark-util-types: 1.1.0
[email protected]:
dependencies:
micromark-util-types: 2.0.2
[email protected]:
dependencies:
micromark-util-character: 1.2.0
micromark-util-encode: 1.1.0
micromark-util-symbol: 1.1.0
[email protected]:
dependencies:
micromark-util-character: 2.1.1
micromark-util-encode: 2.0.1
micromark-util-symbol: 2.0.1
[email protected]:
dependencies:
micromark-util-chunked: 1.1.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
[email protected]:
dependencies:
devlop: 1.1.0
@@ -16616,36 +16210,10 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.3
decode-named-character-reference: 1.2.0
micromark-core-commonmark: 1.1.0
micromark-factory-space: 1.1.0
micromark-util-character: 1.2.0
micromark-util-chunked: 1.1.0
micromark-util-combine-extensions: 1.1.0
micromark-util-decode-numeric-character-reference: 1.1.0
micromark-util-encode: 1.1.0
micromark-util-normalize-identifier: 1.1.0
micromark-util-resolve-all: 1.1.0
micromark-util-sanitize-uri: 1.2.0
micromark-util-subtokenize: 1.1.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
'@types/debug': 4.1.12
@@ -17471,7 +17039,6 @@ snapshots:
dependencies:
jsesc: 3.1.0
[email protected]:
dependencies:
'@types/mdast': 4.0.4
@@ -17576,10 +17143,6 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
[email protected]:
dependencies:
mri: 1.2.0
[email protected]:
dependencies:
call-bind: 1.0.8
@@ -18427,16 +17990,6 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
'@types/unist': 2.0.11
bail: 2.0.2
extend: 3.0.2
is-buffer: 2.0.5
is-plain-obj: 4.1.0
trough: 2.2.0
vfile: 5.3.7
[email protected]:
dependencies:
'@types/unist': 3.0.3
@@ -18451,10 +18004,6 @@ snapshots:
dependencies:
crypto-random-string: 2.0.0
[email protected]:
dependencies:
'@types/unist': 2.0.11
[email protected]:
dependencies:
'@types/unist': 3.0.3
@@ -18463,30 +18012,15 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
[email protected]:
dependencies:
'@types/unist': 2.0.11
[email protected]:
dependencies:
'@types/unist': 3.0.3
[email protected]:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 5.2.1
[email protected]:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.1
[email protected]:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
[email protected]:
dependencies:
'@types/unist': 3.0.3
@@ -18560,13 +18094,6 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
dequal: 2.0.3
diff: 5.2.0
kleur: 4.1.5
sade: 1.8.1
[email protected]: {}
[email protected]:
@@ -18615,23 +18142,11 @@ snapshots:
- supports-color
- typescript
[email protected]:
dependencies:
'@types/unist': 2.0.11
unist-util-stringify-position: 3.0.3
[email protected]:
dependencies:
'@types/unist': 3.0.3
unist-util-stringify-position: 4.0.0
[email protected]:
dependencies:
'@types/unist': 2.0.11
is-buffer: 2.0.5
unist-util-stringify-position: 3.0.3
vfile-message: 3.1.4
[email protected]:
dependencies:
'@types/unist': 3.0.3
@@ -19013,6 +18528,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
+39 -3
View File
@@ -1,12 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';
import { legadoClient } from '@/lib/legado.client';
import crypto from 'crypto';
import { validateProxyUrlServerSide } from '@/lib/server/ssrf';
import { getAuthorizedBooksUsername } from '../_utils';
export const runtime = 'nodejs';
const imageCache = new Map<string, { expiresAt: number; contentType: string; data: Uint8Array }>();
function asObjectHeader(value?: string | Record<string, string>): Record<string, string> {
if (!value) return {};
if (typeof value === 'object') return value;
@@ -33,19 +36,52 @@ export async function GET(request: NextRequest) {
if (!sourceId || !url) return NextResponse.json({ error: '缺少 sourceId 或 url' }, { status: 400 });
if (!(await validateProxyUrlServerSide(url))) return NextResponse.json({ error: '图片地址未通过安全校验' }, { status: 400 });
const source = await legadoClient.getSourceById(sourceId);
let imageOptions: any = {};
try {
const rawOptions = searchParams.get('options') || '';
imageOptions = rawOptions ? JSON.parse(rawOptions) : {};
} catch {
imageOptions = {};
}
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36',
Referer: source.legado?.bookSourceUrl || source.url,
...asObjectHeader(source.legado?.header),
...(imageOptions.headers && typeof imageOptions.headers === 'object' ? imageOptions.headers : {}),
};
delete headers.Host;
delete headers.host;
const res = await fetch(url, { headers, cache: 'no-store' });
const cacheKey = crypto.createHash('sha1').update(`${sourceId}|${url}|${JSON.stringify(imageOptions)}`).digest('hex');
const cached = imageCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now() && !request.headers.get('range')) {
return new NextResponse(cached.data, { headers: { 'Content-Type': cached.contentType, 'Cache-Control': 'public, max-age=86400' } });
}
const range = request.headers.get('range');
if (range) headers.Range = range;
const res = await fetch(url, { method: imageOptions.method || 'GET', headers, body: imageOptions.body, cache: 'no-store', redirect: 'manual' });
if ([301, 302, 303, 307, 308].includes(res.status)) {
const location = res.headers.get('location');
if (!location) return NextResponse.json({ error: '图片重定向缺少地址' }, { status: 502 });
const redirected = new URL(location, url).toString();
if (!(await validateProxyUrlServerSide(redirected))) return NextResponse.json({ error: '图片重定向地址未通过安全校验' }, { status: 400 });
const second = await fetch(redirected, { method: imageOptions.method || 'GET', headers, body: imageOptions.body, cache: 'no-store' });
if (!second.ok) return NextResponse.json({ error: `图片请求失败: ${second.status}` }, { status: second.status });
const data = new Uint8Array(await second.arrayBuffer());
const contentType = second.headers.get('content-type') || 'image/jpeg';
if (!range) imageCache.set(cacheKey, { data, contentType, expiresAt: Date.now() + 86400_000 });
return new NextResponse(data, { status: second.status, headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' } });
}
if (!res.ok) return NextResponse.json({ error: `图片请求失败: ${res.status}` }, { status: res.status });
return new NextResponse(res.body, {
const data = new Uint8Array(await res.arrayBuffer());
const contentType = res.headers.get('content-type') || 'image/jpeg';
if (!range) imageCache.set(cacheKey, { data, contentType, expiresAt: Date.now() + 86400_000 });
return new NextResponse(data, {
status: res.status,
headers: {
'Content-Type': res.headers.get('content-type') || 'image/jpeg',
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
...(res.headers.get('content-range') ? { 'Content-Range': res.headers.get('content-range') as string } : {}),
...(res.headers.get('accept-ranges') ? { 'Accept-Ranges': res.headers.get('accept-ranges') as string } : {}),
},
});
} catch (error) {
+22
View File
@@ -17,8 +17,11 @@ export interface LegadoRuleSearch {
bookUrl?: string;
kind?: string;
lastChapter?: string;
checkKeyWord?: string;
wordCount?: string;
}
export type LegadoRuleExplore = LegadoRuleSearch;
export interface LegadoRuleBookInfo {
@@ -29,6 +32,7 @@ export interface LegadoRuleBookInfo {
tocUrl?: string;
kind?: string;
lastChapter?: string;
wordCount?: string;
}
export interface LegadoRuleToc {
@@ -37,11 +41,18 @@ export interface LegadoRuleToc {
chapterUrl?: string;
isVip?: string;
isPay?: string;
nextTocUrl?: string;
updateTime?: string;
wordCount?: string;
chapterInfo?: string;
}
export interface LegadoRuleContent {
content?: string;
nextContentUrl?: string;
imageStyle?: string;
sourceRegex?: string;
payAction?: string;
}
export interface LegadoBookSourceRule {
@@ -49,13 +60,23 @@ export interface LegadoBookSourceRule {
bookSourceUrl?: string;
bookSourceGroup?: string;
bookSourceType?: number;
customButton?: boolean | unknown;
eventListener?: boolean | unknown;
variable?: string | Record<string, unknown>;
loginUi?: string | unknown;
concurrentRate?: string | number;
respondTime?: number;
lastUpdateTime?: string | number;
enabled?: boolean;
enabledCookieJar?: boolean;
enabledExplore?: boolean;
exploreUrl?: string;
header?: string | Record<string, string>;
loginUrl?: string;
searchUrl?: string;
bookInfoUrl?: string;
bookInfoInit?: string;
bookUrlPattern?: string;
tocUrl?: string;
chapterUrl?: string;
ruleSearch?: LegadoRuleSearch;
@@ -63,6 +84,7 @@ export interface LegadoBookSourceRule {
ruleBookInfo?: LegadoRuleBookInfo;
ruleToc?: LegadoRuleToc;
ruleContent?: LegadoRuleContent;
ruleReview?: unknown;
customOrder?: number;
weight?: number;
}
+403 -45
View File
@@ -2,6 +2,9 @@
import * as cheerio from 'cheerio/slim';
import crypto from 'crypto';
import he from 'he';
import vm from 'vm';
import { DOMParser } from '@xmldom/xmldom';
import xpath from 'xpath';
import { getConfig } from './config';
import {
@@ -36,6 +39,19 @@ const detailCache = new Map<string, { expiresAt: number; data: BookDetail }>();
const tocCache = new Map<string, { expiresAt: number; data: BookChapter[] }>();
const chapterCache = new Map<string, { expiresAt: number; data: BookChapterContent }>();
interface RequestOptions {
url: string;
method?: string;
body?: string;
headers?: Record<string, string>;
charset?: string;
retry?: number;
}
const cookieJar = new Map<string, string>();
const variableStore = new Map<string, any>();
const imageMemoryCache = new Map<string, { expiresAt: number; contentType: string; data: Uint8Array }>();
function stableId(input: string) {
return crypto.createHash('sha1').update(input).digest('hex').slice(0, 16);
}
@@ -87,18 +103,84 @@ function encodeRuleParam(value: string) {
return encodeURIComponent(value).replace(/%20/g, '+');
}
function safeEvalTemplateExpression(expr: string, keyword: string, page: number) {
const key = keyword;
const searchTerms = keyword;
const java = {
base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'),
base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'),
encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')),
encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')),
};
try {
// eslint-disable-next-line no-new-func
const fn = new Function('key', 'keyword', 'searchTerms', 'page', 'java', `return (${expr});`);
return jsonPrimitiveToString(fn(key, keyword, searchTerms, page, java));
} catch {
return '';
}
}
function renderTemplateExpressions(raw: string, keyword = '', page = 1) {
return raw.replace(/<([^,<>]*),\s*\{\{(.*?)\}\}>/g, (_, prefix, expr) => {
const value = safeEvalTemplateExpression(String(expr).trim(), keyword, page);
return value ? `${prefix || ''}${value}` : '';
}).replace(/\{\{(.*?)\}\}/g, (_, expr) => {
const value = String(expr).trim();
if (/^(key|keyword|searchTerms)$/.test(value)) return encodeRuleParam(keyword || '');
if (/^(page|pageIndex)$/.test(value)) return String(page);
const evaluated = safeEvalTemplateExpression(value, keyword, page);
return encodeRuleParam(evaluated);
});
}
function runJsSnippet(code: string, context: Record<string, any>, timeout = 1000): string {
const java = {
base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'),
base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'),
md5Encode: (value: unknown) => crypto.createHash('md5').update(String(value ?? '')).digest('hex'),
encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')),
encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')),
put: (key: string, value: any) => { variableStore.set(key, value); return value; },
get: (key: string) => variableStore.get(key),
htmlFormat: (value: unknown) => he.decode(String(value ?? '')).replace(/<br\s*\/?/gi, '\n').replace(/<[^>]+>/g, ''),
};
const sandbox: Record<string, any> = { ...context, java, Buffer, JSON, String, Number, Math, Array, Object, console: { log: () => undefined } };
try {
const script = new vm.Script(`(function(){ ${code.replace(/^@js:/, '')}\n})()`);
const result = script.runInNewContext(sandbox, { timeout });
return jsonPrimitiveToString(result ?? sandbox.result ?? '');
} catch {
return '';
}
}
function applyPutGetRules(rule: string, value: string) {
const putMatch = rule.match(/@put:\s*\{([\s\S]*?)\}/);
if (putMatch) {
try {
const obj = JSON.parse(`{${putMatch[1]}}`);
Object.entries(obj).forEach(([key, val]) => variableStore.set(key, val));
} catch {
const pair = putMatch[1].match(/([A-Za-z0-9_$-]+)\s*:\s*['"]?([^,'"]+)['"]?/);
if (pair) variableStore.set(pair[1], value || pair[2]);
}
}
const getMatch = rule.match(/@get:\s*\{\s*([A-Za-z0-9_$-]+)\s*\}/);
if (getMatch) return jsonPrimitiveToString(variableStore.get(getMatch[1]));
return value;
}
function buildUrlFromTemplate(template: string, source: BookSource, keyword?: string, page = 1, baseOverride?: string) {
const base = baseOverride || sourceBase(source);
let raw = template || base;
raw = raw.replace(/\{\{(?:key|keyword|searchTerms)\}\}/g, encodeRuleParam(keyword || ''));
raw = raw.replace(/\{\{(?:page|pageIndex)\}\}/g, String(page));
raw = renderTemplateExpressions(raw, keyword || '', page);
raw = raw
.replace(/\{searchTerms\}/g, encodeRuleParam(keyword || ''))
.replace(/\{key\}/g, encodeRuleParam(keyword || ''))
.replace(/\{keyword\}/g, encodeRuleParam(keyword || ''))
.replace(/\{page\}/g, String(page))
.replace(/\{pageIndex\}/g, String(page));
if (raw.includes('{{') && keyword) raw = raw.replace(/\{\{.*?\}\}/g, encodeRuleParam(keyword));
return normalizeUrl(base, raw);
}
@@ -129,33 +211,54 @@ function readJsonPath(input: any, path?: string): any {
if (normalized.startsWith('-@json:')) normalized = normalized.slice(7);
if (!normalized || normalized === '$') return input;
const recursive = normalized.match(/^\$\.\.([A-Za-z0-9_$-]+)\[\*\]$/);
const filterMatch = normalized.match(/^(.*)\[\?\(@\.([A-Za-z0-9_$-]+)\s*(==|=|!=)\s*['"]?([^'"\]]+)['"]?\)\](.*)$/);
if (filterMatch) {
const base = readJsonPath(input, filterMatch[1] || '$');
const list = Array.isArray(base) ? base : [];
const filtered = list.filter((item) => {
const actual = jsonPrimitiveToString(item?.[filterMatch[2]]);
return filterMatch[3] === '!=' ? actual !== filterMatch[4] : actual === filterMatch[4];
});
return filterMatch[5] ? readJsonPath(filtered, `$${filterMatch[5]}`) : filtered;
}
const recursive = normalized.match(/^\$\.\.([A-Za-z0-9_$-]+)(.*)$/);
if (recursive) {
const key = recursive[1];
const rest = recursive[2] || '';
const out: any[] = [];
const walk = (node: any) => {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node)) {
node.forEach(walk);
return;
}
if (Array.isArray(node[key])) out.push(...node[key]);
if (Array.isArray(node)) return node.forEach(walk);
if (node[key] !== undefined) out.push(node[key]);
Object.values(node).forEach(walk);
};
walk(input);
return out;
return rest ? readJsonPath(out.flat(), `$${rest}`) : out.flat();
}
normalized = normalized.replace(/^\$\.?/, '');
const tokens = normalized.match(/[^.[\]]+|\[\*\]|\[\d+\]/g) || [];
const tokens = normalized.match(/[^.[\]]+|\[\*\]|\[-?\d+\]|\[-?\d*:-?\d*(?::-?\d+)?\]|\[['"][^\]]+['"](?:,\s*['"][^\]]+['"])*\]/g) || [];
let current = input;
for (const token of tokens) {
if (current === undefined || current === null) return undefined;
if (token === '[*]') {
current = Array.isArray(current) ? current.flat() : [];
} else if (/^\[-?\d+\]$/.test(token)) {
const idx = Number(token.slice(1, -1));
current = Array.isArray(current) ? current[idx < 0 ? current.length + idx : idx] : undefined;
} else if (/^\[-?\d*:-?\d*/.test(token)) {
if (!Array.isArray(current)) return [];
current = current.flat();
} else if (/^\[\d+\]$/.test(token)) {
current = Array.isArray(current) ? current[Number(token.slice(1, -1))] : undefined;
const parts = token.slice(1, -1).split(':').map((item) => item === '' ? undefined : Number(item));
const start = parts[0] === undefined ? 0 : parts[0] < 0 ? current.length + parts[0] : parts[0];
const end = parts[1] === undefined ? current.length : parts[1] < 0 ? current.length + parts[1] : parts[1];
const step = parts[2] || 1;
const sliced = current.slice(start, end);
current = step === 1 ? sliced : sliced.filter((_, index) => index % Math.abs(step) === 0);
} else if (/^\[/.test(token)) {
const keys = Array.from(token.matchAll(/['"]([^'"]+)['"]/g)).map((m) => m[1]);
if (Array.isArray(current)) current = current.map((item) => keys.map((key) => item?.[key])).flat().filter((item) => item !== undefined);
else current = keys.map((key) => current?.[key]).filter((item) => item !== undefined);
} else if (Array.isArray(current)) {
current = current.map((item) => item?.[token]).filter((item) => item !== undefined);
} else {
@@ -201,7 +304,15 @@ function readJsonRule(json: any, rule?: string, source?: BookSource, baseUrl?: s
.filter(Boolean)
.join('');
}
return '';
const resultText = typeof json === 'string' ? json : JSON.stringify(json);
if (/java\.base64Decode/.test(trimmed)) return Buffer.from(resultText, 'base64').toString('utf8');
if (/java\.base64Encode/.test(trimmed)) return Buffer.from(resultText, 'utf8').toString('base64');
if (/java\.md5Encode/.test(trimmed)) return crypto.createHash('md5').update(resultText).digest('hex');
const replaceMatch = trimmed.match(/result\.replace\(\s*\/([^/]+)\/[gimuy]*\s*,\s*['"]([^'"]*)['"]\s*\)/);
if (replaceMatch) return resultText.replace(new RegExp(replaceMatch[1], 'g'), replaceMatch[2]);
const matchMatch = trimmed.match(/result\.match\(\s*\/([^/]+)\/[gimuy]*\s*\)/);
if (matchMatch) return resultText.match(new RegExp(matchMatch[1]))?.[1] || resultText.match(new RegExp(matchMatch[1]))?.[0] || '';
return runJsSnippet(trimmed, { result: json, baseUrl, src: json });
}
const value = readJsonPath(json, trimmed);
const text = jsonPrimitiveToString(value);
@@ -233,7 +344,7 @@ function applyRuleFilters(value: string, filters: string[]) {
const replacement = filters[index + 1] ?? '';
if (!pattern) continue;
try {
result = result.replace(new RegExp(pattern, 'g'), replacement);
result = result.replace(new RegExp(pattern, 'g'), replacement.replace(/\$(\d+)/g, '$$$$1'));
} catch {
result = result.split(pattern).join(replacement);
}
@@ -241,8 +352,37 @@ function applyRuleFilters(value: string, filters: string[]) {
return result;
}
function readAllInOneList(raw: string, rule?: string): Array<Record<string, string>> {
const trimmed = (rule || '').trim();
if (!trimmed.startsWith(':')) return [];
const pattern = trimmed.slice(1);
try {
const regex = new RegExp(pattern, 'gs');
const out: Array<Record<string, string>> = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(raw)) !== null) {
const groups = (match.groups || {}) as Record<string, string>;
const item: Record<string, string> = { _0: match[0] };
Array.from(match).forEach((value: string | undefined, index: number) => { item[`_${index}`] = value || ''; });
Object.entries(groups).forEach(([key, value]) => { item[key] = String(value || ''); });
out.push(item);
if (match[0] === '') regex.lastIndex += 1;
}
return out;
} catch {
return [];
}
}
function readRegexItem(item: Record<string, string>, rule?: string, baseUrl?: string) {
const key = (rule || '').trim().replace(/^\$?\{?/, '').replace(/\}?$/, '');
const value = item[key] || item[`_${key}`] || '';
if ((/url|href|src|cover/i.test(key) || /^https?:\/\//i.test(value)) && value && baseUrl) return normalizeUrl(baseUrl, value);
return value;
}
function isLegadoAttrToken(value: string) {
return /^(href|src|title|alt|text|textNodes|html|content|value|data-[\w-]+)$/i.test(value.trim());
return /^(href|src|title|alt|text|textNodes|ownText|all|html|content|value|data-[\w-]+)$/i.test(value.trim());
}
function normalizeLegadoSelector(selector: string) {
@@ -272,7 +412,7 @@ function parseStep(step: string): { selector: string; attr: string } {
};
}
const attrMatch = trimmed.match(/(?:@|::)(text|textNodes|html|href|src|title|alt|content|value|data-[\w-]+)$/i);
const attrMatch = trimmed.match(/(?:@|::)(text|textNodes|ownText|all|html|href|src|title|alt|content|value|data-[\w-]+)$/i);
if (attrMatch) {
return { selector: normalizeLegadoSelector(trimmed.slice(0, attrMatch.index).trim()), attr: attrMatch[1] };
}
@@ -285,28 +425,125 @@ function stripFilters(rule: string) {
return splitRuleFilters(rule).base;
}
function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: string): { current: cheerio.Cheerio<any>; selector: string } {
let normalized = selector.trim();
const exclude = normalized.match(/\[!(-?\d+)\]$/);
if (exclude) {
normalized = normalized.slice(0, exclude.index).trim();
current = normalized ? current.find(normalized) : current;
const idx = Number(exclude[1]);
const real = idx < 0 ? current.length + idx : idx;
return { current: current.filter((index) => index !== real), selector: '' };
}
const range = normalized.match(/\[(-?\d*):(-?\d*)(?::(-?\d+))?\]$/);
if (range) {
normalized = normalized.slice(0, range.index).trim();
current = normalized ? current.find(normalized) : current;
const length = current.length;
const start = range[1] ? Number(range[1]) : 0;
const end = range[2] ? Number(range[2]) : length;
const realStart = start < 0 ? length + start : start;
const realEnd = end < 0 ? length + end : end;
return { current: current.slice(realStart, realEnd), selector: '' };
}
const indexMatch = normalized.match(/(?:\[(-?\d+)\]|\.(-?\d+))$/);
if (indexMatch) {
normalized = normalized.slice(0, indexMatch.index).trim();
current = normalized ? current.find(normalized) : current;
const idx = Number(indexMatch[1] ?? indexMatch[2]);
const real = idx < 0 ? current.length + idx : idx;
return { current: current.eq(real), selector: '' };
}
return { current, selector: normalized };
}
function selectElements($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: string): cheerio.Cheerio<any> {
const normalized = stripFilters(rule || '');
if (!normalized) return root;
const steps = normalized.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean);
const reverse = normalized.trim().startsWith('-') && !normalized.trim().startsWith('-@');
const effective = reverse ? normalized.trim().slice(1).trim() : normalized;
if (!effective) return root;
const steps = effective.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean);
let current = root;
for (const rawStep of steps) {
const { selector, attr } = parseStep(rawStep);
if (!selector || attr) break;
current = current.find(selector);
if (attr) break;
if (!selector) continue;
if (/^children$/i.test(selector)) {
current = current.children();
continue;
}
const indexed = applyLegadoIndexSelector(current, selector);
current = indexed.selector ? current.find(indexed.selector) : indexed.current;
}
if (reverse) current = $(current.toArray().reverse());
return current;
}
function selectXPath($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule: string): { nodes: cheerio.Cheerio<any>; attr: string; value?: string } {
const expr = rule.trim().replace(/^@XPath:/i, '');
try {
const html = $.html(root);
const doc = new DOMParser({ errorHandler: () => undefined }).parseFromString(html, 'text/html');
const selected = xpath.select(expr, doc as any) as any;
const list = Array.isArray(selected) ? selected : [selected];
const values = list.map((node) => {
if (node === undefined || node === null) return '';
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') return String(node);
if (node.nodeType === 2) return node.nodeValue || '';
if (node.nodeType === 3 || node.nodeType === 4) return node.nodeValue || '';
return node.textContent || '';
}).filter(Boolean);
if (values.length > 0) return { nodes: root, attr: '', value: values.join('\n') };
} catch {
// fallback below
}
let fallbackExpr = expr;
let attr = '';
const attrMatch = fallbackExpr.match(/\/@([A-Za-z0-9_-]+)$/);
if (attrMatch) {
attr = attrMatch[1];
fallbackExpr = fallbackExpr.slice(0, attrMatch.index);
} else if (/\/text\(\)$/.test(fallbackExpr)) {
attr = 'text';
fallbackExpr = fallbackExpr.replace(/\/text\(\)$/, '');
}
const parts = fallbackExpr.split('/').filter(Boolean).map((part) => {
const match = part.match(/^([A-Za-z0-9_*.-]+)(?:\[@([A-Za-z0-9_-]+)=['"]([^'"]+)['"]\])?(?:\[(\d+)\])?$/);
if (!match) return '';
const tag = match[1] === '*' ? '*' : match[1];
const filter = match[2] ? `[${match[2]}="${match[3]}"]` : '';
const index = match[4] ? `:nth-of-type(${match[4]})` : '';
return `${tag}${filter}${index}`;
}).filter(Boolean);
return { nodes: parts.length ? root.find(parts.join(' ')) : root, attr };
}
function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: string, baseUrl?: string): string {
for (const alternative of splitAlternatives(rule)) {
const normalized = stripFilters(alternative);
if (/^@?XPath:/i.test(normalized) || normalized.startsWith('//')) {
const { nodes, attr, value: xpathValue } = selectXPath($, root, normalized);
const node = nodes.first();
let value = xpathValue !== undefined ? xpathValue : attr === 'text' || !attr ? node.text() : node.attr(attr) || '';
value = he.decode(value || '').replace(/\u00a0/g, ' ').trim();
value = applyRuleFilters(value, splitRuleFilters(alternative).filters);
if (value) return value;
continue;
}
const steps = normalized.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean);
let current = root;
let attr = '';
for (const rawStep of steps) {
const parsed = parseStep(rawStep);
if (parsed.selector) current = current.find(parsed.selector);
if (parsed.selector) {
if (/^children$/i.test(parsed.selector)) current = current.children();
else {
const indexed = applyLegadoIndexSelector(current, parsed.selector);
current = indexed.selector ? current.find(indexed.selector) : indexed.current;
}
}
if (parsed.attr) attr = parsed.attr;
}
if (current.length === 0 && steps.length === 1) {
@@ -317,11 +554,14 @@ function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: str
let value = '';
const normalizedAttr = attr.toLowerCase();
if (!attr || normalizedAttr === 'text' || normalizedAttr === 'textnodes') value = node.text();
else if (normalizedAttr === 'owntext') value = node.clone().children().remove().end().text();
else if (normalizedAttr === 'all') value = current.toArray().map((el) => $(el).text()).join('\n');
else if (normalizedAttr === 'html') value = node.html() || '';
else value = node.attr(attr) || '';
value = he.decode(value || '').replace(/\u00a0/g, ' ').trim();
if ((normalizedAttr === 'href' || normalizedAttr === 'src') && value && baseUrl) value = normalizeUrl(baseUrl, value);
value = applyRuleFilters(value, splitRuleFilters(alternative).filters);
value = applyPutGetRules(alternative, value);
if (value) return value;
}
return '';
@@ -335,7 +575,13 @@ function readValues($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: st
let attr = '';
for (const rawStep of steps) {
const parsed = parseStep(rawStep);
if (parsed.selector) current = current.find(parsed.selector);
if (parsed.selector) {
if (/^children$/i.test(parsed.selector)) current = current.children();
else {
const indexed = applyLegadoIndexSelector(current, parsed.selector);
current = indexed.selector ? current.find(indexed.selector) : indexed.current;
}
}
if (parsed.attr) attr = parsed.attr;
}
if (current.length === 0 && steps.length === 1) {
@@ -347,6 +593,8 @@ function readValues($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: st
const node = $(element);
let value = '';
if (!attr || normalizedAttr === 'text' || normalizedAttr === 'textnodes') value = node.text();
else if (normalizedAttr === 'owntext') value = node.clone().children().remove().end().text();
else if (normalizedAttr === 'all') value = node.text();
else if (normalizedAttr === 'html') value = node.html() || '';
else value = node.attr(attr) || '';
value = he.decode(value || '').replace(/\u00a0/g, ' ').trim();
@@ -367,7 +615,7 @@ function applyContentJsRule(value: string, jsRule: string): string {
.map((src) => `<img src="${src}" style="max-width:100%; display:block;" referrerpolicy="no-referrer">`)
.join('');
}
return value;
return runJsSnippet(jsRule, { result: value, src: value }) || value;
}
function contentFromRule(raw: string, rule?: string, baseUrl?: string): string {
@@ -406,9 +654,12 @@ function proxyChapterImages(content: string, source: BookSource) {
if (!/<img\b/i.test(content)) return content;
const rule = source.legado;
if (rule?.bookSourceType !== 2 && source.legado?.bookSourceType !== 2) return content;
return content.replace(/<img\b([^>]*?)\bsrc=(['"])(.*?)\2([^>]*)>/gi, (match, before, quote, src, after) => {
if (!src || src.startsWith('/api/books/image')) return match;
const proxied = `/api/books/image?sourceId=${encodeURIComponent(source.id)}&url=${encodeURIComponent(src)}`;
return content.replace(/<img\b([^>]*?)\bsrc=(['"])(.*?)\2([^>]*)>/gi, (match, before, quote, rawSrc, after) => {
if (!rawSrc || rawSrc.startsWith('/api/books/image')) return match;
const optionIndex = rawSrc.indexOf(',{');
const src = optionIndex > 0 ? rawSrc.slice(0, optionIndex) : rawSrc;
const options = optionIndex > 0 ? rawSrc.slice(optionIndex + 1) : '';
const proxied = `/api/books/image?sourceId=${encodeURIComponent(source.id)}&url=${encodeURIComponent(src)}${options ? `&options=${encodeURIComponent(options)}` : ''}`;
return `<img${before}src=${quote}${proxied}${quote}${after}>`;
});
}
@@ -488,27 +739,83 @@ function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function splitUrlOptions(input: string): RequestOptions {
const raw = input.trim();
const comma = raw.indexOf(',');
if (comma <= 0) return { url: raw };
const candidate = raw.slice(comma + 1).trim();
if (!candidate.startsWith('{')) return { url: raw };
try {
const options = JSON.parse(candidate);
return {
url: raw.slice(0, comma).trim(),
method: options.method,
body: options.body,
headers: options.headers && typeof options.headers === 'object' ? options.headers : undefined,
charset: options.charset,
retry: Number.isFinite(Number(options.retry)) ? Number(options.retry) : undefined,
};
} catch {
return { url: raw };
}
}
function getCookieHeader(sourceId: string) {
return cookieJar.get(sourceId) || '';
}
function mergeSetCookie(sourceId: string, setCookie: string | null) {
if (!setCookie) return;
const current = new Map<string, string>();
(cookieJar.get(sourceId) || '').split(/;\s*/).filter(Boolean).forEach((item) => {
const idx = item.indexOf('=');
if (idx > 0) current.set(item.slice(0, idx), item.slice(idx + 1));
});
setCookie.split(/,(?=\s*[^;,]+=)/).forEach((cookie) => {
const pair = cookie.split(';')[0]?.trim();
const idx = pair?.indexOf('=') ?? -1;
if (idx > 0) current.set(pair.slice(0, idx), pair.slice(idx + 1));
});
cookieJar.set(sourceId, Array.from(current.entries()).map(([key, value]) => `${key}=${value}`).join('; '));
}
async function fetchText(source: BookSource, url: string): Promise<string> {
if (!url?.trim()) throw new Error('书源请求地址为空');
const safe = await validateProxyUrlServerSide(url);
if (!safe) throw new Error(`书源地址未通过安全校验: ${url}`);
const cacheKey = `${LEGADO_CACHE_VERSION}|text|${source.id}|${url}`;
const request = splitUrlOptions(url);
const safe = await validateProxyUrlServerSide(request.url);
if (!safe) throw new Error(`书源地址未通过安全校验: ${request.url}`);
const cacheKey = `${LEGADO_CACHE_VERSION}|text|${source.id}|${request.method || 'GET'}|${request.url}|${request.body || ''}`;
const cached = textCache.get(cacheKey);
const { cacheTTL } = await resolveLegadoConfig();
if (cached && cached.expiresAt > Date.now()) return cached.data;
let lastError: unknown;
for (let attempt = 0; attempt < 3; attempt += 1) {
const maxAttempts = Math.max(1, (request.retry ?? 2) + 1);
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
try {
const response = await fetch(url, { headers: buildHeaders(source), signal: controller.signal, cache: 'no-store' });
const headers: Record<string, string> = {
...(buildHeaders(source) as Record<string, string>),
...(request.headers || {}),
};
const cookie = getCookieHeader(source.id);
if (source.legado?.enabledCookieJar && cookie) headers.Cookie = cookie;
const method = (request.method || (request.body ? 'POST' : 'GET')).toUpperCase();
const response = await fetch(request.url, {
method,
headers,
body: method === 'GET' || method === 'HEAD' ? undefined : request.body,
signal: controller.signal,
cache: 'no-store',
});
if (source.legado?.enabledCookieJar) mergeSetCookie(source.id, response.headers.get('set-cookie'));
if (!response.ok) throw new Error(`请求失败: ${response.status}`);
const contentLength = Number(response.headers.get('content-length') || '0');
if (contentLength > MAX_TEXT_BYTES) throw new Error('响应内容过大');
const buffer = await response.arrayBuffer();
const contentType = response.headers.get('content-type') || '';
const charset = contentType.match(/charset=([^;]+)/i)?.[1]?.trim().replace(/^['"]|['"]$/g, '');
const charset = request.charset || contentType.match(/charset=([^;]+)/i)?.[1]?.trim().replace(/^['"]|['"]$/g, '');
const decoderName = /gbk|gb2312|gb18030/i.test(charset || '') ? 'gb18030' : (charset || 'utf-8');
let text: string;
try {
@@ -521,7 +828,7 @@ async function fetchText(source: BookSource, url: string): Promise<string> {
return text;
} catch (error) {
lastError = error;
if (attempt < 2) await wait(300 * (attempt + 1));
if (attempt < maxAttempts - 1) await wait(300 * (attempt + 1));
} finally {
clearTimeout(timeout);
}
@@ -573,8 +880,8 @@ function parseExploreUrl(exploreUrl?: string): Array<{ title: string; template:
const json = parseJsonMaybe(raw);
if (Array.isArray(json)) {
return json
.map((item) => ({ title: jsonPrimitiveToString(item?.title), template: jsonPrimitiveToString(item?.url) }))
.filter((item) => !!item.title && !!item.template);
.map((item) => ({ title: jsonPrimitiveToString(item?.title), template: jsonPrimitiveToString(item?.url) || `__group__:${jsonPrimitiveToString(item?.title)}` }))
.filter((item) => !!item.title);
}
return raw
.split('&&')
@@ -642,6 +949,9 @@ export class LegadoClient {
for (let page = 1; page <= DEFAULT_LEGADO_SEARCH_PAGES; page += 1) {
const targetUrl = buildUrlFromTemplate(rule.searchUrl, source, q, page);
const html = await fetchText(source, targetUrl);
if (page === 1 && rule.ruleSearch.checkKeyWord && !html.includes(rule.ruleSearch.checkKeyWord) && !html.includes(q)) {
throw new Error('搜索结果校验失败');
}
let pageCount = 0;
const json = parseJsonMaybe(html);
if (json && ruleIsJson(rule.ruleSearch.bookList)) {
@@ -666,6 +976,27 @@ export class LegadoClient {
tags: readJsonRule(item, rule.ruleSearch?.kind, source, targetUrl).split(/[,\s]+/).filter(Boolean),
}));
});
} else if (rule.ruleSearch.bookList.trim().startsWith(':')) {
const items = readAllInOneList(html, rule.ruleSearch.bookList);
pageCount = items.length;
items.forEach((item) => {
const detailHref = readRegexItem(item, rule.ruleSearch?.bookUrl, targetUrl);
const title = readRegexItem(item, rule.ruleSearch?.name, targetUrl);
if (!title && !detailHref) return;
const cover = readRegexItem(item, rule.ruleSearch?.coverUrl, targetUrl);
const dedupeKey = detailHref || `${title}|${cover}`;
if (dedupeKey && seen.has(dedupeKey)) return;
if (dedupeKey) seen.add(dedupeKey);
results.push(makeItem(source, {
id: detailHref || undefined,
title,
author: readRegexItem(item, rule.ruleSearch?.author, targetUrl),
summary: readRegexItem(item, rule.ruleSearch?.intro, targetUrl),
cover: cover || undefined,
detailHref,
tags: readRegexItem(item, rule.ruleSearch?.kind, targetUrl).split(/[,\s]+/).filter(Boolean),
}));
});
} else {
const $ = cheerio.load(html);
const items = selectElements($, $.root(), rule.ruleSearch.bookList);
@@ -722,8 +1053,8 @@ export class LegadoClient {
const target = decodeExploreTarget(href) || null;
const navigation = categories.map((item) => ({
title: item.title,
href: encodeExploreTarget({ title: item.title, template: item.template, page: 1 }),
rel: 'legado:explore',
href: item.template.startsWith('__group__:') ? '' : encodeExploreTarget({ title: item.title, template: item.template, page: 1 }),
rel: item.template.startsWith('__group__:') ? 'legado:group' : 'legado:explore',
type: 'application/x-legado-explore',
}));
@@ -731,6 +1062,9 @@ export class LegadoClient {
return { sourceId: source.id, sourceName: source.name, title: source.name, subtitle: '请选择分类', href: href || source.url, entries: [], navigation };
}
if (target.template.startsWith('__group__:')) {
return { sourceId: source.id, sourceName: source.name, title: target.title, href: href || '', entries: [], navigation };
}
const targetUrl = buildExploreTargetUrl(source, target);
const html = await fetchText(source, targetUrl);
const exploreRule = rule.ruleExplore || rule.ruleSearch;
@@ -823,12 +1157,15 @@ export class LegadoClient {
if (detailHref && rule.ruleBookInfo) {
const targetUrl = rule.bookInfoUrl ? buildUrlFromTemplate(rule.bookInfoUrl, source, undefined, 1, detailHref).replace(/\{bookUrl\}/g, encodeURIComponent(detailHref)) : detailHref;
const html = await fetchText(source, targetUrl);
const json = parseJsonMaybe(html);
const $ = json ? null : cheerio.load(html);
const initData = rule.bookInfoInit?.trim().startsWith(':') ? readAllInOneList(html, rule.bookInfoInit)[0] : null;
const json = initData || parseJsonMaybe(html);
const $ = json && initData ? null : json ? null : cheerio.load(html);
const root = $?.root();
const read = (itemRule?: string) => json
? readJsonRule(json, itemRule, source, targetUrl)
: readValue($ as cheerio.CheerioAPI, root as cheerio.Cheerio<any>, itemRule, targetUrl);
const read = (itemRule?: string) => initData
? readRegexItem(initData, itemRule, targetUrl)
: json
? readJsonRule(json, itemRule, source, targetUrl)
: readValue($ as cheerio.CheerioAPI, root as cheerio.Cheerio<any>, itemRule, targetUrl);
const tocUrl = read(rule.ruleBookInfo.tocUrl) || (rule.tocUrl ? buildUrlFromTemplate(rule.tocUrl, source, undefined, 1, detailHref).replace(/\{bookUrl\}/g, encodeURIComponent(detailHref)) : targetUrl);
const cover = read(rule.ruleBookInfo.coverUrl) || fallback?.cover;
const title = read(rule.ruleBookInfo.name) || fallback?.title || '未命名电子书';
@@ -908,6 +1245,27 @@ export class LegadoClient {
chapters.push({ id: stableId(`${source.id}|${normalizedHref}`), title, href: normalizedHref, order: index });
});
}
if (rule.ruleToc.nextTocUrl) {
let nextTocUrl = contentFromRule(html, rule.ruleToc.nextTocUrl, targetUrl);
const visited = new Set([targetUrl]);
for (let page = 0; page < 8 && nextTocUrl; page += 1) {
const normalizedNext = normalizeUrl(targetUrl, nextTocUrl);
if (!normalizedNext || visited.has(normalizedNext)) break;
visited.add(normalizedNext);
const nextHtml = await fetchText(source, normalizedNext);
const $next = cheerio.load(nextHtml);
const nextItems = selectElements($next, $next.root(), rule.ruleToc.chapterList);
nextItems.each((index, element) => {
const root = $next(element);
const title = readValue($next, root, rule.ruleToc?.chapterName, normalizedNext) || `${chapters.length + index + 1}`;
const href = readValue($next, root, rule.ruleToc?.chapterUrl, normalizedNext) || root.attr('href') || '';
if (!href) return;
const normalizedHref = normalizeUrl(normalizedNext, href);
chapters.push({ id: stableId(`${source.id}|${normalizedHref}`), title, href: normalizedHref, order: chapters.length });
});
nextTocUrl = contentFromRule(nextHtml, rule.ruleToc.nextTocUrl, normalizedNext);
}
}
tocCache.set(cacheKey, { data: chapters, expiresAt: Date.now() + cacheTTL });
return chapters;
}
@@ -941,7 +1299,7 @@ export class LegadoClient {
id: stableId(`${source.id}|${targetUrl}`),
title: index >= 0 ? chapters[index].title : '',
href: targetUrl,
content: proxyChapterImages(cleanContent(rawContent), source),
content: proxyChapterImages(cleanContent(rule.ruleContent.sourceRegex ? applyRuleFilters(rawContent, [rule.ruleContent.sourceRegex, '']) : rawContent), source),
previousHref: index > 0 ? chapters[index - 1].href : undefined,
nextHref: index >= 0 && index + 1 < chapters.length ? chapters[index + 1].href : undefined,
};