├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── bun.lock ├── clashx-meta.png ├── cli.ts ├── package.json ├── src ├── convert.ts ├── geo │ ├── geo.d.ts │ ├── geo.js │ ├── geo.proto │ └── geoHelper.ts ├── index.ts ├── sub.ts └── utils.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc /.gitattributes: -------------------------------------------------------------------------------- 1 | src/geo/geo.js linguist-generated=true 2 | src/geo/geo.d.ts linguist-generated=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # prod 2 | dist/ 3 | 4 | # dev 5 | .yarn/ 6 | !.yarn/releases 7 | .vscode/* 8 | !.vscode/launch.json 9 | !.vscode/*.code-snippets 10 | .idea/workspace.xml 11 | .idea/usage.statistics.xml 12 | .idea/shelf 13 | 14 | # deps 15 | node_modules/ 16 | .wrangler 17 | 18 | # env 19 | .env 20 | .env.production 21 | .dev.vars 22 | 23 | # logs 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | pnpm-debug.log* 30 | lerna-debug.log* 31 | 32 | # misc 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jctaoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clash Config Script 2 | 3 | 一个基于 Cloudflare Workers 的订阅转换服务,用于将机场订阅转换为优化的 Clash 配置文件。 4 | 5 | [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https%3A%2F%2Fgithub.com%2Fjctaoo%2FClashConfig) 6 | 7 | ## ✨ 特性 8 | 9 | - 🚀 **无服务器部署**: 基于 Cloudflare Workers,全球加速访问 10 | - 🎯 **智能规则**: 内置优化的分流规则,支持 GEOIP、GEOSITE 数据 11 | - 🔐 **Token 管理**: 支持 Token 订阅管理,可配置节点过滤,feel free to share token with your friends 12 | - 🌍 **地区筛选**: 支持按地区过滤节点 13 | - 📦 **多内核支持**: 第一方支持 **Clash.Meta** 和 **Stash** 内核 14 | 15 | ## 🎯 TODO 16 | 17 | - [ ] 1. 迁移到 GEOSITE, 避免使用 classic behavior 规则 18 | - [ ] 2. 检查 https://github.com/DustinWin/ShellCrash/blob/dev/public/fake_ip_filter.list 以补全 fake-ip-filter 19 | - [ ] 3. subrequest 被 cloudflare 缓存 20 | 21 | ## ⚡ 快速开始 22 | 23 | ### PowerShell 24 | 25 | ```ps1 26 | $RawUrl = "https://your-raw-url"; 27 | $SubUrl = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($RawUrl)); 28 | $ConfigUrl = "https://clash.jctaoo.site/sub?sub=$SubUrl"; 29 | $EncodedConfigUrl = [System.Net.WebUtility]::UrlEncode($ConfigUrl) 30 | $UrlScheme = "clash://install-config?url=$EncodedConfigUrl"; 31 | Start-Process $UrlScheme 32 | ``` 33 | 34 | ### MacOS 35 | 36 | ```sh 37 | RAW_URL="https://your-raw-url" 38 | SUB_URL=$(echo -n $RAW_URL | base64) 39 | CONFIG_URL="https://clash.jctaoo.site/sub?sub=$SUB_URL" 40 | ENCODED_CONFIG_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$CONFIG_URL'''))") 41 | URL_SCHEME="clash://install-config?url=$ENCODED_CONFIG_URL" 42 | open $URL_SCHEME 43 | ``` 44 | 45 | ### iOS 46 | 47 | 获取并运行 [快捷指令](https://www.icloud.com/shortcuts/e3afa7a85e924aa3926e6ea6b686bc83) (mac 也可以用) 48 | 49 | 更进一步,可以使用 token 管理的后台订阅 api,支持筛选节点等功能,参考下方使用方法 50 | 51 | ## 🖥️ 使用方法 52 | 53 | ### 📡 API Endpoints 54 | 55 | #### 1. `/sub` - 基础订阅转换 56 | 57 | **功能**: 将机场订阅地址转换为优化后的 Clash 配置 58 | 59 | **参数**: 60 | - `sub` (必需): Base64 编码的订阅 URL 61 | - `convert` (可选): 是否进行配置转换,默认为 `true`。设置为 `false` 可跳过转换直接返回原始配置 62 | 63 | **使用示例**: 64 | ``` 65 | https://clash.jctaoo.site/sub?sub= 66 | https://clash.jctaoo.site/sub?sub=&convert=false 67 | ``` 68 | 69 | #### 2. `/:token` - Token 订阅(推荐) 70 | 71 | **功能**: 使用 Token 获取订阅,支持自动缓存和配置管理, 支持过滤订阅的节点 72 | 73 | **参数**: 74 | - `token` (必需): 通过 CLI 工具生成的用户 Token(格式: `sk-xxxxx`) 75 | 76 | **使用示例**: 77 | ``` 78 | https://clash.jctaoo.site/sk-your-token 79 | ``` 80 | 81 | **使用流程**: 82 | 1. 使用 `bun run cli add` 添加订阅并获取 token 83 | 2. 将 token 添加到 Clash 订阅地址: `https://clash.jctaoo.site/sk-your-token` 84 | 3. 使用 CLI 工具管理和更新订阅配置 85 | 86 | ### 💡 客户端说明 87 | 88 | - 可以为订阅设置自动更新,1440分钟更新一次 89 | - clash-verge-rev: 打开 虚拟网卡模式,关闭系统代理,虚拟网卡配置中,开启 严格路由 90 | - clashx.meta: 根据如下图片配置,然后使用 tun 模式,关闭系统代理 ![clashx-meta](./clashx-meta.png) 91 | > https://github.com/MetaCubeX/ClashX.Meta/issues/103#issuecomment-2510050389 92 | - 其他 clash: 使用 tun 模式 93 | 94 | 95 | ## 💻 Development 96 | 97 | ### 前置要求 98 | 99 | 1. 安装 [Bun](https://bun.sh) 100 | 101 | ### 开发步骤 102 | 103 | 1. **安装依赖** 104 | ```bash 105 | bun install 106 | ``` 107 | 108 | 2. **登录 Cloudflare**(重要!) 109 | ```bash 110 | bun wrangler login 111 | ``` 112 | 这将打开浏览器进行 Cloudflare 账户授权。登录后才能访问 KV 存储和部署服务。 113 | 114 | 3. **生成 Geo 相关脚本** 115 | ```bash 116 | bun run pb-gen && bun run pb-gen-dts 117 | ``` 118 | 119 | 4. **生成 Cloudflare Workers 类型定义** 120 | ```bash 121 | bun run cf-typegen 122 | ``` 123 | 这将根据 `wrangler.jsonc` 配置生成 TypeScript 类型定义文件,包括 KV、环境变量等的类型。 124 | 125 | 5. **启动开发服务器** 126 | ```bash 127 | bun run dev 128 | ``` 129 | 开发服务器将在本地启动,可以进行调试和测试。 130 | 131 | 132 | ## 🔧 CLI 工具使用指南 133 | 134 | 这是一个用于管理 Cloudflare KV 中订阅的命令行工具。 135 | 136 | ### CLI 命令 137 | 138 | #### 1. 添加订阅(交互式) 139 | 140 | ```bash 141 | bun run cli add 142 | ``` 143 | 144 | 该命令会通过交互式提示引导你输入所有必要信息,并自动生成 token。 145 | 146 | **提示说明:** 147 | - **Subscription label**: 订阅标签(必填) 148 | - **Subscription URL**: 订阅 URL(必填,必须以 http:// 或 https:// 开头) 149 | - **Filter label**: 过滤器标签(可选,默认使用订阅标签) 150 | - **Filter regions**: 地区列表(可选,多个地区用逗号分隔,如:HK,US,JP) 151 | - **Set maximum billing rate**: 是否设置最大计费倍率(y/N) 152 | - **Maximum billing rate**: 最大计费倍率(仅在上一步选择 y 时显示) 153 | - **Exclude regex pattern**: 排除正则表达式(可选,用于过滤节点) 154 | 155 | #### 2. 获取订阅信息 156 | 157 | ```bash 158 | bun run cli get sk-your-token 159 | ``` 160 | 161 | 该命令会显示指定 token 的订阅详细信息。Token 会保存在订阅信息中,可以随时通过此命令重新获取。 162 | 163 | #### 3. 获取订阅链接 164 | 165 | ```bash 166 | # 使用默认 base-url (https://clash.jctaoo.site) 167 | bun run cli link sk-your-token 168 | 169 | # 获取链接并自动在 Clash 中打开 170 | bun run cli link sk-your-token --go 171 | # 或使用简写 172 | bun run cli link sk-your-token -g 173 | 174 | # 自定义 base-url 175 | bun run cli link sk-your-token --base-url https://your-worker.workers.dev 176 | 177 | # 自定义 base-url 并打开 178 | bun run cli link sk-your-token -b https://your-worker.workers.dev -g 179 | ``` 180 | 181 | 该命令会生成完整的订阅链接。使用 `--go`/`-g` 参数可以自动生成 Clash URL scheme 并打开 Clash 客户端导入配置。 182 | 183 | **参数说明:** 184 | - `--base-url` / `-b`: Worker 部署的 base URL(默认:`https://clash.jctaoo.site`) 185 | - `--go` / `-g`: 生成 Clash URL scheme 并自动打开(支持 Windows/macOS/Linux) 186 | 187 | #### 4. 更新订阅(使用编辑器) 188 | 189 | ```bash 190 | bun run cli update sk-your-token 191 | ``` 192 | 193 | 该命令会打开你的默认编辑器,显示当前订阅信息的 JSON 格式,你可以直接在编辑器中修改。保存后会自动更新订阅。 194 | 195 | **注意事项:** 196 | - 必填字段:`label`, `url`, `filter.label` 197 | - `token` 字段是只读的,即使在编辑器中修改也会被忽略 198 | - `regions` 为空数组时会被忽略 199 | - `maxBillingRate` 和 `excludeRegex` 为空时会被移除 200 | - `content` 字段会被保留,不会在编辑器中显示(避免编辑器卡顿) 201 | - 保存时会自动验证 JSON 格式和必填字段,如果验证失败会提示错误并允许继续编辑 202 | 203 | #### 5. 删除订阅 204 | 205 | ```bash 206 | bun run cli delete sk-your-token 207 | ``` 208 | 209 | #### 6. 列出所有订阅 210 | 211 | ```bash 212 | bun run cli list 213 | ``` 214 | 215 | 该命令会列出所有已保存的订阅信息,包括 token、标签、URL 等关键信息。 216 | 217 | ### KV Key 格式 218 | 219 | - **用户 Token 格式**: `sk-{32位随机十六进制字符串}` 220 | - **KV Key 格式**: `kv:{SHA256(用户Token)}` 221 | - **存储值**: JSON 格式的 `ClashSubInformation` 对象 222 | 223 | ### 示例:完整工作流 224 | 225 | ```bash 226 | # 1. 添加订阅 227 | bun run cli add 228 | 229 | # 2. 查看订阅信息 230 | bun run cli get sk-your-token 231 | 232 | # 3. 获取订阅链接并在 Clash 中打开 233 | bun run cli link sk-your-token --go 234 | 235 | # 4. 更新订阅 236 | bun run cli update sk-your-token 237 | 238 | # 5. 列出所有订阅 239 | bun run cli list 240 | 241 | # 6. 删除订阅 242 | bun run cli delete sk-your-token 243 | ``` 244 | 245 | ### CLI 注意事项 246 | 247 | 1. **Token 持久化**: 生成的 User Token 会自动保存在订阅信息中,可以随时通过 `get` 命令重新获取,无需担心丢失 248 | 2. **KV 命名空间**: 默认使用 wrangler.jsonc 中配置的 KV binding (默认为 "KV") 249 | 3. **Wrangler 依赖**: 需要安装并配置 Wrangler CLI 250 | 4. **身份验证**: 确保已通过 `wrangler login` 登录到 Cloudflare 账户 251 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "clash-config", 6 | "dependencies": { 7 | "@inquirer/prompts": "^7.8.6", 8 | "@types/yargs": "^17.0.33", 9 | "hono": "^4.9.9", 10 | "protobufjs": "^7.5.4", 11 | "yaml": "^2.8.1", 12 | "yargs": "^18.0.0", 13 | }, 14 | "devDependencies": { 15 | "protobufjs-cli": "^1.1.3", 16 | "wrangler": "^4.4.0", 17 | }, 18 | }, 19 | }, 20 | "packages": { 21 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 22 | 23 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], 24 | 25 | "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], 26 | 27 | "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], 28 | 29 | "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], 30 | 31 | "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.5", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250924.0" }, "optionalPeers": ["workerd"] }, "sha512-eB3UAIVhrvY+CMZrRXS/bAv5kWdNiH+dgwu+1M1S7keDeonxkfKIGVIrhcCLTkcqYlN30MPURPuVFUEzIWuuvg=="], 32 | 33 | "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251001.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-y1ST/cCscaRewWRnsHZdWbgiLJbki5UMGd0hMo/FLqjlztwPeDgQ5CGm5jMiCDdw/IBCpWxEukftPYR34rWNog=="], 34 | 35 | "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251001.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+z4QHHZ/Yix82zLFYS+ZS2UV09IENFPwDCEKUWfnrM9Km2jOOW3Ua4hJNob1EgQUYs8fFZo7k5O/tpwxMsSbbQ=="], 36 | 37 | "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251001.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hGS+O2V9Mm2XjJUaB9ZHMA5asDUaDjKko42e+accbew0PQR7zrAl1afdII6hMqCLV4tk4GAjvhv281pN4g48rg=="], 38 | 39 | "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251001.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QYaMK+pRgt28N7CX1JlJ+ToegJF9LxzqdT7MjWqPgVj9D2WTyIhBVYl3wYjJRcgOlnn+DRt42+li4T64CPEeuA=="], 40 | 41 | "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251001.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ospnDR/FlyRvrv9DSHuxDAXmzEBLDUiAHQrQHda1iUH9HqxnNQ8giz9VlPfq7NIRc7bQ1ZdIYPGLJOY4Q366Ng=="], 42 | 43 | "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], 44 | 45 | "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], 46 | 47 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], 48 | 49 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], 50 | 51 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="], 52 | 53 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="], 54 | 55 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="], 56 | 57 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="], 58 | 59 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="], 60 | 61 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="], 62 | 63 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="], 64 | 65 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="], 66 | 67 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="], 68 | 69 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="], 70 | 71 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="], 72 | 73 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="], 74 | 75 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="], 76 | 77 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="], 78 | 79 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="], 80 | 81 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="], 82 | 83 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="], 84 | 85 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="], 86 | 87 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="], 88 | 89 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="], 90 | 91 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], 92 | 93 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], 94 | 95 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], 96 | 97 | "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], 98 | 99 | "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], 100 | 101 | "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], 102 | 103 | "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], 104 | 105 | "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], 106 | 107 | "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], 108 | 109 | "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], 110 | 111 | "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], 112 | 113 | "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], 114 | 115 | "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], 116 | 117 | "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], 118 | 119 | "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], 120 | 121 | "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], 122 | 123 | "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], 124 | 125 | "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], 126 | 127 | "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], 128 | 129 | "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], 130 | 131 | "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], 132 | 133 | "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], 134 | 135 | "@inquirer/ansi": ["@inquirer/ansi@1.0.0", "", {}, "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA=="], 136 | 137 | "@inquirer/checkbox": ["@inquirer/checkbox@4.2.4", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw=="], 138 | 139 | "@inquirer/confirm": ["@inquirer/confirm@5.1.18", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw=="], 140 | 141 | "@inquirer/core": ["@inquirer/core@10.2.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA=="], 142 | 143 | "@inquirer/editor": ["@inquirer/editor@4.2.20", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/external-editor": "^1.0.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g=="], 144 | 145 | "@inquirer/expand": ["@inquirer/expand@4.0.20", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow=="], 146 | 147 | "@inquirer/external-editor": ["@inquirer/external-editor@1.0.2", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ=="], 148 | 149 | "@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="], 150 | 151 | "@inquirer/input": ["@inquirer/input@4.2.4", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw=="], 152 | 153 | "@inquirer/number": ["@inquirer/number@3.0.20", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg=="], 154 | 155 | "@inquirer/password": ["@inquirer/password@4.0.20", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug=="], 156 | 157 | "@inquirer/prompts": ["@inquirer/prompts@7.8.6", "", { "dependencies": { "@inquirer/checkbox": "^4.2.4", "@inquirer/confirm": "^5.1.18", "@inquirer/editor": "^4.2.20", "@inquirer/expand": "^4.0.20", "@inquirer/input": "^4.2.4", "@inquirer/number": "^3.0.20", "@inquirer/password": "^4.0.20", "@inquirer/rawlist": "^4.1.8", "@inquirer/search": "^3.1.3", "@inquirer/select": "^4.3.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig=="], 158 | 159 | "@inquirer/rawlist": ["@inquirer/rawlist@4.1.8", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg=="], 160 | 161 | "@inquirer/search": ["@inquirer/search@3.1.3", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q=="], 162 | 163 | "@inquirer/select": ["@inquirer/select@4.3.4", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA=="], 164 | 165 | "@inquirer/type": ["@inquirer/type@3.0.8", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw=="], 166 | 167 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 168 | 169 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 170 | 171 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], 172 | 173 | "@jsdoc/salty": ["@jsdoc/salty@0.2.9", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw=="], 174 | 175 | "@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="], 176 | 177 | "@poppinss/dumper": ["@poppinss/dumper@0.6.4", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ=="], 178 | 179 | "@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="], 180 | 181 | "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], 182 | 183 | "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], 184 | 185 | "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], 186 | 187 | "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], 188 | 189 | "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], 190 | 191 | "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], 192 | 193 | "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], 194 | 195 | "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], 196 | 197 | "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], 198 | 199 | "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], 200 | 201 | "@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="], 202 | 203 | "@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="], 204 | 205 | "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], 206 | 207 | "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], 208 | 209 | "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], 210 | 211 | "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], 212 | 213 | "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], 214 | 215 | "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], 216 | 217 | "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], 218 | 219 | "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 220 | 221 | "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], 222 | 223 | "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 224 | 225 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 226 | 227 | "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 228 | 229 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 230 | 231 | "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], 232 | 233 | "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], 234 | 235 | "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 236 | 237 | "catharsis": ["catharsis@0.9.0", "", { "dependencies": { "lodash": "^4.17.15" } }, "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A=="], 238 | 239 | "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 240 | 241 | "chardet": ["chardet@2.1.0", "", {}, "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA=="], 242 | 243 | "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], 244 | 245 | "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], 246 | 247 | "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], 248 | 249 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 250 | 251 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 252 | 253 | "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], 254 | 255 | "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], 256 | 257 | "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 258 | 259 | "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 260 | 261 | "detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="], 262 | 263 | "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], 264 | 265 | "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 266 | 267 | "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], 268 | 269 | "esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], 270 | 271 | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 272 | 273 | "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], 274 | 275 | "escodegen": ["escodegen@1.14.3", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw=="], 276 | 277 | "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 278 | 279 | "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], 280 | 281 | "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], 282 | 283 | "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 284 | 285 | "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 286 | 287 | "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], 288 | 289 | "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], 290 | 291 | "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 292 | 293 | "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], 294 | 295 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 296 | 297 | "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 298 | 299 | "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 300 | 301 | "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], 302 | 303 | "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], 304 | 305 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 306 | 307 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 308 | 309 | "hono": ["hono@4.9.9", "", {}, "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw=="], 310 | 311 | "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], 312 | 313 | "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], 314 | 315 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 316 | 317 | "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], 318 | 319 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 320 | 321 | "js2xmlparser": ["js2xmlparser@4.0.2", "", { "dependencies": { "xmlcreate": "^2.0.4" } }, "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA=="], 322 | 323 | "jsdoc": ["jsdoc@4.0.4", "", { "dependencies": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", "@types/markdown-it": "^14.1.1", "bluebird": "^3.7.2", "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", "js2xmlparser": "^4.0.2", "klaw": "^3.0.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^8.6.7", "marked": "^4.0.10", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", "strip-json-comments": "^3.1.0", "underscore": "~1.13.2" }, "bin": { "jsdoc": "./jsdoc.js" } }, "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw=="], 324 | 325 | "klaw": ["klaw@3.0.0", "", { "dependencies": { "graceful-fs": "^4.1.9" } }, "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g=="], 326 | 327 | "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], 328 | 329 | "levn": ["levn@0.3.0", "", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="], 330 | 331 | "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], 332 | 333 | "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 334 | 335 | "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], 336 | 337 | "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], 338 | 339 | "markdown-it-anchor": ["markdown-it-anchor@8.6.7", "", { "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA=="], 340 | 341 | "marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="], 342 | 343 | "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], 344 | 345 | "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], 346 | 347 | "miniflare": ["miniflare@4.20251001.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251001.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-OHd31D2LT8JH+85nVXClV0Z18jxirCohzKNAcZs/fgt4mIkUDtidX3VqR3ovAM0jWooNxrFhB9NSs3iDbiJF7Q=="], 348 | 349 | "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], 350 | 351 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 352 | 353 | "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], 354 | 355 | "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], 356 | 357 | "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], 358 | 359 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 360 | 361 | "optionator": ["optionator@0.8.3", "", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="], 362 | 363 | "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], 364 | 365 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 366 | 367 | "prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], 368 | 369 | "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], 370 | 371 | "protobufjs-cli": ["protobufjs-cli@1.1.3", "", { "dependencies": { "chalk": "^4.0.0", "escodegen": "^1.13.0", "espree": "^9.0.0", "estraverse": "^5.1.0", "glob": "^8.0.0", "jsdoc": "^4.0.0", "minimist": "^1.2.0", "semver": "^7.1.2", "tmp": "^0.2.1", "uglify-js": "^3.7.7" }, "peerDependencies": { "protobufjs": "^7.0.0" }, "bin": { "pbjs": "bin/pbjs", "pbts": "bin/pbts" } }, "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ=="], 372 | 373 | "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], 374 | 375 | "requizzle": ["requizzle@0.2.4", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw=="], 376 | 377 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 378 | 379 | "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], 380 | 381 | "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], 382 | 383 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 384 | 385 | "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], 386 | 387 | "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 388 | 389 | "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], 390 | 391 | "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], 392 | 393 | "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 394 | 395 | "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 396 | 397 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 398 | 399 | "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], 400 | 401 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 402 | 403 | "type-check": ["type-check@0.3.2", "", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="], 404 | 405 | "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], 406 | 407 | "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], 408 | 409 | "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], 410 | 411 | "underscore": ["underscore@1.13.7", "", {}, "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="], 412 | 413 | "undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="], 414 | 415 | "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], 416 | 417 | "unenv": ["unenv@2.0.0-rc.21", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A=="], 418 | 419 | "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 420 | 421 | "workerd": ["workerd@1.20251001.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251001.0", "@cloudflare/workerd-darwin-arm64": "1.20251001.0", "@cloudflare/workerd-linux-64": "1.20251001.0", "@cloudflare/workerd-linux-arm64": "1.20251001.0", "@cloudflare/workerd-windows-64": "1.20251001.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oT/K4YWNhmwpVmGeaHNmF7mLRfgjszlVr7lJtpS4jx5khmxmMzWZEEQRrJEpgzeHP6DOq9qWLPNT0bjMK7TchQ=="], 422 | 423 | "wrangler": ["wrangler@4.41.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.5", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251001.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20251001.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251001.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-SPiBk/5SgCSIdcWw8EXc8DzqtrjbIU+/n22fQjyz4RnULAqCFJjy84F5crcWnb1J/iPiOzm7mS9bMGFFtpwS/w=="], 424 | 425 | "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], 426 | 427 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 428 | 429 | "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], 430 | 431 | "xmlcreate": ["xmlcreate@2.0.4", "", {}, "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="], 432 | 433 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 434 | 435 | "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], 436 | 437 | "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], 438 | 439 | "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], 440 | 441 | "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], 442 | 443 | "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], 444 | 445 | "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], 446 | 447 | "zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], 448 | 449 | "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], 450 | 451 | "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], 452 | 453 | "escodegen/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], 454 | 455 | "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 456 | 457 | "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 458 | 459 | "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 460 | 461 | "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 462 | 463 | "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /clashx-meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/ClashConfig/15eb645636f372b58751c30ea27cf81feb64ee1b/clashx-meta.png -------------------------------------------------------------------------------- /cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | import yargs from "yargs"; 3 | import { hideBin } from "yargs/helpers"; 4 | import { createHash, randomBytes } from "crypto"; 5 | import { exec } from "child_process"; 6 | import { promisify } from "util"; 7 | import { input, confirm, number, editor } from "@inquirer/prompts"; 8 | import type { ClashSubInformation } from "./src/sub"; 9 | 10 | const execAsync = promisify(exec); 11 | 12 | // CLI 使用的订阅信息类型,不包含 content 字段 13 | type ClashSubInformationCLI = Omit; 14 | 15 | const KV_BINDING = "KV"; 16 | 17 | /** 18 | * Generate a new user token in the format sk-xxxx 19 | */ 20 | function generateToken(): string { 21 | const randomPart = randomBytes(16).toString("hex"); 22 | return `sk-${randomPart}`; 23 | } 24 | 25 | /** 26 | * Hash token with SHA256 27 | */ 28 | function hashToken(token: string): string { 29 | return createHash("sha256").update(token).digest("hex"); 30 | } 31 | 32 | /** 33 | * Get KV key from user token 34 | */ 35 | function getKVKey(userToken: string): string { 36 | const hashedToken = hashToken(userToken); 37 | return `kv:${hashedToken}`; 38 | } 39 | 40 | /** 41 | * Execute wrangler kv command 42 | */ 43 | async function kvPut(key: string, value: string, metadata?: Record): Promise { 44 | let command = `wrangler kv key put --binding=${KV_BINDING} --remote "${key}" "${value.replace(/"/g, '\\"')}"`; 45 | 46 | // Add metadata if provided 47 | if (metadata) { 48 | const metadataJson = JSON.stringify(metadata).replace(/"/g, '\\"'); 49 | command += ` --metadata "${metadataJson}"`; 50 | } 51 | 52 | console.log(`Executing: ${command}`); 53 | const { stdout, stderr } = await execAsync(command); 54 | if (stderr) console.error(stderr); 55 | if (stdout) console.log(stdout); 56 | } 57 | 58 | async function kvGet(key: string): Promise { 59 | try { 60 | const command = `wrangler kv key get --binding=${KV_BINDING} --remote "${key}"`; 61 | const { stdout, stderr } = await execAsync(command); 62 | const output = stdout.trim(); 63 | 64 | // Check if output indicates key not found 65 | if ( 66 | output.toLowerCase().includes("not found") || 67 | output.toLowerCase().includes("value not found") || 68 | output === "" 69 | ) { 70 | return null; 71 | } 72 | 73 | return output; 74 | } catch (error: any) { 75 | // Check various error conditions that indicate key not found 76 | const errorMessage = (error.message || "").toLowerCase(); 77 | const errorStderr = (error.stderr || "").toLowerCase(); 78 | 79 | if ( 80 | errorMessage.includes("not found") || 81 | errorMessage.includes("value not found") || 82 | errorStderr.includes("not found") || 83 | errorStderr.includes("value not found") || 84 | error.code === 1 85 | ) { 86 | return null; 87 | } 88 | throw error; 89 | } 90 | } 91 | 92 | async function kvDelete(key: string): Promise { 93 | const command = `wrangler kv key delete --binding=${KV_BINDING} --remote "${key}"`; 94 | console.log(`Executing: ${command}`); 95 | const { stdout, stderr } = await execAsync(command); 96 | if (stderr) console.error(stderr); 97 | if (stdout) console.log(stdout); 98 | } 99 | 100 | async function kvList(prefix?: string): Promise> { 101 | const prefixArg = prefix ? `--prefix="${prefix}"` : ""; 102 | const command = `wrangler kv key list --binding=${KV_BINDING} --remote ${prefixArg}`; 103 | const { stdout } = await execAsync(command); 104 | return JSON.parse(stdout); 105 | } 106 | 107 | /** 108 | * Log subscription information in a formatted way 109 | */ 110 | function logSubInfo(subInfo: ClashSubInformationCLI, kvKey?: string): void { 111 | console.log("\n✅ Subscription Information:"); 112 | 113 | console.log(` 🔑 Token: ${subInfo.token}`); 114 | 115 | if (kvKey) { 116 | console.log(` 🗝️ KV Key: ${kvKey}`); 117 | } 118 | 119 | console.log(` 🏷️ Label: ${subInfo.label}`); 120 | console.log(` 🔗 URL: ${subInfo.url}`); 121 | console.log(` 🎯 Filter Label: ${subInfo.filter.label}`); 122 | 123 | if (subInfo.filter.regions && subInfo.filter.regions.length > 0) { 124 | console.log(` 🌍 Regions: ${subInfo.filter.regions.join(", ")}`); 125 | } 126 | if (subInfo.filter.maxBillingRate) { 127 | console.log(` 💰 Max Billing: ${subInfo.filter.maxBillingRate}`); 128 | } 129 | if (subInfo.filter.excludeRegex) { 130 | console.log(` 🚫 Exclude Regex: ${subInfo.filter.excludeRegex}`); 131 | } 132 | } 133 | 134 | // CLI Commands 135 | yargs(hideBin(process.argv)) 136 | .scriptName("clash-sub-cli") 137 | .usage("$0 [options]") 138 | 139 | // Add a new subscription 140 | .command( 141 | "add", 142 | "Add a new subscription and generate a token", 143 | () => {}, 144 | async () => { 145 | console.log("\n📝 Add New Subscription\n"); 146 | 147 | // Collect subscription information interactively 148 | const label = await input({ 149 | message: "Subscription label:", 150 | required: true, 151 | }); 152 | 153 | const url = await input({ 154 | message: "Subscription URL:", 155 | required: true, 156 | validate: (value) => { 157 | if (!value.startsWith("http://") && !value.startsWith("https://")) { 158 | return "URL must start with http:// or https://"; 159 | } 160 | return true; 161 | }, 162 | }); 163 | 164 | const filterLabel = await input({ 165 | message: "Filter label:", 166 | default: label, 167 | }); 168 | 169 | const regionsInput = await input({ 170 | message: "Filter regions (comma-separated, e.g., HK,US,JP):", 171 | default: "", 172 | }); 173 | 174 | const regions = regionsInput 175 | .split(",") 176 | .map((r) => r.trim()) 177 | .filter((r) => r.length > 0); 178 | 179 | const hasMaxBillingRate = await confirm({ 180 | message: "Set maximum billing rate?", 181 | default: false, 182 | }); 183 | 184 | let maxBillingRate: number | undefined; 185 | if (hasMaxBillingRate) { 186 | maxBillingRate = await number({ 187 | message: "Maximum billing rate:", 188 | required: true, 189 | }); 190 | } 191 | 192 | const excludeRegex = await input({ 193 | message: "Exclude regex pattern (optional):", 194 | default: "", 195 | }); 196 | 197 | // Generate a new token 198 | const token = generateToken(); 199 | const kvKey = getKVKey(token); 200 | 201 | const subInfo: ClashSubInformationCLI = { 202 | token: token, 203 | label: label, 204 | url: url, 205 | filter: { 206 | label: filterLabel, 207 | regions: regions.length > 0 ? regions : undefined, 208 | maxBillingRate: maxBillingRate, 209 | excludeRegex: excludeRegex || undefined, 210 | }, 211 | }; 212 | 213 | const updatedAt = Date.now(); 214 | await kvPut(kvKey, JSON.stringify(subInfo), { updatedAt }); 215 | 216 | console.log("\n✨ Successfully added subscription!"); 217 | logSubInfo(subInfo, kvKey); 218 | console.log("\n💡 Tip: You can retrieve the token anytime using 'get' command."); 219 | } 220 | ) 221 | 222 | // Get subscription info 223 | .command( 224 | "get ", 225 | "Get subscription information", 226 | (yargs) => { 227 | return yargs.positional("token", { 228 | describe: "User token (sk-xxxx format)", 229 | type: "string", 230 | }); 231 | }, 232 | async (argv) => { 233 | const token = argv.token as string; 234 | const kvKey = getKVKey(token); 235 | 236 | const value = await kvGet(kvKey); 237 | if (!value) { 238 | console.error(`❌ No subscription found for token: ${token}`); 239 | console.error(` KV Key: ${kvKey}`); 240 | process.exit(1); 241 | } 242 | 243 | try { 244 | const subInfo: ClashSubInformationCLI = JSON.parse(value); 245 | logSubInfo(subInfo, kvKey); 246 | } catch (error: any) { 247 | console.error(`❌ Failed to parse subscription data: ${error.message}`); 248 | console.error(` The data in KV might be corrupted.`); 249 | process.exit(1); 250 | } 251 | } 252 | ) 253 | 254 | // Get subscription link 255 | .command( 256 | "link ", 257 | "Get subscription link and optionally open in Clash", 258 | (yargs) => { 259 | return yargs 260 | .positional("token", { 261 | describe: "User token (sk-xxxx format)", 262 | type: "string", 263 | }) 264 | .option("base-url", { 265 | alias: "b", 266 | describe: "Base URL of your deployed worker", 267 | type: "string", 268 | default: "https://clash.jctaoo.site", 269 | }) 270 | .option("go", { 271 | alias: "g", 272 | describe: "Generate and open Clash URL scheme", 273 | type: "boolean", 274 | default: false, 275 | }); 276 | }, 277 | async (argv) => { 278 | const token = argv.token as string; 279 | const baseUrl = (argv["base-url"] as string); 280 | const shouldGo = argv.go as boolean; 281 | const kvKey = getKVKey(token); 282 | 283 | const value = await kvGet(kvKey); 284 | if (!value) { 285 | console.error(`❌ No subscription found for token: ${token}`); 286 | console.error(` KV Key: ${kvKey}`); 287 | process.exit(1); 288 | } 289 | 290 | try { 291 | const subInfo: ClashSubInformationCLI = JSON.parse(value); 292 | 293 | // Generate subscription link 294 | const normalizedBaseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash 295 | const subLink = `${normalizedBaseUrl}/${token}`; 296 | 297 | console.log(`\n📎 Subscription Link:`); 298 | console.log(` ${subLink}`); 299 | console.log(`\n🏷️ Label: ${subInfo.label}`); 300 | 301 | // Generate and optionally open Clash URL scheme 302 | if (shouldGo) { 303 | const encodedUrl = encodeURIComponent(subLink); 304 | const clashUrlScheme = `clash://install-config?url=${encodedUrl}`; 305 | 306 | console.log(`\n🚀 Opening Clash URL scheme...`); 307 | console.log(` ${clashUrlScheme}`); 308 | 309 | try { 310 | // Detect platform and use appropriate command 311 | const platform = process.platform; 312 | let openCommand: string; 313 | 314 | if (platform === "win32") { 315 | openCommand = `start "" "${clashUrlScheme}"`; 316 | } else if (platform === "darwin") { 317 | openCommand = `open "${clashUrlScheme}"`; 318 | } else { 319 | openCommand = `xdg-open "${clashUrlScheme}"`; 320 | } 321 | 322 | await execAsync(openCommand); 323 | console.log(`\n✅ Successfully opened Clash URL scheme!`); 324 | } catch (error: any) { 325 | console.error(`\n❌ Failed to open URL scheme: ${error.message}`); 326 | console.error(` Please manually open the URL in your Clash app.`); 327 | } 328 | } 329 | } catch (error: any) { 330 | console.error(`❌ Failed to parse subscription data: ${error.message}`); 331 | console.error(` The data in KV might be corrupted.`); 332 | process.exit(1); 333 | } 334 | } 335 | ) 336 | 337 | // Update subscription 338 | .command( 339 | "update ", 340 | "Update an existing subscription (opens editor)", 341 | (yargs) => { 342 | return yargs.positional("token", { 343 | describe: "User token (sk-xxxx format)", 344 | type: "string", 345 | }); 346 | }, 347 | async (argv) => { 348 | const token = argv.token as string; 349 | const kvKey = getKVKey(token); 350 | 351 | // Get existing subscription 352 | const existingValue = await kvGet(kvKey); 353 | if (!existingValue) { 354 | console.error(`❌ No subscription found for token: ${token}`); 355 | console.error(` KV Key: ${kvKey}`); 356 | process.exit(1); 357 | } 358 | 359 | let existingSubInfo: ClashSubInformationCLI; 360 | try { 361 | existingSubInfo = JSON.parse(existingValue); 362 | } catch (error: any) { 363 | console.error(`❌ Failed to parse existing subscription data: ${error.message}`); 364 | console.error(` The data in KV might be corrupted.`); 365 | process.exit(1); 366 | } 367 | 368 | console.log("\n✏️ Opening editor to update subscription..."); 369 | console.log("💡 Tip: Modify the JSON and save to update the subscription."); 370 | console.log("⚠️ Note: The 'token' field is read-only and cannot be changed.\n"); 371 | 372 | // Create a clean version for editing (without content field to keep it clean) 373 | const editableVersion = { 374 | token: existingSubInfo.token, 375 | label: existingSubInfo.label, 376 | url: existingSubInfo.url, 377 | filter: { 378 | label: existingSubInfo.filter.label, 379 | regions: existingSubInfo.filter.regions || [], 380 | maxBillingRate: existingSubInfo.filter.maxBillingRate, 381 | excludeRegex: existingSubInfo.filter.excludeRegex, 382 | }, 383 | }; 384 | 385 | const editedContent = await editor({ 386 | message: "Edit subscription (JSON format):", 387 | default: JSON.stringify(editableVersion, null, 2), 388 | waitForUseInput: false, 389 | postfix: ".json", 390 | validate: (value) => { 391 | try { 392 | const parsedContent = JSON.parse(value); 393 | 394 | // Validate required fields 395 | if (!parsedContent.label || typeof parsedContent.label !== "string") { 396 | return "Field 'label' is required and must be a string"; 397 | } 398 | if (!parsedContent.url || typeof parsedContent.url !== "string") { 399 | return "Field 'url' is required and must be a string"; 400 | } 401 | if (!parsedContent.filter || typeof parsedContent.filter !== "object") { 402 | return "Field 'filter' is required and must be an object"; 403 | } 404 | if (!parsedContent.filter.label || typeof parsedContent.filter.label !== "string") { 405 | return "Field 'filter.label' is required and must be a string"; 406 | } 407 | 408 | return true; 409 | } catch (error: any) { 410 | return `Invalid JSON: ${error.message}`; 411 | } 412 | }, 413 | }); 414 | 415 | const parsedContent = JSON.parse(editedContent); 416 | 417 | // Build updated subscription info 418 | const updatedSubInfo: ClashSubInformationCLI = { 419 | token: existingSubInfo.token, // Always keep the original token 420 | label: parsedContent.label, 421 | url: parsedContent.url, 422 | filter: { 423 | label: parsedContent.filter.label, 424 | regions: Array.isArray(parsedContent.filter.regions) && parsedContent.filter.regions.length > 0 425 | ? parsedContent.filter.regions 426 | : undefined, 427 | maxBillingRate: parsedContent.filter.maxBillingRate || undefined, 428 | excludeRegex: parsedContent.filter.excludeRegex || undefined, 429 | }, 430 | }; 431 | 432 | const updatedAt = Date.now(); 433 | await kvPut(kvKey, JSON.stringify(updatedSubInfo), { updatedAt }); 434 | console.log(`\n✅ Successfully updated subscription!`); 435 | logSubInfo(updatedSubInfo, kvKey); 436 | } 437 | ) 438 | 439 | // Delete subscription 440 | .command( 441 | "delete ", 442 | "Delete a subscription", 443 | (yargs) => { 444 | return yargs.positional("token", { 445 | describe: "User token (sk-xxxx format)", 446 | type: "string", 447 | }); 448 | }, 449 | async (argv) => { 450 | const token = argv.token as string; 451 | const kvKey = getKVKey(token); 452 | 453 | await kvDelete(kvKey); 454 | console.log(`\n🗑️ Successfully deleted subscription for token: ${token}`); 455 | } 456 | ) 457 | 458 | // List all subscriptions 459 | .command( 460 | "list", 461 | "List all subscriptions", 462 | () => {}, 463 | async () => { 464 | const keys = await kvList("kv:"); 465 | 466 | if (keys.length === 0) { 467 | console.log("\n📭 No subscriptions found."); 468 | return; 469 | } 470 | 471 | console.log(`\n📋 Found ${keys.length} subscription(s):`); 472 | 473 | for (const key of keys) { 474 | try { 475 | const value = await kvGet(key.name); 476 | if (value) { 477 | try { 478 | const subInfo: ClashSubInformationCLI = JSON.parse(value); 479 | logSubInfo(subInfo, key.name); 480 | } catch (parseError: any) { 481 | console.error(`\n❌ Error parsing ${key.name}: Invalid JSON data`); 482 | } 483 | } 484 | } catch (error: any) { 485 | console.error(`\n❌ Error reading ${key.name}: ${error.message}`); 486 | } 487 | } 488 | } 489 | ) 490 | 491 | .demandCommand(1, "You need to specify a command") 492 | .help() 493 | .alias("help", "h") 494 | .version() 495 | .alias("version", "v") 496 | .strict() 497 | .parse(); 498 | 499 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clash-config", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev:bun": "bun run --hot src/index.ts", 7 | "dev": "wrangler dev --ip 0.0.0.0", 8 | "deploy": "wrangler deploy --minify", 9 | "cf-typegen": "wrangler types --env-interface CloudflareBindings", 10 | "pb-gen": "pbjs -t static-module -w es6 -o ./src/geo/geo.js ./src/geo/geo.proto", 11 | "pb-gen-dts": "pbts -o ./src/geo/geo.d.ts ./src/geo/geo.js", 12 | "cli": "bun run cli.ts", 13 | "wrangler": "wrangler" 14 | }, 15 | "dependencies": { 16 | "@inquirer/prompts": "^7.8.6", 17 | "@types/yargs": "^17.0.33", 18 | "hono": "^4.9.9", 19 | "protobufjs": "^7.5.4", 20 | "yaml": "^2.8.1", 21 | "yargs": "^18.0.0" 22 | }, 23 | "devDependencies": { 24 | "protobufjs-cli": "^1.1.3", 25 | "wrangler": "^4.4.0" 26 | } 27 | } -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | 3 | import { env } from "cloudflare:workers"; 4 | import { ClashSubInformation } from "./sub"; 5 | 6 | type AnyJson = Record; 7 | type ConfigVariant = "stash" | "mihomo"; 8 | 9 | // 额外自定义规则 10 | const customRules: string[] = [ 11 | // extra china site 12 | "DOMAIN-SUFFIX,aihubmix.com,🇨🇳 国内网站", 13 | ]; 14 | 15 | // prettier-ignore 16 | const REGIONS = [ 17 | { id: "hk", name: "香港", regexes: [/\bHK\b/i, /香港/i, /hong\s*kong/i], emoji: "🇭🇰" }, 18 | { id: "mo", name: "澳门", regexes: [/\bMO\b/i, /澳門|澳门/i, /macao|macau/i], emoji: "🇲🇴" }, 19 | { id: "jp", name: "日本", regexes: [/\bJP\b/i, /日本|japan/i, /tokyo|osaka|nagoya/i], emoji: "🇯🇵" }, 20 | { id: "tw", name: "台湾", regexes: [/\bTW\b/i, /台灣|台湾|taiwan/i, /taipei|taichung|kaohsiung/i], emoji: "🇹🇼" }, 21 | { id: "sg", name: "新加坡", regexes: [/\bSG\b/i, /新加坡|singapore/i], emoji: "🇸🇬" }, 22 | { id: "us", name: "美国", regexes: [/\bUS\b|\bUSA\b/i, /美国|united\s*states|america/i, /los\s*angeles|san\s*francisco|new\s*york|seattle|chicago|dallas|miami/i], emoji: "🇺🇸" }, 23 | { id: "gb", name: "英国", regexes: [/\bUK\b/i, /英国|united\s*kingdom|london/i], emoji: "🇬🇧" }, 24 | { id: "de", name: "德国", regexes: [/\bDE\b/i, /德国|germany|frankfurt|munich|berlin/i], emoji: "🇩🇪" }, 25 | { id: "fr", name: "法国", regexes: [/\bFR\b/i, /法国|france|paris/i], emoji: "🇫🇷" }, 26 | { id: "nl", name: "荷兰", regexes: [/\bNL\b/i, /荷兰|netherlands|amsterdam/i], emoji: "🇳🇱" }, 27 | { id: "kr", name: "韩国", regexes: [/\bKR\b/i, /韩国|korea|seoul/i], emoji: "🇰🇷" }, 28 | { id: "au", name: "澳大利亚", regexes: [/\bAU\b/i, /澳大利亚|australia|sydney|melbourne/i], emoji: "🇦🇺" }, 29 | { id: "ca", name: "加拿大", regexes: [/\bCA\b/i, /加拿大|canada|toronto|vancouver|montreal/i], emoji: "🇨🇦" }, 30 | 31 | // { id: "my", name: "马来西亚", regexes: [/\bMY\b/i, /马来西亚|malaysia/i], emoji: "🇲🇾" }, 32 | // { id: "th", name: "泰国", regexes: [/\bTH\b/i, /泰国|thailand/i], emoji: "🇹🇭" }, 33 | 34 | // 可继续加入更多国家... 35 | ]; 36 | const UNKNOWN_REGION = { 37 | name: "未知", 38 | id: "unknown", 39 | emoji: "🏳️", 40 | }; 41 | 42 | function normalizeName(name: string): string { 43 | if (!name || typeof name !== "string") return ""; 44 | // 删除 emoji(简单方式:剔除高位 unicode,这里做基本处理) 45 | // NOTE: 这不是 100% 完整的 emoji 移除,但对常见 emoji 有效 46 | const noEmoji = name.replace(/[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, ""); 47 | // 把特殊竖线/分隔符/中文标点换成空格,合并多空格,trim,转小写 48 | return noEmoji 49 | .replace(/[/|丨\|·••—–_,,。::\-]+/g, " ") 50 | .replace(/[^\w\s\u4e00-\u9fa5\-]/g, " ") // 保留中文字符、字母数字、下划线、短横 51 | .replace(/\s+/g, " ") 52 | .trim() 53 | .toLowerCase(); 54 | } 55 | 56 | function generalConfig() { 57 | return { 58 | "allow-lan": true, 59 | "bind-address": "*", 60 | mode: "rule", 61 | profile: { 62 | "store-selected": true, 63 | "store-fake-ip": true, 64 | }, 65 | // 开启统一延迟时,会计算 RTT,以消除连接握手等带来的不同类型节点的延迟差异 66 | "unified-delay": true, 67 | // 启用 TCP 并发连接,将会使用 dns 解析出的所有 IP 地址进行连接,使用第一个成功的连接 68 | "tcp-concurrent": true, 69 | // geo 70 | "geodata-loader": "standard", 71 | "geo-auto-update": true, 72 | "geo-update-interval": 24, // 更新间隔,单位为小时 73 | "geox-url": { 74 | geoip: env.geoip, 75 | geosite: env.geosite, 76 | mmdb: env.mmdb, 77 | asn: env.asn, 78 | }, 79 | "geodata-mode": true, // meta 80 | }; 81 | } 82 | 83 | /** 84 | * @param {boolean} conservative 使用保守配置,默认不使用, 保守配置适用于 stash 85 | * @param {string[]} extraFakeIpFilters 额外的 fake-ip-filter 条目 86 | */ 87 | function dnsConfig(conservative = false, extraFakeIpFilters: string[] = []) { 88 | // 默认 DNS, 用于解析 DNS 服务器 的域名 89 | const defaultDNS = ["tls://223.5.5.5"]; 90 | 91 | const chinaDNS = ["223.5.5.5", "119.29.29.29"]; 92 | const chinaDoH = [ 93 | "https://223.5.5.5/dns-query", // 阿里DoH 94 | "https://doh.pub/dns-query", // 腾讯DoH,因腾讯云即将关闭免费版IP访问,故用域名 95 | ]; 96 | 97 | const foreignDNS = [ 98 | "https://cloudflare-dns.com/dns-query", // CloudflareDNS 99 | "https://77.88.8.8/dns-query", //YandexDNS 100 | "https://8.8.4.4/dns-query#ecs=1.1.1.1/24&ecs-override=true", // GoogleDNS 101 | "https://208.67.222.222/dns-query#ecs=1.1.1.1/24&ecs-override=true", // OpenDNS 102 | "https://9.9.9.9/dns-query", //Quad9DNS 103 | ]; 104 | 105 | /** 106 | * DNS相关配置 107 | */ 108 | return { 109 | enable: true, 110 | listen: ":1053", 111 | ipv6: true, 112 | "enhanced-mode": "fake-ip", 113 | "fake-ip-range": "198.18.0.1/16", 114 | "fake-ip-filter": [ 115 | "*", 116 | "+.lan", 117 | "+.local", 118 | // clash premium 不支持 geosite 配置 119 | ...(conservative ? [] : ["geosite:connectivity-check", "geosite:private"]), 120 | // 额外: 微信快速登录检测失败 (private, 与 connectivity check 不包含) 121 | "localhost.work.weixin.qq.com", 122 | 123 | // 额外 124 | ...extraFakeIpFilters, 125 | ], 126 | "default-nameserver": [...defaultDNS], 127 | nameserver: conservative ? chinaDoH : foreignDNS, 128 | ...(conservative 129 | ? {} 130 | : // prettier-ignore 131 | { 132 | "prefer-h3": true, 133 | "use-hosts": true, 134 | "use-system-hosts": true, 135 | // 代理节点域名解析服务器,仅用于解析代理节点的域名 136 | "proxy-server-nameserver": chinaDoH, 137 | "respect-rules": true, 138 | // 用于 direct 出口域名解析的 DNS 服务器 139 | "direct-nameserver": chinaDNS, 140 | "direct-nameserver-follow-policy": false, 141 | /** 142 | * 这里对域名解析进行分流 143 | * 由于默认dns是国外的了,只需要把国内ip和域名分流到国内dns 144 | * 优先于 nameserver/fallback 查询 145 | */ 146 | "nameserver-policy": { 147 | "geosite:private": "system", 148 | "geosite:cn,steam@cn,category-games@cn,microsoft@cn,apple@cn": chinaDNS, 149 | }, 150 | }), 151 | }; 152 | } 153 | 154 | function mergeConfig(config: AnyJson, patch: AnyJson) { 155 | for (const key in patch) { 156 | if (config[key] && typeof config[key] === "object") { 157 | mergeConfig(config[key], patch[key]); 158 | } else { 159 | config[key] = patch[key]; 160 | } 161 | } 162 | } 163 | 164 | function replaceConfig(config: AnyJson, patch: AnyJson) { 165 | for (const key in patch) { 166 | config[key] = patch[key]; 167 | } 168 | } 169 | 170 | function proxyGroups(proxies: AnyJson[], conservative = false) { 171 | // 代理组通用配置 172 | const groupBaseOption = { 173 | interval: 0, 174 | timeout: 3000, 175 | url: "https://www.google.com/generate_204", 176 | lazy: true, 177 | "max-failed-times": 3, 178 | hidden: false, 179 | }; 180 | 181 | function generateRuleBasedGroup(name: string, options: AnyJson) { 182 | return { 183 | ...groupBaseOption, 184 | name: name, 185 | type: "select", 186 | proxies: ["🔰 模式选择", "⚙️ 节点选择", "🔗 全局直连", ...modeNames], 187 | ...options, 188 | }; 189 | } 190 | 191 | const regionsToProxies: Record = {}; 192 | const addProxyToRegion = (regionId: string, proxy: AnyJson) => { 193 | if (!regionsToProxies[regionId]) { 194 | regionsToProxies[regionId] = []; 195 | } 196 | regionsToProxies[regionId].push(proxy); 197 | }; 198 | 199 | // handle original proxy groups 200 | for (const proxy of proxies) { 201 | const normalizedName = normalizeName(proxy.name); 202 | let matched = false; 203 | 204 | for (const region of REGIONS) { 205 | if (region.regexes.some((regex) => regex.test(normalizedName))) { 206 | addProxyToRegion(region.id, proxy); 207 | 208 | matched = true; 209 | break; 210 | } 211 | } 212 | 213 | if (!matched) { 214 | addProxyToRegion(UNKNOWN_REGION.id, proxy); 215 | } 216 | } 217 | 218 | const regionBasedGroups = Object.entries(regionsToProxies).map(([regionId, proxies]) => { 219 | const region = REGIONS.find((r) => r.id === regionId) ?? UNKNOWN_REGION; 220 | const icon = 221 | regionId === "unknown" 222 | ? "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/unknown.svg" 223 | : `https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.3.2/flags/1x1/${region.id}.svg`; 224 | 225 | return { 226 | ...groupBaseOption, 227 | name: `${region.emoji} ${region.name}节点`, 228 | type: "select", 229 | proxies: proxies.map((proxy) => proxy.name), 230 | icon: icon, 231 | }; 232 | }); 233 | 234 | // 排序 regionBasedGroups,按 alphabetically 排序,未知节点排在最后 235 | regionBasedGroups.sort((a, b) => { 236 | if (a.name === "未知节点") { 237 | return 1; 238 | } 239 | return a.name.localeCompare(b.name); 240 | }); 241 | 242 | // 通用地区节点组 243 | const regionGroupNames = regionBasedGroups.map((group) => group.name); 244 | 245 | // declare modes 246 | const modes = [ 247 | { 248 | ...groupBaseOption, 249 | name: "♻️ 延迟选优", 250 | type: "url-test", 251 | tolerance: 50, 252 | "include-all": true, 253 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/speed.svg", 254 | }, 255 | { 256 | ...groupBaseOption, 257 | name: "🚑 故障转移", 258 | type: "fallback", 259 | "include-all": true, 260 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/ambulance.svg", 261 | }, 262 | { 263 | ...groupBaseOption, 264 | name: "⚖️ 负载均衡(散列)", 265 | type: "load-balance", 266 | strategy: "consistent-hashing", 267 | "include-all": true, 268 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/merry_go.svg", 269 | }, 270 | { 271 | ...groupBaseOption, 272 | name: "☁️ 负载均衡(轮询)", 273 | type: "load-balance", 274 | strategy: "round-robin", 275 | "include-all": true, 276 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/balance.svg", 277 | }, 278 | ]; 279 | const modeNames = modes.map((mode) => mode.name); 280 | 281 | const proxyGroupsConfig = [ 282 | { 283 | ...groupBaseOption, 284 | name: "🔰 模式选择", 285 | type: "select", 286 | proxies: ["⚙️ 节点选择", ...modeNames, "🔗 全局直连"], 287 | }, 288 | { 289 | ...groupBaseOption, 290 | name: "⚙️ 节点选择", 291 | type: "select", 292 | proxies: [...regionGroupNames], 293 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/adjust.svg", 294 | }, 295 | 296 | generateRuleBasedGroup("🌍 国外媒体", { 297 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 298 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/youtube.svg", 299 | }), 300 | 301 | generateRuleBasedGroup("💸 AI Services", { 302 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 303 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/chatgpt.svg", 304 | }), 305 | 306 | generateRuleBasedGroup("💸 Google AI Services", { 307 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 308 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/google.svg", 309 | }), 310 | 311 | generateRuleBasedGroup("🪙 Bybit", { 312 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 313 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/link.svg", 314 | }), 315 | 316 | generateRuleBasedGroup("🅿️ PikPak", { 317 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 318 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/link.svg", 319 | }), 320 | 321 | generateRuleBasedGroup("📲 电报消息", { 322 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 323 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/telegram.svg", 324 | }), 325 | 326 | generateRuleBasedGroup("📢 谷歌服务", { 327 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 328 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/google.svg", 329 | }), 330 | 331 | generateRuleBasedGroup("🍎 苹果服务", { 332 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 333 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/apple.svg", 334 | }), 335 | 336 | generateRuleBasedGroup("Ⓜ️ 微软服务", { 337 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连", ...regionGroupNames], 338 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/microsoft.svg", 339 | }), 340 | { 341 | ...groupBaseOption, 342 | name: "🇨🇳 国内网站", 343 | type: "select", 344 | proxies: ["🔗 全局直连", "🔰 模式选择", "⚙️ 节点选择", ...modeNames, ...regionGroupNames], 345 | icon: "https://fastly.jsdelivr.net/gh/lipis/flag-icons@7.3.2/flags/1x1/cn.svg", 346 | }, 347 | 348 | ...regionBasedGroups, 349 | ...modes, 350 | 351 | { 352 | ...groupBaseOption, 353 | name: "🔗 全局直连", 354 | type: "select", 355 | proxies: ["DIRECT"], 356 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/link.svg", 357 | }, 358 | { 359 | ...groupBaseOption, 360 | name: "❌ 全局拦截", 361 | type: "select", 362 | proxies: ["REJECT", "DIRECT"], 363 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/block.svg", 364 | }, 365 | { 366 | ...groupBaseOption, 367 | name: "🐟 漏网之鱼", 368 | type: "select", 369 | proxies: ["🔰 模式选择", "⚙️ 节点选择", ...modeNames, "🔗 全局直连"], 370 | icon: "https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/fish.svg", 371 | }, 372 | ]; 373 | 374 | if (conservative) { 375 | // 保守模式不要设置 svg icon 376 | for (const group of proxyGroupsConfig) { 377 | if ("icon" in group && group.icon.endsWith(".svg")) { 378 | // @ts-expect-error 379 | delete group.icon; 380 | } 381 | } 382 | } 383 | 384 | return { 385 | "proxy-groups": proxyGroupsConfig, 386 | }; 387 | } 388 | 389 | function rules() { 390 | // 规则集通用配置 391 | const ruleProviderCommon = { 392 | type: "http", 393 | format: "yaml", 394 | interval: 86400, // 更新间隔,单位为秒 86400秒 = 24小时 395 | }; 396 | // 规则集配置 397 | const ruleProviders = { 398 | reject: { 399 | ...ruleProviderCommon, 400 | behavior: "domain", 401 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt", 402 | path: "./ruleset/loyalsoldier/reject.yaml", 403 | }, 404 | icloud: { 405 | ...ruleProviderCommon, 406 | behavior: "domain", 407 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/icloud.txt", 408 | path: "./ruleset/loyalsoldier/icloud.yaml", 409 | }, 410 | apple: { 411 | ...ruleProviderCommon, 412 | behavior: "domain", 413 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/apple.txt", 414 | path: "./ruleset/loyalsoldier/apple.yaml", 415 | }, 416 | google: { 417 | ...ruleProviderCommon, 418 | behavior: "domain", 419 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt", 420 | path: "./ruleset/loyalsoldier/google.yaml", 421 | }, 422 | proxy: { 423 | ...ruleProviderCommon, 424 | behavior: "domain", 425 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/proxy.txt", 426 | path: "./ruleset/loyalsoldier/proxy.yaml", 427 | }, 428 | direct: { 429 | ...ruleProviderCommon, 430 | behavior: "domain", 431 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt", 432 | path: "./ruleset/loyalsoldier/direct.yaml", 433 | }, 434 | gfw: { 435 | ...ruleProviderCommon, 436 | behavior: "domain", 437 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/gfw.txt", 438 | path: "./ruleset/loyalsoldier/gfw.yaml", 439 | }, 440 | "tld-not-cn": { 441 | ...ruleProviderCommon, 442 | behavior: "domain", 443 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/tld-not-cn.txt", 444 | path: "./ruleset/loyalsoldier/tld-not-cn.yaml", 445 | }, 446 | telegramcidr: { 447 | ...ruleProviderCommon, 448 | behavior: "ipcidr", 449 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/telegramcidr.txt", 450 | path: "./ruleset/loyalsoldier/telegramcidr.yaml", 451 | }, 452 | cncidr: { 453 | ...ruleProviderCommon, 454 | behavior: "ipcidr", 455 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt", 456 | path: "./ruleset/loyalsoldier/cncidr.yaml", 457 | }, 458 | lancidr: { 459 | ...ruleProviderCommon, 460 | behavior: "ipcidr", 461 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/lancidr.txt", 462 | path: "./ruleset/loyalsoldier/lancidr.yaml", 463 | }, 464 | applications: { 465 | ...ruleProviderCommon, 466 | behavior: "classical", 467 | url: "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/applications.txt", 468 | path: "./ruleset/loyalsoldier/applications.yaml", 469 | }, 470 | openai: { 471 | ...ruleProviderCommon, 472 | behavior: "classical", 473 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/openai.yaml", 474 | path: "./ruleset/MetaCubeX/openai.yaml", 475 | }, 476 | bybit: { 477 | ...ruleProviderCommon, 478 | behavior: "classical", 479 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/bybit.yaml", 480 | path: "./ruleset/MetaCubeX/bybit.yaml", 481 | }, 482 | pikpak: { 483 | ...ruleProviderCommon, 484 | behavior: "classical", 485 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/pikpak.yaml", 486 | path: "./ruleset/MetaCubeX/pikpak.yaml", 487 | }, 488 | anthropic: { 489 | ...ruleProviderCommon, 490 | behavior: "classical", 491 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/anthropic.yaml", 492 | path: "./ruleset/MetaCubeX/anthropic.yaml", 493 | }, 494 | "google-gemini": { 495 | ...ruleProviderCommon, 496 | behavior: "classical", 497 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/google-gemini.yaml", 498 | path: "./ruleset/MetaCubeX/google-gemini.yaml", 499 | }, 500 | xai: { 501 | ...ruleProviderCommon, 502 | behavior: "classical", 503 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/xai.yaml", 504 | path: "./ruleset/MetaCubeX/xai.yaml", 505 | }, 506 | perplexity: { 507 | ...ruleProviderCommon, 508 | behavior: "classical", 509 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/perplexity.yaml", 510 | path: "./ruleset/MetaCubeX/perplexity.yaml", 511 | }, 512 | microsoft: { 513 | ...ruleProviderCommon, 514 | behavior: "classical", 515 | url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/refs/heads/meta/geo/geosite/classical/microsoft.yaml", 516 | path: "./ruleset/MetaCubeX/microsoft.yaml", 517 | }, 518 | }; 519 | // 规则 520 | const rules = [ 521 | // 额外自定义规则 522 | ...customRules, 523 | 524 | // MetaCubeX 规则集 525 | "RULE-SET,openai,💸 AI Services", 526 | "RULE-SET,pikpak,🅿️ PikPak", 527 | "RULE-SET,bybit,🪙 Bybit", 528 | "RULE-SET,anthropic,💸 AI Services", 529 | "RULE-SET,google-gemini,💸 Google AI Services", 530 | "RULE-SET,xai,💸 AI Services", 531 | "RULE-SET,perplexity,💸 AI Services", 532 | // Geo Site 规则集 533 | "GEOSITE,microsoft@cn,🇨🇳 国内网站", 534 | "GEOSITE,apple@cn,🇨🇳 国内网站", 535 | "GEOSITE,category-games@cn,🇨🇳 国内网站", 536 | // Loyalsoldier 规则集 537 | "RULE-SET,applications,🔗 全局直连", 538 | // "RULE-SET,reject,🥰 广告过滤", 539 | "RULE-SET,microsoft,Ⓜ️ 微软服务", 540 | "RULE-SET,icloud,🍎 苹果服务", 541 | "RULE-SET,apple,🍎 苹果服务", 542 | "RULE-SET,google,📢 谷歌服务", 543 | "RULE-SET,proxy,🔰 模式选择", 544 | "RULE-SET,gfw,🔰 模式选择", 545 | // 非中国大陆使用的顶级域名,比如 .ai 546 | // "RULE-SET,tld-not-cn,🔰 模式选择", 547 | // other 548 | "RULE-SET,direct,🇨🇳 国内网站", 549 | "GEOSITE,private,🔗 全局直连", 550 | "RULE-SET,lancidr,🔗 全局直连,no-resolve", 551 | "RULE-SET,cncidr,🇨🇳 国内网站,no-resolve", 552 | "RULE-SET,telegramcidr,📲 电报消息,no-resolve", 553 | // 其他规则 554 | "GEOIP,private,🔗 全局直连,no-resolve", 555 | "GEOIP,LAN,🔗 全局直连,no-resolve", 556 | "GEOIP,CN,🇨🇳 国内网站,no-resolve", 557 | "MATCH,🐟 漏网之鱼", 558 | ]; 559 | 560 | return { 561 | rules: rules, 562 | "rule-providers": ruleProviders, 563 | }; 564 | } 565 | 566 | function filterNodes(cfg: AnyJson, filter: ClashSubInformation["filter"]) { 567 | const { regions = [], maxBillingRate, excludeRegex } = filter; 568 | 569 | const lowerCasedRegions = regions.map((region) => region.toLowerCase()); 570 | 571 | // filter by regions 572 | if (regions.length > 0) { 573 | cfg.proxies = cfg.proxies.filter((proxy: AnyJson) => { 574 | const normalizedName = normalizeName(proxy.name); 575 | const region = REGIONS.find((region) => { 576 | return region.regexes.some((regex) => regex.test(normalizedName)); 577 | }); 578 | 579 | return region && lowerCasedRegions.includes(region.id.toLowerCase()); 580 | }); 581 | } 582 | 583 | // filter by maxBillingRate 584 | if (maxBillingRate) { 585 | // E.G. 🇭🇰 香港游戏丨2x HK --> 计费倍率为 2 586 | cfg.proxies = cfg.proxies.filter((proxy: AnyJson) => { 587 | const normalizedName = normalizeName(proxy.name); 588 | 589 | // const [m1, m2] = [ 590 | // /(?<=[xX✕✖⨉倍率])([1-9]+(\.\d+)*|0{1}\.\d+)(?=[xX✕✖⨉倍率])*/i, 591 | // /(?<=[xX✕✖⨉倍率]?)([1-9]+(\.\d+)*|0{1}\.\d+)(?=[xX✕✖⨉倍率])/i, 592 | // ] 593 | const m1 = /(?:(?<=[xX✕✖⨉倍率])([1-9]\d*(?:\.\d+)?|0\.\d+)|([1-9]\d*(?:\.\d+)?|0\.\d+)(?=[xX✕✖⨉倍率]))/i; 594 | const match = m1.exec(normalizedName); 595 | 596 | const multiplier = match?.[1] ?? match?.[2] ?? "0"; 597 | return parseFloat(multiplier) <= maxBillingRate; 598 | }) 599 | } 600 | 601 | // filter by excludeRegex 602 | if (excludeRegex) { 603 | cfg.proxies = cfg.proxies.filter((proxy: AnyJson) => { 604 | const normalizedName = normalizeName(proxy.name); 605 | const regex = new RegExp(excludeRegex); 606 | return !regex.test(normalizedName); 607 | }); 608 | } 609 | } 610 | 611 | export function convertClashConfig(options: { 612 | config: AnyJson; 613 | profile: string; 614 | variant: ConfigVariant; 615 | extra: { 616 | fakeIpFilters?: string[]; 617 | }; 618 | filter?: ClashSubInformation["filter"]; 619 | }): AnyJson { 620 | const { config, profile, variant = "mihomo", extra, filter } = options; 621 | 622 | const conservative = variant === "stash"; 623 | 624 | // do filter 625 | if (filter) { 626 | const { label, ...rest } = filter; 627 | console.log("Do filter by label", filter.label, rest); 628 | filterNodes(config, filter); 629 | } 630 | 631 | // General Config 632 | mergeConfig(config, generalConfig()); 633 | 634 | // Config DNS 635 | config.dns = dnsConfig(conservative, extra.fakeIpFilters ?? []); 636 | 637 | // Config Proxy Groups and rules 638 | replaceConfig(config, rules()); 639 | replaceConfig(config, proxyGroups(config["proxies"], conservative)); 640 | 641 | // remove hosts 642 | delete config["hosts"]; 643 | 644 | // fix port settings 645 | delete config["port"]; 646 | delete config["socks-port"]; 647 | delete config["redir-port"]; 648 | delete config["tproxy-port"]; 649 | config["mixed-port"] = 7890; 650 | 651 | return config; 652 | } 653 | -------------------------------------------------------------------------------- /src/geo/geo.d.ts: -------------------------------------------------------------------------------- 1 | import * as $protobuf from "protobufjs"; 2 | import Long = require("long"); 3 | /** Namespace mihomo. */ 4 | export namespace mihomo { 5 | 6 | /** Namespace component. */ 7 | namespace component { 8 | 9 | /** Namespace geodata. */ 10 | namespace geodata { 11 | 12 | /** Namespace router. */ 13 | namespace router { 14 | 15 | /** Properties of a Domain. */ 16 | interface IDomain { 17 | 18 | /** Domain type */ 19 | type?: (mihomo.component.geodata.router.Domain.Type|null); 20 | 21 | /** Domain value */ 22 | value?: (string|null); 23 | 24 | /** Domain attribute */ 25 | attribute?: (mihomo.component.geodata.router.Domain.IAttribute[]|null); 26 | } 27 | 28 | /** Represents a Domain. */ 29 | class Domain implements IDomain { 30 | 31 | /** 32 | * Constructs a new Domain. 33 | * @param [properties] Properties to set 34 | */ 35 | constructor(properties?: mihomo.component.geodata.router.IDomain); 36 | 37 | /** Domain type. */ 38 | public type: mihomo.component.geodata.router.Domain.Type; 39 | 40 | /** Domain value. */ 41 | public value: string; 42 | 43 | /** Domain attribute. */ 44 | public attribute: mihomo.component.geodata.router.Domain.IAttribute[]; 45 | 46 | /** 47 | * Creates a new Domain instance using the specified properties. 48 | * @param [properties] Properties to set 49 | * @returns Domain instance 50 | */ 51 | public static create(properties?: mihomo.component.geodata.router.IDomain): mihomo.component.geodata.router.Domain; 52 | 53 | /** 54 | * Encodes the specified Domain message. Does not implicitly {@link mihomo.component.geodata.router.Domain.verify|verify} messages. 55 | * @param message Domain message or plain object to encode 56 | * @param [writer] Writer to encode to 57 | * @returns Writer 58 | */ 59 | public static encode(message: mihomo.component.geodata.router.IDomain, writer?: $protobuf.Writer): $protobuf.Writer; 60 | 61 | /** 62 | * Encodes the specified Domain message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.Domain.verify|verify} messages. 63 | * @param message Domain message or plain object to encode 64 | * @param [writer] Writer to encode to 65 | * @returns Writer 66 | */ 67 | public static encodeDelimited(message: mihomo.component.geodata.router.IDomain, writer?: $protobuf.Writer): $protobuf.Writer; 68 | 69 | /** 70 | * Decodes a Domain message from the specified reader or buffer. 71 | * @param reader Reader or buffer to decode from 72 | * @param [length] Message length if known beforehand 73 | * @returns Domain 74 | * @throws {Error} If the payload is not a reader or valid buffer 75 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 76 | */ 77 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.Domain; 78 | 79 | /** 80 | * Decodes a Domain message from the specified reader or buffer, length delimited. 81 | * @param reader Reader or buffer to decode from 82 | * @returns Domain 83 | * @throws {Error} If the payload is not a reader or valid buffer 84 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 85 | */ 86 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.Domain; 87 | 88 | /** 89 | * Verifies a Domain message. 90 | * @param message Plain object to verify 91 | * @returns `null` if valid, otherwise the reason why it is not 92 | */ 93 | public static verify(message: { [k: string]: any }): (string|null); 94 | 95 | /** 96 | * Creates a Domain message from a plain object. Also converts values to their respective internal types. 97 | * @param object Plain object 98 | * @returns Domain 99 | */ 100 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.Domain; 101 | 102 | /** 103 | * Creates a plain object from a Domain message. Also converts values to other types if specified. 104 | * @param message Domain 105 | * @param [options] Conversion options 106 | * @returns Plain object 107 | */ 108 | public static toObject(message: mihomo.component.geodata.router.Domain, options?: $protobuf.IConversionOptions): { [k: string]: any }; 109 | 110 | /** 111 | * Converts this Domain to JSON. 112 | * @returns JSON object 113 | */ 114 | public toJSON(): { [k: string]: any }; 115 | 116 | /** 117 | * Gets the default type url for Domain 118 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 119 | * @returns The default type url 120 | */ 121 | public static getTypeUrl(typeUrlPrefix?: string): string; 122 | } 123 | 124 | namespace Domain { 125 | 126 | /** Type enum. */ 127 | enum Type { 128 | Plain = 0, 129 | Regex = 1, 130 | Domain = 2, 131 | Full = 3 132 | } 133 | 134 | /** Properties of an Attribute. */ 135 | interface IAttribute { 136 | 137 | /** Attribute key */ 138 | key?: (string|null); 139 | 140 | /** Attribute boolValue */ 141 | boolValue?: (boolean|null); 142 | 143 | /** Attribute intValue */ 144 | intValue?: (number|Long|null); 145 | } 146 | 147 | /** Represents an Attribute. */ 148 | class Attribute implements IAttribute { 149 | 150 | /** 151 | * Constructs a new Attribute. 152 | * @param [properties] Properties to set 153 | */ 154 | constructor(properties?: mihomo.component.geodata.router.Domain.IAttribute); 155 | 156 | /** Attribute key. */ 157 | public key: string; 158 | 159 | /** Attribute boolValue. */ 160 | public boolValue?: (boolean|null); 161 | 162 | /** Attribute intValue. */ 163 | public intValue?: (number|Long|null); 164 | 165 | /** Attribute typedValue. */ 166 | public typedValue?: ("boolValue"|"intValue"); 167 | 168 | /** 169 | * Creates a new Attribute instance using the specified properties. 170 | * @param [properties] Properties to set 171 | * @returns Attribute instance 172 | */ 173 | public static create(properties?: mihomo.component.geodata.router.Domain.IAttribute): mihomo.component.geodata.router.Domain.Attribute; 174 | 175 | /** 176 | * Encodes the specified Attribute message. Does not implicitly {@link mihomo.component.geodata.router.Domain.Attribute.verify|verify} messages. 177 | * @param message Attribute message or plain object to encode 178 | * @param [writer] Writer to encode to 179 | * @returns Writer 180 | */ 181 | public static encode(message: mihomo.component.geodata.router.Domain.IAttribute, writer?: $protobuf.Writer): $protobuf.Writer; 182 | 183 | /** 184 | * Encodes the specified Attribute message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.Domain.Attribute.verify|verify} messages. 185 | * @param message Attribute message or plain object to encode 186 | * @param [writer] Writer to encode to 187 | * @returns Writer 188 | */ 189 | public static encodeDelimited(message: mihomo.component.geodata.router.Domain.IAttribute, writer?: $protobuf.Writer): $protobuf.Writer; 190 | 191 | /** 192 | * Decodes an Attribute message from the specified reader or buffer. 193 | * @param reader Reader or buffer to decode from 194 | * @param [length] Message length if known beforehand 195 | * @returns Attribute 196 | * @throws {Error} If the payload is not a reader or valid buffer 197 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 198 | */ 199 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.Domain.Attribute; 200 | 201 | /** 202 | * Decodes an Attribute message from the specified reader or buffer, length delimited. 203 | * @param reader Reader or buffer to decode from 204 | * @returns Attribute 205 | * @throws {Error} If the payload is not a reader or valid buffer 206 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 207 | */ 208 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.Domain.Attribute; 209 | 210 | /** 211 | * Verifies an Attribute message. 212 | * @param message Plain object to verify 213 | * @returns `null` if valid, otherwise the reason why it is not 214 | */ 215 | public static verify(message: { [k: string]: any }): (string|null); 216 | 217 | /** 218 | * Creates an Attribute message from a plain object. Also converts values to their respective internal types. 219 | * @param object Plain object 220 | * @returns Attribute 221 | */ 222 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.Domain.Attribute; 223 | 224 | /** 225 | * Creates a plain object from an Attribute message. Also converts values to other types if specified. 226 | * @param message Attribute 227 | * @param [options] Conversion options 228 | * @returns Plain object 229 | */ 230 | public static toObject(message: mihomo.component.geodata.router.Domain.Attribute, options?: $protobuf.IConversionOptions): { [k: string]: any }; 231 | 232 | /** 233 | * Converts this Attribute to JSON. 234 | * @returns JSON object 235 | */ 236 | public toJSON(): { [k: string]: any }; 237 | 238 | /** 239 | * Gets the default type url for Attribute 240 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 241 | * @returns The default type url 242 | */ 243 | public static getTypeUrl(typeUrlPrefix?: string): string; 244 | } 245 | } 246 | 247 | /** Properties of a CIDR. */ 248 | interface ICIDR { 249 | 250 | /** CIDR ip */ 251 | ip?: (Uint8Array|null); 252 | 253 | /** CIDR prefix */ 254 | prefix?: (number|null); 255 | } 256 | 257 | /** Represents a CIDR. */ 258 | class CIDR implements ICIDR { 259 | 260 | /** 261 | * Constructs a new CIDR. 262 | * @param [properties] Properties to set 263 | */ 264 | constructor(properties?: mihomo.component.geodata.router.ICIDR); 265 | 266 | /** CIDR ip. */ 267 | public ip: Uint8Array; 268 | 269 | /** CIDR prefix. */ 270 | public prefix: number; 271 | 272 | /** 273 | * Creates a new CIDR instance using the specified properties. 274 | * @param [properties] Properties to set 275 | * @returns CIDR instance 276 | */ 277 | public static create(properties?: mihomo.component.geodata.router.ICIDR): mihomo.component.geodata.router.CIDR; 278 | 279 | /** 280 | * Encodes the specified CIDR message. Does not implicitly {@link mihomo.component.geodata.router.CIDR.verify|verify} messages. 281 | * @param message CIDR message or plain object to encode 282 | * @param [writer] Writer to encode to 283 | * @returns Writer 284 | */ 285 | public static encode(message: mihomo.component.geodata.router.ICIDR, writer?: $protobuf.Writer): $protobuf.Writer; 286 | 287 | /** 288 | * Encodes the specified CIDR message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.CIDR.verify|verify} messages. 289 | * @param message CIDR message or plain object to encode 290 | * @param [writer] Writer to encode to 291 | * @returns Writer 292 | */ 293 | public static encodeDelimited(message: mihomo.component.geodata.router.ICIDR, writer?: $protobuf.Writer): $protobuf.Writer; 294 | 295 | /** 296 | * Decodes a CIDR message from the specified reader or buffer. 297 | * @param reader Reader or buffer to decode from 298 | * @param [length] Message length if known beforehand 299 | * @returns CIDR 300 | * @throws {Error} If the payload is not a reader or valid buffer 301 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 302 | */ 303 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.CIDR; 304 | 305 | /** 306 | * Decodes a CIDR message from the specified reader or buffer, length delimited. 307 | * @param reader Reader or buffer to decode from 308 | * @returns CIDR 309 | * @throws {Error} If the payload is not a reader or valid buffer 310 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 311 | */ 312 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.CIDR; 313 | 314 | /** 315 | * Verifies a CIDR message. 316 | * @param message Plain object to verify 317 | * @returns `null` if valid, otherwise the reason why it is not 318 | */ 319 | public static verify(message: { [k: string]: any }): (string|null); 320 | 321 | /** 322 | * Creates a CIDR message from a plain object. Also converts values to their respective internal types. 323 | * @param object Plain object 324 | * @returns CIDR 325 | */ 326 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.CIDR; 327 | 328 | /** 329 | * Creates a plain object from a CIDR message. Also converts values to other types if specified. 330 | * @param message CIDR 331 | * @param [options] Conversion options 332 | * @returns Plain object 333 | */ 334 | public static toObject(message: mihomo.component.geodata.router.CIDR, options?: $protobuf.IConversionOptions): { [k: string]: any }; 335 | 336 | /** 337 | * Converts this CIDR to JSON. 338 | * @returns JSON object 339 | */ 340 | public toJSON(): { [k: string]: any }; 341 | 342 | /** 343 | * Gets the default type url for CIDR 344 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 345 | * @returns The default type url 346 | */ 347 | public static getTypeUrl(typeUrlPrefix?: string): string; 348 | } 349 | 350 | /** Properties of a GeoIP. */ 351 | interface IGeoIP { 352 | 353 | /** GeoIP countryCode */ 354 | countryCode?: (string|null); 355 | 356 | /** GeoIP cidr */ 357 | cidr?: (mihomo.component.geodata.router.ICIDR[]|null); 358 | 359 | /** GeoIP reverseMatch */ 360 | reverseMatch?: (boolean|null); 361 | } 362 | 363 | /** Represents a GeoIP. */ 364 | class GeoIP implements IGeoIP { 365 | 366 | /** 367 | * Constructs a new GeoIP. 368 | * @param [properties] Properties to set 369 | */ 370 | constructor(properties?: mihomo.component.geodata.router.IGeoIP); 371 | 372 | /** GeoIP countryCode. */ 373 | public countryCode: string; 374 | 375 | /** GeoIP cidr. */ 376 | public cidr: mihomo.component.geodata.router.ICIDR[]; 377 | 378 | /** GeoIP reverseMatch. */ 379 | public reverseMatch: boolean; 380 | 381 | /** 382 | * Creates a new GeoIP instance using the specified properties. 383 | * @param [properties] Properties to set 384 | * @returns GeoIP instance 385 | */ 386 | public static create(properties?: mihomo.component.geodata.router.IGeoIP): mihomo.component.geodata.router.GeoIP; 387 | 388 | /** 389 | * Encodes the specified GeoIP message. Does not implicitly {@link mihomo.component.geodata.router.GeoIP.verify|verify} messages. 390 | * @param message GeoIP message or plain object to encode 391 | * @param [writer] Writer to encode to 392 | * @returns Writer 393 | */ 394 | public static encode(message: mihomo.component.geodata.router.IGeoIP, writer?: $protobuf.Writer): $protobuf.Writer; 395 | 396 | /** 397 | * Encodes the specified GeoIP message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.GeoIP.verify|verify} messages. 398 | * @param message GeoIP message or plain object to encode 399 | * @param [writer] Writer to encode to 400 | * @returns Writer 401 | */ 402 | public static encodeDelimited(message: mihomo.component.geodata.router.IGeoIP, writer?: $protobuf.Writer): $protobuf.Writer; 403 | 404 | /** 405 | * Decodes a GeoIP message from the specified reader or buffer. 406 | * @param reader Reader or buffer to decode from 407 | * @param [length] Message length if known beforehand 408 | * @returns GeoIP 409 | * @throws {Error} If the payload is not a reader or valid buffer 410 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 411 | */ 412 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.GeoIP; 413 | 414 | /** 415 | * Decodes a GeoIP message from the specified reader or buffer, length delimited. 416 | * @param reader Reader or buffer to decode from 417 | * @returns GeoIP 418 | * @throws {Error} If the payload is not a reader or valid buffer 419 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 420 | */ 421 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.GeoIP; 422 | 423 | /** 424 | * Verifies a GeoIP message. 425 | * @param message Plain object to verify 426 | * @returns `null` if valid, otherwise the reason why it is not 427 | */ 428 | public static verify(message: { [k: string]: any }): (string|null); 429 | 430 | /** 431 | * Creates a GeoIP message from a plain object. Also converts values to their respective internal types. 432 | * @param object Plain object 433 | * @returns GeoIP 434 | */ 435 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.GeoIP; 436 | 437 | /** 438 | * Creates a plain object from a GeoIP message. Also converts values to other types if specified. 439 | * @param message GeoIP 440 | * @param [options] Conversion options 441 | * @returns Plain object 442 | */ 443 | public static toObject(message: mihomo.component.geodata.router.GeoIP, options?: $protobuf.IConversionOptions): { [k: string]: any }; 444 | 445 | /** 446 | * Converts this GeoIP to JSON. 447 | * @returns JSON object 448 | */ 449 | public toJSON(): { [k: string]: any }; 450 | 451 | /** 452 | * Gets the default type url for GeoIP 453 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 454 | * @returns The default type url 455 | */ 456 | public static getTypeUrl(typeUrlPrefix?: string): string; 457 | } 458 | 459 | /** Properties of a GeoIPList. */ 460 | interface IGeoIPList { 461 | 462 | /** GeoIPList entry */ 463 | entry?: (mihomo.component.geodata.router.IGeoIP[]|null); 464 | } 465 | 466 | /** Represents a GeoIPList. */ 467 | class GeoIPList implements IGeoIPList { 468 | 469 | /** 470 | * Constructs a new GeoIPList. 471 | * @param [properties] Properties to set 472 | */ 473 | constructor(properties?: mihomo.component.geodata.router.IGeoIPList); 474 | 475 | /** GeoIPList entry. */ 476 | public entry: mihomo.component.geodata.router.IGeoIP[]; 477 | 478 | /** 479 | * Creates a new GeoIPList instance using the specified properties. 480 | * @param [properties] Properties to set 481 | * @returns GeoIPList instance 482 | */ 483 | public static create(properties?: mihomo.component.geodata.router.IGeoIPList): mihomo.component.geodata.router.GeoIPList; 484 | 485 | /** 486 | * Encodes the specified GeoIPList message. Does not implicitly {@link mihomo.component.geodata.router.GeoIPList.verify|verify} messages. 487 | * @param message GeoIPList message or plain object to encode 488 | * @param [writer] Writer to encode to 489 | * @returns Writer 490 | */ 491 | public static encode(message: mihomo.component.geodata.router.IGeoIPList, writer?: $protobuf.Writer): $protobuf.Writer; 492 | 493 | /** 494 | * Encodes the specified GeoIPList message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.GeoIPList.verify|verify} messages. 495 | * @param message GeoIPList message or plain object to encode 496 | * @param [writer] Writer to encode to 497 | * @returns Writer 498 | */ 499 | public static encodeDelimited(message: mihomo.component.geodata.router.IGeoIPList, writer?: $protobuf.Writer): $protobuf.Writer; 500 | 501 | /** 502 | * Decodes a GeoIPList message from the specified reader or buffer. 503 | * @param reader Reader or buffer to decode from 504 | * @param [length] Message length if known beforehand 505 | * @returns GeoIPList 506 | * @throws {Error} If the payload is not a reader or valid buffer 507 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 508 | */ 509 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.GeoIPList; 510 | 511 | /** 512 | * Decodes a GeoIPList message from the specified reader or buffer, length delimited. 513 | * @param reader Reader or buffer to decode from 514 | * @returns GeoIPList 515 | * @throws {Error} If the payload is not a reader or valid buffer 516 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 517 | */ 518 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.GeoIPList; 519 | 520 | /** 521 | * Verifies a GeoIPList message. 522 | * @param message Plain object to verify 523 | * @returns `null` if valid, otherwise the reason why it is not 524 | */ 525 | public static verify(message: { [k: string]: any }): (string|null); 526 | 527 | /** 528 | * Creates a GeoIPList message from a plain object. Also converts values to their respective internal types. 529 | * @param object Plain object 530 | * @returns GeoIPList 531 | */ 532 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.GeoIPList; 533 | 534 | /** 535 | * Creates a plain object from a GeoIPList message. Also converts values to other types if specified. 536 | * @param message GeoIPList 537 | * @param [options] Conversion options 538 | * @returns Plain object 539 | */ 540 | public static toObject(message: mihomo.component.geodata.router.GeoIPList, options?: $protobuf.IConversionOptions): { [k: string]: any }; 541 | 542 | /** 543 | * Converts this GeoIPList to JSON. 544 | * @returns JSON object 545 | */ 546 | public toJSON(): { [k: string]: any }; 547 | 548 | /** 549 | * Gets the default type url for GeoIPList 550 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 551 | * @returns The default type url 552 | */ 553 | public static getTypeUrl(typeUrlPrefix?: string): string; 554 | } 555 | 556 | /** Properties of a GeoSite. */ 557 | interface IGeoSite { 558 | 559 | /** GeoSite countryCode */ 560 | countryCode?: (string|null); 561 | 562 | /** GeoSite domain */ 563 | domain?: (mihomo.component.geodata.router.IDomain[]|null); 564 | } 565 | 566 | /** Represents a GeoSite. */ 567 | class GeoSite implements IGeoSite { 568 | 569 | /** 570 | * Constructs a new GeoSite. 571 | * @param [properties] Properties to set 572 | */ 573 | constructor(properties?: mihomo.component.geodata.router.IGeoSite); 574 | 575 | /** GeoSite countryCode. */ 576 | public countryCode: string; 577 | 578 | /** GeoSite domain. */ 579 | public domain: mihomo.component.geodata.router.IDomain[]; 580 | 581 | /** 582 | * Creates a new GeoSite instance using the specified properties. 583 | * @param [properties] Properties to set 584 | * @returns GeoSite instance 585 | */ 586 | public static create(properties?: mihomo.component.geodata.router.IGeoSite): mihomo.component.geodata.router.GeoSite; 587 | 588 | /** 589 | * Encodes the specified GeoSite message. Does not implicitly {@link mihomo.component.geodata.router.GeoSite.verify|verify} messages. 590 | * @param message GeoSite message or plain object to encode 591 | * @param [writer] Writer to encode to 592 | * @returns Writer 593 | */ 594 | public static encode(message: mihomo.component.geodata.router.IGeoSite, writer?: $protobuf.Writer): $protobuf.Writer; 595 | 596 | /** 597 | * Encodes the specified GeoSite message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.GeoSite.verify|verify} messages. 598 | * @param message GeoSite message or plain object to encode 599 | * @param [writer] Writer to encode to 600 | * @returns Writer 601 | */ 602 | public static encodeDelimited(message: mihomo.component.geodata.router.IGeoSite, writer?: $protobuf.Writer): $protobuf.Writer; 603 | 604 | /** 605 | * Decodes a GeoSite message from the specified reader or buffer. 606 | * @param reader Reader or buffer to decode from 607 | * @param [length] Message length if known beforehand 608 | * @returns GeoSite 609 | * @throws {Error} If the payload is not a reader or valid buffer 610 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 611 | */ 612 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.GeoSite; 613 | 614 | /** 615 | * Decodes a GeoSite message from the specified reader or buffer, length delimited. 616 | * @param reader Reader or buffer to decode from 617 | * @returns GeoSite 618 | * @throws {Error} If the payload is not a reader or valid buffer 619 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 620 | */ 621 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.GeoSite; 622 | 623 | /** 624 | * Verifies a GeoSite message. 625 | * @param message Plain object to verify 626 | * @returns `null` if valid, otherwise the reason why it is not 627 | */ 628 | public static verify(message: { [k: string]: any }): (string|null); 629 | 630 | /** 631 | * Creates a GeoSite message from a plain object. Also converts values to their respective internal types. 632 | * @param object Plain object 633 | * @returns GeoSite 634 | */ 635 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.GeoSite; 636 | 637 | /** 638 | * Creates a plain object from a GeoSite message. Also converts values to other types if specified. 639 | * @param message GeoSite 640 | * @param [options] Conversion options 641 | * @returns Plain object 642 | */ 643 | public static toObject(message: mihomo.component.geodata.router.GeoSite, options?: $protobuf.IConversionOptions): { [k: string]: any }; 644 | 645 | /** 646 | * Converts this GeoSite to JSON. 647 | * @returns JSON object 648 | */ 649 | public toJSON(): { [k: string]: any }; 650 | 651 | /** 652 | * Gets the default type url for GeoSite 653 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 654 | * @returns The default type url 655 | */ 656 | public static getTypeUrl(typeUrlPrefix?: string): string; 657 | } 658 | 659 | /** Properties of a GeoSiteList. */ 660 | interface IGeoSiteList { 661 | 662 | /** GeoSiteList entry */ 663 | entry?: (mihomo.component.geodata.router.IGeoSite[]|null); 664 | } 665 | 666 | /** Represents a GeoSiteList. */ 667 | class GeoSiteList implements IGeoSiteList { 668 | 669 | /** 670 | * Constructs a new GeoSiteList. 671 | * @param [properties] Properties to set 672 | */ 673 | constructor(properties?: mihomo.component.geodata.router.IGeoSiteList); 674 | 675 | /** GeoSiteList entry. */ 676 | public entry: mihomo.component.geodata.router.IGeoSite[]; 677 | 678 | /** 679 | * Creates a new GeoSiteList instance using the specified properties. 680 | * @param [properties] Properties to set 681 | * @returns GeoSiteList instance 682 | */ 683 | public static create(properties?: mihomo.component.geodata.router.IGeoSiteList): mihomo.component.geodata.router.GeoSiteList; 684 | 685 | /** 686 | * Encodes the specified GeoSiteList message. Does not implicitly {@link mihomo.component.geodata.router.GeoSiteList.verify|verify} messages. 687 | * @param message GeoSiteList message or plain object to encode 688 | * @param [writer] Writer to encode to 689 | * @returns Writer 690 | */ 691 | public static encode(message: mihomo.component.geodata.router.IGeoSiteList, writer?: $protobuf.Writer): $protobuf.Writer; 692 | 693 | /** 694 | * Encodes the specified GeoSiteList message, length delimited. Does not implicitly {@link mihomo.component.geodata.router.GeoSiteList.verify|verify} messages. 695 | * @param message GeoSiteList message or plain object to encode 696 | * @param [writer] Writer to encode to 697 | * @returns Writer 698 | */ 699 | public static encodeDelimited(message: mihomo.component.geodata.router.IGeoSiteList, writer?: $protobuf.Writer): $protobuf.Writer; 700 | 701 | /** 702 | * Decodes a GeoSiteList message from the specified reader or buffer. 703 | * @param reader Reader or buffer to decode from 704 | * @param [length] Message length if known beforehand 705 | * @returns GeoSiteList 706 | * @throws {Error} If the payload is not a reader or valid buffer 707 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 708 | */ 709 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): mihomo.component.geodata.router.GeoSiteList; 710 | 711 | /** 712 | * Decodes a GeoSiteList message from the specified reader or buffer, length delimited. 713 | * @param reader Reader or buffer to decode from 714 | * @returns GeoSiteList 715 | * @throws {Error} If the payload is not a reader or valid buffer 716 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 717 | */ 718 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): mihomo.component.geodata.router.GeoSiteList; 719 | 720 | /** 721 | * Verifies a GeoSiteList message. 722 | * @param message Plain object to verify 723 | * @returns `null` if valid, otherwise the reason why it is not 724 | */ 725 | public static verify(message: { [k: string]: any }): (string|null); 726 | 727 | /** 728 | * Creates a GeoSiteList message from a plain object. Also converts values to their respective internal types. 729 | * @param object Plain object 730 | * @returns GeoSiteList 731 | */ 732 | public static fromObject(object: { [k: string]: any }): mihomo.component.geodata.router.GeoSiteList; 733 | 734 | /** 735 | * Creates a plain object from a GeoSiteList message. Also converts values to other types if specified. 736 | * @param message GeoSiteList 737 | * @param [options] Conversion options 738 | * @returns Plain object 739 | */ 740 | public static toObject(message: mihomo.component.geodata.router.GeoSiteList, options?: $protobuf.IConversionOptions): { [k: string]: any }; 741 | 742 | /** 743 | * Converts this GeoSiteList to JSON. 744 | * @returns JSON object 745 | */ 746 | public toJSON(): { [k: string]: any }; 747 | 748 | /** 749 | * Gets the default type url for GeoSiteList 750 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 751 | * @returns The default type url 752 | */ 753 | public static getTypeUrl(typeUrlPrefix?: string): string; 754 | } 755 | } 756 | } 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /src/geo/geo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mihomo.component.geodata.router; 4 | option csharp_namespace = "Mihomo.Component.Geodata.Router"; 5 | option go_package = "github.com/metacubex/mihomo/component/geodata/router"; 6 | option java_package = "com.mihomo.component.geodata.router"; 7 | option java_multiple_files = true; 8 | 9 | // Domain for routing decision. 10 | message Domain { 11 | // Type of domain value. 12 | enum Type { 13 | // The value is used as is. 14 | Plain = 0; 15 | // The value is used as a regular expression. 16 | Regex = 1; 17 | // The value is a root domain. 18 | Domain = 2; 19 | // The value is a domain. 20 | Full = 3; 21 | } 22 | 23 | // Domain matching type. 24 | Type type = 1; 25 | 26 | // Domain value. 27 | string value = 2; 28 | 29 | message Attribute { 30 | string key = 1; 31 | 32 | oneof typed_value { 33 | bool bool_value = 2; 34 | int64 int_value = 3; 35 | } 36 | } 37 | 38 | // Attributes of this domain. May be used for filtering. 39 | repeated Attribute attribute = 3; 40 | } 41 | 42 | // IP for routing decision, in CIDR form. 43 | message CIDR { 44 | // IP address, should be either 4 or 16 bytes. 45 | bytes ip = 1; 46 | 47 | // Number of leading ones in the network mask. 48 | uint32 prefix = 2; 49 | } 50 | 51 | message GeoIP { 52 | string country_code = 1; 53 | repeated CIDR cidr = 2; 54 | bool reverse_match = 3; 55 | } 56 | 57 | message GeoIPList { 58 | repeated GeoIP entry = 1; 59 | } 60 | 61 | message GeoSite { 62 | string country_code = 1; 63 | repeated Domain domain = 2; 64 | } 65 | 66 | message GeoSiteList { 67 | repeated GeoSite entry = 1; 68 | } -------------------------------------------------------------------------------- /src/geo/geoHelper.ts: -------------------------------------------------------------------------------- 1 | import protobuf from "protobufjs/light"; 2 | import { mihomo } from "./geo"; 3 | 4 | const { 5 | GeoSiteList, 6 | Domain: { Type: DomainType }, 7 | } = mihomo.component.geodata.router; 8 | 9 | /** 10 | * Extract geo domains from given geo site data url and filter by country codes 11 | * 12 | * NOTE: This will filtered out the regex domain. 13 | * 14 | * @param geoSiteUrl The URL of the geo site data 15 | * @param countryCodes The list of country codes to filter by 16 | * @returns A list of geo domains 17 | */ 18 | export async function extractGeoDomains(geoSiteUrl: string, countryCodes: string[]): Promise { 19 | const res = await fetch(geoSiteUrl); 20 | const data = await res.arrayBuffer(); 21 | 22 | const bytes = new Uint8Array(data); 23 | 24 | const geoSiteList = GeoSiteList.decode(bytes); 25 | const filtered = geoSiteList.entry.filter((i) => { 26 | return countryCodes.includes(i.countryCode?.toLowerCase() ?? ""); 27 | }); 28 | 29 | const domainList = filtered.flatMap((i) => { 30 | if (!i.domain) return []; 31 | 32 | const filteredDomain = i.domain.filter( 33 | (d) => d.type && [DomainType.Plain, DomainType.Domain, DomainType.Full].includes(d.type) 34 | ); 35 | 36 | return filteredDomain.map((d) => d.value).filter((d) => !!d); 37 | }); 38 | 39 | console.log( 40 | `Extracted ${domainList.length} geo domains from ${geoSiteUrl} for countries: ${countryCodes.join(", ")}` 41 | ); 42 | 43 | return domainList as string[]; 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { convertSub, getOrFetchSubContent, getSubContent, parseSubHeaders, TokenNotFoundError, JSONParseError } from "./sub"; 3 | import { checkUserAgent } from "./utils"; 4 | import { logger } from "hono/logger"; 5 | 6 | const app = new Hono(); 7 | 8 | app.use(logger()); 9 | 10 | /** 11 | * Basic clash config converter 12 | * - Parameter sub: base64 encoded sub url 13 | * - Parameter convert: true/false, default true, whether to convert the config 14 | */ 15 | app.get("/sub", async (c) => { 16 | const userAgent = c.req.header("User-Agent"); 17 | const subEncoded = c.req.query("sub"); 18 | const convert = c.req.query("convert"); 19 | 20 | if (userAgent && !checkUserAgent(userAgent)) { 21 | console.log("Blocked request with User-Agent:", userAgent); 22 | c.status(400); 23 | return c.text("Not supported, must request inside clash app"); 24 | } 25 | 26 | if (!subEncoded) { 27 | console.log("Missing sub parameter"); 28 | c.status(400); 29 | return c.text("sub is required"); 30 | } 31 | 32 | const subUrl = atob(subEncoded); 33 | 34 | try { 35 | const [content, subHeaders] = await getSubContent(subUrl, userAgent!); 36 | 37 | const disableConvert = convert === "false"; 38 | let contentFinal = content; 39 | 40 | if (!disableConvert) { 41 | contentFinal = await convertSub( 42 | content, 43 | subHeaders.fileName ?? "Clash-Config-Sub", 44 | userAgent! 45 | ); 46 | console.log("Converted config"); 47 | } 48 | 49 | return c.text(contentFinal, 200, { 50 | ...subHeaders.rawHeaders, 51 | "Content-Type": "text/yaml; charset=utf-8", 52 | }); 53 | } catch (error) { 54 | if (error instanceof DOMException && error.name === "AbortError") { 55 | const msg = `Upstream error: ${error.message}`; 56 | return c.text(msg, 502); 57 | } 58 | if (error instanceof Error) { 59 | c.status(500); 60 | return c.text(`Internal server error: ${error.message}`); 61 | } 62 | c.status(500); 63 | return c.text(`Internal server error`); 64 | } 65 | }); 66 | 67 | app.get(":token", async (c) => { 68 | const token = c.req.param("token"); 69 | const userAgent = c.req.header("User-Agent"); 70 | 71 | if (userAgent && !checkUserAgent(userAgent)) { 72 | console.log("Blocked request with User-Agent:", userAgent); 73 | c.status(400); 74 | return c.text("Not supported, must request inside clash app"); 75 | } 76 | 77 | try { 78 | const { content, headers, subInfo } = await getOrFetchSubContent(token, userAgent!); 79 | const contentFinal = await convertSub(content, subInfo.label, userAgent!, subInfo.filter); 80 | 81 | // Use subInfo.label as the filename in Content-Disposition header 82 | const contentDisposition = `attachment; filename*=UTF-8''${subInfo.label}`; 83 | 84 | return c.text(contentFinal, 200, { 85 | ...headers.rawHeaders, 86 | "Content-Disposition": contentDisposition, 87 | "Content-Type": "text/yaml; charset=utf-8", 88 | }); 89 | } catch (error) { 90 | // Token not found or JSON parse error should return 400 91 | if (error instanceof TokenNotFoundError || error instanceof JSONParseError) { 92 | console.log(`Bad request: ${error.message}`); 93 | c.status(400); 94 | return c.text(error.message); 95 | } 96 | 97 | // Other errors return 500 98 | if (error instanceof Error) { 99 | c.status(500); 100 | return c.text(`Internal server error: ${error.message}`); 101 | } 102 | c.status(500); 103 | return c.text(`Internal server error`); 104 | } 105 | }) 106 | 107 | export default { 108 | port: 8787, 109 | fetch: app.fetch, 110 | }; 111 | -------------------------------------------------------------------------------- /src/sub.ts: -------------------------------------------------------------------------------- 1 | import { env } from "cloudflare:workers"; 2 | import YAML from "yaml"; 3 | import { convertClashConfig } from "./convert"; 4 | import { extractGeoDomains } from "./geo/geoHelper"; 5 | import { detectClashPremium, detectClashXMeta } from "./utils"; 6 | 7 | /** 8 | * 自定义错误类:Token 未找到 9 | */ 10 | export class TokenNotFoundError extends Error { 11 | constructor(token: string) { 12 | super(`Subscription not found for token: ${token}`); 13 | this.name = "TokenNotFoundError"; 14 | } 15 | } 16 | 17 | /** 18 | * 自定义错误类:JSON 解析失败 19 | */ 20 | export class JSONParseError extends Error { 21 | constructor(message: string) { 22 | super(`Failed to parse subscription data: ${message}`); 23 | this.name = "JSONParseError"; 24 | } 25 | } 26 | 27 | export interface ClashSubInformation { 28 | /** 用户 Token */ 29 | token: string; 30 | /** 订阅标签 */ 31 | label: string; 32 | /** 订阅 URL */ 33 | url: string; 34 | 35 | filter: { 36 | label: string; 37 | /** 地区 */ 38 | regions?: string[]; 39 | /** 最大计费倍率 */ 40 | maxBillingRate?: number; 41 | /** 排除正则 */ 42 | excludeRegex?: string; 43 | }; 44 | } 45 | 46 | export interface CachedSubContent { 47 | /** 订阅内容 */ 48 | content: string; 49 | /** 订阅 Headers */ 50 | headers: SubHeaders; 51 | } 52 | 53 | export interface SubContentWithInfo extends CachedSubContent { 54 | /** 订阅信息 */ 55 | subInfo: ClashSubInformation; 56 | } 57 | 58 | /** 59 | * @see https://www.clashverge.dev/guide/url_schemes.html#_4 60 | */ 61 | export type SubHeaders = { 62 | rawHeaders: Record; 63 | fileName?: string; 64 | profileUpdateIntervalHour?: string; 65 | expireAt?: Date; 66 | usage?: { 67 | totalMiB: number; 68 | usedMiB: number; 69 | }; 70 | }; 71 | 72 | export function parseSubHeaders(response: Response): SubHeaders { 73 | const subHeaders = { 74 | contentDisposition: response.headers.get("Content-Disposition"), 75 | profileUpdateInterval: response.headers.get("Profile-Update-Interval"), 76 | subscriptionUserInfo: response.headers.get("Subscription-Userinfo"), 77 | profileWebPageUrl: response.headers.get("Profile-Web-Page-Url"), 78 | }; 79 | 80 | const rawHeaders: Record = { 81 | "Content-Disposition": subHeaders.contentDisposition || "", 82 | "Profile-Update-Interval": subHeaders.profileUpdateInterval || "", 83 | "Subscription-Userinfo": subHeaders.subscriptionUserInfo || "", 84 | "Profile-Web-Page-Url": subHeaders.profileWebPageUrl || "", 85 | }; 86 | 87 | // filter empty headers 88 | Object.keys(rawHeaders).forEach((key) => { 89 | if (!rawHeaders[key]) { 90 | delete rawHeaders[key]; 91 | } 92 | }); 93 | 94 | const result: SubHeaders = { rawHeaders }; 95 | 96 | // Parse fileName from Content-Disposition header 97 | // Supports both filename and filename* (RFC 5987) syntax 98 | // Example: attachment; filename=config.yaml 99 | // Example: attachment; filename*=UTF-8''config%20file.yaml 100 | if (subHeaders.contentDisposition) { 101 | // Try filename* first (RFC 5987 - encoded filename) 102 | const filenameStarMatch = subHeaders.contentDisposition.match(/filename\*=([^;']+'')?([^;\n]*)/i); 103 | if (filenameStarMatch && filenameStarMatch[2]) { 104 | try { 105 | // Decode the URL-encoded filename 106 | result.fileName = decodeURIComponent(filenameStarMatch[2]); 107 | } catch (e) { 108 | // If decoding fails, use the raw value 109 | result.fileName = filenameStarMatch[2]; 110 | } 111 | } else { 112 | // Fall back to regular filename parameter 113 | const filenameMatch = subHeaders.contentDisposition.match(/filename=([^;\n]*)/i); 114 | if (filenameMatch && filenameMatch[1]) { 115 | // Remove quotes if present 116 | result.fileName = filenameMatch[1].replace(/^["']|["']$/g, "").trim(); 117 | } 118 | } 119 | } 120 | 121 | // Parse Profile-Update-Interval (in hours) 122 | // Example: 24 123 | if (subHeaders.profileUpdateInterval) { 124 | const intervalHours = parseInt(subHeaders.profileUpdateInterval, 10); 125 | if (!isNaN(intervalHours)) { 126 | result.profileUpdateIntervalHour = intervalHours.toString(); 127 | } 128 | } 129 | 130 | // Parse Subscription-Userinfo header 131 | // Example: upload=0; download=123456789; total=1073741824; expire=1696377600 132 | if (subHeaders.subscriptionUserInfo) { 133 | const userInfo = subHeaders.subscriptionUserInfo; 134 | 135 | // Parse usage information (convert bytes to MiB) 136 | const uploadMatch = userInfo.match(/upload=(\d+)/); 137 | const downloadMatch = userInfo.match(/download=(\d+)/); 138 | const totalMatch = userInfo.match(/total=(\d+)/); 139 | 140 | if (totalMatch) { 141 | const totalBytes = parseInt(totalMatch[1], 10); 142 | const uploadBytes = uploadMatch ? parseInt(uploadMatch[1], 10) : 0; 143 | const downloadBytes = downloadMatch ? parseInt(downloadMatch[1], 10) : 0; 144 | const usedBytes = uploadBytes + downloadBytes; 145 | 146 | result.usage = { 147 | totalMiB: Math.round(totalBytes / 1024 / 1024), 148 | usedMiB: Math.round(usedBytes / 1024 / 1024), 149 | }; 150 | } 151 | 152 | // Parse expire time (Unix timestamp in seconds) 153 | const expireMatch = userInfo.match(/expire=(\d+)/); 154 | if (expireMatch) { 155 | const expireTimestamp = parseInt(expireMatch[1], 10); 156 | if (!isNaN(expireTimestamp)) { 157 | result.expireAt = new Date(expireTimestamp * 1000); 158 | } 159 | } 160 | } 161 | 162 | return result; 163 | } 164 | 165 | /** 166 | * get sub content from subUrl 167 | * @param subUrl 168 | * @param userAgent 169 | */ 170 | export async function getSubContent(subUrl: string, userAgent: string): Promise<[string, SubHeaders]> { 171 | const controller = new AbortController(); 172 | const t = setTimeout(() => controller.abort(), 10000); // 10 seconds 173 | 174 | try { 175 | const response = await fetch(subUrl, { 176 | method: "GET", 177 | redirect: "follow", 178 | headers: { 179 | "User-Agent": userAgent, 180 | // no cache content 181 | "Cache-Control": "no-cache", 182 | }, 183 | signal: controller.signal, 184 | }); 185 | 186 | if (!response.ok) { 187 | throw new Error(`Upstream error: ${response.status} ${response.statusText}`); 188 | } 189 | 190 | const text = await response.text(); 191 | const subHeaders = parseSubHeaders(response); 192 | 193 | console.log(`Got subscription content with length ${text.length}`, subHeaders); 194 | 195 | // check is yaml 196 | // check first line is xxx: xxx 197 | const firstLine = text.trim().split("\n")[0]; 198 | if (!firstLine.includes(":")) { 199 | throw new Error("Upstream error: content is not yaml"); 200 | } 201 | 202 | return [text, subHeaders]; 203 | } finally { 204 | clearTimeout(t); 205 | } 206 | } 207 | 208 | /** 209 | * @param yaml Subscription content 210 | * @param profile Profile name 211 | * @param userAgent User-Agent string 212 | * @param filter Filter configuration 213 | */ 214 | export async function convertSub( 215 | yaml: string, 216 | profile: string, 217 | userAgent: string, 218 | filter?: ClashSubInformation["filter"] 219 | ): Promise { 220 | const cfg = YAML.parse(yaml); 221 | 222 | const isPremium = detectClashPremium(userAgent); 223 | const extra: { fakeIpFilters?: string[] } = {}; 224 | 225 | // get fake-ip-filter for premium core 226 | if (isPremium) { 227 | const domainList = await extractGeoDomains(env.geosite, ["private", "connectivity-check"]); 228 | extra.fakeIpFilters = domainList; 229 | } 230 | 231 | const converted = convertClashConfig({ 232 | config: cfg, 233 | profile, 234 | variant: isPremium ? "stash" : "mihomo", 235 | extra, 236 | filter, 237 | }); 238 | 239 | // https://github.com/MetaCubeX/ClashX.Meta/issues/58 240 | if (detectClashXMeta(userAgent)) { 241 | converted["tun"] = { 242 | enable: true, 243 | device: "utun6", 244 | stack: "gVisor", 245 | "dns-hijack": ["0.0.0.0:53"], 246 | "auto-route": true, 247 | "auto-detect-interface": true, 248 | }; 249 | } 250 | 251 | const convertedYaml = YAML.stringify(converted); 252 | 253 | return convertedYaml; 254 | } 255 | 256 | /** 257 | * Hash token with SHA256 using Web Crypto API 258 | */ 259 | async function hashToken(token: string): Promise { 260 | const encoder = new TextEncoder(); 261 | const data = encoder.encode(token); 262 | const hashBuffer = await crypto.subtle.digest("SHA-256", data); 263 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 264 | const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); 265 | return hashHex; 266 | } 267 | 268 | /** 269 | * 从 KV 中根据 token 获取订阅信息,获取订阅内容并缓存到 KV 270 | * @param token 用户 token (sk-xxxx 格式) 271 | * @param userAgent User-Agent 字符串 272 | * @param cacheTTL 缓存 TTL(秒),默认 1 小时 273 | * @returns 订阅信息、内容和 headers 274 | */ 275 | export async function fetchAndCacheSubContent( 276 | token: string, 277 | userAgent: string, 278 | cacheTTL: number = 3600 279 | ): Promise { 280 | const hashedToken = await hashToken(token); 281 | const kvKey = `kv:${hashedToken}`; 282 | const cacheKey = `sub:${hashedToken}`; 283 | 284 | // 1. 从 KV 中获取订阅配置,同时获取 metadata 中的 updatedAt 285 | const kvValue = await env.KV.getWithMetadata<{ updatedAt?: number }>(kvKey); 286 | if (!kvValue.value) { 287 | throw new TokenNotFoundError(token); 288 | } 289 | 290 | let subInfo: ClashSubInformation; 291 | try { 292 | subInfo = JSON.parse(kvValue.value); 293 | } catch (error) { 294 | throw new JSONParseError(error instanceof Error ? error.message : "Unknown error"); 295 | } 296 | 297 | const kvUpdatedAt = kvValue.metadata?.updatedAt; 298 | 299 | // 2. 获取订阅内容 300 | console.log(`Fetching subscription from: ${subInfo.url}`); 301 | const [content, headers] = await getSubContent(subInfo.url, userAgent); 302 | 303 | // 3. 缓存到 KV,将 kvUpdatedAt 保存到 metadata 用于缓存失效判断 304 | const cachedData: CachedSubContent = { 305 | content, 306 | headers, 307 | }; 308 | 309 | await env.KV.put(cacheKey, JSON.stringify(cachedData), { 310 | expirationTtl: cacheTTL, 311 | metadata: { kvUpdatedAt }, 312 | }); 313 | 314 | console.log( 315 | `Cached subscription content to ${cacheKey} with TTL ${cacheTTL}s (kv updated at ${ 316 | kvUpdatedAt ? new Date(kvUpdatedAt).toISOString() : "unknown" 317 | })` 318 | ); 319 | 320 | return { 321 | ...cachedData, 322 | subInfo, 323 | }; 324 | } 325 | 326 | /** 327 | * 从 KV 缓存中获取订阅内容,如果不存在或订阅信息已更新则从源获取并缓存 328 | * @param token 用户 token (sk-xxxx 格式) 329 | * @param userAgent User-Agent 字符串 330 | * @param cacheTTL 缓存 TTL(秒),默认 1 小时 331 | * @returns 订阅信息、内容和 headers 332 | */ 333 | export async function getOrFetchSubContent( 334 | token: string, 335 | userAgent: string, 336 | cacheTTL: number = 3600 337 | ): Promise { 338 | const hashedToken = await hashToken(token); 339 | const kvKey = `kv:${hashedToken}`; 340 | const cacheKey = `sub:${hashedToken}`; 341 | 342 | // 获取 kv:xxx 的 metadata,检查 updatedAt 343 | const kvValue = await env.KV.getWithMetadata<{ updatedAt?: number }>(kvKey); 344 | if (!kvValue.value) { 345 | throw new TokenNotFoundError(token); 346 | } 347 | 348 | let subInfo: ClashSubInformation; 349 | try { 350 | subInfo = JSON.parse(kvValue.value); 351 | } catch (error) { 352 | throw new JSONParseError(error instanceof Error ? error.message : "Unknown error"); 353 | } 354 | 355 | const currentKvUpdatedAt = kvValue.metadata?.updatedAt; 356 | 357 | // 尝试从缓存获取,同时获取 metadata 358 | const cacheValue = await env.KV.getWithMetadata<{ kvUpdatedAt?: number }>(cacheKey); 359 | if (cacheValue.value) { 360 | try { 361 | const cached = JSON.parse(cacheValue.value) as CachedSubContent; 362 | const cachedKvUpdatedAt = cacheValue.metadata?.kvUpdatedAt; 363 | 364 | // 检查订阅信息是否已更新:如果 kv 的 updatedAt 不同于缓存时的 kvUpdatedAt,则使缓存失效 365 | if (currentKvUpdatedAt && cachedKvUpdatedAt && currentKvUpdatedAt > cachedKvUpdatedAt) { 366 | console.log( 367 | `Subscription info updated (kv updatedAt: ${new Date( 368 | currentKvUpdatedAt 369 | ).toISOString()} > cached kvUpdatedAt: ${new Date(cachedKvUpdatedAt).toISOString()}), invalidating cache` 370 | ); 371 | } else { 372 | const kvUpdatedAtStr = cachedKvUpdatedAt ? ` (kv updated at ${new Date(cachedKvUpdatedAt).toISOString()})` : ""; 373 | console.log(`Using cached subscription content from ${cacheKey}${kvUpdatedAtStr}`); 374 | return { 375 | ...cached, 376 | subInfo, 377 | }; 378 | } 379 | } catch (error) { 380 | console.warn(`Failed to parse cached data, fetching fresh content: ${error}`); 381 | } 382 | } 383 | 384 | // 缓存不存在、解析失败或订阅信息已更新,重新获取 385 | return await fetchAndCacheSubContent(token, userAgent, cacheTTL); 386 | } 387 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * strictly check userAgent is clash 4 | * - DEMO: stash on iOS: Stash/3.1.1 Clash/1.9.0 5 | * - DEMO: clash verge rev on Windows: clash-verge/v2.4.2 6 | * - DEMO: ClashX.meta on Mac: ClashX Meta/v1.4.24 (com.metacubex.ClashX.meta; build:622; macOS 26.0.0) Alamofire/5.10.2 7 | * - DEMO: Clas Meta on Android: ClashMetaForAndroid/2.11.16.Meta 8 | * @param userAgent 9 | * @returns 10 | */ 11 | export function checkUserAgent(userAgent: string) { 12 | if (!userAgent) { 13 | return false; 14 | } 15 | 16 | // check stash 17 | if (userAgent.toLocaleLowerCase().startsWith("stash/")) { 18 | return true; 19 | } 20 | 21 | // check clash verge rev 22 | if (userAgent.toLocaleLowerCase().startsWith("clash-verge/")) { 23 | return true; 24 | } 25 | 26 | // check clashx.meta 27 | if (userAgent.toLocaleLowerCase().startsWith("clashx")) { 28 | return true; 29 | } 30 | 31 | // check clash meta for android 32 | if (userAgent.toLocaleLowerCase().startsWith("clashmetaforandroid/")) { 33 | return true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * 当前仅检测 stash,该内核不支持全部 mihomo 内核特性 41 | */ 42 | export function detectClashPremium(userAgent: string): boolean { 43 | return userAgent.toLowerCase().startsWith("stash/"); 44 | } 45 | 46 | /** 47 | * 检测 clashx.meta 48 | */ 49 | export function detectClashXMeta(userAgent: string): boolean { 50 | return userAgent.toLowerCase().startsWith("clashx meta/"); 51 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "lib": [ 9 | "ESNext" 10 | ], 11 | "jsx": "react-jsx", 12 | "jsxImportSource": "hono/jsx" 13 | }, 14 | } -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "clash-config", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-10-01", 6 | "workers_dev": false, 7 | "observability": { 8 | "enabled": true, 9 | "head_sampling_rate": 1 10 | }, 11 | "vars": { 12 | "geoip": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat", 13 | "geosite": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", 14 | "mmdb": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb", 15 | "asn": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb" 16 | }, 17 | "kv_namespaces": [ 18 | { 19 | "binding": "KV", 20 | "id": "d80c97c302ea4f279583202acb798621", 21 | "remote": true 22 | } 23 | ] 24 | } 25 | --------------------------------------------------------------------------------