feat: 实现 Mermaid 代码块魔改支持

- 添加 convertMermaidCodeblocks() 函数,在代码高亮前拦截 mermaid 代码块
- 支持标准 Markdown 代码块 (\\\mermaid) 渲染
- 更新 detectMermaidBlocks() 添加 mermaid-from-codeblock 选择器
- 更新 extractMermaidCode() 支持新容器类型
- 创建测试文件 test-codeblock-magic.html
- 更新用户文档、开发者文档和 FAQ
- 完全绕过代码高亮和 WordPress 格式化
- 支持 PJAX 页面切换
- 特殊字符和换行符正确保留
This commit is contained in:
2026-01-24 21:35:12 +08:00
parent e5e8a245be
commit 29bfd284e0
18 changed files with 5566 additions and 50 deletions

View File

@@ -0,0 +1,187 @@
# AI 评论审核流程说明
## 流程概述
本文档通过 Mermaid 流程图详细展示用户提交评论后,系统进行 AI 审核的全流程涵盖预处理、规则判断、AI 检测、结果处理、数据学习等核心环节。
## 流程图
::: mermaid
flowchart TD
Start([用户提交评论]) --> PreProcess[预处理评论<br/>钩子preprocess_comment/post_comment_preprocessing]
PreProcess --> CheckEnabled{启用 AI 检测?}
CheckEnabled -->|否| SaveComment[保存评论]
CheckEnabled -->|是| CheckMode{检测模式?}
CheckMode -->|manual关闭实时检测| SaveComment
CheckMode -->|keyword/sample/all| CheckLoggedIn{已登录用户?}
CheckLoggedIn -->|是且跳过登录用户| SaveComment
CheckLoggedIn -->|否或不跳过| CheckWhitelist{在白名单?}
CheckWhitelist -->|是| SaveComment
CheckWhitelist -->|否| CheckKeyword[检查关键字触发]
CheckKeyword --> DecideCheck{决定是否检测}
DecideCheck -->|keyword模式且触发关键字| NeedCheck[需要检测]
DecideCheck -->|sample模式且触发关键字| NeedCheck
DecideCheck -->|sample模式且随机抽中| NeedCheck
DecideCheck -->|all模式| NeedCheck
DecideCheck -->|其他情况| SaveComment
NeedCheck --> SetPending[强制设置 comment_approved=0<br/>待审核状态]
SetPending --> SetFlag[设置 _argon_needs_spam_check=true]
SetFlag --> SaveKeywords[保存触发的关键字]
SaveKeywords --> SaveComment
SaveComment --> CommentPost[评论入库后处理<br/>钩子comment_post/post_comment_updatemetas]
CommentPost --> SaveMeta[保存评论元数据]
SaveMeta --> CheckNeedFlag{有 _argon_needs_spam_check 标记?}
CheckNeedFlag -->|否| ShowPending[显示审核中状态]
CheckNeedFlag -->|是| SaveCheckMeta[保存检测标记到数据库]
SaveCheckMeta --> AutoDetect[触发自动检测<br/>钩子comment_post/argon_auto_detect_spam_on_comment]
AutoDetect --> CheckSaved{有检测标记?}
CheckSaved -->|否| End1([结束])
CheckSaved -->|是| CheckDetected{已检测过?}
CheckDetected -->|是| End2([结束])
CheckDetected -->|否| CheckReason{检测原因?}
CheckReason -->|关键字触发| SyncDetect[立即同步检测]
CheckReason -->|其他| AsyncDetect[异步检测延迟1秒]
SyncDetect --> DetectHandler[检测处理函数<br/>argon_async_spam_detection_handler]
AsyncDetect --> DetectHandler
DetectHandler --> CallAI[调用 AI API<br/>argon_detect_spam_comment]
CallAI --> GetPrompt[根据 Prompt 模式获取提示词]
CallAI --> BuildContext[构建评论上下文(用户名+内容)]
BuildContext --> SendAPI[发送到 AI API]
SendAPI --> ParseResult[解析 AI 返回结果]
ParseResult --> SaveTime[记录检测时间]
SaveTime --> GenCode[生成识别码]
GenCode --> UpdateStats[更新用户统计]
UpdateStats --> CheckResult{检测结果?}
CheckResult -->|内容违规(高置信度)| CheckAction1{自动处理方式?}
CheckResult -->|内容违规(低置信度)| MarkLowConf[标记为疑似垃圾<br/>等待人工审核]
CheckResult -->|内容正常/用户名违规/无邮箱| TrashNoEmail[移入回收站]
CheckResult -->|内容正常/用户名违规/有邮箱| ChangeUsername[修改用户名]
CheckResult -->|都正常| MarkNormal[标记为正常评论]
CheckAction1 -->|trash| TrashComment[移入回收站]
CheckAction1 -->|hold| HoldComment[标记为待审核]
CheckAction1 -->|mark| MarkOnly[仅标记不处理]
TrashComment --> SendSpamEmail[发送垃圾评论通知]
HoldComment --> SendSpamEmail
MarkOnly --> SaveResult1[保存检测结果]
SendSpamEmail --> SaveResult1
MarkLowConf --> SaveResult2[保存检测结果]
TrashNoEmail --> SaveResult3[保存检测结果]
ChangeUsername --> GenNewName[生成新用户名]
GenNewName --> UpdateDB[更新数据库]
UpdateDB --> SendChangeEmail[发送用户名变更通知]
SendChangeEmail --> SaveResult4[保存检测结果]
MarkNormal --> SaveResult5[保存检测结果]
SaveResult1 --> AILearn{启用 AI 学习?}
SaveResult2 --> AILearn
SaveResult3 --> AILearn
SaveResult4 --> AILearn
SaveResult5 --> AILearn
AILearn -->|是| ExtractKeywords[提取关键词]
AILearn -->|否| DetectEnd([检测完成])
ExtractKeywords --> UpdateKeywords[更新学习关键词库]
UpdateKeywords --> DetectEnd
ShowPending --> UserView([用户看到:审核中状态])
DetectEnd --> AdminReview([管理员审核])
style Start fill:#e1f5e1,stroke:#2e7d32,stroke-width:2px
style End1 fill:#ffe1e1,stroke:#c62828,stroke-width:2px
style End2 fill:#ffe1e1,stroke:#c62828,stroke-width:2px
style DetectEnd fill:#e1f5e1,stroke:#2e7d32,stroke-width:2px
style UserView fill:#fff4e1,stroke:#ff8f00,stroke-width:2px
style AdminReview fill:#e1e8ff,stroke:#1565c0,stroke-width:2px
style NeedCheck fill:#ffcccc,stroke:#c62828,stroke-width:1px
style SetPending fill:#ffcccc,stroke:#c62828,stroke-width:1px
style TrashComment fill:#ff6b6b,stroke:#c62828,stroke-width:1px
style TrashNoEmail fill:#ff6b6b,stroke:#c62828,stroke-width:1px
style HoldComment fill:#ffa500,stroke:#ff8f00,stroke-width:1px
style MarkLowConf fill:#ffa500,stroke:#ff8f00,stroke-width:1px
style ChangeUsername fill:#4ecdc4,stroke:#00897b,stroke-width:1px
style MarkNormal fill:#95e1d3,stroke:#2e7d32,stroke-width:1px
:::
## 流程关键节点说明
### 1. 预处理阶段
- 触发 `preprocess_comment`/`post_comment_preprocessing` 钩子,完成评论基础清洗。
- 优先判断是否启用 AI 检测,未启用则直接保存评论。
### 2. 检测规则判断
- 检测模式分 4 类manual关闭、keyword关键字、sample抽样、all全量
- 白名单用户、已登录且配置跳过的用户,直接跳过检测。
- keyword/sample 模式下,仅关键字触发/随机抽中时才启动检测。
### 3. AI 检测执行
- 关键字触发:立即同步检测;其他情况:延迟 1 秒异步检测。
- 调用 `argon_detect_spam_comment` 接口,拼接用户名+评论内容作为上下文发送 AI。
### 4. 结果处理逻辑
| 检测结果 | 处理方式 |
|-------------------------|-----------------------------------|
| 内容违规(高置信度)| 按配置自动处理(移入回收站/待审核/仅标记) |
| 内容违规(低置信度)| 标记疑似垃圾,等待人工审核 |
| 用户名违规(无邮箱)| 直接移入回收站 |
| 用户名违规(有邮箱)| 自动生成新用户名并通知用户 |
| 内容+用户名均正常 | 标记为正常评论 |
### 5. 后续环节
- 所有检测结果均保存至数据库,支持 AI 学习功能(提取关键词更新词库)。
- 用户侧仅展示"审核中"状态,最终结果需管理员复核。
## 技术说明
### 使用的钩子
- `preprocess_comment` - 评论预处理
- `post_comment_preprocessing` - 评论预处理(备用)
- `comment_post` - 评论入库后
- `post_comment_updatemetas` - 更新评论元数据
- `argon_auto_detect_spam_on_comment` - 自动检测触发
### 关键函数
- `argon_detect_spam_comment()` - AI 检测主函数
- `argon_async_spam_detection_handler()` - 异步检测处理
- `argon_get_spam_detection_prompt()` - 获取检测 Prompt
- `argon_build_comment_context()` - 构建评论上下文
### 数据库字段
- `comment_approved` - 评论审核状态0=待审核1=已通过)
- `_argon_needs_spam_check` - 是否需要 AI 检测标记
- `_argon_spam_detection_result` - AI 检测结果
- `_argon_spam_detection_time` - 检测时间戳
- `_argon_spam_keywords` - 触发的关键字
## 配置建议
### 小型博客(评论量 < 100/天)
- 检测模式keyword关键字触发
- Prompt 模式:标准模式
- 自动处理阈值:置信度 > 0.9
### 中型博客(评论量 100-500/天)
- 检测模式sample智能抽样
- Prompt 模式:标准模式
- 自动处理阈值:置信度 > 0.85
### 大型博客(评论量 > 500/天)
- 检测模式sample智能抽样 30-40%
- Prompt 模式:极简模式
- 自动处理阈值:置信度 > 0.8
- 定期批量扫描待审核评论
## 注意事项
1. **API 成本控制**:合理设置检测模式和抽样比例
2. **误判处理**:始终保留人工审核入口
3. **隐私保护**:不要将敏感信息发送给 AI
4. **性能优化**:关键字触发使用同步检测,其他使用异步
5. **定期优化**:根据误判率调整 Prompt 和阈值

