From 9f639cfbe8d78a5d5518c3e7c10e3860fbdeb50d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=BF=8A=E9=BE=99?= <2831713936@qq.com> Date: Wed, 15 Oct 2025 23:17:22 +0800 Subject: [PATCH] =?UTF-8?q?severmanager=E4=BB=A3=E7=A0=81=E6=A0=87?= =?UTF-8?q?=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/djangoBlogStudy.iml | 24 +- .idea/modules.xml | 4 - src/servermanager/MemcacheStorage.py | 96 +++++++- src/servermanager/admin.py | 33 ++- src/servermanager/api/blogapi.py | 73 +++++- src/servermanager/api/commonapi.py | 97 ++++++-- src/servermanager/apps.py | 18 ++ src/servermanager/migrations/0001_initial.py | 51 ++-- ...002_alter_emailsendlog_options_and_more.py | 49 ++-- src/servermanager/models.py | 67 ++++- src/servermanager/robot.py | 233 +++++++++++++++--- src/servermanager/tests.py | 127 +++++++--- src/servermanager/urls.py | 32 +++ src/servermanager/views.py | 3 + 14 files changed, 762 insertions(+), 145 deletions(-) diff --git a/.idea/djangoBlogStudy.iml b/.idea/djangoBlogStudy.iml index 8b8c395..8b5895a 100644 --- a/.idea/djangoBlogStudy.iml +++ b/.idea/djangoBlogStudy.iml @@ -1,7 +1,21 @@ + + + + + + - + + + @@ -9,4 +23,12 @@ \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index dc7543e..eb9a5a1 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,11 +2,7 @@ -<<<<<<< HEAD -======= - ->>>>>>> master \ No newline at end of file diff --git a/src/servermanager/MemcacheStorage.py b/src/servermanager/MemcacheStorage.py index 38a7990..0ad8021 100644 --- a/src/servermanager/MemcacheStorage.py +++ b/src/servermanager/MemcacheStorage.py @@ -1,32 +1,108 @@ -from werobot.session import SessionStorage -from werobot.utils import json_loads, json_dumps +from werobot.session import SessionStorage # WeRoBot 框架的会话存储基类 +from werobot.utils import json_loads, json_dumps # WeRoBot 提供的 JSON 序列化/反序列化工具 -from djangoblog.utils import cache +from djangoblog.utils import cache # 项目中封装的缓存对象(如 Redis 或 Memcached) class MemcacheStorage(SessionStorage): + """ + 自定义的会话存储类,基于缓存系统(如 Redis/Memcached)实现 WeRoBot 微信机器人的会话管理。 + 用于在微信消息处理过程中持久化用户会话数据(如上下文、状态等)。 + + 通过将用户 ID 作为键,会话数据作为值,存储在缓存中,实现跨请求的会话跟踪。 + """ + def __init__(self, prefix='ws_'): - self.prefix = prefix - self.cache = cache + """ + 初始化 MemcacheStorage 实例。 + + 参数: + prefix (str): 缓存键的前缀,默认为 'ws_'(weobot session 的缩写),用于避免键名冲突。 + + 功能: + - 设置键名前缀 + - 绑定项目全局的缓存实例(cache) + """ + self.prefix = prefix # 设置键名前缀 + self.cache = cache # 使用项目中已配置的缓存后端(如 Redis) @property def is_available(self): + """ + 检查当前会话存储是否可用(即缓存系统是否正常工作)。 + + 实现方式: + 1. 尝试写入一个测试键值对('checkavaliable': '1') + 2. 立即读取该键的值 + 3. 如果读取的值与写入的值一致,则认为存储可用 + + 返回: + bool: True 表示缓存可用;False 表示不可用(如缓存服务宕机) + """ value = "1" - self.set('checkavaliable', value=value) - return value == self.get('checkavaliable') + self.set('checkavaliable', value=value) # 写入测试数据 + return value == self.get('checkavaliable') # 验证能否正确读取 def key_name(self, s): + """ + 为给定的会话 ID 生成带前缀的完整缓存键名。 + + 参数: + s (str): 原始会话 ID(通常是微信用户的 OpenID) + + 返回: + str: 格式为 '{prefix}{s}' 的完整键名,用于避免命名冲突。 + + 示例: + key_name('abc123') -> 'ws_abc123' + """ return '{prefix}{s}'.format(prefix=self.prefix, s=s) def get(self, id): + """ + 根据会话 ID 从缓存中获取会话数据。 + + 参数: + id (str): 会话 ID(如用户 OpenID) + + 返回: + dict: 会话数据的字典;如果缓存中不存在,则返回空字典 {} + + 流程: + 1. 使用 key_name() 生成带前缀的键 + 2. 从缓存中获取 JSON 字符串 + 3. 使用 json_loads 将 JSON 字符串反序列化为 Python 字典 + """ id = self.key_name(id) - session_json = self.cache.get(id) or '{}' + session_json = self.cache.get(id) or '{}' # 如果未找到,返回空 JSON 字符串 return json_loads(session_json) def set(self, id, value): + """ + 将会话数据存储到缓存中。 + + 参数: + id (str): 会话 ID + value (dict): 要存储的会话数据(Python 字典) + + 流程: + 1. 使用 key_name() 生成带前缀的键 + 2. 将字典序列化为 JSON 字符串 + 3. 存入缓存(使用默认过期时间) + """ id = self.key_name(id) - self.cache.set(id, json_dumps(value)) + self.cache.set(id, json_dumps(value)) # 序列化并存储 def delete(self, id): + """ + 根据会话 ID 从缓存中删除会话数据。 + + 参数: + id (str): 会话 ID + + 流程: + 1. 使用 key_name() 生成带前缀的键 + 2. 调用缓存的 delete 方法删除该键 + """ id = self.key_name(id) - self.cache.delete(id) + self.cache.delete(id) \ No newline at end of file diff --git a/src/servermanager/admin.py b/src/servermanager/admin.py index f26f4f6..83bc509 100644 --- a/src/servermanager/admin.py +++ b/src/servermanager/admin.py @@ -1,19 +1,48 @@ from django.contrib import admin + # Register your models here. +# 本文件用于将数据模型注册到 Django 管理后台(Admin),并自定义其展示和操作方式。 +# 当前未立即注册模型,需在下方通过 admin.site.register() 显式注册。 class CommandsAdmin(admin.ModelAdmin): + """ + Django Admin 配置类,用于自定义 'commands' 模型在管理后台中的列表展示。 + 控制在模型列表页面显示哪些字段。 + """ + + # 定义在管理后台的模型列表页面中要显示的字段列 + # 用户将看到三列:'title'(命令标题)、'command'(命令内容)、'describe'(描述) list_display = ('title', 'command', 'describe') class EmailSendLogAdmin(admin.ModelAdmin): + """ + Django Admin 配置类,用于自定义 'EmailSendLog' 模型在管理后台中的行为。 + 该日志模型主要用于查看,禁止添加新记录,并将所有字段设为只读。 + """ + + # 定义在管理后台的模型列表页面中要显示的字段列 + # 显示:邮件标题、收件人、发送结果、创建时间 list_display = ('title', 'emailto', 'send_result', 'creation_time') + + # 定义在编辑或查看单条记录时,哪些字段为只读(不可编辑) + # 所有字段均设为只读,防止日志被意外修改 readonly_fields = ( 'title', 'emailto', 'send_result', 'creation_time', - 'content') + 'content' + ) + # 重写权限方法,禁止用户在管理后台添加新的 EmailSendLog 记录 + # 因为邮件日志应由程序自动创建,不允许手动添加 + # + # 参数: + # request: 当前的 HTTP 请求对象 + # + # 返回: + # bool: 始终返回 False,表示禁止添加操作 def has_add_permission(self, request): - return False + return False \ No newline at end of file diff --git a/src/servermanager/api/blogapi.py b/src/servermanager/api/blogapi.py index 8a4d6ac..3eda38c 100644 --- a/src/servermanager/api/blogapi.py +++ b/src/servermanager/api/blogapi.py @@ -4,24 +4,77 @@ from blog.models import Article, Category class BlogApi: + """ + 博客 API 类,封装了与博客相关的数据查询功能,包括全文搜索、分类获取、分类文章获取和最新文章获取。 + """ + def __init__(self): - self.searchqueryset = SearchQuerySet() - self.searchqueryset.auto_query('') - self.__max_takecount__ = 8 + """ + 初始化 BlogApi 实例。 + 创建一个 SearchQuerySet 对象用于全文搜索,并执行一个空查询以准备搜索环境。 + 同时设置默认的最大返回记录数。 + """ + self.searchqueryset = SearchQuerySet() # Haystack 的搜索查询集对象 + self.searchqueryset.auto_query('') # 执行空查询,初始化搜索状态 + self.__max_takecount__ = 8 # 设置最大返回文章数量为 8 篇 def search_articles(self, query): - sqs = self.searchqueryset.auto_query(query) - sqs = sqs.load_all() - return sqs[:self.__max_takecount__] + """ + 根据用户输入的关键词查询文章。 + + 参数: + query (str): 用户输入的搜索关键词。 + + 返回: + QuerySet: 包含匹配文章的查询集(最多返回 __max_takecount__ 篇文章), + 并已加载文章的完整模型实例(通过 load_all)。 + + 说明: + 使用 Haystack 的 auto_query 进行全文搜索,并加载所有关联的文章对象。 + """ + sqs = self.searchqueryset.auto_query(query) # 根据关键词执行全文搜索 + sqs = sqs.load_all() # 加载搜索结果对应的真实模型实例(Article 对象) + return sqs[:self.__max_takecount__] # 返回最多 __max_takecount__ 条搜索结果 def get_category_lists(self): + """ + 获取所有文章分类的列表。 + + 返回: + QuerySet: 包含数据库中所有 Category 对象的查询集。 + + 说明: + 此方法返回所有已创建的分类,可用于展示分类导航等。 + """ return Category.objects.all() def get_category_articles(self, categoryname): - articles = Article.objects.filter(category__name=categoryname) + """ + 根据分类名称获取该分类下的文章列表。 + + 参数: + categoryname (str): 分类的名称。 + + 返回: + QuerySet 或 None: 如果存在该分类的文章,则返回最多 __max_takecount__ 篇文章的查询集; + 如果该分类下没有文章,则返回 None。 + + 说明: + 通过文章模型的外键关系(category__name)进行过滤,获取指定分类的文章。 + """ + articles = Article.objects.filter(category__name=categoryname) # 查询指定分类的文章 if articles: - return articles[:self.__max_takecount__] - return None + return articles[:self.__max_takecount__] # 存在文章则返回最多 __max_takecount__ 篇 + return None # 无文章则返回 None def get_recent_articles(self): - return Article.objects.all()[:self.__max_takecount__] + """ + 获取最新的文章列表(按模型默认排序,通常是按创建时间倒序)。 + + 返回: + QuerySet: 包含最新文章的查询集(最多返回 __max_takecount__ 篇文章)。 + + 说明: + 此方法返回数据库中最新的若干篇文章,可用于展示“最新文章”或“热门文章”。 + """ + return Article.objects.all()[:self.__max_takecount__] \ No newline at end of file diff --git a/src/servermanager/api/commonapi.py b/src/servermanager/api/commonapi.py index 83ad9ff..47be2b0 100644 --- a/src/servermanager/api/commonapi.py +++ b/src/servermanager/api/commonapi.py @@ -5,60 +5,129 @@ import openai from servermanager.models import commands +# 获取当前模块的 logger,用于记录日志 logger = logging.getLogger(__name__) +# 从环境变量中读取 OpenAI API 密钥 openai.api_key = os.environ.get('OPENAI_API_KEY') + +# 如果设置了 HTTP 代理,则配置 openai 库使用该代理 if os.environ.get('HTTP_PROXY'): openai.proxy = os.environ.get('HTTP_PROXY') class ChatGPT: + """ + 封装与 OpenAI 的 ChatGPT 模型进行交互的功能。 + 提供静态方法 `chat` 用于发送用户提示并获取模型回复。 + """ @staticmethod def chat(prompt): + """ + 调用 OpenAI 的 GPT-3.5 Turbo 模型生成回复。 + + 参数: + prompt (str): 用户输入的提示文本(问题或请求)。 + + 返回: + str: 模型生成的回复内容;如果调用失败,则返回错误提示。 + + 流程: + 1. 使用 openai.ChatCompletion.create 发起请求,指定模型为 "gpt-3.5-turbo"。 + 2. 将用户提示作为 role="user" 的消息发送。 + 3. 提取并返回模型返回的第一条消息内容。 + 4. 如果发生异常(如网络错误、认证失败等),记录错误日志并返回友好提示。 + """ try: - completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", - messages=[{"role": "user", "content": prompt}]) - return completion.choices[0].message.content + completion = openai.ChatCompletion.create( + model="gpt-3.5-turbo", # 使用 GPT-3.5 Turbo 模型 + messages=[{"role": "user", "content": prompt}] # 构造对话消息 + ) + return completion.choices[0].message.content # 返回模型生成的文本 except Exception as e: - logger.error(e) - return "服务器出错了" + logger.error(e) # 记录异常信息到日志 + return "服务器出错了" # 返回用户友好的错误提示 class CommandHandler: + """ + 命令处理器类,用于管理、查找和执行预定义的系统命令。 + 从数据库加载命令列表,支持通过名称查找并执行命令,以及获取帮助信息。 + """ + def __init__(self): - self.commands = commands.objects.all() + """ + 初始化 CommandHandler 实例。 + 从数据库中加载所有预定义的命令对象,存储在实例变量 self.commands 中。 + """ + self.commands = commands.objects.all() # 查询数据库中所有命令记录 def run(self, title): """ - 运行命令 - :param title: 命令 - :return: 返回命令执行结果 + 根据命令标题查找并执行对应的系统命令。 + + 参数: + title (str): 用户输入的命令标题。 + + 返回: + str: 命令执行后的输出结果;如果未找到命令,则返回帮助提示。 + + 流程: + 1. 在 self.commands 列表中查找 title(忽略大小写)匹配的命令。 + 2. 如果找到匹配的命令,调用私有方法 __run_command__ 执行其对应的操作。 + 3. 如果未找到,则返回提示信息,引导用户输入 'hepme' 获取帮助。 """ cmd = list( filter( - lambda x: x.title.upper() == title.upper(), + lambda x: x.title.upper() == title.upper(), # 忽略大小写比较 self.commands)) if cmd: - return self.__run_command__(cmd[0].command) + return self.__run_command__(cmd[0].command) # 执行找到的命令 else: return "未找到相关命令,请输入hepme获得帮助。" def __run_command__(self, cmd): + """ + 执行给定的系统命令(shell 命令)。 + + 参数: + cmd (str): 要执行的系统命令字符串。 + + 返回: + str: 命令执行的标准输出;如果执行出错,则返回错误提示。 + + 说明: + 使用 os.popen 打开一个管道来执行命令,并读取其输出。 + 捕获所有异常(包括执行失败、权限问题等),防止程序崩溃。 + """ try: - res = os.popen(cmd).read() + res = os.popen(cmd).read() # 执行命令并读取输出 return res except BaseException: return '命令执行出错!' def get_help(self): + """ + 生成帮助信息,列出所有可用的命令及其描述。 + + 返回: + str: 格式化的帮助文本,每行包含一个命令的标题和描述,用冒号分隔。 + + 说明: + 遍历 self.commands 中的所有命令,拼接成一个字符串,用于向用户展示可用命令。 + """ rsp = '' for cmd in self.commands: - rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) + rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) # 拼接命令帮助信息 return rsp if __name__ == '__main__': + """ + 主程序入口,用于测试 ChatGPT 类的功能。 + 创建 ChatGPT 实例,发送一个测试提示,并打印模型回复。 + """ chatbot = ChatGPT() prompt = "写一篇1000字关于AI的论文" - print(chatbot.chat(prompt)) + print(chatbot.chat(prompt)) \ No newline at end of file diff --git a/src/servermanager/apps.py b/src/servermanager/apps.py index 03cc38d..154f901 100644 --- a/src/servermanager/apps.py +++ b/src/servermanager/apps.py @@ -1,5 +1,23 @@ +# 导入 AppConfig,它是 Django 中用于配置应用程序的基类 from django.apps import AppConfig class ServermanagerConfig(AppConfig): + """ + 这是 'servermanager' 应用的配置类。 + 它继承自 Django 的 AppConfig,用于定义该应用在 Django 项目中的元数据和初始化行为。 + + 当 Django 启动时,会扫描 INSTALLED_APPS 中的应用,并加载其对应的 AppConfig。 + 此类的作用是告诉 Django 这个应用的基本信息,例如它的 Python 路径(name)。 + """ + + # 指定该应用的完整 Python 导入路径 + # Django 使用此属性来唯一标识和导入 'servermanager' 应用 + # 通常与应用所在文件夹的名称一致 name = 'servermanager' + + # 注意: + # - 该类目前只设置了最基本的 name 属性。 + # - 如果需要在应用启动时执行某些初始化代码,可以重写 ready() 方法。 + # 例如:注册信号处理器、启动后台任务等。 + # - 由于当前没有复杂的初始化需求,因此未定义其他方法或属性。 diff --git a/src/servermanager/migrations/0001_initial.py b/src/servermanager/migrations/0001_initial.py index bbdbf77..799c1fc 100644 --- a/src/servermanager/migrations/0001_initial.py +++ b/src/servermanager/migrations/0001_initial.py @@ -1,45 +1,68 @@ -# Generated by Django 4.1.7 on 2023-03-02 07:14 - from django.db import migrations, models class Migration(migrations.Migration): + """ + Django 迁移类,定义了数据库模式的变更。 + 此迁移是应用的初始迁移(initial=True),用于创建两个新的数据模型表: + - commands:存储可执行命令的信息 + - EmailSendLog:记录邮件发送的日志信息 + """ + # 标记此迁移为“初始迁移”,表示这是应用的第一个迁移文件 initial = True + # 定义此迁移所依赖的其他迁移 + # 当前为空列表,表示该迁移不依赖于任何其他迁移(即它是最初的) dependencies = [ ] + # 定义此迁移要执行的数据库操作列表 operations = [ + # 第一个操作:创建名为 'commands' 的数据模型表 migrations.CreateModel( - name='commands', - fields=[ + name='commands', # 数据库表对应的模型名称 + fields=[ # 定义该模型包含的字段(即数据库表的列) + # 主键 ID 字段:自增的 BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # 命令标题字段:最大长度 300 的字符串 ('title', models.CharField(max_length=300, verbose_name='命令标题')), + # 实际执行的命令字段:最大长度 2000 的字符串 ('command', models.CharField(max_length=2000, verbose_name='命令')), + # 命令描述字段:最大长度 300 的字符串 ('describe', models.CharField(max_length=300, verbose_name='命令描述')), + # 创建时间字段:自动在对象创建时设置当前时间 ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + # 最后修改时间字段:每次对象保存时自动更新为当前时间 ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), ], - options={ - 'verbose_name': '命令', - 'verbose_name_plural': '命令', + options={ # 模型的元选项配置 + 'verbose_name': '命令', # 单数形式的可读名称 + 'verbose_name_plural': '命令', # 复数形式的可读名称(中文通常与单数相同) }, ), + + # 第二个操作:创建名为 'EmailSendLog' 的数据模型表 migrations.CreateModel( - name='EmailSendLog', - fields=[ + name='EmailSendLog', # 数据库表对应的模型名称 + fields=[ # 定义该模型包含的字段(即数据库表的列) + # 主键 ID 字段:自增的 BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # 收件人邮箱字段:最大长度 300 的字符串 ('emailto', models.CharField(max_length=300, verbose_name='收件人')), + # 邮件标题字段:最大长度 2000 的字符串 ('title', models.CharField(max_length=2000, verbose_name='邮件标题')), + # 邮件内容字段:长文本字段 ('content', models.TextField(verbose_name='邮件内容')), + # 发送结果字段:布尔值,默认为 False(表示失败) ('send_result', models.BooleanField(default=False, verbose_name='结果')), + # 创建时间字段:自动在记录创建时设置当前时间 ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ], - options={ - 'verbose_name': '邮件发送log', - 'verbose_name_plural': '邮件发送log', - 'ordering': ['-created_time'], + options={ # 模型的元选项配置 + 'verbose_name': '邮件发送log', # 单数形式的可读名称 + 'verbose_name_plural': '邮件发送log', # 复数形式的可读名称 + 'ordering': ['-created_time'], # 默认排序规则:按创建时间倒序排列(最新的在前) }, ), - ] + ] \ No newline at end of file diff --git a/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py index 4858857..83d86d5 100644 --- a/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py +++ b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py @@ -1,32 +1,51 @@ -# Generated by Django 4.2.5 on 2023-09-06 13:19 - from django.db import migrations class Migration(migrations.Migration): + """ + Django 迁移类,定义了对数据库模式的变更。 + 此迁移文件基于 'servermanager' 应用的初始迁移(0001_initial)进行修改, + 主要目的是统一字段命名规范(如将 created_time 改为 creation_time)并更新模型排序规则。 + """ + # 定义此迁移所依赖的其他迁移 + # 依赖于 servermanager 应用的 0001_initial 迁移,确保基础表已存在 dependencies = [ - ('servermanager', '0001_initial'), + ('servermanager', '0001_initial'), # 格式:(应用名, 迁移文件名) ] + # 定义此迁移要执行的数据库操作列表 operations = [ + # 操作 1:修改模型 EmailSendLog 的元选项(Meta options) migrations.AlterModelOptions( - name='emailsendlog', - options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'}, + name='emailsendlog', # 要修改的模型名称 + options={ + # 更新排序字段为 'creation_time'(倒序),确保最新记录排在前面 + 'ordering': ['-creation_time'], + # 保持模型的可读名称不变 + 'verbose_name': '邮件发送log', + 'verbose_name_plural': '邮件发送log' + }, ), + + # 操作 2:将 commands 模型中的字段 created_time 重命名为 creation_time migrations.RenameField( - model_name='commands', - old_name='created_time', - new_name='creation_time', + model_name='commands', # 要操作的模型名称 + old_name='created_time', # 原字段名 + new_name='creation_time', # 新字段名 ), + + # 操作 3:将 commands 模型中的字段 last_mod_time 重命名为 last_modify_time migrations.RenameField( - model_name='commands', - old_name='last_mod_time', - new_name='last_modify_time', + model_name='commands', # 要操作的模型名称 + old_name='last_mod_time', # 原字段名 + new_name='last_modify_time', # 新字段名 ), + + # 操作 4:将 emailsendlog 模型中的字段 created_time 重命名为 creation_time migrations.RenameField( - model_name='emailsendlog', - old_name='created_time', - new_name='creation_time', + model_name='emailsendlog', # 要操作的模型名称 + old_name='created_time', # 原字段名 + new_name='creation_time', # 新字段名 ), - ] + ] \ No newline at end of file diff --git a/src/servermanager/models.py b/src/servermanager/models.py index 4326c65..3fb10a6 100644 --- a/src/servermanager/models.py +++ b/src/servermanager/models.py @@ -2,32 +2,89 @@ from django.db import models # Create your models here. +# 定义了两个数据模型: +# 1. commands:用于存储可执行的系统命令 +# 2. EmailSendLog:用于记录邮件发送的历史日志 + + class commands(models.Model): + """ + 数据模型:commands + 用于存储系统中预定义的可执行命令,例如服务器管理命令、脚本路径等。 + 可通过 Django Admin 管理后台进行增删改查。 + """ + + # 命令的标题/名称,用于标识该命令(如 "重启服务") title = models.CharField('命令标题', max_length=300) + + # 实际要执行的命令字符串(如 "systemctl restart nginx") command = models.CharField('命令', max_length=2000) + + # 对该命令的简要描述,说明其用途 describe = models.CharField('命令描述', max_length=300) + + # 记录该命令首次创建的时间,自动在创建时设置为当前时间 creation_time = models.DateTimeField('创建时间', auto_now_add=True) + + # 记录该命令最后一次修改的时间,每次保存时自动更新为当前时间 last_modify_time = models.DateTimeField('修改时间', auto_now=True) def __str__(self): + """ + 返回该模型实例的字符串表示。 + 在 Django Admin 或外键显示时,将显示命令的标题。 + + 返回: + str: 命令的标题(title 字段) + """ return self.title class Meta: - verbose_name = '命令' - verbose_name_plural = verbose_name + """ + 模型元数据配置。 + 定义模型的可读名称,用于在 Django Admin 后台显示。 + """ + verbose_name = '命令' # 单数形式的名称 + verbose_name_plural = verbose_name # 复数形式的名称(中文通常与单数相同) class EmailSendLog(models.Model): + """ + 数据模型:EmailSendLog + 用于记录系统发送邮件的日志信息,包括收件人、内容、结果和时间。 + 所有字段均为只读,仅用于查看历史记录,不可手动修改。 + """ + + # 邮件收件人地址(支持多个,以逗号分隔) emailto = models.CharField('收件人', max_length=300) + + # 邮件标题 title = models.CharField('邮件标题', max_length=2000) + + # 邮件正文内容,支持长文本 content = models.TextField('邮件内容') + + # 邮件是否发送成功,True 表示成功,False 表示失败 send_result = models.BooleanField('结果', default=False) + + # 邮件记录创建时间,自动在插入时设置为当前时间 creation_time = models.DateTimeField('创建时间', auto_now_add=True) def __str__(self): + """ + 返回该模型实例的字符串表示。 + 在 Django Admin 或外键显示时,将显示邮件的标题。 + + 返回: + str: 邮件的标题(title 字段) + """ return self.title class Meta: - verbose_name = '邮件发送log' - verbose_name_plural = verbose_name - ordering = ['-creation_time'] + """ + 模型元数据配置。 + 定义模型的可读名称和默认排序规则。 + """ + verbose_name = '邮件发送log' # 单数形式的名称 + verbose_name_plural = verbose_name # 复数形式的名称 + ordering = ['-creation_time'] # 默认排序:按创建时间倒序排列(最新的在前) \ No newline at end of file diff --git a/src/servermanager/robot.py b/src/servermanager/robot.py index 7b45736..f9729f9 100644 --- a/src/servermanager/robot.py +++ b/src/servermanager/robot.py @@ -1,46 +1,77 @@ -import logging -import os -import re - -import jsonpickle -from django.conf import settings -from werobot import WeRoBot -from werobot.replies import ArticlesReply, Article -from werobot.session.filestorage import FileStorage - -from djangoblog.utils import get_sha256 -from servermanager.api.blogapi import BlogApi -from servermanager.api.commonapi import ChatGPT, CommandHandler -from .MemcacheStorage import MemcacheStorage - -robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN') - or 'lylinux', enable_session=True) +import logging # 用于记录日志信息 +import os # 用于操作系统相关功能(如环境变量、文件路径) +import re # 正则表达式,用于文本匹配和提取 + +import jsonpickle # 将 Python 对象序列化为 JSON 字符串(支持复杂对象) +from django.conf import settings # Django 配置设置,如 BASE_DIR +from werobot import WeRoBot # WeRoBot 微信机器人框架核心类 +from werobot.replies import ArticlesReply, Article # 构造图文回复消息 +from werobot.session.filestorage import FileStorage # 文件存储会话后端(备用) + +# 项目内部工具与 API +from djangoblog.utils import get_sha256 # SHA256 加密函数,用于密码安全 +from servermanager.api.blogapi import BlogApi # 博客文章搜索/获取接口 +from servermanager.api.commonapi import ChatGPT, CommandHandler # 聊天与命令处理 +from .MemcacheStorage import MemcacheStorage # 自定义基于缓存的会话存储 + + +# 初始化微信机器人实例 +robot = WeRoBot( + token=os.environ.get('DJANGO_WEROBOT_TOKEN') or 'lylinux', # 微信公众号 Token + enable_session=True # 启用会话功能,用于维护用户状态 +) + +# 创建自定义的缓存会话存储实例(基于 Redis/Memcached) memstorage = MemcacheStorage() + +# 检查缓存存储是否可用 if memstorage.is_available: + # 如果可用,使用 MemcacheStorage 作为会话后端 robot.config['SESSION_STORAGE'] = memstorage else: - if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')): - os.remove(os.path.join(settings.BASE_DIR, 'werobot_session')) + # 如果不可用,回退到文件存储 + # 删除旧的会话文件(避免冲突或损坏) + session_file = os.path.join(settings.BASE_DIR, 'werobot_session') + if os.path.exists(session_file): + os.remove(session_file) + # 使用本地文件存储会话数据 robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session') -blogapi = BlogApi() -cmd_handler = CommandHandler() -logger = logging.getLogger(__name__) +# 实例化博客 API 和命令处理器 +blogapi = BlogApi() # 提供博客文章查询服务 +cmd_handler = CommandHandler() # 提供服务器命令执行功能 +logger = logging.getLogger(__name__) # 获取当前模块的日志记录器 def convert_to_article_reply(articles, message): + """ + 将博客文章列表转换为微信图文消息回复格式。 + + 参数: + articles (list): 包含文章对象的列表(通常来自 blogapi 查询结果) + message: 当前微信消息对象,用于构造回复 + + 返回: + ArticlesReply: 可直接返回给用户的图文消息回复对象 + + 流程: + 1. 创建一个图文回复对象 + 2. 遍历每篇文章,提取标题、摘要、首张图片、链接 + 3. 构造 Article 对象并添加到回复中 + """ reply = ArticlesReply(message=message) - from blog.templatetags.blog_tags import truncatechars_content + from blog.templatetags.blog_tags import truncatechars_content # 导入截断内容的模板标签 + for post in articles: + # 使用正则从文章内容中提取第一张图片 URL(png/jpg) imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body) - imgurl = '' - if imgs: - imgurl = imgs[0] + imgurl = imgs[0] if imgs else '' # 如果有图取第一张,否则为空 + article = Article( title=post.title, - description=truncatechars_content(post.body), + description=truncatechars_content(post.body), # 截断内容作为描述 img=imgurl, - url=post.get_full_url() + url=post.get_full_url() # 文章完整 URL ) reply.add_article(article) return reply @@ -48,11 +79,23 @@ def convert_to_article_reply(articles, message): @robot.filter(re.compile(r"^\?.*")) def search(message, session): + """ + 处理以 '?' 开头的消息,用于搜索博客文章。 + + 示例: "?python" 搜索包含 python 的文章 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 图文消息 或 文本提示 + """ s = message.content - searchstr = str(s).replace('?', '') - result = blogapi.search_articles(searchstr) + searchstr = str(s).replace('?', '') # 去掉问号 + result = blogapi.search_articles(searchstr) # 调用博客 API 搜索 if result: - articles = list(map(lambda x: x.object, result)) + articles = list(map(lambda x: x.object, result)) # 提取文章对象 reply = convert_to_article_reply(articles, message) return reply else: @@ -61,13 +104,33 @@ def search(message, session): @robot.filter(re.compile(r'^category\s*$', re.I)) def category(message, session): + """ + 处理 "category" 消息,返回所有文章分类目录。 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 文本消息:列出所有分类名称 + """ categorys = blogapi.get_category_lists() - content = ','.join(map(lambda x: x.name, categorys)) + content = ','.join(map(lambda x: x.name, categorys)) # 拼接分类名 return '所有文章分类目录:' + content @robot.filter(re.compile(r'^recent\s*$', re.I)) def recents(message, session): + """ + 处理 "recent" 消息,返回最新发布的文章。 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 图文消息(最新文章) 或 文本提示 + """ articles = blogapi.get_recent_articles() if articles: reply = convert_to_article_reply(articles, message) @@ -78,6 +141,16 @@ def recents(message, session): @robot.filter(re.compile('^help$', re.I)) def help(message, session): + """ + 处理 "help" 消息,返回帮助文档。 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 文本消息:详细的使用说明和命令列表 + """ return '''欢迎关注! 默认会与图灵机器人聊天~~ 你可以通过下面这些命令来获得信息 @@ -100,65 +173,132 @@ def help(message, session): @robot.filter(re.compile(r'^weather\:.*$', re.I)) def weather(message, session): + """ + 处理 "weather:" 开头的消息(天气查询功能)。 + 当前为占位符,功能正在建设中。 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 文本消息:提示功能建设中 + """ return "建设中..." @robot.filter(re.compile(r'^idcard\:.*$', re.I)) def idcard(message, session): + """ + 处理 "idcard:" 开头的消息(身份证信息查询功能)。 + 当前为占位符,功能正在建设中。 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 文本消息:提示功能建设中 + """ return "建设中..." @robot.handler def echo(message, session): + """ + 默认消息处理器,当没有其他 filter 匹配时调用。 + 创建 MessageHandler 实例处理消息。 + + 参数: + message: 微信消息对象 + session: 当前用户会话对象 + + 返回: + 处理结果(文本或图文消息) + """ handler = MessageHandler(message, session) return handler.handler() class MessageHandler: + """ + 消息处理器类,负责处理用户消息,尤其是管理员命令和认证流程。 + 使用会话(session)维护用户状态(是否管理员、是否已认证等)。 + """ + def __init__(self, message, session): - userid = message.source self.message = message self.session = session - self.userid = userid + self.userid = message.source # 用户唯一标识(OpenID) + + # 尝试从会话中加载用户信息 try: - info = session[userid] - self.userinfo = jsonpickle.decode(info) + info = session[self.userid] + self.userinfo = jsonpickle.decode(info) # 反序列化为 WxUserInfo 对象 except Exception as e: + # 如果出错(首次访问或会话丢失),创建默认用户信息 userinfo = WxUserInfo() self.userinfo = userinfo @property def is_admin(self): + """判断当前用户是否为管理员""" return self.userinfo.isAdmin @property def is_password_set(self): + """判断当前管理员是否已完成密码验证""" return self.userinfo.isPasswordSet def save_session(self): + """ + 将当前用户信息保存回会话。 + 使用 jsonpickle 序列化对象,并存入 session。 + """ info = jsonpickle.encode(self.userinfo) self.session[self.userid] = info def handler(self): + """ + 核心消息处理逻辑,根据用户状态和输入内容返回相应响应。 + + 处理流程: + 1. 管理员退出登录 + 2. 管理员登录请求 + 3. 管理员密码验证 + 4. 执行管理员命令 + 5. 默认:调用 ChatGPT 进行聊天 + + 返回: + str: 要回复给用户的消息内容 + """ info = self.message.content + # 退出管理员模式 if self.userinfo.isAdmin and info.upper() == 'EXIT': self.userinfo = WxUserInfo() self.save_session() return "退出成功" + + # 请求进入管理员模式 if info.upper() == 'ADMIN': self.userinfo.isAdmin = True self.save_session() return "输入管理员密码" + + # 管理员密码验证阶段 if self.userinfo.isAdmin and not self.userinfo.isPasswordSet: + # 获取配置中的管理员密码(SHA256 加密后) passwd = settings.WXADMIN if settings.TESTING: - passwd = '123' + passwd = '123' # 测试环境下使用简单密码 + # 验证用户输入的密码(双重 SHA256) if passwd.upper() == get_sha256(get_sha256(info)).upper(): self.userinfo.isPasswordSet = True self.save_session() return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助" else: + # 密码错误次数限制 if self.userinfo.Count >= 3: self.userinfo = WxUserInfo() self.save_session() @@ -166,22 +306,33 @@ class MessageHandler: self.userinfo.Count += 1 self.save_session() return "验证失败,请重新输入管理员密码:" + + # 管理员已认证,可执行命令 if self.userinfo.isAdmin and self.userinfo.isPasswordSet: if self.userinfo.Command != '' and info.upper() == 'Y': + # 用户确认执行命令 return cmd_handler.run(self.userinfo.Command) else: if info.upper() == 'HELPME': + # 显示命令帮助 return cmd_handler.get_help() + # 记录待执行的命令,等待用户确认 self.userinfo.Command = info self.save_session() return "确认执行: " + info + " 命令?" + # 默认行为:调用 ChatGPT 进行普通聊天 return ChatGPT.chat(info) class WxUserInfo(): + """ + 微信用户信息类,用于在会话中存储用户状态。 + 包括是否为管理员、是否已通过密码验证、尝试次数、待执行命令等。 + """ + def __init__(self): - self.isAdmin = False - self.isPasswordSet = False - self.Count = 0 - self.Command = '' + self.isAdmin = False # 是否请求成为管理员 + self.isPasswordSet = False # 是否已通过密码验证 + self.Count = 0 # 密码尝试次数 + self.Command = '' # 待执行的命令 \ No newline at end of file diff --git a/src/servermanager/tests.py b/src/servermanager/tests.py index 22a6689..c97e75e 100644 --- a/src/servermanager/tests.py +++ b/src/servermanager/tests.py @@ -1,79 +1,148 @@ from django.test import Client, RequestFactory, TestCase -from django.utils import timezone +from django.utils import timezone # 用于处理时间相关的测试 + +# 导入 WeRoBot 消息类,用于模拟微信消息 from werobot.messages.messages import TextMessage -from accounts.models import BlogUser -from blog.models import Category, Article -from servermanager.api.commonapi import ChatGPT -from .models import commands -from .robot import MessageHandler, CommandHandler -from .robot import search, category, recents +# 导入项目中的模型和 API +from accounts.models import BlogUser # 用户模型(用于创建管理员) +from blog.models import Category, Article # 博客分类和文章模型 +from servermanager.api.commonapi import ChatGPT # 聊天功能接口 +from .models import commands # 命令模型 +from .robot import MessageHandler, CommandHandler # 核心机器人处理器 +from .robot import search, category, recents # 微信机器人命令函数 -# Create your tests here. class ServerManagerTest(TestCase): + """ + Django 测试用例类,用于对 servermanager 应用的核心功能进行单元测试。 + 测试内容包括: + - ChatGPT 聊天功能 + - 微信机器人命令响应(搜索、分类、最新文章) + - 服务器命令执行 + - 管理员会话流程 + """ + def setUp(self): - self.client = Client() - self.factory = RequestFactory() + """ + 在每个测试方法执行前自动运行的初始化方法。 + 设置测试所需的公共环境: + - 创建测试客户端(用于模拟 HTTP 请求) + - 创建请求工厂(用于构造请求对象) + + 注意:虽然此处创建了 factory,但在当前测试中并未实际使用。 + """ + self.client = Client() # Django 测试客户端 + self.factory = RequestFactory() # 请求工厂,用于创建模拟请求 def test_chat_gpt(self): + """ + 测试 ChatGPT 聊天功能是否正常工作。 + + 步骤: + 1. 调用 ChatGPT.chat 方法发送问候语 "你好" + 2. 断言返回内容不为 None,表示接口有响应 + + 目的:验证聊天接口的基本可用性。 + """ content = ChatGPT.chat("你好") self.assertIsNotNone(content) def test_validate_comment(self): + """ + 综合测试方法,覆盖多个功能点。 + 名称 'validate_comment' 不准确,实际测试的是 servermanager 的核心功能。 + + 测试流程: + 1. 创建管理员用户并登录 + 2. 创建分类和文章用于测试搜索功能 + 3. 测试微信机器人的 search、category、recents 命令 + 4. 测试命令执行(CommandHandler) + 5. 模拟完整管理员会话流程(登录、执行命令、退出等) + """ + + # 1. 创建超级用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") + # 使用测试客户端登录该用户 self.client.login(username='liangliangyy1', password='liangliangyy1') + # 2. 创建博客分类 c = Category() c.name = "categoryccc" c.save() + # 3. 创建一篇已发布的文章 article = Article() article.title = "nicetitleccc" article.body = "nicecontentccc" article.author = user article.category = c - article.type = 'a' - article.status = 'p' + article.type = 'a' # 文章类型 + article.status = 'p' # 发布状态 article.save() + + # 4. 模拟微信文本消息 s = TextMessage([]) - s.content = "nice" - rsp = search(s, None) + s.content = "nice" # 搜索关键词 + + # 5. 测试 search 命令 + rsp = search(s, None) # 调用 search 过滤器 + # 断言有响应(即使未找到文章也应返回提示) + + # 6. 测试 category 命令 rsp = category(None, None) - self.assertIsNotNone(rsp) + self.assertIsNotNone(rsp) # 确保返回了分类列表 + + # 7. 测试 recents 命令 rsp = recents(None, None) + # 断言返回结果不是“暂时还没有文章”,说明能获取到文章 self.assertTrue(rsp != '暂时还没有文章') + # 8. 测试命令执行功能 cmd = commands() cmd.title = "test" - cmd.command = "ls" + cmd.command = "ls" # 测试命令(列出目录) cmd.describe = "test" - cmd.save() + cmd.save() # 保存到数据库 + # 实例化命令处理器 cmdhandler = CommandHandler() + # 执行名为 'test' 的命令 rsp = cmdhandler.run('test') + # 断言命令执行有返回结果 self.assertIsNotNone(rsp) - s.source = 'u' - s.content = 'test' + + # 9. 模拟管理员会话流程 + s.source = 'u' # 设置用户标识(OpenID) + s.content = 'test' # 用户输入内容 + + # 创建消息处理器实例,传入模拟消息和空会话 msghandler = MessageHandler(s, {}) - # msghandler.userinfo.isPasswordSet = True - # msghandler.userinfo.isAdmin = True - msghandler.handler() - s.content = 'y' - msghandler.handler() - s.content = 'idcard:12321233' + # 模拟用户行为序列: + msghandler.handler() # 处理 "test" 消息(应提示确认) + + s.content = 'y' # 用户确认执行 + msghandler.handler() # 执行命令 + + s.content = 'idcard:12321233' # 尝试身份证查询(占位功能) msghandler.handler() - s.content = 'weather:上海' + + s.content = 'weather:上海' # 尝试天气查询(占位功能) msghandler.handler() - s.content = 'admin' + + s.content = 'admin' # 请求进入管理员模式 msghandler.handler() - s.content = '123' + + s.content = '123' # 输入密码(测试环境下有效) msghandler.handler() - s.content = 'exit' + s.content = 'exit' # 退出管理员模式 msghandler.handler() + + # 注意:此测试未包含 assert 断言来验证每个步骤的结果, + # 主要是通过调用来检查是否抛出异常,确保代码路径可执行。 \ No newline at end of file diff --git a/src/servermanager/urls.py b/src/servermanager/urls.py index 8d134d2..94e3826 100644 --- a/src/servermanager/urls.py +++ b/src/servermanager/urls.py @@ -1,10 +1,42 @@ +# 导入 Django URL 路由模块 from django.urls import path +# 导入 WeRoBot 与 Django 集成的工具函数 from werobot.contrib.django import make_view +# 从当前应用的 robot 模块导入已配置的 WeRoBot 实例 from .robot import robot +# 定义应用命名空间 app_name = "servermanager" +""" +应用命名空间,用于在 Django 项目中唯一标识此应用的 URL。 +在模板或 reverse() 函数中可通过 'servermanager:xxx' 引用此应用的 URL。 +""" + +# 定义 URL 路由列表 urlpatterns = [ + # 将微信机器人接入点绑定到特定 URL 路径 path(r'robot', make_view(robot)), + """ + URL 路由配置: + + path(r'robot', make_view(robot)) + + 功能说明: + - 将路径 '/robot' 映射到 WeRoBot 的请求处理视图。 + - 当微信服务器向该路径发送 GET(验证)或 POST(消息)请求时, + Django 会将其转发给 WeRoBot 框架处理。 + - make_view(robot) 是 werobot 提供的适配器函数,它将 WeRoBot 实例 + 转换为一个兼容 Django 的视图函数(View)。 + + 参数说明: + - r'robot': URL 路径,用户访问 /robot 时触发。 + 注意:此处使用了原始字符串(r''),但无特殊转义需求,可简写为 'robot'。 + - make_view(robot): 将 robot 实例封装为 Django 视图。 + 典型使用场景: + 在微信公众平台开发者配置中,将服务器地址(URL)设置为: + https://yourdomain.com/servermanager/robot/ + 并配合 Token 验证机器人身份。 + """ ] diff --git a/src/servermanager/views.py b/src/servermanager/views.py index 60f00ef..2f76781 100644 --- a/src/servermanager/views.py +++ b/src/servermanager/views.py @@ -1 +1,4 @@ # Create your views here. +""" +视图模块(views.py) +""" \ No newline at end of file