Files
hms/docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md

23 KiB
Raw Blame History

erp-ai Phase 1 MVP 实施计划

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 为 HMS 新建 erp-ai crate实现 AI 智能分析流 SSE 端点,支持化验单解读/趋势分析/个性化方案/报告摘要

Architecture: 新建独立 erp-ai crate通过 HealthDataProvider trait 从 erp-health 获取脱敏数据AiProvider trait 抽象 AI 提供商Phase 1 实现 Claude SSE请求驱动管道 + SSE 流式返回

Tech Stack: Rust/Axum/SeaORM/PostgreSQL + futures/tokio-stream/async-stream (SSE) + serde_json/uuid/chrono/thiserror/utoipa

设计规格: docs/superpowers/specs/2026-04-25-erp-ai-module-design.md


Chunk 1: Crate 骨架 + 错误类型 + erp-core 扩展

Task 1: 创建 erp-ai crate 骨架

Files:

  • Create: crates/erp-ai/Cargo.toml

  • Create: crates/erp-ai/src/lib.rs

  • Create: crates/erp-ai/src/error.rs

  • Modify: Cargo.toml (workspace root) — 添加 erp-ai 到 workspace

  • Step 1: 创建 crate 目录

mkdir -p crates/erp-ai/src
  • Step 2: 创建 Cargo.toml
# crates/erp-ai/Cargo.toml
[package]
name = "erp-ai"
version.workspace = true
edition.workspace = true

[dependencies]
erp-core.workspace = true
tokio = { workspace = true, features = ["full"] }
tokio-stream.workspace = true
futures.workspace = true
async-stream.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
chrono.workspace = true
axum.workspace = true
sea-orm.workspace = true
tracing.workspace = true
thiserror.workspace = true
utoipa.workspace = true
async-trait.workspace = true
reqwest = { version = "0.12", features = ["stream", "json"] }
handlebars = "6"
sha2 = "0.10"
hex = "0.4"

注意: futures, tokio-stream, async-stream, reqwest, handlebars, sha2 需要加入 workspace 依赖或在此声明版本。参照 crates/erp-health/Cargo.toml 模式。

  • Step 3: 创建 error.rs
// crates/erp-ai/src/error.rs
use erp_core::AppError;

#[derive(Debug, thiserror::Error)]
pub enum AiError {
    #[error("验证失败: {0}")]
    Validation(String),

    #[error("分析未找到: {0}")]
    AnalysisNotFound(String),

    #[error("Prompt 模板未找到: {0}")]
    PromptNotFound(String),

    #[error("AI 提供商不可用: {0}")]
    ProviderUnavailable(String),

    #[error("AI 提供商错误: {0}")]
    ProviderError(String),

    #[error("数据脱敏失败: {0}")]
    SanitizationError(String),

    #[error("模板渲染失败: {0}")]
    TemplateError(String),

    #[error("速率超限")]
    RateLimitExceeded,

    #[error("版本不匹配")]
    VersionMismatch,

    #[error("数据库错误: {0}")]
    DbError(String),
}

impl From<AiError> for AppError {
    fn from(e: AiError) -> Self {
        match e {
            AiError::Validation(msg) => AppError::Validation(msg),
            AiError::AnalysisNotFound(id) => AppError::NotFound(format!("分析结果: {id}")),
            AiError::PromptNotFound(name) => AppError::NotFound(format!("Prompt 模板: {name}")),
            AiError::ProviderUnavailable(p) => AppError::ServiceUnavailable(format!("AI 提供商 {p} 不可用")),
            AiError::RateLimitExceeded => AppError::TooManyRequests,
            AiError::VersionMismatch => AppError::VersionMismatch,
            AiError::DbError(msg) => AppError::Internal(msg),
            other => AppError::Internal(other.to_string()),
        }
    }
}

impl From<sea_orm::DbErr> for AiError {
    fn from(e: sea_orm::DbErr) -> Self {
        AiError::DbError(e.to_string())
    }
}

pub type AiResult<T> = Result<T, AiError>;

注意: 检查 AppError 是否有 ServiceUnavailable 变体。如果没有,使用 AppError::Internal 替代。

  • Step 4: 创建 lib.rs (最小骨架)
// crates/erp-ai/src/lib.rs
pub mod error;

pub use error::{AiError, AiResult};
  • Step 5: 注册到 workspace

在根 Cargo.toml[workspace] members 数组中添加 "crates/erp-ai",在 [workspace.dependencies] 中添加:

erp-ai = { path = "crates/erp-ai" }

