mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
fix(hermes): self-host dashboard_auth + web_dist stubs to survive upstream missing pieces
Upstream hermes-agent 0.14.0 (both the PyPI wheel and the public git source
at NousResearch/hermes-agent) ships hermes_cli/web_server.py with hard imports
of hermes_cli.dashboard_auth.{audit,middleware,prefix,routes,ws_tickets} but
those source files are not included in the distribution, and the dashboard
SPA bundle (hermes_cli/web_dist/) is also missing. On Windows this makes
'hermes dashboard' crash on startup, which collapses every ClawPanel page
that talks to port 9119 (Profile / Kanban / OAuth / Channels / Sessions
detail) into a 'request rejected by target machine' error.
Per project policy we do not patch upstream. Instead, after each install /
upgrade and again before spawning the dashboard, ClawPanel now injects a
minimal pass-through stub into the installed venv:
- hermes_cli/dashboard_auth/{__init__,audit,middleware,prefix,routes,
ws_tickets}.py — no-op providers, safe on loopback (127.0.0.1) where
the auth gate is intentionally disabled.
- hermes_cli/web_dist/index.html + assets/ — so mount_spa() takes the
token-injecting branch instead of the 'Frontend not built' 404 branch.
Without this the panel can never scrape window.__HERMES_SESSION_TOKEN__
from the dashboard HTML and every /api/* call fails with 401.
Injection is idempotent: if upstream eventually ships the real files, the
stub writer skips them so the real implementation wins. Failures are
logged-and-swallowed so install/upgrade is never blocked by best-effort
compatibility patches.
Group-chat page is also fixed: hermes_agent_run resolves to the run_id
string (not a result object), so the page was rendering 'run_xxx...' as
the assistant reply. It now listens to hermes-run-{started,delta,done,
error,cancelled} events and resolves to payload.output (with accumulated
delta as fallback).
This commit is contained in:
@@ -1193,7 +1193,7 @@ fn kill_dashboard_pid() -> bool {
|
||||
/// 3. 进程提前退出 → 读日志尾部检测 deps_missing / port_in_use
|
||||
/// 返回 `{ started, kind?, port, pid?, exit_code?, log_tail? }`
|
||||
#[tauri::command]
|
||||
pub async fn hermes_dashboard_start() -> Result<Value, String> {
|
||||
pub async fn hermes_dashboard_start(app: tauri::AppHandle) -> Result<Value, String> {
|
||||
let port = hermes_dashboard_port();
|
||||
// 1. 已运行?
|
||||
if hermes_dashboard_tcp_running(port, 500)
|
||||
@@ -1211,6 +1211,11 @@ pub async fn hermes_dashboard_start() -> Result<Value, String> {
|
||||
// 2. 清掉残留 PID(来自上一次 spawn)
|
||||
let _ = kill_dashboard_pid();
|
||||
|
||||
// 2b. 上游 wheel 漏装 dashboard_auth + web_dist 时,dashboard 进程会直接崩。
|
||||
// 在 spawn 前先做一次幂等 stub 注入,覆盖既有用户(从早期版本升上来、没走过 install_hermes
|
||||
// 的新代码路径)也能立即恢复。已存在的真实文件不会被覆盖。
|
||||
inject_hermes_dashboard_compat_stub(&app);
|
||||
|
||||
let home = hermes_home();
|
||||
let log_path = home.join("dashboard-run.log");
|
||||
let log_file =
|
||||
@@ -1392,6 +1397,9 @@ pub async fn install_hermes(
|
||||
|
||||
let _ = app.emit("hermes-install-progress", 90u32);
|
||||
|
||||
// Step 2b: 注入 dashboard 兼容 stub(弥补上游 wheel 漏装 dashboard_auth + web_dist)
|
||||
inject_hermes_dashboard_compat_stub(&app);
|
||||
|
||||
// Step 3: 验证安装
|
||||
let _ = app.emit("hermes-install-log", "🔍 验证安装...");
|
||||
let enhanced = hermes_enhanced_path();
|
||||
@@ -1589,6 +1597,315 @@ fn hermes_runtime_extras_log_segment() -> String {
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hermes Dashboard compat stubs
|
||||
//
|
||||
// hermes-agent 0.14.0 (both PyPI wheel and `git+...` source) ships
|
||||
// `hermes_cli/web_server.py` with hard imports of `hermes_cli.dashboard_auth.*`
|
||||
// submodules whose source files are NOT included in the distribution. It also
|
||||
// omits the built dashboard SPA (`hermes_cli/web_dist/`). On Windows in
|
||||
// particular, the missing dashboard_auth subpackage breaks `hermes dashboard`
|
||||
// completely, taking down every ClawPanel page that talks to port 9119
|
||||
// (Profile, Kanban, OAuth, Channels, Sessions detail).
|
||||
//
|
||||
// To stay self-sufficient (per project policy: do not patch upstream), we
|
||||
// inject a minimal pass-through stub into the installed venv:
|
||||
// - `hermes_cli/dashboard_auth/{__init__,audit,middleware,prefix,routes,ws_tickets}.py`
|
||||
// so all `from hermes_cli.dashboard_auth.* import ...` lines resolve.
|
||||
// Auth is a no-op; valid for loopback (127.0.0.1) bindings where the
|
||||
// auth gate is intentionally disabled.
|
||||
// - `hermes_cli/web_dist/index.html` so `mount_spa()` takes the
|
||||
// token-injecting branch instead of the `Frontend not built` 404 branch.
|
||||
// Without this, the panel's `dashboard_session_token` scrape returns
|
||||
// 404 and all `/api/*` calls fail with 401.
|
||||
//
|
||||
// The injection is idempotent: if upstream eventually ships either piece,
|
||||
// the corresponding stub write is skipped so the real implementation wins.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const HERMES_DASHBOARD_AUTH_INIT_PY: &str = r#""""ClawPanel-injected stub for hermes_cli.dashboard_auth.
|
||||
|
||||
Upstream hermes-agent ships web_server.py with imports referencing this
|
||||
subpackage, but the actual source files are NOT included in the wheel or
|
||||
the public git repo. To keep Hermes Dashboard usable in loopback
|
||||
(127.0.0.1) mode, ClawPanel injects this minimal pass-through stub at
|
||||
install/upgrade time.
|
||||
|
||||
When upstream eventually ships the real module, delete this directory
|
||||
and reinstall hermes-agent; the real implementation will be picked up.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List
|
||||
|
||||
|
||||
class DashboardAuthProvider:
|
||||
"""Stub base class. Real providers inherit from this."""
|
||||
|
||||
name: str = ""
|
||||
|
||||
|
||||
_REGISTERED: List["DashboardAuthProvider"] = []
|
||||
|
||||
|
||||
def register_provider(provider: "DashboardAuthProvider") -> None:
|
||||
"""No-op stub. ClawPanel binds to 127.0.0.1 so the gate is disabled."""
|
||||
if isinstance(provider, DashboardAuthProvider):
|
||||
_REGISTERED.append(provider)
|
||||
|
||||
|
||||
def list_providers() -> Iterable["DashboardAuthProvider"]:
|
||||
"""Return registered providers (empty on loopback)."""
|
||||
return list(_REGISTERED)
|
||||
|
||||
|
||||
__all__ = ["DashboardAuthProvider", "register_provider", "list_providers"]
|
||||
"#;
|
||||
|
||||
const HERMES_DASHBOARD_AUTH_AUDIT_PY: &str = r#""""ClawPanel stub: hermes_cli.dashboard_auth.audit"""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AuditEvent(str, Enum):
|
||||
LOGIN = "login"
|
||||
LOGOUT = "logout"
|
||||
LOGIN_FAILED = "login_failed"
|
||||
WS_TICKET_MINTED = "ws_ticket_minted"
|
||||
WS_TICKET_REJECTED = "ws_ticket_rejected"
|
||||
PROVIDER_REGISTERED = "provider_registered"
|
||||
|
||||
|
||||
def audit_log(event: Any, **fields: Any) -> None:
|
||||
"""No-op stub. Real implementation appends to an audit log file."""
|
||||
return None
|
||||
|
||||
|
||||
__all__ = ["AuditEvent", "audit_log"]
|
||||
"#;
|
||||
|
||||
const HERMES_DASHBOARD_AUTH_MIDDLEWARE_PY: &str = r#""""ClawPanel stub: hermes_cli.dashboard_auth.middleware"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
async def gated_auth_middleware(request, call_next):
|
||||
"""Pass-through ASGI middleware. Real one enforces JWT on non-loopback."""
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
__all__ = ["gated_auth_middleware"]
|
||||
"#;
|
||||
|
||||
const HERMES_DASHBOARD_AUTH_PREFIX_PY: &str = r#""""ClawPanel stub: hermes_cli.dashboard_auth.prefix"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def normalise_prefix(prefix: str) -> str:
|
||||
"""Normalise X-Forwarded-Prefix style values to a leading-slash form."""
|
||||
if not prefix:
|
||||
return ""
|
||||
return "/" + prefix.strip("/")
|
||||
|
||||
|
||||
__all__ = ["normalise_prefix"]
|
||||
"#;
|
||||
|
||||
const HERMES_DASHBOARD_AUTH_ROUTES_PY: &str = r#""""ClawPanel stub: hermes_cli.dashboard_auth.routes"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
"#;
|
||||
|
||||
const HERMES_DASHBOARD_AUTH_WS_TICKETS_PY: &str = r#""""ClawPanel stub: hermes_cli.dashboard_auth.ws_tickets"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class TicketInvalid(Exception):
|
||||
"""Raised when a WS ticket is rejected. Stub never raises."""
|
||||
|
||||
|
||||
def mint_ticket(*args, **kwargs) -> str:
|
||||
"""Stub. Real one mints short-lived JWTs."""
|
||||
return "stub-loopback-ticket"
|
||||
|
||||
|
||||
def consume_ticket(*args, **kwargs) -> None:
|
||||
"""Stub. Real one validates signature + expiry. Never raises here."""
|
||||
return None
|
||||
|
||||
|
||||
__all__ = ["TicketInvalid", "mint_ticket", "consume_ticket"]
|
||||
"#;
|
||||
|
||||
const HERMES_DASHBOARD_WEB_DIST_INDEX_HTML: &str = r#"<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hermes Dashboard (ClawPanel stub)</title>
|
||||
<meta name="generator" content="clawpanel-dashboard-spa-stub">
|
||||
</head>
|
||||
<body>
|
||||
<main style="font-family:system-ui,-apple-system,sans-serif;padding:32px;color:#333">
|
||||
<h1 style="margin:0 0 16px">Hermes Dashboard</h1>
|
||||
<p>This SPA placeholder is injected by ClawPanel so the dashboard backend
|
||||
emits a session token. ClawPanel provides its own UI; the upstream
|
||||
SPA is not shipped with the wheel.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
/// Locate the installed `hermes_cli` package directory inside the uv tool venv.
|
||||
///
|
||||
/// Layouts vary by platform:
|
||||
/// - Windows: `<uv tool dir>/hermes-agent/Lib/site-packages/hermes_cli`
|
||||
/// - macOS / Linux: `<uv tool dir>/hermes-agent/lib/python3.X/site-packages/hermes_cli`
|
||||
///
|
||||
/// Returns `None` if uv is unavailable or hermes-agent is not installed.
|
||||
fn locate_hermes_cli_package_dir() -> Option<std::path::PathBuf> {
|
||||
let uv_path = uv_bin_path();
|
||||
let uv_cmd = if uv_path.exists() {
|
||||
uv_path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"uv".into()
|
||||
};
|
||||
let mut cmd = std::process::Command::new(&uv_cmd);
|
||||
cmd.args(["tool", "dir"]);
|
||||
cmd.env("PATH", hermes_enhanced_path());
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
let output = cmd.output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let hermes_root = std::path::PathBuf::from(&stdout).join("hermes-agent");
|
||||
if !hermes_root.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let windows_path = hermes_root
|
||||
.join("Lib")
|
||||
.join("site-packages")
|
||||
.join("hermes_cli");
|
||||
if windows_path.exists() {
|
||||
return Some(windows_path);
|
||||
}
|
||||
let lib_dir = hermes_root.join("lib");
|
||||
if let Ok(entries) = std::fs::read_dir(&lib_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let pkg = entry.path().join("site-packages").join("hermes_cli");
|
||||
if pkg.exists() {
|
||||
return Some(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Inject the dashboard_auth and web_dist stubs into the installed hermes-agent
|
||||
/// venv if upstream did not ship them. Idempotent: existing files are never
|
||||
/// overwritten so the real implementation, if/when it lands, wins.
|
||||
///
|
||||
/// Stub injection failures are logged and swallowed — install/upgrade succeeds
|
||||
/// regardless so users aren't blocked by best-effort compatibility patches.
|
||||
fn inject_hermes_dashboard_compat_stub(app: &tauri::AppHandle) {
|
||||
let hermes_cli = match locate_hermes_cli_package_dir() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
"⚠ 跳过 dashboard 兼容 stub 注入:未找到 hermes_cli 包目录",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut wrote_auth = false;
|
||||
let auth_dir = hermes_cli.join("dashboard_auth");
|
||||
if !auth_dir.join("__init__.py").exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(&auth_dir) {
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
format!("⚠ 无法创建 dashboard_auth 目录: {e}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let files: [(&str, &str); 6] = [
|
||||
("__init__.py", HERMES_DASHBOARD_AUTH_INIT_PY),
|
||||
("audit.py", HERMES_DASHBOARD_AUTH_AUDIT_PY),
|
||||
("middleware.py", HERMES_DASHBOARD_AUTH_MIDDLEWARE_PY),
|
||||
("prefix.py", HERMES_DASHBOARD_AUTH_PREFIX_PY),
|
||||
("routes.py", HERMES_DASHBOARD_AUTH_ROUTES_PY),
|
||||
("ws_tickets.py", HERMES_DASHBOARD_AUTH_WS_TICKETS_PY),
|
||||
];
|
||||
for (name, content) in files {
|
||||
let path = auth_dir.join(name);
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
format!("⚠ 写入 dashboard_auth/{name} 失败: {e}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
wrote_auth = true;
|
||||
}
|
||||
|
||||
let mut wrote_dist = false;
|
||||
let dist_dir = hermes_cli.join("web_dist");
|
||||
let index_path = dist_dir.join("index.html");
|
||||
if !index_path.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(dist_dir.join("assets")) {
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
format!("⚠ 无法创建 web_dist 目录: {e}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = std::fs::write(&index_path, HERMES_DASHBOARD_WEB_DIST_INDEX_HTML) {
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
format!("⚠ 写入 web_dist/index.html 失败: {e}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
wrote_dist = true;
|
||||
}
|
||||
|
||||
if wrote_auth || wrote_dist {
|
||||
let mut parts: Vec<&str> = Vec::new();
|
||||
if wrote_auth {
|
||||
parts.push("dashboard_auth");
|
||||
}
|
||||
if wrote_dist {
|
||||
parts.push("web_dist");
|
||||
}
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
format!("📦 已注入 Hermes Dashboard 兼容 stub: {}", parts.join(", ")),
|
||||
);
|
||||
} else {
|
||||
let _ = app.emit(
|
||||
"hermes-install-log",
|
||||
"✓ Hermes Dashboard 兼容 stub 已存在,无需注入",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_hermes_install_output(text: &str) -> String {
|
||||
let mut out = text.replace(HERMES_GIT_URL, "hermes-agent");
|
||||
out = out.replace(
|
||||
@@ -13463,6 +13780,8 @@ pub async fn update_hermes(app: tauri::AppHandle) -> Result<String, String> {
|
||||
}
|
||||
|
||||
if output.status.success() {
|
||||
// 注入 dashboard 兼容 stub(升级路径与安装路径保持一致,避免上游 wheel 漏装的子包再次缺失)
|
||||
inject_hermes_dashboard_compat_stub(&app);
|
||||
let _ = app.emit("hermes-install-log", "✅ 升级完成");
|
||||
let _ = app.emit("hermes-install-progress", 100u32);
|
||||
Ok("升级完成".into())
|
||||
|
||||
@@ -12,11 +12,92 @@
|
||||
* - 不持久化(一次性会话,刷新清空)
|
||||
*/
|
||||
import { t } from '../../../lib/i18n.js'
|
||||
import { api, isTauriRuntime } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { api, isTauriRuntime, safeTauriListen } from '../../../lib/tauri-api.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
/**
|
||||
* Hermes `hermes_agent_run` 是 streaming-with-events:它通过 SSE 消费 Hermes Gateway
|
||||
* 的 `/v1/runs/{id}/events` 并把每个事件用 `app.emit("hermes-run-*")` 派发到前端,
|
||||
* 命令本身 resolve 的是 *run_id 字符串*(不是 final 输出)。
|
||||
*
|
||||
* 群聊页之前把 run_id 当成回复直接展示出来(典型现象:消息气泡里只有 `"run_xxx..."`),
|
||||
* 是因为 onSend 把 `await api.hermesAgentRun(...)` 的返回值当成结果对象去解析。
|
||||
*
|
||||
* 这个 helper 串联两端:
|
||||
* 1. 注册 `hermes-run-{started,delta,done,error,cancelled}` listener
|
||||
* 2. 调用 `hermesAgentRun(input)` 触发 run;命令在 SSE 流结束后才 resolve,
|
||||
* 所以 done 事件一般已经先到了 — listener 即可拿到 `payload.output`。
|
||||
* 3. 兜底:done 没到时累积 delta 文本作为最终结果。
|
||||
*
|
||||
* 注意:并发场景下 listener 会全局收事件,因此用 run_id 过滤,
|
||||
* 串行模式(当前群聊调度方式)也能 race-safe。
|
||||
*/
|
||||
async function runHermesAgentAndWaitFinal(input) {
|
||||
if (!isTauriRuntime()) {
|
||||
throw new Error('Hermes group chat requires Tauri runtime')
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const unsubs = []
|
||||
let runId = null
|
||||
let accumulated = ''
|
||||
let settled = false
|
||||
const cleanup = () => {
|
||||
for (const u of unsubs) {
|
||||
try { u() } catch { /* listener already detached */ }
|
||||
}
|
||||
}
|
||||
const finish = (text) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
resolve(text)
|
||||
}
|
||||
const fail = (err) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(err)
|
||||
}
|
||||
const matchesRun = (rid) => !runId || !rid || rid === runId
|
||||
;(async () => {
|
||||
try {
|
||||
unsubs.push(await safeTauriListen('hermes-run-started', (e) => {
|
||||
if (!runId && e?.payload?.run_id) runId = e.payload.run_id
|
||||
}))
|
||||
unsubs.push(await safeTauriListen('hermes-run-delta', (e) => {
|
||||
if (!matchesRun(e?.payload?.run_id)) return
|
||||
accumulated += e?.payload?.delta || ''
|
||||
}))
|
||||
unsubs.push(await safeTauriListen('hermes-run-done', (e) => {
|
||||
if (!matchesRun(e?.payload?.run_id)) return
|
||||
const out = (e?.payload?.output || accumulated || '').trim()
|
||||
finish(out)
|
||||
}))
|
||||
unsubs.push(await safeTauriListen('hermes-run-error', (e) => {
|
||||
if (!matchesRun(e?.payload?.run_id)) return
|
||||
fail(new Error(e?.payload?.error || 'unknown error'))
|
||||
}))
|
||||
unsubs.push(await safeTauriListen('hermes-run-cancelled', (e) => {
|
||||
if (!matchesRun(e?.payload?.run_id)) return
|
||||
finish(accumulated.trim() || '(cancelled)')
|
||||
}))
|
||||
|
||||
// 触发 run。Rust 端 hermes_agent_run 内部消费 SSE 直到 [DONE] 才 resolve,
|
||||
// 因此 done 事件一般已经先到,listener 已经 finish 过;这里拿到的 run_id 仅作兜底。
|
||||
const ridFromAck = await api.hermesAgentRun(input, null, null, null, null)
|
||||
if (!runId) runId = ridFromAck
|
||||
|
||||
// 防御:如果 done 事件因为顺序问题尚未派发(理论上不会发生),等一拍兜底
|
||||
setTimeout(() => {
|
||||
if (!settled) finish(accumulated.trim())
|
||||
}, 300)
|
||||
} catch (e) {
|
||||
fail(e)
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
@@ -238,16 +319,11 @@ export function render() {
|
||||
await api.hermesProfileUse(profile)
|
||||
activeProfile = profile
|
||||
}
|
||||
// 调 agent run(非流式)
|
||||
const result = await api.hermesAgentRun(text, null, null, null, null)
|
||||
// result 形如 { final, messages, ... }
|
||||
const finalText = result?.final?.content
|
||||
|| result?.final
|
||||
|| result?.output
|
||||
|| (Array.isArray(result?.messages) && result.messages.filter(m => m.role === 'assistant').slice(-1)[0]?.content)
|
||||
|| JSON.stringify(result || '').slice(0, 500)
|
||||
// 触发 agent run,并通过 hermes-run-* 事件等真正的 final 输出。
|
||||
// 不能直接用 hermesAgentRun 的返回值,它只是 run_id 字符串,不是回复内容。
|
||||
const finalText = await runHermesAgentAndWaitFinal(text)
|
||||
placeholder.loading = false
|
||||
placeholder.content = String(finalText || '').trim() || t('engine.hermesGroupChatNoOutput')
|
||||
placeholder.content = finalText || t('engine.hermesGroupChatNoOutput')
|
||||
placeholder.ts = Date.now()
|
||||
} catch (e) {
|
||||
placeholder.loading = false
|
||||
|
||||
Reference in New Issue
Block a user