mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: honor WEBUI_HOST for desktop backend binding (#1897)
* fix desktop WebUI host binding * fix desktop dotenv host parsing
This commit is contained in:
@@ -32,6 +32,8 @@ const LATEST_RELEASE_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${G
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
|
||||
const DESKTOP_UPDATE_BACKUP_DIR = '.dsa-desktop-update-backup';
|
||||
const DESKTOP_UPDATE_BACKUP_MANIFEST_FILE = 'runtime-state.json';
|
||||
const DESKTOP_BACKEND_DEFAULT_HOST = '127.0.0.1';
|
||||
const PUBLIC_BIND_HOSTS = Object.freeze(new Set(['0.0.0.0', '::', '[::]', '*']));
|
||||
const MAC_DESKTOP_CLI_PATH_ENTRIES = Object.freeze([
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
@@ -691,8 +693,166 @@ function extendMacDesktopBackendPath(rawPath) {
|
||||
return entries.join(path.delimiter);
|
||||
}
|
||||
|
||||
function buildBackendEnvironment({ envFile, dbPath, logDir, port = null, sourceEnv = process.env }) {
|
||||
function normalizeBackendHost(value, fallback = '') {
|
||||
const normalized = String(value || '').trim();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function hasOwnValue(object, key) {
|
||||
return Object.prototype.hasOwnProperty.call(object || {}, key);
|
||||
}
|
||||
|
||||
function parseQuotedEnvValue(value, quote) {
|
||||
let result = '';
|
||||
for (let index = 1; index < value.length; index += 1) {
|
||||
const char = value[index];
|
||||
if (char === quote) {
|
||||
if (quote === '"') {
|
||||
return result.replace(/\\([nrt"\\$])/g, (_match, escaped) => {
|
||||
if (escaped === 'n') {
|
||||
return '\n';
|
||||
}
|
||||
if (escaped === 'r') {
|
||||
return '\r';
|
||||
}
|
||||
if (escaped === 't') {
|
||||
return '\t';
|
||||
}
|
||||
return escaped;
|
||||
});
|
||||
}
|
||||
return result.replace(/\\'/g, "'").replace(/\\\\/g, '\\');
|
||||
}
|
||||
result += char;
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function parseEnvScalarValue(rawValue) {
|
||||
const value = String(rawValue || '').trimStart();
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const quote = value[0];
|
||||
if (quote === '"' || quote === "'") {
|
||||
return parseQuotedEnvValue(value, quote);
|
||||
}
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
if (value[index] === '#' && (index === 0 || /\s/.test(value[index - 1]))) {
|
||||
return value.slice(0, index).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function expandEnvReferences(value, values = {}, sourceEnv = process.env) {
|
||||
return String(value || '').replace(
|
||||
/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-(.*?))?\}/g,
|
||||
(_match, name, defaultValue) => {
|
||||
if (hasOwnValue(sourceEnv, name)) {
|
||||
return String(sourceEnv[name]);
|
||||
}
|
||||
if (hasOwnValue(values, name)) {
|
||||
return String(values[name]);
|
||||
}
|
||||
return defaultValue === undefined ? '' : defaultValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function readEnvFileValues(envFile, sourceEnv = process.env) {
|
||||
if (!envFile || !fs.existsSync(envFile)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
content = fs.readFileSync(envFile, 'utf-8');
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const values = {};
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const match = line.match(/^\uFEFF?\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
values[match[1]] = expandEnvReferences(
|
||||
parseEnvScalarValue(match[2]),
|
||||
values,
|
||||
sourceEnv
|
||||
);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function readEnvFileValue(envFile, key, sourceEnv = process.env) {
|
||||
const values = readEnvFileValues(envFile, sourceEnv);
|
||||
return hasOwnValue(values, key) ? values[key] : null;
|
||||
}
|
||||
|
||||
function resolveBackendBindHost({
|
||||
envFile,
|
||||
sourceEnv = process.env,
|
||||
fallback = DESKTOP_BACKEND_DEFAULT_HOST,
|
||||
} = {}) {
|
||||
const sourceHost = normalizeBackendHost(sourceEnv.WEBUI_HOST);
|
||||
if (sourceHost) {
|
||||
return sourceHost;
|
||||
}
|
||||
|
||||
const envFileHost = normalizeBackendHost(readEnvFileValue(envFile, 'WEBUI_HOST', sourceEnv));
|
||||
return envFileHost || fallback;
|
||||
}
|
||||
|
||||
function resolveDesktopConnectHost(bindHost) {
|
||||
const host = normalizeBackendHost(bindHost, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
if (PUBLIC_BIND_HOSTS.has(host.toLowerCase())) {
|
||||
return DESKTOP_BACKEND_DEFAULT_HOST;
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
function formatUrlHost(host) {
|
||||
const normalized = normalizeBackendHost(host, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
if (normalized.startsWith('[') && normalized.endsWith(']')) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.includes(':') ? `[${normalized}]` : normalized;
|
||||
}
|
||||
|
||||
function buildBackendUrl(host, port, pathname = '/') {
|
||||
const url = new URL(`http://${formatUrlHost(host)}:${port}/`);
|
||||
url.pathname = pathname;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function buildBackendArgs({ host, port }) {
|
||||
return [
|
||||
'--serve-only',
|
||||
'--host',
|
||||
normalizeBackendHost(host, DESKTOP_BACKEND_DEFAULT_HOST),
|
||||
'--port',
|
||||
String(port),
|
||||
];
|
||||
}
|
||||
|
||||
function buildBackendEnvironment({
|
||||
envFile,
|
||||
dbPath,
|
||||
logDir,
|
||||
port = null,
|
||||
host = null,
|
||||
sourceEnv = process.env,
|
||||
}) {
|
||||
const selectedPort = Number(port);
|
||||
const selectedHost = normalizeBackendHost(host) || resolveBackendBindHost({ envFile, sourceEnv });
|
||||
const env = {
|
||||
...sourceEnv,
|
||||
DSA_DESKTOP_MODE: 'true',
|
||||
@@ -701,6 +861,7 @@ function buildBackendEnvironment({ envFile, dbPath, logDir, port = null, sourceE
|
||||
LOG_DIR: logDir,
|
||||
PYTHONUTF8: '1',
|
||||
PYTHONIOENCODING: 'utf-8',
|
||||
WEBUI_HOST: selectedHost,
|
||||
WEBUI_ENABLED: 'false',
|
||||
BOT_ENABLED: 'false',
|
||||
DINGTALK_STREAM_ENABLED: 'false',
|
||||
@@ -802,7 +963,8 @@ function ensureEnvFile(envPath) {
|
||||
fs.writeFileSync(envPath, '# Configure your API keys and stock list here.\n', 'utf-8');
|
||||
}
|
||||
|
||||
function findAvailablePort(startPort = 8000, endPort = 8100) {
|
||||
function findAvailablePort(startPort = 8000, endPort = 8100, host = DESKTOP_BACKEND_DEFAULT_HOST) {
|
||||
const bindHost = normalizeBackendHost(host, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
return new Promise((resolve, reject) => {
|
||||
const tryPort = (port) => {
|
||||
if (port > endPort) {
|
||||
@@ -817,7 +979,7 @@ function findAvailablePort(startPort = 8000, endPort = 8100) {
|
||||
server.once('listening', () => {
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.listen(port, '127.0.0.1');
|
||||
server.listen(port, bindHost);
|
||||
};
|
||||
|
||||
tryPort(startPort);
|
||||
@@ -982,14 +1144,15 @@ function waitForHealth(
|
||||
});
|
||||
}
|
||||
|
||||
function startBackend({ port, envFile, dbPath, logDir }) {
|
||||
function startBackend({ port, envFile, dbPath, logDir, host = null }) {
|
||||
const backendPath = resolveBackendPath();
|
||||
backendStartError = null;
|
||||
const launchStartedAt = Date.now();
|
||||
const bindHost = normalizeBackendHost(host) || resolveBackendBindHost({ envFile });
|
||||
|
||||
const env = buildBackendEnvironment({ envFile, dbPath, logDir, port });
|
||||
const env = buildBackendEnvironment({ envFile, dbPath, logDir, port, host: bindHost });
|
||||
|
||||
const args = ['--serve-only', '--host', '127.0.0.1', '--port', String(port)];
|
||||
const args = buildBackendArgs({ host: bindHost, port });
|
||||
let launchMode = '';
|
||||
let launchCommand = '';
|
||||
let launchCwd = '';
|
||||
@@ -1147,8 +1310,8 @@ function resolveDesktopVersion() {
|
||||
return String(app.getVersion() || '').trim();
|
||||
}
|
||||
|
||||
function buildMainPageUrl(port, timestamp = Date.now()) {
|
||||
const url = new URL(`http://127.0.0.1:${port}/`);
|
||||
function buildMainPageUrl(port, timestamp = Date.now(), host = DESKTOP_BACKEND_DEFAULT_HOST) {
|
||||
const url = new URL(buildBackendUrl(host, port, '/'));
|
||||
url.searchParams.set('desktop_version', resolveDesktopVersion() || 'unknown');
|
||||
url.searchParams.set('cache_bust', String(timestamp));
|
||||
return url.toString();
|
||||
@@ -1652,8 +1815,12 @@ async function createWindow() {
|
||||
ensureEnvFile(envPath);
|
||||
logStartup(`Env file ready: ${envPath}`);
|
||||
|
||||
const backendBindHost = resolveBackendBindHost({ envFile: envPath });
|
||||
const backendConnectHost = resolveDesktopConnectHost(backendBindHost);
|
||||
logStartup(`Backend bind host=${backendBindHost}; desktop connect host=${backendConnectHost}`);
|
||||
|
||||
const portFindStartedAt = Date.now();
|
||||
const port = await findAvailablePort(8000, 8100);
|
||||
const port = await findAvailablePort(8000, 8100, backendBindHost);
|
||||
logStartup(`Using port ${port} (selected in ${Date.now() - portFindStartedAt}ms)`);
|
||||
logStartup(`App directory=${appDir}`);
|
||||
|
||||
@@ -1661,7 +1828,7 @@ async function createWindow() {
|
||||
const logDir = path.join(appDir, 'logs');
|
||||
|
||||
try {
|
||||
const launchInfo = startBackend({ port, envFile: envPath, dbPath, logDir });
|
||||
const launchInfo = startBackend({ port, envFile: envPath, dbPath, logDir, host: backendBindHost });
|
||||
logStartup(`Backend launch mode=${launchInfo.mode}`);
|
||||
logStartup(`Backend launch command=${launchInfo.command}`);
|
||||
logStartup(`Backend launch cwd=${launchInfo.cwd}`);
|
||||
@@ -1673,7 +1840,7 @@ async function createWindow() {
|
||||
return;
|
||||
}
|
||||
|
||||
const healthUrl = `http://127.0.0.1:${port}/api/health`;
|
||||
const healthUrl = buildBackendUrl(backendConnectHost, port, '/api/health');
|
||||
let lastHealthProgressLogAt = 0;
|
||||
const healthProgressLogIntervalMs = 2000;
|
||||
|
||||
@@ -1738,7 +1905,7 @@ async function createWindow() {
|
||||
);
|
||||
logStartup(`Backend ready in ${healthInfo.elapsedMs}ms (${healthInfo.attempts} probes)`);
|
||||
const mainPageStartedAt = Date.now();
|
||||
const mainPageUrl = buildMainPageUrl(port);
|
||||
const mainPageUrl = buildMainPageUrl(port, Date.now(), backendConnectHost);
|
||||
await mainWindow.loadURL(mainPageUrl);
|
||||
logStartup(`Main page loadURL resolved in ${Date.now() - mainPageStartedAt}ms url=${mainPageUrl}`);
|
||||
logStartup(`Main UI loaded in ${Date.now() - startupStartedAt}ms`);
|
||||
@@ -1782,20 +1949,27 @@ module.exports = {
|
||||
UPDATE_STATUS,
|
||||
buildUpdateState,
|
||||
backupPackagedRuntimeState,
|
||||
buildBackendArgs,
|
||||
checkForDesktopUpdates,
|
||||
compareVersions,
|
||||
evaluateReleaseUpdate,
|
||||
buildBackendUrl,
|
||||
buildBackendEnvironment,
|
||||
extendMacDesktopBackendPath,
|
||||
extractReleaseMetadata,
|
||||
fetchLatestReleaseJson,
|
||||
findAvailablePort,
|
||||
buildMainPageUrl,
|
||||
migrateMacPackagedRuntimeState,
|
||||
normalizeVersionString,
|
||||
parseSemver,
|
||||
readEnvFileValue,
|
||||
resolveAppDir,
|
||||
resolveBackendBindHost,
|
||||
resolveDesktopConnectHost,
|
||||
restorePackagedRuntimeStateFromBackup,
|
||||
sanitizeReleaseUrl,
|
||||
startBackend,
|
||||
stopBackend,
|
||||
__getBackendProcessForTest() {
|
||||
return backendProcess;
|
||||
|
||||
@@ -136,6 +136,27 @@ test('buildMainPageUrl includes desktop version and cache buster', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('buildMainPageUrl uses a connect host when provided', (t) => {
|
||||
const mainModule = loadMainModule(t, {
|
||||
app: {
|
||||
getVersion: () => '3.17.1',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
mainModule.buildMainPageUrl(8123, 1234567890, '192.168.1.9'),
|
||||
'http://192.168.1.9:8123/?desktop_version=3.17.1&cache_bust=1234567890'
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveDesktopConnectHost keeps desktop navigation local for public binds', (t) => {
|
||||
const mainModule = loadMainModule(t);
|
||||
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('0.0.0.0'), '127.0.0.1');
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('::'), '127.0.0.1');
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('192.168.1.9'), '192.168.1.9');
|
||||
});
|
||||
|
||||
test('buildBackendEnvironment extends macOS GUI PATH with Homebrew CLI directories', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'darwin' });
|
||||
|
||||
@@ -160,6 +181,7 @@ test('buildBackendEnvironment extends macOS GUI PATH with Homebrew CLI directori
|
||||
assert.equal(env.ENV_FILE, '/tmp/dsa/.env');
|
||||
assert.equal(env.DATABASE_PATH, '/tmp/dsa/data.db');
|
||||
assert.equal(env.LOG_DIR, '/tmp/dsa/logs');
|
||||
assert.equal(env.WEBUI_HOST, '127.0.0.1');
|
||||
});
|
||||
|
||||
test('buildBackendEnvironment keeps non-macOS PATH unchanged', (t) => {
|
||||
@@ -192,6 +214,187 @@ test('buildBackendEnvironment pins WEBUI_PORT to the Electron-selected backend p
|
||||
});
|
||||
|
||||
assert.equal(env.WEBUI_PORT, '8000');
|
||||
assert.equal(env.WEBUI_HOST, '127.0.0.1');
|
||||
});
|
||||
|
||||
test('resolveBackendBindHost reads WEBUI_HOST from env file', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'WEBUI_HOST=0.0.0.0 # allow LAN\nWEBUI_PORT=8000\n', 'utf-8');
|
||||
|
||||
assert.equal(mainModule.readEnvFileValue(envPath, 'WEBUI_HOST'), '0.0.0.0');
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({ envFile: envPath, sourceEnv: {} }),
|
||||
'0.0.0.0'
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveBackendBindHost expands WEBUI_HOST dotenv references', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'BIND_HOST=0.0.0.0\nWEBUI_HOST=${BIND_HOST}\n', 'utf-8');
|
||||
|
||||
assert.equal(mainModule.readEnvFileValue(envPath, 'WEBUI_HOST', {}), '0.0.0.0');
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({ envFile: envPath, sourceEnv: {} }),
|
||||
'0.0.0.0'
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveBackendBindHost handles quoted WEBUI_HOST with inline comment', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'WEBUI_HOST="0.0.0.0" # allow LAN\n', 'utf-8');
|
||||
|
||||
assert.equal(mainModule.readEnvFileValue(envPath, 'WEBUI_HOST', {}), '0.0.0.0');
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({ envFile: envPath, sourceEnv: {} }),
|
||||
'0.0.0.0'
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveBackendBindHost supports dotenv default expansion', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'WEBUI_HOST=${MISSING_HOST:-127.0.0.1}\n', 'utf-8');
|
||||
|
||||
assert.equal(mainModule.readEnvFileValue(envPath, 'WEBUI_HOST', {}), '127.0.0.1');
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({ envFile: envPath, sourceEnv: {} }),
|
||||
'127.0.0.1'
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveBackendBindHost keeps process WEBUI_HOST override ahead of env file', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'WEBUI_HOST=0.0.0.0\n', 'utf-8');
|
||||
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({
|
||||
envFile: envPath,
|
||||
sourceEnv: { WEBUI_HOST: '192.168.1.9' },
|
||||
}),
|
||||
'192.168.1.9'
|
||||
);
|
||||
});
|
||||
|
||||
test('buildBackendEnvironment injects env file WEBUI_HOST into backend process', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'BIND_HOST=0.0.0.0\nWEBUI_HOST=${BIND_HOST}\n', 'utf-8');
|
||||
|
||||
const env = mainModule.buildBackendEnvironment({
|
||||
envFile: envPath,
|
||||
dbPath: 'C:\\Users\\user\\AppData\\Roaming\\Daily Stock Analysis\\data\\stock_analysis.db',
|
||||
logDir: 'C:\\Users\\user\\AppData\\Roaming\\Daily Stock Analysis\\logs',
|
||||
port: 8000,
|
||||
sourceEnv: {
|
||||
PATH: 'C:\\Windows\\System32',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(env.WEBUI_HOST, '0.0.0.0');
|
||||
assert.equal(env.WEBUI_PORT, '8000');
|
||||
});
|
||||
|
||||
test('buildBackendArgs passes resolved host to main.py', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
|
||||
assert.deepEqual(mainModule.buildBackendArgs({ host: '0.0.0.0', port: 8123 }), [
|
||||
'--serve-only',
|
||||
'--host',
|
||||
'0.0.0.0',
|
||||
'--port',
|
||||
'8123',
|
||||
]);
|
||||
});
|
||||
|
||||
test('findAvailablePort listens on requested bind host', async (t) => {
|
||||
let listenedHost = '';
|
||||
const fakeNet = {
|
||||
createServer: () => {
|
||||
const server = new EventEmitter();
|
||||
server.listen = (_port, host) => {
|
||||
listenedHost = host;
|
||||
process.nextTick(() => server.emit('listening'));
|
||||
};
|
||||
server.close = (callback) => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
return server;
|
||||
},
|
||||
};
|
||||
const mainModule = loadMainModule(t, { platform: 'win32', net: fakeNet });
|
||||
|
||||
const port = await mainModule.findAvailablePort(8123, 8123, '0.0.0.0');
|
||||
|
||||
assert.equal(port, 8123);
|
||||
assert.equal(listenedHost, '0.0.0.0');
|
||||
});
|
||||
|
||||
test('startBackend passes WEBUI_HOST from env file to backend args and env', (t) => {
|
||||
const previousWebuiHost = process.env.WEBUI_HOST;
|
||||
delete process.env.WEBUI_HOST;
|
||||
t.after(() => {
|
||||
if (previousWebuiHost === undefined) {
|
||||
delete process.env.WEBUI_HOST;
|
||||
} else {
|
||||
process.env.WEBUI_HOST = previousWebuiHost;
|
||||
}
|
||||
});
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsa-desktop-host-'));
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
const envPath = path.join(tmpDir, '.env');
|
||||
fs.writeFileSync(envPath, 'BIND_HOST=0.0.0.0\nWEBUI_HOST=${BIND_HOST}\n', 'utf-8');
|
||||
const spawned = [];
|
||||
const fakeBackendProcess = new EventEmitter();
|
||||
fakeBackendProcess.stdout = new EventEmitter();
|
||||
fakeBackendProcess.stderr = new EventEmitter();
|
||||
fakeBackendProcess.exitCode = null;
|
||||
fakeBackendProcess.signalCode = null;
|
||||
fakeBackendProcess.kill = () => true;
|
||||
const mainModule = loadMainModule(t, {
|
||||
platform: 'win32',
|
||||
childProcess: {
|
||||
spawn: (command, args, options) => {
|
||||
spawned.push({ command, args, options });
|
||||
return fakeBackendProcess;
|
||||
},
|
||||
},
|
||||
});
|
||||
t.after(() => mainModule.__setBackendProcessForTest(null));
|
||||
|
||||
mainModule.startBackend({
|
||||
port: 8123,
|
||||
envFile: envPath,
|
||||
dbPath: path.join(tmpDir, 'stock_analysis.db'),
|
||||
logDir: path.join(tmpDir, 'logs'),
|
||||
});
|
||||
|
||||
assert.equal(spawned.length, 1);
|
||||
assert.deepEqual(spawned[0].args.slice(-5), [
|
||||
'--serve-only',
|
||||
'--host',
|
||||
'0.0.0.0',
|
||||
'--port',
|
||||
'8123',
|
||||
]);
|
||||
assert.equal(spawned[0].options.env.WEBUI_HOST, '0.0.0.0');
|
||||
});
|
||||
|
||||
test('extendMacDesktopBackendPath preserves existing order and avoids duplicates', (t) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
> For user-friendly release highlights, see the [GitHub Releases](https://github.com/ZhuLinsen/daily_stock_analysis/releases) page.
|
||||
|
||||
## [Unreleased]
|
||||
- [修复] 修复 Windows 桌面端启动后端时固定传入 `--host 127.0.0.1` 导致 `.env` 中 `WEBUI_HOST=0.0.0.0` 不生效、局域网无法访问 WebUI 的问题;桌面端仍默认使用 `127.0.0.1`,仅在显式配置 `WEBUI_HOST` 后按配置绑定,并继续使用本机地址完成健康检查和窗口加载。
|
||||
- [新功能] 钉钉群机器人通知支持 — 支持通过 `DINGTALK_WEBHOOK_URL` 和 `DINGTALK_SECRET` 配置钉钉推送,并支持长文本自动切片以适配 20KB 限制。
|
||||
|
||||
- [文档] 记录 Agent `/chat/stream` progress event 契约,说明新增 `stage_start`、`stage_done`、`pipeline_timeout`、`pipeline_budget_skipped` 的字段语义、Web 兼容边界、验证方式、回滚方式;其中 `pipeline_budget_skipped` 表示剩余预算不足、未启动下一阶段即跳过的语义;本变更不触及 provider/model/Base URL 或运行时配置迁移语义。
|
||||
|
||||
@@ -267,6 +267,14 @@ win-unpacked/
|
||||
- 开发态 `npm run dev` 与打包态 `npm run build` / 安装包都会复用同一条版本注入链路,不再在 `preload.js` 里维护独立硬编码版本号
|
||||
- `README.md` 继续保留安装和运行入口说明;这类桌面端运行时细节统一落在本专题文档维护,避免入门文档膨胀
|
||||
|
||||
### 局域网访问 Windows 桌面端 WebUI
|
||||
|
||||
- 桌面端默认仍按 `WEBUI_HOST=127.0.0.1` 只允许本机访问,避免安装后无意暴露后端服务
|
||||
- 如需让同一局域网内其他设备访问,在桌面端 `.env` 或 `系统设置 -> WebUI 监听地址` 中设置 `WEBUI_HOST=0.0.0.0`,保存后重启桌面端
|
||||
- 桌面端会自动选择 `8000-8100` 中可用端口并传给后端;常见情况下仍是 `8000`,若端口被占用,可在 `logs/desktop.log` 查看 `Using port ...` 和 `Backend launch command=...`
|
||||
- Windows 防火墙或服务器安全组仍需放行实际监听端口;对外暴露前建议同时启用 `ADMIN_AUTH_ENABLED`
|
||||
- 即使后端绑定 `0.0.0.0`,桌面窗口自身仍会使用本机可访问地址完成健康检查和页面加载
|
||||
|
||||
### 桌面端更新提醒
|
||||
|
||||
- 应用在主界面加载完成后会后台检查 GitHub Releases 的最新正式版,并与当前 `app.getVersion()` 做语义化版本比较
|
||||
|
||||
Reference in New Issue
Block a user