Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion backend/example/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@
"enable": true,
"ttl_seconds": 60,
"srv_ttl_second": 600
},
"appImage": {
"width": 700,
"height": 389,
"ttlSeconds": 300,
"tempDir": "./data/appImage-temp"
}
}
}
75 changes: 75 additions & 0 deletions backend/routes/appImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const express = require('express');

const router = express.Router();
const logger = require('../utils/logger');
const { queryServerStatus } = require('../services/queryService');
const { generateAppImage } = require('../services/appImageRenderer');
const { default: parseHost } = require('../utils/parsehost');
const { logQuery } = require('../services/analyticsService');

router.get('/', async (req, res) => {
const clientIP = req.ip === '::1' ? '127.0.0.1' : req.ip.replace(/^::ffff:/, '');
const { ip, port, host, stype, icon, srv, lang, dark } = req.query;
const isInternalRequest = req.headers['x-internal-request'] === 'true';

let referrer = req.headers.referer || null;
if (referrer) {
try {
referrer = new URL(referrer).hostname;
} catch {
referrer = null;
}
}

const preHost = parseHost(ip, port, host);
if (!preHost.success) {
return res.status(400).json({
status: 'error',
message: preHost.message || preHost.error
});
}

const fullAddress = preHost.port ? `${preHost.ip}:${preHost.port}` : preHost.ip;
const displayHost = typeof host === 'string' && host.trim() ? host.trim() : fullAddress;
logger.debug('[APP_IMAGE]', `${clientIP} 查询 ${fullAddress}`);
res.setHeader('Content-Type', 'image/png');

try {
const serverData = await queryServerStatus(preHost.ip, preHost.port, icon, stype, Boolean(srv === 'true'), isInternalRequest);
const imageBuffer = await generateAppImage({ ...serverData, displayHost }, { lang, imageUrl: icon, dark });

logQuery({
endpoint: '/api/app_img',
ip: preHost.ip,
port: preHost.port,
clientIp: clientIP,
success: true,
serverType: serverData.type,
referrer,
from_cache: serverData.cached
});

return res.send(imageBuffer);
} catch (error) {
logger.warn('[APP_IMAGE]', `实时查服失败: ${fullAddress}`, error.message);
const imageBuffer = await generateAppImage({
status: 'offline',
host: fullAddress,
displayHost
}, { lang, imageUrl: icon, dark });

logQuery({
endpoint: '/api/app_img',
ip: preHost.ip,
port: preHost.port,
clientIp: clientIP,
success: false,
serverType: null,
referrer
});

return res.send(imageBuffer);
}
});

module.exports = router;
21 changes: 21 additions & 0 deletions backend/routes/screenshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const express = require('express');

const router = express.Router();
const { cleanupExpiredFiles, readTemporaryImage } = require('../services/appImageStorage');

router.get('/', (req, res) => {
cleanupExpiredFiles();

const result = readTemporaryImage(req.query.file);
if (!result) {
return res.status(404).json({
status: 'error',
message: '图片不存在或已过期'
});
}

res.setHeader('Content-Type', 'image/png');
return res.send(result.buffer);
});

module.exports = router;
88 changes: 88 additions & 0 deletions backend/routes/sync_appImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const express = require('express');

const router = express.Router();
const logger = require('../utils/logger');
const { queryServerStatus } = require('../services/queryService');
const { generateAppImage } = require('../services/appImageRenderer');
const { cleanupExpiredFiles, saveTemporaryImage } = require('../services/appImageStorage');
const { default: parseHost } = require('../utils/parsehost');
const { logQuery } = require('../services/analyticsService');

async function handleSync(req, res) {
const clientIP = req.ip === '::1' ? '127.0.0.1' : req.ip.replace(/^::ffff:/, '');
const params = req.method === 'POST' ? { ...req.query, ...req.body } : req.query;
const { ip, port, host, stype, icon, srv, lang, dark } = params;
const isInternalRequest = req.headers['x-internal-request'] === 'true';

let referrer = req.headers.referer || null;
if (referrer) {
try {
referrer = new URL(referrer).hostname;
} catch {
referrer = null;
}
}

const preHost = parseHost(ip, port, host);
if (!preHost.success) {
return res.status(400).json({
status: 'error',
message: preHost.message || preHost.error
});
}

const fullAddress = preHost.port ? `${preHost.ip}:${preHost.port}` : preHost.ip;
const displayHost = typeof host === 'string' && host.trim() ? host.trim() : fullAddress;
logger.debug('[SYNC_APP_IMAGE]', `${clientIP} 同步 ${fullAddress}`);

try {
cleanupExpiredFiles();

const serverData = await queryServerStatus(preHost.ip, preHost.port, icon, stype, Boolean(srv === 'true'), isInternalRequest);
const responseData = { ...serverData, displayHost };
delete responseData.cached;

const imageBuffer = await generateAppImage(responseData, { lang, imageUrl: icon, dark });
const stored = saveTemporaryImage(imageBuffer, responseData);
const screenshotUrl = `${req.protocol}://${req.get('host')}/api/screenshot?file=${stored.file}`;

logQuery({
endpoint: '/api/sync_app_img',
ip: preHost.ip,
port: preHost.port,
clientIp: clientIP,
success: true,
serverType: responseData.type,
referrer,
from_cache: serverData.cached
});

return res.json({
serverData: responseData,
screenshotUrl,
expireAt: stored.expireAt
});
} catch (error) {
logger.warn('[SYNC_APP_IMAGE]', `生成失败: ${fullAddress}`, error.message);

logQuery({
endpoint: '/api/sync_app_img',
ip: preHost.ip,
port: preHost.port,
clientIp: clientIP,
success: false,
serverType: null,
referrer
});

return res.status(500).json({
status: 'error',
message: '生成 appImage 失败'
});
}
}

router.get('/', handleSync);
router.post('/', handleSync);

module.exports = router;
11 changes: 10 additions & 1 deletion backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,19 @@ analyticsService.init();

const cacheService = require('./services/cacheService');
cacheService.init();
const appImageStorage = require('./services/appImageStorage');
appImageStorage.ensureTempDir();
appImageStorage.cleanupExpiredFiles();




// 导入所有路由模块
const statusRoute = require('./routes/status');
const statusImageRoute = require('./routes/status_img'); // 导入新的图片路由
const appImageRoute = require('./routes/appImage');
const syncAppImageRoute = require('./routes/sync_appImage');
const screenshotRoute = require('./routes/screenshot');
const configRoute = require('./routes/config');
const AuthRoute = require('./routes/auth');
const adminRoutes = require('./routes/admin');
Expand All @@ -75,6 +81,9 @@ app.use(express.json());
// --- API 路由 ---
app.use('/api/status', statusRoute);
app.use('/api/status_img', statusImageRoute); // 挂载新的图片路由
app.use('/api/app_img', appImageRoute);
app.use('/api/sync_app_img', syncAppImageRoute);
app.use('/api/screenshot', screenshotRoute);
app.use('/api/config', configRoute);
app.use('/api/login', AuthRoute);
app.use('/api/admin', adminRoutes); // 挂载 admin 路由
Expand All @@ -93,4 +102,4 @@ app.listen(PORT, (err) => {
} else {
logger.info('[SERVER]', `后端服务器正在 http://localhost:${PORT} 上运行`);
}
});
});
Loading