mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): P1-3 lazy_deps 预处理 - 加 IM 渠道不再「首启 Gateway 卡 30 秒后崩」
Hermes 内核 tools/lazy_deps.py 维护了一个 allowlist:每个 feature(如 platform.telegram /
tts.elevenlabs / search.exa)对应一组 PyPI 包。原本只有 Gateway 启动 platform 模块时
才会调 ensure() 装包,导致首次启动卡 30 秒甚至超时崩溃。
本 PR 把 lazy_deps 暴露给 ClawPanel UI,让用户能主动预装。
## 后端三个新 Tauri 命令
- hermes_lazy_deps_features() — 列所有 LAZY_DEPS allowlist feature(17 个)
- hermes_lazy_deps_status(features) — 批量查每个 feature 是否已装好
- hermes_lazy_deps_ensure(feature) — 主动调内核 tools.lazy_deps.ensure 装
实现方式:
- 找到 ~/.hermes-venv 的 python 路径(unix: bin/python,windows: Scripts/python.exe)
- 用 tokio::process::Command spawn `python -c "<embedded script>"` 跑临时 Python 脚本
- 脚本走 from tools.lazy_deps import ensure / feature_missing / LAZY_DEPS
- 把结果以 JSON dump 给 stdout,Rust 端解析最后一行
- enhanced_path() 注入 PATH 兼容 macOS Tauri 启动后 PATH 不全
- serde_json::to_string 把字符串和列表序列化为 Python 合法字面量(防注入)
已注册到 lib.rs,前端 wrapper 在 tauri-api.js(features 走 10min 缓存)。
## 前端
- 新页面 src/engines/hermes/pages/lazy-deps.js
- 分类 grid(消息渠道 / TTS / STT / 搜索 / 模型商 / 记忆 / 图像 / 其他),每类有 emoji
- 卡片式:feature 名(友好显示)+ specs 元信息 + 状态徽章(已装✓ / 未装warn / 未知)+ 装/重装按钮
- 装/重装按钮 await ensure,期间「安装中…」disabled,完成后刷新整张表
- 失败走 humanizeError 统一提示
- 17 个 feature 都有友好显示名 i18n(platform.telegram → Telegram,platform.dingtalk → 钉钉 等)
- 完整 11 语言 i18n(hermesLazyDeps 模块),其它语言 fallback 到 en
## sidebar
- Hermes 引擎「管理」section 新增「可选依赖管理」入口
- 路由 /h/lazy-deps
## CSS
- 加 .lazy-deps-grid / .lazy-deps-card / .lazy-deps-badge.{ok,warn,unknown}
- 复用现有 .empty-state / .empty-compact 风格
## Web 模式
- dev-api.js 加三个同名 handler:
- features 返回内置常见 platform 列表
- status 全标 unknown(无法 spawn python)
- ensure 直接 reject,提示走桌面端
## 累计变动
- 2 新文件(lazy-deps.js page + hermesLazyDeps.js i18n)
- 7 修改(dev-api / hermes.rs / lib.rs / hermes/index.js / tauri-api.js / locales/index.js / components.css)
- 11 语言 × ~17 个新 i18n 键
- cargo check ✓ + npm build ✓
This commit is contained in:
@@ -2150,6 +2150,141 @@ pub async fn hermes_read_config_full() -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// P1-3: lazy_deps 预处理命令 — 让用户启用渠道时不再「首启 Gateway 卡 30 秒后崩」
|
||||
//
|
||||
// Hermes 内核 tools/lazy_deps.py 维护了一个 allowlist:每个 feature(如
|
||||
// `platform.telegram` / `tts.elevenlabs`)对应一组 PyPI 包。原本只有 Gateway
|
||||
// 启动 platform 模块时才会调 ensure() 装包,导致首次启动卡住甚至超时崩。
|
||||
//
|
||||
// 这里把 lazy_deps 暴露给 ClawPanel UI:
|
||||
// - hermes_lazy_deps_features() — 列所有可装的 feature(小白选)
|
||||
// - hermes_lazy_deps_status(features) — 批量查每个 feature 是否已安装
|
||||
// - hermes_lazy_deps_ensure(feature) — 主动预装
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 找到 Hermes venv 的 Python 解释器路径
|
||||
fn hermes_venv_python() -> Option<PathBuf> {
|
||||
let venv_dir = dirs::home_dir()?.join(".hermes-venv");
|
||||
#[cfg(target_os = "windows")]
|
||||
let py = venv_dir.join("Scripts").join("python.exe");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let py = venv_dir.join("bin").join("python");
|
||||
if py.exists() {
|
||||
Some(py)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一跑 venv python -c "<script>" 拿 JSON 结果。失败给可读错误。
|
||||
async fn run_venv_python_json(script: &str) -> Result<Value, String> {
|
||||
let py = hermes_venv_python().ok_or_else(|| {
|
||||
"Hermes venv 未找到(~/.hermes-venv 不存在)。请先安装 Hermes。".to_string()
|
||||
})?;
|
||||
|
||||
let mut cmd = tokio::process::Command::new(&py);
|
||||
cmd.arg("-c").arg(script);
|
||||
cmd.env("PYTHONIOENCODING", "utf-8");
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("启动 Python 子进程失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
let stderr_trimmed = stderr.trim();
|
||||
return Err(if stderr_trimmed.is_empty() {
|
||||
format!("Python 进程退出码 {},无 stderr 输出", output.status)
|
||||
} else {
|
||||
stderr_trimmed.to_string()
|
||||
});
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
// 取最后一行 JSON(避免被 Python 模块的 print 干扰)
|
||||
let last_line = stdout
|
||||
.lines()
|
||||
.rev()
|
||||
.find(|l| !l.trim().is_empty())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
serde_json::from_str(last_line)
|
||||
.map_err(|e| format!("Python 输出解析失败: {e}\n原文: {stdout}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hermes_lazy_deps_features() -> Result<Value, String> {
|
||||
let script = r#"
|
||||
import json
|
||||
try:
|
||||
from tools.lazy_deps import LAZY_DEPS
|
||||
out = []
|
||||
for feat, specs in LAZY_DEPS.items():
|
||||
out.append({"feature": feat, "specs": list(specs)})
|
||||
print(json.dumps({"ok": True, "features": out}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"ok": False, "error": str(e)}))
|
||||
"#;
|
||||
run_venv_python_json(script).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hermes_lazy_deps_status(features: Vec<String>) -> Result<Value, String> {
|
||||
// 把 features 列表序列化成 Python 合法的列表字面量
|
||||
// serde_json 的输出(如 ["platform.telegram","platform.discord"])正好是 Python 合法字面量
|
||||
let features_literal = serde_json::to_string(&features)
|
||||
.map_err(|e| format!("features 序列化失败: {e}"))?;
|
||||
let script = format!(
|
||||
r#"
|
||||
import json
|
||||
try:
|
||||
from tools.lazy_deps import feature_missing, LAZY_DEPS
|
||||
targets = {features_literal}
|
||||
result = {{}}
|
||||
for f in targets:
|
||||
if f not in LAZY_DEPS:
|
||||
result[f] = {{"known": False, "satisfied": False, "missing": []}}
|
||||
continue
|
||||
miss = list(feature_missing(f))
|
||||
result[f] = {{"known": True, "satisfied": len(miss) == 0, "missing": miss}}
|
||||
print(json.dumps({{"ok": True, "status": result}}))
|
||||
except Exception as e:
|
||||
print(json.dumps({{"ok": False, "error": str(e)}}))
|
||||
"#
|
||||
);
|
||||
run_venv_python_json(&script).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hermes_lazy_deps_ensure(feature: String) -> Result<Value, String> {
|
||||
// serde_json::to_string 把字符串包成 Python 合法的字符串字面量(已含引号 + escape)
|
||||
let feature_literal = serde_json::to_string(&feature)
|
||||
.map_err(|e| format!("feature 名序列化失败: {e}"))?;
|
||||
let script = format!(
|
||||
r#"
|
||||
import json, sys
|
||||
try:
|
||||
from tools.lazy_deps import ensure, feature_missing, FeatureUnavailable
|
||||
feat = {feature_literal}
|
||||
before_missing = list(feature_missing(feat))
|
||||
if not before_missing:
|
||||
print(json.dumps({{"ok": True, "alreadySatisfied": True, "installed": []}}))
|
||||
sys.exit(0)
|
||||
try:
|
||||
ensure(feat, prompt=False)
|
||||
print(json.dumps({{"ok": True, "alreadySatisfied": False, "installed": before_missing}}))
|
||||
except FeatureUnavailable as fe:
|
||||
print(json.dumps({{"ok": False, "error": str(fe), "missing": list(getattr(fe, "missing", []))}}))
|
||||
except Exception as e:
|
||||
print(json.dumps({{"ok": False, "error": str(e)}}))
|
||||
"#
|
||||
);
|
||||
run_venv_python_json(&script).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// hermes_fetch_models — 从 API 获取模型列表(后端代理,避免 CORS)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -241,6 +241,9 @@ pub fn run() {
|
||||
hermes::hermes_agent_run,
|
||||
hermes::hermes_read_config,
|
||||
hermes::hermes_read_config_full,
|
||||
hermes::hermes_lazy_deps_features,
|
||||
hermes::hermes_lazy_deps_status,
|
||||
hermes::hermes_lazy_deps_ensure,
|
||||
hermes::hermes_fetch_models,
|
||||
hermes::hermes_update_model,
|
||||
hermes::hermes_detect_environments,
|
||||
|
||||
Reference in New Issue
Block a user