一、哪吒dashboard+Cloudflared穿透

1、index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const url = require('url');

const NZ_DIR = path.join(process.cwd(), 'nz');
const BINARY_NAME = 'dashboard-linux';
const CONFIG_PATH = path.join(NZ_DIR, 'data/config.yaml');
const CLOUDFLARED_NAME = 'cloudflared';
const ENV_PATH = path.join(process.cwd(), '.env');

// ==================== 加载 .env ====================
function loadEnv() {
if (!fs.existsSync(ENV_PATH)) return;
const content = fs.readFileSync(ENV_PATH, 'utf8');
for (let line of content.split('\n')) {
line = line.trim();
if (!line || line.startsWith('#')) continue;
const [key, ...valueArr] = line.split('=');
if (!key) continue;
const value = valueArr.join('=').trim().replace(/^["']|["']$/g, '');
if (!process.env[key.trim()]) process.env[key.trim()] = value;
}
}
loadEnv();

const PORT = process.env.PORT || 20234;
const WEBHOOK_PORT = process.env.WEBHOOK_PORT || 3000;
const WECHAT_WEBHOOK = process.env.WECHAT_WEBHOOK || '';
const CF_TUNNEL_TOKEN = process.env.CF_TUNNEL_TOKEN || '';
const DASHBOARD_URL = process.env.DASHBOARD_URL || `http://localhost:${PORT}`;

let dashboardProcess = null;
let webhookServer = null;
let cloudflaredProcess = null;
const serverStatus = {};

// ==================== 架构检测 ====================
function getArch() {
let uname = '';
try { uname = execSync('uname -m', { encoding: 'utf8' }).trim(); } catch {}
if (uname === 'x86_64') return 'amd64';
if (uname === 'aarch64') return 'arm64';
if (uname.includes('armv7')) return 'arm';
return 'amd64';
}
const ARCH = getArch();

// ==================== 下载文件 ====================
async function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
}
res.pipe(file);
file.on('finish', () => { file.close(); fs.fsyncSync(file.fd); resolve(); });
}).on('error', (err) => { if (fs.existsSync(dest)) fs.unlinkSync(dest); reject(err); });
});
}

// ==================== Cloudflared ====================
async function downloadCloudflared() {
const cfPath = path.join(process.cwd(), CLOUDFLARED_NAME);
if (fs.existsSync(cfPath)) return;
const cfArch = ARCH === 'arm64' ? 'arm64' : ARCH === 'arm' ? 'arm' : 'amd64';
await downloadFile(`https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}`, cfPath);
await new Promise(r => setTimeout(r, 1500));
fs.chmodSync(cfPath, 0o755);
}

function startTunnel() {
if (!CF_TUNNEL_TOKEN) return;
const cfPath = path.resolve(process.cwd(), CLOUDFLARED_NAME);
if (!fs.existsSync(cfPath)) return;
cloudflaredProcess = spawn(cfPath, ['tunnel', 'run', '--token', CF_TUNNEL_TOKEN], { stdio: 'inherit' });
cloudflaredProcess.on('close', () => setTimeout(startTunnel, 10000));
}

// ==================== Dashboard ====================
async function downloadDashboard() {
if (!fs.existsSync(NZ_DIR)) fs.mkdirSync(NZ_DIR, { recursive: true });
process.chdir(NZ_DIR);
if (fs.existsSync(BINARY_NAME)) return;
await downloadFile(`https://github.com/nezhahq/nezha/releases/latest/download/dashboard-linux-${ARCH}.zip`, 'dashboard.zip');
execSync('unzip -o dashboard.zip');
if (fs.existsSync(`dashboard-linux-${ARCH}`)) fs.renameSync(`dashboard-linux-${ARCH}`, BINARY_NAME);
fs.chmodSync(BINARY_NAME, 0o755);
if (fs.existsSync('dashboard.zip')) fs.unlinkSync('dashboard.zip');
}

function createConfig() {
if (fs.existsSync(CONFIG_PATH)) return;
const config = `debug: false
listen_port: ${PORT}
listen_host: "0.0.0.0"
grpcport: 5555
site:
brand: "哪吒监控"
cookiename: "nezha-dashboard"
`;
fs.mkdirSync(path.join(NZ_DIR, 'data'), { recursive: true });
fs.writeFileSync(CONFIG_PATH, config);
}

