Documentation

Step 13: 皮肤引擎与配置子系统

13. 皮肤引擎与配置子系统

概述

Hermes Agent的皮肤引擎、配置系统、Profile管理和模型切换构成了”视觉身份与运行时参数”层。皮肤引擎是纯数据驱动的设计——添加新skin只需编写YAML文件,零代码改动。配置系统管理10,000+行默认值并通过版本化迁移保持兼容性。Profile系统提供完全隔离的多实例支持。模型切换链路连接了用户输入、别名解析、Provider发现和运行时凭证获取。


一、皮肤引擎 (hermes_cli/skin_engine.py)

架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户YAML: ~/.hermes/skins/cyberpunk.yaml
    ↓ _load_skin_from_yaml()
    ↓
_BUILTIN_SKINS["default"] (Python dict)
    ↓ _build_skin_config() → 深度合并
    ↓
SkinConfig dataclass
    ├─ name, description
    ├─ colors: Dict[str, str]     # 20+颜色键
    ├─ spinner: Dict[str, Any]     # faces, verbs, wings
    ├─ branding: Dict[str, str]    # agent_name, welcome, etc.
    ├─ tool_prefix: str            # "┊" or custom
    ├─ tool_emojis: Dict[str, str] # per-tool emoji overrides
    ├─ banner_logo: str            # Rich-markup ASCII art
    └─ banner_hero: str            # Rich-markup center art

关键函数

函数 作用
init_skin_from_config(config) 启动时调用,从config.yaml的display.skin初始化
get_active_skin() 返回缓存的SkinConfig,惰性加载
set_active_skin(name) 运行时切换skin,/skin命令调用
load_skin(name) 加载:用户skin → 内置skin → default回退
get_active_prompt_symbol() 供prompt_toolkit使用的提示符
get_prompt_toolkit_style_overrides() 17项TUI样式覆盖

继承机制_build_skin_config()以default skin为基础,用户YAML只覆盖指定的键。未指定的颜色/branding自动继承default的值。

7个内置skin:default、ares、mono、slate、daylight、warm-lightmode、poseidon、sisyphus、charizard——每个都有完整的color/spinner/branding定义,部分包含自定义ASCII banner art。

渲染集成点

皮肤值通过以下路径传递到实际显示:

  1. Banner (banner.py):_skin_color(key, fallback)_skin_branding(key, fallback) 延迟读取active skin
  2. Spinner (display.py):_get_skin()skin.spinner["waiting_faces"]
  3. Tool输出 (display.py):get_skin_tool_prefix()skin.tool_prefix
  4. TUI样式 (cli.py):get_prompt_toolkit_style_overrides() → 17项样式定义
  5. Diff渲染 (display.py):_diff_ansi() 从skin的ui_error/ui_ok颜色派生diff颜色

延迟加载设计:所有skin读取都使用try/except回退模式,确保skin引擎不可用时仍能运行。缓存机制——_diff_colors_cached只在首次渲染时解析,避免每帧导入。

评估

优势

  • 纯数据驱动,零代码改动添加新skin
  • 完美继承机制,用户YAML只需覆盖想要的键
  • 延迟加载+回退,不破坏任何路径

问题

  • 7个内置skin在skin_engine.py中硬编码了约300行ASCII art,使文件膨胀。建议将内置skin移到hermes_cli/skins/目录下的YAML文件,通过importlib.resources加载
  • get_prompt_toolkit_style_overrides()返回17项硬编码的样式名到颜色映射。颜色键名与SkinConfig.colors的键名不同(如status_bar_bg vs status_bar_bg),但映射逻辑不可见。建议在SkinConfig中添加get_prompt_style_overrides()方法统一管理

二、配置系统 (hermes_cli/config.py)

DEFAULT_CONFIG 结构

