feat: 初始化知识库系统项目

- 实现基于 Laravel 11 和 Filament 3.X 的文档管理系统
- 添加用户认证和分组管理功能
- 实现文档上传、分类和权限控制
- 集成 Word 文档自动转换为 Markdown
- 集成 Meilisearch 全文搜索引擎
- 实现文档在线预览功能
- 添加安全日志和审计功能
- 完整的简体中文界面
- 包含完整的项目文档和部署指南

技术栈:
- Laravel 11.x
- Filament 3.X
- Meilisearch 1.5+
- Pandoc 文档转换
- Redis 队列系统
- Pest PHP 测试框架
This commit is contained in:
Knowledge Base System
2025-12-05 14:44:44 +08:00
commit acf549c43c
165 changed files with 32838 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
<?php
namespace App\Services;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
class MarkdownRenderService
{
protected MarkdownConverter $converter;
protected bool $sanitize;
public function __construct()
{
// 从配置文件读取设置
$this->sanitize = config('documents.markdown.sanitize', true);
// 创建环境配置
$config = [
'html_input' => $this->sanitize ? 'strip' : 'allow', // 根据配置决定是否剥离 HTML 标签
'allow_unsafe_links' => false, // 不允许不安全的链接
'max_nesting_level' => 10, // 最大嵌套层级
];
// 创建环境并添加扩展
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension()); // 支持表格
$environment->addExtension(new StrikethroughExtension()); // 支持删除线
$environment->addExtension(new TaskListExtension()); // 支持任务列表
// 创建转换器
$this->converter = new MarkdownConverter($environment);
}
/**
* Markdown 内容渲染为 HTML
*
* @param string $markdown Markdown 内容
* @return string 渲染后的 HTML
*/
public function render(string $markdown): string
{
try {
// 转换 Markdown 为 HTML
$html = $this->converter->convert($markdown)->getContent();
// 清理和美化 HTML
$html = $this->sanitize($html);
return $html;
} catch (\Exception $e) {
// 如果渲染失败,返回错误信息
return '<div class="alert alert-danger">Markdown 渲染失败:' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
/**
* 清理 HTML 内容,防止 XSS 攻击
*
* @param string $html HTML 内容
* @return string 清理后的 HTML
*/
public function sanitize(string $html): string
{
// CommonMark 已经配置了 html_input => 'strip',会自动剥离 HTML 标签
// 这里我们添加额外的样式包装
$styledHtml = '<div class="markdown-content" style="
font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', Arial, sans-serif;
line-height: 1.6;
color: #333;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
">';
// 添加基本的 Markdown 样式
$styledHtml .= $this->getMarkdownStyles();
$styledHtml .= $html;
$styledHtml .= '</div>';
return $styledHtml;
}
/**
* 获取 Markdown 内容的 CSS 样式
*
* @return string CSS 样式
*/
protected function getMarkdownStyles(): string
{
return '<style>
.markdown-content h1 { font-size: 2em; margin-top: 0.67em; margin-bottom: 0.67em; font-weight: bold; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
.markdown-content h2 { font-size: 1.5em; margin-top: 0.83em; margin-bottom: 0.83em; font-weight: bold; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
.markdown-content h3 { font-size: 1.17em; margin-top: 1em; margin-bottom: 1em; font-weight: bold; }
.markdown-content h4 { font-size: 1em; margin-top: 1.33em; margin-bottom: 1.33em; font-weight: bold; }
.markdown-content h5 { font-size: 0.83em; margin-top: 1.67em; margin-bottom: 1.67em; font-weight: bold; }
.markdown-content h6 { font-size: 0.67em; margin-top: 2.33em; margin-bottom: 2.33em; font-weight: bold; }
.markdown-content p { margin: 1em 0; }
.markdown-content ul, .markdown-content ol { margin: 1em 0; padding-left: 2em; }
.markdown-content li { margin: 0.5em 0; }
.markdown-content code {
background-color: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
font-family: \'SFMono-Regular\', \'Consolas\', \'Liberation Mono\', \'Menlo\', \'Courier\', monospace;
font-size: 0.9em;
color: #24292e;
}
.markdown-content pre {
background-color: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
margin: 1em 0;
border: 1px solid #e1e4e8;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
border: none;
}
.markdown-content blockquote {
border-left: 4px solid #dfe2e5;
padding-left: 16px;
margin: 1em 0;
color: #6a737d;
}
.markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
display: block;
overflow-x: auto;
}
.markdown-content table th, .markdown-content table td {
border: 1px solid #dfe2e5;
padding: 8px 12px;
text-align: left;
}
.markdown-content table th {
background-color: #f6f8fa;
font-weight: bold;
}
.markdown-content table tr:nth-child(even) {
background-color: #f6f8fa;
}
.markdown-content a {
color: #0366d6;
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.markdown-content hr {
border: none;
border-top: 2px solid #e1e4e8;
margin: 2em 0;
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.markdown-content del {
text-decoration: line-through;
color: #6a737d;
}
.markdown-content input[type="checkbox"] {
margin-right: 0.5em;
}
</style>';
}
/**
* Markdown 内容中提取摘要
*
* @param string $markdown Markdown 内容
* @param int|null $length 摘要长度(字符数),如果为 null 则使用配置文件中的默认值
* @return string 摘要文本
*/
public function extractPreview(string $markdown, ?int $length = null): string
{
// 如果未指定长度,使用配置文件中的默认值
if ($length === null) {
$length = config('documents.markdown.preview_length', 500);
}
// 移除 Markdown 标记,获取纯文本
$text = $this->stripMarkdown($markdown);
// 移除多余的空白字符
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
// 截取指定长度
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length) . '...';
}
return $text;
}
/**
* 移除 Markdown 标记,返回纯文本
*
* @param string $markdown Markdown 内容
* @return string 纯文本
*/
protected function stripMarkdown(string $markdown): string
{
// 移除代码块
$text = preg_replace('/```[\s\S]*?```/', '', $markdown);
$text = preg_replace('/`[^`]+`/', '', $text);
// 移除标题标记
$text = preg_replace('/^#{1,6}\s+/m', '', $text);
// 移除链接,保留文本
$text = preg_replace('/\[([^\]]+)\]\([^\)]+\)/', '$1', $text);
// 移除图片
$text = preg_replace('/!\[([^\]]*)\]\([^\)]+\)/', '', $text);
// 移除粗体和斜体标记
$text = preg_replace('/\*\*([^\*]+)\*\*/', '$1', $text);
$text = preg_replace('/\*([^\*]+)\*/', '$1', $text);
$text = preg_replace('/__([^_]+)__/', '$1', $text);
$text = preg_replace('/_([^_]+)_/', '$1', $text);
// 移除删除线
$text = preg_replace('/~~([^~]+)~~/', '$1', $text);
// 移除引用标记
$text = preg_replace('/^>\s+/m', '', $text);
// 移除列表标记
$text = preg_replace('/^[\*\-\+]\s+/m', '', $text);
$text = preg_replace('/^\d+\.\s+/m', '', $text);
// 移除水平线
$text = preg_replace('/^[\-\*_]{3,}$/m', '', $text);
return $text;
}
}