mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 11:56:23 +08:00
feat(glossary): 实现 Rust Glossary V1
This commit is contained in:
@@ -0,0 +1,649 @@
|
||||
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(),
|
||||
)?
|
||||
}
|
||||
_ => 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::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));
|
||||
}
|
||||
}
|
||||
_ => 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 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))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user