配置字典共780+行(L341-L781),按功能分区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DEFAULT_CONFIG = {
    "model": "",
    "providers": {},
    "fallback_providers": [],
    "terminal": { ... },        # 30+项后端配置
    "browser": { ... },        # 浏览器自动化
    "checkpoints": { ... },    # 文件系统快照
    "compression": { ... },    # 上下文压缩
    "smart_model_routing": {},  # 简单/复杂路由
    "agent": { ... },           # 轮次限制、 personalities
    "display": { ... },         # compact, skin, streaming等
    "auxiliary": { ... },       # 辅助模型配置(vision/web_extract/compression等)
    "delegation": { ... },      # 子代理委托
    "approvals": { ... },       # 危险命令审批
    "personalities": { ... },   # 预设人格
    "security": { ... },       # 安全扫描
    "cron": { ... },            # 定时任务
    "logging": { ... },        # 日志配置
    "voice": { ... },           # 语音模式
    "tts": { ... },             # TTS引擎
    "stt": { ... },             # 语音识别
    ...
    "_config_version": 18,      # 版本号,用于迁移
}

配置迁移机制

1
2
3
4
5
6
_config_version = 18  # 当前版本

# 每次新增配置字段时:
# 1. 在DEFAULT_CONFIG中添加默认值
# 2. bump _config_version
# 3. 在OPTIONAL_ENV_VARS中添加相关环境变量

迁移流程migrate_config()函数,约500行):

  1. 读取~/.hermes/config.yaml中的_config_version
  2. 逐版本应用迁移补丁
  3. 写回更新后的配置

问题

  • 迁移函数是500+行的if/elif链,每版本一个分支
  • 没有自动化测试保证迁移的幂等性
  • _config_version只在config.py中定义,但cli.py中的load_cli_config()有自己的DEFAULT_CONFIG——两处需要同步

环境变量系统

OPTIONAL_ENV_VARS字典包含130+项环境变量,每项有:

1
2
3
4
5
6
7
8
9
"OPENROUTER_API_KEY": {
    "description": "OpenRouter API key",
    "prompt": "OpenRouter API key",
    "url": "https://openrouter.ai/keys",
    "password": True,
    "tools": ["vision_analyze", "mixture_of_agents"],
    "category": "provider",
    "advanced": True,
}

用途

  • hermes setup向导的交互式提示
  • hermes doctor诊断缺少的API密钥
  • 文档自动生成

问题OPTIONAL_ENV_VARSPROVIDER_REGISTRY(auth.py)有大量重叠信息(如API key的描述和URL)。两处需要保持同步但无自动化验证。

配置安全

  • .env文件save_env_value_secure()将密钥写入~/.hermes/.env,权限0600
  • auth.json:OAuth令牌存储,权限0600,跨进程文件锁(fcntl/msvcrt)
  • Managed mode:NixOS安装禁用chmod 0600(群组可读0750),通过is_managed()检测
  • Container detection_is_container()跳过权限设置,Docker卷需要更宽权限

三、Profile多实例管理 (hermes_cli/profiles.py)

架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~/.hermes/                  ← default profile (HERMES_HOME)
~/.hermes/profiles/coder/   ← named profile
~/.hermes/profiles/work/    ← named profile
    ├─ config.yaml
    ├─ .env
    ├─ SOUL.md
    ├─ memories/MEMORY.md
    ├─ memories/USER.md
    ├─ skills/
    ├─ sessions/
    ├─ logs/
    ├─ cron/
    ├─ home/          ← subprocess HOME隔离
    └── workspace/

核心机制_apply_profile_override()(在hermes_cli/main.py中)在所有模块导入之前设置HERMES_HOME环境变量。之后所有get_hermes_home()调用返回profile目录。

CRUD操作

操作 函数 关键步骤
创建 create_profile(name, clone_config=) mkdir结构 + 可选复制config/env/SOUL.md
删除 delete_profile(name, yes=) 停gateway → 清service → 删目录 → 清alias
列表 list_profiles() 遍历profiles/目录 + default
切换 set_active_profile(name) 写active_profile文件
重命名 rename_profile(old, new) 停gateway → mv目录 → 更新alias → 更新active_profile
导出 export_profile(name, path) tar.gz打包,排除敏感文件
导入 import_profile(path, name) 安全提取+路径验证

Profile隔离的关键规则(来自AGENTS.md):

  1. get_hermes_home() — 永远不要硬编码~/.hermes
  2. display_hermes_home() — 用户消息中显示友好路径
  3. Module-level constants安全 — 在_apply_profile_override()之后缓存
  4. 测试mock — 必须同时mock Path.home()和设置HERMES_HOME env var
  5. Home锚定_get_profiles_root()返回Path.home() / ".hermes" / "profiles",不用get_hermes_home()

Wrapper脚本create_wrapper_script(name)~/.local/bin/<name>创建shell脚本:

