Files
BlueArchiveToolkit/infrastructure/src/bin/bat/glossary_query.rs
T
nyaKazuha 0275a890bc
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
fix(glossary): 补齐术语删除和审核门禁
2026-09-08 20:59:27 +08:00

729 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(&params, "source_text", RPC_METHOD_GLOSSARY_QUERY)?;
let category = glossary_string(&params, "category", RPC_METHOD_GLOSSARY_QUERY)?;
let review_status = glossary_string(&params, "review_status", RPC_METHOD_GLOSSARY_QUERY)?;
let limit = glossary_limit(&params, 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(&params, "source_text", RPC_METHOD_GLOSSARY_DIAGNOSE)?
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DIAGNOSE, "缺少 source_text"))?;
let context = glossary_context(&params, 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(&params, "reviewer", method)?;
if update && reviewer.is_none() {
return Err(glossary_invalid(method, "update 缺少 reviewer"));
}
let reason = glossary_string(&params, "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(&params, "term_id", method)?
.ok_or_else(|| glossary_invalid(method, "缺少 term_id"))?;
let reviewer = glossary_string(&params, "reviewer", method)?
.ok_or_else(|| glossary_invalid(method, "缺少 reviewer"))?;
let reason = glossary_string(&params, "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(&params, "term_id", RPC_METHOD_GLOSSARY_DELETE)?
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 term_id"))?;
let reviewer = glossary_string(&params, "reviewer", RPC_METHOD_GLOSSARY_DELETE)?
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reviewer"))?;
let reason = glossary_string(&params, "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())
}