View File

@@ -0,0 +1,295 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mermaid 代码块魔改测试</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.test-case {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-case h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.test-case .description {
color: #666;
margin-bottom: 15px;
font-size: 14px;
}
.mermaid-from-codeblock {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
border: 1px solid #dee2e6;
}
pre {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
border: 1px solid #dee2e6;
overflow-x: auto;
}
code {
font-family: 'Courier New', Courier, monospace;
}
</style>
<!-- Mermaid 库 -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
</head>
<body>
<h1>Mermaid 代码块魔改测试</h1>
<p>本页面用于测试标准 Markdown 代码块 (<code>```mermaid</code>) 的 Mermaid 图表渲染功能。</p>
<!-- 测试用例 1标准格式 -->
<div class="test-case">
<h3>测试用例 1标准 Markdown 格式</h3>
<div class="description">
格式:<code>&lt;pre&gt;&lt;code class="language-mermaid"&gt;</code>
</div>
<pre><code class="language-mermaid">
flowchart TD
A[开始] --> B{判断}
B -->|是| C[执行操作]
B -->|否| D[跳过]
C --> E[结束]
D --> E
</code></pre>
</div>
<!-- 测试用例 2多个代码块 -->
<div class="test-case">
<h3>测试用例 2多个代码块批量处理</h3>
<div class="description">
测试同一页面中多个 Mermaid 代码块的处理
</div>
<pre><code class="language-mermaid">
graph LR
A[客户端] --> B[服务器]
B --> C[数据库]
C --> B
B --> A
</code></pre>
<br>
<pre><code class="language-mermaid">
sequenceDiagram
participant 用户
participant 系统
用户->>系统: 发送请求
系统->>用户: 返回响应
</code></pre>
</div>
<!-- 测试用例 3特殊字符 -->
<div class="test-case">
<h3>测试用例 3特殊字符保留</h3>
<div class="description">
测试箭头符号 <code>--&gt;</code>、双横线 <code>--</code>、等号 <code>==</code> 等特殊字符是否正确保留
</div>
<pre><code class="language-mermaid">
flowchart TD
A --> B
B -- 文本 --> C
C ==> D
D -.-> E
E ~~~ F
</code></pre>
</div>
<!-- 测试用例 4空代码块 -->
<div class="test-case">
<h3>测试用例 4空代码块边界情况</h3>
<div class="description">
测试空代码块是否会导致错误
</div>
<pre><code class="language-mermaid"></code></pre>
<p style="color: #666; font-size: 14px;">空代码块应该被跳过,不会创建容器</p>
</div>
<!-- 测试用例 5多行代码块 -->
<div class="test-case">
<h3>测试用例 5多行代码块换行符保留</h3>
<div class="description">
测试多行代码块中的换行符是否正确保留
</div>
<pre><code class="language-mermaid">
graph TB
subgraph 子图1
A1[节点A1]
A2[节点A2]
end
subgraph 子图2
B1[节点B1]
B2[节点B2]
end
A1 --> B1
A2 --> B2
</code></pre>
</div>
<!-- 测试用例 6简化格式 -->
<div class="test-case">
<h3>测试用例 6简化格式</h3>
<div class="description">
格式:<code>&lt;pre&gt;&lt;code class="mermaid"&gt;</code>
</div>
<pre><code class="mermaid">
pie title 数据分布
"类别A" : 45
"类别B" : 30
"类别C" : 25
</code></pre>
</div>
<!-- 测试用例 7无 pre 包裹 -->
<div class="test-case">
<h3>测试用例 7无 pre 包裹</h3>
<div class="description">
格式:<code>&lt;code class="language-mermaid"&gt;</code>(无 pre 标签)
</div>
<code class="language-mermaid">
flowchart LR
Start --> Stop
</code>
</div>
<!-- 测试用例 8自定义属性格式 -->
<div class="test-case">
<h3>测试用例 8自定义属性格式</h3>
<div class="description">
格式:<code>&lt;pre data-lang="mermaid"&gt;</code>
</div>
<pre data-lang="mermaid">
stateDiagram-v2
[*] --> 状态1
状态1 --> 状态2
状态2 --> [*]
</pre>
</div>
<!-- 测试用例 9复杂图表 -->
<div class="test-case">
<h3>测试用例 9复杂图表</h3>
<div class="description">
测试复杂的 Mermaid 图表渲染
</div>
<pre><code class="language-mermaid">
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
</code></pre>
</div>
<script>
// 初始化 Mermaid
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose'
});
// 模拟主题的 convertMermaidCodeblocks 函数
function convertMermaidCodeblocks(){
const selectors = [
'pre > code.language-mermaid',
'pre > code.mermaid',
'code.language-mermaid',
'pre[data-lang="mermaid"]'
];
let processedCount = 0;
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(element => {
if (element.dataset.mermaidProcessed) {
return;
}
let code = element.textContent.trim();
if (!code) {
return;
}
const container = document.createElement('div');
container.className = 'mermaid-from-codeblock';
container.textContent = code;
container.dataset.processed = 'true';
const targetElement = element.closest('pre') || element;
if (targetElement.parentNode) {
targetElement.parentNode.replaceChild(container, targetElement);
processedCount++;
}
element.dataset.mermaidProcessed = 'true';
});
});
console.log('[Mermaid] 转换了 ' + processedCount + ' 个代码块');
}
// 渲染 Mermaid 图表
function renderMermaidCharts(){
const blocks = document.querySelectorAll('.mermaid-from-codeblock');
console.log('[Mermaid] 找到 ' + blocks.length + ' 个待渲染的图表');
blocks.forEach((block, index) => {
const id = 'mermaid-' + Date.now() + '-' + index;
const code = block.textContent;
try {
mermaid.render(id, code).then(result => {
block.innerHTML = result.svg;
console.log('[Mermaid] 成功渲染图表 ' + (index + 1));
}).catch(error => {
console.error('[Mermaid] 渲染失败:', error);
block.innerHTML = '<div style="color: red; padding: 10px; border: 1px solid red; border-radius: 4px;">渲染失败: ' + error.message + '</div>';
});
} catch (error) {
console.error('[Mermaid] 渲染异常:', error);
}
});
}
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function(){
console.log('[测试] 开始执行代码块转换');
convertMermaidCodeblocks();
console.log('[测试] 开始渲染 Mermaid 图表');
setTimeout(renderMermaidCharts, 100);
});
</script>
</body>
</html>

View File

@@ -120,7 +120,7 @@ flowchart TD
return blocks;
}
// 提取代码块内容
// 提取代码块内容(使用改进后的正则)
function extractMermaidCode(element) {
let code = '';
@@ -128,25 +128,31 @@ flowchart TD
const scriptTag = element.querySelector('script');
if (scriptTag) {
const scriptContent = scriptTag.textContent || scriptTag.innerText;
console.log('[测试] 原始 script 内容:', scriptContent);
console.log('[测试] 原始 script 内容:', scriptContent.substring(0, 100));
// 匹配 document.write("...") 或 document.write('...')
let match = scriptContent.match(/document\.write\s*\(\s*"((?:[^"\\]|\\.)*)"\s*\)/s);
if (!match) {
match = scriptContent.match(/document\.write\s*\(\s*'((?:[^'\\]|\\.)*)'\s*\)/s);
}
// 使用改进后的正则:[\s\S]*? 匹配包括换行在内的所有字符
let match = scriptContent.match(/document\.write\s*\(\s*["']([\s\S]*?)["']\s*\)/);
if (match && match[1]) {
code = match[1];
console.log('[测试] 提取到代码(转义前):', code);
console.log('[测试] 从 document.write() 提取到代码,长度:', code.length);
} else {
const clonedElement = element.cloneNode(true);
const scripts = clonedElement.querySelectorAll('script');
scripts.forEach(script => script.remove());
code = clonedElement.textContent;
// 降级方案:直接提取引号内容
match = scriptContent.match(/["']([\s\S]*?)["']/);
if (match && match[1]) {
code = match[1];
console.log('[测试] 从引号内提取到代码,长度:', code.length);
} else {
const clonedElement = element.cloneNode(true);
const scripts = clonedElement.querySelectorAll('script');
scripts.forEach(script => script.remove());
code = clonedElement.textContent;
console.log('[测试] 使用降级方案提取代码');
}
}
} else {
code = element.textContent;
console.log('[测试] 从纯文本提取代码');
}
} else if (element.tagName === 'CODE') {
code = element.textContent;
@@ -172,7 +178,7 @@ flowchart TD
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\');
console.log('[测试] 最终提取的代码:', code);
console.log('[测试] 最终提取的代码:', code.substring(0, 100) + (code.length > 100 ? '...' : ''));
return code.trim();
}