Initial commit
This commit is contained in:
3
.cursorindexingignore
Normal file
3
.cursorindexingignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||||
|
.specstory/**
|
||||||
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.DS_Store
|
||||||
|
File/
|
||||||
|
.specstory/
|
||||||
|
.cursorindexingignore
|
||||||
|
|
||||||
|
|
||||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
pids/
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage & cache
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
File/
|
||||||
|
.specstory/
|
||||||
|
.cursor/
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# 1) 构建阶段:安装仅生产依赖
|
||||||
|
FROM node:22-bookworm-slim AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# 仅复制依赖清单,提升缓存命中率
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
# 无锁文件时回退 npm install;并关闭审计/基金提示
|
||||||
|
RUN if [ -f package-lock.json ]; then \
|
||||||
|
npm ci --omit=dev --no-audit --no-fund; \
|
||||||
|
else \
|
||||||
|
npm install --omit=dev --no-audit --no-fund; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 复制应用源码
|
||||||
|
COPY index.js ./index.js
|
||||||
|
COPY src ./src
|
||||||
|
COPY public ./public
|
||||||
|
|
||||||
|
# 2) 运行阶段:Debian 12 distroless(极简、无 Shell)
|
||||||
|
FROM gcr.io/distroless/nodejs22-debian12
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
USER nonroot:nonroot
|
||||||
|
|
||||||
|
# distroless nodejs 以 node 为入口,传入脚本路径即可
|
||||||
|
CMD ["/app/index.js"]
|
||||||
|
|
||||||
|
|
||||||
53
index.js
Normal file
53
index.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const express = require("express");
|
||||||
|
const { convertTextToClashMihomoYAML } = require("./src/convert");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json({ limit: "2mb" }));
|
||||||
|
app.use(
|
||||||
|
express.text({ type: ["text/*", "application/octet-stream"], limit: "2mb" })
|
||||||
|
);
|
||||||
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/convert/clash", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const input = (req.query.uri || "").toString();
|
||||||
|
if (!input) return res.status(400).json({ error: "missing uri" });
|
||||||
|
const yamlText = await convertTextToClashMihomoYAML(input);
|
||||||
|
res.setHeader("Content-Type", "application/yaml; charset=utf-8");
|
||||||
|
res.send(yamlText);
|
||||||
|
} catch (err) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: err && err.message ? err.message : String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/convert/clash", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const input =
|
||||||
|
typeof req.body === "string"
|
||||||
|
? req.body
|
||||||
|
: (req.body && req.body.input) || "";
|
||||||
|
if (!input) return res.status(400).json({ error: "missing input" });
|
||||||
|
const yamlText = await convertTextToClashMihomoYAML(input);
|
||||||
|
res.setHeader("Content-Type", "application/yaml; charset=utf-8");
|
||||||
|
res.send(yamlText);
|
||||||
|
} catch (err) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: err && err.message ? err.message : String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = process.env.PORT || 8080;
|
||||||
|
app.listen(port, () => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`listening on http://127.0.0.1:${port}`);
|
||||||
|
});
|
||||||
18
package.json
Normal file
18
package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "xray-to-mihomo-converter",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Convert Xray (vless/vmess/trojan links or subscription text) to Clash Mihomo YAML",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"dev": "node index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"yaml": "^2.5.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
595
pnpm-lock.yaml
generated
Normal file
595
pnpm-lock.yaml
generated
Normal file
@ -0,0 +1,595 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
express:
|
||||||
|
specifier: ^4.19.2
|
||||||
|
version: 4.21.2
|
||||||
|
yaml:
|
||||||
|
specifier: ^2.5.1
|
||||||
|
version: 2.8.1
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
accepts@1.3.8:
|
||||||
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
array-flatten@1.1.1:
|
||||||
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
|
|
||||||
|
body-parser@1.20.3:
|
||||||
|
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||||
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
|
||||||
|
bytes@3.1.2:
|
||||||
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
call-bound@1.0.4:
|
||||||
|
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
content-disposition@0.5.4:
|
||||||
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
content-type@1.0.5:
|
||||||
|
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
cookie-signature@1.0.6:
|
||||||
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
|
cookie@0.7.1:
|
||||||
|
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
depd@2.0.0:
|
||||||
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
destroy@1.2.0:
|
||||||
|
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||||
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
ee-first@1.1.1:
|
||||||
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
|
encodeurl@1.0.2:
|
||||||
|
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
encodeurl@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
es-define-property@1.0.1:
|
||||||
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-errors@1.3.0:
|
||||||
|
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
escape-html@1.0.3:
|
||||||
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
|
etag@1.8.1:
|
||||||
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
express@4.21.2:
|
||||||
|
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
||||||
|
engines: {node: '>= 0.10.0'}
|
||||||
|
|
||||||
|
finalhandler@1.3.1:
|
||||||
|
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
forwarded@0.2.0:
|
||||||
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
fresh@0.5.2:
|
||||||
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
function-bind@1.1.2:
|
||||||
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
gopd@1.2.0:
|
||||||
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
has-symbols@1.1.0:
|
||||||
|
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ipaddr.js@1.9.1:
|
||||||
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
media-typer@0.3.0:
|
||||||
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
merge-descriptors@1.0.3:
|
||||||
|
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
||||||
|
|
||||||
|
methods@1.1.2:
|
||||||
|
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime@1.6.0:
|
||||||
|
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
negotiator@0.6.3:
|
||||||
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
object-inspect@1.13.4:
|
||||||
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
on-finished@2.4.1:
|
||||||
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
parseurl@1.3.3:
|
||||||
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
path-to-regexp@0.1.12:
|
||||||
|
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
|
||||||
|
|
||||||
|
proxy-addr@2.0.7:
|
||||||
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
qs@6.13.0:
|
||||||
|
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
range-parser@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
raw-body@2.5.2:
|
||||||
|
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2:
|
||||||
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
send@0.19.0:
|
||||||
|
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
serve-static@1.16.2:
|
||||||
|
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
setprototypeof@1.2.0:
|
||||||
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
|
side-channel-list@1.0.0:
|
||||||
|
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
side-channel-map@1.0.1:
|
||||||
|
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
side-channel-weakmap@1.0.2:
|
||||||
|
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
side-channel@1.1.0:
|
||||||
|
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
statuses@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
toidentifier@1.0.1:
|
||||||
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
type-is@1.6.18:
|
||||||
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
unpipe@1.0.0:
|
||||||
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
utils-merge@1.0.1:
|
||||||
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
vary@1.1.2:
|
||||||
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
yaml@2.8.1:
|
||||||
|
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
|
||||||
|
engines: {node: '>= 14.6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
accepts@1.3.8:
|
||||||
|
dependencies:
|
||||||
|
mime-types: 2.1.35
|
||||||
|
negotiator: 0.6.3
|
||||||
|
|
||||||
|
array-flatten@1.1.1: {}
|
||||||
|
|
||||||
|
body-parser@1.20.3:
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
content-type: 1.0.5
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
destroy: 1.2.0
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
on-finished: 2.4.1
|
||||||
|
qs: 6.13.0
|
||||||
|
raw-body: 2.5.2
|
||||||
|
type-is: 1.6.18
|
||||||
|
unpipe: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
call-bound@1.0.4:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
|
||||||
|
content-disposition@0.5.4:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
content-type@1.0.5: {}
|
||||||
|
|
||||||
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
|
cookie@0.7.1: {}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.0.0
|
||||||
|
|
||||||
|
depd@2.0.0: {}
|
||||||
|
|
||||||
|
destroy@1.2.0: {}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-errors: 1.3.0
|
||||||
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
|
encodeurl@1.0.2: {}
|
||||||
|
|
||||||
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
|
es-errors@1.3.0: {}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
express@4.21.2:
|
||||||
|
dependencies:
|
||||||
|
accepts: 1.3.8
|
||||||
|
array-flatten: 1.1.1
|
||||||
|
body-parser: 1.20.3
|
||||||
|
content-disposition: 0.5.4
|
||||||
|
content-type: 1.0.5
|
||||||
|
cookie: 0.7.1
|
||||||
|
cookie-signature: 1.0.6
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
etag: 1.8.1
|
||||||
|
finalhandler: 1.3.1
|
||||||
|
fresh: 0.5.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
merge-descriptors: 1.0.3
|
||||||
|
methods: 1.1.2
|
||||||
|
on-finished: 2.4.1
|
||||||
|
parseurl: 1.3.3
|
||||||
|
path-to-regexp: 0.1.12
|
||||||
|
proxy-addr: 2.0.7
|
||||||
|
qs: 6.13.0
|
||||||
|
range-parser: 1.2.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
send: 0.19.0
|
||||||
|
serve-static: 1.16.2
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.1
|
||||||
|
type-is: 1.6.18
|
||||||
|
utils-merge: 1.0.1
|
||||||
|
vary: 1.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
finalhandler@1.3.1:
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
parseurl: 1.3.3
|
||||||
|
statuses: 2.0.1
|
||||||
|
unpipe: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
|
fresh@0.5.2: {}
|
||||||
|
|
||||||
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-proto: 1.0.1
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-symbols: 1.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
dunder-proto: 1.0.1
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
|
has-symbols@1.1.0: {}
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
depd: 2.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.1
|
||||||
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
|
merge-descriptors@1.0.3: {}
|
||||||
|
|
||||||
|
methods@1.1.2: {}
|
||||||
|
|
||||||
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
|
||||||
|
mime@1.6.0: {}
|
||||||
|
|
||||||
|
ms@2.0.0: {}
|
||||||
|
|
||||||
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
|
object-inspect@1.13.4: {}
|
||||||
|
|
||||||
|
on-finished@2.4.1:
|
||||||
|
dependencies:
|
||||||
|
ee-first: 1.1.1
|
||||||
|
|
||||||
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
|
path-to-regexp@0.1.12: {}
|
||||||
|
|
||||||
|
proxy-addr@2.0.7:
|
||||||
|
dependencies:
|
||||||
|
forwarded: 0.2.0
|
||||||
|
ipaddr.js: 1.9.1
|
||||||
|
|
||||||
|
qs@6.13.0:
|
||||||
|
dependencies:
|
||||||
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
range-parser@1.2.1: {}
|
||||||
|
|
||||||
|
raw-body@2.5.2:
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
|
send@0.19.0:
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
destroy: 1.2.0
|
||||||
|
encodeurl: 1.0.2
|
||||||
|
escape-html: 1.0.3
|
||||||
|
etag: 1.8.1
|
||||||
|
fresh: 0.5.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
mime: 1.6.0
|
||||||
|
ms: 2.1.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
range-parser: 1.2.1
|
||||||
|
statuses: 2.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
serve-static@1.16.2:
|
||||||
|
dependencies:
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
parseurl: 1.3.3
|
||||||
|
send: 0.19.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
|
side-channel-list@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
|
||||||
|
side-channel-map@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
|
||||||
|
side-channel-weakmap@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
side-channel-map: 1.0.1
|
||||||
|
|
||||||
|
side-channel@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
side-channel-list: 1.0.0
|
||||||
|
side-channel-map: 1.0.1
|
||||||
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
|
toidentifier@1.0.1: {}
|
||||||
|
|
||||||
|
type-is@1.6.18:
|
||||||
|
dependencies:
|
||||||
|
media-typer: 0.3.0
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
yaml@2.8.1: {}
|
||||||
150
public/index.html
Normal file
150
public/index.html
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>订阅格式转换 · Xray → Clash Mihomo</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
|
||||||
|
Helvetica, Arial;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 880px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #8883;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.row > * {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #8883;
|
||||||
|
background: #09f;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button.secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.out {
|
||||||
|
white-space: pre;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #8883;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>订阅格式转换(Xray 链接/订阅 → Clash Mihomo)</h1>
|
||||||
|
<p class="hint">
|
||||||
|
支持 vless/vmess/trojan 链接,或整段订阅文本/订阅地址(自动识别
|
||||||
|
base64)。
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
id="input"
|
||||||
|
placeholder="在此粘贴 vless://、vmess:// 或 trojan:// 链接,或订阅文本"
|
||||||
|
></textarea>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="hint">
|
||||||
|
输出为 YAML 的 proxies 段,可作 provider 或粘贴入主配置。
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<button id="btnConvert">转换为 Clash (YAML)</button>
|
||||||
|
<button id="btnDownload" class="secondary">下载 proxies.yaml</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="out" class="out"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const $ = (s) => document.querySelector(s);
|
||||||
|
const inputEl = $("#input");
|
||||||
|
const outEl = $("#out");
|
||||||
|
const btnConvert = $("#btnConvert");
|
||||||
|
const btnDownload = $("#btnDownload");
|
||||||
|
|
||||||
|
async function convert() {
|
||||||
|
const val = inputEl.value.trim();
|
||||||
|
if (!val) {
|
||||||
|
outEl.textContent = "请输入内容";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/convert/clash", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ input: val }),
|
||||||
|
});
|
||||||
|
const text = await resp.text();
|
||||||
|
if (!resp.ok) throw new Error(text);
|
||||||
|
outEl.textContent = text;
|
||||||
|
return text;
|
||||||
|
} catch (e) {
|
||||||
|
outEl.textContent =
|
||||||
|
"错误: " + (e && e.message ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btnConvert.addEventListener("click", convert);
|
||||||
|
btnDownload.addEventListener("click", async () => {
|
||||||
|
const text = outEl.textContent || (await convert());
|
||||||
|
if (!text) return;
|
||||||
|
const blob = new Blob([text], {
|
||||||
|
type: "application/yaml;charset=utf-8",
|
||||||
|
});
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = "proxies.yaml";
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
src/convert.js
Normal file
20
src/convert.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { parseXrayLikeInput } = require('./parsers');
|
||||||
|
const { toClashProxiesYAML } = require('./emitters');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts a single URL (vless://, vmess://, trojan://) or a subscription text (possibly base64),
|
||||||
|
* parses into nodes, then emits Clash Mihomo YAML (only proxies section for MVP).
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {Promise<string>} YAML text
|
||||||
|
*/
|
||||||
|
async function convertTextToClashMihomoYAML(input) {
|
||||||
|
const nodes = await parseXrayLikeInput(input);
|
||||||
|
const yaml = toClashProxiesYAML(nodes);
|
||||||
|
return yaml;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { convertTextToClashMihomoYAML };
|
||||||
|
|
||||||
|
|
||||||
121
src/emitters.js
Normal file
121
src/emitters.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./parsers').Node[]} nodes
|
||||||
|
*/
|
||||||
|
function toClashProxiesYAML(nodes) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push("proxies:");
|
||||||
|
for (const n of nodes) {
|
||||||
|
switch (n.type) {
|
||||||
|
case "vless":
|
||||||
|
emitVless(lines, n);
|
||||||
|
break;
|
||||||
|
case "vmess":
|
||||||
|
emitVmess(lines, n);
|
||||||
|
break;
|
||||||
|
case "trojan":
|
||||||
|
emitTrojan(lines, n);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// skip unsupported
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join("\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitVless(lines, n) {
|
||||||
|
lines.push(` - name: ${yamlStr(n.name || "vless")}`);
|
||||||
|
lines.push(" type: vless");
|
||||||
|
lines.push(` server: ${yamlStr(n.server)}`);
|
||||||
|
lines.push(` port: ${Number(n.port)}`);
|
||||||
|
lines.push(` uuid: ${yamlStr(n.uuid)}`);
|
||||||
|
if (n.flow) {
|
||||||
|
lines.push(` flow: ${yamlStr(n.flow)}`);
|
||||||
|
}
|
||||||
|
lines.push(` udp: true`);
|
||||||
|
if (n.tls && n.tls.enabled) {
|
||||||
|
lines.push(" tls: true");
|
||||||
|
if (n.tls.serverName)
|
||||||
|
lines.push(` servername: ${yamlStr(n.tls.serverName)}`);
|
||||||
|
if (n.reality) {
|
||||||
|
lines.push(" reality-opts:");
|
||||||
|
if (n.reality.publicKey)
|
||||||
|
lines.push(` public-key: ${yamlStr(n.reality.publicKey)}`);
|
||||||
|
if (n.reality.shortId)
|
||||||
|
lines.push(` short-id: ${yamlStr(n.reality.shortId)}`);
|
||||||
|
}
|
||||||
|
if (n.tls.utls && n.tls.utls.fingerprint) {
|
||||||
|
lines.push(` client-fingerprint: ${yamlStr(n.tls.utls.fingerprint)}`);
|
||||||
|
}
|
||||||
|
if (Array.isArray(n.alpn) && n.alpn.length > 0) {
|
||||||
|
lines.push(` alpn: [${n.alpn.map(yamlStr).join(", ")}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const net = (n.network || "tcp").toLowerCase();
|
||||||
|
lines.push(` network: ${yamlStr(net)}`);
|
||||||
|
if (net === "grpc" && n.grpc) {
|
||||||
|
lines.push(" grpc-opts:");
|
||||||
|
if (n.grpc.serviceName != null)
|
||||||
|
lines.push(` grpc-service-name: ${yamlStr(n.grpc.serviceName)}`);
|
||||||
|
if (n.grpc.authority)
|
||||||
|
lines.push(` authority: ${yamlStr(n.grpc.authority)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitVmess(lines, n) {
|
||||||
|
lines.push(` - name: ${yamlStr(n.name || "vmess")}`);
|
||||||
|
lines.push(" type: vmess");
|
||||||
|
lines.push(` server: ${yamlStr(n.server)}`);
|
||||||
|
lines.push(` port: ${Number(n.port)}`);
|
||||||
|
lines.push(` uuid: ${yamlStr(n.uuid)}`);
|
||||||
|
// vmess specific
|
||||||
|
lines.push(` alterId: ${Number(n.alterId || 0)}`);
|
||||||
|
lines.push(` cipher: ${yamlStr(n.cipher || "auto")}`);
|
||||||
|
lines.push(` udp: true`);
|
||||||
|
if (n.tls && (n.tls.enabled || n.tls.serverName)) {
|
||||||
|
lines.push(" tls: true");
|
||||||
|
if (n.tls.serverName)
|
||||||
|
lines.push(` servername: ${yamlStr(n.tls.serverName)}`);
|
||||||
|
if (Array.isArray(n.alpn) && n.alpn.length > 0) {
|
||||||
|
lines.push(` alpn: [${n.alpn.map(yamlStr).join(", ")}]`);
|
||||||
|
}
|
||||||
|
if (n.tls.utls && n.tls.utls.fingerprint) {
|
||||||
|
lines.push(` client-fingerprint: ${yamlStr(n.tls.utls.fingerprint)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const net = (n.network || "tcp").toLowerCase();
|
||||||
|
lines.push(` network: ${yamlStr(net)}`);
|
||||||
|
if (net === "ws" && n.ws) {
|
||||||
|
lines.push(" ws-opts:");
|
||||||
|
if (n.ws.path != null) lines.push(` path: ${yamlStr(n.ws.path)}`);
|
||||||
|
if (n.ws.headers && n.ws.headers.Host) {
|
||||||
|
lines.push(" headers:");
|
||||||
|
lines.push(` Host: ${yamlStr(n.ws.headers.Host)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitTrojan(lines, n) {
|
||||||
|
lines.push(` - name: ${yamlStr(n.name || "trojan")}`);
|
||||||
|
lines.push(" type: trojan");
|
||||||
|
lines.push(` server: ${yamlStr(n.server)}`);
|
||||||
|
lines.push(` port: ${Number(n.port)}`);
|
||||||
|
lines.push(` password: ${yamlStr(n.password)}`);
|
||||||
|
lines.push(" sni: null");
|
||||||
|
if (n.tls && (n.tls.enabled || n.tls.serverName)) {
|
||||||
|
lines.push(" tls: true");
|
||||||
|
if (n.tls.serverName) lines.push(` sni: ${yamlStr(n.tls.serverName)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function yamlStr(v) {
|
||||||
|
if (v == null) return '""';
|
||||||
|
if (typeof v === "number") return String(v);
|
||||||
|
const s = String(v);
|
||||||
|
if (/^[A-Za-z0-9._-]+$/.test(s)) return s;
|
||||||
|
return JSON.stringify(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { toClashProxiesYAML };
|
||||||
200
src/parsers.js
Normal file
200
src/parsers.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { Buffer } = require("buffer");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Node
|
||||||
|
* @property {string} type vless|vmess|trojan
|
||||||
|
* @property {string} name tag/remark
|
||||||
|
* @property {string} server
|
||||||
|
* @property {number} port
|
||||||
|
* @property {string} uuid for vless/vmess
|
||||||
|
* @property {string} password for trojan or vmess alterId? (not used here)
|
||||||
|
* @property {Object} tls
|
||||||
|
* @property {boolean} tls.enabled
|
||||||
|
* @property {string} [tls.serverName]
|
||||||
|
* @property {Object} [tls.utls]
|
||||||
|
* @property {string} [tls.fingerprint]
|
||||||
|
* @property {string[]} [alpn]
|
||||||
|
* @property {Object} [reality]
|
||||||
|
* @property {string} [reality.publicKey]
|
||||||
|
* @property {string} [reality.shortId]
|
||||||
|
* @property {string} [network] grpc|ws|tcp
|
||||||
|
* @property {number} [alterId]
|
||||||
|
* @property {string} [cipher]
|
||||||
|
* @property {string} [flow]
|
||||||
|
* @property {Object} [grpc]
|
||||||
|
* @property {string} [grpc.serviceName]
|
||||||
|
* @property {string} [grpc.authority]
|
||||||
|
* @property {Object} [ws]
|
||||||
|
* @property {string} [ws.path]
|
||||||
|
* @property {Object} [ws.headers]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse input: single URL or subscription content (raw or base64) possibly containing multiple lines.
|
||||||
|
* Returns list of Node objects.
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {Promise<Node[]>}
|
||||||
|
*/
|
||||||
|
async function parseXrayLikeInput(input) {
|
||||||
|
if (!input || typeof input !== "string") throw new Error("empty input");
|
||||||
|
|
||||||
|
// If looks like a URL schema, parse single
|
||||||
|
if (/^(vless|vmess|trojan):\/\//i.test(input.trim())) {
|
||||||
|
return [parseSingleURL(input.trim())];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try base64 subscription (vmess often is base64 lines, but vless/trojan lists can be raw)
|
||||||
|
let text = input;
|
||||||
|
try {
|
||||||
|
const maybe = Buffer.from(input.trim(), "base64").toString("utf8");
|
||||||
|
// heuristic: if decode yields many control chars, keep original
|
||||||
|
if (/\w+:\/\//.test(maybe) || /\n/.test(maybe)) {
|
||||||
|
text = maybe;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s && /^(vless|vmess|trojan):\/\//i.test(s));
|
||||||
|
|
||||||
|
return lines.map(parseSingleURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSingleURL(url) {
|
||||||
|
if (/^vless:\/\//i.test(url)) return parseVless(url);
|
||||||
|
if (/^vmess:\/\//i.test(url)) return parseVmess(url);
|
||||||
|
if (/^trojan:\/\//i.test(url)) return parseTrojan(url);
|
||||||
|
throw new Error("unsupported schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVless(uri) {
|
||||||
|
const raw = uri.replace(/^vless:\/\//i, "");
|
||||||
|
const [credsHost, hashTag] = raw.split("#");
|
||||||
|
const name = decodeURIComponent(hashTag || "").trim() || "vless-node";
|
||||||
|
const atIdx = credsHost.indexOf("@");
|
||||||
|
if (atIdx < 0) throw new Error("invalid vless");
|
||||||
|
const uuid = credsHost.slice(0, atIdx);
|
||||||
|
const rest = credsHost.slice(atIdx + 1);
|
||||||
|
const qIdx = rest.indexOf("?");
|
||||||
|
const hostPort = qIdx >= 0 ? rest.slice(0, qIdx) : rest;
|
||||||
|
const queryStr = qIdx >= 0 ? rest.slice(qIdx + 1) : "";
|
||||||
|
const [server, portStr] = hostPort.split(":");
|
||||||
|
const params = new URLSearchParams(queryStr);
|
||||||
|
const get = (k) => {
|
||||||
|
const v = params.get(k);
|
||||||
|
return v ? decodeURIComponent(v) : undefined;
|
||||||
|
};
|
||||||
|
const type = (get("type") || "tcp").toLowerCase();
|
||||||
|
const security = (get("security") || "").toLowerCase();
|
||||||
|
const tlsEnabled = security === "tls" || security === "reality";
|
||||||
|
const node = {
|
||||||
|
type: "vless",
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
port: Number(portStr || 0),
|
||||||
|
uuid,
|
||||||
|
network: type,
|
||||||
|
flow: get("flow") || undefined,
|
||||||
|
tls: {
|
||||||
|
enabled: tlsEnabled,
|
||||||
|
serverName: tlsEnabled ? get("sni") || undefined : undefined,
|
||||||
|
utls: tlsEnabled && get("fp") ? { fingerprint: get("fp") } : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// optional alpn
|
||||||
|
const alpnStr = get("alpn");
|
||||||
|
if (tlsEnabled && alpnStr && alpnStr.trim()) {
|
||||||
|
node.alpn = alpnStr
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
if (security === "reality") {
|
||||||
|
node.reality = { publicKey: get("pbk") || "", shortId: get("sid") || "" };
|
||||||
|
node.tls.enabled = true;
|
||||||
|
}
|
||||||
|
if (node.network === "grpc") {
|
||||||
|
node.grpc = {
|
||||||
|
serviceName: get("serviceName") || "",
|
||||||
|
authority: get("authority") || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!node.port) throw new Error("invalid port");
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVmess(uri) {
|
||||||
|
// vmess:// base64(json)
|
||||||
|
const payload = uri.replace(/^vmess:\/\//i, "").trim();
|
||||||
|
const json = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
||||||
|
/** @type {Node} */
|
||||||
|
const node = {
|
||||||
|
type: "vmess",
|
||||||
|
name: json.ps || "vmess-node",
|
||||||
|
server: json.add,
|
||||||
|
port: Number(json.port),
|
||||||
|
uuid: json.id,
|
||||||
|
network: (json.net || "tcp").toLowerCase(),
|
||||||
|
alterId: Number(json.aid || 0),
|
||||||
|
cipher: json.scy || json.scypher || undefined,
|
||||||
|
tls: {
|
||||||
|
enabled: json.tls === "tls" || Boolean(json.sni),
|
||||||
|
serverName: json.sni || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// alpn
|
||||||
|
if (typeof json.alpn === "string" && json.alpn.trim()) {
|
||||||
|
node.alpn = json.alpn
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
// fingerprint
|
||||||
|
if (typeof json.fp === "string" && json.fp.trim()) {
|
||||||
|
node.tls.utls = { fingerprint: json.fp };
|
||||||
|
}
|
||||||
|
// ws
|
||||||
|
if (node.network === "ws") {
|
||||||
|
node.ws = { path: json.path || undefined, headers: {} };
|
||||||
|
if (json.host && String(json.host).trim()) {
|
||||||
|
node.ws.headers = { Host: String(json.host).trim() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!node.port) throw new Error("invalid vmess port");
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTrojan(uri) {
|
||||||
|
const raw = uri.replace(/^trojan:\/\//i, "");
|
||||||
|
const [credsHost, hashTag] = raw.split("#");
|
||||||
|
const name = decodeURIComponent(hashTag || "").trim() || "trojan-node";
|
||||||
|
const atIdx = credsHost.indexOf("@");
|
||||||
|
if (atIdx < 0) throw new Error("invalid trojan");
|
||||||
|
const password = credsHost.slice(0, atIdx);
|
||||||
|
const rest = credsHost.slice(atIdx + 1);
|
||||||
|
const qIdx = rest.indexOf("?");
|
||||||
|
const hostPort = qIdx >= 0 ? rest.slice(0, qIdx) : rest;
|
||||||
|
const queryStr = qIdx >= 0 ? rest.slice(qIdx + 1) : "";
|
||||||
|
const [server, portStr] = hostPort.split(":");
|
||||||
|
const params = new URLSearchParams(queryStr);
|
||||||
|
const sni = params.get("sni") || params.get("peer") || undefined;
|
||||||
|
/** @type {Node} */
|
||||||
|
const node = {
|
||||||
|
type: "trojan",
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
port: Number(portStr || 0),
|
||||||
|
password,
|
||||||
|
network: "tcp",
|
||||||
|
tls: { enabled: true, serverName: sni },
|
||||||
|
};
|
||||||
|
if (!node.port) throw new Error("invalid trojan port");
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseXrayLikeInput };
|
||||||
Reference in New Issue
Block a user