fix(src): 从版本库移除 node_modules_old 依赖缓存污染
此前提交误将 src/frontend-h5/node_modules_old(11071 个依赖缓存文件)纳入版本控制。 本次基于已净化的索引生成新树, 彻底剔除该污染: - .gitignore 新增 **/node_modules_*/ 排除规则, 防止再次误入 - .gitignore 根 data/ 锚定为 /data/, 避免误伤 src/.../data/ 真实源码 - 补入被误伤源码: seed_quiz.py / seed_rbac.py / qrData.ts
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 测验题目种子数据
|
||||
# =============================================================================
|
||||
# 说明:启动时调用,如果 quiz_questions 表为空:
|
||||
# 1. 调用 Dify API 生成 70 道知识题(7 类 × 10 题),is_active=True
|
||||
# 2. 插入手写诊断模板(7 个,每类 1 个),is_active=True
|
||||
#
|
||||
# 降级策略:Dify 不可用时记录错误,不阻塞启动
|
||||
# 管理员后续可通过 POST /api/admin/quiz/generate 手动触发生成
|
||||
#
|
||||
# 幂等性:已有数据时跳过(不重复插入)
|
||||
# =============================================================================
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.quiz import QuizQuestion
|
||||
from app.models.diagnostic import DiagnosticTemplate
|
||||
from app.services.quiz_generation_service import get_quiz_generation_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 7 个类别,每个类别生成 10 道知识题
|
||||
SEED_CATEGORIES = ["network", "vpn", "email", "system", "printer", "security", "office"]
|
||||
SEED_QUESTIONS_PER_CATEGORY = 10
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 手写诊断模板种子数据(7 个,每类 1 个)
|
||||
# 这些模板包含 PowerShell 脚本,需精确控制,不由 AI 生成
|
||||
# =============================================================================
|
||||
|
||||
SEED_DIAGNOSTIC_TEMPLATES: List[Dict[str, Any]] = [
|
||||
{
|
||||
"category": "network",
|
||||
"name": "网关连通性检测",
|
||||
"check_type": "script",
|
||||
"script_template": (
|
||||
"# 检测默认网关是否可达\n"
|
||||
"$gateway = (Get-NetRoute -DestinationPrefix '0.0.0.0/0' | "
|
||||
"Select-Object -First 1).NextHop\n"
|
||||
"if ($gateway) {\n"
|
||||
" $result = Test-Connection -ComputerName $gateway -Count 4 -Quiet\n"
|
||||
" Write-Output \"gateway=$gateway;reachable=$result\"\n"
|
||||
"} else {\n"
|
||||
" Write-Output \"gateway=none;reachable=false\"\n"
|
||||
"}"
|
||||
),
|
||||
"fix_template": (
|
||||
"# 释放并重新获取IP地址\n"
|
||||
"ipconfig /release\n"
|
||||
"Start-Sleep -Seconds 2\n"
|
||||
"ipconfig /renew\n"
|
||||
"ipconfig /flushdns"
|
||||
),
|
||||
"fix_risk_level": "low",
|
||||
"target_condition": "network_connectivity=fail",
|
||||
"description": "检测本地网关是否可达,不通时尝试重新获取IP",
|
||||
},
|
||||
{
|
||||
"category": "vpn",
|
||||
"name": "VPN进程状态检测",
|
||||
"check_type": "script",
|
||||
"script_template": (
|
||||
"# 检测VPN客户端进程是否运行\n"
|
||||
"$vpnProcesses = Get-Process | "
|
||||
"Where-Object {$_.Name -match 'vpn|aTrust|SangforConnect'}\n"
|
||||
"if ($vpnProcesses) {\n"
|
||||
" Write-Output \"vpn_process=running;name=$($vpnProcesses.Name -join ',')\"\n"
|
||||
"} else {\n"
|
||||
" Write-Output \"vpn_process=not_found\"\n"
|
||||
"}"
|
||||
),
|
||||
"fix_template": (
|
||||
"# 重启VPN客户端\n"
|
||||
"Stop-Process -Name 'aTrust*' -Force -ErrorAction SilentlyContinue\n"
|
||||
"Start-Sleep -Seconds 3\n"
|
||||
"Start-Process 'C:\\Program Files\\aTrust\\aTrust.exe' "
|
||||
"-ErrorAction SilentlyContinue"
|
||||
),
|
||||
"fix_risk_level": "medium",
|
||||
"target_condition": "vpn_process=not_found",
|
||||
"description": "检测VPN/aTrust客户端进程是否运行",
|
||||
},
|
||||
{
|
||||
"category": "system",
|
||||
"name": "磁盘空间检测",
|
||||
"check_type": "script",
|
||||
"script_template": (
|
||||
"# 检测各磁盘分区可用空间\n"
|
||||
"Get-WmiObject Win32_LogicalDisk | "
|
||||
"ForEach-Object {\n"
|
||||
" $freeGB = [math]::Round($_.FreeSpace / 1GB, 1)\n"
|
||||
" Write-Output \"disk=$($_.DeviceID);free_gb=$freeGB\"\n"
|
||||
"}"
|
||||
),
|
||||
"fix_template": (
|
||||
"# 清理临时文件\n"
|
||||
"Remove-Item -Path \"$env:TEMP\\*\" -Recurse -Force "
|
||||
"-ErrorAction SilentlyContinue\n"
|
||||
"Cleanmgr /autoclean"
|
||||
),
|
||||
"fix_risk_level": "low",
|
||||
"target_condition": "disk_free<5GB",
|
||||
"description": "检测各磁盘分区可用空间,低于5GB时清理临时文件",
|
||||
},
|
||||
{
|
||||
"category": "printer",
|
||||
"name": "打印后台服务检测",
|
||||
"check_type": "script",
|
||||
"script_template": (
|
||||
"# 检测Windows打印后台服务状态\n"
|
||||
"$spooler = Get-Service -Name Spooler -ErrorAction SilentlyContinue\n"
|
||||
"if ($spooler) {\n"
|
||||
" Write-Output \"print_spooler=$($spooler.Status)\"\n"
|
||||
"} else {\n"
|
||||
" Write-Output \"print_spooler=not_found\"\n"
|
||||
"}"
|
||||
),
|
||||
"fix_template": (
|
||||
"# 重启打印后台服务\n"
|
||||
"Stop-Service -Name Spooler -Force\n"
|
||||
"Start-Sleep -Seconds 2\n"
|
||||
"Start-Service -Name Spooler"
|
||||
),
|
||||
"fix_risk_level": "medium",
|
||||
"target_condition": "print_spooler=stopped",
|
||||
"description": "检测Windows打印后台服务状态,停止时自动重启",
|
||||
},
|
||||
{
|
||||
"category": "email",
|
||||
"name": "Outlook进程检测",
|
||||
"check_type": "script",
|
||||
"script_template": (
|
||||
"# 检测Outlook是否运行\n"
|
||||
"$outlook = Get-Process -Name OUTLOOK -ErrorAction SilentlyContinue\n"
|
||||
"if ($outlook) {\n"
|
||||
" Write-Output \"outlook_process=running;pid=$($outlook.Id)\"\n"
|
||||
"} else {\n"
|
||||
" Write-Output \"outlook_process=not_found\"\n"
|
||||
"}"
|
||||
),
|
||||
"fix_template": (
|
||||
"# 重启Outlook\n"
|
||||
"Stop-Process -Name OUTLOOK -Force -ErrorAction SilentlyContinue\n"
|
||||
"Start-Sleep -Seconds 3\n"
|
||||
"Start-Process 'C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE' "
|
||||
"-ErrorAction SilentlyContinue"
|
||||
),
|
||||
"fix_risk_level": "medium",
|
||||
"target_condition": "outlook_process=not_found",
|
||||
"description": "检测Outlook是否运行,未运行时尝试启动",
|
||||
},
|
||||
{
|
||||
"category": "security",
|
||||
"name": "火绒防护状态检测",
|
||||
"check_type": "api",
|
||||
"api_source": "huorong",
|
||||
"api_method": "get_protection_status",
|
||||
"fix_template": None,
|
||||
"fix_risk_level": "high",
|
||||
"target_condition": "huorong_protection=disabled",
|
||||
"description": "通过火绒API检测终端防护是否开启(需火绒企业版在线)",
|
||||
},
|
||||
{
|
||||
"category": "office",
|
||||
"name": "企微进程检测",
|
||||
"check_type": "script",
|
||||
"script_template": (
|
||||
"# 检测企微客户端是否运行\n"
|
||||
"$wxwork = Get-Process -Name WXWork -ErrorAction SilentlyContinue\n"
|
||||
"if ($wxwork) {\n"
|
||||
" Write-Output \"wxwork_process=running;pid=$($wxwork.Id)\"\n"
|
||||
"} else {\n"
|
||||
" Write-Output \"wxwork_process=not_found\"\n"
|
||||
"}"
|
||||
),
|
||||
"fix_template": (
|
||||
"# 重启企微客户端\n"
|
||||
"Stop-Process -Name WXWork -Force -ErrorAction SilentlyContinue\n"
|
||||
"Start-Sleep -Seconds 3\n"
|
||||
"Start-Process 'C:\\Program Files\\WXWork\\WXWork.exe' "
|
||||
"-ErrorAction SilentlyContinue"
|
||||
),
|
||||
"fix_risk_level": "low",
|
||||
"target_condition": "wxwork_process=not_found",
|
||||
"description": "检测企微客户端是否运行,未运行时尝试启动",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 种子函数
|
||||
# =============================================================================
|
||||
|
||||
async def seed_quiz_data(db: AsyncSession) -> Dict[str, int]:
|
||||
"""种子测验题目和诊断模板数据。
|
||||
|
||||
行为:
|
||||
- diagnostic_templates 表为空 → 插入手写诊断模板(快速,本地操作)
|
||||
- quiz_questions 表为空且未设置 SKIP_SEED_QUIZ → 调用 Dify 生成 70 道知识题
|
||||
- 已有数据 → 跳过(幂等)
|
||||
- SKIP_SEED_QUIZ=true → 跳过 Dify 生成(仅插入模板),管理员后续可手动触发
|
||||
|
||||
降级:Dify 不可用时记录错误,不阻塞启动
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: {"questions_created": N, "templates_created": N}
|
||||
"""
|
||||
created_questions = 0
|
||||
created_templates = 0
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 1. 种子诊断模板(手写,不依赖 Dify,快速插入)
|
||||
# ----------------------------------------------------------------
|
||||
existing_templates = await db.scalar(
|
||||
select(func.count(DiagnosticTemplate.id))
|
||||
)
|
||||
existing_templates = existing_templates or 0
|
||||
|
||||
if existing_templates == 0:
|
||||
for template_data in SEED_DIAGNOSTIC_TEMPLATES:
|
||||
template = DiagnosticTemplate(
|
||||
category=template_data["category"],
|
||||
name=template_data["name"],
|
||||
check_type=template_data.get("check_type", "script"),
|
||||
api_source=template_data.get("api_source"),
|
||||
api_method=template_data.get("api_method"),
|
||||
script_template=template_data.get("script_template"),
|
||||
fix_template=template_data.get("fix_template"),
|
||||
fix_risk_level=template_data.get("fix_risk_level", "medium"),
|
||||
target_condition=template_data.get("target_condition"),
|
||||
description=template_data.get("description"),
|
||||
is_active=True,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(template)
|
||||
created_templates += 1
|
||||
|
||||
logger.info(f"种子诊断模板插入完成: {created_templates} 个")
|
||||
else:
|
||||
logger.debug(f"diagnostic_templates 已有 {existing_templates} 条数据,跳过种子")
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 2. 种子知识题(Dify 生成,慢操作)
|
||||
# ----------------------------------------------------------------
|
||||
skip_seed_quiz = os.getenv("SKIP_SEED_QUIZ", "").lower() == "true"
|
||||
|
||||
if skip_seed_quiz:
|
||||
logger.info("SKIP_SEED_QUIZ=true,跳过 Dify 知识题生成。管理员可通过 POST /api/admin/quiz/generate 手动触发")
|
||||
else:
|
||||
existing_questions = await db.scalar(
|
||||
select(func.count(QuizQuestion.id))
|
||||
)
|
||||
existing_questions = existing_questions or 0
|
||||
|
||||
if existing_questions == 0:
|
||||
logger.info("===== 开始 Dify 生成种子知识题(70题)=====")
|
||||
service = get_quiz_generation_service()
|
||||
|
||||
for category in SEED_CATEGORIES:
|
||||
try:
|
||||
result = await service.generate_knowledge_questions_batch(
|
||||
db=db,
|
||||
category=category,
|
||||
count=SEED_QUESTIONS_PER_CATEGORY,
|
||||
is_active=True, # 种子数据直接激活
|
||||
)
|
||||
created_questions += result["success_count"]
|
||||
|
||||
if result["errors"]:
|
||||
logger.warning(
|
||||
f"种子知识题 [{category}] 有错误: {result['errors'][:3]}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Dify 不可用时跳过该类别,不阻塞启动
|
||||
logger.error(f"种子知识题 [{category}] 生成失败: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"===== 种子知识题生成完成: {created_questions} 道 =====")
|
||||
|
||||
if created_questions == 0:
|
||||
logger.warning(
|
||||
"Dify 种子知识题生成全部失败!"
|
||||
"管理员可通过 POST /api/admin/quiz/generate 手动触发生成"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"quiz_questions 已有 {existing_questions} 条数据,跳过种子")
|
||||
|
||||
# 注意:不在此处 commit,由调用方 _init_default_data 统一 commit
|
||||
await db.flush()
|
||||
return {
|
||||
"questions_created": created_questions,
|
||||
"templates_created": created_templates,
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — RBAC 角色种子数据 (v0.7.1 task #86)
|
||||
# =============================================================================
|
||||
# 启动时调用,把 5 角色 + 权限矩阵写入 roles 表
|
||||
# 兼容"角色已存在"的场景: 不重复插入,但更新 permissions
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.role import Role
|
||||
from app.services.rbac_service import (
|
||||
ROLE_METADATA,
|
||||
get_role_default_permissions,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def seed_rbac_roles(db: AsyncSession) -> int:
|
||||
"""种子 RBAC 5 角色。
|
||||
|
||||
行为:
|
||||
1. 遍历 ROLE_METADATA
|
||||
2. 角色不存在 → 创建(UUID + 默认 permissions)
|
||||
3. 角色存在 → 更新 display_name / description / permissions
|
||||
(不动 is_default,避免影响手动设置)
|
||||
|
||||
Returns:
|
||||
int: 新建角色数
|
||||
"""
|
||||
created_count = 0
|
||||
|
||||
for role_name, meta in ROLE_METADATA.items():
|
||||
# 查询是否已存在
|
||||
stmt = select(Role).where(Role.name == role_name)
|
||||
result = await db.execute(stmt)
|
||||
role = result.scalars().first()
|
||||
|
||||
permissions = get_role_default_permissions(role_name)
|
||||
|
||||
if role:
|
||||
# 更新现有角色(不动 is_default,防止覆盖手动设置)
|
||||
role.display_name = meta["display_name"]
|
||||
role.description = meta["description"]
|
||||
role.permissions = permissions
|
||||
role.updated_at = datetime.now()
|
||||
logger.debug(f"更新角色: {role_name} ({len(permissions)} 项权限)")
|
||||
else:
|
||||
# 创建新角色
|
||||
role = Role(
|
||||
id=str(uuid.uuid4()),
|
||||
name=role_name,
|
||||
display_name=meta["display_name"],
|
||||
description=meta["description"],
|
||||
permissions=permissions,
|
||||
is_default=(meta["is_default"] == "true"),
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(role)
|
||||
created_count += 1
|
||||
logger.info(f"创建角色: {role_name} ({len(permissions)} 项权限)")
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"RBAC 角色种子完成: 新建 {created_count} 个")
|
||||
return created_count
|
||||
Reference in New Issue
Block a user