mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:00:40 +08:00
729 lines
27 KiB
Rust
729 lines
27 KiB
Rust
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<std::path::PathBuf> {
|
||
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<Option<serde_json::Value>> {
|
||
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<GlossaryTermDraft> {
|
||
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<String> {
|
||
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<Vec<String>> {
|
||
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<TranslationMemoryContext> {
|
||
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<serde_json::Value> {
|
||
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<serde_json::Value> {
|
||
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<serde_json::Value> {
|
||
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<String>,
|
||
) -> anyhow::Result<serde_json::Value> {
|
||
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<String>,
|
||
) -> anyhow::Result<serde_json::Value> {
|
||
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<serde_json::Value> {
|
||
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<serde_json::Value, ApiError>,
|
||
) -> 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<serde_json::Value, ApiError> {
|
||
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<serde_json::Value, ApiError> {
|
||
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<serde_json::Value, ApiError> {
|
||
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<serde_json::Value, ApiError> {
|
||
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<serde_json::Value, ApiError> {
|
||
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<serde_json::Value, ApiError> {
|
||
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<std::path::PathBuf, ApiError> {
|
||
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<serde_json::Map<String, serde_json::Value>, 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<String, serde_json::Value>,
|
||
key: &str,
|
||
method: &'static str,
|
||
) -> Result<Option<&'a str>, 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<String, serde_json::Value>,
|
||
method: &'static str,
|
||
) -> Result<TranslationMemoryContext, ApiError> {
|
||
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<String, serde_json::Value>,
|
||
method: &'static str,
|
||
) -> Result<usize, ApiError> {
|
||
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<String>) -> 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())
|
||
}
|