阶段2:新增主站 Client Builder 工厂及验收测试
新增 AGC 主站专用 reqwest Client Builder 工厂。 通过 default_headers 统一注入 X-Genarrative-Client: agc。 保留调用方对 timeout、connect timeout、redirect 和 no_proxy 的配置能力。 验证请求级 Authorization 和 Idempotency-Key 继续生效。 更新 Issue 226 实施方案与分阶段验收计划,明确阶段 3 才迁移业务 Client 创建点。
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
|
||||
const AGC_CLIENT_MARKER_HEADER: &str = "x-genarrative-client";
|
||||
const AGC_CLIENT_MARKER_VALUE: &str = "agc";
|
||||
|
||||
pub(crate) fn agc_main_site_client_builder() -> reqwest::ClientBuilder {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
HeaderName::from_static(AGC_CLIENT_MARKER_HEADER),
|
||||
HeaderValue::from_static(AGC_CLIENT_MARKER_VALUE),
|
||||
);
|
||||
reqwest::Client::builder().default_headers(headers)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{agc_main_site_client_builder, AGC_CLIENT_MARKER_HEADER, AGC_CLIENT_MARKER_VALUE};
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
fn read_http_request(stream: &mut TcpStream) -> String {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.expect("set HTTP fixture read timeout");
|
||||
let mut bytes = Vec::new();
|
||||
let mut buffer = [0_u8; 4096];
|
||||
loop {
|
||||
let read = stream.read(&mut buffer).expect("read HTTP fixture request");
|
||||
assert!(read > 0, "HTTP fixture request closed before headers");
|
||||
bytes.extend_from_slice(&buffer[..read]);
|
||||
if bytes.windows(4).any(|value| value == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
fn request_header(request: &str, expected_name: &str) -> Option<String> {
|
||||
request.lines().find_map(|line| {
|
||||
let (name, value) = line.split_once(':')?;
|
||||
name.eq_ignore_ascii_case(expected_name)
|
||||
.then(|| value.trim().to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_http_fixture(listener: TcpListener) -> std::thread::JoinHandle<String> {
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept HTTP fixture request");
|
||||
let request = read_http_request(&mut stream);
|
||||
stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
|
||||
)
|
||||
.expect("write HTTP fixture response");
|
||||
request
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factory_sets_the_agc_marker_as_a_default_header() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP fixture");
|
||||
let address = listener.local_addr().expect("read HTTP fixture address");
|
||||
let fixture = spawn_http_fixture(listener);
|
||||
let client = agc_main_site_client_builder()
|
||||
.build()
|
||||
.expect("build AGC main-site client");
|
||||
let response = client
|
||||
.get(format!("http://{address}/api/auth/me"))
|
||||
.send()
|
||||
.await
|
||||
.expect("send request");
|
||||
let request = fixture.join().expect("join HTTP fixture");
|
||||
|
||||
assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
assert_eq!(
|
||||
request_header(&request, AGC_CLIENT_MARKER_HEADER),
|
||||
Some(AGC_CLIENT_MARKER_VALUE.to_string())
|
||||
);
|
||||
assert!(request_header(&request, AUTHORIZATION.as_str()).is_none());
|
||||
assert!(request_header(&request, "idempotency-key").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factory_keeps_request_headers_and_transport_options_configurable() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP fixture");
|
||||
let address = listener.local_addr().expect("read HTTP fixture address");
|
||||
let fixture = spawn_http_fixture(listener);
|
||||
let client = agc_main_site_client_builder()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(60))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("build configured AGC main-site client");
|
||||
let response = client
|
||||
.post(format!("http://{address}/api/editor/images/generations"))
|
||||
.bearer_auth("fixture-token")
|
||||
.header("Idempotency-Key", "fixture-id")
|
||||
.send()
|
||||
.await
|
||||
.expect("send configured request");
|
||||
let request = fixture.join().expect("join HTTP fixture");
|
||||
|
||||
assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
assert_eq!(
|
||||
request_header(&request, AGC_CLIENT_MARKER_HEADER),
|
||||
Some(AGC_CLIENT_MARKER_VALUE.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_header(&request, AUTHORIZATION.as_str()),
|
||||
Some("Bearer fixture-token".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_header(&request, "idempotency-key"),
|
||||
Some("fixture-id".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,7 @@ mod debug;
|
||||
mod delegation;
|
||||
mod git_inspect;
|
||||
mod goal;
|
||||
mod http_client;
|
||||
mod image_inspect;
|
||||
mod isolated_agent;
|
||||
mod patchset;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- `#226 添加客户端特殊标识`:本方案实际实施范围
|
||||
- `#225 添加客户端埋点统计`:主站接收、落库和后台统计,本文只冻结交接契约,不在本次实施
|
||||
|
||||
状态:阶段 1 已完成,待阶段 2 实施<br>
|
||||
状态:阶段 2 已完成,待阶段 3 实施<br>
|
||||
本方案不包含代码修改
|
||||
|
||||
## 1. 一句话交付结果
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- `#226 添加客户端特殊标识`:本计划全部实施范围
|
||||
- `#225 添加客户端埋点统计`:只接收交接契约,不在本计划实现
|
||||
|
||||
当前状态:阶段 1 已完成;阶段 2 尚未开始。阶段 0 的证据与验收记录见
|
||||
当前状态:阶段 2 已完成;阶段 3 尚未开始。阶段 0 的证据与验收记录见
|
||||
[【阶段验收】Issue226阶段0现状基线与契约冻结-2026-09-01.md](C:/projects/narrative/Genarrative/local-docs/【阶段验收】Issue226阶段0现状基线与契约冻结-2026-09-01.md)。
|
||||
|
||||
## 1. 交付目标
|
||||
@@ -172,6 +172,15 @@ fn agc_main_site_client_builder() -> reqwest::ClientBuilder;
|
||||
- factory 的职责没有扩展到业务编排。
|
||||
- 现有测试或新增测试能锁定 Header 默认值。
|
||||
|
||||
### 6.4 阶段 2 实施记录
|
||||
|
||||
- 新增 `src-tauri/src/http_client.rs`,提供 `agc_main_site_client_builder()`。
|
||||
- factory 仅通过 `default_headers` 注入 `x-genarrative-client: agc`,不接管认证、幂等、请求体、重试或错误解析。
|
||||
- 调用方仍可在返回的 builder 上继续配置 connect timeout、request timeout、redirect policy 和 `no_proxy`。
|
||||
- 新增两个本地 TCP fixture 单元测试,验证真实发送请求带标记、请求级 Authorization/Idempotency-Key 保留,且 factory 不自动注入认证信息。
|
||||
- 阶段 2 验证命令:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml http_client -- --nocapture`(2 tests passed);`cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` 通过。
|
||||
- 阶段 2 未迁移任何业务 Client 创建点;迁移属于阶段 3。
|
||||
|
||||
## 7. 阶段 3:主站 Client 创建点迁移
|
||||
|
||||
### 7.1 工作内容
|
||||
|
||||
Reference in New Issue
Block a user