use super::report_output::print_json_value; use super::*; use bat_core::domain::{ GlossaryReviewStatus, GlossarySourceKind, GlossarySourceRecord, GlossaryTermDraft, GlossaryTermSnapshot, TranslationMemoryContext, }; use bat_infrastructure::{SqliteGlossaryRepository, GLOSSARY_SCHEMA_VERSION}; use std::collections::BTreeMap; pub(super) fn run_glossary_command(options: &CliOptions) -> anyhow::Result<()> { let method = glossary_method(options.command)?; if daemon_rpc_available(&options.state_dir) && options.resource_root.is_none() && !options.output_explicit { let _control_lock = DaemonControlLock::acquire(&options.state_dir)?; let report = daemon_rpc_call(&options.state_dir, method, glossary_cli_params(options)?)?; print_json_value(options.output_format, &report)?; return Ok(()); } let path = glossary_cli_path(options)?; let report = match options.command { CliCommand::GlossarySummary => build_glossary_summary_report(&path)?, CliCommand::GlossaryQuery => build_glossary_query_report( &path, options.glossary_source_text.as_deref(), options.glossary_category.as_deref(), options.glossary_review_status.as_deref(), options.query_limit, )?, CliCommand::GlossaryDiagnose => build_glossary_diagnose_report( &path, options.glossary_source_text.as_deref().unwrap_or_default(), parse_glossary_context(options.glossary_context_json.as_deref())?, )?, CliCommand::GlossaryAdd | CliCommand::GlossaryUpdate => { let draft = glossary_term_draft(options)?; let reviewer = options.glossary_reviewer.as_deref(); build_glossary_mutation_report( &path, &draft, options.command == CliCommand::GlossaryUpdate, reviewer, options.glossary_reason.clone(), )? } CliCommand::GlossaryApprove | CliCommand::GlossaryDeprecate => { let term_id = options.glossary_term_id.as_deref().ok_or_else(|| { anyhow::anyhow!("Glossary review 必须指定 --glossary-term-id") })?; let reviewer = options.glossary_reviewer.as_deref().ok_or_else(|| { anyhow::anyhow!("Glossary review 必须指定 --glossary-reviewer") })?; let status = if options.command == CliCommand::GlossaryApprove { GlossaryReviewStatus::Approved } else { GlossaryReviewStatus::Deprecated }; build_glossary_review_report( &path, term_id, status, reviewer, options.glossary_reason.clone(), )? } CliCommand::GlossaryDelete => { let term_id = options.glossary_term_id.as_deref().ok_or_else(|| { anyhow::anyhow!("Glossary delete 必须指定 --glossary-term-id") })?; let reviewer = options.glossary_reviewer.as_deref().ok_or_else(|| { anyhow::anyhow!("Glossary delete 必须指定 --glossary-reviewer") })?; let reason = options .glossary_reason .as_deref() .ok_or_else(|| anyhow::anyhow!("Glossary delete 必须指定 --glossary-reason"))?; build_glossary_delete_report(&path, term_id, reviewer, reason)? } _ => unreachable!(), }; print_json_value(options.output_format, &report) } fn glossary_method(command: CliCommand) -> anyhow::Result<&'static str> { Ok(match command { CliCommand::GlossarySummary => RPC_METHOD_GLOSSARY_SUMMARY, CliCommand::GlossaryQuery => RPC_METHOD_GLOSSARY_QUERY, CliCommand::GlossaryAdd => RPC_METHOD_GLOSSARY_ADD, CliCommand::GlossaryUpdate => RPC_METHOD_GLOSSARY_UPDATE, CliCommand::GlossaryApprove => RPC_METHOD_GLOSSARY_APPROVE, CliCommand::GlossaryDeprecate => RPC_METHOD_GLOSSARY_DEPRECATE, CliCommand::GlossaryDelete => RPC_METHOD_GLOSSARY_DELETE, CliCommand::GlossaryDiagnose => RPC_METHOD_GLOSSARY_DIAGNOSE, _ => return Err(anyhow::anyhow!("不是 Glossary 命令")), }) } fn glossary_cli_path(options: &CliOptions) -> anyhow::Result { if let Some(path) = options.glossary_path.as_ref() { return lexical_absolute(path).map_err(anyhow::Error::msg); } let resource_root = options .resource_root .as_deref() .map(lexical_absolute) .transpose() .map_err(anyhow::Error::msg)? .unwrap_or(active_official_resource_root(&options.config.output_root)?); Ok(SqliteGlossaryRepository::repository_path(&resource_root)) } fn glossary_cli_params(options: &CliOptions) -> anyhow::Result> { let mut params = serde_json::Map::new(); if let Some(path) = options.glossary_path.as_ref() { params.insert("glossary_path".to_string(), serde_json::json!(path)); } match options.command { CliCommand::GlossarySummary => {} CliCommand::GlossaryQuery => { if let Some(source_text) = options.glossary_source_text.as_deref() { params.insert("source_text".to_string(), serde_json::json!(source_text)); } if let Some(category) = options.glossary_category.as_deref() { params.insert("category".to_string(), serde_json::json!(category)); } if let Some(status) = options.glossary_review_status.as_deref() { params.insert("review_status".to_string(), serde_json::json!(status)); } params.insert("limit".to_string(), serde_json::json!(options.query_limit)); } CliCommand::GlossaryDiagnose => { let source_text = options.glossary_source_text.as_deref().ok_or_else(|| { anyhow::anyhow!("Glossary diagnose 必须指定 --glossary-source-text") })?; params.insert("source_text".to_string(), serde_json::json!(source_text)); params.insert( "context".to_string(), serde_json::json!(parse_glossary_context( options.glossary_context_json.as_deref() )?), ); } CliCommand::GlossaryAdd | CliCommand::GlossaryUpdate => { let draft = glossary_term_draft(options)?; params.extend( serde_json::to_value(draft)? .as_object() .cloned() .unwrap_or_default(), ); if options.command == CliCommand::GlossaryUpdate { params.insert( "reviewer".to_string(), serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()), ); if let Some(reason) = options.glossary_reason.as_deref() { params.insert("reason".to_string(), serde_json::json!(reason)); } } } CliCommand::GlossaryApprove | CliCommand::GlossaryDeprecate => { params.insert( "term_id".to_string(), serde_json::json!(options.glossary_term_id.as_deref().unwrap_or_default()), ); params.insert( "reviewer".to_string(), serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()), ); if let Some(reason) = options.glossary_reason.as_deref() { params.insert("reason".to_string(), serde_json::json!(reason)); } } CliCommand::GlossaryDelete => { params.insert( "term_id".to_string(), serde_json::json!(options.glossary_term_id.as_deref().unwrap_or_default()), ); params.insert( "reviewer".to_string(), serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()), ); params.insert( "reason".to_string(), serde_json::json!(options.glossary_reason.as_deref().unwrap_or_default()), ); } _ => unreachable!(), } Ok(Some(serde_json::Value::Object(params))) } fn glossary_term_draft(options: &CliOptions) -> anyhow::Result { let term_id = required_option(options.glossary_term_id.as_deref(), "--glossary-term-id")?; let source_term = required_option( options.glossary_source_term.as_deref(), "--glossary-source-term", )?; let recommended_translation = required_option( options.glossary_recommended_translation.as_deref(), "--glossary-recommended-translation", )?; let aliases = parse_string_array( options.glossary_aliases_json.as_deref(), "--glossary-aliases-json", )?; let allowed_translations = parse_string_array( options.glossary_allowed_translations_json.as_deref(), "--glossary-allowed-translations-json", )?; let scope = parse_glossary_context(options.glossary_scope_json.as_deref())?; let source_kind = options.glossary_source_kind.as_deref().unwrap_or("manual"); let source_kind = GlossarySourceKind::parse(source_kind) .ok_or_else(|| anyhow::anyhow!("Glossary source kind 无效:{source_kind}"))?; let review_status = options.glossary_review_status.as_deref().unwrap_or("draft"); let review_status = GlossaryReviewStatus::parse(review_status) .ok_or_else(|| anyhow::anyhow!("Glossary review status 无效:{review_status}"))?; let now = unix_seconds_now(); Ok(GlossaryTermDraft { term_id, definition: GlossaryTermSnapshot { source_term, aliases, recommended_translation, allowed_translations, source_language: options.glossary_source_language.clone(), target_language: options.glossary_target_language.clone(), category: options.glossary_category.clone(), priority: options.glossary_priority, scope, }, review_status, source: GlossarySourceRecord { source_kind, source_ref: options.glossary_source_ref.clone(), source_author: options.glossary_source_author.clone(), source_note: options.glossary_source_note.clone(), observed_unix_seconds: now, }, }) } fn required_option(value: Option<&str>, label: &str) -> anyhow::Result { value .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) .ok_or_else(|| anyhow::anyhow!("Glossary 必须指定 {label}")) } fn parse_string_array(value: Option<&str>, label: &str) -> anyhow::Result> { let Some(value) = value else { return Ok(Vec::new()); }; serde_json::from_str(value) .map_err(|error| anyhow::anyhow!("{label} 必须是 JSON string array:{error}")) } fn parse_glossary_context(value: Option<&str>) -> anyhow::Result { let Some(value) = value else { return Ok(BTreeMap::new()); }; serde_json::from_str(value) .map_err(|error| anyhow::anyhow!("Glossary context 必须是 JSON object:{error}")) } pub(super) fn build_glossary_summary_report( path: &std::path::Path, ) -> anyhow::Result { if !sqlite_file_exists_no_symlink(path, "Glossary 数据库")? { return Ok(serde_json::json!({ "available": false, "path": path, "reason": "database_missing", })); } let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let summary = runtime.block_on(async { let repository = SqliteGlossaryRepository::open(path) .await .map_err(|error| anyhow::anyhow!("{error}"))?; repository .summary() .await .map_err(|error| anyhow::anyhow!("{error}")) })?; Ok(serde_json::json!({ "available": true, "path": path, "schema_version": GLOSSARY_SCHEMA_VERSION, "summary": summary, })) } pub(super) fn build_glossary_query_report( path: &std::path::Path, source_text: Option<&str>, category: Option<&str>, review_status: Option<&str>, limit: usize, ) -> anyhow::Result { let status = review_status .map(|value| { GlossaryReviewStatus::parse(value) .ok_or_else(|| anyhow::anyhow!("Glossary review_status 无效")) }) .transpose()?; if !sqlite_file_exists_no_symlink(path, "Glossary 数据库")? { return Ok(serde_json::json!({ "available": false, "path": path, "source_text": source_text, "terms": [], "reason": "database_missing", })); } let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let terms = runtime.block_on(async { let repository = SqliteGlossaryRepository::open(path) .await .map_err(|error| anyhow::anyhow!("{error}"))?; repository .query(source_text, category, status, limit) .await .map_err(|error| anyhow::anyhow!("{error}")) })?; Ok(serde_json::json!({ "available": true, "path": path, "source_text": source_text, "terms": terms, })) } pub(super) fn build_glossary_diagnose_report( path: &std::path::Path, source_text: &str, context: TranslationMemoryContext, ) -> anyhow::Result { if source_text.trim().is_empty() { return Err(anyhow::anyhow!("Glossary diagnose 的 source_text 不能为空")); } if !sqlite_file_exists_no_symlink(path, "Glossary 数据库")? { return Ok(serde_json::json!({ "available": false, "path": path, "source_text": source_text, "context": context, "evaluation": { "constraints": [], "diagnostics": [], "blocked": false }, "reason": "database_missing", })); } let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let evaluation = runtime.block_on(async { let repository = SqliteGlossaryRepository::open(path) .await .map_err(|error| anyhow::anyhow!("{error}"))?; repository .diagnose(source_text, &context) .await .map_err(|error| anyhow::anyhow!("{error}")) })?; Ok(serde_json::json!({ "available": true, "path": path, "source_text": source_text, "context": context, "evaluation": evaluation, })) } pub(super) fn build_glossary_mutation_report( path: &std::path::Path, draft: &GlossaryTermDraft, update: bool, reviewer: Option<&str>, reason: Option, ) -> anyhow::Result { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let term = runtime.block_on(async { let repository = SqliteGlossaryRepository::new(path) .await .map_err(|error| anyhow::anyhow!("{error}"))?; let result = if update { repository .update( draft.clone(), reviewer.ok_or_else(|| anyhow::anyhow!("Glossary update 需要 reviewer"))?, reason, ) .await } else { repository.add(draft.clone()).await }; result.map_err(|error| anyhow::anyhow!("{error}")) })?; Ok(serde_json::json!({ "available": true, "path": path, "schema_version": GLOSSARY_SCHEMA_VERSION, "term": term, })) } pub(super) fn build_glossary_review_report( path: &std::path::Path, term_id: &str, status: GlossaryReviewStatus, reviewer: &str, reason: Option, ) -> anyhow::Result { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let term = runtime.block_on(async { let repository = SqliteGlossaryRepository::open(path) .await .map_err(|error| anyhow::anyhow!("{error}"))?; repository .review(term_id, status, reviewer, reason) .await .map_err(|error| anyhow::anyhow!("{error}")) })?; Ok(serde_json::json!({ "available": true, "path": path, "term": term, })) } pub(super) fn build_glossary_delete_report( path: &std::path::Path, term_id: &str, reviewer: &str, reason: &str, ) -> anyhow::Result { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let term = runtime.block_on(async { let repository = SqliteGlossaryRepository::open(path) .await .map_err(|error| anyhow::anyhow!("{error}"))?; repository .delete(term_id, reviewer, reason) .await .map_err(|error| anyhow::anyhow!("{error}")) })?; Ok(serde_json::json!({ "available": true, "path": path, "schema_version": GLOSSARY_SCHEMA_VERSION, "deleted": true, "term": term, })) } pub(super) fn glossary_rpc_envelope( request_id: String, result: Result, ) -> RpcEnvelope { match result { Ok(data) => rpc_envelope_ok(request_id, "ok", data), Err(error) => rpc_envelope_error(request_id, error), } } pub(super) fn glossary_summary_rpc_report( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, ) -> Result { let path = glossary_rpc_path( state_dir, output_root, default_path, params, RPC_METHOD_GLOSSARY_SUMMARY, )?; build_glossary_summary_report(&path) .map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_SUMMARY, error)) } pub(super) fn glossary_query_rpc_report( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, ) -> Result { let params = glossary_params(params, RPC_METHOD_GLOSSARY_QUERY)?; let source_text = glossary_string(¶ms, "source_text", RPC_METHOD_GLOSSARY_QUERY)?; let category = glossary_string(¶ms, "category", RPC_METHOD_GLOSSARY_QUERY)?; let review_status = glossary_string(¶ms, "review_status", RPC_METHOD_GLOSSARY_QUERY)?; let limit = glossary_limit(¶ms, RPC_METHOD_GLOSSARY_QUERY)?; let path = glossary_rpc_path( state_dir, output_root, default_path, Some(&serde_json::Value::Object(params.clone())), RPC_METHOD_GLOSSARY_QUERY, )?; build_glossary_query_report(&path, source_text, category, review_status, limit) .map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_QUERY, error)) } pub(super) fn glossary_diagnose_rpc_report( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, ) -> Result { let params = glossary_params(params, RPC_METHOD_GLOSSARY_DIAGNOSE)?; let source_text = glossary_string(¶ms, "source_text", RPC_METHOD_GLOSSARY_DIAGNOSE)? .ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DIAGNOSE, "缺少 source_text"))?; let context = glossary_context(¶ms, RPC_METHOD_GLOSSARY_DIAGNOSE)?; let path = glossary_rpc_path( state_dir, output_root, default_path, Some(&serde_json::Value::Object(params.clone())), RPC_METHOD_GLOSSARY_DIAGNOSE, )?; build_glossary_diagnose_report(&path, source_text, context) .map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_DIAGNOSE, error)) } pub(super) fn glossary_mutation_rpc_report( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, update: bool, ) -> Result { let method = if update { RPC_METHOD_GLOSSARY_UPDATE } else { RPC_METHOD_GLOSSARY_ADD }; let params = glossary_params(params, method)?; let draft: GlossaryTermDraft = serde_json::from_value(serde_json::Value::Object(params.clone())).map_err(|error| { glossary_invalid(method, format!("Glossary term 参数无效:{error}")) })?; let reviewer = glossary_string(¶ms, "reviewer", method)?; if update && reviewer.is_none() { return Err(glossary_invalid(method, "update 缺少 reviewer")); } let reason = glossary_string(¶ms, "reason", method)?.map(str::to_string); let path = glossary_rpc_path( state_dir, output_root, default_path, Some(&serde_json::Value::Object(params.clone())), method, )?; build_glossary_mutation_report(&path, &draft, update, reviewer, reason) .map_err(|error| glossary_internal_error(method, error)) } pub(super) fn glossary_review_rpc_report( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, status: GlossaryReviewStatus, ) -> Result { let method = if status == GlossaryReviewStatus::Approved { RPC_METHOD_GLOSSARY_APPROVE } else { RPC_METHOD_GLOSSARY_DEPRECATE }; let params = glossary_params(params, method)?; let term_id = glossary_string(¶ms, "term_id", method)? .ok_or_else(|| glossary_invalid(method, "缺少 term_id"))?; let reviewer = glossary_string(¶ms, "reviewer", method)? .ok_or_else(|| glossary_invalid(method, "缺少 reviewer"))?; let reason = glossary_string(¶ms, "reason", method)?.map(str::to_string); let path = glossary_rpc_path( state_dir, output_root, default_path, Some(&serde_json::Value::Object(params.clone())), method, )?; build_glossary_review_report(&path, term_id, status, reviewer, reason) .map_err(|error| glossary_internal_error(method, error)) } pub(super) fn glossary_delete_rpc_report( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, ) -> Result { let params = glossary_params(params, RPC_METHOD_GLOSSARY_DELETE)?; let term_id = glossary_string(¶ms, "term_id", RPC_METHOD_GLOSSARY_DELETE)? .ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 term_id"))?; let reviewer = glossary_string(¶ms, "reviewer", RPC_METHOD_GLOSSARY_DELETE)? .ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reviewer"))?; let reason = glossary_string(¶ms, "reason", RPC_METHOD_GLOSSARY_DELETE)? .ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reason"))?; let path = glossary_rpc_path( state_dir, output_root, default_path, Some(&serde_json::Value::Object(params.clone())), RPC_METHOD_GLOSSARY_DELETE, )?; build_glossary_delete_report(&path, term_id, reviewer, reason) .map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_DELETE, error)) } fn glossary_rpc_path( state_dir: &Path, output_root: &Path, default_path: Option<&Path>, params: Option<&serde_json::Value>, method: &'static str, ) -> Result { if let Some(path) = params .and_then(|value| value.get("glossary_path")) .and_then(serde_json::Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) { return lexical_absolute(Path::new(path)) .map_err(|error| glossary_internal_error(method, anyhow::anyhow!(error))); } if let Some(path) = default_path { return lexical_absolute(path) .map_err(|error| glossary_internal_error(method, anyhow::anyhow!(error))); } let (_, version_state) = read_daemon_resource_state(state_dir) .map_err(|error| glossary_internal_error(method, error))?; if let Some(record) = version_state .as_ref() .and_then(|state| state.current_completed_version.as_ref()) { return Ok(SqliteGlossaryRepository::repository_path( &record.resource_root, )); } Ok(SqliteGlossaryRepository::repository_path(output_root)) } fn glossary_params( params: Option<&serde_json::Value>, method: &'static str, ) -> Result, ApiError> { match params { None | Some(serde_json::Value::Null) => Ok(serde_json::Map::new()), Some(serde_json::Value::Object(value)) => Ok(value.clone()), Some(_) => Err(glossary_invalid(method, "params 必须是 JSON object")), } } fn glossary_string<'a>( params: &'a serde_json::Map, key: &str, method: &'static str, ) -> Result, ApiError> { let Some(value) = params.get(key) else { return Ok(None); }; if value.is_null() { return Ok(None); } value .as_str() .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| glossary_invalid(method, format!("{key} 必须是非空字符串"))) .map(Some) } fn glossary_context( params: &serde_json::Map, method: &'static str, ) -> Result { let Some(value) = params .get("context") .or_else(|| params.get("source_context")) else { return Ok(BTreeMap::new()); }; serde_json::from_value(value.clone()).map_err(|error| { glossary_invalid(method, format!("context 必须是 JSON string map:{error}")) }) } fn glossary_limit( params: &serde_json::Map, method: &'static str, ) -> Result { let limit = params .get("limit") .and_then(serde_json::Value::as_u64) .unwrap_or(100); let limit = usize::try_from(limit) .map_err(|error| glossary_invalid(method, format!("limit 无效:{error}")))?; if !(1..=1000).contains(&limit) { return Err(glossary_invalid(method, "limit 必须在 1..=1000 范围内")); } Ok(limit) } fn glossary_invalid(method: &'static str, message: impl Into) -> ApiError { ApiError::new(ErrorCode::RPC_INVALID_PARAMS, method, message.into()) } fn glossary_internal_error(method: &'static str, error: anyhow::Error) -> ApiError { ApiError::new(ErrorCode::INTERNAL, method, error.to_string()) }