This commit is contained in:
tt-P607
2025-09-14 16:44:44 +08:00
12 changed files with 362 additions and 143 deletions

32
.github/workflows/create-prerelease.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
# 当代码推送到 master 分支时,自动创建一个 pre-release
name: Create Pre-release
on:
push:
branches:
- master
jobs:
create-prerelease:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# 获取所有提交历史,以便生成 release notes
fetch-depth: 0
- name: Generate tag name
id: generate_tag
run: echo "TAG_NAME=prerelease-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
- name: Create Pre-release
env:
# 使用仓库自带的 GITHUB_TOKEN 进行认证
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create ${{ steps.generate_tag.outputs.TAG_NAME }} \
--title "Pre-release ${{ steps.generate_tag.outputs.TAG_NAME }}" \
--prerelease \
--generate-notes

View File

@@ -29,7 +29,7 @@
## 📖 项目介绍 ## 📖 项目介绍
**MoFox_Bot** 是一个基于 [MaiCore](https://github.com/MaiM-with-u/MaiBot) `0.10.0 snapshot.5` 版本的增强型 `fork` 项目。 **MoFox_Bot** 是一个基于 [MaiCore](https://github.com/MaiM-with-u/MaiBot) `0.10.0 snapshot.5` 版本的增强型 `fork` 项目。
我们在保留原版所有功能的基础上,进行了一系列的改进和功能拓展,致力于提供更强的稳定性、更丰富的功能和更流畅的用户体验 我们在保留原版所有功能的基础上,进行了一系列的改进和功能拓展,致力于提供更强的稳定性、更丰富的功能和更流畅的用户体验
> [!IMPORTANT] > [!IMPORTANT]
> **第三方项目声明** > **第三方项目声明**

127
bot.py
View File

@@ -1,6 +1,5 @@
# import asyncio # import asyncio
import asyncio import asyncio
import hashlib
import os import os
import sys import sys
import time import time
@@ -9,6 +8,7 @@ import traceback
from pathlib import Path from pathlib import Path
from rich.traceback import install from rich.traceback import install
from colorama import init, Fore from colorama import init, Fore
from dotenv import load_dotenv # 处理.env文件
# maim_message imports for console input # maim_message imports for console input
@@ -34,6 +34,28 @@ script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir) os.chdir(script_dir)
logger.info(f"已设置工作目录为: {script_dir}") logger.info(f"已设置工作目录为: {script_dir}")
# 检查并创建.env文件
def ensure_env_file():
"""确保.env文件存在如果不存在则从模板创建"""
env_file = Path(".env")
template_env = Path("template/template.env")
if not env_file.exists():
if template_env.exists():
logger.info("未找到.env文件正在从模板创建...")
import shutil
shutil.copy(template_env, env_file)
logger.info("已从template/template.env创建.env文件")
logger.warning("请编辑.env文件将EULA_CONFIRMED设置为true并配置其他必要参数")
else:
logger.error("未找到.env文件和template.env模板文件")
sys.exit(1)
# 确保环境文件存在
ensure_env_file()
# 加载环境变量
load_dotenv()
confirm_logger = get_logger("confirm") confirm_logger = get_logger("confirm")
# 获取没有加载env时的环境变量 # 获取没有加载env时的环境变量
@@ -105,82 +127,41 @@ async def graceful_shutdown():
logger.error(f"麦麦关闭失败: {e}", exc_info=True) logger.error(f"麦麦关闭失败: {e}", exc_info=True)
def _calculate_file_hash(file_path: Path, file_type: str) -> str:
"""计算文件的MD5哈希值"""
if not file_path.exists():
logger.error(f"{file_type} 文件不存在")
raise FileNotFoundError(f"{file_type} 文件不存在")
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
return hashlib.md5(content.encode("utf-8")).hexdigest()
def _check_agreement_status(file_hash: str, confirm_file: Path, env_var: str) -> tuple[bool, bool]:
"""检查协议确认状态
Returns:
tuple[bool, bool]: (已确认, 未更新)
"""
# 检查环境变量确认
if file_hash == os.getenv(env_var):
return True, False
# 检查确认文件
if confirm_file.exists():
with open(confirm_file, "r", encoding="utf-8") as f:
confirmed_content = f.read()
if file_hash == confirmed_content:
return True, False
return False, True
def _prompt_user_confirmation(eula_hash: str, privacy_hash: str) -> None:
"""提示用户确认协议"""
confirm_logger.critical("EULA或隐私条款内容已更新请在阅读后重新确认继续运行视为同意更新后的以上两款协议")
confirm_logger.critical(
f'输入"同意""confirmed"或设置环境变量"EULA_AGREE={eula_hash}""PRIVACY_AGREE={privacy_hash}"继续运行'
)
while True:
user_input = input().strip().lower()
if user_input in ["同意", "confirmed"]:
return
confirm_logger.critical('请输入"同意""confirmed"以继续运行')
def _save_confirmations(eula_updated: bool, privacy_updated: bool, eula_hash: str, privacy_hash: str) -> None:
"""保存用户确认结果"""
if eula_updated:
logger.info(f"更新EULA确认文件{eula_hash}")
Path("eula.confirmed").write_text(eula_hash, encoding="utf-8")
if privacy_updated:
logger.info(f"更新隐私条款确认文件{privacy_hash}")
Path("privacy.confirmed").write_text(privacy_hash, encoding="utf-8")
def check_eula(): def check_eula():
"""检查EULA和隐私条款确认状态""" """检查EULA和隐私条款确认状态 - 环境变量版类似Minecraft"""
# 计算文件哈希值 # 检查环境变量中的EULA确认
eula_hash = _calculate_file_hash(Path("EULA.md"), "EULA.md") eula_confirmed = os.getenv('EULA_CONFIRMED', '').lower()
privacy_hash = _calculate_file_hash(Path("PRIVACY.md"), "PRIVACY.md")
# 检查确认状态 if eula_confirmed == 'true':
eula_confirmed, eula_updated = _check_agreement_status(eula_hash, Path("eula.confirmed"), "EULA_AGREE") logger.info("EULA已通过环境变量确认")
privacy_confirmed, privacy_updated = _check_agreement_status(
privacy_hash, Path("privacy.confirmed"), "PRIVACY_AGREE"
)
# 早期返回:如果都已确认且未更新
if eula_confirmed and privacy_confirmed:
return return
# 如果有更新,需要重新确认 # 如果没有确认,提示用户
if eula_updated or privacy_updated: confirm_logger.critical("您需要同意EULA和隐私条款才能使用MoFox_Bot")
_prompt_user_confirmation(eula_hash, privacy_hash) confirm_logger.critical("请阅读以下文件:")
_save_confirmations(eula_updated, privacy_updated, eula_hash, privacy_hash) confirm_logger.critical(" - EULA.md (用户许可协议)")
confirm_logger.critical(" - PRIVACY.md (隐私条款)")
confirm_logger.critical("然后编辑 .env 文件,将 'EULA_CONFIRMED=false' 改为 'EULA_CONFIRMED=true'")
# 等待用户确认
while True:
try:
load_dotenv(override=True) # 重新加载.env文件
eula_confirmed = os.getenv('EULA_CONFIRMED', '').lower()
if eula_confirmed == 'true':
confirm_logger.info("EULA确认成功感谢您的同意")
return
confirm_logger.critical("请修改 .env 文件中的 EULA_CONFIRMED=true 后重新启动程序")
input("按Enter键检查.env文件状态...")
except KeyboardInterrupt:
confirm_logger.info("用户取消,程序退出")
sys.exit(0)
except Exception as e:
confirm_logger.error(f"检查EULA状态失败: {e}")
sys.exit(1)
class MaiBotMain(BaseMain): class MaiBotMain(BaseMain):

