fix: load DeepSeek config from browser runtime
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
@@ -7,6 +8,7 @@ use sgclaw::compat::config_adapter::{
|
||||
zeroclaw_workspace_dir,
|
||||
};
|
||||
use sgclaw::config::DeepSeekSettings;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
@@ -53,3 +55,44 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
|
||||
assert_eq!(config.default_model.as_deref(), Some("deepseek-reasoner"));
|
||||
assert_eq!(config.api_url.as_deref(), Some("https://proxy.example.com/v1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
|
||||
let root = std::env::temp_dir().join(format!("sgclaw-config-{}", Uuid::new_v4()));
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
let config_path = root.join("sgclaw_config.json");
|
||||
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"{
|
||||
"apiKey": "sk-first",
|
||||
"baseUrl": "",
|
||||
"model": ""
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let first = DeepSeekSettings::load(Some(config_path.as_path()))
|
||||
.unwrap()
|
||||
.expect("expected config file to produce settings");
|
||||
assert_eq!(first.api_key, "sk-first");
|
||||
assert_eq!(first.base_url, "https://api.deepseek.com");
|
||||
assert_eq!(first.model, "deepseek-chat");
|
||||
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"{
|
||||
"apiKey": "sk-second",
|
||||
"baseUrl": "https://proxy.example.com/v1",
|
||||
"model": "deepseek-reasoner"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let second = DeepSeekSettings::load(Some(config_path.as_path()))
|
||||
.unwrap()
|
||||
.expect("expected updated config file to produce settings");
|
||||
assert_eq!(second.api_key, "sk-second");
|
||||
assert_eq!(second.base_url, "https://proxy.example.com/v1");
|
||||
assert_eq!(second.model, "deepseek-reasoner");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod common;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::path::PathBuf;
|
||||
@@ -9,8 +10,13 @@ use std::time::Duration;
|
||||
|
||||
use common::MockTransport;
|
||||
use serde_json::{json, Value};
|
||||
use sgclaw::agent::handle_browser_message;
|
||||
use sgclaw::agent::{
|
||||
handle_browser_message,
|
||||
handle_browser_message_with_context,
|
||||
AgentRuntimeContext,
|
||||
};
|
||||
use sgclaw::compat::runtime::execute_task;
|
||||
use sgclaw::config::DeepSeekSettings;
|
||||
use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing};
|
||||
use sgclaw::security::MacPolicy;
|
||||
use uuid::Uuid;
|
||||
@@ -40,6 +46,21 @@ fn temp_workspace_root() -> PathBuf {
|
||||
root
|
||||
}
|
||||
|
||||
fn write_deepseek_config(root: &PathBuf, api_key: &str, base_url: &str, model: &str) -> PathBuf {
|
||||
let config_path = root.join("sgclaw_config.json");
|
||||
fs::write(
|
||||
&config_path,
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"apiKey": api_key,
|
||||
"baseUrl": base_url,
|
||||
"model": model,
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
config_path
|
||||
}
|
||||
|
||||
fn start_fake_deepseek_server(
|
||||
responses: Vec<Value>,
|
||||
) -> (String, Arc<Mutex<Vec<Value>>>, thread::JoinHandle<()>) {
|
||||
@@ -177,6 +198,7 @@ fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
|
||||
std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat");
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let settings = DeepSeekSettings::from_env().unwrap();
|
||||
let transport = Arc::new(MockTransport::new(vec![
|
||||
BrowserMessage::Response {
|
||||
seq: 1,
|
||||
@@ -211,6 +233,7 @@ fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
|
||||
browser_tool,
|
||||
"打开百度搜索天气",
|
||||
&workspace_root,
|
||||
&settings,
|
||||
)
|
||||
.unwrap();
|
||||
server_handle.join().unwrap();
|
||||
@@ -255,7 +278,151 @@ fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set() {
|
||||
fn handle_browser_message_prefers_compat_runtime_for_supported_instruction_when_deepseek_is_configured() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let first_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browser_action",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"action": "navigate",
|
||||
"expected_domain": "www.baidu.com",
|
||||
"url": "https://www.baidu.com"
|
||||
})).unwrap()
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "call_2",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browser_action",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"action": "type",
|
||||
"expected_domain": "www.baidu.com",
|
||||
"selector": "#kw",
|
||||
"text": "天气",
|
||||
"clear_first": true
|
||||
})).unwrap()
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "call_3",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browser_action",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"action": "click",
|
||||
"expected_domain": "www.baidu.com",
|
||||
"selector": "#su"
|
||||
})).unwrap()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let second_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "已通过 DeepSeek 执行任务: 打开百度搜索天气"
|
||||
}
|
||||
}]
|
||||
});
|
||||
let (base_url, requests, server_handle) =
|
||||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||||
|
||||
std::env::remove_var("DEEPSEEK_API_KEY");
|
||||
std::env::remove_var("DEEPSEEK_BASE_URL");
|
||||
std::env::remove_var("DEEPSEEK_MODEL");
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let config_path = write_deepseek_config(
|
||||
&workspace_root,
|
||||
"deepseek-test-key",
|
||||
&base_url,
|
||||
"deepseek-chat",
|
||||
);
|
||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||||
|
||||
let transport = Arc::new(MockTransport::new(vec![
|
||||
BrowserMessage::Response {
|
||||
seq: 1,
|
||||
success: true,
|
||||
data: json!({ "navigated": true }),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 10,
|
||||
},
|
||||
},
|
||||
BrowserMessage::Response {
|
||||
seq: 2,
|
||||
success: true,
|
||||
data: json!({ "typed": true }),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 10,
|
||||
},
|
||||
},
|
||||
BrowserMessage::Response {
|
||||
seq: 3,
|
||||
success: true,
|
||||
data: json!({ "clicked": true }),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 10,
|
||||
},
|
||||
},
|
||||
]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
test_policy(),
|
||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
handle_browser_message_with_context(
|
||||
transport.as_ref(),
|
||||
&browser_tool,
|
||||
&runtime_context,
|
||||
BrowserMessage::SubmitTask {
|
||||
instruction: "打开百度搜索天气".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
server_handle.join().unwrap();
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
let request_bodies = requests.lock().unwrap().clone();
|
||||
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::TaskComplete { success, summary }
|
||||
if *success && summary == "已通过 DeepSeek 执行任务: 打开百度搜索天气"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "compat_llm_primary"
|
||||
)
|
||||
}));
|
||||
assert_eq!(request_bodies.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_falls_back_to_compat_runtime_for_unsupported_instruction() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let first_response = json!({
|
||||
@@ -284,7 +451,8 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set()
|
||||
}
|
||||
}]
|
||||
});
|
||||
let (base_url, _, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]);
|
||||
let (base_url, requests, server_handle) =
|
||||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||||
|
||||
std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key");
|
||||
std::env::set_var("DEEPSEEK_BASE_URL", base_url);
|
||||
@@ -315,7 +483,7 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set()
|
||||
transport.as_ref(),
|
||||
&browser_tool,
|
||||
BrowserMessage::SubmitTask {
|
||||
instruction: "打开百度搜索天气".to_string(),
|
||||
instruction: "帮我打开百度首页".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -323,6 +491,7 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set()
|
||||
std::env::set_current_dir(original_dir).unwrap();
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
let request_bodies = requests.lock().unwrap().clone();
|
||||
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
@@ -331,4 +500,12 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set()
|
||||
if *success && summary == "来自 ZeroClaw runtime"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "compat_llm_primary"
|
||||
)
|
||||
}));
|
||||
assert_eq!(request_bodies.len(), 2);
|
||||
}
|
||||
|
||||
@@ -74,39 +74,44 @@ fn submit_task_sends_three_commands_and_finishes_with_task_complete() {
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
|
||||
assert_eq!(sent.len(), 7);
|
||||
assert_eq!(sent.len(), 8);
|
||||
assert!(matches!(
|
||||
&sent[0],
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "deterministic_planner"
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[1],
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "navigate https://www.baidu.com"
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[1],
|
||||
&sent[2],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 1 && action == &Action::Navigate
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[2],
|
||||
&sent[3],
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "type 天气 into #kw"
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[3],
|
||||
&sent[4],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 2 && action == &Action::Type
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[4],
|
||||
&sent[5],
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "click #su"
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[5],
|
||||
&sent[6],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 3 && action == &Action::Click
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[6],
|
||||
&sent[7],
|
||||
AgentMessage::TaskComplete { success, summary }
|
||||
if *success && summary == "已在百度搜索天气"
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user