fix(feishu): honor NO_PROXY before proxying the long connection

webSocketProxyUrl() picks up https_proxy/HTTPS_PROXY/http_proxy/
HTTP_PROXY and forces the Feishu WSS long connection through the proxy,
without consulting NO_PROXY/no_proxy. With a local proxy exported in
the shell (Clash/V2Ray setups), every long-connection attempt is
rejected by Feishu's server and the runtime force-closes and retries
in a loop, so the bot never reaches a stable connected state.

Resolve NO_PROXY/no_proxy for the long-connection endpoints
(open.feishu.cn / open.larksuite.com) before falling back to the proxy
env: exact host, parent-domain (.cn, feishu.cn) and * entries follow
the common NO_PROXY semantics.

- production.mjs: NO_PROXY gate in front of the proxy lookup
- feishu-proxy.test.mjs: NO_PROXY exclusion and fallthrough cases
- lib/index.js: regenerated host bundle
This commit is contained in:
evanfang0054 2026-08-31 18:59:09 +08:00
parent 009f1abec9
commit adc0a89f13
3 changed files with 351 additions and 217 deletions

View file

@ -25,7 +25,34 @@ import {
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
// The WebSocket agent built here is only used for the Feishu long connection,
// whose endpoint is open.feishu.cn (Feishu) or open.larksuite.com (Lark).
// Honor NO_PROXY/no_proxy for those hosts so users with blanket proxy
// environments exported in their shell (Clash/V2Ray etc.) keep a direct
// connection to Feishu instead of routing the WSS handshake through the proxy.
const LONG_CONNECTION_HOSTS = ['open.feishu.cn', 'open.larksuite.com'];
function hostExcludedByNoProxyEntry(host, entry) {
if (entry === '*') return true;
const bare = entry.startsWith('.') ? entry.slice(1) : entry;
if (!bare) return false;
return host === bare || host.endsWith(`.${bare}`);
}
function longConnectionExcludedByNoProxy(env) {
for (const key of ['no_proxy', 'NO_PROXY']) {
const value = env?.[key];
if (typeof value !== 'string' || !value.trim()) continue;
const entries = value.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean);
if (LONG_CONNECTION_HOSTS.some((host) => entries.some((entry) => hostExcludedByNoProxyEntry(host, entry)))) {
return true;
}
}
return false;
}
function webSocketProxyUrl(env) {
if (longConnectionExcludedByNoProxy(env)) return undefined;
for (const key of ['https_proxy', 'HTTPS_PROXY', 'http_proxy', 'HTTP_PROXY']) {
const value = env?.[key];
if (typeof value === 'string' && value.trim()) return value.trim();