View File

@@ -201,7 +201,7 @@ class CycleProcessor:
result = await event_manager.trigger_event( result = await event_manager.trigger_event(
EventType.ON_PLAN, permission_group="SYSTEM", stream_id=self.context.chat_stream EventType.ON_PLAN, permission_group="SYSTEM", stream_id=self.context.chat_stream
) )
if not result.all_continue_process(): if result and not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成")
with Timer("规划器", cycle_timers): with Timer("规划器", cycle_timers):
actions, _ = await self.action_planner.plan(mode=mode) actions, _ = await self.action_planner.plan(mode=mode)
@@ -298,9 +298,12 @@ class CycleProcessor:
if reply_actions: if reply_actions:
logger.info(f"{self.log_prefix} 正在执行文本回复...") logger.info(f"{self.log_prefix} 正在执行文本回复...")
for action in reply_actions: for action in reply_actions:
target_user_id = action.get("action_message",{}).get("chat_info_user_id","") action_message = action.get("action_message")
action_message_test =action.get("action_message",{}) if not action_message:
logger.info(f"如果你探到这条日志请把它复制下来发到Q群里,如果你探到这条日志请把它复制下来发到Q群里,如果你探到这条日志请把它复制下来发到Q群里,调试内容:{action_message_test}") logger.warning(f"{self.log_prefix} reply 动作缺少 action_message,跳过")
continue
target_user_id = action_message.get("chat_info_user_id","")
if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self: if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self:
logger.warning("选取的reply的目标为bot自己跳过reply action") logger.warning("选取的reply的目标为bot自己跳过reply action")
continue continue

