Merge pull request #135 from router-for-me/dev

feat: add DeepSeek Harness configuration support
This commit is contained in:
lzt404 authored and GitHub committed 2026-08-15 00:59:49 +08:00
commit c3a0934fcd
12 files changed
+839 -13

No files matched your search

+1
View File
@@ -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))
+404
View File
@@ -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(&current),
"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(&current),
"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)
+127
View File
@@ -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)
}
+67 -12
View File
@@ -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,
+9
View File
@@ -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"],
}
}
}
+139
View File
@@ -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]
+79
View File
@@ -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");
+1
View File
@@ -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': '現在のプラットフォームでは非対応',
+1
View File
@@ -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',
+1
View File
@@ -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': '当前平台不支持',
+9 -1
View File
@@ -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',
+1
View File
@@ -5,6 +5,7 @@ export type AgentConfigurationClientId =
| 'opencode'
| 'openclaw'
| 'hermes'
| 'deepseek-harness'
| 'pi';
export type AgentConfigurationModificationState = 'unconfigured' | 'applied' | 'invalid';