mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-09-04 17:23:28 +00:00
refactor(web-panel): update architecture + cheat i18n
Reorganize the panel around product capabilities and integrate the remote i18n feature from origin/master into the new structure. - Restructure src into capability features (app, trainer, library, remote-session, appearance, shared); move the bridge to bridge/src. - Add lingui-based UI localization and wrap remaining user-facing strings (Trans / msg macros); fix the 'END В·' mojibake. - Port WeMod cheat-metadata i18n: capture the access token from the snapshot's renderer in the bridge and fetch localized trainer_meta in remote-session.i18n; guarantee snapshot sync on token failure. - Wire vitest to the app's lingui/preact pipeline (mergeConfig).
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import demoSession from '../fixtures/demo-session.json' with { type: 'json' };
|
||||
import webContract from '../protocol/web-contract.json' with { type: 'json' };
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const distDir = path.join(rootDir, 'dist');
|
||||
const DEFAULT_REMOTE_PORT = webContract.defaultRemotePort;
|
||||
const DEFAULT_REMOTE_HOST = webContract.defaultRemoteHost;
|
||||
const REMOTE_BASE_PATH = webContract.basePath;
|
||||
const REMOTE_WS_PATH = webContract.webSocketPath;
|
||||
const REMOTE_HEALTH_PATH = webContract.healthPath;
|
||||
const REMOTE_ASSETS_PREFIX = webContract.assetsPath;
|
||||
const host = process.env.HOST || DEFAULT_REMOTE_HOST;
|
||||
const port = Number(process.env.PORT || DEFAULT_REMOTE_PORT);
|
||||
|
||||
const trainerMeta = structuredClone(demoSession.trainerMeta);
|
||||
const trainerValues = structuredClone(demoSession.trainerValues);
|
||||
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
function jsonMessage(type, payload, requestId = null) {
|
||||
return JSON.stringify({
|
||||
type,
|
||||
version: webContract.protocolVersion,
|
||||
requestId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
function sendSnapshot(ws) {
|
||||
ws.send(
|
||||
jsonMessage('trainer_meta', trainerMeta)
|
||||
);
|
||||
ws.send(
|
||||
jsonMessage('trainer_values', trainerValues)
|
||||
);
|
||||
}
|
||||
|
||||
function broadcast(type, payload, requestId = null) {
|
||||
const serialized = jsonMessage(type, payload, requestId);
|
||||
for (const client of wss.clients) {
|
||||
if (client.readyState === 1) {
|
||||
client.send(serialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeValue(target, value) {
|
||||
const cheat = trainerMeta.schema.cheats.find((entry) => entry.target === target);
|
||||
if (!cheat) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (cheat.type === 'toggle') {
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
if (cheat.type === 'slider' || cheat.type === 'number') {
|
||||
const numeric = typeof value === 'string' ? Number(value) : value;
|
||||
return Number.isFinite(numeric) ? numeric : trainerValues.values[target];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
if (filePath.endsWith('.html')) return 'text/html; charset=utf-8';
|
||||
if (filePath.endsWith('.js')) return 'application/javascript; charset=utf-8';
|
||||
if (filePath.endsWith('.css')) return 'text/css; charset=utf-8';
|
||||
if (filePath.endsWith('.json')) return 'application/json; charset=utf-8';
|
||||
if (filePath.endsWith('.svg')) return 'image/svg+xml';
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
async function serveFile(res, filePath) {
|
||||
try {
|
||||
const content = await readFile(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': contentTypeFor(filePath),
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(content);
|
||||
} catch {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Not found');
|
||||
}
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
|
||||
if (url.pathname === '/' || url.pathname === '') {
|
||||
res.writeHead(302, { Location: REMOTE_BASE_PATH });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) {
|
||||
res.writeHead(302, { Location: REMOTE_BASE_PATH });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === REMOTE_BASE_PATH) {
|
||||
await serveFile(res, path.join(distDir, 'index.html'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === REMOTE_HEALTH_PATH) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ ok: true, trainerId: trainerMeta.trainer.trainerId }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) {
|
||||
const relativePath = url.pathname.replace(REMOTE_BASE_PATH, '');
|
||||
await serveFile(res, path.join(distDir, relativePath));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Not found');
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
||||
if (url.pathname !== REMOTE_WS_PATH) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('error', console.error);
|
||||
|
||||
ws.on('message', (raw) => {
|
||||
try {
|
||||
const message = JSON.parse(String(raw));
|
||||
if (message?.version !== webContract.protocolVersion || typeof message?.type !== 'string' || !message?.payload) {
|
||||
ws.send(jsonMessage('error', {
|
||||
code: 'invalid_message',
|
||||
message: 'Expected a compatible protocol envelope.',
|
||||
}, message?.requestId ?? null));
|
||||
return;
|
||||
}
|
||||
if (message?.type === 'hello') {
|
||||
ws.send(
|
||||
jsonMessage('hello_ack', {
|
||||
sessionId: `sess_${Date.now()}`,
|
||||
accepted: true,
|
||||
serverVersion: '0.1.0-demo',
|
||||
protocolVersion: webContract.protocolVersion,
|
||||
}, message.requestId ?? null)
|
||||
);
|
||||
sendSnapshot(ws);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message?.type === 'set_value') {
|
||||
const target = message.payload?.target;
|
||||
if (message.payload?.trainerId !== trainerMeta.trainer.trainerId || typeof target !== 'string' || !(target in trainerValues.values)) {
|
||||
ws.send(
|
||||
jsonMessage('set_value_result', {
|
||||
ok: false,
|
||||
trainerId: trainerMeta.trainer.trainerId,
|
||||
target: typeof target === 'string' ? target : '',
|
||||
error: {
|
||||
code: 'invalid_target',
|
||||
message: 'Unknown cheat target.',
|
||||
},
|
||||
}, message.requestId ?? null)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const previousValue = trainerValues.values[target];
|
||||
const nextValue = normalizeValue(target, message.payload?.value);
|
||||
trainerValues.values[target] = nextValue;
|
||||
|
||||
ws.send(
|
||||
jsonMessage('set_value_result', {
|
||||
ok: true,
|
||||
trainerId: trainerMeta.trainer.trainerId,
|
||||
target,
|
||||
}, message.requestId ?? null)
|
||||
);
|
||||
|
||||
broadcast('value_changed', {
|
||||
trainerId: trainerMeta.trainer.trainerId,
|
||||
target,
|
||||
value: nextValue,
|
||||
oldValue: previousValue,
|
||||
source: 'remote',
|
||||
cheatId: message.payload?.cheatId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
ws.send(
|
||||
jsonMessage('error', {
|
||||
code: 'invalid_message',
|
||||
message: error instanceof Error ? error.message : 'Failed to parse client message.',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
console.log(`Wand web panel bridge listening on http://${host === DEFAULT_REMOTE_HOST ? 'localhost' : host}:${port}${REMOTE_BASE_PATH}`);
|
||||
});
|
||||
Reference in New Issue
Block a user