mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
feat(pi): expose explicit prompt library reconciliation
This commit is contained in:
@@ -9,7 +9,7 @@ use crate::services::pi_prompt_files::{
|
||||
PiPromptFileKind, PiPromptFileService, PiPromptFileSnapshot, PiPromptTemplate,
|
||||
PiPromptTemplateService,
|
||||
};
|
||||
use crate::services::PromptService;
|
||||
use crate::services::prompt::{PiPromptLibraryStatus, PromptService};
|
||||
use crate::store::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
@@ -67,6 +67,18 @@ pub async fn get_current_prompt_file_content(app: String) -> Result<Option<Strin
|
||||
PromptService::get_current_file_content(app_type).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_pi_prompt_library_status(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PiPromptLibraryStatus, String> {
|
||||
PromptService::get_pi_library_status(&state).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reconcile_pi_prompt_library(state: State<'_, AppState>) -> Result<(), String> {
|
||||
PromptService::reconcile_pi_library(&state).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_pi_prompt_file(kind: PiPromptFileKind) -> Result<PiPromptFileSnapshot, String> {
|
||||
PiPromptFileService::read(kind).map_err(|error| error.to_string())
|
||||
|
||||
@@ -1420,6 +1420,8 @@ pub fn run() {
|
||||
commands::enable_prompt,
|
||||
commands::import_prompt_from_file,
|
||||
commands::get_current_prompt_file_content,
|
||||
commands::get_pi_prompt_library_status,
|
||||
commands::reconcile_pi_prompt_library,
|
||||
commands::get_pi_prompt_file,
|
||||
commands::replace_pi_prompt_file,
|
||||
commands::delete_pi_prompt_file,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::services::pi_prompt_files::{
|
||||
PiPromptFileSnapshot,
|
||||
};
|
||||
use crate::store::AppState;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// 安全地获取当前 Unix 时间戳
|
||||
@@ -23,14 +24,41 @@ fn get_unix_timestamp() -> Result<i64, AppError> {
|
||||
|
||||
pub struct PromptService;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PiPromptLibraryStatus {
|
||||
pub native_exists: bool,
|
||||
pub native_revision: String,
|
||||
pub matched_prompt_id: Option<String>,
|
||||
pub needs_reconciliation: bool,
|
||||
}
|
||||
|
||||
impl PromptService {
|
||||
pub fn get_prompts(
|
||||
state: &AppState,
|
||||
app: AppType,
|
||||
) -> Result<IndexMap<String, Prompt>, AppError> {
|
||||
if matches!(app, AppType::Pi) {
|
||||
let guard = lock_instruction_files()?;
|
||||
return Self::inspect_pi_library_under_guard(state.db.as_ref(), &guard)
|
||||
.map(|(prompts, _)| prompts);
|
||||
}
|
||||
state.db.get_prompts(app.as_str())
|
||||
}
|
||||
|
||||
/// Inspect Pi's live AGENTS.md without adopting it into the portable
|
||||
/// library. File presence and exact bytes are the effective active truth;
|
||||
/// persisted `enabled` flags are only a projection that explicit
|
||||
/// reconciliation may repair.
|
||||
pub fn get_pi_library_status(state: &AppState) -> Result<PiPromptLibraryStatus, AppError> {
|
||||
let guard = lock_instruction_files()?;
|
||||
Self::inspect_pi_library_under_guard(state.db.as_ref(), &guard).map(|(_, status)| status)
|
||||
}
|
||||
|
||||
pub fn reconcile_pi_library(state: &AppState) -> Result<(), AppError> {
|
||||
Self::reconcile_pi_portable_import(state)
|
||||
}
|
||||
|
||||
pub fn upsert_prompt(
|
||||
state: &AppState,
|
||||
app: AppType,
|
||||
@@ -371,6 +399,43 @@ impl PromptService {
|
||||
))
|
||||
}
|
||||
|
||||
fn inspect_pi_library_under_guard(
|
||||
db: &Database,
|
||||
guard: &PiInstructionFileGuard,
|
||||
) -> Result<(IndexMap<String, Prompt>, PiPromptLibraryStatus), AppError> {
|
||||
let snapshot =
|
||||
PiPromptFileService::read_under_guard(guard, PiPromptFileKind::GlobalContext)?;
|
||||
let mut prompts = db.get_prompts(AppType::Pi.as_str())?;
|
||||
let persisted_enabled = prompts
|
||||
.iter()
|
||||
.filter_map(|(id, prompt)| prompt.enabled.then_some(id.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matched_prompt_id = if snapshot.exists {
|
||||
prompts
|
||||
.iter()
|
||||
.find_map(|(id, prompt)| (prompt.content == snapshot.content).then(|| id.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let expected_enabled = matched_prompt_id.iter().cloned().collect::<Vec<_>>();
|
||||
let needs_reconciliation = persisted_enabled != expected_enabled
|
||||
|| (snapshot.exists && matched_prompt_id.is_none());
|
||||
|
||||
for (id, prompt) in &mut prompts {
|
||||
prompt.enabled = matched_prompt_id.as_deref() == Some(id.as_str());
|
||||
}
|
||||
|
||||
Ok((
|
||||
prompts,
|
||||
PiPromptLibraryStatus {
|
||||
native_exists: snapshot.exists,
|
||||
native_revision: snapshot.revision,
|
||||
matched_prompt_id,
|
||||
needs_reconciliation,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn upsert_pi_prompt(state: &AppState, prompt: Prompt) -> Result<(), AppError> {
|
||||
let guard = lock_instruction_files()?;
|
||||
let mut prompts = state.db.get_prompts(AppType::Pi.as_str())?;
|
||||
@@ -736,6 +801,18 @@ mod tests {
|
||||
std::fs::write(temp.path().join("AGENTS.md"), "external-after").expect("external edit");
|
||||
let prompts = PromptService::get_prompts(&state, AppType::Pi).expect("read prompt list");
|
||||
assert_eq!(prompts["managed"].content, "managed-before");
|
||||
assert!(
|
||||
prompts.values().all(|prompt| !prompt.enabled),
|
||||
"read-only inspection must report native truth instead of the stale DB projection"
|
||||
);
|
||||
assert!(
|
||||
state
|
||||
.db
|
||||
.get_prompts(AppType::Pi.as_str())
|
||||
.expect("read persisted projection")["managed"]
|
||||
.enabled,
|
||||
"inspection must not silently adopt or rewrite the portable library"
|
||||
);
|
||||
assert!(
|
||||
PromptService::enable_prompt(&state, AppType::Pi, "other").is_err(),
|
||||
"the write boundary must still report the external drift conflict"
|
||||
@@ -745,4 +822,91 @@ mod tests {
|
||||
"external-after"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn explicit_pi_library_reconciliation_adopts_external_native_truth() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let _restore = EnvRestore::set("PI_CODING_AGENT_DIR", temp.path());
|
||||
std::fs::write(temp.path().join("AGENTS.md"), "external-after").expect("seed AGENTS.md");
|
||||
let state = AppState::new(Arc::new(Database::memory().expect("database")));
|
||||
state
|
||||
.db
|
||||
.save_prompt(
|
||||
AppType::Pi.as_str(),
|
||||
&Prompt {
|
||||
id: "stale".to_string(),
|
||||
name: "Stale".to_string(),
|
||||
content: "managed-before".to_string(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
created_at: Some(1),
|
||||
updated_at: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("seed stale projection");
|
||||
|
||||
let status = PromptService::get_pi_library_status(&state).expect("inspect");
|
||||
assert!(status.native_exists);
|
||||
assert!(status.matched_prompt_id.is_none());
|
||||
assert!(status.needs_reconciliation);
|
||||
|
||||
PromptService::reconcile_pi_library(&state).expect("explicit reconcile");
|
||||
let prompts = PromptService::get_prompts(&state, AppType::Pi).expect("read reconciled");
|
||||
let active = prompts
|
||||
.values()
|
||||
.filter(|prompt| prompt.enabled)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(active.len(), 1);
|
||||
assert_eq!(active[0].content, "external-after");
|
||||
assert!(
|
||||
!PromptService::get_pi_library_status(&state)
|
||||
.expect("reinspect")
|
||||
.needs_reconciliation
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(temp.path().join("AGENTS.md")).expect("native remains"),
|
||||
"external-after"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn missing_agents_file_is_effectively_inactive_until_explicit_reconciliation() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let _restore = EnvRestore::set("PI_CODING_AGENT_DIR", temp.path());
|
||||
let state = AppState::new(Arc::new(Database::memory().expect("database")));
|
||||
state
|
||||
.db
|
||||
.save_prompt(
|
||||
AppType::Pi.as_str(),
|
||||
&Prompt {
|
||||
id: "shadow".to_string(),
|
||||
name: "Shadow".to_string(),
|
||||
content: "not live".to_string(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
created_at: Some(1),
|
||||
updated_at: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("seed shadow");
|
||||
|
||||
assert!(PromptService::get_prompts(&state, AppType::Pi)
|
||||
.expect("read effective")
|
||||
.values()
|
||||
.all(|prompt| !prompt.enabled));
|
||||
let status = PromptService::get_pi_library_status(&state).expect("inspect");
|
||||
assert!(!status.native_exists);
|
||||
assert!(status.needs_reconciliation);
|
||||
|
||||
PromptService::reconcile_pi_library(&state).expect("explicit reconcile");
|
||||
assert!(state
|
||||
.db
|
||||
.get_prompts(AppType::Pi.as_str())
|
||||
.expect("read persisted")
|
||||
.values()
|
||||
.all(|prompt| !prompt.enabled));
|
||||
assert!(!temp.path().join("AGENTS.md").exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user