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:
Simon
2026-08-08 19:07:33 +08:00
parent 35c5580c3d
commit bdc5a3be50
11075 changed files with 1320 additions and 2248978 deletions
+306
View File
@@ -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,
}
+71
View File
@@ -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