Initial commit

This commit is contained in:
2026-03-08 01:34:54 +08:00
commit 1f104f73c8
441 changed files with 64911 additions and 0 deletions

153
remote/src/config/index.js Normal file
View File

@@ -0,0 +1,153 @@
const path = require('path');
const fs = require('fs');
const { validate, getDefaults, mergeWithDefaults } = require('./schema');
const paths = require('../utils/paths');
let cachedConfig = null;
function getConfigPath() {
const configDir = paths.getConfigPath();
return path.join(configDir, 'default.json');
}
function loadConfigFile() {
const configPath = getConfigPath();
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf8');
const parsed = JSON.parse(content);
return parsed;
}
} catch (error) {
console.error(`Failed to load config file: ${error.message}`);
}
return {};
}
function getEnvOverrides() {
const overrides = {};
const envPrefix = 'REMOTE_';
for (const [envKey, envValue] of Object.entries(process.env)) {
if (!envKey.startsWith(envPrefix)) continue;
const parts = envKey.slice(envPrefix.length).split('_');
if (parts.length < 2) continue;
const section = parts[0].toLowerCase();
const configKey = parts.slice(1).join('_').toLowerCase();
if (!overrides[section]) {
overrides[section] = {};
}
let parsedValue = envValue;
if (envValue === 'true') {
parsedValue = true;
} else if (envValue === 'false') {
parsedValue = false;
} else if (!isNaN(envValue) && envValue !== '') {
parsedValue = parseFloat(envValue);
if (Number.isInteger(parsedValue)) {
parsedValue = parseInt(envValue, 10);
}
}
overrides[section][configKey] = parsedValue;
}
return overrides;
}
function applyEnvOverrides(config, envOverrides) {
const result = { ...config };
for (const [section, values] of Object.entries(envOverrides)) {
if (!result[section]) {
result[section] = {};
}
for (const [key, value] of Object.entries(values)) {
result[section][key] = value;
}
}
return result;
}
function loadConfig() {
if (cachedConfig) {
return cachedConfig;
}
const defaults = getDefaults();
const fileConfig = loadConfigFile();
const envOverrides = getEnvOverrides();
let merged = mergeWithDefaults(fileConfig);
merged = applyEnvOverrides(merged, envOverrides);
const validation = validate(merged);
if (!validation.valid) {
console.warn('Config validation warnings:', validation.errors);
}
cachedConfig = merged;
return cachedConfig;
}
function get(key, defaultValue = undefined) {
const config = loadConfig();
if (!key) {
return config;
}
const keys = key.split('.');
let result = config;
for (const k of keys) {
if (result && typeof result === 'object' && k in result) {
result = result[k];
} else {
return defaultValue;
}
}
return result;
}
function getSection(section) {
const config = loadConfig();
if (!section) {
return config;
}
return config[section] || null;
}
function getAll() {
return loadConfig();
}
function reload() {
cachedConfig = null;
return loadConfig();
}
function clearCache() {
cachedConfig = null;
}
module.exports = {
get,
getSection,
getAll,
reload,
clearCache,
validate
};

201
remote/src/config/schema.js Normal file
View File

@@ -0,0 +1,201 @@
const defaultConfig = {
server: {
port: 3000,
host: '0.0.0.0'
},
stream: {
fps: 30,
bitrate: '4000k',
gop: 10,
preset: 'ultrafast',
resolution: {
width: 1920,
height: 1080
}
},
input: {
mouseEnabled: true,
keyboardEnabled: true,
sensitivity: 1.0
},
security: {
password: '',
tokenExpiry: 3600
},
frp: {
enabled: true
},
gitea: {
enabled: true
}
};
const schema = {
server: {
type: 'object',
properties: {
port: { type: 'number', required: true, min: 1, max: 65535 },
host: { type: 'string', required: true }
}
},
stream: {
type: 'object',
properties: {
fps: { type: 'number', required: true, min: 1, max: 120 },
bitrate: { type: 'string', required: true },
gop: { type: 'number', required: true, min: 1 },
preset: { type: 'string', required: true },
resolution: {
type: 'object',
properties: {
width: { type: 'number', required: true, min: 1 },
height: { type: 'number', required: true, min: 1 }
}
}
}
},
input: {
type: 'object',
properties: {
mouseEnabled: { type: 'boolean', required: true },
keyboardEnabled: { type: 'boolean', required: true },
sensitivity: { type: 'number', required: true, min: 0.1, max: 10 }
}
},
security: {
type: 'object',
properties: {
password: { type: 'string', required: false },
tokenExpiry: { type: 'number', required: true, min: 60 }
}
},
frp: {
type: 'object',
properties: {
enabled: { type: 'boolean', required: true }
}
},
gitea: {
type: 'object',
properties: {
enabled: { type: 'boolean', required: true }
}
}
};
function validateType(value, expectedType) {
if (expectedType === 'number') {
return typeof value === 'number' && !isNaN(value);
}
if (expectedType === 'string') {
return typeof value === 'string';
}
if (expectedType === 'boolean') {
return typeof value === 'boolean';
}
if (expectedType === 'object') {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
return false;
}
function validateField(value, fieldSchema, fieldPath) {
const errors = [];
if (!validateType(value, fieldSchema.type)) {
errors.push(`${fieldPath}: expected type ${fieldSchema.type}, got ${typeof value}`);
return errors;
}
if (fieldSchema.type === 'number') {
if (fieldSchema.min !== undefined && value < fieldSchema.min) {
errors.push(`${fieldPath}: value ${value} is less than minimum ${fieldSchema.min}`);
}
if (fieldSchema.max !== undefined && value > fieldSchema.max) {
errors.push(`${fieldPath}: value ${value} is greater than maximum ${fieldSchema.max}`);
}
}
if (fieldSchema.type === 'object' && fieldSchema.properties) {
const nestedErrors = validateObject(value, fieldSchema, fieldPath);
errors.push(...nestedErrors);
}
return errors;
}
function validateObject(obj, objectSchema, basePath) {
const errors = [];
if (!objectSchema.properties) return errors;
for (const [key, fieldSchema] of Object.entries(objectSchema.properties)) {
const fieldPath = basePath ? `${basePath}.${key}` : key;
if (!(key in obj)) {
if (fieldSchema.required) {
errors.push(`${fieldPath}: required field is missing`);
}
continue;
}
const fieldErrors = validateField(obj[key], fieldSchema, fieldPath);
errors.push(...fieldErrors);
}
return errors;
}
function validate(config) {
const errors = [];
if (!config || typeof config !== 'object') {
return { valid: false, errors: ['Config must be a non-null object'] };
}
for (const [sectionKey, sectionSchema] of Object.entries(schema)) {
if (!(sectionKey in config)) {
errors.push(`${sectionKey}: required section is missing`);
continue;
}
const sectionErrors = validateObject(config[sectionKey], sectionSchema, sectionKey);
errors.push(...sectionErrors);
}
return {
valid: errors.length === 0,
errors
};
}
function getDefaults() {
return JSON.parse(JSON.stringify(defaultConfig));
}
function mergeWithDefaults(config) {
const defaults = getDefaults();
return deepMerge(defaults, config);
}
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
result[key] = deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
module.exports = {
schema,
defaultConfig,
validate,
getDefaults,
mergeWithDefaults
};