function startDashboard() {
if (dashboardProcess) dashboardProcess.kill();
dashboardProcess = spawn(`./${BINARY_NAME}`, [], { stdio: 'inherit', cwd: NZ_DIR });
dashboardProcess.on('close', () => setTimeout(startDashboard, 3000));
}

// ==================== Webhook中转服务 ====================

// 发送企业微信 text 消息
function sendWechat(content) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ msgtype: 'text', text: { content: content } });
const u = url.parse(WECHAT_WEBHOOK);
const req = https.request({
hostname: u.hostname, path: u.path, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
}, (res) => {
let r = ''; res.on('data', c => r += c); res.on('end', () => resolve(r));
});
req.on('error', reject);
req.write(data); req.end();
});
}

// 【新增】从 content 字符串提取信息
function extractFromContent(content) {
if (!content || typeof content !== 'string') return {};

const result = {};

// 提取服务器名称
const nameMatch = content.match(/服务器名称[::]\s*([^\n]+)/)
|| content.match(/🏷️\s*([^\n]+)/);
if (nameMatch) result.name = nameMatch[1].trim();

// 提取IP
const ipMatch = content.match(/服务器IP[::]\s*([^\n]+)/)
|| content.match(/🌐\s*([^\n]+)/);
if (ipMatch) result.ip = ipMatch[1].trim();

// 提取时间
const timeMatch = content.match(/时间[::]\s*([^\n]+)/)
|| content.match(/⏰\s*([^\n]+)/);
if (timeMatch) result.time = timeMatch[1].trim();

return result;
}

// 【修复】getField 支持从 text.content 提取
function getField(data, fieldList, defaultValue = '未知') {
if (!data) return defaultValue;

// 【新增】检查嵌套 text.content
let content = '';
if (data.text && data.text.content) {
content = data.text.content;
} else if (data.content) {
content = data.content;
}

// 如果 content 是字符串,从中提取
if (typeof content === 'string' && content.length > 0) {
const extracted = extractFromContent(content);

// 根据请求的字段类型返回对应值
for (let field of fieldList) {
if ((field.includes('NAME') || field.includes('name')) && extracted.name) {
return extracted.name;
}
if ((field.includes('IP') || field.includes('ip')) && extracted.ip) {
return extracted.ip;
}
if ((field.includes('TIME') || field.includes('DATETIME') || field.includes('time')) && extracted.time) {
return extracted.time;
}
}
}

// 原有逻辑:直接字段匹配
for (let field of fieldList) {
if (data[field] !== undefined && data[field] !== '') return String(data[field]);
// 尝试带#的占位符格式
if (data[`#${field}#`] !== undefined && data[`#${field}#`] !== '') return String(data[`#${field}#`]);
}

return defaultValue;
}

// 【不变】原有状态判断逻辑
function determineStatus(serverName, serverIP) {
const key = `${serverName || 'Unknown'}_${serverIP || 'Unknown'}`;
const now = Date.now();

if (!serverStatus[key]) {
serverStatus[key] = { status: 'online', lastTime: now - 600000 };
}

const prev = serverStatus[key].status;
const current = prev === 'online' ? 'offline' : 'online';
serverStatus[key] = { status: current, lastTime: now };

return current;
}

// 【新增】获取北京时间 YYYY-MM-DD HH:mm:ss
function getBeijingTime() {
const now = new Date();
const formatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});

const parts = formatter.formatToParts(now);
const getPart = (type) => parts.find(p => p.type === type).value;

return `${getPart('year')}-${getPart('month')}-${getPart('day')} ${getPart('hour')}:${getPart('minute')}:${getPart('second')}`;
}

// 【修改】使用当前北京时间
function buildMessage(status, data) {
const isOffline = status === 'offline';
const serverName = getField(data, ['SERVER.NAME', 'ServerName', 'name']);
const serverIP = getField(data, ['SERVER.IPV4', 'SERVER.IP', 'ServerIP', 'ip', 'IP']);

// 获取当前北京时间
const timeStr = getBeijingTime();

const title = isOffline ? '🔴 [告警] 服务器离线' : '🟢 [恢复] 服务器上线';
const desc = isOffline ? '服务器已离线,请立即检查!' : '服务器已恢复正常运行。';

return `${title}
${desc}

📍 节点名称:${serverName}
🌐 节点IP:${serverIP}
⏰ 时间:${timeStr}
📊 当前状态:${isOffline ? '[❌] 离线' : '[✅] 在线'}

💻 面板地址:${DASHBOARD_URL}`;
}

