From 1351371bd22914884f4daacccfdb89d888237fad Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 8 Apr 2026 13:32:55 +0800 Subject: [PATCH] feat: Add mermaid-fixer skill for automatic Mermaid syntax error correction - Add skill that uses check-mermaid.js to validate Mermaid diagrams - Skill guides Claude to parse error reports and apply intelligent fixes - Fixes common Mermaid parser bugs (parentheses, brackets, braces in labels) - Include demo scripts with test Markdown file for validation Co-Authored-By: Claude Opus 4.6 (1M context) --- demo_scripts/check-mermaid.js | 128 +++++ demo_scripts/demo.md | 482 ++++++++++++++++++ skills/mermaid-fixer/SKILL.md | 150 ++++++ skills/mermaid-fixer/evals/evals.json | 23 + skills/mermaid-fixer/scripts/check-mermaid.js | 128 +++++ 5 files changed, 911 insertions(+) create mode 100644 demo_scripts/check-mermaid.js create mode 100644 demo_scripts/demo.md create mode 100644 skills/mermaid-fixer/SKILL.md create mode 100644 skills/mermaid-fixer/evals/evals.json create mode 100644 skills/mermaid-fixer/scripts/check-mermaid.js diff --git a/demo_scripts/check-mermaid.js b/demo_scripts/check-mermaid.js new file mode 100644 index 0000000..741f9a4 --- /dev/null +++ b/demo_scripts/check-mermaid.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const { execSync } = require('child_process'); +const path = require('path'); +const os = require('os'); + +// 获取命令行传入的 Markdown 文件路径 +const fileArg = process.argv[2]; +if (!fileArg) { + console.error('❌ 请提供 Markdown 文件路径。用法: node check-mermaid.js '); + process.exit(1); +} + +const filePath = path.resolve(fileArg); +const fileName = path.basename(filePath); + +if (!fs.existsSync(filePath)) { + console.error(`❌ 文件不存在: ${filePath}`); + process.exit(1); +} + +const content = fs.readFileSync(filePath, 'utf-8'); + +// 正则匹配 ```mermaid ... ``` 代码块,兼容 Windows(\r\n) 和 Linux(\n) +const mermaidRegex = /```mermaid\r?\n([\s\S]*?)```/g; +let match; +let blockCount = 0; +let errorCount = 0; + +// 用于收集所有格式化后的错误报告块 +const errorReports = []; + +while ((match = mermaidRegex.exec(content)) !== null) { + blockCount++; + + const fullMatch = match[0]; + const code = match[1]; + + // 1. 计算在 Markdown 文件中的绝对行号 + const textBeforeMatch = content.substring(0, match.index); + const startLine = textBeforeMatch.split(/\r?\n/).length; + const endLine = startLine + fullMatch.split(/\r?\n/).length - 1; + + // 创建临时文件存放单个 Mermaid 代码 + const tmpFile = path.join(os.tmpdir(), `mermaid-check-${Date.now()}-${blockCount}.mmd`); + fs.writeFileSync(tmpFile, code.trim()); + + try { + // 调用 mmdc 进行静默渲染检查 + execSync(`npx mmdc -i "${tmpFile}" -o "${tmpFile}.svg" -q`, { stdio: 'pipe' }); + } catch (error) { + errorCount++; + + // 2. 净化报错信息:剔除底层执行堆栈 + const stderr = error.stderr ? error.stderr.toString() : error.message; + const errorLines = stderr.split(/\r?\n/); + const cleanErrorLines = []; + + for (const line of errorLines) { + if (line.trim().startsWith('at ') || + line.includes('Parser3.parseError') || + line.includes('fromText')) { + break; + } + cleanErrorLines.push(line); + } + const cleanErrorOutput = cleanErrorLines.join('\n').trim(); + + // 3. 截断代码内容:最少3行,最多6行 + const codeLines = code.split(/\r?\n/).filter(l => l.trim() !== ''); + let displayCode = ''; + + if (codeLines.length <= 6) { + displayCode = codeLines.join('\n'); + } else { + const head = codeLines.slice(0, 3).join('\n'); + const tail = codeLines.slice(-3).join('\n'); + displayCode = `${head}\n ...\n ... (中间省略 ${codeLines.length - 6} 行) ...\n ...\n${tail}`; + } + + // 4. 组装单个错误的 Markdown 块 + errorReports.push(`### ❌ 错误 #${errorCount} (代码块 #${blockCount}) + +- **文档位置:** 第 \`${startLine}\` 行至第 \`${endLine}\` 行 + +#### 核心错误详情 +\`\`\`text +${cleanErrorOutput} +\`\`\` + +#### 代码内容片段 +\`\`\`text +${displayCode} +\`\`\``); + + } finally { + // 清理临时文件 + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + if (fs.existsSync(`${tmpFile}.svg`)) fs.unlinkSync(`${tmpFile}.svg`); + } +} + +// ------------------------------------------------------------------ +// 生成最终的 Markdown 报告输出 +// ------------------------------------------------------------------ + +if (errorCount > 0) { + // 存在错误的情况 + console.log(`## 🚨 Mermaid 语法检查报告\n`); + console.log(`**检查文件:** \`${fileName}\``); + console.log(`**检查结果:** ❌ 发现 ${errorCount} 处语法错误 (共检测到 ${blockCount} 个代码块)\n`); + console.log(`---\n`); + console.log(errorReports.join('\n\n---\n\n')); + + // 返回非零状态码,确保在 CI/CD 或 Git Hook 中能够阻断流程 + process.exit(1); +} else { + // 全部正确或没有代码块的情况 + console.log(`## 🎉 Mermaid 语法检查报告\n`); + console.log(`**检查文件:** \`${fileName}\``); + if (blockCount === 0) { + console.log(`**检查结果:** ⚠️ 未检测到 Mermaid 代码块`); + } else { + console.log(`**检查结果:** ✅ 全部通过 (共检测到 ${blockCount} 个代码块)`); + } + process.exit(0); +} \ No newline at end of file diff --git a/demo_scripts/demo.md b/demo_scripts/demo.md new file mode 100644 index 0000000..cb3207a --- /dev/null +++ b/demo_scripts/demo.md @@ -0,0 +1,482 @@ +# Admin 用户物料清理数据流说明文档 + +## 概述 + +本文档详细说明 ERPAuto 系统中,Admin 用户执行物料清理操作时,被清理物料的完整获取流程、数据来源和处理链路。 + +## 核心结论 + +**Admin 用户清理的物料来源**:被清理的物料代码从数据库表 `dbo.MaterialsToBeDeleted` 中获取,根据 Admin 用户在 UI 界面选择的负责人(Manager)进行过滤。 + +--- + +## 数据流总览 + +```mermaid +sequenceDiagram + participant UI as CleanerPage (UI) + participant Hook as useCleaner Hook + participant API as Renderer API + participant IPC as IPC Channel + participant Service as ValidationService + participant DB as Database + + UI->>Hook: handleExecuteDeletion() + Hook->>API: runCleanerExecution() + API->>IPC: validation.getCleanerData({selectedManagers}) + + IPC->>Service: getCleanerData(userInfo, selectedManagers) + + alt Admin User + Service->>Service: loadMaterialCodesForCleaner() + Service->>DB: SELECT MaterialCode FROM dbo.MaterialsToBeDeleted
WHERE ManagerName IN (selectedManagers) + DB-->>Service: materialCodes[] + Service-->>IPC: {orderNumbers[], materialCodes[]} + else Regular User + Service->>DB: SELECT MaterialCode FROM dbo.MaterialsToBeDeleted
WHERE ManagerName = username + DB-->>Service: materialCodes[] + Service-->>IPC: {orderNumbers[], materialCodes[]} + end + + IPC-->>API: cleanerData + API->>IPC: cleaner.runCleaner({orderNumbers, materialCodes}) + IPC->>Service: CleanerApplicationService.runCleaner() + Service->>UI: 执行清理 (ERP 删除) +``` + +--- + +## 架构分层 + +```mermaid +graph TB + subgraph "渲染进程 (Renderer)" + UI[CleanerPage.tsx] + HOOK[useCleaner.ts] + RAPI[renderer/api.ts] + end + + subgraph "主进程 (Main)" + IPC_VAL[IPC: validation.getCleanerData] + IPC_CLEAN[IPC: cleaner.runCleaner] + VAS[ValidationApplicationService] + CAS[CleanerApplicationService] + ERP["CleanerService (ERP)"] + end + + subgraph "数据库 (Database)" + MTBD[(dbo.MaterialsToBeDeleted)] + DMPD[(dbo.DiscreteMaterialPlanData)] + MTTD[(dbo.MaterialsTypeToBeDeleted)] + end + + UI --> HOOK + HOOK --> RAPI + RAPI --> IPC_VAL + RAPI --> IPC_CLEAN + IPC_VAL --> VAS + IPC_CLEAN --> CAS + VAS --> MTBD + VAS --> DMPD + VAS --> MTTD + CAS --> ERP + + style MTBD fill:#f9f,stroke:#333 + style MTTD fill:#f9f,stroke:#333 +``` + +--- + +## 关键数据表 + +### 1. `dbo.MaterialsToBeDeleted` (核心来源表) + +**作用**:存储所有被标记为待删除的物料代码及其负责人。 + +**表结构**: +| 字段 | 类型 | 说明 | +|------|------|------| +| `ID` | int | 主键 | +| `MaterialCode` | varchar | **物料代码 (被清理的目标)** | +| `ManagerName` | varchar | 负责人姓名 | + +**Admin 获取物料的 SQL 查询**: + +```sql +SELECT MaterialCode +FROM dbo.MaterialsToBeDeleted +WHERE ManagerName IN (@manager0, @manager1, ...) + AND MaterialCode IS NOT NULL +``` + +### 2. `dbo.MaterialsTypeToBeDeleted` (物料类型配置表) + +**作用**:定义物料名称关键词与负责人的映射关系,用于自动分配负责人。 + +**表结构**: +| 字段 | 类型 | 说明 | +|------|------|------| +| `MaterialName` | varchar | 物料名称关键词 | +| `ManagerName` | varchar | 对应的负责人 | + +**示例**: +| MaterialName | ManagerName | +|-------------|-------------| +| "电阻" | "张三" | +| "电容" | "李四" | + +### 3. `dbo.DiscreteMaterialPlanData` (物料计划数据表) + +**作用**:存储从 ERP 提取的完整物料计划数据,用于校验和展示物料详情。 + +**关键字段**: + +- `MaterialCode` - 物料代码 +- `MaterialName` - 物料名称 +- `Specification` - 规格 +- `Model` - 型号 +- `SourceNo` - 订单号 + +--- + +## Admin 用户完整数据流 + +### 阶段 1: 校验 (Validation) + +```mermaid +graph LR + subgraph "步骤 1: 用户触发的数据来源" + A1[Admin 点击'数据校验'] + end + + subgraph "步骤 2: 数据获取模式" + A2{校验模式?} + A21[Full Mode
全量数据] + A22[Filtered Mode
筛选数据] + end + + subgraph "步骤 3: 数据库查询" + A3[DiscreteMaterialPlanDAO] + A31["queryAllDistinctByMaterialCode()"] + A32["queryBySourceNumbersDistinct(orderNumbers)"] + end + + subgraph "步骤 4: 数据增强" + A4[加载负责人信息] + A41[加载 MaterialsTypeToBeDeleted
关键词匹配] + A42[加载 MaterialsToBeDeleted
已标记记录] + end + + subgraph "结果" + A5["ValidationResult[]
包含 isMarkedForDeletion 标志"] + end + + A1 --> A2 + A2 -->|Full| A21 + A2 -->|Filtered| A22 + A21 --> A31 + A22 --> A32 + A31 --> A4 + A32 --> A4 + A4 --> A41 + A4 --> A42 + A4 --> A5 + + style A5 fill:#9f9,stroke:#333 +``` + +**Admin 特殊逻辑**: + +- Admin 可以看到**所有负责人**的物料 +- UI 会显示 Manager 列下拉筛选器 +- Admin 默认选中所有 Manager + +### 阶段 2: 保存删除计划 (可选) + +用户在 UI 上勾选物料 → 点击"确认删除" → 数据写入 `MaterialsToBeDeleted` 表: + +```mermaid +sequenceDiagram + participant UI as 用户界面 + participant DAO as MaterialsToBeDeletedDAO + participant DB as Database + + UI->>DAO: upsertBatch(materials[]) + + loop 对每个物料 + DAO->>DB: MERGE INTO dbo.MaterialsToBeDeleted
ON MaterialCode
WHEN MATCHED UPDATE
WHEN NOT MATCHED INSERT + end + + DB-->>DAO: 成功/失败统计 + DAO-->>UI: { total, success, failed } +``` + +**SQL 逻辑**(MERGE UPSERT): + +```sql +MERGE INTO dbo.MaterialsToBeDeleted AS target +USING (VALUES (@MaterialCode, @ManagerName)) AS source (MaterialCode, ManagerName) +ON target.MaterialCode = source.MaterialCode +WHEN MATCHED THEN + UPDATE SET ManagerName = source.ManagerName +WHEN NOT MATCHED THEN + INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); +``` + +### 阶段 3: 执行清理 (关键步骤) + +这是 Admin 用户执行实际删除操作的核心流程: + +```mermaid +graph TD + subgraph "Renderer 层" + S1["handleExecuteDeletion()"] + S1-->S2["runCleanerExecution {selectedManagers}"] + end + + subgraph "获取清理数据 (主进程)" + S2-->S3[ValidationApplicationService.getCleanerData] + S3-->S4{用户类型?} + + S4-->|Admin + selectedManagers| S5[loadMaterialCodesForCleaner
Admin With Managers] + S5-->S6[SELECT FROM MaterialsToBeDeleted
WHERE ManagerName IN selectedManagers] + + S4-->|Admin no managers| S7[Admin Without Managers] + S7-->S8[SELECT FROM DiscreteMaterialPlanData
WHERE SourceNo IN orderNumbers] + + S4-->|普通用户 | S9[SELECT FROM MaterialsToBeDeleted
WHERE ManagerName = username] + end + + subgraph "执行清理" + S6-->S10["返回 materialCodes[]"] + S8-->S10 + S9-->S10 + S10-->S11[CleanerApplicationService.runCleaner] + S11-->S12[CleanerService.clean
连接 ERP 执行删除] + end + + S12-->S13[清理完成报告] + + style S5 fill:#ff9,stroke:#333 + style S6 fill:#f96,stroke:#333 + style S12 fill:#f99,stroke:#333 +``` + +**关键代码路径**: + +```typescript +// src/main/services/validation/validation-application-service.ts +// loadMaterialCodesForCleaner() 方法 + +// Admin with selected managers: +if (isAdmin && selectedManagers && selectedManagers.length > 0) { + const materialCodes = await this.queryMaterialCodesByManagers( + dbService, + markedTableName, // dbo.MaterialsToBeDeleted + selectedManagers + ) + return materialCodes +} + +// Admin without selected managers (fallback): +if (isAdmin) { + if (orderNumbers.length === 0) { + return [] // 无数据可处理 + } + const materialDao = new DiscreteMaterialPlanDAO() + const records = await materialDao.queryBySourceNumbersDistinct(orderNumbers) + const materialCodes = [...new Set(records.map((r) => r.MaterialCode as string).filter(Boolean))] + return materialCodes +} +``` + +### 阶段 4: ERP 删除执行 + +```mermaid +sequenceDiagram + participant CAS as CleanerApplicationService + participant Cleaner as CleanerService + participant ERP as ERP System + + CAS->>Cleaner: clean({orderNumbers, materialCodes}) + + loop 每个订单号 + Cleaner->>ERP: 登录 (一次) + loop 每个物料代码 + Cleaner->>ERP: 查询物料计划 + alt 物料存在 + Cleaner->>ERP: 执行删除 + ERP-->>Cleaner: 删除结果 + else 物料不存在 + Cleaner-->>Cleaner: 跳过 + end + end + end + + Cleaner-->>CAS: CleanerResult +``` + +--- + +## Admin 用户的数据来源路径 + +整理出 Admin 用户获取物料的完整路径: + +```mermaid +graph TB + Start[Admin 点击执行清理] --> Check{有 selectedManagers?} + + Check -->|Yes| Path1[路径 1: MaterialsToBeDeleted 表] + Path1 --> Q1["SELECT MaterialCode FROM MaterialsToBeDeleted
WHERE ManagerName IN (selectedManagers)"] + Q1 --> Merge[合并所有物料代码] + + Check -->|No| Path2[路径 2: DiscreteMaterialPlanData 表] + Path2 --> NeedProd{有共享 Production ID?} + NeedProd -->|Yes| GetOrder[从共享 ID 解析订单号] + GetOrder --> Q2["SELECT DISTINCT MaterialCode FROM DiscreteMaterialPlanData
WHERE SourceNo IN (orderNumbers)"] + Q2 --> Merge + + NeedProd -->|No| Empty[返回空数组
无法执行清理] + + Merge --> Execute[传递给 CleanerService
执行 ERP 删除] + + style Path1 fill:#9f9,stroke:#333,stroke-width:2px + style Path2 fill:#ff9,stroke:#333,stroke-width:2px + style Empty fill:#f99,stroke:#333 + style Execute fill:#f96,stroke:#333,stroke-width:3px +``` + +--- + +## 关键代码位置参考 + +### 渲染层 (Renderer) + +| 文件 | 函数 | 说明 | +| ---------------------------------------- | ----------------------- | -------------------------- | +| `src/renderer/src/pages/CleanerPage.tsx` | `handleExecuteDeletion` | 清理入口 | +| `src/renderer/src/hooks/useCleaner.ts` | `handleExecuteDeletion` | 调用 `runCleanerExecution` | +| `src/renderer/src/hooks/cleaner/api.ts` | `runCleanerExecution` | 先获取数据再执行清理 | + +### 主进程层 (Main) + +| 文件 | 类/函数 | 说明 | +| ---------------------------------------------------------------- | ----------------------------------------------- | ---------------- | +| `src/main/ipc/validation-handler.ts` | `VALIDATION_GET_CLEANER_DATA` | IPC 入口 | +| `src/main/services/validation/validation-application-service.ts` | `getCleanerData`, `loadMaterialCodesForCleaner` | **核心逻辑** | +| `src/main/services/cleaner/cleaner-application-service.ts` | `runCleaner` | 执行 ERP 清理 | +| `src/main/services/erp/cleaner.ts` | `CleanerService.clean` | ERP 浏览器自动化 | + +### 数据库层 (DAO) + +| 文件 | 类 | 说明 | +| ----------------------------------------------------------- | ------------------------- | ---------------------------- | +| `src/main/services/database/materials-to-be-deleted-dao.ts` | `MaterialsToBeDeletedDAO` | `MaterialsToBeDeleted`表操作 | +| `src/main/services/database/discrete-material-plan-dao.ts` | `DiscreteMaterialPlanDAO` | 物料计划数据查询 | + +--- + +## 数据流状态图 + +```mermaid +stateDiagram-v2 + [*] --> 物料录入:用户在 UI 输入物料 + 物料录入 --> 待校验:保存到 MaterialsToBeDeleted 表 + + 待校验 --> 校验中:点击"数据校验" + 校验中 --> 已标记:通过关键词匹配负责人 + 已标记 --> 待清理:用户勾选物料 + + 待清理 --> 清理执行中:点击"执行清理" + 清理执行中 --> 清理完成:ERP 删除成功 + 清理执行中 --> 部分失败:部分物料删除失败 + + 清理完成 --> [*] + 部分失败 --> [*] + + note right of 待清理 + Admin 可以查看和选择 + 所有负责人的物料 + end note + + note right of 清理执行中 + 从 MaterialsToBeDeleted 表 + 根据 selectedManagers 过滤 + 获取要删除的物料代码 + end note +``` + +--- + +## 数据流对比:Admin vs 普通用户 + +```mermaid +graph TB + subgraph "Admin 用户" + A1[勾选多个 Manager] + A2[SELECT FROM MaterialsToBeDeleted
WHERE ManagerName IN selectedManagers] + A3[获取所有选中负责人的物料] + end + + subgraph "普通用户" + B1[只能看到自己的物料] + B2[SELECT FROM MaterialsToBeDeleted
WHERE ManagerName = username] + B3[只能删除自己的物料] + end + + A1 --> A2 --> A3 --> Exec[执行清理] + B1 --> B2 --> B3 --> Exec +``` + +--- + +## 常见问题解答 + +### Q1: **物料代码是如何被录入到 `MaterialsToBeDeleted` 表的?** + +**A**: 有三种方式: + +1. **用户手动勾选** → 在 CleanerPage 勾选物料 → 点击"确认删除" → `upsertBatch` +2. **关键词自动匹配** → 校验时根据 `MaterialsTypeToBeDeleted` 配置自动分配负责人和标记 +3. **API 直接写入** → 其他服务调用 `materials.upsertBatch` IPC + +### Q2: **Admin 如果不选 Manager 会怎样?** + +**A**: Admin 可以不选 Manager,此时: + +- 系统会尝试从"共享 Production ID"解析订单号 +- 然后从 `DiscreteMaterialPlanData` 表查询所有物料代码(不经过 `MaterialsToBeDeleted` 过滤) +- 如果没有共享 Production ID,则返回空数组,无法执行清理 + +### Q3: **物料代码会在清理后被自动删除吗?** + +**A**: + +- ✅ **干运行模式**:不删除 ERP 数据,但会保留 UI 状态 +- ✅ **正式执行**: + - ERP 中的物料计划被删除 + - `MaterialsToBeDeleted` 表中记录**不会被自动删除**(需要手动清理) + +### Q4: **如何清理已删除的物料记录?** + +**A**: Admin 可以在物料管理界面: + +- 按 Manager 筛选 +- 批量删除已处理的物料记录 +- 调用 `materials.delete` IPC 接口 + +--- + +## 总结 + +Admin 用户执行清理时,被清理物料的来源路径: + +1. **主要来源**: `dbo.MaterialsToBeDeleted` 表 +2. **过滤条件**: `ManagerName IN (selectedManagers)` +3. **执行流程**: + - 从数据库查询物料代码 + - 结合订单号列表 + - 通过 Playwright 连接 ERP 系统 + - 逐个物料执行删除操作 + +**关键点**:被清理的物料**必须**先在 `MaterialsToBeDeleted` 表中存在记录,并且其 `ManagerName` 与 Admin 选择的负责人匹配。 diff --git a/skills/mermaid-fixer/SKILL.md b/skills/mermaid-fixer/SKILL.md new file mode 100644 index 0000000..824761c --- /dev/null +++ b/skills/mermaid-fixer/SKILL.md @@ -0,0 +1,150 @@ +--- +name: mermaid-fixer +description: Fix Mermaid diagram syntax errors in Markdown files. Use when: (1) Mermaid diagrams aren't rendering or show parse errors, (2) User mentions "mermaid syntax error", "mermaid not working", "diagram broken", (3) CI/CD fails on Mermaid validation, (4) Documentation contains Mermaid code blocks that need fixing. +--- + +# Mermaid Diagram Fixer + +Automatically detects and fixes Mermaid diagram syntax errors in Markdown files using the `check-mermaid.js` validation script. + +## How It Works + +1. **Run `check-mermaid.js`** to get detailed error reports +2. **Parse error information** (line numbers, error types, code snippets) +3. **Apply intelligent fixes** based on the specific error +4. **Verify** by running the check again + +## Core Script + +The `scripts/check-mermaid.js` script is the heart of this skill. It: +- Scans Markdown files for ```` ```mermaid ```` code blocks +- Validates each block using the Mermaid CLI (`mmdc`) +- Returns detailed error reports with line numbers and error messages + +## Workflow + +### Step 1: Run the Check Script + +```bash +node scripts/check-mermaid.js +``` + +**Expected output format:** + +- **No errors**: Returns exit code 0 with success message +- **Has errors**: Returns exit code 1 with structured error report: + +```markdown +## 🚨 Mermaid 语法检查报告 + +**检查文件:** `example.md` +**检查结果:** ❌ 发现 2 处语法错误 (共检测到 5 个代码块) + +--- + +### ❌ 错误 #1 (代码块 #2) + +- **文档位置:** 第 `51` 行至第 `86` 行 + +#### 核心错误详情 +\`\`\`text +Error: Parse error on line 13: ... +Expecting 'SQE', 'DOUBLECIRCLEEND', ... got 'PS' +\`\`\` + +#### 代码内容片段 +\`\`\`text +[shows the problematic code] +\`\`\` +``` + +### Step 2: Parse and Understand Errors + +Key information from each error: +- **文档位置**: Which line numbers contain the error +- **核心错误详情**: The parser error message +- **代码内容片段**: The actual Mermaid code causing the issue + +### Step 3: Apply Fixes Based on Error Type + +#### Error Type: "got 'PS'" (Parentheses Issue) + +**Cause**: Parentheses `()` in node labels or arrow labels within subgraphs + +**Fix**: Quote the label +```mermaid +# Before (causes error) +NodeA[Label (with parens)] +-->|Label (parens)| NodeB + +# After (fixed) +NodeA["Label (with parens)"] +-->"|Label (parens)|" NodeB +``` + +#### Error Type: "got 'SQS'" (Square Brackets Issue) + +**Cause**: Square brackets `[]` in node labels + +**Fix**: Quote the label +```mermaid +# Before +NodeA[Result[]
text] + +# After +NodeA["Result[]
text"] +``` + +#### Error Type: "got 'DIAMOND_START'" or similar + +**Cause**: Curly braces `{}` or other special characters in labels + +**Fix**: Quote the label +```mermaid +# Before +NodeA[Label {variable}] + +# After +NodeA["Label {variable}"] +``` + +### Step 4: Apply the Fix to the File + +1. Read the Markdown file +2. Locate the problematic code block using the reported line numbers +3. Apply the appropriate fix (quote labels with special characters) +4. Save the file + +### Step 5: Verify + +Run `check-mermaid.js` again to confirm all errors are resolved. + +## Common Fix Patterns + +| Error Pattern | Fix Strategy | +|---------------|--------------| +| `NodeID[label (text)]` | Change to `NodeID["label (text)"]` | +| `-->|label (text)|` | Change to `-->|"label (text)"\|` | +| `NodeID[label[]text]` | Change to `NodeID["label[]text"]` | +| `NodeID[label{text}]` | Change to `NodeID["label{text}"]` | + +## Important Notes + +1. **Always quote labels** containing: `()`, `[]`, `{}`, `<`, `>`, `#`, or Chinese characters +2. **Preserve the original structure** - only modify the problematic labels +3. **Check all reported errors** - don't stop after fixing just one +4. **Re-verify** after each fix round + +## Example Usage + +``` +User: "My Mermaid diagrams in docs/architecture.md aren't rendering" + +Claude: +1. Run: node scripts/check-mermaid.js docs/architecture.md +2. Parse error output +3. Identify errors (e.g., "got 'PS'" on line 45) +4. Read file, locate line 45, find the Mermaid block +5. Apply fix: quote labels with parentheses +6. Save and verify +``` diff --git a/skills/mermaid-fixer/evals/evals.json b/skills/mermaid-fixer/evals/evals.json new file mode 100644 index 0000000..3e03d9f --- /dev/null +++ b/skills/mermaid-fixer/evals/evals.json @@ -0,0 +1,23 @@ +{ + "skill_name": "mermaid-fixer", + "evals": [ + { + "id": 1, + "prompt": "The Mermaid diagrams in demo_scripts/demo.md have syntax errors. Please fix them.", + "expected_output": "The skill should run check-mermaid.js, identify the 3 syntax errors, and apply fixes by quoting labels with special characters. After fixes, all 9 Mermaid blocks should pass validation.", + "files": ["demo_scripts/demo.md"] + }, + { + "id": 2, + "prompt": "My documentation file README.md has broken Mermaid diagrams. Can you check and fix them?", + "expected_output": "The skill runs check-mermaid.js on README.md, parses any error reports, applies appropriate fixes based on error types, and verifies the fixes by running the check again.", + "files": [] + }, + { + "id": 3, + "prompt": "CI/CD is failing with Mermaid validation errors on docs/api.md. Please fix the diagrams.", + "expected_output": "The skill identifies all Mermaid syntax errors using check-mermaid.js, applies fixes for each error type (parentheses, brackets, braces in labels), and ensures the file passes validation.", + "files": [] + } + ] +} diff --git a/skills/mermaid-fixer/scripts/check-mermaid.js b/skills/mermaid-fixer/scripts/check-mermaid.js new file mode 100644 index 0000000..741f9a4 --- /dev/null +++ b/skills/mermaid-fixer/scripts/check-mermaid.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const { execSync } = require('child_process'); +const path = require('path'); +const os = require('os'); + +// 获取命令行传入的 Markdown 文件路径 +const fileArg = process.argv[2]; +if (!fileArg) { + console.error('❌ 请提供 Markdown 文件路径。用法: node check-mermaid.js '); + process.exit(1); +} + +const filePath = path.resolve(fileArg); +const fileName = path.basename(filePath); + +if (!fs.existsSync(filePath)) { + console.error(`❌ 文件不存在: ${filePath}`); + process.exit(1); +} + +const content = fs.readFileSync(filePath, 'utf-8'); + +// 正则匹配 ```mermaid ... ``` 代码块,兼容 Windows(\r\n) 和 Linux(\n) +const mermaidRegex = /```mermaid\r?\n([\s\S]*?)```/g; +let match; +let blockCount = 0; +let errorCount = 0; + +// 用于收集所有格式化后的错误报告块 +const errorReports = []; + +while ((match = mermaidRegex.exec(content)) !== null) { + blockCount++; + + const fullMatch = match[0]; + const code = match[1]; + + // 1. 计算在 Markdown 文件中的绝对行号 + const textBeforeMatch = content.substring(0, match.index); + const startLine = textBeforeMatch.split(/\r?\n/).length; + const endLine = startLine + fullMatch.split(/\r?\n/).length - 1; + + // 创建临时文件存放单个 Mermaid 代码 + const tmpFile = path.join(os.tmpdir(), `mermaid-check-${Date.now()}-${blockCount}.mmd`); + fs.writeFileSync(tmpFile, code.trim()); + + try { + // 调用 mmdc 进行静默渲染检查 + execSync(`npx mmdc -i "${tmpFile}" -o "${tmpFile}.svg" -q`, { stdio: 'pipe' }); + } catch (error) { + errorCount++; + + // 2. 净化报错信息:剔除底层执行堆栈 + const stderr = error.stderr ? error.stderr.toString() : error.message; + const errorLines = stderr.split(/\r?\n/); + const cleanErrorLines = []; + + for (const line of errorLines) { + if (line.trim().startsWith('at ') || + line.includes('Parser3.parseError') || + line.includes('fromText')) { + break; + } + cleanErrorLines.push(line); + } + const cleanErrorOutput = cleanErrorLines.join('\n').trim(); + + // 3. 截断代码内容:最少3行,最多6行 + const codeLines = code.split(/\r?\n/).filter(l => l.trim() !== ''); + let displayCode = ''; + + if (codeLines.length <= 6) { + displayCode = codeLines.join('\n'); + } else { + const head = codeLines.slice(0, 3).join('\n'); + const tail = codeLines.slice(-3).join('\n'); + displayCode = `${head}\n ...\n ... (中间省略 ${codeLines.length - 6} 行) ...\n ...\n${tail}`; + } + + // 4. 组装单个错误的 Markdown 块 + errorReports.push(`### ❌ 错误 #${errorCount} (代码块 #${blockCount}) + +- **文档位置:** 第 \`${startLine}\` 行至第 \`${endLine}\` 行 + +#### 核心错误详情 +\`\`\`text +${cleanErrorOutput} +\`\`\` + +#### 代码内容片段 +\`\`\`text +${displayCode} +\`\`\``); + + } finally { + // 清理临时文件 + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + if (fs.existsSync(`${tmpFile}.svg`)) fs.unlinkSync(`${tmpFile}.svg`); + } +} + +// ------------------------------------------------------------------ +// 生成最终的 Markdown 报告输出 +// ------------------------------------------------------------------ + +if (errorCount > 0) { + // 存在错误的情况 + console.log(`## 🚨 Mermaid 语法检查报告\n`); + console.log(`**检查文件:** \`${fileName}\``); + console.log(`**检查结果:** ❌ 发现 ${errorCount} 处语法错误 (共检测到 ${blockCount} 个代码块)\n`); + console.log(`---\n`); + console.log(errorReports.join('\n\n---\n\n')); + + // 返回非零状态码,确保在 CI/CD 或 Git Hook 中能够阻断流程 + process.exit(1); +} else { + // 全部正确或没有代码块的情况 + console.log(`## 🎉 Mermaid 语法检查报告\n`); + console.log(`**检查文件:** \`${fileName}\``); + if (blockCount === 0) { + console.log(`**检查结果:** ⚠️ 未检测到 Mermaid 代码块`); + } else { + console.log(`**检查结果:** ✅ 全部通过 (共检测到 ${blockCount} 个代码块)`); + } + process.exit(0); +} \ No newline at end of file