1
2
#!/bin/sh
exec hermes -p <name> "$@"

这样coder chat等价于hermes -p coder chat

评估

优势

  • 完全文件系统隔离——每个profile有独立的config/env/技能/会话/日志
  • 克隆功能支持config-only或full复制
  • 安全导出排除API密钥
  • 与systemd/launchd服务集成(删除profile时自动清理service)

问题

  • _apply_profile_override()必须在所有导入之前执行——这是一个fragile的启动时序要求。如果任何模块在导入时调用get_hermes_home(),将返回错误的路径
  • resolve_profile_env()返回目录路径而非激活profile——CLI入口点需要处理HERMES_HOME环境变量的设置
  • 导出功能使用shutil.copytree+shutil.make_archive,对大profile(含技能缓存、日志等)可能很慢

四、Provider凭证解析 (hermes_cli/auth.py)

架构

1
2
3
4
5
6
7
8
9
10
11
12
PROVIDER_REGISTRY (30+ providers)
    ├─ OAuth providers (nous, openai-codex, qwen-oauth, google-gemini-cli)
    ├─ API key providers (anthropic, gemini, xai, deepseek, ...)
    ├─ AWS SDK (bedrock)
    ├─ External process (copilot-acp)
    └─ Custom (user-defined endpoints)

auth.json
    ├─ version: 1
    ├─ providers: {nous: {...}, openai-codex: {...}}
    ├─ credential_pool: {anthropic: [...]}
    └─ active_provider: "nous"

认证流程(以Nous Portal为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
resolve_provider("auto")
    ├─ 检查auth.json active_provider → "nous"
    ├─ get_auth_status("nous") → logged_in
    └─ return "nous"

resolve_nous_runtime_credentials()
    ├─ 读取auth.json nous.tokens
    ├─ 检查access_token过期 (JWT exp claim)
    ├─ 若过期 → refresh_token → 更新auth.json
    └─ 返回 {api_key, base_url, source: "nous_oauth"}

switch_model("sonnet", "nous", ...)
    ├─ resolve_alias("sonnet", "nous") → ("nous", "anthropic/claude-sonnet-4-6", "sonnet")
    ├─ resolve_runtime_provider("nous")
    │   └─ resolve_nous_runtime_credentials() → {api_key, base_url}
    ├─ normalize_model_for_provider(...)
    ├─ 获取ModelCapabilities / ModelInfo
    └─ 返回 ModelSwitchResult(success=True, ...)

Z.AI端点探测(特殊的凭证解析):

1
2
3
4
5
6
7
8
_resolve_zai_base_url(api_key, default_url, env_override)
    ├─ 有env_override → 直接返回
    ├─ 检查auth.json缓存的detected_endpoint
    │   └─ key_hash匹配 → 使用缓存
    └─ 探测4个端点 (global, cn, coding-global, coding-cn)
        └─ 每个端点尝试多个probe_models
        └─ 返回第一个200响应的端点
        └─ 缓存到auth.json provider_state

跨进程文件锁_auth_store_lock()使用fcntl.flock()(Linux/Mac)或msvcrt.locking()(Windows),支持重入(threading.local跟踪锁深度)。

评估

优势

  • 30+ provider的开箱即用支持
  • OAuth device code + API key + AWS SDK + external process四种认证类型
  • Z.AI等多端点provider的自动探测
  • Kimi Code的key前缀检测自动路由
  • 跨进程安全(文件锁+原子写入)

问题

  • PROVIDER_REGISTRY硬编码在auth.py中(约300行),难以扩展自定义provider。用户只能通过config.yamlproviders:.env来配置自定义端点
  • resolve_provider()的优先级链有6层,逻辑复杂。当多个API key同时存在时,优先级可能不符合用户预期
  • auth.json的结构在v1版本中有两种格式兼容(旧的systems和新的providers),增加了_load_auth_store()的复杂度

五、模型切换 (hermes_cli/model_switch.py)

完整链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
用户输入: "/model sonnet --provider anthropic"
    ↓
HermesCLI._handle_model_switch()
    ↓ parse_model_flags() → ("sonnet", "anthropic", False)
    ↓
switch_model("sonnet", current_provider, current_model, ...)
    ├─ PATH A: explicit_provider="anthropic"
    │   ├─ resolve_provider_full("anthropic") → ProviderDef
    │   ├─ resolve_alias("sonnet", "anthropic") → ("anthropic", "claude-sonnet-4-6", "sonnet")
    │   └─ new_model = "claude-sonnet-4-6"
    │
    ├─ PATH B: 无--provider (自动解析)
    │   ├─ resolve_alias("sonnet", current_provider)
    │   ├─ 若不在当前provider → fallback到有凭证的provider
    │   ├─ 聚合器目录搜索 (OpenRouter等)
    │   └─ detect_provider_for_model() 作为最后手段
    │
    ├─ COMMON: 凭证解析
    │   ├─ resolve_runtime_provider() → {api_key, base_url, api_mode}
    │   ├─ normalize_model_for_provider()
    │   ├─ validate_requested_model()
    │   ├─ get_model_capabilities() / get_model_info()
    │   └─ 检测Nous Hermes非agentic模型警告
    │
    └─ 返回 ModelSwitchResult
        ├─ success, new_model, target_provider
        ├─ api_key, base_url, api_mode
        ├─ capabilities, model_info
        └─ warning_message

别名系统

1
2
3
4
5
6
7
8
9
MODEL_ALIASES = {
    "sonnet":  ModelIdentity("anthropic", "claude-sonnet"),
    "opus":    ModelIdentity("anthropic", "claude-opus"),
    "gpt5":    ModelIdentity("openai", "gpt-5"),
    "gemini":  ModelIdentity("google", "gemini"),
    # ... 20+ aliases
}

DIRECT_ALIASES = {}  # 从config.yaml model_aliases: 加载,覆盖内置别名

解析优先级

  1. DIRECT_ALIASES(config.yaml中的精确映射)
  2. MODEL_ALIASES(内置短名 → vendor/family)
  3. 聚合器目录搜索(OpenRouter模型列表)
  4. detect_provider_for_model()(按模型名推断provider)

模型名规范化model_normalize.py):每个provider有自己的模型名格式规则:

  • OpenRouter: provider/model-name
  • Anthropic: claude-sonnet-4-6 (no anthropic/ prefix)
  • Google: gemini-3-flash-preview (带日期后缀)

