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:
zhulinsen
2026-07-04 10:49:55 +08:00
committed by GitHub
parent 35e16b0c6f
commit 1ed0d34496
3 changed files with 123 additions and 11 deletions

View File

@@ -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 });

View File

@@ -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);

View File

@@ -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-->
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->