mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: normalize desktop wildcard bind hosts (#1901)
* fix: normalize desktop wildcard bind hosts * fix(review-feedback-1901): 修正 docs/CHANGELOG.md 中与本 PR 无关/疑似已发布条目的范围漂移 * fix: address PR 1901 review feedback * fix: remove stale changelog entries from PR 1901
This commit is contained in:
@@ -46,6 +46,7 @@ const MAC_DESKTOP_SYSTEM_PATH_ENTRIES = Object.freeze([
|
||||
'/usr/sbin',
|
||||
'/sbin',
|
||||
]);
|
||||
const DESKTOP_BACKEND_PATH_DELIMITER = isWindows ? ';' : ':';
|
||||
const DESKTOP_UPDATE_RUNTIME_RELATIVE_FILES = Object.freeze([
|
||||
'.env',
|
||||
path.join('data', 'stock_analysis.db'),
|
||||
@@ -672,7 +673,7 @@ function extendMacDesktopBackendPath(rawPath) {
|
||||
|
||||
const seen = new Set();
|
||||
const entries = String(rawPath || '')
|
||||
.split(path.delimiter)
|
||||
.split(DESKTOP_BACKEND_PATH_DELIMITER)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.filter((entry) => {
|
||||
@@ -690,7 +691,7 @@ function extendMacDesktopBackendPath(rawPath) {
|
||||
}
|
||||
});
|
||||
|
||||
return entries.join(path.delimiter);
|
||||
return entries.join(DESKTOP_BACKEND_PATH_DELIMITER);
|
||||
}
|
||||
|
||||
function normalizeBackendHost(value, fallback = '') {
|
||||
@@ -698,6 +699,18 @@ function normalizeBackendHost(value, fallback = '') {
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function normalizeBackendBindHost(value, fallback = DESKTOP_BACKEND_DEFAULT_HOST) {
|
||||
const host = normalizeBackendHost(value, fallback);
|
||||
const lowerHost = host.toLowerCase();
|
||||
if (lowerHost === '*') {
|
||||
return '0.0.0.0';
|
||||
}
|
||||
if (lowerHost === '[::]') {
|
||||
return '::';
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
function hasOwnValue(object, key) {
|
||||
return Object.prototype.hasOwnProperty.call(object || {}, key);
|
||||
}
|
||||
@@ -804,15 +817,15 @@ function resolveBackendBindHost({
|
||||
} = {}) {
|
||||
const sourceHost = normalizeBackendHost(sourceEnv.WEBUI_HOST);
|
||||
if (sourceHost) {
|
||||
return sourceHost;
|
||||
return normalizeBackendBindHost(sourceHost, fallback);
|
||||
}
|
||||
|
||||
const envFileHost = normalizeBackendHost(readEnvFileValue(envFile, 'WEBUI_HOST', sourceEnv));
|
||||
return envFileHost || fallback;
|
||||
return normalizeBackendBindHost(envFileHost || fallback, fallback);
|
||||
}
|
||||
|
||||
function resolveDesktopConnectHost(bindHost) {
|
||||
const host = normalizeBackendHost(bindHost, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
const host = normalizeBackendBindHost(bindHost, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
if (PUBLIC_BIND_HOSTS.has(host.toLowerCase())) {
|
||||
return DESKTOP_BACKEND_DEFAULT_HOST;
|
||||
}
|
||||
@@ -837,7 +850,7 @@ function buildBackendArgs({ host, port }) {
|
||||
return [
|
||||
'--serve-only',
|
||||
'--host',
|
||||
normalizeBackendHost(host, DESKTOP_BACKEND_DEFAULT_HOST),
|
||||
normalizeBackendBindHost(host, DESKTOP_BACKEND_DEFAULT_HOST),
|
||||
'--port',
|
||||
String(port),
|
||||
];
|
||||
@@ -852,7 +865,10 @@ function buildBackendEnvironment({
|
||||
sourceEnv = process.env,
|
||||
}) {
|
||||
const selectedPort = Number(port);
|
||||
const selectedHost = normalizeBackendHost(host) || resolveBackendBindHost({ envFile, sourceEnv });
|
||||
const selectedHost = normalizeBackendBindHost(
|
||||
normalizeBackendHost(host) || resolveBackendBindHost({ envFile, sourceEnv }),
|
||||
DESKTOP_BACKEND_DEFAULT_HOST
|
||||
);
|
||||
const env = {
|
||||
...sourceEnv,
|
||||
DSA_DESKTOP_MODE: 'true',
|
||||
@@ -964,7 +980,7 @@ function ensureEnvFile(envPath) {
|
||||
}
|
||||
|
||||
function findAvailablePort(startPort = 8000, endPort = 8100, host = DESKTOP_BACKEND_DEFAULT_HOST) {
|
||||
const bindHost = normalizeBackendHost(host, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
const bindHost = normalizeBackendBindHost(host, DESKTOP_BACKEND_DEFAULT_HOST);
|
||||
return new Promise((resolve, reject) => {
|
||||
const tryPort = (port) => {
|
||||
if (port > endPort) {
|
||||
@@ -1148,7 +1164,10 @@ function startBackend({ port, envFile, dbPath, logDir, host = null }) {
|
||||
const backendPath = resolveBackendPath();
|
||||
backendStartError = null;
|
||||
const launchStartedAt = Date.now();
|
||||
const bindHost = normalizeBackendHost(host) || resolveBackendBindHost({ envFile });
|
||||
const bindHost = normalizeBackendBindHost(
|
||||
normalizeBackendHost(host) || resolveBackendBindHost({ envFile }),
|
||||
DESKTOP_BACKEND_DEFAULT_HOST
|
||||
);
|
||||
|
||||
const env = buildBackendEnvironment({ envFile, dbPath, logDir, port, host: bindHost });
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const POSIX_PATH_DELIMITER = ':';
|
||||
|
||||
function loadMainModule(t, options = {}) {
|
||||
const originalLoad = Module._load;
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
@@ -154,6 +156,8 @@ test('resolveDesktopConnectHost keeps desktop navigation local for public binds'
|
||||
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('0.0.0.0'), '127.0.0.1');
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('::'), '127.0.0.1');
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('*'), '127.0.0.1');
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('[::]'), '127.0.0.1');
|
||||
assert.equal(mainModule.resolveDesktopConnectHost('192.168.1.9'), '192.168.1.9');
|
||||
});
|
||||
|
||||
@@ -170,7 +174,7 @@ test('buildBackendEnvironment extends macOS GUI PATH with Homebrew CLI directori
|
||||
},
|
||||
});
|
||||
|
||||
const entries = env.PATH.split(path.delimiter);
|
||||
const entries = env.PATH.split(POSIX_PATH_DELIMITER);
|
||||
assert.deepEqual(entries.slice(0, 4), ['/usr/bin', '/bin', '/usr/sbin', '/sbin']);
|
||||
assert.ok(entries.includes('/opt/homebrew/bin'));
|
||||
assert.ok(entries.includes('/usr/local/bin'));
|
||||
@@ -289,6 +293,26 @@ test('resolveBackendBindHost keeps process WEBUI_HOST override ahead of env file
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveBackendBindHost normalizes wildcard WEBUI_HOST values', (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=*\n', 'utf-8');
|
||||
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({ envFile: envPath, sourceEnv: {} }),
|
||||
'0.0.0.0'
|
||||
);
|
||||
assert.equal(
|
||||
mainModule.resolveBackendBindHost({
|
||||
envFile: envPath,
|
||||
sourceEnv: { WEBUI_HOST: '[::]' },
|
||||
}),
|
||||
'::'
|
||||
);
|
||||
});
|
||||
|
||||
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-'));
|
||||
@@ -310,6 +334,30 @@ test('buildBackendEnvironment injects env file WEBUI_HOST into backend process',
|
||||
assert.equal(env.WEBUI_PORT, '8000');
|
||||
});
|
||||
|
||||
test('buildBackendEnvironment normalizes wildcard host for backend env', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
|
||||
const wildcardEnv = mainModule.buildBackendEnvironment({
|
||||
envFile: '/tmp/dsa/.env',
|
||||
dbPath: '/tmp/dsa/data.db',
|
||||
logDir: '/tmp/dsa/logs',
|
||||
port: 8000,
|
||||
host: '*',
|
||||
sourceEnv: {},
|
||||
});
|
||||
const ipv6Env = mainModule.buildBackendEnvironment({
|
||||
envFile: '/tmp/dsa/.env',
|
||||
dbPath: '/tmp/dsa/data.db',
|
||||
logDir: '/tmp/dsa/logs',
|
||||
port: 8001,
|
||||
host: '[::]',
|
||||
sourceEnv: {},
|
||||
});
|
||||
|
||||
assert.equal(wildcardEnv.WEBUI_HOST, '0.0.0.0');
|
||||
assert.equal(ipv6Env.WEBUI_HOST, '::');
|
||||
});
|
||||
|
||||
test('buildBackendArgs passes resolved host to main.py', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
|
||||
@@ -322,6 +370,25 @@ test('buildBackendArgs passes resolved host to main.py', (t) => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('buildBackendArgs normalizes wildcard hosts before spawning main.py', (t) => {
|
||||
const mainModule = loadMainModule(t, { platform: 'win32' });
|
||||
|
||||
assert.deepEqual(mainModule.buildBackendArgs({ host: '*', port: 8123 }), [
|
||||
'--serve-only',
|
||||
'--host',
|
||||
'0.0.0.0',
|
||||
'--port',
|
||||
'8123',
|
||||
]);
|
||||
assert.deepEqual(mainModule.buildBackendArgs({ host: '[::]', port: 8124 }), [
|
||||
'--serve-only',
|
||||
'--host',
|
||||
'::',
|
||||
'--port',
|
||||
'8124',
|
||||
]);
|
||||
});
|
||||
|
||||
test('findAvailablePort listens on requested bind host', async (t) => {
|
||||
let listenedHost = '';
|
||||
const fakeNet = {
|
||||
@@ -347,6 +414,31 @@ test('findAvailablePort listens on requested bind host', async (t) => {
|
||||
assert.equal(listenedHost, '0.0.0.0');
|
||||
});
|
||||
|
||||
test('findAvailablePort normalizes wildcard bind hosts before listening', async (t) => {
|
||||
const listenedHosts = [];
|
||||
const fakeNet = {
|
||||
createServer: () => {
|
||||
const server = new EventEmitter();
|
||||
server.listen = (_port, host) => {
|
||||
listenedHosts.push(host);
|
||||
process.nextTick(() => server.emit('listening'));
|
||||
};
|
||||
server.close = (callback) => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
return server;
|
||||
},
|
||||
};
|
||||
const mainModule = loadMainModule(t, { platform: 'win32', net: fakeNet });
|
||||
|
||||
await mainModule.findAvailablePort(8123, 8123, '*');
|
||||
await mainModule.findAvailablePort(8124, 8124, '[::]');
|
||||
|
||||
assert.deepEqual(listenedHosts, ['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;
|
||||
@@ -403,7 +495,7 @@ test('extendMacDesktopBackendPath preserves existing order and avoids duplicates
|
||||
const extended = mainModule.extendMacDesktopBackendPath(
|
||||
'/opt/homebrew/bin:/custom/bin:/usr/bin:/custom/bin'
|
||||
);
|
||||
const entries = extended.split(path.delimiter);
|
||||
const entries = extended.split(POSIX_PATH_DELIMITER);
|
||||
|
||||
assert.deepEqual(entries.slice(0, 3), ['/opt/homebrew/bin', '/custom/bin', '/usr/bin']);
|
||||
assert.equal(entries.filter((entry) => entry === '/opt/homebrew/bin').length, 1);
|
||||
|
||||
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## [Unreleased]
|
||||
- [修复] 修复任务状态接口重建报告动作字段时把合法情绪分 `0` 当成空值的问题,确保低分报告能按评分口径纠正为卖出建议。
|
||||
- [修复] 修复 Agent 流式回复在未收到完成事件就断开时被显示为“(无内容)”的问题,改为提示流式响应中断并保留用户消息,避免误判为空回答。
|
||||
- [修复] 修复桌面端 `WEBUI_HOST=*` / `WEBUI_HOST=[::]` 会被原样传给端口探测和后端启动导致无法监听的问题,启动前分别规范化为 `0.0.0.0` / `::`。
|
||||
|
||||
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore)-->
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
|
||||
Reference in New Issue
Block a user