diff --git a/package.json b/package.json index 49f932e..a014c58 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "next": "^12.1.6", "next-i18next": "^15.3.0", "nprogress": "^0.2.0", + "pino-http": "^10.2.0", "raw-loader": "^4.0.2", "rc-image": "^7.0.0-2", "rc-util": "^5.34.1", diff --git a/server.js b/server.js index db970d0..1eaa34b 100644 --- a/server.js +++ b/server.js @@ -2,6 +2,7 @@ const express = require('express'); const next = require('next'); const cookieParser = require('cookie-parser'); +const pino = require('pino-http')(); const app = next({ dev: false }); const handle = app.getRequestHandler(); @@ -11,81 +12,77 @@ const DEFAULT_LOCALE = 'zh'; const FALLBACK_LOCALE = 'en'; const SUPPORTED_LOCALES = ['zh', 'en']; +const isStaticRoute = (url) => url.startsWith('/_next') || url.startsWith('/static'); + +const getLocaleFromUrl = (path) => { + const urlParts = path.split('/'); + return SUPPORTED_LOCALES.includes(urlParts[1]) ? urlParts[1] : null; +}; + +const determineLocale = (req, urlLocale) => { + let locale = req.cookies.locale; + if (!locale) { + const browserLocale = req.acceptsLanguages(SUPPORTED_LOCALES); + locale = urlLocale || browserLocale || FALLBACK_LOCALE; + if (browserLocale && browserLocale !== 'zh') { + locale = FALLBACK_LOCALE; + } + } + return SUPPORTED_LOCALES.includes(locale) ? locale : FALLBACK_LOCALE; +}; + +const handleRedirect = (req, res, locale, urlLocale) => { + if (urlLocale) { + if (urlLocale !== locale) { + locale = urlLocale; + } + if (locale === DEFAULT_LOCALE) { + const newUrl = req.url.replace(`/${DEFAULT_LOCALE}`, '') || '/'; + return res.redirect(307, newUrl); + } + } else if (locale !== DEFAULT_LOCALE) { + let newUrl = `/${locale}${req.url}`; + if (newUrl.endsWith('/') && newUrl.length > 1) { + newUrl = newUrl.slice(0, -1); + } + return res.redirect(307, newUrl); + } + return false; +}; + +const canSetCookie = (req) => { + // 检查请求头中是否有 'cookie' 字段 + return req.headers.cookie !== undefined; +}; + async function startServer() { try { await app.prepare(); const server = express(); server.use(cookieParser()); + server.use(pino); - // 中间件处理语言选择和路由 server.use((req, res, next) => { - if (req.url.startsWith('/_next') || req.url.startsWith('/static')) { - next(); - } else { - let locale = req.cookies.locale; - - // 解析 URL 中的语言设置 - const urlParts = req.path.split('/'); - const urlLocale = SUPPORTED_LOCALES.includes(urlParts[1]) - ? urlParts[1] - : null; - - if (!locale) { - // 如果没有 cookie,使用 URL 中的语言或浏览器偏好 - const browserLocale = req.acceptsLanguages(SUPPORTED_LOCALES); - locale = urlLocale || browserLocale || FALLBACK_LOCALE; - - // 如果浏览器语言不是中文,默认使用英语 - if (browserLocale && browserLocale !== 'zh') { - locale = FALLBACK_LOCALE; - } - } - - // 确保 locale 是支持的语言之一 - locale = SUPPORTED_LOCALES.includes(locale) ? locale : FALLBACK_LOCALE; - if (urlLocale) { - // URL 中有语言前缀 - if (urlLocale !== locale) { - // URL 语言与 cookie 不匹配,更新 locale - locale = urlLocale; - } - if (locale === DEFAULT_LOCALE) { - // 默认语言不应该在 URL 中显示,重定向到无前缀的 URL - const newUrl = req.url.replace(`/${DEFAULT_LOCALE}`, '') || '/'; - return res.redirect(307, newUrl); - } - } else if (locale !== DEFAULT_LOCALE) { - // URL 中没有语言前缀,且不是默认语言,添加前缀并重定向 - let newUrl = `/${locale}${req.url}`; - if (newUrl.endsWith('/')) { - // 如果以 '/' 结尾,去掉末尾的 '/' - newUrl = newUrl.slice(0, -1); - } - return res.redirect(307, newUrl); - } - - // 设置或更新 cookie - res.cookie('locale', locale, { - maxAge: 365 * 24 * 60 * 60 * 1000, - }); + if (isStaticRoute(req.url)) { + return next(); + } - req.locale = locale; + const urlLocale = getLocaleFromUrl(req.path); + let locale = determineLocale(req, urlLocale); - // // 处理尾部斜杠并重定向 - // if (req.url.length > 1 && req.path.endsWith('/')) { - // const newUrl = - // req.path.slice(0, -1) + req.params - // return res.redirect(301, newUrl); - // } + if (handleRedirect(req, res, locale, urlLocale)) { + return; // 如果发生重定向,立即返回 + } - next(); + // 检查响应头是否已经发送 + if (canSetCookie(req) && !res.headersSent) { + res.cookie('locale', locale, { maxAge: 365 * 24 * 60 * 60 * 1000 }); } + req.locale = locale; + next(); }); - // 处理所有其他请求 - server.all('*', (req, res) => { - return handle(req, res); - }); + server.all('*', (req, res) => handle(req, res)); server.listen(PORT, (err) => { if (err) throw err; @@ -97,4 +94,4 @@ async function startServer() { } } -startServer(); +startServer(); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e877234..2bd863a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2157,6 +2157,13 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.npmmirror.com/abab/-/abab-2.0.6.tgz" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz" @@ -2392,6 +2399,11 @@ asynckit@^0.4.0: resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + autoprefixer@^10.4.7: version "10.4.12" resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.12.tgz" @@ -2523,6 +2535,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz" @@ -2598,6 +2615,14 @@ buffer-from@^1.0.0: resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + bytes@3.1.2: version "3.1.2" resolved "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz" @@ -3876,6 +3901,16 @@ etag@~1.8.1: resolved "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz" @@ -3990,6 +4025,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + fastq@^1.6.0: version "1.13.0" resolved "https://registry.npmmirror.com/fastq/-/fastq-1.13.0.tgz" @@ -4502,6 +4542,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.npmmirror.com/ignore/-/ignore-5.2.0.tgz" @@ -6218,6 +6263,11 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz" @@ -6448,6 +6498,46 @@ pify@^3.0.0: resolved "https://registry.npmmirror.com/pify/-/pify-3.0.0.tgz" integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-http@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-10.2.0.tgz#157b799e84ee4f6fe5a862fa3758f10d25376fed" + integrity sha512-am03BxnV3Ckx68OkbH0iZs3indsrH78wncQ6w1w51KroIbvJZNImBKX2X1wjdY8lSyaJ0UrX/dnO2DY3cTeCRw== + dependencies: + get-caller-file "^2.0.5" + pino "^9.0.0" + pino-std-serializers "^7.0.0" + process-warning "^3.0.0" + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^9.0.0: + version "9.3.2" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.3.2.tgz#a530d6d28f1d954b6f54416a218cbb616f52f901" + integrity sha512-WtARBjgZ7LNEkrGWxMBN/jvlFiE17LTbBoH0konmBU684Kd0uIiDwBXlcTCW7iJnA6HfIKwUssS/2AC6cDEanw== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^7.0.0" + process-warning "^4.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pirates@^4.0.4: version "4.0.5" resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.5.tgz" @@ -6582,6 +6672,21 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + +process-warning@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.0.tgz#581e3a7a1fb456c5f4fd239f76bce75897682d5a" + integrity sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz" @@ -6651,6 +6756,11 @@ queue-microtask@^1.2.2: resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz" @@ -6894,6 +7004,17 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.7.tgz" @@ -6914,6 +7035,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + redent@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz" @@ -7139,6 +7265,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-stable-stringify@^2.3.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz" @@ -7306,6 +7437,13 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== + dependencies: + atomic-sleep "^1.0.0" + sortablejs@^1.15.0: version "1.15.0" resolved "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.0.tgz" @@ -7377,6 +7515,11 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + split@^1.0.0: version "1.0.1" resolved "https://registry.npmmirror.com/split/-/split-1.0.1.tgz" @@ -7489,7 +7632,7 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -7688,6 +7831,13 @@ text-table@^0.2.0: resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + throat@^6.0.1: version "6.0.1" resolved "https://registry.npmmirror.com/throat/-/throat-6.0.1.tgz"