feat(ai): 知识库 V2 菜单迁移 + 文本切片器 + 前端路由权限
- 新增迁移 000168:在 AI 知识库同级添加「知识库 V2」菜单,绑定 admin 角色 - 新增 document/chunker.rs:固定大小 + overlap 文本切片器(5 单元测试) - 前端 routeConfig 添加 /health/ai-knowledge-v2 权限声明 - App.tsx validateRouteCoverage 补充 v2 路径
This commit is contained in:
@@ -258,7 +258,7 @@ export default function App() {
|
||||
"/health/follow-up-records", "/health/consultations",
|
||||
"/health/points-rules", "/health/points-products", "/health/points-orders",
|
||||
"/health/offline-events", "/health/ai-prompts", "/health/ai-analysis",
|
||||
"/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/alerts", "/health/alert-dashboard",
|
||||
"/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/ai-knowledge-v2", "/health/alerts", "/health/alert-dashboard",
|
||||
"/ai/chat",
|
||||
"/health/alert-rules", "/health/devices", "/health/realtime-monitor",
|
||||
"/health/oauth-clients", "/health/dialysis", "/health/action-inbox",
|
||||
|
||||
@@ -151,6 +151,10 @@ const ENTRIES: RoutePermissionEntry[] = [
|
||||
path: "/health/ai-knowledge",
|
||||
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
|
||||
},
|
||||
{
|
||||
path: "/health/ai-knowledge-v2",
|
||||
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
|
||||
},
|
||||
|
||||
// ===== 健康管理 — 积分商城 =====
|
||||
{
|
||||
|
||||
87
crates/erp-ai/src/service/document/chunker.rs
Normal file
87
crates/erp-ai/src/service/document/chunker.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
/// 文本切片:按固定大小 + 重叠切分
|
||||
pub fn chunk_text(text: &str, chunk_size: usize, overlap: usize) -> Vec<String> {
|
||||
if text.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let total = chars.len();
|
||||
|
||||
if total <= chunk_size {
|
||||
return vec![text.to_string()];
|
||||
}
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
let mut start = 0;
|
||||
|
||||
while start < total {
|
||||
let end = (start + chunk_size).min(total);
|
||||
let chunk: String = chars[start..end].iter().collect();
|
||||
|
||||
let trimmed = chunk.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
chunks.push(trimmed);
|
||||
}
|
||||
|
||||
if end >= total {
|
||||
break;
|
||||
}
|
||||
start += chunk_size.saturating_sub(overlap);
|
||||
|
||||
// 防止无限循环
|
||||
if start <= end - chunk_size && start > 0 {
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
chunks
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chunk_empty() {
|
||||
assert_eq!(chunk_text("", 100, 20), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_small_text() {
|
||||
let text = "hello world";
|
||||
let chunks = chunk_text(text, 100, 20);
|
||||
assert_eq!(chunks.len(), 1);
|
||||
assert_eq!(chunks[0], "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_long_text() {
|
||||
let text = "abcdefghij".repeat(100); // 1000 chars
|
||||
let chunks = chunk_text(&text, 200, 50);
|
||||
assert!(chunks.len() > 1);
|
||||
// First chunk should be 200 chars
|
||||
assert_eq!(chars_count(&chunks[0]), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_with_overlap() {
|
||||
let text = "abcdefghijklmnopqrstuvwxyz".repeat(20); // 520 chars
|
||||
let chunks = chunk_text(&text, 100, 20);
|
||||
assert!(chunks.len() > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_chinese() {
|
||||
let text = "你好世界这是一段中文测试文本。".repeat(30);
|
||||
let chunks = chunk_text(&text, 100, 20);
|
||||
assert!(chunks.len() > 1);
|
||||
// 确保中文不被截断
|
||||
for chunk in &chunks {
|
||||
assert!(!chunk.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
fn chars_count(s: &str) -> usize {
|
||||
s.chars().count()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod chat_session;
|
||||
pub mod comparison;
|
||||
pub mod cost;
|
||||
pub mod dialysis_risk_scorer;
|
||||
pub mod document;
|
||||
pub mod embedding;
|
||||
pub mod feature_flag_service;
|
||||
pub mod insight_service;
|
||||
|
||||
@@ -174,6 +174,7 @@ mod m20260526_000164_ai_prompt_add_analysis_type;
|
||||
mod m20260526_000165_ai_prompt_fix_analysis_type;
|
||||
mod m20260526_000166_create_ai_knowledge_bases;
|
||||
mod m20260526_000167_create_ai_knowledge_documents;
|
||||
mod m20260527_000168_ai_knowledge_v2_menu;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -355,6 +356,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260526_000165_ai_prompt_fix_analysis_type::Migration),
|
||||
Box::new(m20260526_000166_create_ai_knowledge_bases::Migration),
|
||||
Box::new(m20260526_000167_create_ai_knowledge_documents::Migration),
|
||||
Box::new(m20260527_000168_ai_knowledge_v2_menu::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 在"AI 知识库"同级位置添加"知识库 V2"菜单
|
||||
// parent_id 取旧 ai-knowledge 菜单的 parent_id
|
||||
let sql = r#"
|
||||
INSERT INTO sys_menu (id, parent_id, name, path, icon, sort, permission, component, is_external, is_cached, status, visible, created_at, updated_at, deleted_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
parent_id,
|
||||
'知识库 V2',
|
||||
'/health/ai-knowledge-v2',
|
||||
'DatabaseOutlined',
|
||||
54,
|
||||
'ai.knowledge.list',
|
||||
'ai/KnowledgeV2Page',
|
||||
false,
|
||||
false,
|
||||
'active',
|
||||
true,
|
||||
now(),
|
||||
now(),
|
||||
NULL
|
||||
FROM sys_menu
|
||||
WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
ON CONFLICT DO NOTHING
|
||||
"#;
|
||||
|
||||
manager.get_connection().execute_unprepared(sql).await?;
|
||||
|
||||
// 绑定到 admin 角色
|
||||
let role_sql = r#"
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT r.id, m.id
|
||||
FROM sys_role r, sys_menu m
|
||||
WHERE r.code = 'admin' AND r.deleted_at IS NULL
|
||||
AND m.path = '/health/ai-knowledge-v2' AND m.deleted_at IS NULL
|
||||
ON CONFLICT DO NOTHING
|
||||
"#;
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(role_sql)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("DELETE FROM sys_menu WHERE path = '/health/ai-knowledge-v2'")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user