</> server.js

263 行9739 字节.js
📥 下载源码 🔙 返回工具
1class=class="string">"keyword">const http = class=class="string">"keyword">require(class="string">'http');
2class=class="string">"keyword">const QRCode = class=class="string">"keyword">require(class="string">'qrcode');
3class=class="string">"keyword">const { PDFDocument } = class=class="string">"keyword">require(class="string">'pdf-lib');
4class=class="string">"keyword">const { IncomingForm } = class=class="string">"keyword">require(class="string">'formidable');
5class=class="string">"keyword">const sharp = class=class="string">"keyword">require(class="string">'sharp');
6class=class="string">"keyword">const fs = class=class="string">"keyword">require(class="string">'fs');
7class=class="string">"keyword">const path = class=class="string">"keyword">require(class="string">'path');
8class=class="string">"keyword">const os = class=class="string">"keyword">require(class="string">'os');
9
10class=class="string">"keyword">const PORT = class="number">3001;
11class=class="string">"keyword">const TMP_DIR = path.join(os.tmpdir(), class="string">'tools_server');
12class=class="string">"keyword">const MAX_TMP_AGE = class="number">60 * class="number">60 * class="number">1000; class=class="string">"keyword">class=class="string">"comment">// class="number">1 hour
13
14class=class="string">"keyword">class=class="string">"comment">// Ensure temp directory exists and clean old files on startup
15class=class="string">"keyword">if (!fs.existsSync(TMP_DIR)) {
16 fs.mkdirSync(TMP_DIR, { recursive: true });
17} class=class="string">"keyword">else {
18 class=class="string">"keyword">class=class="string">"comment">// Clean leftover files older than class="number">1 hour
19 try {
20 class=class="string">"keyword">const now = Date.now();
21 class=class="string">"keyword">const files = fs.readdirSync(TMP_DIR);
22 class=class="string">"keyword">for (class=class="string">"keyword">const f of files) {
23 class=class="string">"keyword">const fp = path.join(TMP_DIR, f);
24 try {
25 class=class="string">"keyword">const stat = fs.statSync(fp);
26 class=class="string">"keyword">if (now - stat.mtimeMs > MAX_TMP_AGE) {
27 fs.unlinkSync(fp);
28 console.class="func">log(class="string">'Cleaned old temp file:', f);
29 }
30 } class="func">catch(e) {}
31 }
32 } class="func">catch(e) { console.error(class="string">'Startup cleanup error:', e.message); }
33}
34
35class=class="string">"keyword">class=class="string">"comment">// Periodic cleanup every class="number">30 minutes
36class="func">setInterval(() => {
37 try {
38 class=class="string">"keyword">const now = Date.now();
39 class=class="string">"keyword">const files = fs.readdirSync(TMP_DIR);
40 class=class="string">"keyword">for (class=class="string">"keyword">const f of files) {
41 class=class="string">"keyword">const fp = path.join(TMP_DIR, f);
42 try {
43 class=class="string">"keyword">const stat = fs.statSync(fp);
44 class=class="string">"keyword">if (now - stat.mtimeMs > MAX_TMP_AGE) {
45 fs.unlinkSync(fp);
46 }
47 } class="func">catch(e) {}
48 }
49 } class="func">catch(e) { console.error(class="string">'Periodic cleanup error:', e.message); }
50}, class="number">30 * class="number">60 * class="number">1000);
51
52class=class="string">"keyword">class=class="string">"comment">// Safe file cleanup
53class=class="string">"keyword">function safeCleanup(...paths) {
54 class=class="string">"keyword">for (class=class="string">"keyword">const p of paths) {
55 try { class=class="string">"keyword">if (p && fs.existsSync(p)) fs.unlinkSync(p); } class="func">catch(e) {}
56 }
57}
58
59class=class="string">"keyword">function parseForm(req) {
60 class=class="string">"keyword">return class=class="string">"keyword">new Promise((resolve, reject) => {
61 class=class="string">"keyword">const form = class=class="string">"keyword">new IncomingForm({ uploadDir: TMP_DIR, keepExtensions: true });
62 form.parse(req, (err, fields, files) => {
63 class=class="string">"keyword">if (err) class=class="string">"keyword">return reject(err);
64 resolve({ fields, files });
65 });
66 });
67}
68
69class=class="string">"keyword">function readBody(req) {
70 class=class="string">"keyword">return class=class="string">"keyword">new Promise((resolve, reject) => {
71 class=class="string">"keyword">let body = class="string">'';
72 req.on(class="string">'data', chunk => body += chunk);
73 req.on(class="string">'end', () => {
74 try { resolve(JSON.parse(body)); }
75 catch (e) { reject(class=class="string">"keyword">new Error(class="string">'Invalid JSON')); }
76 });
77 });
78}
79
80class=class="string">"keyword">const server = http.createServer(async (req, res) => {
81 res.setHeader(class="string">'Access-Control-Allow-Origin', class="string">'*');
82 res.setHeader(class="string">'Access-Control-Allow-Methods', class="string">'POST, GET, OPTIONS');
83 res.setHeader(class="string">'Access-Control-Allow-Headers', class="string">'Content-Type');
84
85 class=class="string">"keyword">if (req.method === class="string">'OPTIONS') {
86 res.writeHead(class="number">204);
87 res.end();
88 class=class="string">"keyword">return;
89 }
90
91 try {
92 class=class="string">"keyword">const url = class=class="string">"keyword">new URL(req.url, `http:class=class="string">"keyword">class=class="string">"comment">//localhost:${PORT}`);
93 class=class="string">"keyword">const route = url.pathname;
94
95 class=class="string">"keyword">if (route === class="string">'/qr' && req.method === class="string">'POST') {
96 class=class="string">"keyword">class=class="string">"comment">// QR code: pure in-memory, no temp files
97 class=class="string">"keyword">const body = await readBody(req);
98 class=class="string">"keyword">const content = (body.content || class="string">'').trim();
99 class=class="string">"keyword">if (!content) throw class=class="string">"keyword">new Error(class="string">'Content is required');
100 class=class="string">"keyword">const size = parseInt(body.size) || class="number">5;
101 class=class="string">"keyword">const level = (body.level || class="string">'M').toUpperCase();
102 class=class="string">"keyword">const validLevels = [class="string">'L', class="string">'M', class="string">'Q', class="string">'H'];
103 class=class="string">"keyword">const ecLevel = validLevels.includes(level) ? level : class="string">'M';
104 class=class="string">"keyword">const width = [class="number">200, class="number">350, class="number">500, class="number">800][[class="number">3, class="number">5, class="number">7, class="number">10].indexOf(size)] || class="number">350;
105 class=class="string">"keyword">const qrBuffer = await QRCode.toBuffer(content, {
106 type: class="string">'png', width, margin: class="number">2, errorCorrectionLevel: ecLevel
107 });
108 res.writeHead(class="number">200, {
109 class="string">'Content-Type': class="string">'image/png',
110 class="string">'Content-Length': qrBuffer.length,
111 class="string">'Cache-Control': class="string">'no-cache'
112 });
113 res.end(qrBuffer);
114
115 } class=class="string">"keyword">else class=class="string">"keyword">if (route === class="string">'/pdf-merge' && req.method === class="string">'POST') {
116 class=class="string">"keyword">const { files } = await parseForm(req);
117 class=class="string">"keyword">const pdfFiles = [];
118 try {
119 class=class="string">"keyword">for (class=class="string">"keyword">const key of Object.keys(files)) {
120 class=class="string">"keyword">const file = files[key];
121 class=class="string">"keyword">const arr = Array.isArray(file) ? file : [file];
122 class=class="string">"keyword">for (class=class="string">"keyword">const f of arr) {
123 class=class="string">"keyword">if (f.mimetype === class="string">'application/pdf' || f.originalFilename.toLowerCase().endsWith(class="string">'.pdf')) {
124 pdfFiles.push(f.filepath);
125 }
126 }
127 }
128 class=class="string">"keyword">if (pdfFiles.length < class="number">2) throw class=class="string">"keyword">new Error(class="string">'需要至少class="number">2个PDF文件');
129 class=class="string">"keyword">const mergedPdf = await PDFDocument.create();
130 class=class="string">"keyword">for (class=class="string">"keyword">const pdfPath of pdfFiles) {
131 class=class="string">"keyword">const pdfBytes = fs.readFileSync(pdfPath);
132 class=class="string">"keyword">const pdf = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
133 class=class="string">"keyword">const indices = pdf.getPageIndices();
134 class=class="string">"keyword">const pages = await mergedPdf.copyPages(pdf, indices);
135 pages.forEach(page => mergedPdf.addPage(page));
136 }
137 class=class="string">"keyword">const mergedBytes = await mergedPdf.save();
138 res.writeHead(class="number">200, {
139 class="string">'Content-Type': class="string">'application/pdf',
140 class="string">'Content-Disposition': class="string">'attachment; filename="merged.pdf"',
141 class="string">'Content-Length': mergedBytes.length
142 });
143 res.end(mergedBytes);
144 } finally {
145 safeCleanup(...pdfFiles);
146 }
147
148 } class=class="string">"keyword">else class=class="string">"keyword">if (route === class="string">'/photo-restore' && req.method === class="string">'POST') {
149 class=class="string">"keyword">const { files } = await parseForm(req);
150 class=class="string">"keyword">let inputPath = null;
151 class=class="string">"keyword">let outputPath = null;
152 try {
153 class=class="string">"keyword">let imgFile = null;
154 class=class="string">"keyword">for (class=class="string">"keyword">const key of Object.keys(files)) {
155 class=class="string">"keyword">const file = files[key];
156 class=class="string">"keyword">const arr = Array.isArray(file) ? file : [file];
157 class=class="string">"keyword">for (class=class="string">"keyword">const f of arr) {
158 class=class="string">"keyword">if (f.mimetype && f.mimetype.startsWith(class="string">'image/')) { imgFile = f; class=class="string">"keyword">break; }
159 }
160 class=class="string">"keyword">if (imgFile) class=class="string">"keyword">break;
161 }
162 class=class="string">"keyword">if (!imgFile) throw class=class="string">"keyword">new Error(class="string">'请上传一张图片');
163 inputPath = imgFile.filepath;
164 outputPath = path.join(TMP_DIR, class="string">'restored_' + Date.now() + class="string">'.jpg');
165 class=class="string">"keyword">const metadata = await sharp(inputPath).metadata();
166 class=class="string">"keyword">let pipeline = sharp(inputPath);
167 class=class="string">"keyword">if (metadata.channels && metadata.channels >= class="number">3) {
168 pipeline = pipeline.normalize()
169 .modulate({ brightness: class="number">1.05, saturation: class="number">1.15 })
170 .sharpen({ sigma: class="number">1.2, m1: class="number">0, m2: class="number">3, x1: class="number">3, y2: class="number">15, y3: class="number">15 });
171 } class=class="string">"keyword">else {
172 pipeline = pipeline.normalize()
173 .sharpen({ sigma: class="number">1.5, m1: class="number">0, m2: class="number">3, x1: class="number">3, y2: class="number">15, y3: class="number">15 });
174 }
175 await pipeline.jpeg({ quality: class="number">92, mozjpeg: true }).toFile(outputPath);
176 class=class="string">"keyword">const resultBuffer = fs.readFileSync(outputPath);
177 res.writeHead(class="number">200, {
178 class="string">'Content-Type': class="string">'image/jpeg',
179 class="string">'Content-Length': resultBuffer.length,
180 class="string">'Cache-Control': class="string">'no-cache'
181 });
182 res.end(resultBuffer);
183 } finally {
184 safeCleanup(inputPath, outputPath);
185 }
186
187
188 } class=class="string">"keyword">else class=class="string">"keyword">if (route === class="string">'/stats') {
189 class=class="string">"keyword">var counterFile = class="string">'/www/wwwroot/273059_cn/data/counter.json';
190 class=class="string">"keyword">var count = class="number">0, updated = null;
191 try {
192 class=class="string">"keyword">var data = fs.readFileSync(counterFile, class="string">'utf-class="number">8');
193 class=class="string">"keyword">var parsed = JSON.parse(data);
194 count = parsed.count || class="number">0;
195 updated = parsed.updated || null;
196 } class="func">catch(e) {}
197 class=class="string">"keyword">var stats = {
198 uptime: process.uptime(),
199 memory: process.memoryUsage(),
200 count: count,
201 updated: updated,
202 node: process.version,
203 platform: process.platform,
204 arch: process.arch,
205 pid: process.pid,
206 pages: [
207 { name: class="string">'首页', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/' },
208 { name: class="string">'不务正业', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/games/' },
209 { name: class="string">'瞎捣鼓', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/' },
210 { name: class="string">'交作业', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/download/' },
211 { name: class="string">'留言', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/liuyan.php' },
212 { name: class="string">'关于', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/blog/' },
213 { name: class="string">'毒鸡汤', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/soup.php' },
214 { name: class="string">'极客工具箱', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/geek-tools.php' },
215 { name: class="string">'图片压缩', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/image-compress.php' },
216 { name: class="string">'二维码生成', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/qr-generator.php' },
217 { name: class="string">'PDF合并', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/pdf-merge.php' },
218 { name: class="string">'网络测速', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/speed-test.php' },
219 { name: class="string">'天气卡片', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/weather-card.php' },
220 { name: class="string">'老照片修复', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/photo-restore.php' },
221 { name: class="string">'雷电战机', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/games/thunder-tank/' },
222 { name: class="string">'贪吃蛇', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/games/snake/' },
223 { name: class="string">'打字练习', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/python-typing-game.html' },
224 { name: class="string">'仪表盘', url: class="string">'https:class="keyword">class="comment">//class="number">273059.cn/tools/dashboard.php' }
225 ],
226 timestamp: class=class="string">"keyword">new class="func">Date().toISOString()
227 };
228 res.writeHead(class="number">200, { class="string">'Content-Type': class="string">'application/json' });
229 res.end(JSON.stringify(stats));
230
231 } class=class="string">"keyword">else class=class="string">"keyword">if (route === class="string">'/counter') {
232 class=class="string">"keyword">const counterFile = class="string">'/www/wwwroot/273059_cn/data/counter.json';
233 class=class="string">"keyword">let count = class="number">0;
234 try {
235 class=class="string">"keyword">const data = fs.readFileSync(counterFile, class="string">'utf-class="number">8');
236 count = JSON.parse(data).count || class="number">0;
237 } class="func">catch(e) {}
238 class=class="string">"keyword">if (req.method === class="string">'POST') {
239 count++;
240 try {
241 class=class="string">"keyword">const dirName = path.dirname(counterFile);
242 class=class="string">"keyword">if (!fs.existsSync(dirName)) fs.mkdirSync(dirName, { recursive: true });
243 fs.writeFileSync(counterFile, JSON.stringify({ count, updated: class=class="string">"keyword">new class="func">Date().toISOString() }));
244 } class="func">catch(e) { console.error(class="string">'Counter write error:', e.message); }
245 }
246 res.writeHead(class="number">200, { class="string">'Content-Type': class="string">'application/json' });
247 res.end(JSON.stringify({ count }));
248
249 } class=class="string">"keyword">else {
250 res.writeHead(class="number">404);
251 res.end(class="string">'Not found');
252 }
253 } catch (err) {
254 console.error(class="string">'Error:', err.message);
255 res.writeHead(class="number">500, { class="string">'Content-Type': class="string">'text/plain; charset=utf-class="number">8' });
256 res.end(err.message || class="string">'Internal error');
257 }
258});
259
260server.listen(PORT, class="string">'class="number">127.0.class="number">0.1', () => {
261 console.class="func">log(`Tools server running on http:class=class="string">"keyword">class=class="string">"comment">//class="number">127.0.class="number">0.1:${PORT}`);
262});
263

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/273059_cn/tools/source-viewer.php:69) in /www/wwwroot/273059_cn/tools/source-viewer.php on line 72

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/273059_cn/tools/source-viewer.php:69) in /www/wwwroot/273059_cn/tools/source-viewer.php on line 73
const http = require('http'); const QRCode = require('qrcode'); const { PDFDocument } = require('pdf-lib'); const { IncomingForm } = require('formidable'); const sharp = require('sharp'); const fs = require('fs'); const path = require('path'); const os = require('os'); const PORT = 3001; const TMP_DIR = path.join(os.tmpdir(), 'tools_server'); const MAX_TMP_AGE = 60 * 60 * 1000; // 1 hour // Ensure temp directory exists and clean old files on startup if (!fs.existsSync(TMP_DIR)) { fs.mkdirSync(TMP_DIR, { recursive: true }); } else { // Clean leftover files older than 1 hour try { const now = Date.now(); const files = fs.readdirSync(TMP_DIR); for (const f of files) { const fp = path.join(TMP_DIR, f); try { const stat = fs.statSync(fp); if (now - stat.mtimeMs > MAX_TMP_AGE) { fs.unlinkSync(fp); console.log('Cleaned old temp file:', f); } } catch(e) {} } } catch(e) { console.error('Startup cleanup error:', e.message); } } // Periodic cleanup every 30 minutes setInterval(() => { try { const now = Date.now(); const files = fs.readdirSync(TMP_DIR); for (const f of files) { const fp = path.join(TMP_DIR, f); try { const stat = fs.statSync(fp); if (now - stat.mtimeMs > MAX_TMP_AGE) { fs.unlinkSync(fp); } } catch(e) {} } } catch(e) { console.error('Periodic cleanup error:', e.message); } }, 30 * 60 * 1000); // Safe file cleanup function safeCleanup(...paths) { for (const p of paths) { try { if (p && fs.existsSync(p)) fs.unlinkSync(p); } catch(e) {} } } function parseForm(req) { return new Promise((resolve, reject) => { const form = new IncomingForm({ uploadDir: TMP_DIR, keepExtensions: true }); form.parse(req, (err, fields, files) => { if (err) return reject(err); resolve({ fields, files }); }); }); } function readBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { reject(new Error('Invalid JSON')); } }); }); } const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } try { const url = new URL(req.url, `http://localhost:${PORT}`); const route = url.pathname; if (route === '/qr' && req.method === 'POST') { // QR code: pure in-memory, no temp files const body = await readBody(req); const content = (body.content || '').trim(); if (!content) throw new Error('Content is required'); const size = parseInt(body.size) || 5; const level = (body.level || 'M').toUpperCase(); const validLevels = ['L', 'M', 'Q', 'H']; const ecLevel = validLevels.includes(level) ? level : 'M'; const width = [200, 350, 500, 800][[3, 5, 7, 10].indexOf(size)] || 350; const qrBuffer = await QRCode.toBuffer(content, { type: 'png', width, margin: 2, errorCorrectionLevel: ecLevel }); res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': qrBuffer.length, 'Cache-Control': 'no-cache' }); res.end(qrBuffer); } else if (route === '/pdf-merge' && req.method === 'POST') { const { files } = await parseForm(req); const pdfFiles = []; try { for (const key of Object.keys(files)) { const file = files[key]; const arr = Array.isArray(file) ? file : [file]; for (const f of arr) { if (f.mimetype === 'application/pdf' || f.originalFilename.toLowerCase().endsWith('.pdf')) { pdfFiles.push(f.filepath); } } } if (pdfFiles.length < 2) throw new Error('需要至少2个PDF文件'); const mergedPdf = await PDFDocument.create(); for (const pdfPath of pdfFiles) { const pdfBytes = fs.readFileSync(pdfPath); const pdf = await PDFDocument.load(pdfBytes, { ignoreEncryption: true }); const indices = pdf.getPageIndices(); const pages = await mergedPdf.copyPages(pdf, indices); pages.forEach(page => mergedPdf.addPage(page)); } const mergedBytes = await mergedPdf.save(); res.writeHead(200, { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename="merged.pdf"', 'Content-Length': mergedBytes.length }); res.end(mergedBytes); } finally { safeCleanup(...pdfFiles); } } else if (route === '/photo-restore' && req.method === 'POST') { const { files } = await parseForm(req); let inputPath = null; let outputPath = null; try { let imgFile = null; for (const key of Object.keys(files)) { const file = files[key]; const arr = Array.isArray(file) ? file : [file]; for (const f of arr) { if (f.mimetype && f.mimetype.startsWith('image/')) { imgFile = f; break; } } if (imgFile) break; } if (!imgFile) throw new Error('请上传一张图片'); inputPath = imgFile.filepath; outputPath = path.join(TMP_DIR, 'restored_' + Date.now() + '.jpg'); const metadata = await sharp(inputPath).metadata(); let pipeline = sharp(inputPath); if (metadata.channels && metadata.channels >= 3) { pipeline = pipeline.normalize() .modulate({ brightness: 1.05, saturation: 1.15 }) .sharpen({ sigma: 1.2, m1: 0, m2: 3, x1: 3, y2: 15, y3: 15 }); } else { pipeline = pipeline.normalize() .sharpen({ sigma: 1.5, m1: 0, m2: 3, x1: 3, y2: 15, y3: 15 }); } await pipeline.jpeg({ quality: 92, mozjpeg: true }).toFile(outputPath); const resultBuffer = fs.readFileSync(outputPath); res.writeHead(200, { 'Content-Type': 'image/jpeg', 'Content-Length': resultBuffer.length, 'Cache-Control': 'no-cache' }); res.end(resultBuffer); } finally { safeCleanup(inputPath, outputPath); } } else if (route === '/stats') { var counterFile = '/www/wwwroot/273059_cn/data/counter.json'; var count = 0, updated = null; try { var data = fs.readFileSync(counterFile, 'utf-8'); var parsed = JSON.parse(data); count = parsed.count || 0; updated = parsed.updated || null; } catch(e) {} var stats = { uptime: process.uptime(), memory: process.memoryUsage(), count: count, updated: updated, node: process.version, platform: process.platform, arch: process.arch, pid: process.pid, pages: [ { name: '首页', url: 'https://273059.cn/' }, { name: '不务正业', url: 'https://273059.cn/games/' }, { name: '瞎捣鼓', url: 'https://273059.cn/tools/' }, { name: '交作业', url: 'https://273059.cn/download/' }, { name: '留言', url: 'https://273059.cn/liuyan.php' }, { name: '关于', url: 'https://273059.cn/blog/' }, { name: '毒鸡汤', url: 'https://273059.cn/tools/soup.php' }, { name: '极客工具箱', url: 'https://273059.cn/tools/geek-tools.php' }, { name: '图片压缩', url: 'https://273059.cn/tools/image-compress.php' }, { name: '二维码生成', url: 'https://273059.cn/tools/qr-generator.php' }, { name: 'PDF合并', url: 'https://273059.cn/tools/pdf-merge.php' }, { name: '网络测速', url: 'https://273059.cn/tools/speed-test.php' }, { name: '天气卡片', url: 'https://273059.cn/tools/weather-card.php' }, { name: '老照片修复', url: 'https://273059.cn/tools/photo-restore.php' }, { name: '雷电战机', url: 'https://273059.cn/games/thunder-tank/' }, { name: '贪吃蛇', url: 'https://273059.cn/games/snake/' }, { name: '打字练习', url: 'https://273059.cn/python-typing-game.html' }, { name: '仪表盘', url: 'https://273059.cn/tools/dashboard.php' } ], timestamp: new Date().toISOString() }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(stats)); } else if (route === '/counter') { const counterFile = '/www/wwwroot/273059_cn/data/counter.json'; let count = 0; try { const data = fs.readFileSync(counterFile, 'utf-8'); count = JSON.parse(data).count || 0; } catch(e) {} if (req.method === 'POST') { count++; try { const dirName = path.dirname(counterFile); if (!fs.existsSync(dirName)) fs.mkdirSync(dirName, { recursive: true }); fs.writeFileSync(counterFile, JSON.stringify({ count, updated: new Date().toISOString() })); } catch(e) { console.error('Counter write error:', e.message); } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ count })); } else { res.writeHead(404); res.end('Not found'); } } catch (err) { console.error('Error:', err.message); res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end(err.message || 'Internal error'); } }); server.listen(PORT, '127.0.0.1', () => { console.log(`Tools server running on http://127.0.0.1:${PORT}`); });