chore: 保存当前架构收口与 bug 修复快照
归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
This commit is contained in:
@@ -149,7 +149,7 @@ impl AcpClient {
|
||||
.take()
|
||||
.ok_or_else(|| AcpError::Internal("failed to take child stdout".into()))?;
|
||||
|
||||
let writer = BufWriter::new(stdin);
|
||||
let writer = Arc::new(Mutex::new(BufWriter::new(stdin)));
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
let pending: Arc<Mutex<HashMap<u64, PendingEntry>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
@@ -159,15 +159,16 @@ impl AcpClient {
|
||||
// Start background reader task
|
||||
let pending_clone = pending.clone();
|
||||
let handler_clone = notification_handler.clone();
|
||||
let writer_clone = writer.clone();
|
||||
let child_pid = child.id().unwrap_or(0);
|
||||
tokio::spawn(async move {
|
||||
Self::reader_loop(reader, pending_clone, handler_clone).await;
|
||||
Self::reader_loop(reader, writer_clone, pending_clone, handler_clone).await;
|
||||
info!("ACP reader loop ended (pid={})", child_pid);
|
||||
});
|
||||
|
||||
let client = Self {
|
||||
child: Some(child),
|
||||
writer: Arc::new(Mutex::new(writer)),
|
||||
writer,
|
||||
pending,
|
||||
next_id: AtomicU64::new(1),
|
||||
notification_handler,
|
||||
@@ -285,6 +286,7 @@ impl AcpClient {
|
||||
/// Reference: `acpClient.ts` L120-180 (onData + dispatch)
|
||||
async fn reader_loop(
|
||||
mut reader: BufReader<ChildStdout>,
|
||||
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
pending: Arc<Mutex<HashMap<u64, PendingEntry>>>,
|
||||
notification_handler: Arc<NotificationHandlerMutex>,
|
||||
) {
|
||||
@@ -319,7 +321,7 @@ impl AcpClient {
|
||||
}
|
||||
};
|
||||
|
||||
Self::dispatch_message(msg, &pending, ¬ification_handler).await;
|
||||
Self::dispatch_message(msg, &writer, &pending, ¬ification_handler).await;
|
||||
}
|
||||
|
||||
// Process died or EOF — resolve all pending
|
||||
@@ -334,6 +336,7 @@ impl AcpClient {
|
||||
/// Reference: `acpClient.ts` L160-200 (dispatch)
|
||||
async fn dispatch_message(
|
||||
msg: Value,
|
||||
writer: &Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
pending: &Arc<Mutex<HashMap<u64, PendingEntry>>>,
|
||||
notification_handler: &Arc<NotificationHandlerMutex>,
|
||||
) {
|
||||
@@ -348,11 +351,32 @@ impl AcpClient {
|
||||
// Incoming request from agent (e.g. session/request_permission)
|
||||
let method = msg["method"].as_str().unwrap_or("unknown").to_string();
|
||||
let params = msg.get("params").cloned().unwrap_or(Value::Null);
|
||||
// For now, reject all incoming requests since we don't need permission dialogs yet.
|
||||
// Reference: acpClient.ts handleIncomingRequest (L200-220)
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
// 当前还没有权限确认 UI,必须明确拒绝,避免 agent 等待到超时。
|
||||
warn!("ACP incoming request not handled: {method} (params={params:?})");
|
||||
// If we wanted to reply, we'd need to write back a response...
|
||||
// For now just log. Phase C will add permission support.
|
||||
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();
|
||||
if let Some(ref handler) = *handler_guard {
|
||||
handler(method, event_params);
|
||||
}
|
||||
}
|
||||
} else if has_id {
|
||||
// Response to one of our requests
|
||||
if let Some(id) = msg["id"].as_u64() {
|
||||
@@ -388,6 +412,18 @@ impl AcpClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_jsonrpc_message(
|
||||
writer: &Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
msg: &Value,
|
||||
) -> Result<(), AcpError> {
|
||||
let line = serde_json::to_string(msg)?;
|
||||
let mut writer = writer.lock().await;
|
||||
writer.write_all(line.as_bytes()).await?;
|
||||
writer.write_all(b"\n").await?;
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AcpClient {
|
||||
@@ -443,6 +479,48 @@ rl.on('line', (line) => {
|
||||
.expect("spawn mock ACP")
|
||||
}
|
||||
|
||||
async fn spawn_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: 77,
|
||||
method: 'session/request_permission',
|
||||
params: { reason: 'test permission' }
|
||||
}), 10);
|
||||
} else if (msg.id === 77 && 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_mock.mjs");
|
||||
std::fs::write(&script_path, script).expect("write permission mock script");
|
||||
|
||||
AcpClient::spawn("node", &[script_path.to_str().unwrap()])
|
||||
.await
|
||||
.expect("spawn permission mock ACP")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_response() {
|
||||
let client = spawn_mock_acp_server().await;
|
||||
@@ -504,4 +582,18 @@ rl.on('line', (line) => {
|
||||
// spawn already calls initialize; if it fails, the test fails
|
||||
let _client = spawn_mock_acp_server().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_incoming_permission_request_gets_response() {
|
||||
let client = spawn_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"], 77);
|
||||
assert!(response.get("result").is_some() || response.get("error").is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user