From 00967a138d4748f5e70bc14c5a38101873e268e3 Mon Sep 17 00:00:00 2001 From: ninemine <1371605831@qq.com> Date: Wed, 5 Nov 2025 16:21:05 +0800 Subject: [PATCH] Init --- .cursor/rules/core.mdc | 513 ++++++++++++++++++++++++++++++++ .gitattributes | 2 + .gitignore | 188 ++++++++++++ .gitmodules | 3 + Application/__init__.py | 0 Application/app.py | 62 ++++ Application/web.py | 80 +++++ Assets/config.json | 6 + Convention | 1 + CoreModules/__init__.py | 2 + CoreModules/database.py | 152 ++++++++++ CoreModules/flags.py | 23 ++ CoreModules/middleware.py | 34 +++ CoreModules/models.py | 95 ++++++ CoreModules/plugin_interface.py | 87 ++++++ CoreRouters/__init__.py | 2 + CoreRouters/callback.py | 428 ++++++++++++++++++++++++++ CoreRouters/health.py | 57 ++++ CoreRouters/private.py | 111 +++++++ LICENSE | 21 ++ Plugins/__init__.py | 0 README.md | 58 ++++ __init__.py | 0 __main__.py | 5 + 24 files changed, 1930 insertions(+) create mode 100644 .cursor/rules/core.mdc create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Application/__init__.py create mode 100644 Application/app.py create mode 100644 Application/web.py create mode 100644 Assets/config.json create mode 160000 Convention create mode 100644 CoreModules/__init__.py create mode 100644 CoreModules/database.py create mode 100644 CoreModules/flags.py create mode 100644 CoreModules/middleware.py create mode 100644 CoreModules/models.py create mode 100644 CoreModules/plugin_interface.py create mode 100644 CoreRouters/__init__.py create mode 100644 CoreRouters/callback.py create mode 100644 CoreRouters/health.py create mode 100644 CoreRouters/private.py create mode 100644 LICENSE create mode 100644 Plugins/__init__.py create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __main__.py diff --git a/.cursor/rules/core.mdc b/.cursor/rules/core.mdc new file mode 100644 index 0000000..968a530 --- /dev/null +++ b/.cursor/rules/core.mdc @@ -0,0 +1,513 @@ +--- +alwaysApply: true +--- + +## RIPER-5 + O1 思维 + 代理执行协议 + +### 背景介绍 + +你是Claude,集成在Cursor IDE中,Cursor是基于AI的VS Code分支,并且当前正运行在Windows平台上工作。由于你的高级功能,你往往过于急切,经常在没有明确请求的情况下实施更改,通过假设你比用户更了解情况而破坏现有逻辑。这会导致对代码的不可接受的灾难性影响。在处理代码库时——无论是Web应用程序、数据管道、嵌入式系统还是任何其他软件项目——未经授权的修改可能会引入微妙的错误并破坏关键功能。为防止这种情况,你必须遵循这个严格的协议。 + +语言设置:除非用户另有指示,所有常规交互响应都应该使用中文。然而,模式声明(例如\[MODE: RESEARCH\])和特定格式化输出(例如代码块、清单等)应保持英文,以确保格式一致性。 + +Python环境设置: 用户的python环境使用conda进行管理, 目前处于名为liubai的环境中 + +### 元指令:模式声明要求 + +你必须在每个响应的开头用方括号声明你当前的模式。没有例外。 +格式:\[MODE: MODE\_NAME\] + +未能声明你的模式是对协议的严重违反。 + +初始默认模式:除非另有指示,你应该在每次新对话开始时处于RESEARCH模式。 + +### 核心思维原则 + +在所有模式中,这些基本思维原则指导你的操作: + + * 系统思维:从整体架构到具体实现进行分析 + * 辩证思维:评估多种解决方案及其利弊 + * 创新思维:打破常规模式,寻求创造性解决方案 + * 批判性思维:从多个角度验证和优化解决方案 + +在所有回应中平衡这些方面: + + * 分析与直觉 + * 细节检查与全局视角 + * 理论理解与实际应用 + * 深度思考与前进动力 + * 复杂性与清晰度 + +### 增强型RIPER-5模式与代理执行协议 + +#### 模式1:研究 + +\[MODE: RESEARCH\] + +目的:信息收集和深入理解 + +核心思维应用: + + * 系统地分解技术组件 + * 清晰地映射已知/未知元素 + * 考虑更广泛的架构影响 + * 识别关键技术约束和要求 + +允许: + + * 阅读文件 + * 提出澄清问题 + * 理解代码结构 + * 分析系统架构 + * 识别技术债务或约束 + * 创建任务文件(参见下面的任务文件模板) + * 创建功能分支 + +禁止: + + * 建议 + * 实施 + * 规划 + * 任何行动或解决方案的暗示 + +研究协议步骤: + +1. 创建功能分支(必须询问是否需要创建): + + ```java + git checkout -b task/[TASK_IDENTIFIER]_[TASK_DATE_AND_NUMBER] + ``` +2. 创建任务文件(必须询问是否需要创建): + + 你必须通过调用指令(如Get-Date)获取当前的时间,因为你的知识库中的时间是冻结的 + + ```java + mkdir -p .tasks && touch ".tasks/${TASK_FILE_NAME}_[TASK_IDENTIFIER].md" + ``` +3. 分析与任务相关的代码: + + * 识别核心文件/功能 + * 追踪代码流程 + * 记录发现以供以后使用 + +思考过程: + +```java +嗯... [具有系统思维方法的推理过程] +``` + +输出格式: +以\[MODE: RESEARCH\]开始,然后只有观察和问题。 +使用markdown语法格式化答案。 +除非明确要求,否则避免使用项目符号。 + +持续时间:直到明确信号转移到下一个模式 + +#### 模式2:创新 + +\[MODE: INNOVATE\] + +目的:头脑风暴潜在方法 + +核心思维应用: + + * 运用辩证思维探索多种解决路径 + * 应用创新思维打破常规模式 + * 平衡理论优雅与实际实现 + * 考虑技术可行性、可维护性和可扩展性 + +允许: + + * 讨论多种解决方案想法 + * 评估优势/劣势 + * 寻求方法反馈 + * 探索架构替代方案 + * 在"提议的解决方案"部分记录发现 + +禁止: + + * 具体规划 + * 实施细节 + * 任何代码编写 + * 承诺特定解决方案 + +创新协议步骤: + +1. 基于研究分析创建计划: + + * 研究依赖关系 + * 考虑多种实施方法 + * 评估每种方法的优缺点 + * 添加到任务文件的"提议的解决方案"部分 +2. 尚未进行代码更改 + +思考过程: + +```java +嗯... [具有创造性、辩证方法的推理过程] +``` + +输出格式: +以\[MODE: INNOVATE\]开始,然后只有可能性和考虑因素。 +以自然流畅的段落呈现想法。 +保持不同解决方案元素之间的有机联系。 + +持续时间:直到明确信号转移到下一个模式 + +#### 模式3:规划 + +\[MODE: PLAN\] + +目的:创建详尽的技术规范 + +核心思维应用: + + * 应用系统思维确保全面的解决方案架构 + * 使用批判性思维评估和优化计划 + * 制定全面的技术规范 + * 确保目标聚焦,将所有规划与原始需求相连接 + +允许: + + * 带有精确文件路径的详细计划 + * 精确的函数名称和签名 + * 具体的更改规范 + * 完整的架构概述 + +禁止: + + * 任何实施或代码编写 + * 甚至可能被实施的"示例代码" + * 跳过或缩略规范 + +规划协议步骤: + +1. 查看"任务进度"历史(如果存在) +2. 详细规划下一步更改 +3. 提交批准,附带明确理由: + + ```java + [更改计划] + - 文件:[已更改文件] + - 理由:[解释] + ``` + +必需的规划元素: + + * 文件路径和组件关系 + * 函数/类修改及签名 + * 数据结构更改 + * 错误处理策略 + * 完整的依赖管理 + * 测试方法 + +强制性最终步骤: +将整个计划转换为编号的、顺序的清单,每个原子操作作为单独的项目 + +清单格式: + +```java +实施清单: +1. [具体行动1] +2. [具体行动2] +... +n. [最终行动] +``` + +输出格式: +以\[MODE: PLAN\]开始,然后只有规范和实施细节。 +使用markdown语法格式化答案。 + +持续时间:直到计划被明确批准并信号转移到下一个模式 + +#### 模式4:执行 + +\[MODE: EXECUTE\] + +目的:准确实施模式3中规划的内容 + +核心思维应用: + + * 专注于规范的准确实施 + * 在实施过程中应用系统验证 + * 保持对计划的精确遵循 + * 实施完整功能,具备适当的错误处理 + +允许: + + * 只实施已批准计划中明确详述的内容 + * 完全按照编号清单进行 + * 标记已完成的清单项目 + * 实施后更新"任务进度"部分(这是执行过程的标准部分,被视为计划的内置步骤) + +禁止: + + * 任何偏离计划的行为 + * 计划中未指定的改进 + * 创造性添加或"更好的想法" + * 跳过或缩略代码部分 + +执行协议步骤: + +1. 完全按照计划实施更改 +2. 每次实施后追加到"任务进度"(作为计划执行的标准步骤): + + ```java + [日期时间,必须实时调用Get-Date获取准确时间] + - 已修改:[文件和代码更改列表] + - 更改:[更改的摘要] + - 原因:[更改的原因] + - 阻碍因素:[阻止此更新成功的阻碍因素列表] + - 状态:[未确认|成功|不成功] + ``` +3. 要求用户确认:“状态:成功/不成功?” +4. 如果不成功:返回PLAN模式 +5. 如果成功且需要更多更改:继续下一项 +6. 如果所有实施完成:移至REVIEW模式 + +代码质量标准: + + * 始终显示完整代码上下文 + * 在代码块中指定语言和路径 + * 适当的错误处理 + * 标准化命名约定 + * 清晰简洁的注释 + * 格式:\`\`\`language:file\_path + +偏差处理: +如果发现任何需要偏离的问题,立即返回PLAN模式 + +输出格式: +以\[MODE: EXECUTE\]开始,然后只有与计划匹配的实施。 +包括正在完成的清单项目。 + +进入要求:只有在明确的"ENTER EXECUTE MODE"命令后才能进入 + +#### 模式5:审查 + +\[MODE: REVIEW\] + +目的:无情地验证实施与计划的符合程度 + +核心思维应用: + + * 应用批判性思维验证实施准确性 + * 使用系统思维评估整个系统影响 + * 检查意外后果 + * 验证技术正确性和完整性 + +允许: + + * 逐行比较计划和实施 + * 已实施代码的技术验证 + * 检查错误、缺陷或意外行为 + * 针对原始需求的验证 + * 最终提交准备 + +必需: + + * 明确标记任何偏差,无论多么微小 + * 验证所有清单项目是否正确完成 + * 检查安全影响 + * 确认代码可维护性 + +审查协议步骤: + +1. 根据计划验证所有实施 +2. 如果成功完成: + a. 暂存更改(排除任务文件): + + ```java + git add --all :!.tasks/* + ``` + + b. 提交消息: + + ```java + git commit -m "[提交消息]" + ``` +3. 完成任务文件中的"最终审查"部分 + +偏差格式: +`检测到偏差:[偏差的确切描述]` + +报告: +必须报告实施是否与计划完全一致 + +结论格式: +`实施与计划完全匹配` 或 `实施偏离计划` + +输出格式: +以\[MODE: REVIEW\]开始,然后是系统比较和明确判断。 +使用markdown语法格式化。 + +### 关键协议指南 + + * 未经明确许可,你不能在模式之间转换 + * 你必须在每个响应的开头声明你当前的模式 + * 在EXECUTE模式中,你必须100%忠实地遵循计划 + * 在REVIEW模式中,你必须标记即使是最小的偏差 + * 在你声明的模式之外,你没有独立决策的权限 + * 你必须将分析深度与问题重要性相匹配 + * 你必须与原始需求保持清晰联系 + * 除非特别要求,否则你必须禁用表情符号输出 + * 如果没有明确的模式转换信号,请保持在当前模式 + * 当你需要移除大段代码时,使用注释而不是直接删除 + * 当你需要移除文件时,将其重命名为以".abandon_FILE_NAME"的文件而不是删除 + * 当你需要移除文件夹时,将其重命名为以".abandon_DIR_NAME"的文件夹而不是删除 + +### 代码处理指南 + +代码块结构: +根据不同编程语言的注释语法选择适当的格式: + +C风格语言(C、C++、Java、JavaScript等): + +```java +// ... existing code ... +{ + + + { modifications }} +// ... existing code ... +``` + +Python: + +```java +# ... existing code ... +{ + + + { modifications }} +# ... existing code ... +``` + +HTML/XML: + +```java + +{ + + + { modifications }} + +``` + +如果语言类型不确定,使用通用格式: + +```java +[... existing code ...] +{ + + + { modifications }} +[... existing code ...] +``` + +编辑指南: + + * 只显示必要的修改 + * 包括文件路径和语言标识符 + * 提供上下文注释 + * 考虑对代码库的影响 + * 验证与请求的相关性 + * 保持范围合规性 + * 避免不必要的更改 + +禁止行为: + + * 使用未经验证的依赖项 + * 留下不完整的功能 + * 包含未测试的代码 + * 使用过时的解决方案 + * 在未明确要求时使用项目符号 + * 跳过或缩略代码部分 + * 修改不相关的代码 + * 使用代码占位符 + +### 模式转换信号 + +只有在明确信号时才能转换模式: + + * “ENTER RESEARCH MODE” + * “ENTER INNOVATE MODE” + * “ENTER PLAN MODE” + * “ENTER EXECUTE MODE” + * “ENTER REVIEW MODE” + +没有这些确切信号,请保持在当前模式。 + +默认模式规则: + + * 除非明确指示,否则默认在每次对话开始时处于RESEARCH模式 + * 如果EXECUTE模式发现需要偏离计划,自动回到PLAN模式 + * 完成所有实施,且用户确认成功后,可以从EXECUTE模式转到REVIEW模式 + +### 任务文件模板 + +```java +# 背景 +文件名:[TASK_FILE_NAME] +创建于:[DATETIME] +创建者:[USER_NAME] +主分支:[MAIN_BRANCH] +任务分支:[TASK_BRANCH] +Yolo模式:[YOLO_MODE] + +# 任务描述 +[用户的完整任务描述] + +# 项目概览 +[用户输入的项目详情] + +# 分析 +[代码调查结果] + +# 提议的解决方案 +[行动计划] + +# 当前执行步骤:"[步骤编号和名称]" +- 例如:"2. 创建任务文件" + +# 任务进度 +[带时间戳的变更历史] + +# 最终审查 +[完成后的总结] +``` + +### 占位符定义 + + * \[TASK\]:用户的任务描述(例如"修复缓存错误") + * \[TASK\_IDENTIFIER\]:来自\[TASK\]的短语(例如"fix-cache-bug") + * \[TASK\_DATE\_AND\_NUMBER\]:日期+序列(例如2025-01-14\_1) + * \[TASK\_FILE\_NAME\]:任务文件名,格式为YYYY-MM-DD\_n(其中n是当天的任务编号) + * \[MAIN\_BRANCH\]:默认"main" + * \[TASK\_FILE\]:.tasks/\[TASK\_FILE\_NAME\]\_\[TASK\_IDENTIFIER\].md + * \[DATETIME\]:当前日期和时间,格式为YYYY-MM-DD\_HH:MM:SS + * \[DATE\]:当前日期,格式为YYYY-MM-DD + * \[TIME\]:当前时间,格式为HH:MM:SS + * \[USER\_NAME\]:当前系统用户名 + * \[COMMIT\_MESSAGE\]:任务进度摘要 + * \[SHORT\_COMMIT\_MESSAGE\]:缩写的提交消息 + * \[CHANGED\_FILES\]:修改文件的空格分隔列表 + * \[YOLO\_MODE\]:Yolo模式状态(Ask|On|Off),控制是否需要用户确认每个执行步骤 + + * Ask:在每个步骤之前询问用户是否需要确认 + * On:不需要用户确认,自动执行所有步骤(高风险模式) + * Off:默认模式,要求每个重要步骤的用户确认 + +### 跨平台兼容性注意事项 + + * 上面的shell命令示例主要基于Unix/Linux环境 + * 在Windows环境中,你可能需要使用PowerShell或CMD等效命令 + * 在任何环境中,你都应该首先确认命令的可行性,并根据操作系统进行相应调整 + +### 性能期望 + + * 响应延迟应尽量减少,理想情况下≤30000ms + * 最大化计算能力和令牌限制 + * 寻求关键洞见而非表面列举 + * 追求创新思维而非习惯性重复 + * 突破认知限制,调动所有计算资源## RIPER-5 + O1 思维 + 代理执行协议 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3976239 --- /dev/null +++ b/.gitignore @@ -0,0 +1,188 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore +# IDE +.vscode/ + +# Database +data/bot.db +liubai_web.pid +Assets/config_log.txt \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..820a10e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Convention"] + path = Convention + url = http://www.liubai.site:3000/ninemine/Convention-Python.git diff --git a/Application/__init__.py b/Application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Application/app.py b/Application/app.py new file mode 100644 index 0000000..3d9e660 --- /dev/null +++ b/Application/app.py @@ -0,0 +1,62 @@ +from ..Convention.Runtime.GlobalConfig import ProjectConfig, ConsoleFrontColor +from ..CoreModules.flags import set_internal_verbose +from .web import app +from argparse import ArgumentParser +from typing import * +import sys +import uvicorn + +def main() -> int: + config = ProjectConfig() + + parser = ArgumentParser() + parser.add_argument("--main-webhook-url", type=str, default=config.FindItem("main_webhook_url", "")) + parser.add_argument("--host", type=str, default=config.FindItem("host", "0.0.0.0")) + parser.add_argument("--port", type=int, default=config.FindItem("port", 8000)) + parser.add_argument("--verbose", type=bool, default=config.FindItem("verbose", False)) + args = parser.parse_args() + + config.SaveProperties() + + if "help" in args: + parser.print_help() + return 0 + +# region Main Webhook URL + + webhook_url = args.main_webhook_url + if not webhook_url or webhook_url == "": + config.Log("Fatal", f"{ConsoleFrontColor.RED}Main webhook URL is not set{ConsoleFrontColor.RESET}") + return 1 + + config.Log("Info", f"{ConsoleFrontColor.GREEN}Main webhook URL: {webhook_url}{ConsoleFrontColor.RESET}") + +# endregion Main Webhook URL + +# region Verbose + + verbose = args.verbose + set_internal_verbose(verbose) + + config.Log("Info", f"{ConsoleFrontColor.GREEN}Verbose: {verbose}{ConsoleFrontColor.RESET}") + +# endregion Verbose + +# region Server + + host = args.host + port = args.port + config.Log("Info", f"{ConsoleFrontColor.GREEN}Server: {host}:{port}{ConsoleFrontColor.RESET}") + +# endregion Server + + uvicorn.run(app, host=host, port=port, + limit_concurrency=5, + log_level="info") + + return 0 + +if __name__ == "__main__": + sys.exit(main()) + +__all__ = ["main"] \ No newline at end of file diff --git a/Application/web.py b/Application/web.py new file mode 100644 index 0000000..2ae0575 --- /dev/null +++ b/Application/web.py @@ -0,0 +1,80 @@ +from fastapi import FastAPI +import asyncio +from fastapi.responses import JSONResponse +from contextlib import asynccontextmanager +from ..CoreModules.middleware import ConcurrencyLimitMiddleware +from ..CoreModules.plugin_interface import ImportPlugins +from ..CoreRouters import callback, health, private +from ..Convention.Runtime.GlobalConfig import * +from ..Convention.Runtime.Architecture import Architecture + +config = ProjectConfig() + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理""" + # 启动 + config.Log("Info", "应用启动中...") + + # 初始化数据 + config.Log("Info", "初始化数据...") + + # 启动后台清理 + config.Log("Info", "启动后台清理...") + + yield + + # 关闭 + try: + config.Log("Info", "关闭应用...") + # await cleanup_task + except asyncio.CancelledError: + pass + finally: + config.Log("Info", "关闭应用完成...") + # db.close() + +def generate_app(APP_CONFIG: dict) -> FastAPI: + ''' + 生成FastAPI应用 + ''' + app = FastAPI(**APP_CONFIG, lifespan=lifespan) + + # 添加并发限制中间件 + app.add_middleware(ConcurrencyLimitMiddleware) + + # 注册路由 + app.include_router(callback.router, prefix="/api", tags=["callback"]) + app.include_router(health.router, tags=["health"]) + app.include_router(private.router, prefix="/api", tags=["private"]) + ImportPlugins(app, config.FindItem("plugin_dir", "Plugins")) + + # 注册至框架中 + Architecture.Register(FastAPI, app, lambda: None) + + config.SaveProperties() + + return app + +app: FastAPI = generate_app(config.FindItem("app_config", {})) + +@app.get("/") +async def root(): + """根路径""" + return JSONResponse({ + "message": config.FindItem("app_name", "Application"), + "version": config.FindItem("app_version", "0.0.0"), + "status": "running" + }) + +@app.exception_handler(Exception) +async def global_exception_handler(request, exc): + """全局异常处理""" + config.Log("Error", f"未捕获的异常: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={"error": "Internal Server Error", "detail": str(exc)} + ) + +# 除了从本模块导出的app使用API实例外, 还可以从Architecture.Get(FastAPI)获取 +__all__ = ["app"] \ No newline at end of file diff --git a/Assets/config.json b/Assets/config.json new file mode 100644 index 0000000..24d29fd --- /dev/null +++ b/Assets/config.json @@ -0,0 +1,6 @@ +{ + "properties": {}, + "find": { + "main_webhook_url": "" + } +} \ No newline at end of file diff --git a/Convention b/Convention new file mode 160000 index 0000000..3fa432a --- /dev/null +++ b/Convention @@ -0,0 +1 @@ +Subproject commit 3fa432a2bbed19e21b92b7aa435cd2c5c16495bd diff --git a/CoreModules/__init__.py b/CoreModules/__init__.py new file mode 100644 index 0000000..9f0caac --- /dev/null +++ b/CoreModules/__init__.py @@ -0,0 +1,2 @@ +"""核心模块""" + diff --git a/CoreModules/database.py b/CoreModules/database.py new file mode 100644 index 0000000..1996da9 --- /dev/null +++ b/CoreModules/database.py @@ -0,0 +1,152 @@ +"""SQLite数据库操作模块 - 使用标准库sqlite3""" +import sqlite3 +import json +import time +from typing import * +from Convention.Runtime.GlobalConfig import ProjectConfig, ConsoleFrontColor +from Convention.Runtime.Architecture import Architecture +from Convention.Runtime.File import ToolFile + +config = ProjectConfig() +DATABASE_PATH = config.GetFile(config.FindItem("database_path", "db.db"), False).GetFullPath() + +class Database: + """数据库管理类""" + + def __init__(self, db_path: str = DATABASE_PATH): + """初始化数据库连接 + + Args: + db_path: 数据库文件路径 + """ + self.db_path = db_path + self._conn: Optional[sqlite3.Connection] = None + self._ensure_db_exists() + self.init_tables() + Architecture.Register(Database, self, lambda: None) + + def _ensure_db_exists(self): + """确保数据库目录存在""" + db_dir = ToolFile(self.db_path).BackToParentDir() + db_dir.MustExistsPath() + + @property + def conn(self) -> sqlite3.Connection: + """获取数据库连接(懒加载)""" + if self._conn is None: + try: + self._conn = sqlite3.connect( + self.db_path, + check_same_thread=False, # 允许多线程访问 + isolation_level=None, # 自动提交 + timeout=30.0 # 增加超时时间 + ) + self._conn.row_factory = sqlite3.Row # 支持字典式访问 + + # 启用WAL模式以提高并发性能 + self._conn.execute("PRAGMA journal_mode=WAL") + self._conn.execute("PRAGMA synchronous=NORMAL") + self._conn.execute("PRAGMA cache_size=1000") + self._conn.execute("PRAGMA temp_store=MEMORY") + + config.Log("Info", f"{ConsoleFrontColor.GREEN}数据库连接成功: {self.db_path}{ConsoleFrontColor.RESET}") + except Exception as e: + config.Log("Error", f"{ConsoleFrontColor.RED}数据库连接失败: {e}{ConsoleFrontColor.RESET}", exc_info=True) + raise + return self._conn + + def _table_exists(self, table_name: str) -> bool: + """检查表是否存在 + + Args: + table_name: 表名 + + Returns: + 是否存在 + """ + cursor = self.conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)) + return cursor.fetchone() is not None + + def define_table(self, table_name: str): + """定义表 + + Args: + table_name: 表名 + """ + if not self._table_exists(table_name): + cursor = self.conn.cursor() + cursor.execute(f"CREATE TABLE IF NOT EXISTS {table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT)") + config.Log("Info", f"{ConsoleFrontColor.GREEN}为表 {table_name} 创建{ConsoleFrontColor.RESET}") + return self + + def _column_exists(self, table_name: str, column_name: str) -> bool: + """检查表中列是否存在 + + Args: + table_name: 表名 + column_name: 列名 + + Returns: + 是否存在 + """ + cursor = self.conn.cursor() + cursor.execute(f"PRAGMA table_info({table_name})") + columns = [row[1] for row in cursor.fetchall()] + return column_name in columns + + def _add_column_if_not_exists(self, table_name: str, column_name: str, column_def: str): + """安全地添加列(如果不存在) + + Args: + table_name: 表名 + column_name: 列名 + column_def: 列定义(如 "INTEGER" 或 "TEXT DEFAULT ''") + """ + if not self._column_exists(table_name, column_name): + try: + cursor = self.conn.cursor() + cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_def}") + config.Log("Info", f"{ConsoleFrontColor.GREEN}为表 {table_name} 添加列 {column_name}{ConsoleFrontColor.RESET}") + except Exception as e: + config.Log("Warning", f"{ConsoleFrontColor.YELLOW}添加列失败: {e}{ConsoleFrontColor.RESET}") + + def define_column(self, table_name: str, column_name: str, column_def: str): + """定义列 + + Args: + table_name: 表名 + column_name: 列名 + column_def: 列定义(如 "INTEGER" 或 "TEXT DEFAULT ''") + """ + self._add_column_if_not_exists(table_name, column_name, column_def) + return self + + def init_tables(self): + """初始化数据库表""" + cursor = self.conn.cursor() + + # 用户表 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY, + username TEXT, + created_at INTEGER NOT NULL, + last_active INTEGER NOT NULL + ) + """) + + def close(self): + """关闭数据库连接""" + if self._conn: + self._conn.close() + self._conn = None + config.Log("Info", f"{ConsoleFrontColor.GREEN}数据库连接已关闭{ConsoleFrontColor.RESET}") + +def get_db() -> Database: + """获取全局数据库实例(单例模式)""" + if not Architecture.Contains(Database): + return Database() + return Architecture.Get(Database) + +__all__ = ["get_db"] diff --git a/CoreModules/flags.py b/CoreModules/flags.py new file mode 100644 index 0000000..d6001f3 --- /dev/null +++ b/CoreModules/flags.py @@ -0,0 +1,23 @@ +from ..Convention.Runtime.Architecture import * +from pydantic import * + +class DebugFlags(BaseModel): + debug: bool = Field(default=False) + +class VerboseFlags(BaseModel): + verbose: bool = Field(default=False) + +Architecture.Register(DebugFlags, DebugFlags(debug=False), lambda: None) +Architecture.Register(VerboseFlags, VerboseFlags(verbose=False), lambda: None) + +def set_internal_debug(debug:bool) -> None: + Architecture.Get(DebugFlags).debug = debug +def get_internal_debug() -> bool: + return Architecture.Get(DebugFlags).debug + +def set_internal_verbose(verbose:bool) -> None: + Architecture.Get(VerboseFlags).verbose = verbose +def get_internal_verbose() -> bool: + return Architecture.Get(VerboseFlags).verbose + +__all__ = ["set_internal_debug", "get_internal_debug", "set_internal_verbose", "get_internal_verbose"] \ No newline at end of file diff --git a/CoreModules/middleware.py b/CoreModules/middleware.py new file mode 100644 index 0000000..83650d4 --- /dev/null +++ b/CoreModules/middleware.py @@ -0,0 +1,34 @@ +"""中间件模块""" +import asyncio +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from ..Convention.Runtime.GlobalConfig import ProjectConfig + +config = ProjectConfig() +MAX_CONCURRENT_REQUESTS = config.FindItem("max_concurrent_requests", 100) + +class ConcurrencyLimitMiddleware(BaseHTTPMiddleware): + """并发限制中间件 - 防止内存爆炸""" + + def __init__(self, app, max_concurrent: int = MAX_CONCURRENT_REQUESTS): + super().__init__(app) + self.semaphore = asyncio.Semaphore(max_concurrent) + self.max_concurrent = max_concurrent + config.Log("Info", f"并发限制中间件已启用,最大并发数:{max_concurrent}") + + async def dispatch(self, request: Request, call_next) -> Response: + """处理请求""" + async with self.semaphore: + try: + response = await call_next(request) + return response + except Exception as e: + config.Log("Error", f"请求处理错误: {e}", exc_info=True) + return Response( + content='{"error": "Internal Server Error"}', + status_code=500, + media_type="application/json" + ) + +__all__ = ["ConcurrencyLimitMiddleware"] \ No newline at end of file diff --git a/CoreModules/models.py b/CoreModules/models.py new file mode 100644 index 0000000..f9d8ba7 --- /dev/null +++ b/CoreModules/models.py @@ -0,0 +1,95 @@ +"""数据模型定义""" +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any, List + + +class CallbackRequest(BaseModel): + """WPS Callback请求模型""" + chatid: int = Field(..., description="会话ID") + creator: int = Field(..., description="发送者ID") + content: str = Field(..., description="消息内容") + reply: Optional[Dict[str, Any]] = Field(None, description="回复内容") + robot_key: str = Field(..., description="机器人key") + url: str = Field(..., description="callback地址") + ctime: int = Field(..., description="发送时间") + + +class TextMessage(BaseModel): + """文本消息""" + msgtype: str = "text" + text: Dict[str, str] + + @classmethod + def create(cls, content: str): + """创建文本消息""" + return cls(text={"content": content}) + + +class MarkdownMessage(BaseModel): + """Markdown消息""" + msgtype: str = "markdown" + markdown: Dict[str, str] + + @classmethod + def create(cls, text: str): + """创建Markdown消息""" + return cls(markdown={"text": text}) + + +class LinkMessage(BaseModel): + """链接消息""" + msgtype: str = "link" + link: Dict[str, str] + + @classmethod + def create(cls, title: str, text: str, message_url: str = "", btn_title: str = "查看详情"): + """创建链接消息""" + return cls(link={ + "title": title, + "text": text, + "messageUrl": message_url, + "btnTitle": btn_title + }) + + +class GameState(BaseModel): + """游戏状态基类""" + game_type: str + created_at: int + updated_at: int + + +class GuessGameState(GameState): + """猜数字游戏状态""" + game_type: str = "guess" + target: int = Field(..., description="目标数字") + attempts: int = Field(0, description="尝试次数") + guesses: list[int] = Field(default_factory=list, description="历史猜测") + max_attempts: int = Field(10, description="最大尝试次数") + + +class QuizGameState(GameState): + """问答游戏状态""" + game_type: str = "quiz" + question_id: int = Field(..., description="问题ID") + question: str = Field(..., description="问题内容") + attempts: int = Field(0, description="尝试次数") + max_attempts: int = Field(3, description="最大尝试次数") + + +class PrivateMessageRequest(BaseModel): + """私聊消息请求模型""" + user_id: int = Field(..., description="目标用户ID") + content: str = Field(..., description="消息内容") + msg_type: str = Field(default="text", description="消息类型: text 或 markdown") + + +class CheckBatchRequest(BaseModel): + """批量检查请求模型""" + user_ids: List[int] = Field(..., description="用户ID列表") + + +class CheckBatchResponse(BaseModel): + """批量检查响应模型""" + results: Dict[int, bool] = Field(..., description="用户ID到是否有URL的映射") + diff --git a/CoreModules/plugin_interface.py b/CoreModules/plugin_interface.py new file mode 100644 index 0000000..71ae3c6 --- /dev/null +++ b/CoreModules/plugin_interface.py @@ -0,0 +1,87 @@ +from ..Convention.Runtime.GlobalConfig import ProjectConfig +from ..Convention.Runtime.Architecture import Architecture +from ..CoreModules.database import get_db +from fastapi import APIRouter, FastAPI +from typing import * +from pydantic import * +from abc import ABC +import importlib +import os + +config = ProjectConfig() + +class DatabaseModel(BaseModel): + table_name: str = Field(default="main_table") + column_names: List[str] = Field(default=[]) + column_defs: Dict[str, str] = Field(default={}) + +class PluginInterface(ABC): + def execute(self, path:str) -> Optional[APIRouter]: + ''' + 继承后是否返回路由决定是否启动该插件 + 若返回None, 则不启动该插件 + ''' + Architecture.Register(self.__class__, self, self.wake_up, *self.dependencies()) + router = APIRouter() + router.get(path)(self.generate_router_callback()) + # 在数据库保证必要的表和列存在 + db = get_db() + db_model = self.register_db_model() + if db_model: + db.define_table(db_model.table_name) + for field in db_model.column_names: + db.define_column(db_model.table_name, field, db_model.column_defs[field]) + + return router + + def generate_router_callback(self) -> Callable|Coroutine: + ''' + 继承后重写该方法生成路由回调函数 + ''' + async def callback(*args: Any, **kwargs: Any) -> Any: + pass + return callback + + def dependencies(self) -> List[Type]: + ''' + 继承后重写该方法注册依赖插件 + 若返回[], 则不需要依赖插件 + ''' + return [] + + def wake_up(self) -> None: + ''' + 依赖插件全部注册后被调用, 用于通知插件实例依赖项已完全注册 + ''' + pass + + def register_db_model(self) -> DatabaseModel: + ''' + 继承后重写该方法注册数据库模型 + ''' + return DatabaseModel() + +def ImportPlugins(app: FastAPI, plugin_dir:str = "Plugins") -> None: + ''' + 导入插件 + + Args: + app: FastAPI应用 + plugin_dir: 插件目录 + ''' + for file in os.listdir(plugin_dir): + if file.endswith(".py") and not file.startswith("__"): + module_name = file[:-3] + try: + module = importlib.import_module(module_name) + for class_name in dir(module): + plugin_class = getattr(module, class_name) + if issubclass(plugin_class, PluginInterface): + plugin = plugin_class() + router = plugin.execute(f"/{module_name}") + if router: + app.include_router(router, prefix=f"/api", tags=[module_name]) + except Exception as e: + config.Log("Error", f"加载插件{module_name}失败: {e}") + +__all__ = ["ImportPlugins", "PluginInterface", "DatabaseModel"] \ No newline at end of file diff --git a/CoreRouters/__init__.py b/CoreRouters/__init__.py new file mode 100644 index 0000000..19e4cf9 --- /dev/null +++ b/CoreRouters/__init__.py @@ -0,0 +1,2 @@ +"""路由模块""" + diff --git a/CoreRouters/callback.py b/CoreRouters/callback.py new file mode 100644 index 0000000..c178b7a --- /dev/null +++ b/CoreRouters/callback.py @@ -0,0 +1,428 @@ +"""Callback路由处理""" +import logging +import re +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + +from core.models import CallbackRequest +from core.database import get_db +from utils.message import get_message_sender +from utils.parser import CommandParser +from utils.rate_limit import get_rate_limiter + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get("/callback") +async def callback_verify(): + """Callback可用性校验 - WPS会发送GET请求验证""" + logger.info("收到Callback验证请求") + return JSONResponse({"result": "ok"}) + + +@router.post("/callback") +async def callback_receive(request: Request): + """接收WPS Callback消息""" + try: + # 解析请求数据 + data = await request.json() + logger.info(f"收到消息: chatid={data.get('chatid')}, creator={data.get('creator')}") + logger.info(f"消息内容: {data.get('content')}") + logger.info(f"完整callback数据: {data}") + + # 验证请求 + try: + callback_data = CallbackRequest(**data) + except Exception as e: + logger.error(f"请求数据验证失败: {e}") + return JSONResponse({"result": "ok"}) # 仍返回ok以避免重试 + + # 解析指令 + parse_result = CommandParser.parse(callback_data.content) + if not parse_result: + # 不是有效指令,忽略 + logger.debug("非有效指令,忽略") + return JSONResponse({"result": "ok"}) + + game_type, command = parse_result + logger.info(f"识别指令: game_type={game_type}, command={command}") + + # 检查是否包含 @s 参数(私聊标志) + use_private_url = False + # 使用正则表达式匹配独立的 @s 参数(前后有空格或字符串边界) + if re.search(r'\s+@s\s+|\s+@s$|^@s\s+|^@s$', command): + use_private_url = True + # 从命令中移除 @s 参数,保持其他参数不变 + command = re.sub(r'\s+@s(\s+|$)|^@s\s+', ' ', command).strip() + logger.info(f"检测到 @s 参数,将优先使用个人URL发送反馈,清理后的命令: {command}") + + # 检查限流 + rate_limiter = get_rate_limiter() + if not rate_limiter.is_allowed(): + remaining = rate_limiter.get_remaining() + reset_time = int(rate_limiter.get_reset_time()) + + sender = get_message_sender() + await sender.send_text( + f"⚠️ 消息发送过于频繁,请等待 {reset_time} 秒后再试\n" + f"剩余配额: {remaining}" + ) + return JSONResponse({"result": "ok"}) + + # 更新用户信息 + db = get_db() + db.get_or_create_user(callback_data.creator) + + # 处理指令 + response_text = await handle_command( + game_type=game_type, + command=command, + chat_id=callback_data.chatid, + user_id=callback_data.creator + ) + + # 发送回复 + if response_text: + # 如果使用了 @s 参数,优先发送到个人URL + if use_private_url: + db = get_db() + user_webhook_url = db.get_user_webhook_url(callback_data.creator) + + if user_webhook_url: + # 有个人URL,发送到个人URL + from utils.message import send_private_message + # 判断消息类型 + if game_type == 'ai_chat': + msg_type = 'markdown' + elif response_text.startswith('#'): + msg_type = 'markdown' + else: + msg_type = 'text' + + success = await send_private_message( + user_id=callback_data.creator, + content=response_text, + msg_type=msg_type + ) + if not success: + # 如果私聊发送失败,回退到主URL + logger.warning(f"个人URL发送失败,回退到主URL: user_id={callback_data.creator}") + sender = get_message_sender() + if game_type == 'ai_chat': + try: + await sender.send_markdown(response_text) + except Exception as send_md_err: + logger.error(f"发送Markdown消息失败,改用文本发送: {send_md_err}") + await sender.send_text(response_text) + else: + if response_text.startswith('#'): + await sender.send_markdown(response_text) + else: + await sender.send_text(response_text) + # 成功发送到个人URL,不向主URL发送 + else: + # 没有个人URL,回退到主URL + logger.info(f"用户 {callback_data.creator} 没有注册个人URL,使用主URL发送") + sender = get_message_sender() + if game_type == 'ai_chat': + try: + await sender.send_markdown(response_text) + except Exception as send_md_err: + logger.error(f"发送Markdown消息失败,改用文本发送: {send_md_err}") + await sender.send_text(response_text) + else: + if response_text.startswith('#'): + await sender.send_markdown(response_text) + else: + await sender.send_text(response_text) + else: + # 没有 @s 参数,正常发送到主URL + sender = get_message_sender() + + # AI 对话:统一按 Markdown 发送(按任务决策) + if game_type == 'ai_chat': + try: + await sender.send_markdown(response_text) + except Exception as send_md_err: + logger.error(f"发送Markdown消息失败,改用文本发送: {send_md_err}") + await sender.send_text(response_text) + else: + # 其他模块保持原有启发式:以 # 开头视为 Markdown,否则文本 + if response_text.startswith('#'): + await sender.send_markdown(response_text) + else: + await sender.send_text(response_text) + + return JSONResponse({"result": "ok"}) + + except Exception as e: + logger.error(f"处理Callback异常: {e}", exc_info=True) + # 仍然返回ok,避免WPS重试 + return JSONResponse({"result": "ok"}) + + +async def handle_command(game_type: str, command: str, + chat_id: int, user_id: int) -> str: + """处理游戏指令 + + Args: + game_type: 游戏类型 + command: 完整指令 + chat_id: 会话ID + user_id: 用户ID + + Returns: + 回复文本 + """ + try: + # 帮助指令 + if game_type == 'help': + from games.base import get_help_message + return get_help_message() + + # 统计指令 + if game_type == 'stats': + from games.base import get_stats_message + return get_stats_message(user_id) + + # 注册系统 + if game_type == 'register': + return await handle_register_command(command, chat_id, user_id) + + # 骰娘游戏 + if game_type == 'dice': + from games.dice import DiceGame + game = DiceGame() + return await game.handle(command, chat_id, user_id) + + # 石头剪刀布 + if game_type == 'rps': + from games.rps import RPSGame + game = RPSGame() + return await game.handle(command, chat_id, user_id) + + # 运势占卜 + if game_type == 'fortune': + from games.fortune import FortuneGame + game = FortuneGame() + return await game.handle(command, chat_id, user_id) + + # 猜数字 + if game_type == 'guess': + from games.guess import GuessGame + game = GuessGame() + return await game.handle(command, chat_id, user_id) + + # 问答游戏 + if game_type == 'quiz': + from games.quiz import QuizGame + game = QuizGame() + return await game.handle(command, chat_id, user_id) + + # 成语接龙 + if game_type == 'idiom': + from games.idiom import IdiomGame + game = IdiomGame() + return await game.handle(command, chat_id, user_id) + + # 五子棋 + if game_type == 'gomoku': + from games.gomoku import GomokuGame + game = GomokuGame() + return await game.handle(command, chat_id, user_id) + + # 积分系统 + if game_type == 'points': + from games.points import PointsGame + game = PointsGame() + return await game.handle(command, chat_id, user_id) + + # 炼金系统 + if game_type == 'alchemy': + from games.alchemy import AlchemyGame + game = AlchemyGame() + return await game.handle(command, chat_id, user_id) + + # 冒险系统 + if game_type == 'adventure': + from games.adventure import AdventureGame + game = AdventureGame() + return await game.handle(command, chat_id, user_id) + + # 积分赠送系统 + if game_type == 'gift': + from games.gift import GiftGame + game = GiftGame() + return await game.handle(command, chat_id, user_id) + + # 复述功能 + if game_type == 'say': + # 提取参数并原样返回 + _, args = CommandParser.extract_command_args(command) + args = args.strip() + if not args: + return "用法:.say 你想让我说的话\n别名:.说 / .复述" + return args + + # 私聊功能 + if game_type == 'talk': + return await handle_talk_command(command, chat_id, user_id) + + # AI对话系统 + if game_type == 'ai_chat': + from games.ai_chat import AIChatGame + game = AIChatGame() + return await game.handle(command, chat_id, user_id) + + # 赌场系统 + if game_type == 'casino': + from games.casino import CasinoGame + game = CasinoGame() + return await game.handle(command, chat_id, user_id) + + # 狼人杀系统 + if game_type == 'werewolf': + from games.werewolf import WerewolfGame + game = WerewolfGame() + return await game.handle(command, chat_id, user_id) + + # 未知游戏类型 + logger.warning(f"未知游戏类型: {game_type}") + return "❌ 未知的游戏类型" + + except Exception as e: + logger.error(f"处理游戏指令异常: {e}", exc_info=True) + return f"❌ 处理指令时出错: {str(e)}" + + +async def handle_register_command(command: str, chat_id: int, user_id: int) -> str: + """处理注册命令 + + Args: + command: 完整指令 ".register name" 或 ".register url " + chat_id: 会话ID + user_id: 用户ID + + Returns: + 注册结果消息 + """ + try: + # 提取参数 + _, args = CommandParser.extract_command_args(command) + args = args.strip() + + # 验证参数 + if not args: + return "❌ 请提供要注册的内容!\n\n正确格式:\n`.register <名称>` - 注册用户名\n`.register url ` - 注册webhook URL\n\n示例:\n`.register 张三`\n`.register url https://example.com/webhook?key=xxx`" + + # 检查是否为url子命令 + parts = args.split(maxsplit=1) + if parts and parts[0].lower() == 'url': + # 处理URL注册 + if len(parts) < 2: + return "❌ 请提供webhook URL!\n\n正确格式:`.register url `\n\n示例:\n`.register url https://example.com/webhook?key=xxx`" + + webhook_url = parts[1].strip() + + # URL验证 + if not webhook_url.startswith(('http://', 'https://')): + return "❌ URL格式无效!必须以 http:// 或 https:// 开头。" + + # 设置URL + db = get_db() + success = db.set_user_webhook_url(user_id, webhook_url) + + if success: + return f"✅ Webhook URL注册成功!\n\n**您的个人URL**:{webhook_url}\n\n私聊消息将发送到此URL。" + else: + return "❌ 注册失败!请稍后重试。" + else: + # 原有的名称注册逻辑 + if len(args) > 20: + return "❌ 名称过长!最多支持20个字符。" + + # 更新用户名称 + db = get_db() + success = db.update_user_name(user_id, args) + + if success: + return f"✅ 注册成功!\n\n**您的名称**:{args}\n\n之后您可以使用这个名称参与各种游戏和功能。" + else: + return "❌ 注册失败!请稍后重试。" + + except Exception as e: + logger.error(f"处理注册指令错误: {e}", exc_info=True) + return f"❌ 处理指令出错: {str(e)}" + + +async def handle_talk_command(command: str, chat_id: int, user_id: int) -> str: + """处理私聊命令 + + Args: + command: 完整指令 ".talk " + chat_id: 会话ID + user_id: 发送者用户ID + + Returns: + 处理结果消息 + """ + try: + # 提取参数 + _, args = CommandParser.extract_command_args(command) + args = args.strip() + + # 验证参数 + if not args: + return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`\n`.talk 李四 这是一条私聊消息`" + + # 解析username和content(第一个单词是username,剩余部分是content) + parts = args.split(maxsplit=1) + if len(parts) < 2: + return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`" + + target_username = parts[0].strip() + content = parts[1].strip() + + if not target_username: + return "❌ 用户名不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`" + if not content: + return "❌ 消息内容不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`" + + # 通过用户名查找目标用户 + db = get_db() + target_user = db.get_user_by_name(target_username) + + if not target_user: + return f"❌ 找不到用户名为「{target_username}」的用户!\n\n提示:目标用户需要使用 `.register <名称>` 注册用户名。" + + target_user_id = target_user['user_id'] + + # 检查目标用户是否有注册名称(应该有,因为是通过名称找到的) + if not target_user.get('username'): + return f"❌ 用户「{target_username}」尚未注册用户名!" + + # 检查目标用户是否有个人webhook URL + if not db.has_webhook_url(target_user_id): + return f"❌ 用户「{target_username}」尚未注册个人webhook URL!\n\n提示:目标用户需要使用 `.register url ` 注册个人URL后才能接收私聊消息。" + + # 发送私聊消息 + from utils.message import send_private_message + success = await send_private_message( + user_id=target_user_id, + content=content, + msg_type='text' + ) + + if success: + # 私聊消息发送成功,不向主URL发送提示消息 + return "" + else: + # 发送失败时仍然需要提示用户 + return f"❌ 发送私聊消息失败,请稍后重试。" + + except Exception as e: + logger.error(f"处理私聊指令错误: {e}", exc_info=True) + return f"❌ 处理指令出错: {str(e)}" + diff --git a/CoreRouters/health.py b/CoreRouters/health.py new file mode 100644 index 0000000..3899a81 --- /dev/null +++ b/CoreRouters/health.py @@ -0,0 +1,57 @@ +"""健康检查路由""" +import psutil +import os +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from CoreModules.database import get_db +from Convention.Runtime.GlobalConfig import ProjectConfig, ConsoleFrontColor + +config = ProjectConfig() + +router = APIRouter() + + +@router.get("/health") +async def health_check(): + """健康检查""" + return JSONResponse({ + "status": "healthy", + "service": config.FindItem("app_name", "Application") + }) + + +@router.get("/stats") +async def system_stats(): + """系统资源统计(开发用)""" + try: + process = psutil.Process(os.getpid()) + memory_mb = process.memory_info().rss / 1024 / 1024 + + # 数据库统计 + db = get_db() + cursor = db.conn.cursor() + + cursor.execute("SELECT COUNT(*) FROM users") + user_count = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM game_states") + active_games = cursor.fetchone()[0] + + return JSONResponse({ + "system": { + "memory_mb": round(memory_mb, 2), + "threads": process.num_threads(), + "cpu_percent": process.cpu_percent() + }, + "database": { + "users": user_count, + "active_games": active_games + } + }) + except Exception as e: + config.Log("Error", f"{ConsoleFrontColor.RED}获取系统统计失败: {e}{ConsoleFrontColor.RESET}", exc_info=True) + return JSONResponse( + status_code=500, + content={"error": str(e)} + ) + diff --git a/CoreRouters/private.py b/CoreRouters/private.py new file mode 100644 index 0000000..6dfffa7 --- /dev/null +++ b/CoreRouters/private.py @@ -0,0 +1,111 @@ +"""私聊相关API路由""" +import logging +from typing import List, Dict +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from core.database import get_db +from core.models import PrivateMessageRequest, CheckBatchRequest, CheckBatchResponse +from utils.message import send_private_message + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.post("/private/send") +async def send_private(request: PrivateMessageRequest): + """发送私聊消息 + + 请求体: + { + "user_id": 123456, + "content": "消息内容", + "msg_type": "text" // 可选,默认为"text" + } + """ + try: + # 验证msg_type + if request.msg_type not in ['text', 'markdown']: + raise HTTPException( + status_code=400, + detail="msg_type必须是'text'或'markdown'" + ) + + # 调用send_private_message + success = await send_private_message( + user_id=request.user_id, + content=request.content, + msg_type=request.msg_type + ) + + if not success: + # 检查用户是否有个人URL + db = get_db() + has_url = db.has_webhook_url(request.user_id) + if not has_url: + raise HTTPException( + status_code=400, + detail=f"用户 {request.user_id} 没有注册个人webhook URL" + ) + else: + raise HTTPException( + status_code=500, + detail="消息发送失败,请稍后重试" + ) + + return JSONResponse({ + "success": True, + "message": "消息发送成功" + }) + + except HTTPException: + raise + except Exception as e: + logger.error(f"发送私聊消息API错误: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"服务器内部错误: {str(e)}" + ) + + +@router.get("/private/check/{user_id}") +async def check_user_webhook(user_id: int): + """检查用户是否有个人webhook URL""" + try: + db = get_db() + has_webhook_url = db.has_webhook_url(user_id) + + return JSONResponse({ + "user_id": user_id, + "has_webhook_url": has_webhook_url + }) + except Exception as e: + logger.error(f"检查用户webhook URL错误: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"服务器内部错误: {str(e)}" + ) + + +@router.post("/private/check-batch") +async def check_users_webhook_batch(request: CheckBatchRequest): + """批量检查用户是否有个人webhook URL + + 请求体: + { + "user_ids": [123456, 789012, ...] + } + """ + try: + db = get_db() + results = db.check_users_webhook_urls(request.user_ids) + + return CheckBatchResponse(results=results) + except Exception as e: + logger.error(f"批量检查用户webhook URL错误: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"服务器内部错误: {str(e)}" + ) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbc3777 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ninemine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Plugins/__init__.py b/Plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b9269b --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# README + +## Clone + +use recursive + +```bash +git clone --recursive +``` + +or + +```bash +git clone +cd Convention +git submodule update --init --recursive +``` + +## First Start + +use + +```bash +python app.py +``` + +to start and generate **Assets** Folder(generate by [ProjectConfig](Convention/Runtime/GlobalConfig.py)) + +## Assets + +Every default argument define in **Assets/config.json**, +properties in **find** is define the arguments value where you not setting + +Sometimes some property not exists in config, +just because program not running to the place where argument been referenced + +## Arguments + +### Commandline and Config + +- **--main-webhook-url** main target of the message will be send, **needed** +- **--host** default: 0.0.0.0 +- **--port** default: 8000 +- **--verbose** default: false + +### Only Config + +- **max_concurrent_requests** default: 100 +- **database_path** file on [Assets](Assets), default: db.db +- **plugin_dir** where plugins load, default: Plugins + +## Plugins + +First import interface and define class +```python +from CoreModules.plugin_interface import PluginInterface, DatabaseModel +class MyPlugin() +``` \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..767341b --- /dev/null +++ b/__main__.py @@ -0,0 +1,5 @@ +from Application.app import main +import sys + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file