View File

@@ -42,15 +42,18 @@ class PlanFilter:
""" """
执行筛选逻辑,并填充 Plan 对象的 decided_actions 字段。 执行筛选逻辑,并填充 Plan 对象的 decided_actions 字段。
""" """
logger.debug(f"墨墨在这里加了日志 -> filter 入口 plan: {plan}")
try: try:
prompt, used_message_id_list = await self._build_prompt(plan) prompt, used_message_id_list = await self._build_prompt(plan)
plan.llm_prompt = prompt plan.llm_prompt = prompt
logger.debug(f"墨墨在这里加了日志 -> LLM prompt: {prompt}")
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt) llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
if llm_content: if llm_content:
logger.debug(f"LLM a原始返回: {llm_content}") logger.debug(f"墨墨在这里加了日志 -> LLM a原始返回: {llm_content}")
parsed_json = orjson.loads(repair_json(llm_content)) parsed_json = orjson.loads(repair_json(llm_content))
logger.debug(f"墨墨在这里加了日志 -> 解析后的 JSON: {parsed_json}")
if isinstance(parsed_json, dict): if isinstance(parsed_json, dict):
parsed_json = [parsed_json] parsed_json = [parsed_json]
@@ -92,6 +95,7 @@ class PlanFilter:
ActionPlannerInfo(action_type="no_action", reasoning=f"筛选时出错: {e}") ActionPlannerInfo(action_type="no_action", reasoning=f"筛选时出错: {e}")
] ]
logger.debug(f"墨墨在这里加了日志 -> filter 出口 decided_actions: {plan.decided_actions}")
return plan return plan
async def _build_prompt(self, plan: Plan) -> tuple[str, list]: async def _build_prompt(self, plan: Plan) -> tuple[str, list]:

View File

@@ -3,6 +3,7 @@ import importlib.metadata
from maim_message import MessageServer from maim_message import MessageServer
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
import os
global_api = None global_api = None
@@ -22,9 +23,18 @@ def get_global_api() -> MessageServer: # sourcery skip: extract-method
maim_message_config = global_config.maim_message maim_message_config = global_config.maim_message
# 设置基本参数 # 设置基本参数
host = os.getenv("HOST", "127.0.0.1")
port_str = os.getenv("PORT", "8000")
try:
port = int(port_str)
except ValueError:
port = 8000
kwargs = { kwargs = {
"host": global_config.server.host, "host": host,
"port": int(global_config.server.port), "port": port,
"app": get_global_server().get_app(), "app": get_global_server().get_app(),
} }

View File

