Files
Hermes-ui/packages/server/src/services/hermes/profile-credentials.ts
T
ekko b4a80aceeb fix: Windows/Termux compatibility, comic theme fonts, and UI fixes (#630)
* fix: comprehensive Windows compatibility and gateway management improvements

This commit addresses multiple Windows compatibility issues and improves
gateway management across all platforms.

## Windows Compatibility Fixes
- Add hermes-path.ts with cross-platform Hermes home/bin detection
- Fix Windows native installation paths (%LOCALAPPDATA%\hermes)
- Update terminal.ts to use PowerShell instead of /bin/bash on Windows
- Fix upload.ts path construction to use path.join() for cross-platform paths
- Fix download.ts to use isAbsolute() for Windows absolute path detection
- Update auth.ts to skip file mode 0o600 on Windows (unsupported)
- Add nodemon.json for cross-platform environment variable handling

## Gateway Management Improvements
- Simplify gateway startup: all platforms use 'run' mode uniformly
- Remove complex init system detection and platform-specific code paths
- Improve PID file validation: use health check instead of port detection
- Remove getPortByPid() method (too complex and error-prone)
- Remove checkPortAvailable() TCP bind test (TIME_WAIT false positives)
- Trust gateway --replace flag to handle real port conflicts
- Add smart PID validation: check if stale process via health check
- Fix port allocation to avoid incrementing when gateway restarts
- Add allocatedPorts.clear() on each startAll() call
- Add clearPidFile() method to clean up stale PID files

## Process Management
- Remove detached:true and unref() from gateway spawn
- Gateway processes now follow parent process lifecycle
- Add process reference storage in ManagedGateway interface
- Improve shutdown logic: call gatewayManager.stopAll() before exit
- Fix Windows process killing: use process.kill(pid) for Windows
- Remove PowerShell command for lock file cleanup (use Node.js fs.unlinkSync)

## Frontend Theme Fixes
- Fix main.ts localStorage key mismatch (hermes_theme → hermes_brightness)
- Add inline script in index.html to prevent FOUC (Flash of Unstyled Content)
- Apply theme classes before Vue mount to avoid visual glitches

## Developer Experience
- Fix nodemon windows-kill popup on Windows by removing signal config
- Add delay and environment variables to nodemon.json
- Add windowsHide: true to all child process spawns

## Breaking Changes
- Gateway management now exclusively uses 'run' mode on all platforms
- systemd/launchd integration removed (use --replace flag instead)

This fix ensures hermes-web-ui works correctly on Windows native
installations while maintaining compatibility with Linux/macOS/WSL2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix gateway lifecycle port handling

* fix: comprehensive Windows compatibility and gateway management improvements

- Simplified hermes CLI binary resolution logic
- Fixed Windows line ending compatibility in profile list parsing
- Migrated gateway restart logic from CLI to GatewayManager
- Added gateway restart to updateCredentials method
- Removed unnecessary gateway restarts from provider operations
- Fixed configuration preservation when switching profiles
- Added nodemon quiet mode and legacy watch to reduce Windows popups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: change back to nodemon due to tsx compatibility issues

- tsx has compatibility issues with Koa generator functions
- Restored nodemon with simplified configuration
- Added cross-env package for future Windows environment variable needs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: replace nodemon with ts-node-dev to eliminate Windows popup windows

- Installed ts-node-dev as nodemon replacement
- ts-node-dev has better Windows compatibility without console popups
- Supports respawning, inspector debugging, and TypeScript compilation
- Uses cross-env for Windows environment variable support
- Removed nodemon.json configuration file (no longer needed)

Benefits:
- No more Windows console popup windows during development
- Faster restart times compared to nodemon
- Built-in TypeScript compilation without ts-node overhead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: improve log parsing and Windows compatibility for agent/error logs

- Fixed Pino JSON log parsing bug where logger field incorrectly used obj.msg
- Changed logger field to use obj.name to properly display log source
- Added Windows line ending support (\r\n) for log file listing
- Added support for 'error' log type in addition to 'errors'
- Improved error message extraction from obj.err when available

This fixes the missing agent and error logs issue on Windows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix gateway health checks and shutdown ownership

* Refine auth lock window and dev shutdown

* fix: improve Hermes plugin discovery on Windows by fixing Python path resolution

- Added support for Windows venv Scripts directory structure
- Fixed Python executable path detection for hermes.exe in venv/Scripts/
- Added Windows LOCALAPPDATA hermes-agent directory to search paths
- Improved cross-platform compatibility for plugin discovery

This fixes the "No module named 'hermes_cli'" error on Windows by correctly
locating the Python virtual environment that contains the Hermes modules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: improve cross-platform compatibility for Hermes plugin discovery

- Added platform detection to only add Windows-specific paths on Windows
- Prevents potential issues on Unix/Linux/macOS systems
- Ensures LOCALAPPDATA path is only used when available on Windows
- Maintains existing behavior for all platforms

This makes the Windows plugin discovery fix safer for cross-platform usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove unused development dependencies

- Removed nodemon (replaced by ts-node-dev)
- Removed tsx (had compatibility issues with Koa)
- Removed nodemon.json configuration file
- Cleaned up development tools to only what's actually used

This reduces dependency size and eliminates the windows-kill popup
source that was part of nodemon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove memory system files

- Removed MEMORY.md index file
- Removed memory/ directory and windows-compatibility.md
- Cleaned up unused memory persistence system

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve TypeScript compilation error in plugins.ts

- Added type assertion 'as string[]' after filter(Boolean)
- Fixes TS2769 error: No overload matches this call
- Ensures type compatibility with hasHermesPluginModule function

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: comprehensive Windows compatibility and gateway management improvements

- Fix gateway detection after nodemon restart by adding health check-based detection
- Prevent port conflicts by detecting already-running gateways without PID files
- Switch to serial gateway startup to avoid lock file race conditions
- Return to nodemon from ts-node-dev for development stability
- Always stop gateways on shutdown to prevent orphan processes
- Prevent project root config files from being committed to git
- Fix syntax issues in plugins.ts

Resolves issues where default profile gateway failed to start after
nodemon restart and gateways were incorrectly marked as stopped
despite running on correct ports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: comic theme multilingual fonts, sidebar collapse fix, plugin discovery for Termux, and cron history

- Add Chinese (ZCOOL KuaiLe), Japanese (Zen Maru Gothic), Korean (Gaegu) handwritten fonts for Comic theme
- Fix collapsed sidebar: hide language switch, stack theme icons vertically
- Add hermes shebang parsing as fallback Python discovery for Termux
- Remove cron source filter from history sessions
- Add 0.5.17 changelog entries for all 8 locales

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: tolerate duplicate YAML keys in config parsing (closes #628)

Add `{ json: true }` to all 7 `yaml.load()` calls so duplicated mapping
keys (e.g. multiple `mcp_servers:` blocks) no longer crash the API.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: gateway ownership check requires PID file to prevent cross-profile port hijacking

Remove fallback that assumed ownership of healthy gateways without PID
verification. Now only claims a gateway if PID file exists and process
is alive, preventing one profile from hijacking another's port.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 20:08:13 +08:00

189 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 智能克隆 Profile 凭据管理
*
* 背景:`hermes profile create --clone` 会完整复制源 profile 的 .env + config.yaml
* 包括各平台的独占凭据(Weixin / Telegram / Slack / ...)。
* 这导致多个 profile 同时持有同一个 bot tokenhermes-agent 内部的 token 互斥机制
* 会让后启动的 gateway 在健康检查阶段被 kill,表现为"profile 加载错误"。
*
* 解决方案:clone 完成后,对新 profile 自动执行:
* 1. 从 .env 中删除所有匹配独占平台前缀的 KEY
* 2. 把 config.yaml 中独占平台的 `enabled: true` 改为 false
* 操作前会备份原文件为 `.bak.<timestamp>`。
*/
import { existsSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
import yaml from 'js-yaml'
import { detectHermesHome } from './hermes-path'
const HERMES_BASE = detectHermesHome()
/**
* 已知"独占型"平台的环境变量前缀正则
*
* 这些平台的凭据本质上是"一对一身份绑定":一个 token / app_id 对应唯一一个机器人或账号。
* 多个 profile 共享同一凭据会触发 hermes-agent 的 token 互斥机制 → 启动失败。
*
* 不在此列表的(模型 provider API key、工具调试开关等)视为可安全共享。
*
* **来源(不要凭主观推测扩展)**:与 hermes-agent `gateway/platforms/` 中实际调用
* `_acquire_platform_lock` / `acquire_scoped_lock` 的 adapter 1:1 对齐。
* 验证方法:`grep -l _acquire_platform_lock gateway/platforms/*.py`。
* 当前匹配上游的 7 个:discord, feishu, signal, slack, telegram, weixin, whatsapp。
*/
export const EXCLUSIVE_PLATFORM_ENV_PATTERNS: RegExp[] = [
/^TELEGRAM_/, // Telegram bot
/^DISCORD_/, // Discord bot
/^SLACK_/, // Slack app
/^WHATSAPP_/, // WhatsApp Business
/^SIGNAL_/, // Signal
/^WEIXIN_/, // 个人微信 bot
/^FEISHU_/, // 飞书
]
/**
* 已知"独占型"平台在 config.yaml 中 `platforms.<name>` 节点的名称集合
* 与 EXCLUSIVE_PLATFORM_ENV_PATTERNS 一一对应,用于禁用 `enabled` 字段。
*/
export const EXCLUSIVE_PLATFORMS = [
'telegram', 'discord', 'slack', 'whatsapp', 'signal', 'weixin', 'feishu',
]
/**
* config.yaml 中独占平台节点下的"敏感凭据字段"黑名单
*
* 仅在 EXCLUSIVE_PLATFORMS 节点(含其 `extra` 子节点)下作用,避免误伤模型 provider key
* 等其他配置。clone 时这些字段会被一并删除,防止用户后续 re-enable 平台时复用源 profile
* 的身份。
*/
export const EXCLUSIVE_PLATFORM_CREDENTIAL_KEYS = [
'token', 'bot_token', 'app_token',
'signing_secret', 'app_secret', 'client_secret',
'access_token', 'webhook_secret',
'account_id', 'phone_number_id', 'app_id',
]
/** 判断 .env 中的 KEY 是否属于独占平台凭据 */
export function isExclusivePlatformKey(key: string): boolean {
return EXCLUSIVE_PLATFORM_ENV_PATTERNS.some(re => re.test(key))
}
/**
* 清理 .env 文件中的独占平台凭据
* @param envPath .env 文件绝对路径
* @returns 被删除的 KEY 名列表(按 .env 中出现顺序);文件不存在或无需删除时返回 []
*
* 副作用:实际删除前会备份为 `.env.bak.<timestamp>`,便于用户恢复。
*/
export function stripExclusivePlatformCredentials(envPath: string): string[] {
if (!existsSync(envPath)) return []
const original = readFileSync(envPath, 'utf-8')
const lines = original.split('\n')
const removedKeys: string[] = []
const kept: string[] = []
for (const line of lines) {
const m = line.match(/^([A-Z_][A-Z0-9_]*)\s*=/)
if (m && isExclusivePlatformKey(m[1])) {
removedKeys.push(m[1])
} else {
kept.push(line)
}
}
if (removedKeys.length === 0) return []
writeFileSync(`${envPath}.bak.${Date.now()}`, original, 'utf-8')
writeFileSync(envPath, kept.join('\n'), 'utf-8')
return removedKeys
}
/**
* 禁用 config.yaml 中已知独占平台的 enabled 字段,并清理节点下的敏感凭据
* @param configPath config.yaml 绝对路径
* @returns
* - disabled: 被禁用的平台名列表
* - strippedConfigCredentials: 被清理的凭据字段路径(如 'weixin.extra.token'
* 无任何修改时两个字段均为空数组。
*
* 副作用:实际改写前会备份为 `config.yaml.bak.<timestamp>`。
*/
export function disableExclusivePlatformsInConfig(configPath: string): {
disabled: string[]
strippedConfigCredentials: string[]
} {
if (!existsSync(configPath)) return { disabled: [], strippedConfigCredentials: [] }
const original = readFileSync(configPath, 'utf-8')
let cfg: any
try {
cfg = yaml.load(original, { json: true })
} catch {
return { disabled: [], strippedConfigCredentials: [] }
}
if (!cfg || typeof cfg !== 'object') return { disabled: [], strippedConfigCredentials: [] }
const platforms = cfg.platforms
if (!platforms || typeof platforms !== 'object') return { disabled: [], strippedConfigCredentials: [] }
const disabled: string[] = []
const strippedConfigCredentials: string[] = []
for (const platName of EXCLUSIVE_PLATFORMS) {
const node = platforms[platName]
if (!node || typeof node !== 'object') continue
if (node.enabled === true) {
node.enabled = false
disabled.push(platName)
}
// 清理节点直挂的凭据字段
for (const k of EXCLUSIVE_PLATFORM_CREDENTIAL_KEYS) {
if (k in node) {
delete node[k]
strippedConfigCredentials.push(`${platName}.${k}`)
}
}
// 清理 extra 子节点中的凭据字段
if (node.extra && typeof node.extra === 'object') {
for (const k of EXCLUSIVE_PLATFORM_CREDENTIAL_KEYS) {
if (k in node.extra) {
delete node.extra[k]
strippedConfigCredentials.push(`${platName}.extra.${k}`)
}
}
}
}
if (disabled.length === 0 && strippedConfigCredentials.length === 0) {
return { disabled: [], strippedConfigCredentials: [] }
}
writeFileSync(`${configPath}.bak.${Date.now()}`, original, 'utf-8')
writeFileSync(configPath, yaml.dump(cfg, { lineWidth: -1 }), 'utf-8')
return { disabled, strippedConfigCredentials }
}
export interface SmartCloneCleanup {
/** 从 .env 中删除的 KEY 名列表 */
strippedCredentials: string[]
/** 在 config.yaml 中被禁用的平台名列表 */
disabledPlatforms: string[]
/** 在 config.yaml 中被清理的内嵌凭据字段路径(如 'weixin.extra.token' */
strippedConfigCredentials: string[]
}
/**
* 一站式:清理新 profile 的独占凭据 + 禁用 config.yaml 中的独占平台
*
* @param profileName profile 名称('default' → ~/.hermes/,其他 → ~/.hermes/profiles/<name>/
*/
export function smartCloneCleanup(profileName: string): SmartCloneCleanup {
const profileDir = profileName === 'default'
? HERMES_BASE
: join(HERMES_BASE, 'profiles', profileName)
const configResult = disableExclusivePlatformsInConfig(join(profileDir, 'config.yaml'))
return {
strippedCredentials: stripExclusivePlatformCredentials(join(profileDir, '.env')),
disabledPlatforms: configResult.disabled,
strippedConfigCredentials: configResult.strippedConfigCredentials,
}
}