// 启动Webhook服务
function startWebhook() {
if (!WECHAT_WEBHOOK) return;

webhookServer = http.createServer((req, res) => {
if (req.method !== 'POST') { res.writeHead(405); res.end(); return; }

let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
try {
console.log('\n========== 收到哪吒通知 ==========');
console.log('【Raw Body】', body.substring(0, 500));

let data = {};
try { data = JSON.parse(body); } catch (e) {
console.log('JSON解析失败:', e.message);
data = { raw: body };
}

// 提取名称和IP(修复后的getField)
const serverName = getField(data, ['SERVER.NAME', 'ServerName']);
const serverIP = getField(data, ['SERVER.IPV4', 'SERVER.IP', 'ServerIP']);

// 【不变】原有状态判断
const status = determineStatus(serverName, serverIP);

console.log(`解析结果: 名称=${serverName}, IP=${serverIP}, 状态=${status}`);

const msg = buildMessage(status, data);
await sendWechat(msg);

res.writeHead(200); res.end('OK');
console.log(`✅ ${status === 'offline' ? '离线告警' : '恢复通知'} 发送成功\n`);

} catch (e) {
console.error('❌ 处理失败:', e);
res.writeHead(500); res.end('Error');
}
});
});

webhookServer.listen(WEBHOOK_PORT, () => {
console.log(`\n📡 Webhook已启动: http://0.0.0.0:${WEBHOOK_PORT}`);
});
}

// ==================== 主程序 ====================
async function main() {
await downloadCloudflared();
startTunnel();
await new Promise(r => setTimeout(r, 8000));
await downloadDashboard();
createConfig();
startDashboard();
startWebhook();
console.log('🚀 系统启动完成');
}

main().catch(console.error);

2、package.json

1
2
3
4
5
6
7
8
9
{
"name": "nezha-dashboard",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {}
}

3、.env

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# nezha 配置
listen_port=20234
ADMIN=
CLIENT_ID=
CLIENT_SECRET=
AUTO_UPDATE=true
RESTART_INTERVAL=24
PORT=20234
# 可选配置
TZ=Asia/Shanghai
WEBHOOK_PORT=28009
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=
DASHBOARD_URL=https://nz.gl.edu.eu.org
CF_TUNNEL_TOKEN=

4、如何快速获取 GitHub Client ID 和 Secret?

打开这个链接:https://github.com/settings/applications/new
填写以下信息:
Application name: Nezha Dashboard
Homepage URL: http://你的IP:20234
Authorization callback URL: http://你的IP:20234/oauth2/callback

点击 Register application
复制 Client ID 和 Client Secret,填入上面的配置文件。

二、哪吒agent+vless

1、index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
const fs = require('fs');
const path = require('path');
const net = require('net');
const crypto = require('crypto');
const { URL } = require('url');
const { createServer } = require('http');
const WebSocket = require('ws');
const { exec } = require('child_process');

// ==========================================
// 全部环境变量(1:1 你给的)
// ==========================================
const ALL_ENV_VARS = [
"PORT", "SUB_PATH", "UUID", "NEZHA_SERVER", "NEZHA_PORT",
"NEZHA_KEY", "ARGO_PORT", "DOMAIN", "ARGO_AUTH",
"REALITY_PORT", "ANYREALITY_PORT", "FAKE_DOMAIN", "CFPORT",
"UPLOAD_URL", "CHAT_ID", "BOT_TOKEN", "REMARKS", "DISABLE_ARGO"
];

