This commit is contained in:
Ubuntu
2026-07-03 16:58:28 +08:00
commit 32b43f85bd
6240 changed files with 1987678 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
安全中间件 - Token 验证
"""
import os
import logging
from functools import wraps
from flask import request, jsonify
logger = logging.getLogger(__name__)
# 从环境变量获取 Token,如果没有则生成一个默认的
AUTH_TOKEN = os.environ.get('NANOBOT_AUTH_TOKEN', 'nanobot-wechat-2024')
def check_auth():
"""检查请求是否携带正确的 Token"""
# 从 Header 或 Query 参数获取 Token
token = request.headers.get('X-Auth-Token') or request.args.get('token')
if not token:
logger.warning(f"缺少 Token,IP: {request.remote_addr}")
return False
if token != AUTH_TOKEN:
logger.warning(f"Token 错误,IP: {request.remote_addr}")
return False
return True
def require_auth(f):
"""装饰器:需要认证"""
@wraps(f)
def decorated(*args, **kwargs):
if not check_auth():
return jsonify({"error": "Unauthorized", "message": "缺少或错误的 Token"}), 401
return f(*args, **kwargs)
return decorated
def get_client_ip():
"""获取客户端真实 IP"""
if request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
elif request.headers.get('X-Real-IP'):
return request.headers.get('X-Real-IP')
else:
return request.remote_addr
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
Nanobot 调用适配器
负责将微信消息转换为 nanobot 调用
"""
import os
import subprocess
import tempfile
import logging
from typing import List, Dict
logger = logging.getLogger(__name__)
class NanobotAdapter:
"""nanobot 适配器"""
def __init__(self, config: Dict):
self.config = config
self.command = config.get('command', 'nanobot')
self.timeout = config.get('timeout', 60)
def chat(self, message: str, history: List[Dict], conversation_id: str) -> str:
"""
调用 nanobot 处理消息
Args:
message: 当前消息
history: 历史消息
conversation_id: 会话ID
Returns:
nanobot 的回复
"""
# 构建上下文提示
context = self._build_context(history, conversation_id)
# 构建完整提示
prompt = self._build_prompt(context, message)
logger.info(f"调用 nanobot,prompt长度: {len(prompt)}")
try:
# 调用 nanobot
result = self._call_nanobot(prompt)
return result
except Exception as e:
logger.error(f"调用 nanobot 失败: {e}")
return f"处理消息时出错: {str(e)}"
def _build_context(self, history: List[Dict], conversation_id: str) -> str:
"""构建上下文"""
if not history:
return ""
lines = ["历史对话:"]
for msg in history[-5:]: # 最近5条
role = "用户" if msg['role'] == 'user' else "助手"
content = msg['content'][:100] # 截断过长内容
lines.append(f"{role}: {content}")
return "\n".join(lines)
def _build_prompt(self, context: str, message: str) -> str:
"""构建提示"""
lines = [
"你是一个 AI 助手,正在通过微信公众号为用户提供服务。",
"",
"规则:",
"1. 保持回答简洁明了,适合微信聊天场景",
"2. 如果需要执行复杂操作,先告知用户",
"3. 不要执行危险命令(删除文件、格式化等)",
"4. 回复长度控制在 4000 字符以内",
"",
]
if context:
lines.extend([context, ""])
lines.extend([
"用户消息:",
message,
"",
"请回复:"
])
return "\n".join(lines)
def _call_nanobot(self, prompt: str) -> str:
"""
调用 nanobot agent 命令
使用: nanobot agent -m "message" --session "id"
"""
try:
# 构建命令
cmd = [
self.command,
"agent",
"-m", prompt,
"--session", f"wechat:{hash(prompt) % 10000}",
"--no-markdown",
"--no-logs"
]
logger.info(f"执行命令: {' '.join(cmd[:5])}...")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=self.timeout
)
if result.returncode == 0:
return result.stdout.strip() or "处理完成"
else:
error_msg = result.stderr.strip() if result.stderr else "未知错误"
return f"调用失败: {error_msg}"
except subprocess.TimeoutExpired:
return "处理超时,请稍后再试"
except Exception as e:
logger.error(f"调用异常: {e}")
return f"系统错误: {str(e)}"
def _parse_output(self, output: str) -> str:
"""解析 nanobot 输出"""
lines = output.strip().split('\n')
# 过滤掉日志和系统信息
filtered = []
for line in lines:
# 跳过日志行
if any(x in line for x in ['DEBUG', 'INFO', 'WARNING', 'ERROR']):
continue
# 跳过工具调用提示
if line.startswith('>'):
continue
filtered.append(line)
return '\n'.join(filtered).strip() or "处理完成,无返回内容"
class SimpleNanobotAdapter:
"""
简化版适配器(用于测试)
不依赖 nanobot 命令,直接返回模拟响应
"""
def __init__(self, config: Dict):
self.config = config
self.response_templates = [
"收到你的消息: {message}",
"我理解你在问: {message}\n这是一个很有趣的问题!",
"正在为你处理: {message}\n请稍候...",
]
def chat(self, message: str, history: List[Dict], conversation_id: str) -> str:
"""模拟回复"""
import random
template = random.choice(self.response_templates)
return template.format(message=message[:50])
# 兼容不同环境的适配器选择
try:
# 检查 nanobot 是否可用
result = subprocess.run(
['nanobot', '--version'],
capture_output=True,
timeout=5
)
NANOBOT_AVAILABLE = result.returncode == 0
except:
NANOBOT_AVAILABLE = False
if not NANOBOT_AVAILABLE:
logger.warning("nanobot 命令不可用,将使用模拟适配器")
NanobotAdapter = SimpleNanobotAdapter
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
安全控制模块
负责权限检查、内容过滤、速率限制
"""
import re
import time
import logging
from typing import Dict, List, Set, Tuple
from collections import defaultdict
logger = logging.getLogger(__name__)
class SecurityChecker:
"""安全检查器"""
def __init__(self, config: Dict):
self.config = config
# 允许的工具
self.allowed_tools: Set[str] = set(
config.get('allowed_tools', ['read', 'write', 'edit', 'exec'])
)
# 禁止的命令
self.blocked_commands: List[str] = config.get(
'blocked_commands',
['rm -rf /', 'dd if=', 'mkfs', 'shutdown', 'reboot', 'halt']
)
# 敏感关键词
self.sensitive_keywords: List[str] = [
'password', 'secret', 'token', 'api_key',
'私钥', '密码', '密钥', '.ssh/id_rsa'
]
# 速率限制
self.max_requests = config.get('max_requests_per_minute', 10)
self.request_records: Dict[str, List[float]] = defaultdict(list)
def check_rate_limit(self, user_id: str) -> bool:
"""
检查速率限制
Args:
user_id: 用户标识
Returns:
是否允许请求
"""
now = time.time()
window = 60 # 60秒窗口
# 清理过期记录
self.request_records[user_id] = [
t for t in self.request_records[user_id]
if now - t < window
]
# 检查是否超过限制
if len(self.request_records[user_id]) >= self.max_requests:
logger.warning(f"用户 {user_id} 触发速率限制")
return False
# 记录本次请求
self.request_records[user_id].append(now)
return True
def check_content(self, content: str) -> bool:
"""
检查内容是否合法
Args:
content: 消息内容
Returns:
是否合法
"""
# 检查敏感关键词
content_lower = content.lower()
for keyword in self.sensitive_keywords:
if keyword.lower() in content_lower:
logger.warning(f"内容包含敏感词: {keyword}")
# 不直接拒绝,只记录警告
# 实际使用时可根据需要调整策略
# 检查是否有明显的恶意命令
for cmd in self.blocked_commands:
if cmd in content:
logger.warning(f"检测到禁止命令: {cmd}")
return False
return True
def check_tool_permission(self, tool_name: str) -> bool:
"""
检查工具使用权限
Args:
tool_name: 工具名称
Returns:
是否允许使用
"""
return tool_name in self.allowed_tools
def check_command_safety(self, command: str) -> Tuple[bool, str]:
"""
检查命令是否安全
Args:
command: 命令字符串
Returns:
(是否安全, 原因)
"""
command_lower = command.lower().strip()
# 检查危险命令模式
dangerous_patterns = [
(r'^\s*rm\s+-rf\s*/', "禁止删除根目录"),
(r'^\s*dd\s+if=', "禁止直接使用 dd 命令"),
(r'^\s*mkfs', "禁止格式化文件系统"),
(r'^\s*shutdown', "禁止关机命令"),
(r'^\s*reboot', "禁止重启命令"),
(r'^\s*halt', "禁止 halt 命令"),
(r'>\s*/dev/sd[a-z]', "禁止直接操作磁盘设备"),
(r'>\s*/dev/null.*<\s*/dev/random', "禁止危险的重定向操作"),
]
for pattern, reason in dangerous_patterns:
if re.search(pattern, command_lower):
return False, reason
# 检查敏感文件访问
sensitive_paths = [
'/etc/passwd',
'/etc/shadow',
'~/.ssh/id_rsa',
'~/.aws/credentials',
]
for path in sensitive_paths:
if path in command:
return False, f"禁止访问敏感文件: {path}"
return True, "安全"
def sanitize_output(self, output: str) -> str:
"""
清理输出内容
Args:
output: 原始输出
Returns:
清理后的输出
"""
# 移除可能的敏感信息
patterns = [
(r'[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}', '[TOKEN_REDACTED]'),
(r'sk-[a-zA-Z0-9]{20,}', '[API_KEY_REDACTED]'),
(r'password\s*[:=]\s*\S+', 'password: [REDACTED]'),
]
result = output
for pattern, replacement in patterns:
result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
return result
def get_blocked_commands_list(self) -> List[str]:
"""获取禁止命令列表"""
return self.blocked_commands.copy()
class SimpleSecurityChecker:
"""简化版安全检查器(用于测试)"""
def __init__(self, config: Dict):
pass
def check_rate_limit(self, user_id: str) -> bool:
return True
def check_content(self, content: str) -> bool:
return True
def check_tool_permission(self, tool_name: str) -> bool:
return True
def check_command_safety(self, command: str) -> Tuple[bool, str]:
return True, "安全"
def sanitize_output(self, output: str) -> str:
return output
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""
WeChat-Nanobot Bridge HTTP Server
提供 REST API 接口供 weixin-agent-sdk 调用
"""
import os
import sys
import json
import logging
from datetime import datetime
from flask import Flask, request, jsonify
from flask_cors import CORS
# 添加项目路径
sys.path.insert(0, os.path.dirname(__file__))
from nanobot_adapter import NanobotAdapter
from session_manager import SessionManager
from security import SecurityChecker
# 配置日志
log_dir = os.path.join(os.path.dirname(__file__), '..', 'logs')
os.makedirs(log_dir, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(os.path.join(log_dir, 'server.log')),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# 全局组件
config = {}
adapter = None
session_manager = None
security = None
def load_config():
"""加载配置文件"""
global config
config_path = os.path.join(
os.path.dirname(__file__), '..', 'config', 'config.json'
)
default_config = {
"server": {
"host": "0.0.0.0",
"port": 5000,
"debug": False
},
"nanobot": {
"command": "nanobot",
"timeout": 60,
"max_output_length": 4000
},
"security": {
"allowed_tools": ["read", "write", "edit", "exec"],
"blocked_commands": ["rm -rf", "dd", "mkfs", "shutdown"],
"max_requests_per_minute": 10
},
"session": {
"max_history": 10,
"expire_hours": 24
}
}
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# 合并默认值
for key, value in default_config.items():
if key not in config:
config[key] = value
else:
config = default_config
# 保存默认配置
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2)
return config
def init_components():
"""初始化组件"""
global adapter, session_manager, security
load_config()
adapter = NanobotAdapter(config.get('nanobot', {}))
session_manager = SessionManager(config.get('session', {}))
security = SecurityChecker(config.get('security', {}))
logger.info("组件初始化完成")
@app.route('/health', methods=['GET'])
def health_check():
"""健康检查接口"""
return jsonify({
"status": "ok",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
})
@app.route('/chat', methods=['POST'])
def chat():
"""
聊天接口
Request Body:
{
"conversationId": "user_123",
"text": "你好",
"media": {
"type": "image",
"filePath": "/path/to/image.jpg",
"mimeType": "image/jpeg"
}
}
Response:
{
"text": "回复内容",
"media": null
}
"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Invalid JSON"}), 400
conversation_id = data.get('conversationId', 'unknown')
text = data.get('text', '')
media = data.get('media')
logger.info(f"收到消息 [{conversation_id}]: {text[:50]}...")
# 安全检查
if not security.check_rate_limit(conversation_id):
return jsonify({
"text": "请求过于频繁,请稍后再试"
})
if not security.check_content(text):
return jsonify({
"text": "消息内容包含敏感信息,无法处理"
})
# 获取会话历史
history = session_manager.get_history(conversation_id)
# 调用 nanobot
reply = adapter.chat(text, history, conversation_id)
# 安全过滤回复
reply = security.sanitize_output(reply)
# 截断过长回复
max_length = config.get('nanobot', {}).get('max_output_length', 4000)
if len(reply) > max_length:
reply = reply[:max_length] + "\n\n[内容过长,已截断]"
# 更新会话历史
session_manager.add_message(conversation_id, 'user', text)
session_manager.add_message(conversation_id, 'assistant', reply)
logger.info(f"回复 [{conversation_id}]: {reply[:50]}...")
return jsonify({
"text": reply,
"media": None
})
except Exception as e:
logger.error(f"处理消息时出错: {e}", exc_info=True)
return jsonify({
"text": f"处理消息时出错: {str(e)}"
}), 500
@app.route('/clear', methods=['POST'])
def clear_session():
"""清空会话历史"""
try:
data = request.get_json()
conversation_id = data.get('conversationId', 'unknown')
session_manager.clear_session(conversation_id)
return jsonify({"status": "ok", "message": "会话已清空"})
except Exception as e:
logger.error(f"清空会话时出错: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/context', methods=['POST'])
def get_context():
"""获取会话上下文/历史记录"""
try:
data = request.get_json()
conversation_id = data.get('conversationId', 'unknown')
history = session_manager.get_history(conversation_id)
return jsonify({
"status": "ok",
"conversationId": conversation_id,
"message_count": len(history),
"history": history
})
except Exception as e:
logger.error(f"获取上下文时出错: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/reset', methods=['POST'])
def reset_session():
"""重置会话(清空历史+重置所有状态)"""
try:
data = request.get_json()
conversation_id = data.get('conversationId', 'unknown')
# 清空会话
session_manager.clear_session(conversation_id)
logger.info(f"会话已重置: {conversation_id}")
return jsonify({
"status": "ok",
"message": "会话已完全重置",
"conversationId": conversation_id
})
except Exception as e:
logger.error(f"重置会话时出错: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/stats', methods=['GET'])
def get_stats():
"""获取服务统计信息"""
try:
stats = session_manager.get_stats()
return jsonify({
"status": "ok",
"stats": stats,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
logger.error(f"获取统计时出错: {e}")
return jsonify({"error": str(e)}), 500
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({"error": "Internal server error"}), 500
def main():
"""主入口"""
init_components()
host = config.get('server', {}).get('host', '0.0.0.0')
port = config.get('server', {}).get('port', 5000)
debug = config.get('server', {}).get('debug', False)
logger.info(f"启动服务器: {host}:{port}")
app.run(host=host, port=port, debug=debug)
if __name__ == '__main__':
main()
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
会话管理器
管理用户会话历史和状态
"""
import os
import json
import time
import threading
from typing import Dict, List, Optional
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
class SessionManager:
"""会话管理器"""
def __init__(self, config: Dict):
self.config = config
self.max_history = config.get('max_history', 10)
self.expire_hours = config.get('expire_hours', 24)
# 内存中的会话存储
self.sessions: Dict[str, Dict] = {}
self.lock = threading.Lock()
# 启动清理线程
self._start_cleanup_thread()
def get_history(self, conversation_id: str) -> List[Dict]:
"""
获取会话历史
Args:
conversation_id: 会话ID
Returns:
消息历史列表
"""
with self.lock:
session = self.sessions.get(conversation_id)
if session:
session['last_access'] = time.time()
return session.get('messages', [])
return []
def add_message(self, conversation_id: str, role: str, content: str):
"""
添加消息到会话
Args:
conversation_id: 会话ID
role: 角色 (user/assistant)
content: 消息内容
"""
with self.lock:
if conversation_id not in self.sessions:
self.sessions[conversation_id] = {
'created_at': time.time(),
'last_access': time.time(),
'messages': []
}
session = self.sessions[conversation_id]
session['last_access'] = time.time()
# 添加消息
session['messages'].append({
'role': role,
'content': content,
'timestamp': datetime.now().isoformat()
})
# 限制历史长度
if len(session['messages']) > self.max_history:
session['messages'] = session['messages'][-self.max_history:]
def clear_session(self, conversation_id: str):
"""清空会话"""
with self.lock:
if conversation_id in self.sessions:
del self.sessions[conversation_id]
logger.info(f"会话已清空: {conversation_id}")
def get_session_info(self, conversation_id: str) -> Optional[Dict]:
"""获取会话信息"""
with self.lock:
session = self.sessions.get(conversation_id)
if session:
return {
'conversation_id': conversation_id,
'created_at': session['created_at'],
'last_access': session['last_access'],
'message_count': len(session['messages'])
}
return None
def _start_cleanup_thread(self):
"""启动清理过期会话的线程"""
def cleanup():
while True:
time.sleep(3600) # 每小时清理一次
self._cleanup_expired()
thread = threading.Thread(target=cleanup, daemon=True)
thread.start()
logger.info("会话清理线程已启动")
def _cleanup_expired(self):
"""清理过期会话"""
expire_seconds = self.expire_hours * 3600
now = time.time()
expired = []
with self.lock:
for cid, session in self.sessions.items():
if now - session['last_access'] > expire_seconds:
expired.append(cid)
for cid in expired:
del self.sessions[cid]
if expired:
logger.info(f"清理了 {len(expired)} 个过期会话")
def get_stats(self) -> Dict:
"""获取统计信息"""
with self.lock:
return {
'total_sessions': len(self.sessions),
'total_messages': sum(
len(s['messages']) for s in self.sessions.values()
)
}