Add README, plugin citations, and multi-platform release CI.

Ship screenshots and third-party notices for the desktop shell, and
package Windows, macOS, deb, rpm, and Flatpak from GitHub Releases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tommy
2026-08-15 22:02:13 +08:00
commit f8a3c2dc6f
72 changed files with 10372 additions and 0 deletions

View File

@@ -0,0 +1,379 @@
use std::path::{Path, PathBuf};
use regex::Regex;
use crate::paths::{copy_tree, dsh_home, BundledPaths};
pub const PRESET_ID: &str = "anchored-standard";
pub const ZERO_PRESET_ID: &str = "zero-anchored-standard";
pub const SOURCE_MARKER: &str = ".dsh-desktop-source";
pub const DEFAULT_BLOCK: &str = "agent-presets:\n default: anchored-standard\n";
pub const PRESET_NAME_ZH: &str = "锚定式标准(实验)";
pub const PRESET_DESCRIPTION_ZH: &str =
"首轮使用 Minimal 的真实工具对(持久 bash + str_replace_editor不自动注入工作区或技能上下文首次工具调用或回复后开放完整 Standard 工具。";
pub const ZERO_PRESET_NAME_ZH: &str = "零工具锚定式标准(实验)";
pub const ZERO_PRESET_DESCRIPTION_ZH: &str =
"先插入一轮无工具的锚定对话(固定提示),从下一轮起开放完整 Standard 工具。";
#[derive(Debug, Clone)]
pub struct BundledPreset {
pub preset_id: &'static str,
pub name_zh: &'static str,
pub description_zh: &'static str,
}
pub const BUNDLED_PRESETS: &[BundledPreset] = &[
BundledPreset {
preset_id: PRESET_ID,
name_zh: PRESET_NAME_ZH,
description_zh: PRESET_DESCRIPTION_ZH,
},
BundledPreset {
preset_id: ZERO_PRESET_ID,
name_zh: ZERO_PRESET_NAME_ZH,
description_zh: ZERO_PRESET_DESCRIPTION_ZH,
},
];
#[derive(Debug, Clone)]
pub struct PresetEnsureResult {
pub status: &'static str,
pub version: Option<String>,
pub message: String,
}
pub fn preset_install_dir(preset_id: &str) -> PathBuf {
dsh_home().join(".agent-presets").join(preset_id)
}
fn is_preset_dir(path: &Path) -> bool {
path.is_dir() && path.join("preset.yml").is_file()
}
pub fn read_source_version(path: &Path) -> Option<String> {
let version = std::fs::read_to_string(path.join(SOURCE_MARKER)).ok()?;
let version = version.trim();
if version.is_empty() {
None
} else {
Some(version.to_string())
}
}
pub fn bundled_preset_dir(paths: &BundledPaths, preset_id: &str) -> Option<PathBuf> {
paths
.find_dir(preset_id, "preset.yml")
.or_else(|| paths.find_dir(&format!("vendor/{preset_id}"), "preset.yml"))
.filter(|p| is_preset_dir(p))
}
pub fn ensure_default_preset(text: &str) -> String {
let mut body = text.replace("\r\n", "\n");
if !body.ends_with('\n') {
body.push('\n');
}
if body.trim().is_empty() {
return DEFAULT_BLOCK.to_string();
}
let re = Regex::new(r"(?m)^agent-presets:\n((?:[ \t]+.*\n)*)").unwrap();
let Some(caps) = re.captures(&body) else {
body.push('\n');
body.push_str(DEFAULT_BLOCK);
return body;
};
let inner = caps.get(1).unwrap().as_str();
if Regex::new(r"(?m)^[ \t]+default:\s*\S+")
.unwrap()
.is_match(inner)
{
return body;
}
let insert = format!(" default: {PRESET_ID}\n");
let start = caps.get(1).unwrap().start();
let end = caps.get(1).unwrap().end();
format!("{}{}{}{}", &body[..start], insert, inner, &body[end..])
}
pub fn localize_preset_yml(text: &str, name: &str, description: &str) -> String {
let mut body = text.replace("\r\n", "\n");
if !body.ends_with('\n') {
body.push('\n');
}
let name_line = format!("name: {}\n", serde_json::to_string(name).unwrap());
let desc_line = format!(
"description: {}\n",
serde_json::to_string(description).unwrap()
);
let name_re = Regex::new(r"(?m)^name:.*\n").unwrap();
if let Some(m) = name_re.find(&body) {
body = format!("{}{}{}", &body[..m.start()], name_line, &body[m.end()..]);
} else {
body = format!("{name_line}{body}");
}
let desc_re = Regex::new(r"(?m)^description:(?:[ \t].*)?\n(?:[ \t].+\n)*").unwrap();
if let Some(m) = desc_re.find(&body) {
return format!("{}{}{}", &body[..m.start()], desc_line, &body[m.end()..]);
}
if let Some(m) = name_re.find(&body) {
return format!("{}{}{}", &body[..m.end()], desc_line, &body[m.end()..]);
}
format!("{desc_line}{body}")
}
fn write_localized_preset(dest: &Path, spec: &BundledPreset) -> std::io::Result<()> {
let path = dest.join("preset.yml");
let previous = std::fs::read_to_string(&path)?;
let updated = localize_preset_yml(&previous, spec.name_zh, spec.description_zh);
if updated != previous {
std::fs::write(path, updated)?;
}
Ok(())
}
fn short_version(version: Option<&str>) -> String {
match version {
None => "bundled".into(),
Some(v) if v.len() > 12 => v[..12].into(),
Some(v) => v.into(),
}
}
fn ensure_one(paths: &BundledPaths, spec: &BundledPreset) -> PresetEnsureResult {
let src = bundled_preset_dir(paths, spec.preset_id);
let dest = preset_install_dir(spec.preset_id);
let bundled = src.as_ref().and_then(|p| read_source_version(p));
let installed = if dest.is_dir() {
read_source_version(&dest)
} else {
None
};
let version = bundled.clone().or_else(|| installed.clone());
let label = spec.name_zh;
if src.is_none() {
if dest.is_dir() && dest.join("preset.yml").is_file() {
let _ = write_localized_preset(&dest, spec);
return PresetEnsureResult {
status: "current",
version: version.clone(),
message: format!("已配置已安装的{label}{}", short_version(version.as_deref())),
};
}
return PresetEnsureResult {
status: "skipped",
version: None,
message: format!("未找到内置{label},跳过安装"),
};
}
let src = src.unwrap();
if let Err(exc) = (|| -> std::io::Result<()> {
if installed != bundled || !dest.join("preset.yml").is_file() {
copy_tree(&src, &dest, false)?;
}
write_localized_preset(&dest, spec)?;
Ok(())
})() {
return PresetEnsureResult {
status: "failed",
version,
message: format!("内置{label}安装失败:{exc}"),
};
}
let sha = short_version(bundled.as_deref());
if installed == bundled && dest.join("preset.yml").is_file() {
return PresetEnsureResult {
status: "current",
version: bundled,
message: format!("内置{label} {sha} 已就绪"),
};
}
if installed.is_some() {
return PresetEnsureResult {
status: "updated",
version: bundled,
message: format!("已将内置{label}更新到 {sha}"),
};
}
PresetEnsureResult {
status: "installed",
version: bundled,
message: format!("已启用内置{label} {sha}"),
}
}
fn combine(results: &[PresetEnsureResult]) -> PresetEnsureResult {
if results.iter().any(|item| item.status == "failed") {
let failed: Vec<_> = results
.iter()
.filter(|item| item.status == "failed")
.map(|item| item.message.clone())
.collect();
return PresetEnsureResult {
status: "failed",
version: None,
message: failed.join(""),
};
}
let active: Vec<_> = results
.iter()
.filter(|item| item.status != "skipped")
.collect();
if active.is_empty() {
return PresetEnsureResult {
status: "skipped",
version: None,
message: "未找到内置锚定预设,跳过安装".into(),
};
}
let version = active.iter().find_map(|item| item.version.clone());
let (status, lead) = if active.iter().any(|item| item.status == "updated") {
("updated", "已更新内置锚定预设")
} else if active.iter().any(|item| item.status == "installed") {
("installed", "已启用内置锚定预设")
} else {
("current", "内置锚定预设已就绪")
};
let detail = active
.iter()
.map(|item| item.message.as_str())
.collect::<Vec<_>>()
.join("");
PresetEnsureResult {
status,
version,
message: format!("{lead}。{detail}"),
}
}
pub fn ensure_anchored_standard(paths: &BundledPaths) -> PresetEnsureResult {
let results: Vec<_> = BUNDLED_PRESETS
.iter()
.map(|spec| ensure_one(paths, spec))
.collect();
let combined = combine(&results);
if combined.status == "failed" {
return combined;
}
let settings = dsh_home().join("settings.yaml");
match (|| -> std::io::Result<()> {
let previous = if settings.is_file() {
std::fs::read_to_string(&settings)?
} else {
String::new()
};
let updated = ensure_default_preset(&previous);
if updated != previous {
if let Some(parent) = settings.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&settings, updated)?;
}
Ok(())
})() {
Ok(()) => combined,
Err(exc) => PresetEnsureResult {
status: "failed",
version: combined.version,
message: format!("写入默认 preset 失败:{exc}"),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::paths::BundledPaths;
#[test]
fn empty_file_gets_default_block() {
assert_eq!(
ensure_default_preset(""),
"agent-presets:\n default: anchored-standard\n"
);
}
#[test]
fn appends_when_section_missing() {
let updated = ensure_default_preset("ui-theme:\n preference: dark\n");
assert!(updated.contains("preference: dark"));
assert!(updated.contains("agent-presets:\n default: anchored-standard\n"));
}
#[test]
fn inserts_default_into_empty_section() {
let updated = ensure_default_preset("agent-presets:\nui-theme:\n preference: dark\n");
assert!(updated.contains("agent-presets:\n default: anchored-standard\n"));
assert!(updated.contains("preference: dark"));
}
#[test]
fn keeps_existing_default() {
let original = "agent-presets:\n default: standard\n";
assert_eq!(ensure_default_preset(original), original);
}
#[test]
fn does_not_match_permission_default_preset() {
let original = "permission:\n defaultPreset: danger-full-access\n";
let updated = ensure_default_preset(original);
assert!(updated.contains("defaultPreset: danger-full-access"));
assert!(updated.contains("agent-presets:\n default: anchored-standard\n"));
}
#[test]
fn replaces_english_name_and_description() {
let original = "\
name: Anchored Standard (experimental)
description: Bootstrap with the Minimal preset's real tool pair.
order: 5
";
let updated = localize_preset_yml(original, PRESET_NAME_ZH, PRESET_DESCRIPTION_ZH);
assert!(updated.contains(PRESET_NAME_ZH));
assert!(updated.contains(PRESET_DESCRIPTION_ZH));
assert!(!updated.contains("Anchored Standard (experimental)"));
assert!(!updated.contains("Bootstrap"));
assert!(updated.contains("order: 5"));
}
#[test]
fn inserts_description_after_name() {
let updated = localize_preset_yml("name: Anchored Standard\norder: 5\n", PRESET_NAME_ZH, PRESET_DESCRIPTION_ZH);
assert!(updated.contains(PRESET_NAME_ZH));
assert!(!updated.contains("name: Anchored Standard\n"));
assert!(updated.contains(PRESET_DESCRIPTION_ZH));
assert!(updated.contains("order: 5"));
}
#[test]
fn installs_from_bundle_and_sets_default() {
let tmp = tempfile::TempDir::new().unwrap();
let src = tmp.path().join("anchored-standard");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("preset.yml"), "name: Anchored Standard\n").unwrap();
std::fs::write(src.join(SOURCE_MARKER), "abc123def456\n").unwrap();
let home = tmp.path().join(".dsh");
std::env::set_var("DSH_HOME", &home);
let paths = BundledPaths::discover().with_resource_dir(tmp.path().to_path_buf());
let result = ensure_anchored_standard(&paths);
std::env::remove_var("DSH_HOME");
assert_eq!(result.status, "installed");
let dest = home.join(".agent-presets").join(PRESET_ID);
assert!(dest.join("preset.yml").is_file());
let yml = std::fs::read_to_string(dest.join("preset.yml")).unwrap();
assert!(yml.contains(PRESET_DESCRIPTION_ZH));
let settings = std::fs::read_to_string(home.join("settings.yaml")).unwrap();
assert!(settings.contains("default: anchored-standard"));
}
#[test]
fn localizes_zero_preset() {
let original = "\
name: Zero-Anchored Standard (experimental)
description: Inject one zero-tool anchor turn.
order: 6
";
let updated = localize_preset_yml(original, ZERO_PRESET_NAME_ZH, ZERO_PRESET_DESCRIPTION_ZH);
assert!(updated.contains(ZERO_PRESET_NAME_ZH));
assert!(updated.contains(ZERO_PRESET_DESCRIPTION_ZH));
assert!(!updated.contains("Zero-Anchored"));
}
}