b4a80aceeb
* 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>
189 lines
7.1 KiB
TypeScript
189 lines
7.1 KiB
TypeScript
/**
|
||
* 智能克隆 Profile 凭据管理
|
||
*
|
||
* 背景:`hermes profile create --clone` 会完整复制源 profile 的 .env + config.yaml,
|
||
* 包括各平台的独占凭据(Weixin / Telegram / Slack / ...)。
|
||
* 这导致多个 profile 同时持有同一个 bot token,hermes-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,
|
||
}
|
||
}
|