feat(server): erp-ai 模块集成 — Config/State/路由注册

- 新增 AiConfig 到 AppConfig
- 新增 FromRef<AppState> for AiState
- 注册 AiModule 到 ModuleRegistry
- 合并 AI protected routes
- 修复 sync_module_permissions 只同步 health.% 的 bug

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-25 14:01:07 +08:00
parent fada33101c
commit 2e555ca72a
5 changed files with 64 additions and 2 deletions

View File

@@ -28,6 +28,7 @@ erp-workflow.workspace = true
erp-message.workspace = true
erp-plugin.workspace = true
erp-health.workspace = true
erp-ai.workspace = true
anyhow.workspace = true
uuid.workspace = true
chrono.workspace = true

View File

@@ -32,3 +32,13 @@ secret = "__MUST_SET_VIA_ENV__"
[health]
aes_key = "__MUST_SET_VIA_ENV__"
hmac_key = "__MUST_SET_VIA_ENV__"
[ai]
default_provider = "claude"
api_key = ""
base_url = "https://api.anthropic.com"
model = "claude-sonnet-4-6"
max_tokens = 2048
temperature = 0.3
cache_ttl_seconds = 604800
rate_limit_patient_daily = 10

View File

@@ -11,6 +11,7 @@ pub struct AppConfig {
pub cors: CorsConfig,
pub wechat: WechatConfig,
pub health: HealthConfig,
pub ai: AiConfig,
}
#[derive(Debug, Clone, Deserialize)]
@@ -69,6 +70,18 @@ pub struct HealthConfig {
pub hmac_key: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AiConfig {
pub default_provider: String,
pub api_key: String,
pub base_url: Option<String>,
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
pub cache_ttl_seconds: u64,
pub rate_limit_patient_daily: u32,
}
impl AppConfig {
pub fn load() -> anyhow::Result<Self> {
let config = config::Config::builder()

View File

@@ -343,13 +343,22 @@ async fn main() -> anyhow::Result<()> {
"Health module initialized"
);
// Initialize AI module
let ai_module = erp_ai::AiModule;
tracing::info!(
module = ai_module.name(),
version = ai_module.version(),
"AI module initialized"
);
// Initialize module registry and register modules
let registry = ModuleRegistry::new()
.register(auth_module)
.register(config_module)
.register(workflow_module)
.register(message_module)
.register(health_module);
.register(health_module)
.register(ai_module);
tracing::info!(
module_count = registry.modules().len(),
"Modules registered"
@@ -464,6 +473,7 @@ async fn main() -> anyhow::Result<()> {
.merge(erp_message::MessageModule::protected_routes())
.merge(erp_plugin::module::PluginModule::protected_routes())
.merge(erp_health::HealthModule::protected_routes())
.merge(erp_ai::AiModule::protected_routes())
.merge(handlers::audit_log::audit_log_router())
.layer(axum::middleware::from_fn_with_state(
state.clone(),
@@ -627,7 +637,7 @@ async fn sync_module_permissions(
SELECT r.id, p.id, p.tenant_id, 'all', NOW(), NOW(), $1, $1, NULL, 1
FROM permissions p
JOIN roles r ON r.code = 'admin' AND r.tenant_id = p.tenant_id AND r.deleted_at IS NULL
WHERE p.tenant_id = $2 AND p.code LIKE 'health.%'
WHERE p.tenant_id = $2
ON CONFLICT DO NOTHING"#,
[system_user_id.into(), tenant_id.into()],
)).await?;

View File

@@ -115,3 +115,31 @@ impl FromRef<AppState> for erp_health::HealthState {
}
}
}
/// Allow erp-ai handlers to extract their required state.
impl FromRef<AppState> for erp_ai::AiState {
fn from_ref(state: &AppState) -> Self {
let mut provider = erp_ai::provider::claude::ClaudeProvider::new(
state.config.ai.api_key.clone(),
);
if let Some(ref base_url) = state.config.ai.base_url {
provider = provider.with_base_url(base_url.clone());
}
let db = state.db.clone();
let event_bus = state.event_bus.clone();
let analysis = std::sync::Arc::new(
erp_ai::service::analysis::AnalysisService::new(Box::new(provider), db.clone()),
);
let prompt = std::sync::Arc::new(erp_ai::service::prompt::PromptService::new(db.clone()));
let usage = std::sync::Arc::new(erp_ai::service::usage::UsageService::new(db.clone()));
Self {
db,
event_bus,
analysis,
prompt,
usage,
}
}
}