diff --git a/crates/erp-ai/src/dto/copilot.rs b/crates/erp-ai/src/dto/copilot.rs index f3b26e2..eabdfa2 100644 --- a/crates/erp-ai/src/dto/copilot.rs +++ b/crates/erp-ai/src/dto/copilot.rs @@ -27,7 +27,7 @@ pub enum RiskLevel { Critical, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct CreateRuleBody { pub name: String, pub category: String, @@ -39,7 +39,7 @@ pub struct CreateRuleBody { pub sort_order: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct UpdateRuleBody { pub name: Option, pub category: Option, diff --git a/crates/erp-ai/src/handler/insight_handler.rs b/crates/erp-ai/src/handler/insight_handler.rs index ab0392d..7b38784 100644 --- a/crates/erp-ai/src/handler/insight_handler.rs +++ b/crates/erp-ai/src/handler/insight_handler.rs @@ -8,6 +8,13 @@ use serde::Deserialize; use crate::dto::copilot::ListInsightsQuery; use crate::state::AiState; +#[utoipa::path( + get, + path = "/copilot/insights", + responses((status = 200, description = "Copilot 洞察列表")), + tag = "Copilot 洞察", + security(("bearer_auth" = [])), +)] pub async fn list_insights( State(state): State, Extension(ctx): Extension, @@ -41,6 +48,13 @@ where })))) } +#[utoipa::path( + get, + path = "/copilot/insights/{id}", + responses((status = 200, description = "洞察详情")), + tag = "Copilot 洞察", + security(("bearer_auth" = [])), +)] pub async fn get_insight( State(state): State, Extension(ctx): Extension, @@ -68,6 +82,13 @@ pub struct DismissBody { pub reason: Option, } +#[utoipa::path( + post, + path = "/copilot/insights/{id}/dismiss", + responses((status = 200, description = "忽略洞察")), + tag = "Copilot 洞察", + security(("bearer_auth" = [])), +)] pub async fn dismiss_insight( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-ai/src/handler/mod.rs b/crates/erp-ai/src/handler/mod.rs index 1f0653f..33a3449 100644 --- a/crates/erp-ai/src/handler/mod.rs +++ b/crates/erp-ai/src/handler/mod.rs @@ -18,7 +18,7 @@ pub mod suggestion_handler; // === 分析请求 Body === -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct AnalyzeBody { pub report_id: Option, pub patient_id: Option, @@ -27,6 +27,14 @@ pub struct AnalyzeBody { // === SSE 分析端点 === +#[utoipa::path( + post, + path = "/ai/analyze/lab-report", + request_body = AnalyzeBody, + responses((status = 200, description = "SSE 化验报告分析流")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn stream_lab_report( State(state): State, Extension(ctx): Extension, @@ -101,6 +109,14 @@ where Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default())) } +#[utoipa::path( + post, + path = "/ai/analyze/trends", + request_body = AnalyzeBody, + responses((status = 200, description = "SSE 趋势分析流")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn stream_trends( State(state): State, Extension(ctx): Extension, @@ -190,6 +206,14 @@ where Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default())) } +#[utoipa::path( + post, + path = "/ai/analyze/checkup-plan", + request_body = AnalyzeBody, + responses((status = 200, description = "SSE 体检计划分析流")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn stream_checkup_plan( State(state): State, Extension(ctx): Extension, @@ -258,6 +282,14 @@ where Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default())) } +#[utoipa::path( + post, + path = "/ai/analyze/report-summary", + request_body = AnalyzeBody, + responses((status = 200, description = "SSE 报告摘要分析流")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn stream_report_summary( State(state): State, Extension(ctx): Extension, @@ -335,7 +367,7 @@ where // === 分析历史 === -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::IntoParams)] pub struct ListAnalysisQuery { pub patient_id: Option, pub analysis_type: Option, @@ -343,6 +375,14 @@ pub struct ListAnalysisQuery { pub page_size: Option, } +#[utoipa::path( + get, + path = "/ai/analysis/history", + params(ListAnalysisQuery), + responses((status = 200, description = "分析历史列表")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn list_analysis( State(state): State, Extension(ctx): Extension, @@ -419,6 +459,13 @@ where })))) } +#[utoipa::path( + get, + path = "/ai/analysis/{id}", + responses((status = 200, description = "分析详情")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn get_analysis( State(state): State, Extension(ctx): Extension, @@ -435,13 +482,21 @@ where // === Prompt 管理 === -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::IntoParams)] pub struct ListPromptsQuery { pub category: Option, pub page: Option, pub page_size: Option, } +#[utoipa::path( + get, + path = "/ai/prompts", + params(ListPromptsQuery), + responses((status = 200, description = "Prompt 模板列表")), + tag = "AI Prompt", + security(("bearer_auth" = [])), +)] pub async fn list_prompts( State(state): State, Extension(ctx): Extension, @@ -468,7 +523,7 @@ where })))) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct CreatePromptBody { pub name: String, pub description: Option, @@ -478,6 +533,14 @@ pub struct CreatePromptBody { pub category: String, } +#[utoipa::path( + post, + path = "/ai/prompts", + request_body = CreatePromptBody, + responses((status = 200, description = "创建 Prompt 模板")), + tag = "AI Prompt", + security(("bearer_auth" = [])), +)] pub async fn create_prompt( State(state): State, Extension(ctx): Extension, @@ -505,6 +568,13 @@ where Ok(Json(ApiResponse::ok(prompt))) } +#[utoipa::path( + post, + path = "/ai/prompts/{id}/activate", + responses((status = 200, description = "激活 Prompt 模板")), + tag = "AI Prompt", + security(("bearer_auth" = [])), +)] pub async fn activate_prompt( State(state): State, Extension(ctx): Extension, @@ -519,6 +589,13 @@ where Ok(Json(ApiResponse::ok(prompt))) } +#[utoipa::path( + post, + path = "/ai/prompts/{id}/rollback", + responses((status = 200, description = "回滚 Prompt 模板")), + tag = "AI Prompt", + security(("bearer_auth" = [])), +)] pub async fn rollback_prompt( State(state): State, Extension(ctx): Extension, @@ -535,6 +612,13 @@ where // === 用量统计 === +#[utoipa::path( + get, + path = "/ai/usage/overview", + responses((status = 200, description = "AI 用量概览")), + tag = "AI 用量", + security(("bearer_auth" = [])), +)] pub async fn usage_overview( State(state): State, Extension(ctx): Extension, @@ -550,6 +634,13 @@ where })))) } +#[utoipa::path( + get, + path = "/ai/usage/by-type", + responses((status = 200, description = "按类型用量统计")), + tag = "AI 用量", + security(("bearer_auth" = [])), +)] pub async fn usage_by_type( State(state): State, Extension(ctx): Extension, @@ -574,6 +665,13 @@ where // === Provider 管理 === +#[utoipa::path( + get, + path = "/ai/providers/health", + responses((status = 200, description = "AI Provider 健康检查")), + tag = "AI Provider", + security(("bearer_auth" = [])), +)] pub async fn provider_health( State(state): State, Extension(ctx): Extension, @@ -602,6 +700,13 @@ where Ok(Json(ApiResponse::ok(result))) } +#[utoipa::path( + get, + path = "/ai/providers", + responses((status = 200, description = "AI Provider 列表")), + tag = "AI Provider", + security(("bearer_auth" = [])), +)] pub async fn provider_names( State(state): State, Extension(ctx): Extension, @@ -616,6 +721,13 @@ where ))) } +#[utoipa::path( + get, + path = "/ai/quota/summary", + responses((status = 200, description = "AI 配额汇总")), + tag = "AI 用量", + security(("bearer_auth" = [])), +)] pub async fn quota_summary( State(state): State, Extension(ctx): Extension, @@ -631,6 +743,13 @@ where // === 透析风险评估(KDIGO 规则) === +#[utoipa::path( + post, + path = "/ai/dialysis/risk-assessment", + responses((status = 200, description = "透析风险评估")), + tag = "AI 分析", + security(("bearer_auth" = [])), +)] pub async fn assess_dialysis_risk( Extension(ctx): Extension, Json(body): Json, @@ -650,6 +769,13 @@ where // === 成本与预算 === +#[utoipa::path( + get, + path = "/ai/budget/status", + responses((status = 200, description = "AI 预算状态")), + tag = "AI 用量", + security(("bearer_auth" = [])), +)] pub async fn budget_status( State(state): State, Extension(ctx): Extension, @@ -670,6 +796,13 @@ pub struct CostEstimateQuery { pub model: Option, } +#[utoipa::path( + get, + path = "/ai/cost/estimate", + responses((status = 200, description = "AI 成本预估")), + tag = "AI 用量", + security(("bearer_auth" = [])), +)] pub async fn cost_estimate( Extension(ctx): Extension, Query(params): Query, diff --git a/crates/erp-ai/src/handler/risk_handler.rs b/crates/erp-ai/src/handler/risk_handler.rs index 0dd87ac..df39038 100644 --- a/crates/erp-ai/src/handler/risk_handler.rs +++ b/crates/erp-ai/src/handler/risk_handler.rs @@ -5,6 +5,13 @@ use erp_core::types::{ApiResponse, TenantContext}; use crate::state::AiState; +#[utoipa::path( + get, + path = "/copilot/patients/{id}/risk", + responses((status = 200, description = "患者风险评分")), + tag = "Copilot 风险", + security(("bearer_auth" = [])), +)] pub async fn get_patient_risk( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-ai/src/handler/rule_handler.rs b/crates/erp-ai/src/handler/rule_handler.rs index 5f2eed7..e6024be 100644 --- a/crates/erp-ai/src/handler/rule_handler.rs +++ b/crates/erp-ai/src/handler/rule_handler.rs @@ -8,6 +8,13 @@ use crate::dto::copilot::{CreateRuleBody, UpdateRuleBody}; use crate::entity::copilot_rules; use crate::state::AiState; +#[utoipa::path( + get, + path = "/copilot/rules", + responses((status = 200, description = "Copilot 规则列表")), + tag = "Copilot 规则", + security(("bearer_auth" = [])), +)] pub async fn list_rules( State(state): State, Extension(ctx): Extension, @@ -40,6 +47,13 @@ where })))) } +#[utoipa::path( + post, + path = "/copilot/rules", + responses((status = 200, description = "创建 Copilot 规则")), + tag = "Copilot 规则", + security(("bearer_auth" = [])), +)] pub async fn create_rule( State(state): State, Extension(ctx): Extension, @@ -75,6 +89,13 @@ where Ok(Json(ApiResponse::ok(result))) } +#[utoipa::path( + put, + path = "/copilot/rules/{id}", + responses((status = 200, description = "更新 Copilot 规则")), + tag = "Copilot 规则", + security(("bearer_auth" = [])), +)] pub async fn update_rule( State(state): State, Extension(ctx): Extension, @@ -128,6 +149,13 @@ where Ok(Json(ApiResponse::ok(result))) } +#[utoipa::path( + delete, + path = "/copilot/rules/{id}", + responses((status = 200, description = "删除 Copilot 规则")), + tag = "Copilot 规则", + security(("bearer_auth" = [])), +)] pub async fn delete_rule( State(state): State, Extension(ctx): Extension, @@ -151,6 +179,7 @@ where active.deleted_at = Set(Some(chrono::Utc::now())); active.updated_at = Set(chrono::Utc::now()); active.updated_by = Set(Some(ctx.user_id)); + active.version_lock = Set(active.version_lock.unwrap() + 1); active.update(&state.db).await?; Ok(Json(ApiResponse::ok(serde_json::json!({"deleted": true})))) diff --git a/crates/erp-ai/src/handler/suggestion_handler.rs b/crates/erp-ai/src/handler/suggestion_handler.rs index 9238988..f0692b5 100644 --- a/crates/erp-ai/src/handler/suggestion_handler.rs +++ b/crates/erp-ai/src/handler/suggestion_handler.rs @@ -14,6 +14,13 @@ pub struct ListSuggestionsQuery { pub status: Option, } +#[utoipa::path( + get, + path = "/ai/suggestions", + responses((status = 200, description = "AI 建议列表")), + tag = "AI 建议", + security(("bearer_auth" = [])), +)] pub async fn list_suggestions( State(state): State, Extension(ctx): Extension, @@ -41,11 +48,18 @@ where } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct ApproveBody { pub action: String, // "approve" or "reject" } +#[utoipa::path( + post, + path = "/ai/suggestions/{id}/approve", + responses((status = 200, description = "审批 AI 建议")), + tag = "AI 建议", + security(("bearer_auth" = [])), +)] pub async fn approve_suggestion( State(state): State, Extension(ctx): Extension, @@ -86,12 +100,19 @@ where })))) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct ExecuteBody { pub action_result: Option, } /// 执行建议:护士标记建议为已执行,可选记录执行结果 +#[utoipa::path( + post, + path = "/ai/suggestions/{id}/execute", + responses((status = 200, description = "执行 AI 建议")), + tag = "AI 建议", + security(("bearer_auth" = [])), +)] pub async fn execute_suggestion( State(state): State, Extension(ctx): Extension, @@ -123,6 +144,13 @@ where } /// 获取 AI 建议的前后对比报告。 +#[utoipa::path( + get, + path = "/ai/suggestions/{id}/comparison", + responses((status = 200, description = "AI 建议对比")), + tag = "AI 建议", + security(("bearer_auth" = [])), +)] pub async fn get_comparison( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-ai/src/service/dialysis_risk_scorer.rs b/crates/erp-ai/src/service/dialysis_risk_scorer.rs index 1870eb9..64438ab 100644 --- a/crates/erp-ai/src/service/dialysis_risk_scorer.rs +++ b/crates/erp-ai/src/service/dialysis_risk_scorer.rs @@ -2,7 +2,7 @@ use crate::dto::suggestion::{RiskLevel, StructuredSuggestion, SuggestionType}; use crate::service::local_rules::{CompareOp, LocalRule, LocalRulesEngine}; /// 透析患者实验室指标输入 -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] pub struct DialysisLabInput { /// Kt/V(透析充分性指标) pub kt_v: Option, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c3fe5bf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,800 @@ +{ + "name": "erp", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "erp", + "devDependencies": { + "js-yaml": "^4.1.1", + "lint-staged": "^15.0.0", + "simple-git-hooks": "^2.12.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git-hooks": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/simple-git-hooks/-/simple-git-hooks-2.13.1.tgz", + "integrity": "sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "simple-git-hooks": "cli.js" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json index ba20364..7d2b813 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "pre-commit": "npx lint-staged" }, "devDependencies": { + "js-yaml": "^4.1.1", "lint-staged": "^15.0.0", "simple-git-hooks": "^2.12.0" } diff --git a/permissions.yaml b/permissions.yaml new file mode 100644 index 0000000..5a36f50 --- /dev/null +++ b/permissions.yaml @@ -0,0 +1,317 @@ +# HMS 权限注册表 — 单一真相源 +# +# 此文件是权限码的权威来源。所有模块的权限必须在此声明。 +# CI 脚本 check-permissions.sh 从此文件验证一致性。 +# +# 用法: +# - 新增权限: 在对应模块下添加条目 +# - 生成 seed: node scripts/gen-permissions.js --seed +# - 验证一致: bash scripts/check-permissions.sh + +auth: + module: erp-auth + description: 用户/角色/权限/组织/部门/岗位 + permissions: + - code: user.list + name: 查看用户列表 + - code: user.create + name: 创建用户 + - code: user.read + name: 查看用户详情 + - code: user.update + name: 编辑用户 + - code: user.delete + name: 删除用户 + - code: role.list + name: 查看角色列表 + - code: role.create + name: 创建角色 + - code: role.read + name: 查看角色详情 + - code: role.update + name: 编辑角色 + - code: role.delete + name: 删除角色 + - code: permission.list + name: 查看权限 + - code: organization.list + name: 查看组织列表 + - code: organization.create + name: 创建组织 + - code: organization.update + name: 编辑组织 + - code: organization.delete + name: 删除组织 + - code: department.list + name: 查看部门列表 + - code: department.create + name: 创建部门 + - code: department.update + name: 编辑部门 + - code: department.delete + name: 删除部门 + - code: position.list + name: 查看岗位列表 + - code: position.create + name: 创建岗位 + - code: position.update + name: 编辑岗位 + - code: position.delete + name: 删除岗位 + +config: + module: erp-config + description: 字典/菜单/配置/编号/主题/语言 + permissions: + - code: dictionary.list + name: 查看字典 + - code: dictionary.create + name: 创建字典 + - code: dictionary.update + name: 编辑字典 + - code: dictionary.delete + name: 删除字典 + - code: menu.list + name: 查看菜单 + - code: menu.update + name: 编辑菜单 + - code: setting.read + name: 查看配置 + - code: setting.update + name: 编辑配置 + - code: setting.delete + name: 删除配置 + - code: numbering.list + name: 查看编号规则 + - code: numbering.create + name: 创建编号规则 + - code: numbering.update + name: 编辑编号规则 + - code: numbering.delete + name: 删除编号规则 + - code: numbering.generate + name: 生成编号 + - code: theme.read + name: 查看主题 + - code: theme.update + name: 编辑主题 + - code: language.list + name: 查看语言 + - code: language.update + name: 编辑语言 + +workflow: + module: erp-workflow + description: 流程定义/审批/委派 + permissions: + - code: workflow.create + name: 创建流程 + - code: workflow.list + name: 查看流程 + - code: workflow.read + name: 查看流程详情 + - code: workflow.update + name: 编辑流程 + - code: workflow.publish + name: 发布流程 + - code: workflow.start + name: 发起流程 + - code: workflow.approve + name: 审批任务 + - code: workflow.delegate + name: 委派任务 + +message: + module: erp-message + description: 消息/模板 + permissions: + - code: message.list + name: 查看消息 + - code: message.send + name: 发送消息 + - code: message.template.list + name: 查看消息模板 + - code: message.template.create + name: 创建消息模板 + - code: message.template.manage + name: 管理消息模板 + +plugin: + module: erp-plugin + description: 插件管理 + permissions: + - code: plugin.admin + name: 插件管理 + - code: plugin.list + name: 查看插件 + +health: + module: erp-health + description: 患者管理/健康数据/预约排班/随访/咨询/告警/设备/积分/内容/媒体 + permissions: + - code: health.patient.list + name: 查看患者列表 + - code: health.patient.manage + name: 管理患者 + - code: health.health-data.list + name: 查看健康数据 + - code: health.health-data.manage + name: 管理健康数据 + - code: health.appointment.list + name: 查看预约 + - code: health.appointment.manage + name: 管理预约 + - code: health.follow-up.list + name: 查看随访 + - code: health.follow-up.manage + name: 管理随访 + - code: health.consultation.list + name: 查看咨询 + - code: health.consultation.manage + name: 管理咨询 + - code: health.doctor.list + name: 查看医护 + - code: health.doctor.manage + name: 管理医护 + - code: health.articles.list + name: 查看资讯 + - code: health.articles.manage + name: 管理资讯 + - code: health.articles.review + name: 审核资讯 + - code: health.points.list + name: 查看积分 + - code: health.points.manage + name: 管理积分 + - code: health.device-readings.list + name: 查看设备数据 + - code: health.device-readings.manage + name: 管理设备数据 + - code: health.devices.list + name: 查看设备绑定 + - code: health.devices.manage + name: 管理设备绑定 + - code: health.alerts.list + name: 查看告警 + - code: health.alerts.manage + name: 管理告警 + - code: health.alert-rules.list + name: 查看告警规则 + - code: health.alert-rules.manage + name: 管理告警规则 + - code: health.critical-alerts.list + name: 查看危急值告警 + - code: health.critical-alerts.manage + name: 处理危急值告警 + - code: health.critical-value-thresholds.list + name: 查看危急值阈值 + - code: health.critical-value-thresholds.manage + name: 管理危急值阈值 + - code: health.follow-up-templates.list + name: 查看随访模板 + - code: health.follow-up-templates.manage + name: 管理随访模板 + - code: health.daily-monitoring.list + name: 查看日常监测 + - code: health.daily-monitoring.manage + name: 管理日常监测 + - code: health.consent.list + name: 查看知情同意 + - code: health.consent.manage + name: 管理知情同意 + - code: health.medication-records.list + name: 查看用药记录 + - code: health.medication-records.manage + name: 管理用药记录 + - code: health.medication-reminders.list + name: 查看药物提醒 + - code: health.medication-reminders.manage + name: 管理药物提醒 + - code: health.action-inbox.list + name: 查看行动收件箱 + - code: health.action-inbox.manage + name: 管理行动项 + - code: health.action-inbox.team + name: 查看团队概览 + - code: health.dashboard.manage + name: 工作台管理 + - code: health.oauth.list + name: 查看合作方 + - code: health.oauth.manage + name: 管理合作方 + - code: health.care-plan.list + name: 查看护理计划 + frozen: true + - code: health.care-plan.manage + name: 管理护理计划 + frozen: true + - code: health.shifts.list + name: 查看班次 + frozen: true + - code: health.shifts.manage + name: 管理班次 + frozen: true + - code: health.ble-gateways.list + name: 查看 BLE 网关 + - code: health.ble-gateways.manage + name: 管理 BLE 网关 + - code: health.family-proxy.list + name: 查看家庭健康代理 + frozen: true + - code: health.family-proxy.manage + name: 管理家庭健康代理 + frozen: true + - code: health.media.list + name: 查看媒体库 + - code: health.media.manage + name: 管理媒体库 + - code: health.banners.list + name: 查看轮播图 + - code: health.banners.manage + name: 管理轮播图 + +ai: + module: erp-ai + description: AI 分析/Prompt/Copilot + permissions: + - code: ai.analysis.list + name: 查看分析历史 + - code: ai.analysis.manage + name: 请求分析 + - code: ai.prompt.list + name: 查看 Prompt + - code: ai.prompt.manage + name: 管理 Prompt + - code: ai.usage.list + name: 查看用量 + - code: ai.provider.manage + name: 管理提供商 + - code: ai.suggestion.list + name: 查看 AI 建议 + - code: ai.suggestion.manage + name: 审批 AI 建议 + - code: copilot.insights.list + name: 查看 Copilot 洞察 + - code: copilot.insights.manage + name: 管理 Copilot 洞察 + - code: copilot.risk.view + name: 查看风险评分 + - code: copilot.rules.list + name: 查看 Copilot 规则 + - code: copilot.rules.manage + name: 管理 Copilot 规则 + +dialysis: + module: erp-dialysis + description: 透析管理 + permissions: + - code: health.dialysis.list + name: 查看透析记录 + - code: health.dialysis.manage + name: 管理透析记录 + - code: health.dialysis-prescription.list + name: 查看透析处方 + - code: health.dialysis-prescription.manage + name: 管理透析处方 + - code: health.dialysis.stats + name: 查看透析统计 diff --git a/scripts/gen-permissions.js b/scripts/gen-permissions.js new file mode 100644 index 0000000..92dd78a --- /dev/null +++ b/scripts/gen-permissions.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node +// gen-permissions.js — 从 permissions.yaml 生成 seed SQL 和前端 routeConfig 片段 +// +// 用法: +// node scripts/gen-permissions.js --sql 输出 seed INSERT SQL +// node scripts/gen-permissions.js --frontend 输出 routeConfig.ts 条目 +// node scripts/gen-permissions.js --validate 验证与代码一致性 + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const YAML_PATH = path.resolve(__dirname, '..', 'permissions.yaml'); + +function loadPermissions() { + const raw = fs.readFileSync(YAML_PATH, 'utf-8'); + return yaml.load(raw); +} + +function getAllCodes(registry) { + const codes = []; + for (const [, group] of Object.entries(registry)) { + for (const perm of group.permissions) { + codes.push(perm.code); + } + } + return codes; +} + +function generateSeedSQL(registry) { + const sys = '00000000-0000-0000-0000-000000000000'; + const lines = [ + '-- Auto-generated from permissions.yaml by gen-permissions.js', + `-- Generated: ${new Date().toISOString().slice(0, 10)}`, + '', + ]; + + for (const [groupKey, group] of Object.entries(registry)) { + lines.push(`-- ${groupKey}: ${group.description}`); + for (const perm of group.permissions) { + const parts = perm.code.split('.'); + const resource = parts.length >= 2 ? parts[0] : groupKey; + const action = parts.slice(1).join('.'); + lines.push( + `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, '${perm.code}', '${perm.name}', '${resource}', '${action}', '${perm.name}',` + + ` NOW(), NOW(), '${sys}', '${sys}', NULL, 1` + + ` FROM tenant t` + + ` WHERE NOT EXISTS (SELECT 1 FROM permissions p WHERE p.code = '${perm.code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL)` + + ` ON CONFLICT DO NOTHING;` + ); + } + lines.push(''); + } + return lines.join('\n'); +} + +function generateFrontendSnippet(registry) { + const lines = ['// Auto-generated from permissions.yaml by gen-permissions.js', '']; + + for (const [, group] of Object.entries(registry)) { + const frozenPerms = group.permissions.filter(p => p.frozen); + const activePerms = group.permissions.filter(p => !p.frozen); + + // Group by entity prefix (e.g., health.patient → {list, manage}) + const entities = {}; + for (const perm of [...activePerms, ...frozenPerms]) { + const parts = perm.code.split('.'); + if (parts.length < 2) continue; + const entity = parts.slice(0, -1).join('.'); + const action = parts[parts.length - 1]; + if (!entities[entity]) entities[entity] = { list: [], frozen: perm.frozen || false }; + entities[entity].list.push(perm.code); + } + + for (const [entity, info] of Object.entries(entities)) { + const frozenAttr = info.frozen ? ',\n frozen: true' : ''; + lines.push(` {`); + lines.push(` path: "/${entity.replace(/\./g, '/')}",`); + lines.push(` permissions: [${info.list.map(c => `"${c}"`).join(', ')}]${frozenAttr}`); + lines.push(` },`); + } + } + + return lines.join('\n'); +} + +function validate(registry) { + const yamlCodes = getAllCodes(registry); + const rootDir = path.resolve(__dirname, '..'); + + // Recursively find .rs files and extract permission codes + const backendCodes = new Set(); + function walkDir(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'target') { + walkDir(full); + } else if (entry.isFile() && entry.name.endsWith('.rs')) { + const content = fs.readFileSync(full, 'utf-8'); + // Match code: "xxx.yyy.zzz" pattern in PermissionDescriptor + // Must be lowercase letters/digits/hyphens with dots (permission code pattern) + const matches = content.matchAll(/code:\s*"([a-z][a-z0-9-]*\.[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*)"/g); + for (const m of matches) { + backendCodes.add(m[1]); + } + } + } + } + walkDir(path.join(rootDir, 'crates')); + + let errors = 0; + + // Check YAML covers all backend codes + for (const code of backendCodes) { + if (!yamlCodes.includes(code)) { + console.log(`MISSING: Backend '${code}' not in permissions.yaml`); + errors++; + } + } + + // Check backend covers all YAML codes + for (const code of yamlCodes) { + if (!backendCodes.has(code)) { + console.log(`MISSING: YAML '${code}' not in backend module.rs`); + errors++; + } + } + + if (errors === 0) { + console.log(`OK: ${yamlCodes.length} YAML / ${backendCodes.size} backend — 0 mismatches`); + } + return errors; +} + +// Main +const args = process.argv.slice(2); +const registry = loadPermissions(); + +if (args.includes('--sql')) { + console.log(generateSeedSQL(registry)); +} else if (args.includes('--frontend')) { + console.log(generateFrontendSnippet(registry)); +} else if (args.includes('--validate')) { + const errors = validate(registry); + process.exit(errors > 0 ? 1 : 0); +} else { + const codes = getAllCodes(registry); + console.log(`permissions.yaml loaded: ${codes.length} permission codes across ${Object.keys(registry).length} modules`); + console.log('Usage:'); + console.log(' node scripts/gen-permissions.js --sql Generate seed SQL'); + console.log(' node scripts/gen-permissions.js --frontend Generate routeConfig snippet'); + console.log(' node scripts/gen-permissions.js --validate Validate consistency'); +}