// ==========================================
// 加载 .env 环境变量
// ==========================================
function loadEnvs() {
function tryLoad(filePath) {
try {
if (!fs.existsSync(filePath)) return;
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
lines.forEach(line => {
const trim = line.trim();
if (!trim || trim.startsWith('#') || !trim.includes('=')) return;
const [k, ...v] = trim.split('=');
const key = k.trim();
const value = v.join('=').trim().replace(/^["']|["']$/g, '');
if (ALL_ENV_VARS.includes(key)) {
process.env[key] = value;
}
});
} catch (e) {}
}

tryLoad(path.join(__dirname, '.env'));
tryLoad('/home/container/.env');
tryLoad(path.join(__dirname, '..', '.env'));
}
loadEnvs();

// ====================== 配置 ======================
const config = {
PORT: process.env.PORT || 3000,
UUID: process.env.UUID || '',
DOMAIN: process.env.DOMAIN || '',
FAKE_DOMAIN: process.env.FAKE_DOMAIN || 'www.visakorea.com',
REMARKS: process.env.REMARKS || 'Server',
SUB_PATH: process.env.SUB_PATH?.replace(/\//g, '') || 'sub',
NEZHA_SERVER: process.env.NEZHA_SERVER || '',
NEZHA_PORT: process.env.NEZHA_PORT || '',
NEZHA_KEY: process.env.NEZHA_KEY || '',
ARGO_AUTH: process.env.ARGO_AUTH || '',
get wsPath() {
return '/' + this.UUID.replace(/-/g, '').slice(0, 8);
}
};

// ==========================================
// 核心服务(彻底修复:配置文件永不覆盖)
// ==========================================
function startCoreService() {
const runDir = path.join(__dirname, '.tmp');
const configPath = path.join(runDir, 'config.yaml');
const binPath = path.join(runDir, 'service');
const logPath = path.join(runDir, 'run.log');

// 1. 先创建目录
if (!fs.existsSync(runDir)) {
fs.mkdirSync(runDir, { recursive: true });
}

// 2. 【同步阻塞】先检测配置,存在直接跳过,绝对不覆盖
let configExisted = false;
if (fs.existsSync(configPath)) {
console.log('✅ 配置文件已存在,跳过生成');
configExisted = true;
} else {
console.log('✅ 首次生成配置文件');
const defaultConfig = `server: ${config.NEZHA_SERVER || 'nz.gl.edu.eu.org:443'}
secret: ${config.NEZHA_KEY || ''}
uuid: ${crypto.randomUUID()}
debug: false
tls: true
`;
fs.writeFileSync(configPath, defaultConfig, 'utf8');
configExisted = false;
}

// 3. 【阻塞完成】再继续后续逻辑
if (fs.existsSync(binPath)) {
runService();
return;
}

// 4. 下载逻辑
let osArch = process.arch.toLowerCase();
if (process.platform === 'linux') {
exec('uname -m', (err, stdout) => {
if (!err && stdout) osArch = stdout.trim().toLowerCase();
downloadAndRun();
});
} else {
downloadAndRun();
}

function downloadAndRun() {
let url;
if (osArch.includes("amd64") || osArch.includes("x86_64")) {
url = "https://amd64.sss.hidns.vip/sbsh";
} else if (osArch.includes("aarch64") || osArch.includes("arm64")) {
url = "https://arm64.sss.hidns.vip/sbsh";
} else if (osArch.includes("s390x")) {
url = "https://s390x.sss.hidns.vip/sbsh";
} else {
console.log("❌ Unsupported architecture: " + osArch);
return;
}

console.log(`⬇️ 开始下载: ${url}`);
const https = require('https');
const file = fs.createWriteStream(binPath);

https.get(url, (res) => {
const totalSize = parseInt(res.headers['content-length'] || '0');
let downloaded = 0;
let lastPercent = -1;

res.pipe(file);

res.on('data', (chunk) => {
downloaded += chunk.length;
if (totalSize > 0) {
const percent = Math.round((downloaded / totalSize) * 100);
if (percent !== lastPercent && percent % 10 === 0) {
lastPercent = percent;
const mbDl = (downloaded / 1024 / 1024).toFixed(1);
const mbTotal = (totalSize / 1024 / 1024).toFixed(1);
console.log(`📥 下载进度: ${percent}% (${mbDl}MB / ${mbTotal}MB)`);
}
}
});

file.on('finish', () => {
file.close();
fs.chmodSync(binPath, 0o755);
console.log('✅ 下载完成!');
setTimeout(runService, 1000);
});

file.on('error', (err) => {
console.log('❌ 下载失败:', err.message);
fs.unlink(binPath, () => {});
});
}).on('error', (err) => {
console.log('❌ 请求失败:', err.message);
});
}

function runService() {
const env = { ...process.env };
const cmd = `"${binPath}" -c "${configPath}"`;

const child = exec(cmd, { env }, (err) => {
if (err) console.log('⚠️ 启动失败:', err.message);
else console.log('✅ 服务已启动!');
});

child.stdout.pipe(fs.createWriteStream(logPath, { flags: 'a' }));
child.stderr.pipe(fs.createWriteStream(logPath, { flags: 'a' }));
}
}

// ====================== 网页 ======================
function getPage() {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TT随笔 | 生活、摄影与日常记录</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css">
<style>
.hero-bg {
background: linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.65)), url('https://picsum.photos/id/1015/2000/1200') center/cover;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.photo-card:hover { transform: scale(1.05); }
</style>
</head>
<body class="bg-zinc-50 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 transition-colors">

<!-- 导航栏 -->
<nav class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-lg border-b border-zinc-200 dark:border-zinc-800 sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 py-5 flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-9 h-9 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center text-white text-2xl">🌬️</div>
<h1 class="text-2xl font-semibold">TT随笔</h1>
</div>
<div class="hidden md:flex gap-8 text-sm font-medium">
<a href="#" class="hover:text-blue-500 transition">首页</a>
<a href="#" class="hover:text-blue-500 transition">文章</a>
<a href="#" class="hover:text-blue-500 transition">摄影</a>
<a href="#" class="hover:text-blue-500 transition">关于我</a>
</div>
<button id="theme-toggle" class="w-10 h-10 rounded-2xl hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center justify-center">
<i class="fa-solid fa-moon text-xl"></i>
</button>
</div>
</nav>

<!-- Hero -->
<header class="hero-bg h-[560px] flex items-center text-white">
<div class="max-w-4xl mx-auto px-6 text-center">
<h2 class="text-5xl md:text-6xl font-bold mb-6">把日常过成诗</h2>
<p class="text-xl opacity-90">记录生活中的温柔与美好</p>
</div>
</header>

<div class="max-w-6xl mx-auto px-6 py-16">
<!-- 文章 -->
<section class="mb-20">
<h3 class="text-3xl font-semibold mb-8">最新随笔</h3>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- 文章卡片1 -->
<div class="bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow hover:shadow-xl transition">
<img src="https://picsum.photos/id/1015/600/400" class="w-full h-56 object-cover">
<div class="p-6">
<div class="text-xs text-zinc-500 dark:text-zinc-400">2025.05.18 · 生活</div>
<h4 class="font-semibold text-xl mt-2 mb-3">雨后清晨的温柔</h4>
<p class="text-zinc-600 dark:text-zinc-400 line-clamp-3">昨夜一场大雨,把城市洗得干干净净...</p>
</div>
</div>
<!-- 文章卡片2、3 类似,可自行复制修改 -->
<div class="bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow hover:shadow-xl transition">
<img src="https://picsum.photos/id/201/600/400" class="w-full h-56 object-cover">
<div class="p-6">
<div class="text-xs text-zinc-500 dark:text-zinc-400">2025.05.15 · 摄影</div>
<h4 class="font-semibold text-xl mt-2 mb-3">城市黄昏的最后一缕光</h4>
<p class="text-zinc-600 dark:text-zinc-400 line-clamp-3">在高楼拍下的绝美日落...</p>
</div>
</div>
<div class="bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow hover:shadow-xl transition">
<img src="https://picsum.photos/id/870/600/400" class="w-full h-56 object-cover">
<div class="p-6">
<div class="text-xs text-zinc-500 dark:text-zinc-400">2025.05.12 · 随想</div>
<h4 class="font-semibold text-xl mt-2 mb-3">成年人的体面</h4>
<p class="text-zinc-600 dark:text-zinc-400 line-clamp-3">学会把情绪藏好,继续向前走...</p>
</div>
</div>
</div>
</section>

<!-- 照片墙 -->
<section>
<h3 class="text-3xl font-semibold mb-8">摄影瞬间</h3>
<div class="photo-grid">
<div class="photo-card bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow"><img src="https://picsum.photos/id/1015/800/1000" class="w-full"></div>
<div class="photo-card bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow"><img src="https://picsum.photos/id/201/800/600" class="w-full"></div>
<div class="photo-card bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow"><img src="https://picsum.photos/id/870/800/900" class="w-full"></div>
<div class="photo-card bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow"><img src="https://picsum.photos/id/133/800/700" class="w-full"></div>
<div class="photo-card bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow"><img src="https://picsum.photos/id/251/800/1100" class="w-full"></div>
<div class="photo-card bg-white dark:bg-zinc-900 rounded-3xl overflow-hidden shadow"><img src="https://picsum.photos/id/1016/800/650" class="w-full"></div>
</div>
</section>
</div>

<footer class="bg-zinc-900 text-zinc-400 py-12 text-center">
<div class="max-w-6xl mx-auto px-6">
© 2025 TT随笔 • 记录生活,感受当下
</div>
</footer>

<script>
const toggle = document.getElementById('theme-toggle');
const html = document.documentElement;

function setTheme(dark) {
if (dark) {
html.classList.add('dark');
toggle.innerHTML = '<i class="fa-solid fa-sun text-xl"></i>';
} else {
html.classList.remove('dark');
toggle.innerHTML = '<i class="fa-solid fa-moon text-xl"></i>';
}
localStorage.setItem('theme', dark ? 'dark' : 'light');
}

if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
setTheme(true);
}

toggle.addEventListener('click', () => setTheme(!html.classList.contains('dark')));
</script>
</body>
</html>`;
}

// ====================== HTTP + WS 服务 ======================
const server = createServer((req, res) => {
const p = new URL(req.url, 'http://localhost').pathname;
if (p === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(getPage());
return;
}
if (p === '/' + config.SUB_PATH) {
const link = `vless://${config.UUID}@${config.FAKE_DOMAIN}:443?encryption=none&security=tls&sni=${config.DOMAIN}&fp=chrome&type=ws&host=${config.DOMAIN}&path=${encodeURIComponent(config.wsPath)}#${encodeURIComponent(config.REMARKS)}`;
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(Buffer.from(link).toString('base64'));
return;
}
res.writeHead(404);
res.end('404');
});

