fix local office resource editing
- add local-folder OnlyOffice sign/callback writeback and edit-tab handling - align main resource tabs, attachment edit menu, slash isolation, and filetree context behavior - record Sidex/Hermes gap reviews and Reasonix task checklists
This commit is contained in:
@@ -80,6 +80,17 @@ type NotificationHandler = Box<dyn Fn(String, Value) + Send + 'static>;
|
||||
/// Thread-safe mutex for notification handler (std mutex — lightweight, never held across awaits).
|
||||
type NotificationHandlerMutex = std::sync::Mutex<Option<NotificationHandler>>;
|
||||
|
||||
// ── Incoming request handler type ────────────────────
|
||||
///
|
||||
/// agent 发送 JSON-RPC request(同时包含 `id` 与 `method`)时调用。
|
||||
/// 返回 `true` 表示 handler 已负责稍后响应;返回 `false` 则由 dispatch_message
|
||||
/// 直接回复 method-not-found。handler 应通过 [`AcpClient::respond_to_incoming`]
|
||||
/// 或 [`AcpClient::respond_to_incoming_error`] 回写响应。
|
||||
type IncomingRequestHandler = Box<dyn Fn(Value, String, Value) -> bool + Send + 'static>;
|
||||
|
||||
/// incoming request handler 的线程安全容器。
|
||||
type IncomingRequestHandlerMutex = std::sync::Mutex<Option<IncomingRequestHandler>>;
|
||||
|
||||
// ── Pending request entry ────────────────────────────
|
||||
|
||||
type PendingEntry = oneshot::Sender<Result<Value, AcpError>>;
|
||||
@@ -108,6 +119,9 @@ pub struct AcpClient {
|
||||
pending: Arc<Mutex<HashMap<u64, PendingEntry>>>,
|
||||
next_id: AtomicU64,
|
||||
notification_handler: Arc<NotificationHandlerMutex>,
|
||||
/// agent 发来的 incoming JSON-RPC request handler(同时有 id 和 method)。
|
||||
/// 未设置时会直接回复 method-not-found。
|
||||
incoming_request_handler: Arc<IncomingRequestHandlerMutex>,
|
||||
}
|
||||
|
||||
impl AcpClient {
|
||||
@@ -162,14 +176,24 @@ impl AcpClient {
|
||||
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));
|
||||
let incoming_request_handler: Arc<IncomingRequestHandlerMutex> =
|
||||
Arc::new(std::sync::Mutex::new(None));
|
||||
|
||||
// Start background reader task
|
||||
let pending_clone = pending.clone();
|
||||
let handler_clone = notification_handler.clone();
|
||||
let incoming_clone = incoming_request_handler.clone();
|
||||
let writer_clone = writer.clone();
|
||||
let child_pid = child.id().unwrap_or(0);
|
||||
tokio::spawn(async move {
|
||||
Self::reader_loop(reader, writer_clone, pending_clone, handler_clone).await;
|
||||
Self::reader_loop(
|
||||
reader,
|
||||
writer_clone,
|
||||
pending_clone,
|
||||
handler_clone,
|
||||
incoming_clone,
|
||||
)
|
||||
.await;
|
||||
info!("ACP reader loop ended (pid={})", child_pid);
|
||||
});
|
||||
|
||||
@@ -179,6 +203,7 @@ impl AcpClient {
|
||||
pending,
|
||||
next_id: AtomicU64::new(1),
|
||||
notification_handler,
|
||||
incoming_request_handler,
|
||||
};
|
||||
|
||||
// Handshake: initialize (reference: acpClient.ts L108 → call('initialize', {protocolVersion: 1}))
|
||||
@@ -272,6 +297,53 @@ impl AcpClient {
|
||||
*guard = Some(Box::new(handler));
|
||||
}
|
||||
|
||||
/// 注册 incoming JSON-RPC request handler(消息同时包含 `id` 与 `method`)。
|
||||
/// handler 接收原始 request id、method 和 params,并应稍后通过
|
||||
/// [`respond_to_incoming`] 或 [`respond_to_incoming_error`] 响应。
|
||||
/// 同一时间只保留一个 handler,后续注册会覆盖前一个。
|
||||
pub fn on_incoming_request<F>(&self, handler: F)
|
||||
where
|
||||
F: Fn(Value, String, Value) -> bool + Send + 'static,
|
||||
{
|
||||
let mut guard = self.incoming_request_handler.lock().unwrap();
|
||||
*guard = Some(Box::new(handler));
|
||||
}
|
||||
|
||||
/// 用 result 响应 agent 发来的 incoming JSON-RPC request。
|
||||
///
|
||||
/// 必须使用 incoming request handler 收到的原始 `id`,避免丢失字符串 id。
|
||||
pub async fn respond_to_incoming(&self, id: Value, result: Value) -> Result<(), AcpError> {
|
||||
let msg = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id.clone(),
|
||||
"result": result,
|
||||
});
|
||||
debug!("ACP <-- respond to incoming #{}", id);
|
||||
Self::write_jsonrpc_message(&self.writer, &msg).await
|
||||
}
|
||||
|
||||
/// 用 error 响应 incoming JSON-RPC request。
|
||||
pub async fn respond_to_incoming_error(
|
||||
&self,
|
||||
id: Value,
|
||||
code: i64,
|
||||
message: &str,
|
||||
) -> Result<(), AcpError> {
|
||||
let msg = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id.clone(),
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message,
|
||||
},
|
||||
});
|
||||
debug!(
|
||||
"ACP <-- respond error to incoming #{}: [{}] {}",
|
||||
id, code, message
|
||||
);
|
||||
Self::write_jsonrpc_message(&self.writer, &msg).await
|
||||
}
|
||||
|
||||
/// Gracefully close the ACP connection and kill the subprocess.
|
||||
pub async fn close(&mut self) {
|
||||
if let Some(mut child) = self.child.take() {
|
||||
@@ -296,6 +368,7 @@ impl AcpClient {
|
||||
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
pending: Arc<Mutex<HashMap<u64, PendingEntry>>>,
|
||||
notification_handler: Arc<NotificationHandlerMutex>,
|
||||
incoming_request_handler: Arc<IncomingRequestHandlerMutex>,
|
||||
) {
|
||||
let mut line_buf = String::new();
|
||||
loop {
|
||||
@@ -328,7 +401,14 @@ impl AcpClient {
|
||||
}
|
||||
};
|
||||
|
||||
Self::dispatch_message(msg, &writer, &pending, ¬ification_handler).await;
|
||||
Self::dispatch_message(
|
||||
msg,
|
||||
&writer,
|
||||
&pending,
|
||||
¬ification_handler,
|
||||
&incoming_request_handler,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Process died or EOF — resolve all pending
|
||||
@@ -346,6 +426,7 @@ impl AcpClient {
|
||||
writer: &Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
pending: &Arc<Mutex<HashMap<u64, PendingEntry>>>,
|
||||
notification_handler: &Arc<NotificationHandlerMutex>,
|
||||
incoming_request_handler: &Arc<IncomingRequestHandlerMutex>,
|
||||
) {
|
||||
let has_id = msg.get("id").is_some();
|
||||
let has_method = msg
|
||||
@@ -355,33 +436,36 @@ impl AcpClient {
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_id && has_method {
|
||||
// Incoming request from agent (e.g. session/request_permission)
|
||||
// agent 发来的 incoming request,例如 session/request_permission。
|
||||
let method = msg["method"].as_str().unwrap_or("unknown").to_string();
|
||||
let params = msg.get("params").cloned().unwrap_or(Value::Null);
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
// 当前还没有权限确认 UI,必须明确拒绝,避免 agent 等待到超时。
|
||||
warn!("ACP incoming request not handled: {method} (params={params:?})");
|
||||
let response = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": format!("ACP incoming request not supported: {method}")
|
||||
}
|
||||
});
|
||||
if let Err(error) = Self::write_jsonrpc_message(writer, &response).await {
|
||||
warn!("ACP incoming request response write failed: {error}");
|
||||
}
|
||||
if method == "session/request_permission" {
|
||||
let mut event_params = params;
|
||||
if let Some(object) = event_params.as_object_mut() {
|
||||
object.insert("decision".into(), Value::String("denied".into()));
|
||||
object.insert("method".into(), Value::String(method.clone()));
|
||||
object.insert("jsonrpcId".into(), id);
|
||||
}
|
||||
let handler_guard = notification_handler.lock().unwrap();
|
||||
let id_val = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
|
||||
// 若已注册 handler,则由 handler 决定是否负责稍后响应。
|
||||
let handled = {
|
||||
let handler_guard = incoming_request_handler.lock().unwrap();
|
||||
if let Some(ref handler) = *handler_guard {
|
||||
handler(method, event_params);
|
||||
handler(id_val.clone(), method.clone(), params.clone())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if handled {
|
||||
debug!("ACP incoming request dispatched: {method} #{}", id_val);
|
||||
} else {
|
||||
// 没有 handler 时必须立即响应,避免 agent 一直等待。
|
||||
warn!("ACP incoming request not handled (no handler registered): {method}");
|
||||
let response = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id_val,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": format!("ACP incoming request not supported: {method}")
|
||||
}
|
||||
});
|
||||
if let Err(error) = Self::write_jsonrpc_message(writer, &response).await {
|
||||
warn!("ACP incoming request response write failed: {error}");
|
||||
}
|
||||
}
|
||||
} else if has_id {
|
||||
@@ -528,6 +612,48 @@ rl.on('line', (line) => {
|
||||
.expect("spawn permission mock ACP")
|
||||
}
|
||||
|
||||
async fn spawn_string_id_permission_request_mock_server() -> AcpClient {
|
||||
let script = r#"
|
||||
import * as readline from 'node:readline';
|
||||
import { stdin as input, stdout as output } from 'node:process';
|
||||
let permissionResponse = null;
|
||||
const rl = readline.createInterface({ input, output, terminal: false });
|
||||
function send(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); }
|
||||
rl.on('line', (line) => {
|
||||
const msg = JSON.parse(line);
|
||||
if (msg.id !== undefined && msg.method === 'initialize') {
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id: msg.id,
|
||||
result: { protocolVersion: 1, agentCapabilities: {}, agentInfo: { name: 'mock', version: '1.0' }, authMethods: [] }
|
||||
});
|
||||
setTimeout(() => send({
|
||||
jsonrpc: '2.0',
|
||||
id: 'perm-string-id',
|
||||
method: 'session/request_permission',
|
||||
params: { reason: 'test permission' }
|
||||
}), 10);
|
||||
} else if (msg.id === 'perm-string-id' && msg.method === undefined) {
|
||||
permissionResponse = msg;
|
||||
} else if (msg.id !== undefined && msg.method === 'get_permission_response') {
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id: msg.id,
|
||||
result: { permissionResponse }
|
||||
});
|
||||
}
|
||||
});
|
||||
"#;
|
||||
|
||||
let dir = std::env::temp_dir();
|
||||
let script_path = dir.join("acp_permission_request_string_id_mock.mjs");
|
||||
std::fs::write(&script_path, script).expect("write permission string id mock script");
|
||||
|
||||
AcpClient::spawn("node", &[script_path.to_str().unwrap()])
|
||||
.await
|
||||
.expect("spawn permission string id mock ACP")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_response() {
|
||||
let client = spawn_mock_acp_server().await;
|
||||
@@ -603,4 +729,18 @@ rl.on('line', (line) => {
|
||||
assert_eq!(response["id"], 77);
|
||||
assert!(response.get("result").is_some() || response.get("error").is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_incoming_permission_request_preserves_string_id() {
|
||||
let client = spawn_string_id_permission_request_mock_server().await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
let result: Value = client
|
||||
.request("get_permission_response", json!({}))
|
||||
.await
|
||||
.expect("permission response probe");
|
||||
let response = &result["permissionResponse"];
|
||||
assert_eq!(response["jsonrpc"], "2.0");
|
||||
assert_eq!(response["id"], "perm-string-id");
|
||||
assert!(response.get("result").is_some() || response.get("error").is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user