feat: stabilize page AI ACP runtimes
实现并稳定页面 AI 的 ACP Hermes / ACP Reasonix 运行路径。 主要内容: - 分离 profile 与 acpRuntime,ACP Hermes 按所选 Hermes profile 启动并注入 provider key。 - 修复 Reasonix ACP wrapper 的 API key 读取、ToolRegistry 注册、LoopEvent role 映射和 reasoning/final 分流。 - 修复 ACP agent_thought_chunk 被 untagged enum 误解析为 message.delta 的问题,补充 thought 相关单测。 - 补充页面 AI 浏览器验证 skill 证据到 7-15 设计稿,并记录严格验收标准。 - 同步提交当前仓库中已存在的 rust-web / Hermes tools / SSE / bug 文档相关改动。 验证: - node --check scripts/reasonix-acp-wrapper.mjs - cargo test -p mnote-web acp -- --nocapture - 页面 AI ACP 浏览器验证:tmp/page-ai-acp-browser-UAYwyM/
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
/// Response: { jsonrpc: "2.0", id: number, result?: any, error?: { code, message } }
|
||||
/// Notification:{ jsonrpc: "2.0", method: string, params?: object } (no id)
|
||||
/// Incoming: { jsonrpc: "2.0", id: number, method: string, params?: object } (from agent)
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
@@ -21,7 +20,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
|
||||
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
|
||||
use tokio::sync::{Mutex, oneshot};
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
@@ -120,27 +119,40 @@ impl AcpClient {
|
||||
///
|
||||
/// Reference: `hermes-vscode-main/src/acpClient.ts` L50-80 (spawn + stdio setup)
|
||||
pub async fn spawn(bin: &str, args: &[&str]) -> Result<Self, AcpError> {
|
||||
let mut child = Command::new(bin)
|
||||
Self::spawn_with_env(bin, args, None).await
|
||||
}
|
||||
|
||||
/// Spawn an ACP subprocess with extra environment variables.
|
||||
pub async fn spawn_with_env(
|
||||
bin: &str,
|
||||
args: &[&str],
|
||||
env_overrides: Option<&HashMap<String, String>>,
|
||||
) -> Result<Self, AcpError> {
|
||||
let mut command = Command::new(bin);
|
||||
command
|
||||
.args(args)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.map_err(AcpError::Spawn)?;
|
||||
.kill_on_drop(true);
|
||||
if let Some(env) = env_overrides {
|
||||
command.envs(env);
|
||||
}
|
||||
let mut child = command.spawn().map_err(AcpError::Spawn)?;
|
||||
|
||||
let stdin = child.stdin.take().ok_or_else(|| {
|
||||
AcpError::Internal("failed to take child stdin".into())
|
||||
})?;
|
||||
let stdout = child.stdout.take().ok_or_else(|| {
|
||||
AcpError::Internal("failed to take child stdout".into())
|
||||
})?;
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| AcpError::Internal("failed to take child stdin".into()))?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| AcpError::Internal("failed to take child stdout".into()))?;
|
||||
|
||||
let writer = BufWriter::new(stdin);
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
let pending: Arc<Mutex<HashMap<u64, PendingEntry>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
let pending: Arc<Mutex<HashMap<u64, PendingEntry>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
let notification_handler: Arc<NotificationHandlerMutex> =
|
||||
Arc::new(std::sync::Mutex::new(None));
|
||||
|
||||
@@ -182,7 +194,8 @@ impl AcpClient {
|
||||
method: &str,
|
||||
params: P,
|
||||
) -> Result<R, AcpError> {
|
||||
self.request_with_timeout(method, params, Duration::from_secs(300)).await
|
||||
self.request_with_timeout(method, params, Duration::from_secs(300))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Same as [`request`] but with a configurable timeout.
|
||||
@@ -298,7 +311,10 @@ impl AcpClient {
|
||||
let msg: Value = match serde_json::from_str(trimmed) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
warn!("ACP parse error: {e} (line: {})", &trimmed[..trimmed.len().min(80)]);
|
||||
warn!(
|
||||
"ACP parse error: {e} (line: {})",
|
||||
&trimmed[..trimmed.len().min(80)]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -322,7 +338,11 @@ impl AcpClient {
|
||||
notification_handler: &Arc<NotificationHandlerMutex>,
|
||||
) {
|
||||
let has_id = msg.get("id").is_some();
|
||||
let has_method = msg.get("method").and_then(|v| v.as_str()).map(|s| !s.is_empty()).unwrap_or(false);
|
||||
let has_method = msg
|
||||
.get("method")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_id && has_method {
|
||||
// Incoming request from agent (e.g. session/request_permission)
|
||||
@@ -458,14 +478,14 @@ rl.on('line', (line) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Send a notification that the mock server will echo back as...
|
||||
// Send a notification that the mock server will echo back as...
|
||||
// Actually the mock doesn't send unsolicited notifications.
|
||||
// This test just validates the handler registration doesn't crash.
|
||||
client
|
||||
.notification("test_push", json!({}))
|
||||
.await
|
||||
.expect("notification");
|
||||
|
||||
|
||||
// Give background task time to process
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
// In this mock, no notification will be received; that's OK
|
||||
|
||||
Reference in New Issue
Block a user