Merge pull request #772 from liangliangyy/dev

插件能力
且听风吟 7 months ago committed by GitHub
commit cf2c4854fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -17,6 +17,8 @@ from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
@ -154,7 +156,14 @@ class ArticleDetailView(DetailView):
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
return super(ArticleDetailView, self).get_context_data(**kwargs)
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, request=self.request)
return context
class CategoryDetailView(ArticleListView):

@ -0,0 +1,41 @@
import logging
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
self.init_plugin()
self.register_hooks()
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}

@ -0,0 +1,7 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -0,0 +1,44 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
"""
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
@ -20,9 +21,8 @@ def env_to_bool(env, default):
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
@ -330,3 +330,20 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
]
# 加载插件
for plugin_dir in os.listdir(PLUGINS_DIR):
plugin_path = os.path.join(PLUGINS_DIR, plugin_dir)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, '__init__.py')):
try:
__import__(f'plugins.{plugin_dir}.plugin')
except ImportError as e:
print("Failed to import plugin:", plugin_dir, e)

@ -0,0 +1 @@
# This file makes this a Python package

@ -0,0 +1 @@
# This file makes this a Python package

@ -0,0 +1,32 @@
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ArticleCopyrightPlugin(BasePlugin):
PLUGIN_NAME = '文章结尾版权声明'
PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。'
PLUGIN_VERSION = '0.2.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 2. 实现 register_hooks 方法,专门用于注册钩子
def register_hooks(self):
# 在这里将插件的方法注册到指定的钩子上
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content)
def add_copyright_to_content(self, content, *args, **kwargs):
"""
这个方法会被注册到 'the_content' 过滤器钩子上
它接收原始内容并返回添加了版权信息的新内容
"""
article = kwargs.get('article')
if not article:
return content
copyright_info = f"\n<hr><p>本文由 {article.author.username} 原创,转载请注明出处。</p>"
return content + copyright_info
# 3. 实例化插件。
# 这会自动调用 BasePlugin.__init__然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。
plugin = ArticleCopyrightPlugin()

@ -0,0 +1 @@
# This file makes this a Python package

@ -0,0 +1,48 @@
import re
from urllib.parse import urlparse
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ExternalLinksPlugin(BasePlugin):
PLUGIN_NAME = '外部链接处理器'
PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。'
PLUGIN_VERSION = '0.1.0'
PLUGIN_AUTHOR = 'liangliangyy'
def register_hooks(self):
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links)
def process_external_links(self, content, *args, **kwargs):
from djangoblog.utils import get_current_site
site_domain = get_current_site().domain
# 正则表达式查找所有 <a> 标签
link_pattern = re.compile(r'(<a\s+(?:[^>]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE)
def replacer(match):
# match.group(1) 是 <a ... href="
# match.group(2) 是链接 URL
# match.group(3) 是 ">...</a>
href = match.group(2)
# 如果链接已经有 target 属性,则不处理
if 'target=' in match.group(0).lower():
return match.group(0)
# 解析链接
parsed_url = urlparse(href)
# 如果链接是外部的 (有域名且域名不等于当前网站域名)
if parsed_url.netloc and parsed_url.netloc != site_domain:
# 添加 target 和 rel 属性
return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}'
# 否则返回原样
return match.group(0)
return link_pattern.sub(replacer, content)
plugin = ExternalLinksPlugin()

@ -0,0 +1 @@
# This file makes this a Python package

@ -0,0 +1,43 @@
import math
import re
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ReadingTimePlugin(BasePlugin):
PLUGIN_NAME = '阅读时间预测'
PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。'
PLUGIN_VERSION = '0.1.0'
PLUGIN_AUTHOR = 'liangliangyy'
def register_hooks(self):
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time)
def add_reading_time(self, content, *args, **kwargs):
"""
计算阅读时间并添加到内容开头
"""
# 移除HTML标签和空白字符以获得纯文本
clean_content = re.sub(r'<[^>]*>', '', content)
clean_content = clean_content.strip()
# 中文和英文单词混合计数的一个简单方法
# 匹配中文字符或连续的非中文字符(视为单词)
words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content)
word_count = len(words)
# 按平均每分钟200字的速度计算
reading_speed = 200
reading_minutes = math.ceil(word_count / reading_speed)
# 如果阅读时间少于1分钟则显示为1分钟
if reading_minutes < 1:
reading_minutes = 1
reading_time_html = f'<p style="color: #888;"><em>预计阅读时间:{reading_minutes} 分钟</em></p>'
return reading_time_html + content
plugin = ReadingTimePlugin()
Loading…
Cancel
Save