feat(ai): 数据脱敏服务 + Prompt 模板渲染引擎
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
pub mod dto;
|
||||
pub mod entity;
|
||||
pub mod error;
|
||||
pub mod prompt;
|
||||
pub mod provider;
|
||||
pub mod sanitization;
|
||||
|
||||
pub use error::{AiError, AiResult};
|
||||
|
||||
25
crates/erp-ai/src/prompt/mod.rs
Normal file
25
crates/erp-ai/src/prompt/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use handlebars::Handlebars;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::{AiError, AiResult};
|
||||
|
||||
/// Prompt 模板渲染引擎
|
||||
pub struct PromptRenderer {
|
||||
registry: Handlebars<'static>,
|
||||
}
|
||||
|
||||
impl PromptRenderer {
|
||||
pub fn new() -> Self {
|
||||
let mut registry = Handlebars::new();
|
||||
registry.set_strict_mode(true);
|
||||
Self { registry }
|
||||
}
|
||||
|
||||
/// 渲染 Prompt 模板 — 使用 Handlebars {{variable}} 语法
|
||||
/// JSON 序列化注入,不做字符串拼接,防止 Prompt 注入
|
||||
pub fn render(&self, template: &str, data: &Value) -> AiResult<String> {
|
||||
self.registry
|
||||
.render_template(template, data)
|
||||
.map_err(|e| AiError::TemplateError(format!("模板渲染失败: {e}")))
|
||||
}
|
||||
}
|
||||
67
crates/erp-ai/src/sanitization/mod.rs
Normal file
67
crates/erp-ai/src/sanitization/mod.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use erp_core::health_provider::{
|
||||
HealthReportDto, LabReportDto, PatientSummaryDto, VitalSignDto,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::{AiError, AiResult};
|
||||
|
||||
/// 数据脱敏服务 — 确保发送给 AI 的数据不含 PII
|
||||
/// HealthDataProvider 返回的 DTO 已经是脱敏的(只有年龄/性别/医疗数据)
|
||||
/// 此服务做二次检查和安全约束注入
|
||||
pub struct SanitizationService;
|
||||
|
||||
impl SanitizationService {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn sanitize_lab_report(&self, report: &LabReportDto) -> AiResult<Value> {
|
||||
let sanitized = serde_json::to_value(report)
|
||||
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||
self.verify_no_pii(&sanitized)?;
|
||||
Ok(sanitized)
|
||||
}
|
||||
|
||||
pub fn sanitize_vital_signs(&self, signs: &[VitalSignDto]) -> AiResult<Value> {
|
||||
let sanitized = serde_json::to_value(signs)
|
||||
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||
self.verify_no_pii(&sanitized)?;
|
||||
Ok(sanitized)
|
||||
}
|
||||
|
||||
pub fn sanitize_patient_summary(&self, summary: &PatientSummaryDto) -> AiResult<Value> {
|
||||
let sanitized = serde_json::to_value(summary)
|
||||
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||
self.verify_no_pii(&sanitized)?;
|
||||
Ok(sanitized)
|
||||
}
|
||||
|
||||
pub fn sanitize_health_report(&self, report: &HealthReportDto) -> AiResult<Value> {
|
||||
let sanitized = serde_json::to_value(report)
|
||||
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||
self.verify_no_pii(&sanitized)?;
|
||||
Ok(sanitized)
|
||||
}
|
||||
|
||||
/// 二次验证: 确保没有意外泄漏的 PII
|
||||
fn verify_no_pii(&self, value: &Value) -> AiResult<()> {
|
||||
let pii_keys = [
|
||||
"name",
|
||||
"phone",
|
||||
"id_number",
|
||||
"address",
|
||||
"birth_date",
|
||||
"email",
|
||||
];
|
||||
if let Value::Object(map) = value {
|
||||
for key in pii_keys {
|
||||
if map.contains_key(key) {
|
||||
return Err(AiError::SanitizationError(format!(
|
||||
"检测到疑似 PII 字段: {key}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user