mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
25635ddb38
* ✨ feat(task): auto-ensure qstash schedule
chore: cleanup code
chore: cleanup code
chore: cleanup code
* chore: migrate qstash init workflow to startServer
chore: migrate qstash init workflow to startServer
* fix: set default QSTASH_URL to eu region, same as SDK
fix: set default QSTASH_URL to eu region, same as SDK
254 lines
8.0 KiB
JavaScript
254 lines
8.0 KiB
JavaScript
const dns = require('node:dns').promises;
|
|
const fs = require('node:fs').promises;
|
|
const path = require('node:path');
|
|
const { spawn } = require('node:child_process');
|
|
const { existsSync } = require('node:fs');
|
|
|
|
// Resolve shared module path for both local dev and Docker environments
|
|
// Local: scripts/serverLauncher/startServer.js -> scripts/_shared/...
|
|
// Docker: /app/startServer.js -> /app/scripts/_shared/...
|
|
const localPath = path.join(__dirname, '..', '_shared', 'checkDeprecatedAuth.js');
|
|
const dockerPath = '/app/scripts/_shared/checkDeprecatedAuth.js';
|
|
const sharedModulePath = existsSync(localPath) ? localPath : dockerPath;
|
|
|
|
const { checkDeprecatedAuth } = require(sharedModulePath);
|
|
|
|
// Set file paths
|
|
const DB_MIGRATION_SCRIPT_PATH = '/app/docker.cjs';
|
|
const SERVER_SCRIPT_PATH = '/app/server.js';
|
|
const PROXYCHAINS_CONF_PATH = '/etc/proxychains4.conf';
|
|
|
|
// Function to check if a string is a valid IP address
|
|
const isValidIP = (ip, version = 4) => {
|
|
const ipv4Regex = /^(25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3}$/;
|
|
const ipv6Regex =
|
|
/^(([\da-f]{1,4}:){7}[\da-f]{1,4}|([\da-f]{1,4}:){1,7}:|([\da-f]{1,4}:){1,6}:[\da-f]{1,4}|([\da-f]{1,4}:){1,5}(:[\da-f]{1,4}){1,2}|([\da-f]{1,4}:){1,4}(:[\da-f]{1,4}){1,3}|([\da-f]{1,4}:){1,3}(:[\da-f]{1,4}){1,4}|([\da-f]{1,4}:){1,2}(:[\da-f]{1,4}){1,5}|[\da-f]{1,4}:((:[\da-f]{1,4}){1,6})|:((:[\da-f]{1,4}){1,7}|:)|fe80:(:[\da-f]{0,4}){0,4}%[\da-z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d)|([\da-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))$/;
|
|
|
|
switch (version) {
|
|
case 4: {
|
|
return ipv4Regex.test(ip);
|
|
}
|
|
case 6: {
|
|
return ipv6Regex.test(ip);
|
|
}
|
|
default: {
|
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Function to parse protocol, host and port from a URL
|
|
const parseUrl = (url) => {
|
|
const { protocol, hostname: host, port, username: user, password: pass } = new URL(url);
|
|
return { host, pass, port: port || 443, protocol: protocol.replace(':', ''), user };
|
|
};
|
|
|
|
// Function to resolve host IP via DNS
|
|
const resolveHostIP = async (host, version = 4) => {
|
|
try {
|
|
const { address } = await dns.lookup(host, { family: version });
|
|
|
|
if (!isValidIP(address, version)) {
|
|
console.error(
|
|
`❌ DNS Error: Invalid resolved IP: ${address}. IP address must be IPv${version}.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
return address;
|
|
} catch (err) {
|
|
console.error(`❌ DNS Error: Could not resolve ${host}. Check DNS server:`);
|
|
console.error(err);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
// Function to generate proxychains configuration
|
|
const runProxyChainsConfGenerator = async (url) => {
|
|
const { protocol, host, port, user, pass } = parseUrl(url);
|
|
|
|
if (!['http', 'socks4', 'socks5'].includes(protocol)) {
|
|
console.error(
|
|
`❌ ProxyChains: Invalid protocol (${protocol}). Protocol must be 'http', 'socks4' and 'socks5'.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const validPort = parseInt(port, 10);
|
|
if (isNaN(validPort) || validPort <= 0 || validPort > 65_535) {
|
|
console.error(
|
|
`❌ ProxyChains: Invalid port (${port}). Port must be a number between 1 and 65535.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
let ip = isValidIP(host, 4) ? host : await resolveHostIP(host, 4);
|
|
|
|
const proxyDNSConfig =
|
|
process.env.ENABLE_PROXY_DNS === '1'
|
|
? `
|
|
proxy_dns
|
|
remote_dns_subnet 224
|
|
`.trim()
|
|
: '';
|
|
|
|
const configContent = `
|
|
localnet 127.0.0.0/8
|
|
localnet 10.0.0.0/8
|
|
localnet 172.16.0.0/12
|
|
localnet 192.168.0.0/16
|
|
localnet ::/127
|
|
${proxyDNSConfig}
|
|
strict_chain
|
|
tcp_connect_time_out 8000
|
|
tcp_read_time_out 15000
|
|
[ProxyList]
|
|
${protocol} ${ip} ${port} ${user} ${pass}
|
|
`
|
|
.replaceAll(/\n{2,}/g, '\n')
|
|
.trim();
|
|
|
|
await fs.writeFile(PROXYCHAINS_CONF_PATH, configContent);
|
|
console.log(`✅ ProxyChains: All outgoing traffic routed via ${url}.`);
|
|
console.log('-------------------------------------');
|
|
};
|
|
|
|
// Function to execute a script with child process spawn
|
|
const runScript = (scriptPath, useProxy = false) => {
|
|
const command = useProxy
|
|
? ['/bin/proxychains', '-q', '/bin/node', scriptPath]
|
|
: ['/bin/node', scriptPath];
|
|
return new Promise((resolve, reject) => {
|
|
const process = spawn(command.shift(), command, { stdio: 'inherit' });
|
|
process.on('close', (code) =>
|
|
code === 0 ? resolve() : reject(new Error(`🔴 Process exited with code ${code}`)),
|
|
);
|
|
});
|
|
};
|
|
|
|
// Function to start the bot gateway by calling the local API endpoint
|
|
const startGateway = async () => {
|
|
const KEY_VAULTS_SECRET = process.env.KEY_VAULTS_SECRET;
|
|
if (!KEY_VAULTS_SECRET) return;
|
|
|
|
const port = process.env.PORT || 3210;
|
|
const url = `http://localhost:${port}/api/agent/gateway/start`;
|
|
const maxRetries = 10;
|
|
const retryDelay = 3000;
|
|
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${KEY_VAULTS_SECRET}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
if (res.ok) {
|
|
console.log('✅ Gateway: Started successfully.');
|
|
return;
|
|
}
|
|
|
|
console.warn(`⚠️ Gateway: Received status ${res.status}, retrying...`);
|
|
} catch {
|
|
if (i < maxRetries - 1) {
|
|
await new Promise((r) => setTimeout(r, retryDelay));
|
|
}
|
|
}
|
|
}
|
|
|
|
console.error('❌ Gateway: Failed to start after retries.');
|
|
};
|
|
|
|
// Function to create QStash schedule for dispatching workflow tasks every 10 minutes
|
|
const createQstashSchedule = async () => {
|
|
const QSTASH_URL = process.env.QSTASH_URL || 'https://qstash-eu-central-1.upstash.io';
|
|
|
|
const QSTASH_TOKEN = process.env.QSTASH_TOKEN;
|
|
if (!QSTASH_TOKEN) {
|
|
console.warn('⚠️ QStash: QSTASH_TOKEN not set. Skipping schedule creation.');
|
|
return;
|
|
}
|
|
|
|
const APP_URL = process.env.APP_URL;
|
|
if (!APP_URL) {
|
|
console.warn('⚠️ QStash: APP_URL not set. Skipping schedule creation.');
|
|
return;
|
|
}
|
|
|
|
const url = `${QSTASH_URL}/v2/schedules/${APP_URL}/api/workflows/task/schedule-dispatch`;
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${QSTASH_TOKEN}`,
|
|
'Content-Type': 'application/json',
|
|
'Upstash-Method': 'POST',
|
|
'Upstash-Cron': '*/10 * * * *',
|
|
'Upstash-Schedule-Id': 'lobe-task-schedule-dispatch',
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
if (res.ok) {
|
|
console.log('✅ QStash: Schedule created successfully.');
|
|
} else {
|
|
console.error(`❌ QStash: Failed to create schedule. Status ${res.status}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('❌ QStash: Error creating schedule:', err);
|
|
}
|
|
};
|
|
|
|
// Main function to run the server with optional proxy
|
|
const runServer = async () => {
|
|
const PROXY_URL = process.env.PROXY_URL || ''; // Default empty string to avoid undefined errors
|
|
|
|
if (PROXY_URL) {
|
|
await runProxyChainsConfGenerator(PROXY_URL);
|
|
return runScript(SERVER_SCRIPT_PATH, true);
|
|
}
|
|
return runScript(SERVER_SCRIPT_PATH);
|
|
};
|
|
|
|
// Main execution block
|
|
(async () => {
|
|
// Check for deprecated auth env vars first - fail fast if found
|
|
checkDeprecatedAuth({ action: 'restart' });
|
|
|
|
console.log('🌐 DNS Server:', dns.getServers());
|
|
console.log('-------------------------------------');
|
|
|
|
if (process.env.DATABASE_DRIVER) {
|
|
try {
|
|
await fs.access(DB_MIGRATION_SCRIPT_PATH);
|
|
|
|
await runScript(DB_MIGRATION_SCRIPT_PATH);
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
console.log(
|
|
`⚠️ DB Migration: Not found ${DB_MIGRATION_SCRIPT_PATH}. Skipping DB migration. Ensure to migrate database manually.`,
|
|
);
|
|
console.log('-------------------------------------');
|
|
} else {
|
|
console.error('❌ Error during DB migration:');
|
|
console.error(err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start gateway in background after server is ready
|
|
startGateway();
|
|
|
|
// Create QStash schedule for workflow task dispatching
|
|
createQstashSchedule();
|
|
|
|
// Run the server in either database or non-database mode
|
|
await runServer();
|
|
})();
|