feat(ai): Phase 3A RAG 知识库 — CRUD API + Agent Tool + 向量知识源 + 前端管理页

- 知识库 REST API: 10 个端点 (references/guides CRUD + re-embed)
- search_medical_knowledge Agent Tool: 语义检索参考资料和临床指南
- VectorKnowledgeSource: 实现 KnowledgeSource trait,自动降级
- 沙箱配置: Patient/MedicalStaff 允许使用知识库检索
- 前端 AiKnowledgePage: Tabs(参考资料/临床指南) + Table + Modal CRUD
- 权限码 seed 迁移: ai.knowledge.list + ai.knowledge.manage + 菜单
This commit is contained in:
iven
2026-05-19 09:10:53 +08:00
parent c0570dfbfc
commit 8b88cb4a50
18 changed files with 1389 additions and 5 deletions

View File

@@ -155,6 +155,7 @@ mod m20260518_000150_seed_ai_config_permission;
mod m20260518_000151_fix_ai_config_menu_parent;
mod m20260518_000152_seed_ai_provider_permission;
mod m20260518_000153_ai_health_butler_v2;
mod m20260519_000154_seed_ai_knowledge_permissions;
pub struct Migrator;
@@ -317,6 +318,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260518_000151_fix_ai_config_menu_parent::Migration),
Box::new(m20260518_000152_seed_ai_provider_permission::Migration),
Box::new(m20260518_000153_ai_health_butler_v2::Migration),
Box::new(m20260519_000154_seed_ai_knowledge_permissions::Migration),
]
}
}

View File

@@ -0,0 +1,87 @@
//! AI 知识库权限码 seed + 菜单项
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> {
let db = manager.get_connection();
let sys = "00000000-0000-0000-0000-000000000000";
// 1. Seed 知识库权限码
let perms = [
(
"ai.knowledge.list",
"查看知识库",
"查看 AI 知识库(参考资料和临床指南)",
),
(
"ai.knowledge.manage",
"管理知识库",
"创建/编辑/删除 AI 知识库条目",
),
];
for (code, name, desc) in &perms {
db.execute_unprepared(&format!(
r#"
INSERT INTO permissions (id, tenant_id, code, name, resource, action, description,
created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id, '{code}', '{name}', 'ai', '{code}', '{desc}',
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM permissions p
WHERE p.code = '{code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL
)
"#
)).await?;
// 绑定到管理员角色
db.execute_unprepared(&format!(
r#"
INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope,
created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT r.id, p.id, t.id, 'all',
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
JOIN roles r ON r.tenant_id = t.id AND r.code = 'admin' AND r.deleted_at IS NULL
JOIN permissions p ON p.tenant_id = t.id AND p.code = '{code}' AND p.deleted_at IS NULL
WHERE NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id AND rp.deleted_at IS NULL
)
ON CONFLICT (role_id, permission_id) DO NOTHING
"#
)).await?;
}
// 2. 添加知识库菜单项AI 配置下方)
db.execute_unprepared(&format!(
r#"
INSERT INTO menus (id, tenant_id, parent_id, name, path, icon, sort_order,
permission, menu_type, is_external, status,
created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id,
(SELECT m.id FROM menus m WHERE m.path = '/health/ai-config' AND m.tenant_id = t.id AND m.deleted_at IS NULL LIMIT 1),
'AI 知识库', '/health/ai-knowledge', 'BookOutlined', 4,
'ai.knowledge.list', 1, false, 1,
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM menus m
WHERE m.path = '/health/ai-knowledge' AND m.tenant_id = t.id AND m.deleted_at IS NULL
)
"#
)).await?;
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Ok(())
}
}