Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion openless-all/app/src-tauri/src/persistence/style_pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ impl StylePackStore {

let mut prefs_snapshot = prefs.get();
let mut changed = migrate_style_packs_from_preferences(&mut packs, &prefs_snapshot);
// 内置包版本对账:代码内置包 version 高于本地副本时用官方覆盖——内置提示词
// 升级(如「清晰结构」v3.0 Beta)随 app 更新推向所有已安装用户。保留用户对
// enabled 的设置;本地缺失的内置包直接补入。
if reconcile_builtin_packs(&mut packs) {
changed = true;
}
if ensure_at_least_one_style_pack_enabled(&mut packs) {
changed = true;
}
Expand Down Expand Up @@ -122,9 +128,11 @@ impl StylePackStore {
{
return Ok(pack);
}
// 产品默认包是「清晰结构」(default_active_style_pack_id);用户激活的包
// 被禁用时优先落回它,与默认保持一致,最后才用任意 enabled 包兜底。
if let Some(pack) = packs
.iter()
.find(|pack| pack.id == BUILTIN_STYLE_PACK_LIGHT_ID && pack.enabled)
.find(|pack| pack.id == default_active_style_pack_id() && pack.enabled)
.cloned()
{
return Ok(pack);
Expand Down Expand Up @@ -542,6 +550,56 @@ fn ensure_at_least_one_style_pack_enabled(packs: &mut [StylePack]) -> bool {
false
}

/// 内置包版本对账:官方版本更高 → 仅推进 prompt 与 version(描述/示例/tags 等
/// 用户自定义和 enabled 状态全部保留)。缺失的内置包直接补入。
/// 返回是否有任何包被推进 / 补入。
fn reconcile_builtin_packs(packs: &mut Vec<StylePack>) -> bool {
let mut changed = false;
for builtin in crate::types::builtin_style_packs() {
if let Some(local) = packs.iter_mut().find(|pack| pack.id == builtin.id) {
if pack_version_newer(&builtin.version, &local.version) {
log::info!(
"[style-pack] builtin {} prompt upgraded {} -> {} (prompt-only)",
local.id,
local.version,
builtin.version
);
local.version = builtin.version.clone();
local.prompt = builtin.prompt.clone();
local.updated_at = Some(Utc::now().to_rfc3339());
changed = true;
}
} else {
packs.push(builtin);
changed = true;
}
}
changed
}

/// "X.Y.Z" 数值分段比较。pre-release 后缀(-beta 等)先切掉:pre-release 视为
/// 与正式版同级,不判为更新(与 semver 直觉一致)。
fn pack_version_newer(a: &str, b: &str) -> bool {
fn numeric_parts(v: &str) -> Vec<u64> {
v.split('-')
.next()
.unwrap_or(v)
.split('.')
.filter_map(|s| s.parse::<u64>().ok())
.collect()
}
let (pa, pb) = (numeric_parts(a), numeric_parts(b));
for i in 0..pa.len().max(pb.len()) {
let x = pa.get(i).copied().unwrap_or(0);
let y = pb.get(i).copied().unwrap_or(0);
if x == y {
continue;
}
return x > y;
}
false
}

pub fn sync_style_pack_preferences(prefs: &mut UserPreferences, packs: &[StylePack]) -> bool {
let previous_active_style_pack_id = prefs.active_style_pack_id.clone();
let previous_default_mode = prefs.default_mode;
Expand Down
52 changes: 52 additions & 0 deletions openless-all/app/src-tauri/src/persistence/style_pack_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,55 @@ fn sync_style_pack_preferences_uses_builtin_store_prompts_as_source_of_truth() {
assert_eq!(prefs.style_system_prompts.formal, packs[3].prompt);
assert_eq!(prefs.custom_style_prompts, CustomStylePrompts::default());
}

#[test]
fn pack_version_newer_compares_numeric_segments() {
assert!(super::pack_version_newer("3.0.0", "2.0.0"));
assert!(!super::pack_version_newer("2.0.0", "3.0.0"));
assert!(!super::pack_version_newer("3.0.0", "3.0.0"));
assert!(super::pack_version_newer("3.1.0", "3.0.9"));
assert!(super::pack_version_newer("10.0.0", "9.9.9"));
// pre-release 视为与正式版同级,不判为更新
assert!(!super::pack_version_newer("3.0.0-beta.1", "3.0.0"));
assert!(!super::pack_version_newer("3.0.0", "3.0.0-beta.1"));
assert!(super::pack_version_newer("3.0.1-beta", "3.0.0"));
// 全非数字 → 不判定更新
assert!(!super::pack_version_newer("abc", "def"));
}

#[test]
fn reconcile_builtin_packs_upgrades_prompt_only_and_preserves_user_fields() {
let mut packs = builtin_style_packs();
let local = packs
.iter_mut()
.find(|p| p.id == "builtin.structured")
.expect("builtin structured pack");
local.version = "2.0.0".into();
local.prompt = "用户自定义的旧 prompt".into();
local.name = "我的清晰结构".into();
local.enabled = false; // 用户手动禁用

assert!(super::reconcile_builtin_packs(&mut packs));

let upgraded = packs
.iter()
.find(|p| p.id == "builtin.structured")
.expect("builtin structured pack");
assert_eq!(upgraded.version, "3.0.0", "版本应推进到官方 3.0.0");
assert!(upgraded.prompt.contains("# 场景优先级"), "prompt 应推进为 v3.0 Beta");
assert_eq!(upgraded.name, "我的清晰结构", "用户改名必须保留");
assert!(!upgraded.enabled, "用户 enabled 状态必须保留");
}

#[test]
fn reconcile_builtin_packs_skips_equal_version_and_adds_missing() {
// 等版本(builtin 3.0.0 vs local 3.0.0)→ 不推进、不落盘
let mut packs = builtin_style_packs();
assert!(!super::reconcile_builtin_packs(&mut packs));

// 本地缺失内置包 → 补入全部 4 个
let mut empty: Vec<StylePack> = Vec::new();
assert!(super::reconcile_builtin_packs(&mut empty));
assert_eq!(empty.len(), 4);
assert!(empty.iter().all(|p| p.kind == crate::types::StylePackKind::Builtin));
}
76 changes: 45 additions & 31 deletions openless-all/app/src-tauri/src/polish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2891,14 +2891,14 @@ mod tests {
fn structured_prompt_anchors_on_high_density_examples_and_term_protection() {
let prompt = prompts::system_prompt(PolishMode::Structured);

// v2.0:八节中文序号骨架。结构化判断 + 双层格式 + 事项数规则必须靠前讲清楚。
assert!(prompt.contains("# 二、结构化判断(核心)"));
assert!(prompt.contains("# 三、双层格式"));
assert!(prompt.contains("第一层(主题)"));
assert!(prompt.contains("第二层(子项)"));
assert!(prompt.contains("事项仅 1 条"));
assert!(prompt.contains("事项 = 2 条"));
assert!(prompt.contains("事项 ≥ 3 条"));
// v3.0 Beta:人格化「语修」角色 + 场景优先级分型。结构化判断与双层格式
// 换到 # 场景优先级 / # 输出格式 节,事项数规则必须靠前讲清楚。
assert!(prompt.contains("# 场景优先级"));
assert!(prompt.contains("# 输出格式"));
assert!(prompt.contains("# AI 编程术语纠错"));
assert!(prompt.contains("子项另起一行,用 3 个空格 + `(a)` `(b)` `(c)`"));
assert!(prompt.contains("事项 2 条"));
assert!(prompt.contains("连续编号"));

// 防回归:模型名、字段名、布尔值和版本号必须被显式保护。
assert!(prompt.contains("Claude"));
Expand All @@ -2908,41 +2908,46 @@ mod tests {
assert!(prompt.contains("LongCat"));
assert!(prompt.contains("Secret Key"));
assert!(prompt.contains("true / false / null"));
assert!(prompt.contains("GPT-5.6"));
assert!(prompt.contains("**不**简写成 GPT-5、Claude 4"));
assert!(prompt.contains("不要把 GPT 5.5 写成 GPT 5"));
assert!(prompt.contains("不要把 Claude 4.7 写成 Claude 4"));

// 4 个核心示例的锚点:超长 GitHub 请求、已编号工作日报、散乱长口述、AI 日报
assert!(prompt.contains("帮忙给 GitHub 提个请求,主要包含以下内容:"));
assert!(prompt.contains("代码与功能优化"));
assert!(prompt.contains("今天的工作小结如下:"));
assert!(prompt.contains("Gemini 3.2 版本更名为 Gemini 3.5"));
assert!(prompt.contains("remote control 的参数值更改为 true"));
// 核心示例锚点:AI 编程任务(Codex 请求)与 AI 模型资讯(Gemini 更名 + Codex 远程控制)
assert!(prompt.contains("帮忙给 Codex 提个任务,主要包含以下内容:"));
assert!(prompt.contains("登录页修复"));
assert!(prompt.contains("文档与配置"));
assert!(prompt.contains("Gemini 3.2 更名为 Gemini 3.5"));
assert!(prompt.contains("remote control 改为 true"));
}

#[test]
fn structured_prompt_keeps_regrouping_and_no_loss_guards() {
let prompt = prompts::system_prompt(PolishMode::Structured);

// v1.3.0 回归的关键规则:已编号 ≠ 不用改、≥3 必须重组、仅 1 条事项输出连贯段落
// 回归的关键规则:事项数决定输出形态、防止事项丢失、禁止替用户编造
assert!(
prompt.contains("照抄原结构 = 失败"),
"Structured prompt 必须把照抄原结构判为失败"
prompt.contains("事项 ≤ 2 条 → 直接输出连贯段落"),
"Structured prompt 必须避免短输入过度结构化(事项少 → 连贯段落)"
);
assert!(
prompt.contains("输出连贯段落"),
"Structured prompt 必须避免短输入过度结构化(仅 1 条事项 → 连贯段落)"
prompt.contains("全部列为条目保留"),
"Structured prompt 必须把未决事项原样保留"
);
assert!(
prompt.contains("不丢失任何一件事"),
"Structured prompt 必须明确防止事项丢失"
prompt.contains("是否丢事项"),
"Structured prompt 必须明确防止事项丢失(结构自检)"
);
assert!(
prompt.contains("不补充用户没说过的实现方案"),
prompt.contains("不补充用户没说过的事实、字段、实现方案或功能清单"),
"Structured prompt 必须禁止替用户编造实现方案"
);
assert!(
prompt.contains("即使原文已经写成"),
"Structured prompt 必须显式说明已编号的输入也要重新归类"
prompt.contains("没有编造原文不存在的实现方案"),
"Structured prompt 必须把不编造写进结构自检"
);
// 长输入必须按主题重组:示例 1 把超长口述整理成主题分组双层结构。
assert!(
prompt.contains("帮忙给 Codex 提个任务,主要包含以下内容:"),
"Structured prompt 必须带重组示例锚点"
);
}

Expand Down Expand Up @@ -3123,11 +3128,7 @@ mod tests {
);

// v2 PRO 自带 prompt 必须共享:四/五、ASR 纠错段 + 高/低置信度分级 + 根目录词条。
for mode in [
PolishMode::Light,
PolishMode::Structured,
PolishMode::Formal,
] {
for mode in [PolishMode::Light, PolishMode::Formal] {
let prompt = prompts::system_prompt(mode);
let has_asr_heading =
prompt.contains("# 四、ASR 纠错") || prompt.contains("# 五、ASR 纠错");
Expand All @@ -3141,6 +3142,19 @@ mod tests {
"{mode:?} prompt 缺少分级置信度策略"
);
}

// Structured v3.0 Beta:ASR 纠错段换到 # 通用规则 5(自动纠错按置信度分级),
// 置信度表述为「高/中/低置信度」而非 v2 的 ** 加粗。
let structured = prompts::system_prompt(PolishMode::Structured);
assert!(
structured.contains("自动纠错(ASR 主动纠错,按置信度分级处理)"),
"Structured prompt 缺少自动纠错分级规则"
);
assert!(
structured.contains("高置信度") && structured.contains("低置信度"),
"Structured prompt 缺少置信度分级"
);
assert!(structured.contains("根目录"), "Structured prompt 缺少根目录纠错示例");
}

#[test]
Expand Down
Loading
Loading