feat: add DeepSeek Harness configuration support
This commit is contained in:
1 parent
a99f8c8035
commit
5c770c5605
12 files changed
+839
-13
No files matched your search
@@ -16,6 +16,7 @@ pub(crate) fn inspect_agent_config_statuses(
|
||||
AgentClient::OpenCode,
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|client| inspect_agent_config(client, &home, config.port, api_key))
|
||||
|
||||
@@ -189,6 +189,31 @@ pub(crate) fn build_agent_updates_with_oauth(
|
||||
after,
|
||||
}])
|
||||
}
|
||||
AgentClient::DeepSeekHarness => {
|
||||
if paths.len() != 2 {
|
||||
return Err("DeepSeek Harness 配置路径数量无效".to_string());
|
||||
}
|
||||
let settings_before = read_optional_text(&paths[0])?;
|
||||
let credentials_before = read_optional_text(&paths[1])?;
|
||||
Ok(vec![
|
||||
AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after: build_deepseek_harness_settings(
|
||||
settings_before.as_deref(),
|
||||
&openai_base,
|
||||
model,
|
||||
models,
|
||||
)?,
|
||||
},
|
||||
AgentFileUpdate {
|
||||
path: paths[1].clone(),
|
||||
after: build_deepseek_harness_credentials(
|
||||
credentials_before.as_deref(),
|
||||
api_key,
|
||||
)?,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +226,187 @@ pub(crate) fn read_optional_text(path: &Path) -> Result<Option<String>, String>
|
||||
.map_err(|error| format!("读取配置失败 {}: {error}", path_to_string(path)))
|
||||
}
|
||||
|
||||
pub(crate) fn read_agent_yaml_mapping_or_empty(
|
||||
path: &Path,
|
||||
label: &str,
|
||||
) -> Result<serde_norway::Mapping, String> {
|
||||
if !path.is_file() {
|
||||
return Ok(serde_norway::Mapping::new());
|
||||
}
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|error| format!("读取 {label} 失败 {}: {error}", path_to_string(path)))?;
|
||||
parse_agent_yaml_mapping(Some(&content), label)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_agent_yaml_mapping(
|
||||
content: Option<&str>,
|
||||
label: &str,
|
||||
) -> Result<serde_norway::Mapping, String> {
|
||||
let Some(content) = content.map(str::trim).filter(|content| !content.is_empty()) else {
|
||||
return Ok(serde_norway::Mapping::new());
|
||||
};
|
||||
serde_norway::from_str::<serde_norway::Value>(content)
|
||||
.map_err(|error| format!("{label} YAML 格式无效: {error}"))?
|
||||
.as_mapping()
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("{label} 根节点必须是 YAML 映射"))
|
||||
}
|
||||
|
||||
pub(crate) fn render_agent_yaml_mapping_update<F>(
|
||||
existing: Option<&str>,
|
||||
label: &str,
|
||||
update: F,
|
||||
) -> Result<String, String>
|
||||
where
|
||||
F: FnOnce(&mut serde_norway::Mapping) -> Result<(), String>,
|
||||
{
|
||||
let original_mapping = parse_agent_yaml_mapping(existing, label)?;
|
||||
let original = serde_norway::Value::Mapping(original_mapping.clone());
|
||||
let mut updated_mapping = original_mapping;
|
||||
update(&mut updated_mapping)?;
|
||||
let updated = serde_norway::Value::Mapping(updated_mapping);
|
||||
if updated == original {
|
||||
return Ok(existing.unwrap_or("{}\n").to_string());
|
||||
}
|
||||
let source = existing
|
||||
.map(str::trim)
|
||||
.filter(|content| !content.is_empty())
|
||||
.map(|_| existing.unwrap_or_default())
|
||||
.unwrap_or("{}\n");
|
||||
let mut document = yaml_serde_edit::YamlValue::parse(source)
|
||||
.map_err(|error| format!("{label} YAML 格式无效: {error}"))?;
|
||||
render_updated_core_yaml(&mut document, updated)
|
||||
.map_err(|error| format!("更新 {label} 失败: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn build_deepseek_harness_models(
|
||||
models: &[AgentModelOption],
|
||||
selected_model: &str,
|
||||
) -> Result<serde_norway::Value, String> {
|
||||
let entries = ordered_agent_models(models, selected_model)
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
let mut entry = serde_norway::Mapping::new();
|
||||
entry.insert(yaml_key("id"), serde_norway::Value::String(model.name));
|
||||
if let Some(name) = model.alias.filter(|name| !name.trim().is_empty()) {
|
||||
entry.insert(yaml_key("name"), serde_norway::Value::String(name));
|
||||
}
|
||||
if let Some(context_window) = model.context_window {
|
||||
entry.insert(
|
||||
yaml_key("contextWindow"),
|
||||
serde_norway::to_value(context_window)
|
||||
.map_err(|error| format!("序列化 Harness 模型上下文失败: {error}"))?,
|
||||
);
|
||||
}
|
||||
Ok(serde_norway::Value::Mapping(entry))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
Ok(serde_norway::Value::Sequence(entries))
|
||||
}
|
||||
|
||||
pub(crate) fn build_deepseek_harness_settings(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
model: &str,
|
||||
models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
let published_models = build_deepseek_harness_models(models, model)?;
|
||||
render_agent_yaml_mapping_update(existing, "DeepSeek Harness settings", |root| {
|
||||
let llm = root
|
||||
.entry(yaml_key("llm-pi-ai"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "DeepSeek Harness llm-pi-ai 必须是映射".to_string())?;
|
||||
let providers = llm
|
||||
.entry(yaml_key("providers"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "DeepSeek Harness llm-pi-ai.providers 必须是映射".to_string())?;
|
||||
let provider = providers
|
||||
.entry(yaml_key(DEEPSEEK_HARNESS_PROVIDER_ID))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "DeepSeek Harness EasyCLIProxyAPI provider 必须是映射".to_string())?;
|
||||
for (key, value) in [
|
||||
("displayName", "EasyCLIProxyAPI"),
|
||||
("apiKeyEnv", DEEPSEEK_HARNESS_CREDENTIAL),
|
||||
("api", "openai-completions"),
|
||||
("baseURL", base_url),
|
||||
] {
|
||||
provider.insert(
|
||||
yaml_key(key),
|
||||
serde_norway::Value::String(value.to_string()),
|
||||
);
|
||||
}
|
||||
provider.insert(yaml_key("models"), published_models);
|
||||
|
||||
let selection = root
|
||||
.entry(yaml_key("agent-default-model"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "DeepSeek Harness agent-default-model 必须是映射".to_string())?;
|
||||
selection.insert(
|
||||
yaml_key("provider"),
|
||||
serde_norway::Value::String(DEEPSEEK_HARNESS_PROVIDER_ID.to_string()),
|
||||
);
|
||||
selection.insert(
|
||||
yaml_key("model"),
|
||||
serde_norway::Value::String(model.to_string()),
|
||||
);
|
||||
selection.remove(yaml_key("reasoningEffort"));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_deepseek_harness_credentials(
|
||||
existing: Option<&str>,
|
||||
api_key: &str,
|
||||
) -> Result<String, String> {
|
||||
render_agent_yaml_mapping_update(existing, "DeepSeek Harness credentials", |root| {
|
||||
root.insert(
|
||||
yaml_key(DEEPSEEK_HARNESS_CREDENTIAL),
|
||||
serde_norway::Value::String(api_key.to_string()),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn write_deepseek_harness_file(
|
||||
path: &Path,
|
||||
content: &[u8],
|
||||
owner_only: bool,
|
||||
) -> Result<(), String> {
|
||||
write_bytes_directly(path, content)?;
|
||||
#[cfg(unix)]
|
||||
if owner_only {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|error| {
|
||||
format!(
|
||||
"设置 DeepSeek Harness 凭据权限失败 {}: {error}",
|
||||
path_to_string(path)
|
||||
)
|
||||
})?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = owner_only;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_agent_configuration_file(
|
||||
client: AgentClient,
|
||||
path: &Path,
|
||||
content: &[u8],
|
||||
) -> Result<(), String> {
|
||||
if client == AgentClient::DeepSeekHarness {
|
||||
let owner_only = path.file_name().and_then(|name| name.to_str())
|
||||
== Some(DEEPSEEK_HARNESS_CREDENTIALS_FILE);
|
||||
write_deepseek_harness_file(path, content, owner_only)
|
||||
} else {
|
||||
write_bytes_directly(path, content)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_claude_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
@@ -967,9 +1173,105 @@ pub(crate) fn remove_agent_managed_configuration(
|
||||
AgentClient::OpenCode => remove_opencode_managed_configuration(paths),
|
||||
AgentClient::OpenClaw => remove_openclaw_managed_configuration(paths),
|
||||
AgentClient::Hermes => remove_hermes_managed_configuration(paths),
|
||||
AgentClient::DeepSeekHarness => remove_deepseek_harness_managed_configuration(paths),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_deepseek_harness_settings_fields(root: &mut serde_norway::Mapping) -> bool {
|
||||
let mut changed = false;
|
||||
let mut remove_llm = false;
|
||||
if let Some(llm) = root
|
||||
.get_mut(yaml_key("llm-pi-ai"))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
{
|
||||
let mut remove_providers = false;
|
||||
if let Some(providers) = llm
|
||||
.get_mut(yaml_key("providers"))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
{
|
||||
changed |= providers
|
||||
.remove(yaml_key(DEEPSEEK_HARNESS_PROVIDER_ID))
|
||||
.is_some();
|
||||
remove_providers = providers.is_empty();
|
||||
}
|
||||
if remove_providers {
|
||||
llm.remove(yaml_key("providers"));
|
||||
}
|
||||
remove_llm = llm.is_empty();
|
||||
}
|
||||
if remove_llm {
|
||||
root.remove(yaml_key("llm-pi-ai"));
|
||||
}
|
||||
let managed_selection = root
|
||||
.get(yaml_key("agent-default-model"))
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|selection| yaml_mapping_value(selection, "provider"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some(DEEPSEEK_HARNESS_PROVIDER_ID);
|
||||
if managed_selection {
|
||||
root.remove(yaml_key("agent-default-model"));
|
||||
changed = true;
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
pub(crate) fn remove_deepseek_harness_managed_configuration(
|
||||
paths: &[PathBuf],
|
||||
) -> Result<Vec<String>, String> {
|
||||
if paths.len() != 2 {
|
||||
return Err("DeepSeek Harness 配置路径数量无效".to_string());
|
||||
}
|
||||
let mut changed_paths = Vec::new();
|
||||
if paths[0].is_file() {
|
||||
let current = fs::read_to_string(&paths[0])
|
||||
.map_err(|error| format!("读取 DeepSeek Harness settings 失败: {error}"))?;
|
||||
let mut changed = false;
|
||||
let rendered = render_agent_yaml_mapping_update(
|
||||
Some(¤t),
|
||||
"DeepSeek Harness settings",
|
||||
|root| {
|
||||
changed = remove_deepseek_harness_settings_fields(root);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
if changed {
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "DeepSeek Harness settings")?;
|
||||
if root.is_empty() {
|
||||
fs::remove_file(&paths[0])
|
||||
.map_err(|error| format!("删除空的 DeepSeek Harness settings 失败: {error}"))?;
|
||||
} else {
|
||||
write_bytes_directly(&paths[0], rendered.as_bytes())?;
|
||||
}
|
||||
changed_paths.push(path_to_string(&paths[0]));
|
||||
}
|
||||
}
|
||||
if paths[1].is_file() {
|
||||
let current = fs::read_to_string(&paths[1])
|
||||
.map_err(|error| format!("读取 DeepSeek Harness credentials 失败: {error}"))?;
|
||||
let mut changed = false;
|
||||
let rendered = render_agent_yaml_mapping_update(
|
||||
Some(¤t),
|
||||
"DeepSeek Harness credentials",
|
||||
|root| {
|
||||
changed = root.remove(yaml_key(DEEPSEEK_HARNESS_CREDENTIAL)).is_some();
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
if changed {
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "DeepSeek Harness credentials")?;
|
||||
if root.is_empty() {
|
||||
fs::remove_file(&paths[1]).map_err(|error| {
|
||||
format!("删除空的 DeepSeek Harness credentials 失败: {error}")
|
||||
})?;
|
||||
} else {
|
||||
write_deepseek_harness_file(&paths[1], rendered.as_bytes(), true)?;
|
||||
}
|
||||
changed_paths.push(path_to_string(&paths[1]));
|
||||
}
|
||||
}
|
||||
Ok(changed_paths)
|
||||
}
|
||||
|
||||
pub(crate) fn restore_json_key(
|
||||
current: &mut serde_json::Map<String, serde_json::Value>,
|
||||
original: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
@@ -1442,6 +1744,93 @@ pub(crate) fn build_restored_hermes_config(
|
||||
render_updated_core_yaml(&mut document, updated).map(Some)
|
||||
}
|
||||
|
||||
pub(crate) fn restore_deepseek_harness_provider(
|
||||
current: &mut serde_norway::Mapping,
|
||||
original: Option<&serde_norway::Mapping>,
|
||||
) -> Result<(), String> {
|
||||
let original_llm = original
|
||||
.and_then(|root| yaml_mapping_value(root, "llm-pi-ai"))
|
||||
.and_then(serde_norway::Value::as_mapping);
|
||||
let original_providers = original_llm
|
||||
.and_then(|llm| yaml_mapping_value(llm, "providers"))
|
||||
.and_then(serde_norway::Value::as_mapping);
|
||||
let original_provider = original_providers
|
||||
.and_then(|providers| yaml_mapping_value(providers, DEEPSEEK_HARNESS_PROVIDER_ID))
|
||||
.cloned();
|
||||
|
||||
if let Some(original_provider) = original_provider {
|
||||
let llm = current
|
||||
.entry(yaml_key("llm-pi-ai"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "当前 DeepSeek Harness llm-pi-ai 必须是映射".to_string())?;
|
||||
let providers = llm
|
||||
.entry(yaml_key("providers"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "当前 DeepSeek Harness llm-pi-ai.providers 必须是映射".to_string())?;
|
||||
providers.insert(yaml_key(DEEPSEEK_HARNESS_PROVIDER_ID), original_provider);
|
||||
} else {
|
||||
let mut remove_llm = false;
|
||||
if let Some(llm) = current
|
||||
.get_mut(yaml_key("llm-pi-ai"))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
{
|
||||
let mut remove_providers = false;
|
||||
if let Some(providers) = llm
|
||||
.get_mut(yaml_key("providers"))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
{
|
||||
providers.remove(yaml_key(DEEPSEEK_HARNESS_PROVIDER_ID));
|
||||
remove_providers = providers.is_empty() && original_providers.is_none();
|
||||
}
|
||||
if remove_providers {
|
||||
llm.remove(yaml_key("providers"));
|
||||
}
|
||||
remove_llm = llm.is_empty() && original_llm.is_none();
|
||||
}
|
||||
if remove_llm {
|
||||
current.remove(yaml_key("llm-pi-ai"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_deepseek_harness_settings(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let original_root = parse_agent_yaml_mapping(original, "原始 DeepSeek Harness settings")?;
|
||||
let rendered = render_agent_yaml_mapping_update(
|
||||
Some(current),
|
||||
"当前 DeepSeek Harness settings",
|
||||
|root| {
|
||||
restore_deepseek_harness_provider(root, Some(&original_root))?;
|
||||
restore_yaml_key(root, Some(&original_root), "agent-default-model");
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "恢复后的 DeepSeek Harness settings")?;
|
||||
Ok((!root.is_empty() || original.is_some()).then_some(rendered))
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_deepseek_harness_credentials(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let original_root = parse_agent_yaml_mapping(original, "原始 DeepSeek Harness credentials")?;
|
||||
let rendered = render_agent_yaml_mapping_update(
|
||||
Some(current),
|
||||
"当前 DeepSeek Harness credentials",
|
||||
|root| {
|
||||
restore_yaml_key(root, Some(&original_root), DEEPSEEK_HARNESS_CREDENTIAL);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "恢复后的 DeepSeek Harness credentials")?;
|
||||
Ok((!root.is_empty() || original.is_some()).then_some(rendered))
|
||||
}
|
||||
|
||||
pub(crate) fn agent_config_semantically_equal(
|
||||
client: AgentClient,
|
||||
actual: &str,
|
||||
@@ -1460,6 +1849,10 @@ pub(crate) fn agent_config_semantically_equal(
|
||||
serde_norway::from_str::<serde_norway::Value>(actual).ok()
|
||||
== serde_norway::from_str::<serde_norway::Value>(expected).ok()
|
||||
}
|
||||
AgentClient::DeepSeekHarness => {
|
||||
serde_norway::from_str::<serde_norway::Value>(actual).ok()
|
||||
== serde_norway::from_str::<serde_norway::Value>(expected).ok()
|
||||
}
|
||||
_ => {
|
||||
serde_json::from_str::<serde_json::Value>(actual).ok()
|
||||
== serde_json::from_str::<serde_json::Value>(expected).ok()
|
||||
@@ -1502,6 +1895,17 @@ pub(crate) fn build_agent_session_restored_bytes(
|
||||
AgentClient::OpenCode => build_restored_opencode_config(current, original)?,
|
||||
AgentClient::OpenClaw => build_restored_openclaw_config(current, original)?,
|
||||
AgentClient::Hermes => build_restored_hermes_config(current, original)?,
|
||||
AgentClient::DeepSeekHarness => {
|
||||
let index = paths
|
||||
.iter()
|
||||
.position(|candidate| candidate == path)
|
||||
.ok_or_else(|| "DeepSeek Harness 恢复路径不匹配".to_string())?;
|
||||
match index {
|
||||
0 => build_restored_deepseek_harness_settings(current, original)?,
|
||||
1 => build_restored_deepseek_harness_credentials(current, original)?,
|
||||
_ => return Err("DeepSeek Harness 恢复路径索引无效".to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
if let (Some(restored), Some(original), Some(original_bytes)) =
|
||||
(restored.as_deref(), original, original_bytes)
|
||||
|
||||
@@ -25,9 +25,44 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
|
||||
AgentClient::OpenCode => vec![home.join(".config/opencode/opencode.json")],
|
||||
AgentClient::OpenClaw => vec![home.join(".openclaw/openclaw.json")],
|
||||
AgentClient::Hermes => vec![hermes_agent_config_path(home)],
|
||||
AgentClient::DeepSeekHarness => {
|
||||
let directory = deepseek_harness_home(home);
|
||||
vec![
|
||||
directory.join(DEEPSEEK_HARNESS_SETTINGS_FILE),
|
||||
directory.join(DEEPSEEK_HARNESS_CREDENTIALS_FILE),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deepseek_harness_home(home: &Path) -> PathBuf {
|
||||
env::var_os("DSH_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| home.join(".dsh"))
|
||||
}
|
||||
|
||||
pub(crate) fn read_deepseek_harness_profile_version(home: &Path) -> Option<String> {
|
||||
let directory = deepseek_harness_home(home);
|
||||
[
|
||||
directory.join("profiles/node_modules/@deepseek-ai/dsh/package.json"),
|
||||
directory.join("node_modules/@deepseek-ai/dsh/package.json"),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|path| read_package_json_version(&path))
|
||||
}
|
||||
|
||||
pub(crate) fn read_package_json_version(path: &Path) -> Option<String> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
let package = serde_json::from_str::<serde_json::Value>(&content).ok()?;
|
||||
package
|
||||
.get("version")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|version| !version.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub(crate) fn pi_agent_directory(home: &Path) -> PathBuf {
|
||||
env::var_os("PI_CODING_AGENT_DIR")
|
||||
.map(PathBuf::from)
|
||||
@@ -765,6 +800,7 @@ pub(crate) fn inspect_agent_config(
|
||||
let app_version = match client {
|
||||
AgentClient::ClaudeDesktop => read_claude_desktop_version(home),
|
||||
AgentClient::Codex => read_codex_app_version(home),
|
||||
AgentClient::DeepSeekHarness => read_deepseek_harness_profile_version(home),
|
||||
_ => None,
|
||||
};
|
||||
let version = cli_version.clone().or_else(|| app_version.clone());
|
||||
@@ -902,6 +938,8 @@ pub(crate) fn inspect_agent_managed_config(
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::Hermes => inspect_hermes_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::DeepSeekHarness => inspect_deepseek_harness_config(paths, port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,6 +1058,7 @@ pub(crate) fn agent_has_managed_marker(
|
||||
== Some(MANAGED_AGENT_PROVIDER_ID);
|
||||
Ok(provider_exists && model_selected)
|
||||
}
|
||||
AgentClient::DeepSeekHarness => deepseek_harness_has_managed_marker(paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2277,3 +2316,91 @@ pub(crate) fn inspect_hermes_agent_config(
|
||||
.map(str::to_string);
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_deepseek_harness_config(
|
||||
paths: &[PathBuf],
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
) -> Result<(bool, Option<String>), String> {
|
||||
if paths.len() != 2 {
|
||||
return Err("DeepSeek Harness 配置路径数量无效".to_string());
|
||||
}
|
||||
let settings = read_agent_yaml_mapping_or_empty(&paths[0], "DeepSeek Harness settings")?;
|
||||
let credentials = read_agent_yaml_mapping_or_empty(&paths[1], "DeepSeek Harness credentials")?;
|
||||
let provider = yaml_mapping_value(&settings, "llm-pi-ai")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|section| yaml_mapping_value(section, "providers"))
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|providers| yaml_mapping_value(providers, DEEPSEEK_HARNESS_PROVIDER_ID))
|
||||
.and_then(serde_norway::Value::as_mapping);
|
||||
let selection = yaml_mapping_value(&settings, "agent-default-model")
|
||||
.and_then(serde_norway::Value::as_mapping);
|
||||
let selected_provider = selection
|
||||
.and_then(|selection| yaml_mapping_value(selection, "provider"))
|
||||
.and_then(serde_norway::Value::as_str);
|
||||
let model = (selected_provider == Some(DEEPSEEK_HARNESS_PROVIDER_ID))
|
||||
.then(|| {
|
||||
selection
|
||||
.and_then(|selection| yaml_mapping_value(selection, "model"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|model| !model.is_empty())
|
||||
.map(str::to_string)
|
||||
})
|
||||
.flatten();
|
||||
let expected_base = format!("http://127.0.0.1:{port}/v1");
|
||||
let credential = yaml_mapping_value(&credentials, DEEPSEEK_HARNESS_CREDENTIAL)
|
||||
.and_then(serde_norway::Value::as_str);
|
||||
let model_is_published = model.as_deref().is_some_and(|selected| {
|
||||
provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "models"))
|
||||
.and_then(serde_norway::Value::as_sequence)
|
||||
.is_some_and(|models| {
|
||||
models.iter().any(|model| {
|
||||
model
|
||||
.as_mapping()
|
||||
.and_then(|model| yaml_mapping_value(model, "id"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
.is_some_and(|id| id.eq_ignore_ascii_case(selected))
|
||||
})
|
||||
})
|
||||
});
|
||||
let configured = provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "apiKeyEnv"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some(DEEPSEEK_HARNESS_CREDENTIAL)
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "api"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some("openai-completions")
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "baseURL"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some(expected_base.as_str())
|
||||
&& selected_provider == Some(DEEPSEEK_HARNESS_PROVIDER_ID)
|
||||
&& model_is_published
|
||||
&& credential == Some(api_key);
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn deepseek_harness_has_managed_marker(paths: &[PathBuf]) -> Result<bool, String> {
|
||||
if paths.len() != 2 {
|
||||
return Err("DeepSeek Harness 配置路径数量无效".to_string());
|
||||
}
|
||||
let settings = read_agent_yaml_mapping_or_empty(&paths[0], "DeepSeek Harness settings")?;
|
||||
let credentials = read_agent_yaml_mapping_or_empty(&paths[1], "DeepSeek Harness credentials")?;
|
||||
let provider_exists = yaml_mapping_value(&settings, "llm-pi-ai")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|section| yaml_mapping_value(section, "providers"))
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.is_some_and(|providers| {
|
||||
yaml_mapping_value(providers, DEEPSEEK_HARNESS_PROVIDER_ID).is_some()
|
||||
});
|
||||
let default_selected = yaml_mapping_value(&settings, "agent-default-model")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|selection| yaml_mapping_value(selection, "provider"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some(DEEPSEEK_HARNESS_PROVIDER_ID);
|
||||
let credential_exists = yaml_mapping_value(&credentials, DEEPSEEK_HARNESS_CREDENTIAL).is_some();
|
||||
Ok(provider_exists || default_selected || credential_exists)
|
||||
}
|
||||
@@ -724,6 +724,10 @@ pub(crate) fn fresh_agent_contents_with_oauth(
|
||||
model,
|
||||
models,
|
||||
)?]),
|
||||
AgentClient::DeepSeekHarness => Ok(vec![
|
||||
build_deepseek_harness_settings(None, &openai_base, model, models)?,
|
||||
build_deepseek_harness_credentials(None, api_key)?,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,6 +746,10 @@ pub(crate) fn agent_contents_equal(client: AgentClient, actual: &str, expected:
|
||||
serde_yaml::from_str::<serde_yaml::Value>(actual).ok()
|
||||
== serde_yaml::from_str::<serde_yaml::Value>(expected).ok()
|
||||
}
|
||||
AgentClient::DeepSeekHarness => {
|
||||
serde_norway::from_str::<serde_norway::Value>(actual).ok()
|
||||
== serde_norway::from_str::<serde_norway::Value>(expected).ok()
|
||||
}
|
||||
_ => {
|
||||
serde_json::from_str::<serde_json::Value>(actual).ok()
|
||||
== serde_json::from_str::<serde_json::Value>(expected).ok()
|
||||
@@ -1187,15 +1195,23 @@ pub(crate) fn dated_agent_backup_path(path: &Path) -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_agent_session_backups(
|
||||
client: AgentClient,
|
||||
updates: &[AgentFileUpdate],
|
||||
) -> Result<Vec<AgentAppliedBackupFile>, String> {
|
||||
let mut backups = Vec::with_capacity(updates.len());
|
||||
let mut backups: Vec<AgentAppliedBackupFile> = Vec::with_capacity(updates.len());
|
||||
let mut renamed = Vec::new();
|
||||
for update in updates {
|
||||
let existed_before = update.path.is_file();
|
||||
let backup_path = dated_agent_backup_path(&update.path)?;
|
||||
if existed_before {
|
||||
if backup_path.exists() {
|
||||
if client == AgentClient::DeepSeekHarness {
|
||||
for backup in &backups {
|
||||
if backup.existed_before {
|
||||
let _ = fs::remove_file(&backup.backup_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (path, backup) in renamed.iter().rev() {
|
||||
let _ = fs::rename(backup, path);
|
||||
}
|
||||
@@ -1204,13 +1220,27 @@ pub(crate) fn prepare_agent_session_backups(
|
||||
path_to_string(&backup_path)
|
||||
));
|
||||
}
|
||||
fs::rename(&update.path, &backup_path).map_err(|error| {
|
||||
format!(
|
||||
"重命名原智能体配置为备份失败 {}: {error}",
|
||||
path_to_string(&update.path)
|
||||
)
|
||||
})?;
|
||||
renamed.push((update.path.clone(), backup_path.clone()));
|
||||
if client == AgentClient::DeepSeekHarness {
|
||||
if let Err(error) = fs::copy(&update.path, &backup_path) {
|
||||
for backup in &backups {
|
||||
if backup.existed_before {
|
||||
let _ = fs::remove_file(&backup.backup_path);
|
||||
}
|
||||
}
|
||||
return Err(format!(
|
||||
"备份 DeepSeek Harness 配置失败 {}: {error}",
|
||||
path_to_string(&update.path)
|
||||
));
|
||||
}
|
||||
} else {
|
||||
fs::rename(&update.path, &backup_path).map_err(|error| {
|
||||
format!(
|
||||
"重命名原智能体配置为备份失败 {}: {error}",
|
||||
path_to_string(&update.path)
|
||||
)
|
||||
})?;
|
||||
renamed.push((update.path.clone(), backup_path.clone()));
|
||||
}
|
||||
}
|
||||
backups.push(AgentAppliedBackupFile {
|
||||
path: update.path.clone(),
|
||||
@@ -1284,7 +1314,7 @@ pub(crate) fn restore_agent_applied_state_configuration(
|
||||
let restore_result = (|| -> Result<(), String> {
|
||||
for (file, replacement) in state.backup_files.iter().zip(&replacements).rev() {
|
||||
if let Some(replacement) = replacement {
|
||||
write_bytes_directly(&file.path, replacement)?;
|
||||
write_agent_configuration_file(client, &file.path, replacement)?;
|
||||
} else if file.path.exists() {
|
||||
fs::remove_file(&file.path).map_err(|error| {
|
||||
format!(
|
||||
@@ -1327,12 +1357,13 @@ pub(crate) fn restore_agent_applied_state_configuration(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn agent_clients_restored_on_exit() -> [AgentClient; 4] {
|
||||
pub(crate) fn agent_clients_restored_on_exit() -> [AgentClient; 5] {
|
||||
[
|
||||
AgentClient::Codex,
|
||||
AgentClient::OpenCode,
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1382,7 +1413,7 @@ pub(crate) fn commit_agent_configuration(
|
||||
if let Some(state) = existing_state.filter(|state| !state.backup_files.is_empty()) {
|
||||
state.backup_files
|
||||
} else {
|
||||
prepare_agent_session_backups(updates)?
|
||||
prepare_agent_session_backups(client, updates)?
|
||||
};
|
||||
|
||||
let transaction = (|| -> Result<Vec<String>, String> {
|
||||
@@ -1392,7 +1423,7 @@ pub(crate) fn commit_agent_configuration(
|
||||
if read_agent_bytes(&update.path)?.as_deref() == Some(next) {
|
||||
continue;
|
||||
}
|
||||
write_bytes_directly(&update.path, next)?;
|
||||
write_agent_configuration_file(client, &update.path, next)?;
|
||||
changed.push(path_to_string(&update.path));
|
||||
}
|
||||
let state = AgentAppliedState {
|
||||
@@ -1546,6 +1577,30 @@ pub(crate) fn reset_agent_configuration_to_default_with_oauth(
|
||||
claude_code_model_mappings,
|
||||
claude_desktop_model_mappings,
|
||||
} = request;
|
||||
if client == AgentClient::DeepSeekHarness {
|
||||
let updates = build_agent_updates_with_oauth(
|
||||
client,
|
||||
home,
|
||||
port,
|
||||
api_key,
|
||||
model,
|
||||
AgentConfigurationOptions {
|
||||
models,
|
||||
codex_catalog,
|
||||
oauth_configuration,
|
||||
claude_code_model_mappings,
|
||||
claude_desktop_model_mappings,
|
||||
},
|
||||
)?;
|
||||
return commit_agent_configuration(
|
||||
client,
|
||||
home,
|
||||
model,
|
||||
&updates,
|
||||
"default",
|
||||
claude_desktop_model_mappings,
|
||||
);
|
||||
}
|
||||
let paths = agent_config_paths(client, home);
|
||||
let contents = fresh_agent_contents_with_oauth(
|
||||
client,
|
||||
|
||||
@@ -130,6 +130,10 @@ const DEFAULT_MAX_RETRY_INTERVAL: u32 = 30;
|
||||
const DEFAULT_STREAMING_BOOTSTRAP_RETRIES: u32 = 0;
|
||||
const LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456";
|
||||
const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui";
|
||||
const DEEPSEEK_HARNESS_PROVIDER_ID: &str = "easy-cliproxyapi";
|
||||
const DEEPSEEK_HARNESS_CREDENTIAL: &str = "EASYCLIPROXYAPI_API_KEY";
|
||||
const DEEPSEEK_HARNESS_SETTINGS_FILE: &str = "settings.yaml";
|
||||
const DEEPSEEK_HARNESS_CREDENTIALS_FILE: &str = ".credentials.yaml";
|
||||
const PI_AGENT_ID: &str = "pi";
|
||||
const PI_AGENT_NAME: &str = "Pi";
|
||||
const PI_CLIPROXYAPI_PACKAGE: &str = "npm:@router-for-me/pi-cliproxyapi-provider";
|
||||
@@ -967,6 +971,7 @@ enum AgentClient {
|
||||
OpenCode,
|
||||
OpenClaw,
|
||||
Hermes,
|
||||
DeepSeekHarness,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -986,6 +991,7 @@ impl AgentClient {
|
||||
"opencode" => Ok(Self::OpenCode),
|
||||
"openclaw" => Ok(Self::OpenClaw),
|
||||
"hermes" => Ok(Self::Hermes),
|
||||
"deepseek-harness" => Ok(Self::DeepSeekHarness),
|
||||
_ => Err(format!("不支持的智能体客户端: {value}")),
|
||||
}
|
||||
}
|
||||
@@ -998,6 +1004,7 @@ impl AgentClient {
|
||||
Self::OpenCode => "opencode",
|
||||
Self::OpenClaw => "openclaw",
|
||||
Self::Hermes => "hermes",
|
||||
Self::DeepSeekHarness => "deepseek-harness",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,6 +1016,7 @@ impl AgentClient {
|
||||
Self::OpenCode => "OpenCode",
|
||||
Self::OpenClaw => "OpenClaw",
|
||||
Self::Hermes => "Hermes Agent",
|
||||
Self::DeepSeekHarness => "DeepSeek Harness",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1037,7 @@ impl AgentClient {
|
||||
Self::OpenCode => &["opencode"],
|
||||
Self::OpenClaw => &["openclaw"],
|
||||
Self::Hermes => &["hermes"],
|
||||
Self::DeepSeekHarness => &["dsh"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,6 +781,145 @@ fn exit_preserves_claude_client_configurations() {
|
||||
assert!(!restored.contains(&AgentClient::ClaudeDesktop));
|
||||
assert!(restored.contains(&AgentClient::Codex));
|
||||
assert!(restored.contains(&AgentClient::OpenCode));
|
||||
assert!(restored.contains(&AgentClient::DeepSeekHarness));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_harness_settings_preserve_unrelated_sections_and_publish_models() {
|
||||
let existing = r#"# keep this comment
|
||||
ui-onboarding:
|
||||
welcomeNoticeVersion: 3
|
||||
llm-pi-ai:
|
||||
providers:
|
||||
existing:
|
||||
api: openai-completions
|
||||
baseURL: https://example.invalid/v1
|
||||
agent-default-model:
|
||||
provider: existing
|
||||
model: old-model
|
||||
reasoningEffort: high
|
||||
"#;
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
name: "gpt-selected".to_string(),
|
||||
alias: Some("Selected Model".to_string()),
|
||||
is_alias: false,
|
||||
context_window: Some(272_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
name: "gpt-other".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: None,
|
||||
},
|
||||
];
|
||||
let rendered = build_deepseek_harness_settings(
|
||||
Some(existing),
|
||||
"http://127.0.0.1:8317/v1",
|
||||
"gpt-selected",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
|
||||
|
||||
assert!(rendered.contains("# keep this comment"));
|
||||
assert_eq!(
|
||||
value["ui-onboarding"]["welcomeNoticeVersion"].as_u64(),
|
||||
Some(3)
|
||||
);
|
||||
assert_eq!(
|
||||
value["llm-pi-ai"]["providers"]["existing"]["baseURL"].as_str(),
|
||||
Some("https://example.invalid/v1")
|
||||
);
|
||||
let provider = &value["llm-pi-ai"]["providers"][DEEPSEEK_HARNESS_PROVIDER_ID];
|
||||
assert_eq!(
|
||||
provider["apiKeyEnv"].as_str(),
|
||||
Some(DEEPSEEK_HARNESS_CREDENTIAL)
|
||||
);
|
||||
assert_eq!(provider["api"].as_str(), Some("openai-completions"));
|
||||
assert_eq!(
|
||||
provider["baseURL"].as_str(),
|
||||
Some("http://127.0.0.1:8317/v1")
|
||||
);
|
||||
assert_eq!(provider["models"][0]["id"].as_str(), Some("gpt-selected"));
|
||||
assert_eq!(
|
||||
provider["models"][0]["name"].as_str(),
|
||||
Some("Selected Model")
|
||||
);
|
||||
assert_eq!(
|
||||
provider["models"][0]["contextWindow"].as_u64(),
|
||||
Some(272_000)
|
||||
);
|
||||
assert_eq!(provider["models"][1]["id"].as_str(), Some("gpt-other"));
|
||||
assert_eq!(
|
||||
value["agent-default-model"]["provider"].as_str(),
|
||||
Some(DEEPSEEK_HARNESS_PROVIDER_ID)
|
||||
);
|
||||
assert_eq!(
|
||||
value["agent-default-model"]["model"].as_str(),
|
||||
Some("gpt-selected")
|
||||
);
|
||||
assert!(value["agent-default-model"]
|
||||
.get("reasoningEffort")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_harness_credentials_preserve_other_entries() {
|
||||
let rendered = build_deepseek_harness_credentials(
|
||||
Some("# existing key\nOTHER_API_KEY: other-secret\n"),
|
||||
"cpa-secret",
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
|
||||
|
||||
assert!(rendered.contains("# existing key"));
|
||||
assert_eq!(value["OTHER_API_KEY"].as_str(), Some("other-secret"));
|
||||
assert_eq!(
|
||||
value[DEEPSEEK_HARNESS_CREDENTIAL].as_str(),
|
||||
Some("cpa-secret")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_harness_inspection_requires_managed_route_selection_and_credential() {
|
||||
let home = agent_test_home("deepseek-harness-inspection");
|
||||
let paths = vec![home.join("settings.yaml"), home.join(".credentials.yaml")];
|
||||
let models = test_agent_models(&["gpt-test"]);
|
||||
fs::write(
|
||||
&paths[0],
|
||||
build_deepseek_harness_settings(None, "http://127.0.0.1:8317/v1", "gpt-test", &models)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
&paths[1],
|
||||
build_deepseek_harness_credentials(None, "agent-key").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (configured, model) = inspect_deepseek_harness_config(&paths, 8317, "agent-key").unwrap();
|
||||
assert!(configured);
|
||||
assert_eq!(model.as_deref(), Some("gpt-test"));
|
||||
assert!(deepseek_harness_has_managed_marker(&paths).unwrap());
|
||||
|
||||
let (wrong_key, _) = inspect_deepseek_harness_config(&paths, 8317, "different-key").unwrap();
|
||||
assert!(!wrong_key);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_harness_profile_version_comes_from_installed_profile() {
|
||||
let home = agent_test_home("deepseek-harness-version");
|
||||
let package = home.join(".dsh/profiles/node_modules/@deepseek-ai/dsh/package.json");
|
||||
fs::create_dir_all(package.parent().unwrap()).unwrap();
|
||||
fs::write(&package, r#"{"version":"0.1.0-rc.5"}"#).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
read_deepseek_harness_profile_version(&home).as_deref(),
|
||||
Some("0.1.0-rc.5")
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -204,6 +204,85 @@ fn agent_configuration_is_restored_from_the_dated_session_backup_on_exit() {
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_harness_session_restore_keeps_concurrent_unmanaged_yaml_changes() {
|
||||
let home = agent_test_home("deepseek-harness-session-restore");
|
||||
let paths = agent_config_paths(AgentClient::DeepSeekHarness, &home);
|
||||
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
|
||||
let original_settings = r#"# original settings
|
||||
llm-pi-ai:
|
||||
providers:
|
||||
existing:
|
||||
api: openai-completions
|
||||
baseURL: https://example.invalid/v1
|
||||
agent-default-model:
|
||||
provider: existing
|
||||
model: old-model
|
||||
"#;
|
||||
let original_credentials = "OTHER_API_KEY: old-secret\n";
|
||||
fs::write(&paths[0], original_settings).unwrap();
|
||||
fs::write(&paths[1], original_credentials).unwrap();
|
||||
let models = test_agent_models(&["gpt-test", "gpt-other"]);
|
||||
|
||||
apply_agent_configuration(
|
||||
AgentClient::DeepSeekHarness,
|
||||
&home,
|
||||
8317,
|
||||
"agent-key",
|
||||
"gpt-test",
|
||||
&models,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (configured, model) = inspect_deepseek_harness_config(&paths, 8317, "agent-key").unwrap();
|
||||
assert!(configured);
|
||||
assert_eq!(model.as_deref(), Some("gpt-test"));
|
||||
|
||||
let settings = fs::read_to_string(&paths[0]).unwrap();
|
||||
let settings =
|
||||
render_agent_yaml_mapping_update(Some(&settings), "test Harness settings", |root| {
|
||||
root.insert(
|
||||
yaml_key("added-after-apply"),
|
||||
serde_norway::Value::Bool(true),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
fs::write(&paths[0], settings).unwrap();
|
||||
let credentials = fs::read_to_string(&paths[1]).unwrap();
|
||||
let credentials =
|
||||
render_agent_yaml_mapping_update(Some(&credentials), "test Harness credentials", |root| {
|
||||
root.insert(
|
||||
yaml_key("NEW_API_KEY"),
|
||||
serde_norway::Value::String("new-secret".to_string()),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
fs::write(&paths[1], credentials).unwrap();
|
||||
|
||||
restore_agent_session_configuration(AgentClient::DeepSeekHarness, &home).unwrap();
|
||||
let settings: serde_norway::Value =
|
||||
serde_norway::from_str(&fs::read_to_string(&paths[0]).unwrap()).unwrap();
|
||||
let credentials: serde_norway::Value =
|
||||
serde_norway::from_str(&fs::read_to_string(&paths[1]).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
settings["agent-default-model"]["provider"].as_str(),
|
||||
Some("existing")
|
||||
);
|
||||
assert!(settings["llm-pi-ai"]["providers"]
|
||||
.get(DEEPSEEK_HARNESS_PROVIDER_ID)
|
||||
.is_none());
|
||||
assert_eq!(settings["added-after-apply"].as_bool(), Some(true));
|
||||
assert_eq!(credentials["OTHER_API_KEY"].as_str(), Some("old-secret"));
|
||||
assert_eq!(credentials["NEW_API_KEY"].as_str(), Some("new-secret"));
|
||||
assert!(credentials.get(DEEPSEEK_HARNESS_CREDENTIAL).is_none());
|
||||
assert!(!dated_agent_backup_path(&paths[0]).unwrap().exists());
|
||||
assert!(!dated_agent_backup_path(&paths[1]).unwrap().exists());
|
||||
assert!(!agent_state_path(&paths).unwrap().exists());
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_runtime_edits_survive_close_and_next_apply_owns_conflicts() {
|
||||
let home = agent_test_home("opencode-runtime-edit-merge");
|
||||
|
||||
@@ -465,6 +465,7 @@ export const jaOverrides = {
|
||||
'agents.description.opencode': 'CPA OpenAI Compatible プロバイダーを使用',
|
||||
'agents.description.openclaw': 'CPA モデルプロバイダーと既定モデルを使用',
|
||||
'agents.description.hermes': 'CPA カスタムプロバイダーを使用',
|
||||
'agents.description.deepseekHarness': 'Harness のネイティブ DeepSeek 設定と分離された OpenAI 互換ルートで CPA を使用',
|
||||
'agents.description.pi': 'CLIProxyAPI プロバイダーを使用して Pi CLI のモデルを動的に読み込みます',
|
||||
'agents.list.detecting': '検出中',
|
||||
'agents.list.unsupported': '現在のプラットフォームでは非対応',
|
||||
|
||||
@@ -465,6 +465,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.description.opencode': 'Uses the CPA OpenAI-compatible provider',
|
||||
'agents.description.openclaw': 'Uses the CPA model provider and default model',
|
||||
'agents.description.hermes': 'Uses the CPA custom provider',
|
||||
'agents.description.deepseekHarness': 'Uses CPA through an isolated, OpenAI-compatible Harness route',
|
||||
'agents.description.pi': 'Loads models dynamically in Pi through the CLIProxyAPI provider',
|
||||
'agents.list.detecting': 'Detecting',
|
||||
'agents.list.unsupported': 'Unsupported on this platform',
|
||||
|
||||
@@ -463,6 +463,7 @@ export const zhCN = {
|
||||
'agents.description.opencode': '使用 CPA OpenAI Compatible provider',
|
||||
'agents.description.openclaw': '使用 CPA 模型供应商和默认模型',
|
||||
'agents.description.hermes': '使用 CPA custom provider',
|
||||
'agents.description.deepseekHarness': '通过通用 OpenAI 兼容路由使用 CPA,并与 Harness 原生 DeepSeek 配置隔离',
|
||||
'agents.description.pi': '使用 Pi CLI 和 CLIProxyAPI provider 动态加载模型',
|
||||
'agents.list.detecting': '正在检测',
|
||||
'agents.list.unsupported': '当前平台不支持',
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import claudeIcon from '../assets/icons/claude.svg';
|
||||
import codexIcon from '../assets/icons/codex.svg';
|
||||
import deepseekIcon from '../assets/icons/deepseek.svg';
|
||||
import hermesIcon from '../assets/icons/hermes.png';
|
||||
import openclawIcon from '../assets/icons/openclaw.svg';
|
||||
import opencodeIcon from '../assets/icons/opencode.svg';
|
||||
@@ -54,6 +55,7 @@ type AgentClientId =
|
||||
| 'opencode'
|
||||
| 'openclaw'
|
||||
| 'hermes'
|
||||
| 'deepseek-harness'
|
||||
| 'pi';
|
||||
|
||||
type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop';
|
||||
@@ -168,7 +170,7 @@ type AgentDefinition = {
|
||||
name: string;
|
||||
icon?: string;
|
||||
Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.pi';
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.pi';
|
||||
};
|
||||
|
||||
type AgentSubpageId = 'core' | 'sessions';
|
||||
@@ -216,6 +218,12 @@ const agentDefinitions: AgentDefinition[] = [
|
||||
icon: hermesIcon,
|
||||
descriptionKey: 'agents.description.hermes',
|
||||
},
|
||||
{
|
||||
id: 'deepseek-harness',
|
||||
name: 'DeepSeek Harness',
|
||||
icon: deepseekIcon,
|
||||
descriptionKey: 'agents.description.deepseekHarness',
|
||||
},
|
||||
{
|
||||
id: 'pi',
|
||||
name: 'Pi',
|
||||
|
||||
@@ -5,6 +5,7 @@ export type AgentConfigurationClientId =
|
||||
| 'opencode'
|
||||
| 'openclaw'
|
||||
| 'hermes'
|
||||
| 'deepseek-harness'
|
||||
| 'pi';
|
||||
|
||||
export type AgentConfigurationModificationState = 'unconfigured' | 'applied' | 'invalid';
|
||||
|
||||
Reference in new issue
Block a user