功能修复: 1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查 2. 仪表盘统计容错:单个查询失败返回零值而非 500 3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致 4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径 5. 积分端点权限码:health.health-data.list → health.points.list 6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage 7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档 Clippy 全 workspace 清零(14→0 errors): - erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处 - erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处 - erp-ai: 修复 dead_code、unused import 等 11 处 - erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处 - erp-server-migration: 修复 enum_variant_names 5 处 - erp-auth/config/workflow/message: 各 1-3 处 工程改进: - lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy) - cargo fmt 统一格式化
82 lines
3.0 KiB
Rust
82 lines
3.0 KiB
Rust
use crate::dto::suggestion::{ParsedOutput, StructuredOutput};
|
|
use crate::error::AiResult;
|
|
|
|
const TEXT_MARKER: &str = "===PATIENT_TEXT===";
|
|
const JSON_MARKER: &str = "===STRUCTURED_JSON===";
|
|
|
|
/// 解析 AI 双通道输出。JSON 解析失败时降级为纯文本。
|
|
pub fn parse_dual_channel(raw: &str) -> AiResult<ParsedOutput> {
|
|
let text_content = extract_section(raw, TEXT_MARKER, JSON_MARKER)
|
|
.unwrap_or(raw)
|
|
.trim()
|
|
.to_string();
|
|
|
|
let structured = extract_section(raw, JSON_MARKER, TEXT_MARKER).and_then(|json_str| {
|
|
let parsed: Result<StructuredOutput, _> = serde_json::from_str(json_str.trim());
|
|
parsed.ok()
|
|
});
|
|
|
|
Ok(ParsedOutput {
|
|
text_content,
|
|
structured,
|
|
})
|
|
}
|
|
|
|
fn extract_section<'a>(raw: &'a str, start: &str, end: &str) -> Option<&'a str> {
|
|
let start_idx = raw.find(start)?;
|
|
let content_start = start_idx + start.len();
|
|
let content_end = raw[content_start..]
|
|
.find(end)
|
|
.map(|i| content_start + i)
|
|
.unwrap_or(raw.len());
|
|
Some(&raw[content_start..content_end])
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::dto::suggestion::RiskLevel;
|
|
|
|
#[test]
|
|
fn parse_dual_channel_output_success() {
|
|
let raw = "===PATIENT_TEXT===\n张三的收缩压呈上升趋势\n===STRUCTURED_JSON===\n{\"risk_level\":\"medium\",\"risk_factors\":[\"收缩压偏高\"],\"suggestions\":[{\"type\":\"followup\",\"priority\":1,\"timing\":\"14天内\",\"reason\":\"血压异常\",\"params\":{},\"auto_executable\":false}],\"baseline_summary\":{}}";
|
|
let result = parse_dual_channel(raw).unwrap();
|
|
assert_eq!(result.text_content, "张三的收缩压呈上升趋势");
|
|
assert!(result.structured.is_some());
|
|
let s = result.structured.unwrap();
|
|
assert_eq!(s.risk_level, RiskLevel::Medium);
|
|
assert_eq!(s.suggestions.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_text_only_fallback() {
|
|
let raw = "纯文本分析结果,没有结构化部分";
|
|
let result = parse_dual_channel(raw).unwrap();
|
|
assert_eq!(result.text_content, "纯文本分析结果,没有结构化部分");
|
|
assert!(result.structured.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_invalid_json_falls_back() {
|
|
let raw = "===PATIENT_TEXT===\n分析内容\n===STRUCTURED_JSON===\n{invalid json}";
|
|
let result = parse_dual_channel(raw).unwrap();
|
|
assert_eq!(result.text_content, "分析内容");
|
|
assert!(result.structured.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn empty_suggestions_is_valid() {
|
|
let raw = "===PATIENT_TEXT===\n指标正常\n===STRUCTURED_JSON===\n{\"risk_level\":\"low\",\"risk_factors\":[],\"suggestions\":[],\"baseline_summary\":{}}";
|
|
let result = parse_dual_channel(raw).unwrap();
|
|
let s = result.structured.unwrap();
|
|
assert!(s.suggestions.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn risk_level_auto_executable() {
|
|
assert!(RiskLevel::Low.is_auto_executable());
|
|
assert!(!RiskLevel::Medium.is_auto_executable());
|
|
assert!(!RiskLevel::High.is_auto_executable());
|
|
}
|
|
}
|