@@ -1,9 +1,13 @@
import asyncio import asyncio
import base64
import json
import aiohttp import aiohttp
import platform import platform
from datetime import datetime, timezone from datetime import datetime, timezone
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.tcp_connector import get_tcp_connector from src.common.tcp_connector import get_tcp_connector
from src.config.config import global_config from src.config.config import global_config
@@ -24,9 +28,12 @@ class TelemetryHeartBeatTask(AsyncTask):
self.server_url = TELEMETRY_SERVER_URL self.server_url = TELEMETRY_SERVER_URL
"""遥测服务地址""" """遥测服务地址"""
self.client_uuid: str | None = local_storage["mmc_uuid"] if "mmc_uuid" in local_storage else None # type: ignore self.client_uuid: str | None = local_storage["mofox_uuid"] if "mofox_uuid" in local_storage else None # type: ignore
"""客户端UUID""" """客户端UUID"""
self.private_key_pem: str | None = local_storage["mofox_private_key"] if "mofox_private_key" in local_storage else None # type: ignore
"""客户端私钥"""
self.info_dict = self._get_sys_info() self.info_dict = self._get_sys_info()
"""系统信息字典""" """系统信息字典"""
@@ -36,7 +43,7 @@ class TelemetryHeartBeatTask(AsyncTask):
info_dict = { info_dict = {
"os_type": "Unknown", "os_type": "Unknown",
"py_version": platform.python_version(), "py_version": platform.python_version(),
"mmc_version": global_config.MMC_VERSION, "mofox_version": global_config.MMC_VERSION,
} }
match platform.system(): match platform.system():
@@ -51,77 +58,224 @@ class TelemetryHeartBeatTask(AsyncTask):
return info_dict return info_dict
def _generate_signature(self, request_body: dict) -> tuple[str, str]:
"""
生成RSA签名
Returns:
tuple[str, str]: (timestamp, signature_b64)
"""
if not self.private_key_pem:
raise ValueError("私钥未初始化")
# 生成时间戳
timestamp = datetime.now(timezone.utc).isoformat()
# 创建签名数据字符串
sign_data = f"{self.client_uuid}:{timestamp}:{json.dumps(request_body, separators=(',', ':'))}"
# 加载私钥
private_key = serialization.load_pem_private_key(
self.private_key_pem.encode('utf-8'),
password=None
)
# 确保是RSA私钥
if not isinstance(private_key, rsa.RSAPrivateKey):
raise ValueError("私钥必须是RSA格式")
# 生成签名
signature = private_key.sign(
sign_data.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Base64编码
signature_b64 = base64.b64encode(signature).decode('utf-8')
return timestamp, signature_b64
def _decrypt_challenge(self, challenge_b64: str) -> str:
"""
解密挑战数据
Args:
challenge_b64: Base64编码的挑战数据
Returns:
str: 解密后的UUID字符串
"""
if not self.private_key_pem:
raise ValueError("私钥未初始化")
# 加载私钥
private_key = serialization.load_pem_private_key(
self.private_key_pem.encode('utf-8'),
password=None
)
# 确保是RSA私钥
if not isinstance(private_key, rsa.RSAPrivateKey):
raise ValueError("私钥必须是RSA格式")
# 解密挑战数据
decrypted_bytes = private_key.decrypt(
base64.b64decode(challenge_b64),
padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return decrypted_bytes.decode('utf-8')
async def _req_uuid(self) -> bool: async def _req_uuid(self) -> bool:
""" """
向服务端请求UUID不应在已存在UUID的情况下调用会覆盖原有的UUID 向服务端请求UUID和私钥(两步注册流程
""" """
if "deploy_time" not in local_storage:
logger.error("本地存储中缺少部署时间无法请求UUID")
return False
try_count: int = 0 try_count: int = 0
while True: while True:
# 如果不存在则向服务端请求一个新的UUID注册客户端 logger.info("正在向遥测服务端注册客户端...")
logger.info("正在向遥测服务端请求UUID...")
try: try:
async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session: async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session:
# Step 1: 获取临时UUID、私钥和挑战数据
logger.debug("开始注册步骤1获取临时UUID和私钥")
async with session.post( async with session.post(
f"{TELEMETRY_SERVER_URL}/stat/reg_client", f"{TELEMETRY_SERVER_URL}/stat/reg_client_step1",
json={"deploy_time": datetime.fromtimestamp(local_storage["deploy_time"], tz=timezone.utc).isoformat()}, json={},
timeout=aiohttp.ClientTimeout(total=5), # 设置超时时间为5秒 timeout=aiohttp.ClientTimeout(total=5),
) as response: ) as response:
logger.debug(f"{TELEMETRY_SERVER_URL}/stat/reg_client") logger.debug(f"Step1 Response status: {response.status}")
logger.debug(local_storage["deploy_time"]) # type: ignore
logger.debug(f"Response status: {response.status}") if response.status != 200:
response_text = await response.text()
logger.error(
f"注册步骤1失败状态码: {response.status}, 响应内容: {response_text}"
)
raise aiohttp.ClientResponseError(
request_info=response.request_info,
history=response.history,
status=response.status,
message=f"Step1 failed: {response_text}"
)
step1_data = await response.json()
temp_uuid = step1_data.get("temp_uuid")
private_key = step1_data.get("private_key")
challenge = step1_data.get("challenge")
if not all([temp_uuid, private_key, challenge]):
logger.error("Step1响应缺少必要字段temp_uuid, private_key 或 challenge")
raise ValueError("Step1响应数据不完整")
# 临时保存私钥用于解密
self.private_key_pem = private_key
# 解密挑战数据
logger.debug("解密挑战数据...")
try:
decrypted_uuid = self._decrypt_challenge(challenge)
except Exception as e:
logger.error(f"解密挑战数据失败: {e}")
raise
# 验证解密结果
if decrypted_uuid != temp_uuid:
logger.error(f"解密结果验证失败: 期望 {temp_uuid}, 实际 {decrypted_uuid}")
raise ValueError("解密结果与临时UUID不匹配")
logger.debug("挑战数据解密成功开始注册步骤2")
# Step 2: 发送解密结果完成注册
async with session.post(
f"{TELEMETRY_SERVER_URL}/stat/reg_client_step2",
json={
"temp_uuid": temp_uuid,
"decrypted_uuid": decrypted_uuid
},
timeout=aiohttp.ClientTimeout(total=5),
) as response:
logger.debug(f"Step2 Response status: {response.status}")
if response.status == 200: if response.status == 200:
data = await response.json() step2_data = await response.json()
if client_id := data.get("mmc_uuid"): mofox_uuid = step2_data.get("mofox_uuid")
# 将UUID存储到本地
local_storage["mmc_uuid"] = client_id if mofox_uuid:
self.client_uuid = client_id # 将正式UUID和私钥存储到本地
logger.info(f"成功获取UUID: {self.client_uuid}") local_storage["mofox_uuid"] = mofox_uuid
return True # 成功获取UUID返回True local_storage["mofox_private_key"] = private_key
self.client_uuid = mofox_uuid
self.private_key_pem = private_key
logger.info(f"成功注册客户端UUID: {self.client_uuid}")
return True
else: else:
logger.error("无效的服务端响应") logger.error("Step2响应缺少mofox_uuid字段")
raise ValueError("Step2响应数据不完整")
elif response.status in [400, 401]:
# 临时数据无效,需要重新开始
response_text = await response.text()
logger.warning(f"Step2失败临时数据无效: {response.status}, {response_text}")
raise ValueError(f"Step2失败: {response_text}")
else: else:
response_text = await response.text() response_text = await response.text()
logger.error( logger.error(
f"请求UUID失败不过你还是可以正常使用墨狐,状态码: {response.status}, 响应内容: {response_text}" f"注册步骤2失败,状态码: {response.status}, 响应内容: {response_text}"
) )
raise aiohttp.ClientResponseError(
request_info=response.request_info,
history=response.history,
status=response.status,
message=f"Step2 failed: {response_text}"
)
except Exception as e: except Exception as e:
import traceback import traceback
error_msg = str(e) or "未知错误" error_msg = str(e) or "未知错误"
logger.warning( logger.warning(
f"请求UUID出错,不过你还是可以正常使用墨狐: {type(e).__name__}: {error_msg}" f"注册客户端出错,不过你还是可以正常使用墨狐: {type(e).__name__}: {error_msg}"
) # 可能是网络问题 )
logger.debug(f"完整错误信息: {traceback.format_exc()}") logger.debug(f"完整错误信息: {traceback.format_exc()}")
# 请求失败,重试次数+1 # 请求失败,重试次数+1
try_count += 1 try_count += 1
if try_count > 3: if try_count > 3:
# 如果超过3次仍然失败则退出 # 如果超过3次仍然失败则退出
logger.error("获取UUID失败,请检查网络连接或服务端状态") logger.error("注册客户端失败,请检查网络连接或服务端状态")
return False return False
else: else:
# 如果可以重试,等待后继续(指数退避) # 如果可以重试,等待后继续(指数退避)
logger.info(f"获取UUID失败,将于 {4**try_count} 秒后重试...") logger.info(f"注册客户端失败,将于 {4**try_count} 秒后重试...")
await asyncio.sleep(4**try_count) await asyncio.sleep(4**try_count)
async def _send_heartbeat(self): async def _send_heartbeat(self):
"""向服务器发送心跳""" """向服务器发送心跳"""
headers = { if not self.client_uuid or not self.private_key_pem:
"Client-UUID": self.client_uuid, logger.error("UUID或私钥未初始化无法发送心跳")
"User-Agent": f"HeartbeatClient/{self.client_uuid[:8]}", # type: ignore return
}
logger.debug(f"正在发送心跳到服务器: {self.server_url}")
logger.debug(str(headers))
try: try:
# 生成签名
timestamp, signature = self._generate_signature(self.info_dict)
headers = {
"X-mofox-UUID": self.client_uuid,
"X-mofox-Signature": signature,
"X-mofox-Timestamp": timestamp,
"User-Agent": f"MofoxClient/{self.client_uuid[:8]}",
"Content-Type": "application/json"
}
logger.debug(f"正在发送心跳到服务器: {self.server_url}")
logger.debug(f"Headers: {headers}")
async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session: async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session:
async with session.post( async with session.post(
f"{self.server_url}/stat/client_heartbeat", f"{self.server_url}/stat/client_heartbeat",
@@ -135,31 +289,62 @@ class TelemetryHeartBeatTask(AsyncTask):
if 200 <= response.status < 300: if 200 <= response.status < 300:
# 成功 # 成功
logger.debug(f"心跳发送成功,状态码: {response.status}") logger.debug(f"心跳发送成功,状态码: {response.status}")
elif response.status == 403: elif response.status == 401:
# 403 Forbidden # 401 Unauthorized - 签名验证失败
logger.warning( logger.warning(
"此消息不会影响正常使用心跳发送失败403 Forbidden: 可能是UUID无效或未注册" "此消息不会影响正常使用心跳发送失败401 Unauthorized: 签名验证失败"
"处理措施:重置UUID,下次发送心跳时将尝试重新注册。" "处理措施:重置客户端信息,下次发送心跳时将尝试重新注册。"
) )
self.client_uuid = None self.client_uuid = None
del local_storage["mmc_uuid"] # 删除本地存储的UUID self.private_key_pem = None
if "mofox_uuid" in local_storage:
del local_storage["mofox_uuid"]
if "mofox_private_key" in local_storage:
del local_storage["mofox_private_key"]
elif response.status == 404:
# 404 Not Found - 客户端未注册
logger.warning(
"此消息不会影响正常使用心跳发送失败404 Not Found: 客户端未注册。"
"处理措施:重置客户端信息,下次发送心跳时将尝试重新注册。"
)
self.client_uuid = None
self.private_key_pem = None
if "mofox_uuid" in local_storage:
del local_storage["mofox_uuid"]
if "mofox_private_key" in local_storage:
del local_storage["mofox_private_key"]
elif response.status == 403:
# 403 Forbidden - UUID无效或未注册
response_text = await response.text()
logger.warning(
f"此消息不会影响正常使用心跳发送失败403 Forbidden: UUID无效或未注册。"
f"响应内容: {response_text}"
"处理措施:重置客户端信息,下次发送心跳时将尝试重新注册。"
)
self.client_uuid = None
self.private_key_pem = None
if "mofox_uuid" in local_storage:
del local_storage["mofox_uuid"]
if "mofox_private_key" in local_storage:
del local_storage["mofox_private_key"]
else: else:
# 其他错误 # 其他错误
response_text = await response.text() response_text = await response.text()
logger.warning( logger.warning(
f"(此消息不会影响正常使用)状态未发送,状态码: {response.status}, 响应内容: {response_text}" f"(此消息不会影响正常使用)心跳发送失败,状态码: {response.status}, 响应内容: {response_text}"
) )
except Exception as e: except Exception as e:
import traceback import traceback
error_msg = str(e) or "未知错误" error_msg = str(e) or "未知错误"
logger.warning(f"(此消息不会影响正常使用)状态未发生: {type(e).__name__}: {error_msg}") logger.warning(f"(此消息不会影响正常使用)心跳发送出错: {type(e).__name__}: {error_msg}")
logger.debug(f"完整错误信息: {traceback.format_exc()}") logger.debug(f"完整错误信息: {traceback.format_exc()}")
async def run(self): async def run(self):
# 发送心跳 # 检查是否已注册
if self.client_uuid is None and not await self._req_uuid(): if not self.client_uuid or not self.private_key_pem:
logger.warning("获取UUID失败跳过此次心跳") if not await self._req_uuid():
return logger.warning("客户端注册失败,跳过此次心跳")
return
await self._send_heartbeat() await self._send_heartbeat()

View File

@@ -4,6 +4,7 @@ from typing import Optional
from uvicorn import Config, Server as UvicornServer from uvicorn import Config, Server as UvicornServer
from src.config.config import global_config from src.config.config import global_config
from rich.traceback import install from rich.traceback import install
import os
install(extra_lines=3) install(extra_lines=3)
@@ -98,5 +99,14 @@ def get_global_server() -> Server:
"""获取全局服务器实例""" """获取全局服务器实例"""
global global_server global global_server
if global_server is None: if global_server is None:
global_server = Server(host=global_config.server.host,port=int(global_config.server.port),)
host = os.getenv("HOST", "127.0.0.1")
port_str = os.getenv("PORT", "8000")
try:
port = int(port_str)
except ValueError:
port = 8000
global_server = Server(host=host, port=port)
return global_server return global_server

View File

@@ -43,8 +43,7 @@ from src.config.official_configs import (
CrossContextConfig, CrossContextConfig,
PermissionConfig, PermissionConfig,
CommandConfig, CommandConfig,
PlanningSystemConfig, PlanningSystemConfig
ServerConfig,
) )
from .api_ada_configs import ( from .api_ada_configs import (
@@ -399,7 +398,6 @@ class Config(ValidatedConfigBase):
cross_context: CrossContextConfig = Field( cross_context: CrossContextConfig = Field(
default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置" default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置"
) )
server: ServerConfig = Field(default_factory=lambda: ServerConfig(), description="主服务器配置")
class APIAdapterConfig(ValidatedConfigBase): class APIAdapterConfig(ValidatedConfigBase):

View File

@@ -477,12 +477,6 @@ class ExperimentalConfig(ValidatedConfigBase):
pfc_chatting: bool = Field(default=False, description="启用PFC聊天") pfc_chatting: bool = Field(default=False, description="启用PFC聊天")
class ServerConfig(ValidatedConfigBase):
"""主服务器配置类"""
host: str = Field(default="127.0.0.1", description="主服务器监听地址")
port: int = Field(default=8080, description="主服务器监听端口")
class MaimMessageConfig(ValidatedConfigBase): class MaimMessageConfig(ValidatedConfigBase):
"""maim_message配置类""" """maim_message配置类"""

View File

@@ -5,6 +5,7 @@ import signal
import sys import sys
from maim_message import MessageServer from maim_message import MessageServer
from src.common.remote import TelemetryHeartBeatTask
from src.manager.async_task_manager import async_task_manager from src.manager.async_task_manager import async_task_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from src.common.remote import TelemetryHeartBeatTask from src.common.remote import TelemetryHeartBeatTask

View File

@@ -1,2 +1,3 @@
HOST=127.0.0.1 HOST=127.0.0.1
PORT=8000 PORT=8000
EULA_CONFIRMED=false