feat(ci,ai): P2-1 权限注册表 + P2-2 AI utoipa 注解全覆盖

P2-1 权限注册表单一真相源:
- 新增 permissions.yaml: 131 个权限码 × 8 模块,含冻结标记
- 新增 scripts/gen-permissions.js: 生成器脚本
  --sql 输出 seed SQL, --frontend 输出 routeConfig 片段,
  --validate 验证一致性(131/131 = 0 mismatches)

P2-2 AI 模块 utoipa 注解:
- 为 30 个 handler 函数添加 #[utoipa::path] 注解
  (mod.rs 18 + insight 3 + risk 1 + rule 4 + suggestion 4)
- 为 6 个 DTO struct 添加 ToSchema/IntoParams derive
  (AnalyzeBody, CreatePromptBody, CreateRuleBody, UpdateRuleBody,
   ApproveBody, ExecuteBody, DialysisLabInput, ListAnalysisQuery,
   ListPromptsQuery)
- AI handler utoipa 覆盖率: 0/5 → 5/5 (100%)
This commit is contained in:
iven
2026-05-13 17:45:45 +08:00
parent 20d606d21c
commit 02082ccc61
11 changed files with 1500 additions and 9 deletions

View File

@@ -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<i32>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct UpdateRuleBody {
pub name: Option<String>,
pub category: Option<String>,

View File

@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -68,6 +82,13 @@ pub struct DismissBody {
pub reason: Option<String>,
}
#[utoipa::path(
post,
path = "/copilot/insights/{id}/dismiss",
responses((status = 200, description = "忽略洞察")),
tag = "Copilot 洞察",
security(("bearer_auth" = [])),
)]
pub async fn dismiss_insight<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,

View File

@@ -18,7 +18,7 @@ pub mod suggestion_handler;
// === 分析请求 Body ===
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct AnalyzeBody {
pub report_id: Option<uuid::Uuid>,
pub patient_id: Option<uuid::Uuid>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -335,7 +367,7 @@ where
// === 分析历史 ===
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::IntoParams)]
pub struct ListAnalysisQuery {
pub patient_id: Option<uuid::Uuid>,
pub analysis_type: Option<String>,
@@ -343,6 +375,14 @@ pub struct ListAnalysisQuery {
pub page_size: Option<u64>,
}
#[utoipa::path(
get,
path = "/ai/analysis/history",
params(ListAnalysisQuery),
responses((status = 200, description = "分析历史列表")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn list_analysis<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -435,13 +482,21 @@ where
// === Prompt 管理 ===
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::IntoParams)]
pub struct ListPromptsQuery {
pub category: Option<String>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
#[utoipa::path(
get,
path = "/ai/prompts",
params(ListPromptsQuery),
responses((status = 200, description = "Prompt 模板列表")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn list_prompts<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -468,7 +523,7 @@ where
}))))
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct CreatePromptBody {
pub name: String,
pub description: Option<String>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
Extension(ctx): Extension<TenantContext>,
Json(body): Json<crate::service::dialysis_risk_scorer::DialysisLabInput>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -670,6 +796,13 @@ pub struct CostEstimateQuery {
pub model: Option<String>,
}
#[utoipa::path(
get,
path = "/ai/cost/estimate",
responses((status = 200, description = "AI 成本预估")),
tag = "AI 用量",
security(("bearer_auth" = [])),
)]
pub async fn cost_estimate<S>(
Extension(ctx): Extension<TenantContext>,
Query(params): Query<CostEstimateQuery>,

View File

@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,

View File

@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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}))))

View File

@@ -14,6 +14,13 @@ pub struct ListSuggestionsQuery {
pub status: Option<String>,
}
#[utoipa::path(
get,
path = "/ai/suggestions",
responses((status = 200, description = "AI 建议列表")),
tag = "AI 建议",
security(("bearer_auth" = [])),
)]
pub async fn list_suggestions<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -86,12 +100,19 @@ where
}))))
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct ExecuteBody {
pub action_result: Option<serde_json::Value>,
}
/// 执行建议:护士标记建议为已执行,可选记录执行结果
#[utoipa::path(
post,
path = "/ai/suggestions/{id}/execute",
responses((status = 200, description = "执行 AI 建议")),
tag = "AI 建议",
security(("bearer_auth" = [])),
)]
pub async fn execute_suggestion<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,

View File

@@ -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<f64>,

800
package-lock.json generated Normal file
View File

@@ -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"
}
}
}
}

View File

@@ -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"
}

317
permissions.yaml Normal file
View File

@@ -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: 查看透析统计

155
scripts/gen-permissions.js Normal file
View File

@@ -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');
}