UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

304 lines (303 loc) 11.5 kB
/** * 防护规则 API 路由 * 管理代码质量防护规则的 CRUD 和生命周期操作 */ import express from 'express'; import { ioLimit } from '#shared/concurrency.js'; import { BatchDisableBody, BatchEnableBody, CheckCodeBody, CreateGuardRuleBody, ImportFromRecipeBody, } from '#shared/schemas/http-requests.js'; import { getServiceContainer } from '../../injection/ServiceContainer.js'; import { NotFoundError } from '../../shared/errors/index.js'; import { LanguageService } from '../../shared/LanguageService.js'; import { validate } from '../middleware/validate.js'; import { getContext, safeInt } from '../utils/routeHelpers.js'; const router = express.Router(); /** 将 Recipe 实体 → Guard 规则扁平格式(Dashboard GuardView 期望) */ function mapRecipeToGuardRule(r) { const constraints = r.constraints; const guards = constraints?.guards || []; const firstGuard = (guards[0] || {}); const content = r.content; const tags = r.tags; return { id: r.id, ruleId: r.id, message: firstGuard.message || r.description || r.title || '', severity: firstGuard.severity || 'warning', pattern: firstGuard.pattern || content?.pattern || '', languages: tags && tags.length > 0 ? tags : r.language ? [r.language] : [], note: content?.rationale || '', dimension: r.scope || 'file', rationale: content?.rationale || '', sourceRecipe: r.id, enabled: r.status === 'active', }; } /** * GET /api/v1/rules * 获取防护规则列表(支持筛选和分页) * 同时包含内置规则 + 数据库规则 */ router.get('/', async (req, res) => { const { severity, category, enabled, sourceRecipe, keyword } = req.query; const page = safeInt(req.query.page, 1); const pageSize = safeInt(req.query.limit, 20, 1, 100); const container = getServiceContainer(); const guardService = container.get('guardService'); // 获取数据库中的 boundary-constraint 规则 let result; if (keyword) { result = (await guardService.searchRules(String(keyword), { page, pageSize })); } else { const filters = {}; if (severity) { filters.severity = severity; } if (category) { filters.category = category; } if (enabled !== undefined) { filters.enabled = enabled === 'true'; } if (sourceRecipe) { filters.sourceRecipe = sourceRecipe; } result = (await guardService.listRules(filters, { page, pageSize })); } // 将 Recipe 实体映射为 Guard 规则扁平格式 const dbItems = result?.data || []; const mappedDbRules = dbItems.map(mapRecipeToGuardRule); // 合并内置规则(GuardCheckEngine 内置 9 条 iOS 规则) let guardCheckEngine; try { guardCheckEngine = container.get('guardCheckEngine'); } catch { /* not registered */ } const builtInEntries = guardCheckEngine ? Object.entries(guardCheckEngine.getBuiltInRules()) : []; const dbRuleIds = new Set(mappedDbRules.map((r) => r.id)); const builtInRules = builtInEntries .filter(([id]) => !dbRuleIds.has(id)) .map(([id, r]) => ({ id, ruleId: id, message: r.message, severity: r.severity, pattern: r.pattern, languages: r.languages || [], dimension: r.dimension || 'file', category: r.category || '', fixSuggestion: r.fixSuggestion || '', note: '', enabled: true, source: 'built-in', })); const allRules = [...mappedDbRules, ...builtInRules]; // 获取当前项目检测到的语言列表,供前端按项目语言筛选 // 使用 LanguageService 统一检测(支持 Discoverer + Monorepo 文件标记回退) let projectLanguages = []; try { const moduleService = container.get('moduleService'); await moduleService.load(); const info = moduleService.getProjectInfo(); const discovererIds = info.languages || []; projectLanguages = LanguageService.detectProjectLanguages(process.cwd(), { discovererIds, }); } catch { // moduleService 不可用时纯文件扫描回退 projectLanguages = LanguageService.detectProjectLanguages(process.cwd()); } // Guard 内置规则中 objectivec 记为 'objc',做向后兼容映射 projectLanguages = projectLanguages.map((l) => LanguageService.toGuardLangId(l)); res.json({ success: true, data: { data: allRules, projectLanguages, pagination: result?.pagination || { page, pageSize, total: allRules.length, pages: 1 }, }, }); }); /** * GET /api/v1/rules/stats * 获取防护规则统计 */ router.get('/stats', async (req, res) => { const container = getServiceContainer(); const guardService = container.get('guardService'); const stats = await guardService.getRuleStats(); res.json({ success: true, data: stats }); }); /** * GET /api/v1/rules/:id * 获取防护规则详情 */ router.get('/:id', async (req, res) => { const { id } = req.params; const container = getServiceContainer(); const recipeRepo = container.get('knowledgeRepository'); const rule = await recipeRepo.findById(String(id)); if (!rule) { throw new NotFoundError('Guard rule not found', 'recipe', id); } res.json({ success: true, data: rule }); }); /** * POST /api/v1/rules * 创建防护规则 (Gateway 管控: 权限 + 宪法 + 审计) * 兼容前端字段: { ruleId, message, pattern, languages, note, dimension } * 同时兼容 V2 字段: { name, description, pattern, severity, category } */ router.post('/', validate(CreateGuardRuleBody), async (req, res) => { // 兼容前端 GuardView 发来的字段名 const name = req.body.name || req.body.ruleId; const description = req.body.description || req.body.message || ''; const { pattern, severity, category, sourceRecipeId, sourceReason } = req.body; const note = req.body.note || sourceReason || ''; const languages = req.body.languages || (category ? [category] : []); const dimension = req.body.dimension || null; const result = await req.gw('guard_rule:create', 'guard_rules', { name, description, pattern, severity: severity || 'warning', category: languages[0] || category || 'guard', languages, note, dimension, sourceRecipeId, sourceReason: note, }); res.status(201).json({ success: true, data: result.data, requestId: result.requestId }); }); /** * POST /api/v1/rules/batch-enable * 批量启用防护规则 */ router.post('/batch-enable', validate(BatchEnableBody), async (req, res) => { const { ids } = req.body; const container = getServiceContainer(); const guardService = container.get('guardService'); const context = getContext(req); const results = await Promise.allSettled(ids.map((id) => ioLimit(() => guardService.enableRule(id, context)))); const enabled = results.filter((r) => r.status === 'fulfilled').map((r) => r.value); const failed = results .map((r, i) => (r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null)) .filter(Boolean); res.json({ success: true, data: { enabled, failed, total: ids.length, successCount: enabled.length, failureCount: failed.length, }, }); }); /** * POST /api/v1/rules/batch-disable * 批量禁用防护规则 */ router.post('/batch-disable', validate(BatchDisableBody), async (req, res) => { const { ids, reason } = req.body; const container = getServiceContainer(); const guardService = container.get('guardService'); const context = getContext(req); const results = await Promise.allSettled(ids.map((id) => ioLimit(() => guardService.disableRule(id, reason || '', context)))); const disabled = results.filter((r) => r.status === 'fulfilled').map((r) => r.value); const failed = results .map((r, i) => (r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null)) .filter(Boolean); res.json({ success: true, data: { disabled, failed, total: ids.length, successCount: disabled.length, failureCount: failed.length, }, }); }); /** * PATCH /api/v1/rules/:id/enable * 启用防护规则 */ router.patch('/:id/enable', async (req, res) => { const { id } = req.params; const container = getServiceContainer(); const guardService = container.get('guardService'); const context = getContext(req); const rule = await guardService.enableRule(String(id), context); res.json({ success: true, data: rule }); }); /** * PATCH /api/v1/rules/:id/disable * 禁用防护规则 */ router.patch('/:id/disable', async (req, res) => { const { id } = req.params; const { reason } = req.body; const container = getServiceContainer(); const guardService = container.get('guardService'); const context = getContext(req); const rule = await guardService.disableRule(String(id), reason || '', context); res.json({ success: true, data: rule }); }); /** * POST /api/v1/rules/check * 检查代码是否违反规则 */ router.post('/check', validate(CheckCodeBody), async (req, res) => { const { code, language, ruleIds } = req.body; const container = getServiceContainer(); const guardService = container.get('guardService'); const result = await guardService.checkCode(code, { language, ruleIds }); res.json({ success: true, data: result }); }); /** * POST /api/v1/rules/import-from-recipe * 从 Recipe 导入防护规则 */ router.post('/import-from-recipe', validate(ImportFromRecipeBody), async (req, res) => { const { recipeId, rules } = req.body; const container = getServiceContainer(); const guardService = container.get('guardService'); const context = getContext(req); const importedRules = (await guardService.importRulesFromRecipe(recipeId, rules, context)); res.status(201).json({ success: true, data: { importedRules, count: importedRules.length }, }); }); /** * GET /api/v1/rules/compliance * 生成全项目合规报告 * Query params: * - path: 扫描根目录(默认 projectRoot) * - maxErrors: Quality Gate 最大 error 数(默认 0) * - maxWarnings: Quality Gate 最大 warning 数(默认 20) * - minScore: Quality Gate 最低分(默认 70) * - maxFiles: 最大扫描文件数(默认 500) */ router.get('/compliance', async (req, res) => { const container = getServiceContainer(); const reporter = container.get('complianceReporter'); const projectRoot = String(req.query.path || process.env.ASD_PROJECT_DIR || process.cwd()); const report = await reporter.generate(projectRoot, { qualityGate: { maxErrors: parseInt(req.query.maxErrors) || 0, maxWarnings: parseInt(req.query.maxWarnings) || 20, minScore: parseInt(req.query.minScore) || 70, }, maxFiles: parseInt(req.query.maxFiles) || 500, }); res.json({ success: true, data: report }); }); export default router;