From b231e784982cc9a676c509f5aa73d4ba37d55d67 Mon Sep 17 00:00:00 2001 From: jocayn <1579649885@qq.com> Date: Sat, 13 Jun 2026 16:19:52 +0800 Subject: [PATCH] feat: support NAS imports and proxy names --- .dockerignore | 7 ++++ Dockerfile | 24 ++++++------ README.md | 33 ++++++++++++++++ docker-compose.yml | 19 ++++----- public/index.html | 98 +++++++++++++++++++++++++++++++++++++--------- sub2proxy.js | 78 ++++++++++++++++++++++++++++++++---- 6 files changed, 212 insertions(+), 47 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9aaf1d6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +node_modules +out +subs.json +npm-debug.log +.DS_Store diff --git a/Dockerfile b/Dockerfile index f61de71..b0c67dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,24 @@ -FROM node:22-alpine +FROM node:20-alpine WORKDIR /app -# 复制依赖配置并安装 +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=3000 \ + AUTO_OPEN=0 + COPY package*.json ./ -RUN npm install --production +RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi -# 复制其余的项目代码 COPY sub2proxy.js ./ -COPY public/ ./public/ -COPY templates/ ./templates/ +COPY public ./public +COPY templates ./templates -# 创建持久化存储的目录与文件 -RUN mkdir -p out +RUN mkdir -p out && printf '[]\n' > subs.json -# 暴露 3000 端口 EXPOSE 3000 -# 环境变量:让服务不自动在后台执行浏览器拉起命令 -ENV HEADLESS=true +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "require('http').get('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" -# 启动服务 CMD ["node", "sub2proxy.js"] diff --git a/README.md b/README.md index 35d306c..d7bc336 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,36 @@ node sub2proxy.js node sub2proxy.js --cli ``` 配置会自动输出到 `out/CatMata-sub.yaml` 和 `out/CatMata-sub.json`,并归档于 `out/history`。 + +## Docker / 飞牛部署 + +### 使用 Compose +先确认当前目录下存在 `subs.json`,再运行: +```bash +docker compose up -d --build +``` + +默认访问地址: +```text +http://飞牛IP:3000 +``` + +需要持久化的数据: +* `subs.json` - 订阅源配置,控制面板修改后会写入这里 +* `out/` - 当前输出和历史配置 +* `templates/` - Clash / Sing-box 模板,默认只读挂载 + +### 手机/电脑一键导入 +如果手机或电脑通过 `http://飞牛IP:3000` 打开控制面板,导入链接会自动使用这个地址生成,Clash 客户端可以直接从飞牛拉取配置。 + +如果你通过反向代理、域名或 HTTPS 访问,建议在 `docker-compose.yml` 中设置: +```yaml +PUBLIC_BASE_URL: "https://你的域名" +``` + +如果使用局域网固定 IP,也可以显式设置: +```yaml +PUBLIC_BASE_URL: "http://192.168.1.10:3000" +``` + +注意:不要让导入链接使用 `127.0.0.1` 或 `localhost`,手机/电脑上的 Clash 会把它理解为设备自己,而不是飞牛服务器。 diff --git a/docker-compose.yml b/docker-compose.yml index bca47c1..91b212e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,16 @@ services: sub2proxy: build: . container_name: sub2proxy + restart: unless-stopped ports: - "3000:3000" - volumes: - # 映射订阅配置文件,防止容器重启后订阅丢失 - - ./subs.json:/app/subs.json - # 映射模板文件夹,方便在 NAS 目录下随时修改或添加自定义配置模板 - - ./templates:/app/templates - # 映射输出文件夹,生成的文件和历史记录将同步保存在 NAS 主机目录中 - - ./out:/app/out - restart: unless-stopped environment: - - HEADLESS=true + PORT: "3000" + HOST: "0.0.0.0" + AUTO_OPEN: "0" + # Set this when clients should import through a fixed LAN IP, domain, or reverse proxy. + # PUBLIC_BASE_URL: "http://192.168.1.10:3000" + volumes: + - ./subs.json:/app/subs.json + - ./out:/app/out + - ./templates:/app/templates:ro diff --git a/public/index.html b/public/index.html index 7072d30..f350bec 100644 --- a/public/index.html +++ b/public/index.html @@ -710,7 +710,8 @@ - + + @@ -719,7 +720,7 @@ - @@ -857,14 +858,28 @@ let subscriptions = []; let proxies = []; // In-memory editable proxies let originalDns = { clash: '', singbox: '' }; + let appConfig = { publicBaseUrl: '' }; // On Load - window.addEventListener('DOMContentLoaded', () => { + window.addEventListener('DOMContentLoaded', async () => { lucide.createIcons(); + await loadAppConfig(); loadSubscriptions(); loadDnsTemplates(); }); + async function loadAppConfig() { + try { + const res = await fetch('/api/app-config'); + if (res.ok) { + const data = await res.json(); + appConfig.publicBaseUrl = (data.publicBaseUrl || '').replace(/\/+$/, ''); + } + } catch (e) { + console.warn('Failed to load app config:', e); + } + } + // Logging helper function log(message, type = 'info') { const logs = document.getElementById('console-logs'); @@ -1094,6 +1109,24 @@ } } + function escapeHtml(value) { + return String(value ?? '').replace(/[&<>"']/g, ch => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + })[ch]); + } + + function getParsedName(node) { + return node.name || node.tag || ''; + } + + function getProxyName(node) { + return node.proxyName || node.originalName || getParsedName(node); + } + function renderProxiesTable() { const list = document.getElementById('proxies-list'); const counter = document.getElementById('proxies-counter'); @@ -1104,7 +1137,7 @@ if (proxies.length === 0) { list.innerHTML = ` - @@ -1114,11 +1147,17 @@ proxies.forEach((node, idx) => { const tr = document.createElement('tr'); + const parsedName = getParsedName(node); + const proxyName = getProxyName(node); + const nodeType = node.type || ''; + const server = node.server || ''; + const port = node.port || node.server_port || ''; tr.innerHTML = ` - - - - + + + + +
名称 (Tag)解析名 (Tag)代理名 协议 (Type) 服务器 (Server) 端口 (Port)
+ 请先点击右上角 “一键拉取” 按钮解析节点
+ 未提取到任何代理节点
${node.name || node.tag}${node.type}${node.server}${node.port || node.server_port}${escapeHtml(parsedName)}${escapeHtml(proxyName)}${escapeHtml(nodeType)}${escapeHtml(server)}${escapeHtml(port)}