评估

优势

  • 完整的别名系统让用户只需记住/model sonnet
  • 自动provider检测和fallback
  • 多层凭证解析确保最佳provider自动选择
  • Z.AI端点探测缓存避免每次启动都网络探测

问题

  • switch_model()函数本身400+行,混合了别名解析、凭证获取和验证逻辑。建议拆分为resolve_alias()resolve_credentials()validate_model()三个函数
  • list_authenticated_providers()约200行,包含4层循环(Hermes-mapped providers → Hermes-only providers → canonical providers → user-defined endpoints),每次调用都重新探测所有provider的凭证状态
  • MODEL_ALIASES的键名与model normalize后的名字不总是一致(如"deepseek"ModelIdentity("deepseek", "deepseek-chat"),但实际模型名是deepseek-chat而非deepseek

总体改进建议

  1. 统一配置来源config.pyDEFAULT_CONFIGcli.pyload_cli_config()返回的defaults有大量重叠。建议将DEFAULT_CONFIG作为唯一的配置定义源,cli.py只做配置文件读取+合并+环境变量桥接。

  2. Skin内置数据外置:将_BUILTIN_SKINS中的7个内置skin定义移到hermes_cli/skins/目录的YAML文件,通过importlib.resources加载。这减少skin_engine.py约400行硬编码。

  3. Profile启动时序保证_apply_profile_override()必须在所有import之前执行。当前通过hermes_cli/main.py的早期执行来保证,但缺少显式验证。建议在get_hermes_home()中添加assert检查环境变量已设置。

  4. 命令处理器注册制:将process_command()的40+ elif分支重构为命令处理器注册表,与COMMAND_REGISTRY对接。这样新命令只需在commands.py添加CommandDef + 在处理器文件添加handler函数。

  5. 配置版本自动迁移测试:添加集成测试验证从每个历史版本到当前版本的迁移路径。当前_config_version=18意味着有17个迁移步骤,但缺少自动化验证。

  6. Provider凭证缓存list_authenticated_providers()每次调用都探测所有provider。建议引入TTL缓存(如5分钟),避免在/model命令中重复探测。