#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author : zhibo.wang
# E-mail : gm.zhibo.wang@gmail.com
# Date :
# Desc : 每个群对应一个会话ID
from common.log import logger
from plugins import *
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from channel.chat_message import ChatMessage
from channel.wechatnt.nt_run import wechatnt
try:
import json
import time
import plugins
import datetime
import requests
from fake_useragent import UserAgent
from .xinuo_utils import Util, DailyNumberLimiter
import uuid
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except Exception as e:
logger.error(f"[Xinuo] import error: {e}")
def get_with_retry(get_func, max_retries=5, delay=5):
retries = 0
result = None
while retries < max_retries:
result = get_func()
logger.warning(f"获取数据,第{retries + 1}次······")
if result:
break
retries += 1
time.sleep(delay) # 等待一段时间后重试
return result
@plugins.register(
name="Xinuo", # 插件的名称
desire_priority=666, # 插件的优先级 数据越大优先级越高
hidden=False, # 插件是否隐藏
desc="技术客服", # 插件的描述
version="0.0.5", # 插件的版本号
author="gm.zhibo.wang@gmail.com", # 插件的作者
)
class Xinuo(Plugin):
def __init__(self):
super().__init__()
tag = "xinuo 初始化"
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
try:
self.conf = super().load_config()
self.kb_base_url = self.conf.get("kb_base_url", "")
self.kb_token = self.conf.get("kb_token", "")
self.kb_profile_id = self.conf.get("kb_profile_id", "")
self.kb_group_name_chat_ids = self.conf.get("kb_group_name_chat_ids", {})
logger.info("[Xinuo] inited")
except Exception as e:
log_msg = f"{tag}: init failed error: {e}"
logger.error(log_msg)
def on_handle_context(self, e_context: EventContext, retry_count: int = 0):
"""
TEXT = 1 # 文本消息
VOICE = 2 # 音频消息
IMAGE = 3 # 图片消息
FILE = 4 # 文件信息
VIDEO = 5 # 视频信息
IMAGE_CREATE = 10 # 创建图片命令
ACCEPT_FRIEND = 19 # 同意好友请求
JOIN_GROUP = 20 # 加入群聊
PATPAT = 21 # 拍了拍
FUNCTION = 22 # 函数调用
EXIT_GROUP = 23 # 退出
"""
if e_context["context"].type not in [
ContextType.TEXT,
ContextType.FILE,
ContextType.PATPAT,
ContextType.JOIN_GROUP,
ContextType.EXIT_GROUP,
]:
return
e_msg: ChatMessage = e_context["context"]["msg"]
context = e_context["context"]
cmsg = context["msg"]
isgroup = context.get("isgroup", False)
user = context["receiver"]
content = context.content.lower().strip()
session_id = context["session_id"]
# `session_id`: 会话ID(一般是发送触发bot消息的用户ID
if not Util.is_admin(e_context):
is_admin = False
else:
is_admin = True
if isgroup:
group_name = cmsg.other_user_nickname
group_id = cmsg.other_user_id
actual_user_nickname = cmsg.actual_user_nickname
from_user_nickname = cmsg.from_user_nickname
else:
group_name = ""
group_id = ""
actual_user_nickname = ""
from_user_nickname = cmsg.from_user_nickname
logger.info(f"""[xinuo] on_handle_context,
session_id: {session_id}, isgroup: {isgroup}, is_admin: {is_admin},
group_name: {group_name}, group_id: {group_id},
actual_user_nickname: {actual_user_nickname},
from_user_nickname: {from_user_nickname},
content: {content}
""".replace(" ", "").replace("\n", ""))
if e_context["context"].type == ContextType.TEXT:
if content == "更新群成员":
tag = "更新群成员"
gpt_text = content.strip()
logger.info(f"{tag}: {gpt_text}")
msg = self.update_rooms(tag, group_name)
reply = self.create_reply(ReplyType.TEXT, tag, msg)
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
elif "@所有人" not in content:
# 默认使用 科技智能AI
tag = "技术客服"
gpt_text = content.strip()
logger.info(f"{tag}: {gpt_text}")
reply_1 = Reply(
ReplyType.TEXT,
f"[🤖] {tag}\n🎉正在为您转接客服,请稍候...\n----------------------\n[🤖]")
channel = e_context["channel"]
channel.send(reply_1, context)
msg = self.fun_yunshangkeji_bot(gpt_text, group_name, tag)
reply = self.create_reply(ReplyType.TEXT, tag, msg)
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
else:
e_context.action = EventAction.BREAK_PASS
elif e_context["context"].type == ContextType.PATPAT:
logger.info(f"拍一拍 session_id: {session_id}, content: {content}")
if "拍了拍我" in content:
tag = "拍一拍"
content = "有什么可以帮助您吗?"
reply = self.create_reply(ReplyType.TEXT, tag, content)
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
else:
return
def create_reply(self, reply_type, tag, content):
reply = Reply()
reply.type = reply_type
content = f"{tag}\n{content}"
reply.content = content
return reply
def get_help_text(self, verbose=False, **kwargs):
help_text = "发送关键词执行对应操作\n"
if not verbose:
return help_text
help_text += "输入 '内容', 默认使用技术客服进行回答\n"
return help_text
def get_timestamp(self, n=13):
# 获取时间戳 返回13位或者10位时间戳
if n == 13:
return str(int(time.time()*1000))
else:
return str(int(time.time()))
def random_user_agent(self):
# 随机获取user-agent
U = UserAgent()
return U.random
def edit_config_json(self, key, value):
# 修改xinuo配置文件信息
curdir = os.path.dirname(__file__)
config_path = os.path.join(curdir, "config.json")
with open(config_path, 'r') as file:
data = json.load(file)
data[key] = value
with open(config_path, 'w') as file:
json.dump(data, file, indent=4)
logger.info(f"修改配置文件: key {key}, value: {value}")
def update_rooms(self, tag, group_name):
# tag = "更新群成员"
msg = f"{tag}: 服务器睡着了,请稍后再试"
try:
rooms = get_with_retry(wechatnt.get_rooms)
if rooms:
logger.info(f"{tag}: {rooms}")
for room in rooms:
if group_name == room.get("nickname"):
room_status = True
logger.info(f"找到对应的群聊: {room}")
wxid = room.get("wxid")
room_members = wechatnt.get_room_members(wxid)
if room_members:
logger.info(f"room_members: {room_members}")
room_members_total = room_members.get("total")
msg = f"群成员总数: {room_members_total}"
except Exception as e:
logger.error(f"{tag}: 服务器内部错误 {e}")
return msg
def get_kb_chat_id(self):
# chat_id
tag = "获取对话ID"
msg = f"{tag}: 服务器睡着了,请稍后再试"
chat_id = None
try:
url = f'{self.kb_base_url}/api/application/{self.kb_profile_id}/chat/open'
params = None
headers = {
'Accept': 'application/json',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Content-Type': 'application/json',
'AUTHORIZATION': self.kb_token
}
response = requests.request(
"GET", url, headers=headers, params=params,
timeout=30
)
print(f"{tag}: {response.text}")
r_code = response.status_code
if r_code == 200:
res_json = response.json()
if res_json.get("code") == 200:
chat_id = res_json.get("data")
else:
message = res_json.get("message")
log_msg = f"{tag}: response message:{message}"
logger.info(log_msg)
else:
r_code = response.status_code
log_msg = f"{tag}: response status_code:{r_code}"
logger.info(log_msg)
except Exception as e:
logger.error(f"{tag}: 服务器内部错误 {e}")
return msg, chat_id
def get_group_name_chat_id(self, group_name):
group_name_chat_id = self.kb_group_name_chat_ids.get(group_name)
if group_name_chat_id is None:
_, chat_id = self.get_kb_chat_id()
if chat_id:
self.kb_group_name_chat_ids[group_name] = chat_id
self.edit_config_json(
"kb_group_name_chat_ids",
self.kb_group_name_chat_ids
)
group_name_chat_id = chat_id
return group_name_chat_id
def fun_yunshangkeji_bot(self, gpt_text, group_name, tag):
# kb 知识库
msg = f"{tag}: 服务器睡着了,请稍后再试"
try:
kb_chat_id = self.get_group_name_chat_id(group_name)
url = f'{self.kb_base_url}/api/application/chat_message/{kb_chat_id}'
params = None
payload = json.dumps({
"message": gpt_text,
"re_chat": False,
"stream": False
})
headers = {
'Accept': 'application/json',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Content-Type': 'application/json',
'AUTHORIZATION': self.kb_token
}
response = requests.request(
"POST", url, headers=headers, params=params,
data=payload, timeout=30
)
r_code = response.status_code
if r_code == 200:
res_json = response.json()
if res_json.get("code") == 200:
msg = res_json.get("data").get("content")
else:
message = res_json.get("message")
log_msg = f"{tag}: response message:{message}"
logger.info(log_msg)
else:
r_code = response.status_code
log_msg = f"{tag}: response status_code:{r_code}"
logger.info(log_msg)
except Exception as e:
logger.error(f"{tag}: 服务器内部错误 {e}")
return msg
config.json
{
"kb_base_url": "http://192.168.20.59:8080",
"kb_token": "",
"kb_profile_id": "",
"kb_group_name_chat_ids": {}
}
cat xinuo_utils.py 10:40:59 ☁ master ☀
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: zhibo.wang
# E-mail: gm.zhibo.wang@gmail.com
# Date :
# Desc :
import pytz
from collections import defaultdict
import datetime
from config import global_config
from bridge.reply import Reply, ReplyType
from plugins.event import EventContext, EventAction
class Util:
@staticmethod
def is_admin(e_context: EventContext) -> bool:
"""
判断消息是否由管理员用户发送
:param e_context: 消息上下文
:return: True: 是, False: 否
"""
context = e_context["context"]
if context["isgroup"]:
actual_user_id = context.kwargs.get("msg").actual_user_id
for admin_user in global_config["admin_users"]:
if actual_user_id and actual_user_id in admin_user:
return True
return False
else:
return context["receiver"] in global_config["admin_users"]
@staticmethod
def set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR):
reply = Reply(level, content)
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
class DailyNumberLimiter:
tz = pytz.timezone('Asia/Shanghai')
def __init__(self, max_num):
self.today = -1
self.count = defaultdict(int)
self.max = max_num
def check(self, key) -> bool:
now = datetime.datetime.now(self.tz)
day = (now - datetime.timedelta(hours=5)).day
if day != self.today:
self.today = day
self.count.clear()
return bool(self.count[key] < self.max)
def get_num(self, key):
return self.count[key]
def increase(self, key, num=1):
self.count[key] += num
def reset(self, key):
self.count[key] = 0