diff --git a/packages/server/src/services/hermes/gateway-manager.ts b/packages/server/src/services/hermes/gateway-manager.ts index 49f6bfa..be4a423 100644 --- a/packages/server/src/services/hermes/gateway-manager.ts +++ b/packages/server/src/services/hermes/gateway-manager.ts @@ -107,6 +107,73 @@ const needsRunMode = true // 启动时输出 init 系统检测结果(方便调试) logger.debug('Detected init system: %s (needsRunMode: %s, platform: %s)', initSystem, needsRunMode, process.platform) +const GATEWAY_RUNTIME_ENV_KEYS = new Set([ + 'PATH', + 'HOME', + 'USER', + 'USERNAME', + 'SHELL', + 'LANG', + 'TZ', + 'TMP', + 'TEMP', + 'TMPDIR', + 'HTTP_PROXY', + 'HTTPS_PROXY', + 'ALL_PROXY', + 'NO_PROXY', + 'HERMES_BIN', + 'HERMES_ALLOW_ROOT_GATEWAY', + 'SYSTEMROOT', + 'COMSPEC', + 'APPDATA', + 'LOCALAPPDATA', +]) + +function parseEnvKeys(raw: string): Set { + const keys = new Set() + for (const line of raw.split(/\r?\n/)) { + const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/) + if (match) keys.add(match[1]) + } + return keys +} + +function readProfileEnvKeys(): Set { + const keys = new Set() + const envPaths = [join(HERMES_BASE, '.env')] + const profilesDir = join(HERMES_BASE, 'profiles') + + if (existsSync(profilesDir)) { + for (const entry of readdirSync(profilesDir, { withFileTypes: true })) { + if (entry.isDirectory()) envPaths.push(join(profilesDir, entry.name, '.env')) + } + } + + for (const envPath of envPaths) { + try { + for (const key of parseEnvKeys(readFileSync(envPath, 'utf-8'))) keys.add(key) + } catch {} + } + + return keys +} + +export function buildGatewayProcessEnv(profileName: string, hermesHome: string): NodeJS.ProcessEnv { + const base = { ...process.env } + + if (profileName !== 'default') { + for (const key of readProfileEnvKeys()) { + if (!GATEWAY_RUNTIME_ENV_KEYS.has(key.toUpperCase())) delete base[key] + } + } + + return { + ...base, + HERMES_HOME: hermesHome, + } +} + // ============================ // 类型定义 // ============================ @@ -577,7 +644,7 @@ export class GatewayManager { // 所有平台统一使用 run 模式;dev/nodemon 可通过 env 保留 gateway 进程。 return new Promise((resolve, reject) => { - const env = { ...process.env, HERMES_HOME: hermesHome } + const env = buildGatewayProcessEnv(name, hermesHome) const detachGateway = shouldDetachGatewayProcess() const child = spawn(HERMES_BIN, ['gateway', 'run', '--replace'], { stdio: 'ignore', @@ -689,7 +756,7 @@ export class GatewayManager { const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['gateway', 'stop'], { timeout: 15000, windowsHide: true, - env: { ...process.env, HERMES_HOME: hermesHome }, + env: buildGatewayProcessEnv(name, hermesHome), }) const output = `${stdout}${stderr}`.trim() if (output) logger.debug('%s: hermes gateway stop: %s', name, output) diff --git a/tests/server/gateway-manager.test.ts b/tests/server/gateway-manager.test.ts index c5ebd77..76b2b94 100644 --- a/tests/server/gateway-manager.test.ts +++ b/tests/server/gateway-manager.test.ts @@ -4,6 +4,7 @@ import { join } from 'path' import { afterEach, describe, expect, it, vi } from 'vitest' const originalHermesHome = process.env.HERMES_HOME +const originalEnv = { ...process.env } const tempHomes: string[] = [] function createHermesHome(): string { @@ -22,6 +23,7 @@ async function createManager(home: string): Promise { afterEach(() => { vi.restoreAllMocks() vi.resetModules() + process.env = { ...originalEnv } if (originalHermesHome === undefined) { delete process.env.HERMES_HOME } else { @@ -95,3 +97,76 @@ describe('GatewayManager Windows process recovery', () => { expect(manager.readPidFile('work')).toBe(33333) }) }) + +describe('GatewayManager gateway process env', () => { + it('keeps full inherited env for the default profile for compatibility', async () => { + const home = createHermesHome() + process.env.WEIXIN_TOKEN = 'from-parent' + process.env.CUSTOM_GATEWAY_SETTING = 'keep-me' + process.env.HERMES_HOME = home + vi.resetModules() + const { buildGatewayProcessEnv } = await import('../../packages/server/src/services/hermes/gateway-manager') + + const env = buildGatewayProcessEnv('default', home) + + expect(env.WEIXIN_TOKEN).toBe('from-parent') + expect(env.CUSTOM_GATEWAY_SETTING).toBe('keep-me') + expect(env.HERMES_HOME).toBe(home) + }) + + it('removes parent env keys defined by any profile env for non-default profile gateways', async () => { + const home = createHermesHome() + const workHome = join(home, 'profiles', 'work') + mkdirSync(workHome, { recursive: true }) + writeFileSync(join(home, '.env'), [ + 'WEIXIN_TOKEN=default-weixin', + 'WECOM_SECRET=default-wecom', + 'FUTURE_PLATFORM_TOKEN=default-future', + 'export EXPORTED_SECRET=default-export', + 'PATH=/default/path', + 'HTTP_PROXY=http://default-proxy.local:8080', + 'COMMENTED_OUT_SECRET=not-commented', + '# COMMENTED_OUT_SECRET=commented', + ].join('\n')) + writeFileSync(join(workHome, '.env'), [ + 'WORK_ONLY_TOKEN=work-profile', + 'PARENT_OVERRIDE_ME=work-profile', + ].join('\n')) + + process.env.PATH = '/opt/hermes/.venv/bin:/usr/bin' + process.env.HOME = '/home/agent' + process.env.HTTP_PROXY = 'http://proxy.local:8080' + process.env.HERMES_BIN = '/opt/hermes/.venv/bin/hermes' + process.env.HERMES_ALLOW_ROOT_GATEWAY = '1' + process.env.HERMES_HOME = home + process.env.WEIXIN_TOKEN = 'from-parent' + process.env.WECOM_SECRET = 'from-parent' + process.env.FUTURE_PLATFORM_TOKEN = 'from-parent' + process.env.EXPORTED_SECRET = 'from-parent' + process.env.WORK_ONLY_TOKEN = 'from-parent' + process.env.PARENT_OVERRIDE_ME = 'from-parent' + process.env.UNKNOWN_SERVICE_TOKEN = 'keep-me' + process.env.COMMENTED_OUT_SECRET = 'from-parent' + process.env.CUSTOM_GATEWAY_SETTING = 'from-parent' + vi.resetModules() + const { buildGatewayProcessEnv } = await import('../../packages/server/src/services/hermes/gateway-manager') + + const env = buildGatewayProcessEnv('work', join(home, 'profiles', 'work')) + + expect(env.HERMES_HOME).toBe(join(home, 'profiles', 'work')) + expect(env.PATH).toBe('/opt/hermes/.venv/bin:/usr/bin') + expect(env.HOME).toBe('/home/agent') + expect(env.HTTP_PROXY).toBe('http://proxy.local:8080') + expect(env.HERMES_BIN).toBe('/opt/hermes/.venv/bin/hermes') + expect(env.HERMES_ALLOW_ROOT_GATEWAY).toBe('1') + expect(env.WEIXIN_TOKEN).toBeUndefined() + expect(env.WECOM_SECRET).toBeUndefined() + expect(env.FUTURE_PLATFORM_TOKEN).toBeUndefined() + expect(env.EXPORTED_SECRET).toBeUndefined() + expect(env.WORK_ONLY_TOKEN).toBeUndefined() + expect(env.PARENT_OVERRIDE_ME).toBeUndefined() + expect(env.COMMENTED_OUT_SECRET).toBeUndefined() + expect(env.UNKNOWN_SERVICE_TOKEN).toBe('keep-me') + expect(env.CUSTOM_GATEWAY_SETTING).toBe('from-parent') + }) +})