同时确认以下依赖在 workspace dependencies 中存在(如不存在则添加):

futures = "0.3"
tokio-stream = "0.1"
async-stream = "0.3"
reqwest = { version = "0.12", features = ["stream", "json"] }
handlebars = "6"
sha2 = "0.10"
hex = "0.4"
  • Step 6: 验证编译
cargo check -p erp-ai

Expected: 编译通过,无错误

  • Step 7: 提交
git add crates/erp-ai/ Cargo.toml
git commit -m "feat(ai): 创建 erp-ai crate 骨架 + 错误类型"

Task 2: erp-core 扩展 — HealthDataProvider trait + AI 权限码 + 事件类型

Files:

  • Create: crates/erp-core/src/health_provider.rs — trait + DTO 定义
  • Modify: crates/erp-core/src/lib.rs — 添加 pub mod
  • Modify: crates/erp-health/src/health_provider_impl.rs — trait 实现 (stub)
  • Modify: crates/erp-health/src/lib.rs — 添加 pub mod
  • Modify: crates/erp-health/src/module.rs — permissions() 中声明 AI 权限

注意: AI 权限码放在 erp-ai 模块的 permissions() 中,不在 erp-health。此处仅做 erp-core 的 trait 扩展。

  • Step 1: 创建 HealthDataProvider trait + DTO
// crates/erp-core/src/health_provider.rs
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::AppResult;

/// 健康数据提供者 trait由 erp-health 实现
/// 返回的 DTO 已脱去 PII姓名、身份证号等只包含年龄/性别/医疗数据
#[async_trait]
pub trait HealthDataProvider: Send + Sync {
    /// 获取化验报告(指标列表)
    async fn get_lab_report(
        &self,
        tenant_id: Uuid,
        report_id: Uuid,
    ) -> AppResult<LabReportDto>;

    /// 获取生命体征趋势数据
    async fn get_vital_signs(
        &self,
        tenant_id: Uuid,
        patient_id: Uuid,
        metrics: &[String],
        range: &TimeRange,
    ) -> AppResult<Vec<VitalSignDto>>;

    /// 获取患者摘要(用于个性化方案)
    async fn get_patient_summary(
        &self,
        tenant_id: Uuid,
        patient_id: Uuid,
    ) -> AppResult<PatientSummaryDto>;

    /// 获取完整健康报告(用于摘要生成)
    async fn get_full_report(
        &self,
        tenant_id: Uuid,
        report_id: Uuid,
    ) -> AppResult<HealthReportDto>;
}