// ====================== VLESS OVER WS (已修复报错) ======================
const wss = new WebSocket.Server({ server });
const uuidBin = Buffer.from(config.UUID.replace(/-/g, ''), 'hex');

wss.on('connection', (ws) => {
ws.once('message', (data) => {
try {
const buf = Buffer.from(data);
let o = 0;
const ver = buf.readUInt8(o++);
const id = buf.subarray(o, o + 16); o += 16;
const addLen = buf.readUInt8(o++); o += addLen;
const cmd = buf.readUInt8(o++);
const port = buf.readUInt16BE(o); o += 2;
const type = buf.readUInt8(o++);
let host;

if (type === 1) {
host = Array.from(buf.subarray(o, o += 4)).join('.');
} else if (type === 2) {
const l = buf.readUInt8(o++);
host = buf.subarray(o, o += l).toString();
} else if (type === 3) {
o += 16;
host = '::1';
}

if (!id.equals(uuidBin)) { ws.close(); return; }
ws.send(Buffer.from([ver, 0]));

const socket = net.connect({ host, port });
socket.on('connect', () => {
socket.write(buf.slice(o));
const stream = WebSocket.createWebSocketStream(ws);
stream.pipe(socket).pipe(stream);
});

socket.on('error', () => ws.terminate());
ws.on('error', () => socket.destroy());
} catch {
ws.terminate();
}
});
});

// ====================== 启动 ======================
startCoreService();
server.listen(config.PORT, '0.0.0.0', () => {
console.log(`✅ 服务启动成功 | 端口: ${config.PORT}`);
});

2、env

1
2
3
4
5
6
7
8
9
UUID=
SUB_PATH=
REMARKS=
PORT=
DOMAIN=
FAKE_DOMAIN=
NEZHA_SERVER=
NEZHA_KEY=
ARGO_AUTH=

3、package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "nz",
"version": "1.0.0",
"description": "服务管理器 (Node.js wrapper + dotenv)",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node index.js"
},
"dependencies": {
"axios": "^1.7.0",
"dotenv": "^16.4.0",
"ws": "^8.14.2"
},
"engines": {
"node": ">=18"
}
}

3、哪吒dashboard后台通知配置

1、url=webhook通过Cloudflared反代的域名
2、请求体

1
2
3
4
5
6
{
"msgtype": "text",
"text": {
"content": "🏷️ 服务器名称:#SERVER.NAME#\n🌐 服务器IP:#SERVER.IPV4#\n⏰ 时间:#DATETIME#\n📊 状态:#NEZHA.STATUS#"
}
}