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