// === DTO 定义 ===

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeRange {
    pub start: chrono::DateTime<chrono::Utc>,
    pub end: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabReportDto {
    pub age_group: String,
    pub sex: String,
    pub department: String,
    pub report_date: String,
    pub items: Vec<LabItemDto>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabItemDto {
    pub name: String,
    pub value: f64,
    pub unit: String,
    pub reference_range: String,
    pub is_abnormal: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VitalSignDto {
    pub metric: String,
    pub values: Vec<(String, f64)>,
    pub unit: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatientSummaryDto {
    pub age_group: String,
    pub sex: String,
    pub chronic_conditions: Vec<String>,
    pub medications: Vec<String>,
    pub family_history: Vec<String>,
    pub last_checkup_date: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReportDto {
    pub age_group: String,
    pub sex: String,
    pub department: String,
    pub report_date: String,
    pub sections: Vec<ReportSectionDto>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportSectionDto {
    pub title: String,
    pub findings: Vec<String>,
    pub abnormal_items: Vec<String>,
}
  • Step 2: 在 erp-core/src/lib.rs 中添加 pub mod
pub mod health_provider;

并添加 re-export:

pub use health_provider::{
    HealthDataProvider, LabItemDto, LabReportDto, PatientSummaryDto,
    HealthReportDto, ReportSectionDto, TimeRange, VitalSignDto,
};
  • Step 3: 验证 erp-core 编译
cargo check -p erp-core
  • Step 4: 提交 erp-core 扩展
git add crates/erp-core/src/health_provider.rs crates/erp-core/src/lib.rs
git commit -m "feat(core): 新增 HealthDataProvider trait + DTO 定义"

Task 3: erp-health 实现 HealthDataProvider (stub)

Files:

  • Create: crates/erp-health/src/health_provider_impl.rs
  • Modify: crates/erp-health/src/lib.rs

注意: Phase 1 先创建 stub 实现(返回 todo! 或空数据),确保编译通过。实际数据查询在 Chunk 5 集成时完善。

  • Step 1: 创建 stub 实现
// crates/erp-health/src/health_provider_impl.rs
use async_trait::async_trait;
use erp_core::{
    AppResult, HealthDataProvider, LabReportDto, VitalSignDto,
    PatientSummaryDto, HealthReportDto, TimeRange,
};
use uuid::Uuid;

pub struct HealthDataProviderImpl {
    pub db: sea_orm::DatabaseConnection,
}

#[async_trait]
impl HealthDataProvider for HealthDataProviderImpl {
    async fn get_lab_report(
        &self,
        _tenant_id: Uuid,
        _report_id: Uuid,
    ) -> AppResult<LabReportDto> {
        todo!("Chunk 5: 实现化验报告数据查询")
    }

    async fn get_vital_signs(
        &self,
        _tenant_id: Uuid,
        _patient_id: Uuid,
        _metrics: &[String],
        _range: &TimeRange,
    ) -> AppResult<Vec<VitalSignDto>> {
        todo!("Chunk 5: 实现生命体征趋势查询")
    }

    async fn get_patient_summary(
        &self,
        _tenant_id: Uuid,
        _patient_id: Uuid,
    ) -> AppResult<PatientSummaryDto> {
        todo!("Chunk 5: 实现患者摘要查询")
    }

    async fn get_full_report(
        &self,
        _tenant_id: Uuid,
        _report_id: Uuid,
    ) -> AppResult<HealthReportDto> {
        todo!("Chunk 5: 实现完整报告查询")
    }
}
  • Step 2: 在 erp-health/src/lib.rs 添加 pub mod
pub mod health_provider_impl;
pub use health_provider_impl::HealthDataProviderImpl;
  • Step 3: 验证全 workspace 编译
cargo check --workspace

Expected: 编译通过stub 的 todo! 不影响编译)

  • Step 4: 提交
git add crates/erp-health/src/health_provider_impl.rs crates/erp-health/src/lib.rs
git commit -m "feat(health): 添加 HealthDataProvider stub 实现"

Chunk 2: 数据库迁移 + SeaORM Entity

Task 4: 创建 ai_prompts / ai_analysis_results / ai_usage_logs 迁移

Files:

  • Create: crates/erp-server/migration/src/m20260425_000050_create_ai_tables.rs
  • Modify: crates/erp-server/migration/src/lib.rs — 注册到 Migrator

先查看 migration/src/lib.rs 最后一个 migration 编号,确保 000050 不冲突。

  • Step 1: 创建迁移文件
// crates/erp-server/migration/src/m20260425_000050_create_ai_tables.rs
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> {
        // 1. ai_prompts — Prompt 模板存储
        manager.create_table(
            Table::create()
                .table(AiPrompt::Table)
                .if_not_exists()
                .col(ColumnDef::new(AiPrompt::Id).uuid().not_null().primary_key())
                .col(ColumnDef::new(AiPrompt::TenantId).uuid().not_null())
                .col(ColumnDef::new(AiPrompt::Name).string_len(100).not_null())
                .col(ColumnDef::new(AiPrompt::Description).text().not_null().default(""))
                .col(ColumnDef::new(AiPrompt::SystemPrompt).text().not_null())
                .col(ColumnDef::new(AiPrompt::UserPromptTemplate).text().not_null())
                .col(ColumnDef::new(AiPrompt::VariablesSchema).json().null())
                .col(ColumnDef::new(AiPrompt::ModelConfig).json().not_null())
                .col(ColumnDef::new(AiPrompt::Version).integer().not_null().default(1))
                .col(ColumnDef::new(AiPrompt::IsActive).boolean().not_null().default(true))
                .col(ColumnDef::new(AiPrompt::Category).string_len(50).not_null().default("analysis"))
                .col(ColumnDef::new(AiPrompt::Tags).json().null())
                .col(ColumnDef::new(AiPrompt::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                .col(ColumnDef::new(AiPrompt::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                .col(ColumnDef::new(AiPrompt::CreatedBy).uuid().null())
                .col(ColumnDef::new(AiPrompt::UpdatedBy).uuid().null())
                .col(ColumnDef::new(AiPrompt::DeletedAt).timestamp_with_time_zone().null())
                .col(ColumnDef::new(AiPrompt::VersionLock).integer().not_null().default(1))
                .to_owned(),
        ).await?;

        // ai_prompts 索引
        manager.create_index(
            Index::create()
                .if_not_exists()
                .name("idx_ai_prompts_tenant_name")
                .table(AiPrompt::Table)
                .col(AiPrompt::TenantId)
                .col(AiPrompt::Name)
                .col(AiPrompt::IsActive)
                .to_owned(),
        ).await?;

        // 2. ai_analysis_results — 分析结果存储
        manager.create_table(
            Table::create()
                .table(AiAnalysis::Table)
                .if_not_exists()
                .col(ColumnDef::new(AiAnalysis::Id).uuid().not_null().primary_key())
                .col(ColumnDef::new(AiAnalysis::TenantId).uuid().not_null())
                .col(ColumnDef::new(AiAnalysis::PatientId).uuid().not_null())
                .col(ColumnDef::new(AiAnalysis::AnalysisType).string_len(50).not_null())
                .col(ColumnDef::new(AiAnalysis::SourceRef).string_len(200).not_null())
                .col(ColumnDef::new(AiAnalysis::PromptId).uuid().not_null())
                .col(ColumnDef::new(AiAnalysis::PromptVersion).integer().not_null())
                .col(ColumnDef::new(AiAnalysis::ModelUsed).string_len(100).not_null())
                .col(ColumnDef::new(AiAnalysis::InputDataHash).string_len(64).not_null())
                .col(ColumnDef::new(AiAnalysis::SanitizedInput).json().null())
                .col(ColumnDef::new(AiAnalysis::ResultContent).text().null())
                .col(ColumnDef::new(AiAnalysis::ResultMetadata).json().null())
                .col(ColumnDef::new(AiAnalysis::Status).string_len(20).not_null().default("pending"))
                .col(ColumnDef::new(AiAnalysis::ErrorMessage).text().null())
                .col(ColumnDef::new(AiAnalysis::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                .col(ColumnDef::new(AiAnalysis::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                .col(ColumnDef::new(AiAnalysis::CreatedBy).uuid().null())
                .col(ColumnDef::new(AiAnalysis::UpdatedBy).uuid().null())
                .col(ColumnDef::new(AiAnalysis::DeletedAt).timestamp_with_time_zone().null())
                .col(ColumnDef::new(AiAnalysis::VersionLock).integer().not_null().default(1))
                .to_owned(),
        ).await?;

        // ai_analysis 索引
        manager.create_index(
            Index::create()
                .if_not_exists()
                .name("idx_ai_analysis_tenant_patient")
                .table(AiAnalysis::Table)
                .col(AiAnalysis::TenantId)
                .col(AiAnalysis::PatientId)
                .to_owned(),
        ).await?;

        manager.create_index(
            Index::create()
                .if_not_exists()
                .name("idx_ai_analysis_cache_hash")
                .table(AiAnalysis::Table)
                .col(AiAnalysis::InputDataHash)
                .to_owned(),
        ).await?;

        // 3. ai_usage_logs — AI 调用计量
        manager.create_table(
            Table::create()
                .table(AiUsage::Table)
                .if_not_exists()
                .col(ColumnDef::new(AiUsage::Id).uuid().not_null().primary_key())
                .col(ColumnDef::new(AiUsage::TenantId).uuid().not_null())
                .col(ColumnDef::new(AiUsage::Provider).string_len(50).not_null())
                .col(ColumnDef::new(AiUsage::Model).string_len(100).not_null())
                .col(ColumnDef::new(AiUsage::AnalysisType).string_len(50).not_null())
                .col(ColumnDef::new(AiUsage::InputTokens).integer().not_null().default(0))
                .col(ColumnDef::new(AiUsage::OutputTokens).integer().not_null().default(0))
                .col(ColumnDef::new(AiUsage::DurationMs).integer().not_null().default(0))
                .col(ColumnDef::new(AiUsage::CostCents).integer().not_null().default(0))
                .col(ColumnDef::new(AiUsage::IsCacheHit).boolean().not_null().default(false))
                .col(ColumnDef::new(AiUsage::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                .to_owned(),
        ).await?;

        // ai_usage 索引
        manager.create_index(
            Index::create()
                .if_not_exists()
                .name("idx_ai_usage_tenant_created")
                .table(AiUsage::Table)
                .col(AiUsage::TenantId)
                .col(AiUsage::CreatedAt)
                .to_owned(),
        ).await?;

        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager.drop_table(Table::drop().table(AiUsage::Table).to_owned()).await?;
        manager.drop_table(Table::drop().table(AiAnalysis::Table).to_owned()).await?;
        manager.drop_table(Table::drop().table(AiPrompt::Table).to_owned()).await?;
        Ok(())
    }
}

#[derive(DeriveIden)]
enum AiPrompt {
    Table,
    Id,
    TenantId,
    Name,
    Description,
    SystemPrompt,
    UserPromptTemplate,
    VariablesSchema,
    ModelConfig,
    Version,
    IsActive,
    Category,
    Tags,
    CreatedAt,
    UpdatedAt,
    CreatedBy,
    UpdatedBy,
    DeletedAt,
    VersionLock,
}

#[derive(DeriveIden)]
enum AiAnalysis {
    Table,
    Id,
    TenantId,
    PatientId,
    AnalysisType,
    SourceRef,
    PromptId,
    PromptVersion,
    ModelUsed,
    InputDataHash,
    SanitizedInput,
    ResultContent,
    ResultMetadata,
    Status,
    ErrorMessage,
    CreatedAt,
    UpdatedAt,
    CreatedBy,
    UpdatedBy,
    DeletedAt,
    VersionLock,
}

#[derive(DeriveIden)]
enum AiUsage {
    Table,
    Id,
    TenantId,
    Provider,
    Model,
    AnalysisType,
    InputTokens,
    OutputTokens,
    DurationMs,
    CostCents,
    IsCacheHit,
    CreatedAt,
}
  • Step 2: 在 migration/src/lib.rs 注册

Migrator::migrations()vec![] 末尾添加:

Box::new(m20260425_000050_create_ai_tables::Migration),
  • Step 3: 验证迁移编译
cargo check -p erp-server
  • Step 4: 提交
git add crates/erp-server/migration/src/
git commit -m "feat(db): 添加 ai_prompts / ai_analysis_results / ai_usage_logs 迁移"

Task 5: 创建 SeaORM Entity

Files:

  • Create: crates/erp-ai/src/entity/mod.rs

  • Create: crates/erp-ai/src/entity/ai_prompt.rs

  • Create: crates/erp-ai/src/entity/ai_analysis.rs

  • Create: crates/erp-ai/src/entity/ai_usage.rs

  • Step 1: 创建 entity/mod.rs

// crates/erp-ai/src/entity/mod.rs
pub mod ai_analysis;
pub mod ai_prompt;
pub mod ai_usage;
  • Step 2: 创建 ai_prompt.rs
// crates/erp-ai/src/entity/ai_prompt.rs
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "ai_prompts")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: Uuid,
    pub tenant_id: Uuid,
    pub name: String,
    pub description: String,
    pub system_prompt: String,
    pub user_prompt_template: String,
    pub variables_schema: Option<serde_json::Value>,
    pub model_config: serde_json::Value,
    pub version: i32,
    pub is_active: bool,
    pub category: String,
    pub tags: Option<serde_json::Value>,
    pub created_at: DateTimeUtc,
    pub updated_at: DateTimeUtc,
    pub created_by: Option<Uuid>,
    pub updated_by: Option<Uuid>,
    pub deleted_at: Option<DateTimeUtc>,
    pub version_lock: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
  • Step 3: 创建 ai_analysis.rs
// crates/erp-ai/src/entity/ai_analysis.rs
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "ai_analysis_results")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: Uuid,
    pub tenant_id: Uuid,
    pub patient_id: Uuid,
    pub analysis_type: String,
    pub source_ref: String,
    pub prompt_id: Uuid,
    pub prompt_version: i32,
    pub model_used: String,
    pub input_data_hash: String,
    pub sanitized_input: Option<serde_json::Value>,
    pub result_content: Option<String>,
    pub result_metadata: Option<serde_json::Value>,
    pub status: String,
    pub error_message: Option<String>,
    pub created_at: DateTimeUtc,
    pub updated_at: DateTimeUtc,
    pub created_by: Option<Uuid>,
    pub updated_by: Option<Uuid>,
    pub deleted_at: Option<DateTimeUtc>,
    pub version_lock: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
  • Step 4: 创建 ai_usage.rs
// crates/erp-ai/src/entity/ai_usage.rs
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "ai_usage_logs")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: Uuid,
    pub tenant_id: Uuid,
    pub provider: String,
    pub model: String,
    pub analysis_type: String,
    pub input_tokens: i32,
    pub output_tokens: i32,
    pub duration_ms: i32,
    pub cost_cents: i32,
    pub is_cache_hit: bool,
    pub created_at: DateTimeUtc,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
  • Step 5: 更新 lib.rs 添加 entity mod

crates/erp-ai/src/lib.rs 中添加:

pub mod entity;
  • Step 6: 验证编译
cargo check -p erp-ai
  • Step 7: 提交
git add crates/erp-ai/src/entity/ crates/erp-ai/src/lib.rs
git commit -m "feat(ai): 添加 SeaORM Entity (ai_prompt/ai_analysis/ai_usage)"