feat: 初始化

This commit is contained in:
hackrobot
2024-03-04 18:39:54 +08:00
parent fffeba0b3d
commit e6a5cd45a2
28 changed files with 33048 additions and 62 deletions

22
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Bug 报告
about: 提交一份 bug 报告,帮助 WeChatRobot 变得更好
title: "[\U0001F41BBUG] 用一句话描述您的问题。"
labels: bug
assignees: ''
---
**描述这个 bug**
对 bug 作一个清晰简明的描述:
- 想做什么?
- 现在怎么做?
- 遇到什么问题?
**使用环境(请补全下列信息):**
- 操作系统:【如 Windows 7, Windows 10, Windows Server 2008 等】
- 操作系统版本【32 位或 64 位】
- Python 版本 【如 3.7.9 32 位3.8.15 64 位 等】
**屏幕截图**
添加屏幕截图以帮助解释您的问题。(可选)

View File

@@ -0,0 +1,20 @@
---
name: 请求添加新功能
about: 提出一个关于本项目新功能 / 新特性的建议
title: "[\U0001F4A1SUG] 一句话描述你希望新增的功能或特性"
labels: enhancement
assignees: ''
---
**你希望添加的功能是否与某个问题相关?**
关于这个问题的简洁清晰的描述,例如,当 [...] 时,我总是很沮丧。
**描述你希望的解决方案**
关于解决方案的简洁清晰的描述。
**描述你考虑的替代方案**
关于你考虑的,能实现这个功能的其他替代方案的简洁清晰的描述。
**其他**
你可以添加其他任何的资料、链接或者屏幕截图,以帮助我们理解这个新功能。

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.*
!.gitignore
!.github/
*pyc
__pycache__
logs/
*.log
*.log.*
config.yaml

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Changhua
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.

180
README.md
View File

@@ -1,92 +1,148 @@
# win-hook-bot # WeChatRobot
一个基于 [WeChatFerry](https://github.com/lich0821/WeChatFerry) 的微信机器人示例。
|[📖 文档](https://wechatferry.readthedocs.io/)|[📺 视频教程](https://mp.weixin.qq.com/s/APdjGyZ2hllXxyG_sNCfXQ)|[🙋 FAQ](https://mp.weixin.qq.com/s/bdPNrbJYoXhezCzHMqLoEw)|[🚨【微信机器人】沙雕行为合集](https://mp.weixin.qq.com/s/mc8O5iuhy46X4Bgqs80E8g)|
|:-:|:-:|:-:|:-:|
|![碲矿](https://s2.loli.net/2023/09/25/fub5VAPSa8srwyM.jpg)|![赞赏](https://s2.loli.net/2023/09/25/gkh9uWZVOxzNPAX.jpg)|
|:-:|:-:|
|后台回复 `WeChatFerry` 加群交流|如果你觉得有用|
## Getting started ## Quick Start
0. 遇到问题先看看上面的文档、教程和 FAQ。
- 按照步骤来,版本保持一致,少走弯路。
- 按照步骤来,版本保持一致,少走弯路。
- 按照步骤来,版本保持一致,少走弯路。
1. 安装 Python>=3.9Python 12 需要自己编译依赖,慎选),例如 [3.10.11](https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe)
2. 安装微信 `3.9.2.23`,下载地址在 [这里](https://github.com/lich0821/WeChatFerry/releases/latest);也可以从 [WeChatSetup](https://gitee.com/lch0821/WeChatSetup) 找到。
3. 克隆项目
```sh
git clone https://github.com/lich0821/WeChatRobot.git
To make it easy for you to get started with GitLab, here's a list of recommended next steps. # 如果网络原因打不开可以科学上网或者使用gitee
git clone https://gitee.com/lch0821/WeChatRobot.git
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://gitlab.yidooplanet.com/gitlab-instance-0a899031/win-hook-bot.git
git branch -M main
git push -uf origin main
``` ```
## Integrate with your tools 如果觉得克隆复杂,也可以直接下载 [最新版](https://github.com/lich0821/WeChatRobot/releases/latest) 到本地解压。
- [ ] [Set up project integrations](http://gitlab.yidooplanet.com/gitlab-instance-0a899031/win-hook-bot/-/settings/integrations) 4. 安装依赖
```sh
# 升级 pip
python -m pip install -U pip
# 安装必要依赖
pip install -r requirements.txt
# ChatGLM 还需要安装一个 kernel
ipython kernel install --name chatglm3 --user
```
## Collaborate with your team 5. 运行
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) 我们需要运行两次 `main.py` 第一次是生成配置文件 `config.yaml`, 第二次是真正跑你的机器人。
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) 直接运行程序会自动拉起微信,如果微信未打开,会自动打开微信;如果版本不对,也会有提示;其他报错,请进群交流。
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy 下面代码为第一次运行:第一次运行 `main.py` 会在 WeChatRobot 目录下生成一个 `config.yaml` 文件,参照修改配置进行修改。
Use the built-in continuous integration in GitLab. 其中 chatgpt、tigerbot、chatglm 和 xinghuo_web 是四种模型的配置信息,你需要配置它们的参数,不知道的可以加群交流。
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) ```sh
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) python main.py
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
*** # 需要停止按 Ctrl+C
```
# Editing this README 启动之后,可以正常接收消息但不会响应群消息。参考下方 [修改配置](#config) 进行配置,以便响应特定群聊。
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. 下面代码为第二次运行:你可以通过命令行参数选择模型,默认是不选择,这样你配置了什么参数就跑什么模型。正因如此你需要配置前面所说四种模型中的至少一种(当然也可以都配置,想跑那个模型就选什么参数), 然后就可以开始使用你的机器人了。
```sh
python main.py
## Suggestions for a good README # 需要停止按 Ctrl+C
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. ```
## Name 如果你配置了多个模型(不需要将其他配置注释或者移除),下面的内容才对你有帮助否则略过,通过 `python main.py -h` 通过参数可以选择要跑的模型。
Choose a self-explaining name for your project. ```sh
# 查看帮助
python main.py -h
#optional arguments:
# -h, --help show this help message and exit
# -c C, --chat_model C 选择要使用的AI模型默认不选择可选参数1. tigerbot 模型 2. chatgpt 模型 3. 讯飞星火模型 4. chatglm 模型
```
## Description ```sh
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. # 例: 我想运行选择chatgpt的机器人
python main.py -c 2
## Badges # 需要停止按 Ctrl+C
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. ```
## Visuals > python main.py -c C 其中参数 C 可选择如下所示
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. >> 1. tigerbot 模型
>> 2. chatgpt 模型
>> 3. 讯飞星火模型
>> 4. chatglm 模型
## Installation 6. 停止
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage 不要那么粗暴,温柔点儿;
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support 不要直接关闭窗口,温柔点儿。
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap 输入:`Ctrl+C`。否则,会出现各种奇怪问题。
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing ### <a name="config"></a>修改配置
State if you are open to contributions and what your requirements are for accepting them. *修改配置后,需要重新启动,以便让配置生效。*
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. 配置文件 `config.yaml` 是运行程序后自动从模板复制过来的,功能默认关闭。
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. #### 响应被 @ 消息
为了响应群聊消息,需要添加相应的 `roomId`
## Authors and acknowledgment 第一次运行的时候,可以在手机上往需要响应的群里发消息,打印的消息中方括号里的就是;多个群用 `,` 分隔。
Show your appreciation to those who have contributed to the project. ```yaml
groups:
enable: [] # 允许响应的群 roomId大概长这样2xxxxxxxxx3@chatroom, 多个群用 `,` 分隔
```
## License #### 配置 AI 模型
For open source projects, say how it is licensed. 为了使用 AI 模型,需要对相应模型并进行配置。
## Project status 使用 ChatGLM 见注意事项 [README.MD](base/chatglm/README.MD)
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
```yaml
chatgpt: # -----chatgpt配置这行不填-----
key: # 填写你 ChatGPT 的 key
api: https://api.openai.com/v1 # 如果你不知道这是干嘛的,就不要改
proxy: # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
prompt: 你是智能聊天机器人,你叫 wcferry # 根据需要对角色进行设定
chatglm: # -----chatglm配置这行不填-----
key: sk-012345678901234567890123456789012345678901234567 # 这个应该不用动
api: http://localhost:8000/v1 # 根据自己的chatglm地址修改
proxy: # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
prompt: 你是智能聊天机器人,你叫小薇 # 根据需要对角色进行设定
file_path: C:/Pictures/temp #设定生成图片和代码使用的文件夹路径
tigerbot: # -----tigerbot配置这行不填-----
key: # key
model: # tigerbot-7b-sft
# 抓取方式详见文档https://www.bilibili.com/read/cv27066577
xinghuo_web: # -----讯飞星火web模式api配置这行不填-----
cookie: # cookie
fd: # fd
GtToken: # GtToken
prompt: 你是智能聊天机器人,你叫 wcferry。请用这个角色回答我的问题 # 根据需要对角色进行设定
bard: # -----bard配置这行不填-----
api_key: # api-key 创建地址https://ai.google.dev/pricing创建后复制过来即可
model_name: gemini-pro # 新模型上线后可以选择模型
proxy: http://127.0.0.1:7890 # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
# 提示词尽可能用英文bard对中文提示词的效果不是很理想下方提示词为英语老师的示例请按实际需要修改,默认设置的提示词为谷歌创造的AI大语言模型
# I want you to act as a spoken English teacher and improver. I will speak to you in English and you will reply to me in English to practice my spoken English. I want you to keep your reply neat, limiting the reply to 100 words. I want you to strictly correct my grammar mistakes, typos, and factual errors. I want you to ask me a question in your reply. Now let's start practicing, you could ask me a question first. Remember, I want you to strictly correct my grammar mistakes, typos, and factual errors.
prompt: You am a large language model, trained by Google.
```
## HTTP
如需要使用 HTTP 接口,请参考 [wcfhttp](https://wechatferry.readthedocs.io/zh/latest/?badge=latest)。
[![PyPi](https://img.shields.io/pypi/v/wcfhttp.svg)](https://pypi.python.org/pypi/wcfhttp) [![Downloads](https://static.pepy.tech/badge/wcfhttp)](https://pypi.python.org/pypi/wcfhttp) [![Documentation Status](https://readthedocs.org/projects/wechatferry/badge/?version=latest)](https://wechatferry.readthedocs.io/zh/latest/?badge=latest)

45
base/chatglm/README.MD Normal file
View File

@@ -0,0 +1,45 @@
# ChatGLM3 集成使用说明
1. 需要取消配置中 chatglm 的注释, 并配置对应信息,使用 [ChatGLM3](https://github.com/THUDM/ChatGLM3), 启用最新版 ChatGLM3 根目录下 openai_api.py 获取 api 地址:
```yaml
# 如果要使用 chatglm取消下面的注释并填写相关内容
chatglm:
key: sk-012345678901234567890123456789012345678901234567 # 根据需要自己做key校验
api: http://localhost:8000/v1 # 根据自己的chatglm地址修改
proxy: # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
prompt: 你是智能聊天机器人,你叫小薇 # 根据需要对角色进行设定
file_path: F:/Pictures/temp #设定生成图片和代码使用的文件夹路径
```
2. 修改 chatglm/tool_registry.py 工具里面的一下配置comfyUI 地址或者根据需要自己配置一些工具,函数名上需要加 @register_tool, 函数里面需要叫'''函数描述''',参数需要用 Annotated[str,'',True] 修饰,分别是类型,参数说明,是否必填,再加 ->加上对应的返回类型
```python
@register_tool
def get_confyui_image(prompt: Annotated[str, '要生成图片的提示词,注意必须是英文', True]) -> dict:
'''
生成图片
'''
with open("func_chatglm\\base.json", "r", encoding="utf-8") as f:
data2 = json.load(f)
data2['prompt']['3']['inputs']['seed'] = ''.join(
random.sample('123456789012345678901234567890', 14))
# 模型名称
data2['prompt']['4']['inputs']['ckpt_name'] = 'chilloutmix_NiPrunedFp32Fix.safetensors'
data2['prompt']['6']['inputs']['text'] = prompt # 正向提示词
# data2['prompt']['7']['inputs']['text']='' #反向提示词
cfui = ComfyUIApi(server_address="127.0.0.1:8188") # 根据自己comfyUI地址修改
images = cfui.get_images(data2['prompt'])
return {'res': images[0]['image'], 'res_type': 'image', 'filename': images[0]['filename']}
```
3. 使用 Code Interpreter 还需要安装 Jupyter 内核,默认名称叫 chatglm3
```
ipython kernel install --name chatglm3 --user
```
如果名称需要自定义可以配置系统环境变量IPYKERNEL 或者修改 chatglm/code_kernel.py
```
IPYKERNEL = os.environ.get('IPYKERNEL', 'chatglm3')
```
4. 启动后,发送 #帮助 可以查看 模式和常用指令

13
base/chatglm/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
class UnsupportedPythonVersionError(Exception):
def __init__(self, error_msg: str):
super().__init__(error_msg)
python_version_info = sys.version_info
if not sys.version_info >= (3, 9):
msg = "当前Python版本: " + ".".join(map(str, python_version_info[:3])) + (', 需要python版本 >= 3.9, 前往下载: '
'https://www.python.org/downloads/')
raise UnsupportedPythonVersionError(msg)

88
base/chatglm/base.json Normal file
View File

@@ -0,0 +1,88 @@
{
"prompt": {
"3": {
"inputs": {
"seed": 1000573256060686,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler"
},
"4": {
"inputs": {
"ckpt_name": "(修复)512-inpainting-ema.safetensors"
},
"class_type": "CheckpointLoaderSimple"
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage"
},
"6": {
"inputs": {
"text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,dress, ",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode"
},
"7": {
"inputs": {
"text": "text, watermark",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode"
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode"
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage"
}
}
}

199
base/chatglm/code_kernel.py Normal file
View File

@@ -0,0 +1,199 @@
import base64
import os
import queue
import re
from io import BytesIO
from subprocess import PIPE
from typing import Optional, Union
import jupyter_client
from PIL import Image
IPYKERNEL = os.environ.get('IPYKERNEL', 'chatglm3')
class CodeKernel(object):
def __init__(self,
kernel_name='kernel',
kernel_id=None,
kernel_config_path="",
python_path=None,
ipython_path=None,
init_file_path="./startup.py",
verbose=1):
self.kernel_name = kernel_name
self.kernel_id = kernel_id
self.kernel_config_path = kernel_config_path
self.python_path = python_path
self.ipython_path = ipython_path
self.init_file_path = init_file_path
self.verbose = verbose
if python_path is None and ipython_path is None:
env = None
else:
env = {"PATH": self.python_path + ":$PATH",
"PYTHONPATH": self.python_path}
# Initialize the backend kernel
self.kernel_manager = jupyter_client.KernelManager(kernel_name=IPYKERNEL,
connection_file=self.kernel_config_path,
exec_files=[
self.init_file_path],
env=env)
if self.kernel_config_path:
self.kernel_manager.load_connection_file()
self.kernel_manager.start_kernel(stdout=PIPE, stderr=PIPE)
print("Backend kernel started with the configuration: {}".format(
self.kernel_config_path))
else:
self.kernel_manager.start_kernel(stdout=PIPE, stderr=PIPE)
print("Backend kernel started with the configuration: {}".format(
self.kernel_manager.connection_file))
if verbose:
print(self.kernel_manager.get_connection_info())
# Initialize the code kernel
self.kernel = self.kernel_manager.blocking_client()
# self.kernel.load_connection_file()
self.kernel.start_channels()
print("Code kernel started.")
def execute(self, code):
self.kernel.execute(code)
try:
shell_msg = self.kernel.get_shell_msg(timeout=40)
io_msg_content = self.kernel.get_iopub_msg(timeout=40)['content']
while True:
msg_out = io_msg_content
# Poll the message
try:
io_msg_content = self.kernel.get_iopub_msg(timeout=40)[
'content']
if 'execution_state' in io_msg_content and io_msg_content['execution_state'] == 'idle':
break
except queue.Empty:
break
return shell_msg, msg_out
except Exception as e:
print(e)
return None
def execute_interactive(self, code, verbose=False):
shell_msg = self.kernel.execute_interactive(code)
if shell_msg is queue.Empty:
if verbose:
print("Timeout waiting for shell message.")
self.check_msg(shell_msg, verbose=verbose)
return shell_msg
def inspect(self, code, verbose=False):
msg_id = self.kernel.inspect(code)
shell_msg = self.kernel.get_shell_msg(timeout=30)
if shell_msg is queue.Empty:
if verbose:
print("Timeout waiting for shell message.")
self.check_msg(shell_msg, verbose=verbose)
return shell_msg
def get_error_msg(self, msg, verbose=False) -> Optional[str]:
if msg['content']['status'] == 'error':
try:
error_msg = msg['content']['traceback']
except BaseException:
try:
error_msg = msg['content']['traceback'][-1].strip()
except BaseException:
error_msg = "Traceback Error"
if verbose:
print("Error: ", error_msg)
return error_msg
return None
def check_msg(self, msg, verbose=False):
status = msg['content']['status']
if status == 'ok':
if verbose:
print("Execution succeeded.")
elif status == 'error':
for line in msg['content']['traceback']:
if verbose:
print(line)
def shutdown(self):
# Shutdown the backend kernel
self.kernel_manager.shutdown_kernel()
print("Backend kernel shutdown.")
# Shutdown the code kernel
self.kernel.shutdown()
print("Code kernel shutdown.")
def restart(self):
# Restart the backend kernel
self.kernel_manager.restart_kernel()
# print("Backend kernel restarted.")
def interrupt(self):
# Interrupt the backend kernel
self.kernel_manager.interrupt_kernel()
# print("Backend kernel interrupted.")
def is_alive(self):
return self.kernel.is_alive()
def b64_2_img(data):
buff = BytesIO(base64.b64decode(data))
return Image.open(buff)
def clean_ansi_codes(input_string):
ansi_escape = re.compile(r'(\x9B|\x1B\[|\u001b\[)[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', input_string)
def execute(code, kernel: CodeKernel) -> tuple[str, Union[str, Image.Image]]:
res = ""
res_type = None
code = code.replace("<|observation|>", "")
code = code.replace("<|assistant|>interpreter", "")
code = code.replace("<|assistant|>", "")
code = code.replace("<|user|>", "")
code = code.replace("<|system|>", "")
msg, output = kernel.execute(code)
if msg['metadata']['status'] == "timeout":
return res_type, 'Timed out'
elif msg['metadata']['status'] == 'error':
return res_type, clean_ansi_codes('\n'.join(kernel.get_error_msg(msg, verbose=True)))
if 'text' in output:
res_type = "text"
res = output['text']
elif 'data' in output:
for key in output['data']:
if 'image/png' in key:
res_type = "image"
res = output['data'][key]
break
elif 'text/plain' in key:
res_type = "text"
res = output['data'][key]
if res_type == "image":
return res_type, b64_2_img(res)
elif res_type == "text" or res_type == "traceback":
res = res
return res_type, res
def extract_code(text: str) -> str:
pattern = r'```([^\n]*)\n(.*?)```'
matches = re.findall(pattern, text, re.DOTALL)
return matches[-1][1]

186
base/chatglm/comfyUI_api.py Normal file
View File

@@ -0,0 +1,186 @@
# This is an example that uses the websockets api to know when a prompt execution is done
# Once the prompt execution is done it downloads the images using the /history endpoint
import io
import json
import random
import urllib
import uuid
import requests
# NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
import websocket
from PIL import Image
class ComfyUIApi():
def __init__(self, server_address="127.0.0.1:8188"):
self.server_address = server_address
self.client_id = str(uuid.uuid4())
self.ws = websocket.WebSocket()
self.ws.connect(
"ws://{}/ws?clientId={}".format(server_address, self.client_id))
def queue_prompt(self, prompt):
p = {"prompt": prompt, "client_id": self.client_id}
data = json.dumps(p).encode('utf-8')
req = requests.post(
"http://{}/prompt".format(self.server_address), data=data)
print(req.text)
return json.loads(req.text)
def get_image(self, filename, subfolder, folder_type):
data = {"filename": filename,
"subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data)
with requests.get("http://{}/view?{}".format(self.server_address, url_values)) as response:
image = Image.open(io.BytesIO(response.content))
return image
def get_image_url(self, filename, subfolder, folder_type):
data = {"filename": filename,
"subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data)
return "http://{}/view?{}".format(self.server_address, url_values)
def get_history(self, prompt_id):
with requests.get("http://{}/history/{}".format(self.server_address, prompt_id)) as response:
return json.loads(response.text)
def get_images(self, prompt, isUrl=False):
prompt_id = self.queue_prompt(prompt)['prompt_id']
output_images = []
while True:
out = self.ws.recv()
if isinstance(out, str):
message = json.loads(out)
if message['type'] == 'executing':
data = message['data']
if data['node'] is None and data['prompt_id'] == prompt_id:
break # Execution is done
else:
continue # previews are binary data
history = self.get_history(prompt_id)[prompt_id]
for o in history['outputs']:
for node_id in history['outputs']:
node_output = history['outputs'][node_id]
if 'images' in node_output:
for image in node_output['images']:
image_data = self.get_image_url(image['filename'], image['subfolder'], image['type']) if isUrl else self.get_image(
image['filename'], image['subfolder'], image['type'])
image['image'] = image_data
output_images.append(image)
return output_images
prompt_text = """
{
"3": {
"class_type": "KSampler",
"inputs": {
"cfg": 8,
"denoise": 1,
"latent_image": [
"5",
0
],
"model": [
"4",
0
],
"negative": [
"7",
0
],
"positive": [
"6",
0
],
"sampler_name": "euler",
"scheduler": "normal",
"seed": 8566257,
"steps": 20
}
},
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "chilloutmix_NiPrunedFp32Fix.safetensors"
}
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {
"batch_size": 1,
"height": 512,
"width": 512
}
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [
"4",
1
],
"text": "masterpiece best quality girl"
}
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [
"4",
1
],
"text": "bad hands"
}
},
"8": {
"class_type": "VAEDecode",
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
}
},
"9": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
}
}
}
"""
if __name__ == '__main__':
prompt = json.loads(prompt_text)
# set the text prompt for our positive CLIPTextEncode
prompt["6"]["inputs"]["text"] = "masterpiece best quality man"
# set the seed for our KSampler node
prompt["3"]["inputs"]["seed"] = ''.join(
random.sample('123456789012345678901234567890', 14))
cfui = ComfyUIApi()
images = cfui.get_images(prompt)
# Commented out code to display the output images:
for node_id in images:
for image_data in images[node_id]:
import io
from PIL import Image
image = Image.open(io.BytesIO(image_data))
image.show()

View File

@@ -0,0 +1,167 @@
import inspect
import json
import random
import re
import traceback
from copy import deepcopy
from datetime import datetime
from types import GenericAlias
from typing import Annotated, get_origin
from base.chatglm.comfyUI_api import ComfyUIApi
from base.func_news import News
from zhdate import ZhDate
_TOOL_HOOKS = {}
_TOOL_DESCRIPTIONS = {}
def extract_code(text: str) -> str:
pattern = r'```([^\n]*)\n(.*?)```'
matches = re.findall(pattern, text, re.DOTALL)
return matches[-1][1]
def register_tool(func: callable):
tool_name = func.__name__
tool_description = inspect.getdoc(func).strip()
python_params = inspect.signature(func).parameters
tool_params = []
for name, param in python_params.items():
annotation = param.annotation
if annotation is inspect.Parameter.empty:
raise TypeError(f"Parameter `{name}` missing type annotation")
if get_origin(annotation) != Annotated:
raise TypeError(
f"Annotation type for `{name}` must be typing.Annotated")
typ, (description, required) = annotation.__origin__, annotation.__metadata__
typ: str = str(typ) if isinstance(typ, GenericAlias) else typ.__name__
if not isinstance(description, str):
raise TypeError(f"Description for `{name}` must be a string")
if not isinstance(required, bool):
raise TypeError(f"Required for `{name}` must be a bool")
tool_params.append({
"name": name,
"description": description,
"type": typ,
"required": required
})
tool_def = {
"name": tool_name,
"description": tool_description,
"params": tool_params
}
# print("[registered tool] " + pformat(tool_def))
_TOOL_HOOKS[tool_name] = func
_TOOL_DESCRIPTIONS[tool_name] = tool_def
return func
def dispatch_tool(tool_name: str, tool_params: dict) -> str:
if tool_name not in _TOOL_HOOKS:
return f"Tool `{tool_name}` not found. Please use a provided tool."
tool_call = _TOOL_HOOKS[tool_name]
try:
ret = tool_call(**tool_params)
except BaseException:
ret = traceback.format_exc()
return ret
def get_tools() -> dict:
return deepcopy(_TOOL_DESCRIPTIONS)
# Tool Definitions
# @register_tool
# def random_number_generator(
# seed: Annotated[int, 'The random seed used by the generator', True],
# range: Annotated[tuple[int, int], 'The range of the generated numbers', True],
# ) -> int:
# """
# Generates a random number x, s.t. range[0] <= x < range[1]
# """
# if not isinstance(seed, int):
# raise TypeError("Seed must be an integer")
# if not isinstance(range, tuple):
# raise TypeError("Range must be a tuple")
# if not isinstance(range[0], int) or not isinstance(range[1], int):
# raise TypeError("Range must be a tuple of integers")
# import random
# return random.Random(seed).randint(*range)
@register_tool
def get_weather(
city_name: Annotated[str, 'The name of the city to be queried', True],
) -> str:
"""
Get the current weather for `city_name`
"""
if not isinstance(city_name, str):
raise TypeError("City name must be a string")
key_selection = {
"current_condition": ["temp_C", "FeelsLikeC", "humidity", "weatherDesc", "observation_time"],
}
import requests
try:
resp = requests.get(f"https://wttr.in/{city_name}?format=j1")
resp.raise_for_status()
resp = resp.json()
ret = {k: {_v: resp[k][0][_v] for _v in v}
for k, v in key_selection.items()}
except BaseException:
import traceback
ret = "Error encountered while fetching weather data!\n" + traceback.format_exc()
return str(ret)
@register_tool
def get_confyui_image(prompt: Annotated[str, '要生成图片的提示词,注意必须是英文', True]) -> dict:
'''
生成图片
'''
with open("chatglm\\base.json", "r", encoding="utf-8") as f:
data2 = json.load(f)
data2['prompt']['3']['inputs']['seed'] = ''.join(
random.sample('123456789012345678901234567890', 14))
# 模型名称
data2['prompt']['4']['inputs']['ckpt_name'] = 'chilloutmix_NiPrunedFp32Fix.safetensors'
data2['prompt']['6']['inputs']['text'] = prompt # 正向提示词
# data2['prompt']['7']['inputs']['text']='' #反向提示词
cfui = ComfyUIApi(server_address="127.0.0.1:8188") # 根据自己comfyUI地址修改
images = cfui.get_images(data2['prompt'])
return {'res': images[0]['image'], 'res_type': 'image', 'filename': images[0]['filename']}
@register_tool
def get_news() -> str:
'''
获取最新新闻
'''
news = News()
return news.get_important_news()
@register_tool
def get_time() -> str:
'''
获取当前日期,时间,农历日期,星期几
'''
time = datetime.now()
date2 = ZhDate.from_datetime(time)
week_list = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
return '{} {} {}'.format(time.strftime("%Y年%m月%d%H:%M:%S"), week_list[time.weekday()], '农历:' + date2.chinese())
if __name__ == "__main__":
print(dispatch_tool("get_weather", {"city_name": "beijing"}))
print(get_tools())

30903
base/chengyu.csv Normal file

File diff suppressed because it is too large Load Diff

44
base/func_bard.py Normal file
View File

@@ -0,0 +1,44 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import google.generativeai as genai
class BardAssistant:
def __init__(self, conf: dict) -> None:
self._api_key = conf["api_key"]
self._model_name = conf["model_name"]
self._prompt = conf['prompt']
self._proxy = conf['proxy']
genai.configure(api_key=self._api_key)
self._bard = genai.GenerativeModel(self._model_name)
def __repr__(self):
return 'BardAssistant'
@staticmethod
def value_check(conf: dict) -> bool:
if conf:
if conf.get("api_key") and conf.get("model_name") and conf.get("prompt"):
return True
return False
def get_answer(self, msg: str, sender: str = None) -> str:
response = self._bard.generate_content([{'role': 'user', 'parts': [msg]}])
return response.text
if __name__ == "__main__":
from configuration import Config
config = Config().BardAssistant
if not config:
exit(0)
bard_assistant = BardAssistant(config)
if bard_assistant._proxy:
os.environ['HTTP_PROXY'] = bard_assistant._proxy
os.environ['HTTPS_PROXY'] = bard_assistant._proxy
rsp = bard_assistant.get_answer(bard_assistant._prompt)
print(rsp)

190
base/func_chatglm.py Normal file
View File

@@ -0,0 +1,190 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import random
from datetime import datetime
from typing import Optional
import openai
from base.chatglm.code_kernel import CodeKernel, execute
from base.chatglm.tool_registry import dispatch_tool, extract_code, get_tools
from wcferry import Wcf
functions = get_tools()
class ChatGLM:
def __init__(self, config={}, wcf: Optional[Wcf] = None, max_retry=5) -> None:
openai.api_key = config.get("key", "empty")
# 自己搭建或第三方代理的接口
openai.api_base = config["api"]
proxy = config.get("proxy")
if proxy:
openai.proxy = {"http": proxy, "https": proxy}
self.conversation_list = {}
self.chat_type = {}
self.max_retry = max_retry
self.wcf = wcf
self.filePath = config["file_path"]
self.kernel = CodeKernel()
self.system_content_msg = {"chat": [{"role": "system", "content": config["prompt"]}],
"tool": [{"role": "system", "content": "Answer the following questions as best as you can. You have access to the following tools:"}],
"code": [{"role": "system", "content": "你是一位智能AI助手你叫ChatGLM你连接着一台电脑但请注意不能联网。在使用Python解决任务时你可以运行代码并得到结果如果运行结果有错误你需要尽可能对代码进行改进。你可以处理用户上传到电脑上的文件文件默认存储路径是{}".format(self.filePath)}]}
def __repr__(self):
return 'ChatGLM'
@staticmethod
def value_check(conf: dict) -> bool:
if conf:
if conf.get("api") and conf.get("prompt") and conf.get("file_path"):
return True
return False
def get_answer(self, question: str, wxid: str) -> str:
# wxid或者roomid,个人时为微信id群消息时为群id
if '#帮助' == question:
return '本助手有三种模式,#聊天模式 = #1 #工具模式 = #2 #代码模式 = #3 , #清除模式会话 = #4 , #清除全部会话 = #5 可用发送#对应模式 或者 #编号 进行切换'
elif '#聊天模式' == question or '#1' == question:
self.chat_type[wxid] = 'chat'
return '已切换#聊天模式'
elif '#工具模式' == question or '#2' == question:
self.chat_type[wxid] = 'tool'
return '已切换#工具模式 \n工具有:查看天气,日期,新闻,comfyUI文生图。例如\n帮我生成一张小鸟的图片,提示词必须是英文'
elif '#代码模式' == question or '#3' == question:
self.chat_type[wxid] = 'code'
return '已切换#代码模式 \n代码模式可以用于写python代码例如\n用python画一个爱心'
elif '#清除模式会话' == question or '#4' == question:
self.conversation_list[wxid][self.chat_type[wxid]
] = self.system_content_msg[self.chat_type[wxid]]
return '已清除'
elif '#清除全部会话' == question or '#5' == question:
self.conversation_list[wxid] = self.system_content_msg
return '已清除'
self.updateMessage(wxid, question, "user")
try:
params = dict(model="chatglm3", temperature=1.0,
messages=self.conversation_list[wxid][self.chat_type[wxid]], stream=False)
if 'tool' == self.chat_type[wxid]:
params["functions"] = functions
response = openai.ChatCompletion.create(**params)
for _ in range(self.max_retry):
if response.choices[0].message.get("function_call"):
function_call = response.choices[0].message.function_call
print(
f"Function Call Response: {function_call.to_dict_recursive()}")
function_args = json.loads(function_call.arguments)
observation = dispatch_tool(
function_call.name, function_args)
if isinstance(observation, dict):
res_type = observation['res_type'] if 'res_type' in observation else 'text'
res = observation['res'] if 'res_type' in observation else str(
observation)
if res_type == 'image':
filename = observation['filename']
filePath = os.path.join(self.filePath, filename)
res.save(filePath)
self.wcf and self.wcf.send_image(filePath, wxid)
tool_response = '[Image]' if res_type == 'image' else res
else:
tool_response = observation if isinstance(
observation, str) else str(observation)
print(f"Tool Call Response: {tool_response}")
params["messages"].append(response.choices[0].message)
params["messages"].append(
{
"role": "function",
"name": function_call.name,
"content": tool_response, # 调用函数返回结果
}
)
self.updateMessage(wxid, tool_response, "function")
response = openai.ChatCompletion.create(**params)
elif response.choices[0].message.content.find('interpreter') != -1:
output_text = response.choices[0].message.content
code = extract_code(output_text)
self.wcf and self.wcf.send_text('代码如下:\n' + code, wxid)
self.wcf and self.wcf.send_text('执行代码...', wxid)
try:
res_type, res = execute(code, self.kernel)
except Exception as e:
rsp = f'代码执行错误: {e}'
break
if res_type == 'image':
filename = '{}.png'.format(''.join(random.sample(
'abcdefghijklmnopqrstuvwxyz1234567890', 8)))
filePath = os.path.join(self.filePath, filename)
res.save(filePath)
self.wcf and self.wcf.send_image(filePath, wxid)
else:
self.wcf and self.wcf.send_text("执行结果:\n" + res, wxid)
tool_response = '[Image]' if res_type == 'image' else res
print("Received:", res_type, res)
params["messages"].append(response.choices[0].message)
params["messages"].append(
{
"role": "function",
"name": "interpreter",
"content": tool_response, # 调用函数返回结果
}
)
self.updateMessage(wxid, tool_response, "function")
response = openai.ChatCompletion.create(**params)
else:
rsp = response.choices[0].message.content
break
self.updateMessage(wxid, rsp, "assistant")
except Exception as e0:
rsp = "发生未知错误:" + str(e0)
return rsp
def updateMessage(self, wxid: str, question: str, role: str) -> None:
now_time = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 初始化聊天记录,组装系统信息
if wxid not in self.conversation_list.keys():
self.conversation_list[wxid] = self.system_content_msg
if wxid not in self.chat_type.keys():
self.chat_type[wxid] = 'chat'
# 当前问题
content_question_ = {"role": role, "content": question}
self.conversation_list[wxid][self.chat_type[wxid]].append(
content_question_)
# 只存储10条记录超过滚动清除
i = len(self.conversation_list[wxid][self.chat_type[wxid]])
if i > 10:
print("滚动清除微信记录:" + wxid)
# 删除多余的记录,倒着删,且跳过第一个的系统消息
del self.conversation_list[wxid][self.chat_type[wxid]][1]
if __name__ == "__main__":
from configuration import Config
config = Config().CHATGLM
if not config:
exit(0)
chat = ChatGLM(config)
while True:
q = input(">>> ")
try:
time_start = datetime.now() # 记录开始时间
print(chat.get_answer(q, "wxid"))
time_end = datetime.now() # 记录结束时间
# 计算的时间差为程序的执行时间,单位为秒/s
print(f"{round((time_end - time_start).total_seconds(), 2)}s")
except Exception as e:
print(e)

106
base/func_chatgpt.py Normal file
View File

@@ -0,0 +1,106 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from datetime import datetime
import httpx
from openai import APIConnectionError, APIError, AuthenticationError, OpenAI
class ChatGPT():
def __init__(self, conf: dict) -> None:
key = conf.get("key")
api = conf.get("api")
proxy = conf.get("proxy")
prompt = conf.get("prompt")
self.model = conf.get("model", "gpt-3.5-turbo")
self.LOG = logging.getLogger("ChatGPT")
if proxy:
self.client = OpenAI(api_key=key, base_url=api, http_client=httpx.Client(proxy=proxy))
else:
self.client = OpenAI(api_key=key, base_url=api)
self.conversation_list = {}
self.system_content_msg = {"role": "system", "content": prompt}
def __repr__(self):
return 'ChatGPT'
@staticmethod
def value_check(conf: dict) -> bool:
if conf:
if conf.get("key") and conf.get("api") and conf.get("prompt"):
return True
return False
def get_answer(self, question: str, wxid: str) -> str:
# wxid或者roomid,个人时为微信id群消息时为群id
self.updateMessage(wxid, question, "user")
rsp = ""
try:
ret = self.client.chat.completions.create(model=self.model,
messages=self.conversation_list[wxid],
temperature=0.2)
rsp = ret.choices[0].message.content
rsp = rsp[2:] if rsp.startswith("\n\n") else rsp
rsp = rsp.replace("\n\n", "\n")
self.updateMessage(wxid, rsp, "assistant")
except AuthenticationError:
self.LOG.error("OpenAI API 认证失败,请检查 API 密钥是否正确")
except APIConnectionError:
self.LOG.error("无法连接到 OpenAI API请检查网络连接")
except APIError as e1:
self.LOG.error(f"OpenAI API 返回了错误:{str(e1)}")
except Exception as e0:
self.LOG.error(f"发生未知错误:{str(e0)}")
return rsp
def updateMessage(self, wxid: str, question: str, role: str) -> None:
now_time = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
time_mk = "当需要回答时间时请直接参考回复:"
# 初始化聊天记录,组装系统信息
if wxid not in self.conversation_list.keys():
question_ = [
self.system_content_msg,
{"role": "system", "content": "" + time_mk + now_time}
]
self.conversation_list[wxid] = question_
# 当前问题
content_question_ = {"role": role, "content": question}
self.conversation_list[wxid].append(content_question_)
for cont in self.conversation_list[wxid]:
if cont["role"] != "system":
continue
if cont["content"].startswith(time_mk):
cont["content"] = time_mk + now_time
# 只存储10条记录超过滚动清除
i = len(self.conversation_list[wxid])
if i > 10:
print("滚动清除微信记录:" + wxid)
# 删除多余的记录,倒着删,且跳过第一个的系统消息
del self.conversation_list[wxid][1]
if __name__ == "__main__":
from configuration import Config
config = Config().CHATGPT
if not config:
exit(0)
chat = ChatGPT(config)
while True:
q = input(">>> ")
try:
time_start = datetime.now() # 记录开始时间
print(chat.get_answer(q, "wxid"))
time_end = datetime.now() # 记录结束时间
print(f"{round((time_end - time_start).total_seconds(), 2)}s") # 计算的时间差为程序的执行时间,单位为秒/s
except Exception as e:
print(e)

79
base/func_chengyu.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
import os
import random
import pandas as pd
class Chengyu(object):
def __init__(self) -> None:
root = os.path.dirname(os.path.abspath(__file__))
self.df = pd.read_csv(f"{root}/chengyu.csv", delimiter="\t")
self.cys, self.zis, self.yins = self._build_data()
def _build_data(self):
df = self.df.copy()
df["shouzi"] = df["chengyu"].apply(lambda x: x[0])
df["mozi"] = df["chengyu"].apply(lambda x: x[-1])
df["shouyin"] = df["pingyin"].apply(lambda x: x.split(" ")[0])
df["moyin"] = df["pingyin"].apply(lambda x: x.split(" ")[-1])
cys = dict(zip(df["chengyu"], df["moyin"]))
zis = df.groupby("shouzi").agg({"chengyu": set})["chengyu"].to_dict()
yins = df.groupby("shouyin").agg({"chengyu": set})["chengyu"].to_dict()
return cys, zis, yins
def isChengyu(self, cy: str) -> bool:
return self.cys.get(cy, None) is not None
def getNext(self, cy: str, tongyin: bool = True) -> str:
"""获取下一个成语
cy: 当前成语
tongyin: 是否允许同音字
"""
zi = cy[-1]
ansers = list(self.zis.get(zi, {}))
try:
ansers.remove(cy) # 移除当前成语
except Exception as e:
pass # Just ignore...
if ansers:
return random.choice(ansers)
# 如果找不到同字,允许同音
if tongyin:
yin = self.cys.get(cy)
ansers = list(self.yins.get(yin, {}))
try:
ansers.remove(cy) # 移除当前成语
except Exception as e:
pass # Just ignore...
if ansers:
return random.choice(ansers)
return None
def getMeaning(self, cy: str) -> str:
ress = self.df[self.df["chengyu"] == cy].to_dict(orient="records")
if ress:
res = ress[0]
rsp = res["chengyu"] + "\n" + res["pingyin"] + "\n" + res["jieshi"]
if res["chuchu"] and res["chuchu"] != "":
rsp += "\n出处:" + res["chuchu"]
if res["lizi"] and res["lizi"] != "":
rsp += "\n例子:" + res["lizi"]
return rsp
return None
cy = Chengyu()
if __name__ == "__main__":
answer = cy.getNext("便宜行事")
print(answer)

51
base/func_news.py Normal file
View File

@@ -0,0 +1,51 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import re
import logging
import time
from datetime import datetime
import requests
from lxml import etree
class News(object):
def __init__(self) -> None:
self.LOG = logging.getLogger(__name__)
self.week = {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0"}
def get_important_news(self):
url = "https://www.cls.cn/api/sw?app=CailianpressWeb&os=web&sv=7.7.5"
data = {"type": "telegram", "keyword": "你需要知道的隔夜全球要闻", "page": 0,
"rn": 1, "os": "web", "sv": "7.7.5", "app": "CailianpressWeb"}
try:
rsp = requests.post(url=url, headers=self.headers, data=data)
data = json.loads(rsp.text)["data"]["telegram"]["data"][0]
news = data["descr"]
timestamp = data["time"]
ts = time.localtime(timestamp)
weekday_news = datetime(*ts[:6]).weekday()
except Exception as e:
self.LOG.error(e)
return ""
weekday_now = datetime.now().weekday()
if weekday_news != weekday_now:
return "" # 旧闻观察发现周二周六早晨6点半左右发布
fmt_time = time.strftime("%Y年%m月%d", ts)
news = re.sub(r"(\d{1,2}、)", r"\n\1", news)
fmt_news = "".join(etree.HTML(news).xpath(" // text()"))
fmt_news = re.sub(r"周[一|二|三|四|五|六|日]你需要知道的", r"", fmt_news)
return f"{fmt_time} {self.week[weekday_news]}\n{fmt_news}"
if __name__ == "__main__":
news = News()
print(news.get_important_news())

View File

@@ -0,0 +1,61 @@
import calendar
import datetime
from chinese_calendar import is_workday
from robot import Robot
class ReportReminder:
@staticmethod
def remind(robot: Robot) -> None:
receivers = robot.config.REPORT_REMINDERS
if not receivers:
receivers = ["filehelper"]
# 日报周报月报提醒
for receiver in receivers:
today = datetime.datetime.now().date()
# 如果是非工作日
if not is_workday(today):
robot.sendTextMsg("休息日快乐", receiver)
# 如果是工作日
if is_workday(today):
robot.sendTextMsg("该发日报啦", receiver)
# 如果是本周最后一个工作日
if ReportReminder.last_work_day_of_week(today) == today:
robot.sendTextMsg("该发周报啦", receiver)
# 如果本日是本月最后一整周的最后一个工作日:
if ReportReminder.last_work_friday_of_month(today) == today:
robot.sendTextMsg("该发月报啦", receiver)
# 计算本月最后一个周的最后一个工作日
@staticmethod
def last_work_friday_of_month(d: datetime.date) -> datetime.date:
days_in_month = calendar.monthrange(d.year, d.month)[1]
weekday = calendar.weekday(d.year, d.month, days_in_month)
if weekday == 4:
last_friday_of_month = datetime.date(
d.year, d.month, days_in_month)
else:
if weekday >= 5:
last_friday_of_month = datetime.date(d.year, d.month, days_in_month) - \
datetime.timedelta(days=(weekday - 4))
else:
last_friday_of_month = datetime.date(d.year, d.month, days_in_month) - \
datetime.timedelta(days=(weekday + 3))
while not is_workday(last_friday_of_month):
last_friday_of_month = last_friday_of_month - datetime.timedelta(days=1)
return last_friday_of_month
# 计算本周最后一个工作日
@staticmethod
def last_work_day_of_week(d: datetime.date) -> datetime.date:
weekday = calendar.weekday(d.year, d.month, d.day)
last_work_day_of_week = datetime.date(
d.year, d.month, d.day) + datetime.timedelta(days=(6 - weekday))
while not is_workday(last_work_day_of_week):
last_work_day_of_week = last_work_day_of_week - \
datetime.timedelta(days=1)
return last_work_day_of_week

49
base/func_tigerbot.py Normal file
View File

@@ -0,0 +1,49 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import requests
from random import randint
class TigerBot:
def __init__(self, tbconf=None) -> None:
self.LOG = logging.getLogger(__file__)
self.tburl = "https://api.tigerbot.com/bot-service/ai_service/gpt"
self.tbheaders = {"Authorization": "Bearer " + tbconf["key"]}
self.tbmodel = tbconf["model"]
self.fallback = ["", "快滚", "赶紧滚"]
def __repr__(self):
return 'TigerBot'
@staticmethod
def value_check(conf: dict) -> bool:
if conf:
return all(conf.values())
return False
def get_answer(self, msg: str, sender: str = None) -> str:
payload = {
"text": msg,
"modelVersion": self.tbmodel
}
rsp = ""
try:
rsp = requests.post(self.tburl, headers=self.tbheaders, json=payload).json()
rsp = rsp["data"]["result"][0]
except Exception as e:
self.LOG.error(f"{e}: {payload}\n{rsp}")
idx = randint(0, len(self.fallback) - 1)
rsp = self.fallback[idx]
return rsp
if __name__ == "__main__":
from configuration import Config
c = Config()
tbot = TigerBot(c.TIGERBOT)
rsp = tbot.get_answer("你还活着?")
print(rsp)

38
base/func_xinghuo_web.py Normal file
View File

@@ -0,0 +1,38 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
from sparkdesk_web.core import SparkWeb
class XinghuoWeb:
def __init__(self, xhconf=None) -> None:
self._sparkWeb = SparkWeb(
cookie=xhconf["cookie"],
fd=xhconf["fd"],
GtToken=xhconf["GtToken"],
)
self._chat = self._sparkWeb.create_continuous_chat()
# 如果有提示词
if xhconf["prompt"]:
self._chat.chat(xhconf["prompt"])
def __repr__(self):
return 'XinghuoWeb'
@staticmethod
def value_check(conf: dict) -> bool:
if conf:
return all(conf.values())
return False
def get_answer(self, msg: str, sender: str = None) -> str:
answer = self._chat.chat(msg)
return answer
if __name__ == "__main__":
from configuration import Config
c = Config()
xinghuo = XinghuoWeb(c.XINGHUO_WEB)
rsp = xinghuo.get_answer("你还活着?")
print(rsp)

46
base/func_zhipu.py Normal file
View File

@@ -0,0 +1,46 @@
from zhipuai import ZhipuAI
class ZhiPu():
def __init__(self, conf: dict) -> None:
self.api_key = conf.get("api_key")
self.model = conf.get("model", "glm-4") # 默认使用 glm-4 模型
self.client = ZhipuAI(api_key=self.api_key)
self.converstion_list = {}
@staticmethod
def value_check(conf: dict) -> bool:
if conf and conf.get("api_key"):
return True
return False
def __repr__(self):
return 'ZhiPu'
def get_answer(self, msg: str, wxid: str, **args) -> str:
self._update_message(wxid, str(msg), "user")
response = self.client.chat.completions.create(
model=self.model,
messages=self.converstion_list[wxid]
)
resp_msg = response.choices[0].message
answer = resp_msg.content
self._update_message(wxid, answer, "assistant")
return answer
def _update_message(self, wxid: str, msg: str, role: str) -> None:
if wxid not in self.converstion_list.keys():
self.converstion_list[wxid] = []
content = {"role": role, "content": str(msg)}
self.converstion_list[wxid].append(content)
if __name__ == "__main__":
from configuration import Config
config = Config().ZHIPU
if not config:
exit(0)
zhipu = ZhiPu(config)
rsp = zhipu.get_answer("你好")
print(rsp)

84
config.yaml.template Normal file
View File

@@ -0,0 +1,84 @@
logging:
version: 1
disable_existing_loggers: False
formatters:
simple:
format: "%(asctime)s %(message)s"
datefmt: "%Y-%m-%d %H:%M:%S"
error:
format: "%(asctime)s %(name)s %(levelname)s %(filename)s::%(funcName)s[%(lineno)d]:%(message)s"
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: simple
stream: ext://sys.stdout
info_file_handler:
class: logging.handlers.RotatingFileHandler
level: INFO
formatter: simple
filename: wx_info.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8
error_file_handler:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: error
filename: wx_error.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8
root:
level: INFO
handlers: [console, info_file_handler, error_file_handler]
groups:
enable: [] # 允许响应的群 roomId大概长这样2xxxxxxxxx3@chatroom
news:
receivers: [] # 定时新闻接收人roomid 或者 wxid
report_reminder:
receivers: [] # 定时日报周报月报提醒roomid 或者 wxid
chatgpt: # -----chatgpt配置这行不填-----
key: # 填写你 ChatGPT 的 key
api: https://api.openai.com/v1 # 如果你不知道这是干嘛的,就不要改
model: gpt-3.5-turbo
proxy: # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
prompt: 你是智能聊天机器人,你叫 wcferry # 根据需要对角色进行设定
chatglm: # -----chatglm配置这行不填-----
key: sk-012345678901234567890123456789012345678901234567 # 这个应该不用动
api: http://localhost:8000/v1 # 根据自己的chatglm地址修改
proxy: # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
prompt: 你是智能聊天机器人,你叫小薇 # 根据需要对角色进行设定
file_path: F:/Pictures/temp #设定生成图片和代码使用的文件夹路径
tigerbot: # -----tigerbot配置这行不填-----
key: # key
model: # tigerbot-7b-sft
xinghuo_web: # -----讯飞星火web模式api配置这行不填 抓取方式详见文档https://www.bilibili.com/read/cv27066577-----
cookie: # cookie
fd: # fd
GtToken: # GtToken
prompt: 你是智能聊天机器人,你叫 wcferry。请用这个角色回答我的问题 # 根据需要对角色进行设定
bard: # -----bard配置这行不填-----
api_key: # api-key 创建地址https://ai.google.dev/pricing?hl=en创建后复制过来即可
model_name: gemini-pro # 新模型上线后可以选择模型
proxy: http://127.0.0.1:7890 # 如果你在国内你可能需要魔法大概长这样http://域名或者IP地址:端口号
# 提示词尽可能用英文bard对中文提示词的效果不是很理想下方提示词为英语老师的示例请按实际需要修改,默认设置的提示词为谷歌创造的AI大语言模型
# I want you to act as a spoken English teacher and improver. I will speak to you in English and you will reply to me in English to practice my spoken English. I want you to keep your reply neat, limiting the reply to 100 words. I want you to strictly correct my grammar mistakes, typos, and factual errors. I want you to ask me a question in your reply. Now let's start practicing, you could ask me a question first. Remember, I want you to strictly correct my grammar mistakes, typos, and factual errors.
prompt: You am a large language model, trained by Google.
zhipu: # -----zhipu配置这行不填-----
api_key: #api key
model: # 模型类型

39
configuration.py Normal file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging.config
import os
import shutil
import yaml
class Config(object):
def __init__(self) -> None:
self.reload()
def _load_config(self) -> dict:
pwd = os.path.dirname(os.path.abspath(__file__))
try:
with open(f"{pwd}/config.yaml", "rb") as fp:
yconfig = yaml.safe_load(fp)
except FileNotFoundError:
shutil.copyfile(f"{pwd}/config.yaml.template", f"{pwd}/config.yaml")
with open(f"{pwd}/config.yaml", "rb") as fp:
yconfig = yaml.safe_load(fp)
return yconfig
def reload(self) -> None:
yconfig = self._load_config()
logging.config.dictConfig(yconfig["logging"])
self.GROUPS = yconfig["groups"]["enable"]
self.NEWS = yconfig["news"]["receivers"]
self.REPORT_REMINDERS = yconfig["report_reminder"]["receivers"]
self.CHATGPT = yconfig.get("chatgpt", {})
self.TIGERBOT = yconfig.get("tigerbot", {})
self.XINGHUO_WEB = yconfig.get("xinghuo_web", {})
self.CHATGLM = yconfig.get("chatglm", {})
self.BardAssistant = yconfig.get("bard", {})
self.ZhiPu = yconfig.get("zhipu", {})

26
constants.py Normal file
View File

@@ -0,0 +1,26 @@
from enum import IntEnum, unique
@unique
class ChatType(IntEnum):
# UnKnown = 0 # 未知, 即未设置
TIGER_BOT = 1 # TigerBot
CHATGPT = 2 # ChatGPT
XINGHUO_WEB = 3 # 讯飞星火
CHATGLM = 4 # ChatGLM
BardAssistant = 5 # Google Bard
ZhiPu = 6 # ZhiPu
@staticmethod
def is_in_chat_types(chat_type: int) -> bool:
if chat_type in [ChatType.TIGER_BOT.value, ChatType.CHATGPT.value,
ChatType.XINGHUO_WEB.value, ChatType.CHATGLM.value,
ChatType.BardAssistant.value]:
return True
# return False
return False
@staticmethod
def help_hint() -> str:
return str({member.value: member.name for member in ChatType}).replace('{', '').replace('}', '')

84
job_mgmt.py Normal file
View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
import time
from typing import Any, Callable
import schedule
class Job(object):
def __init__(self) -> None:
pass
def onEverySeconds(self, seconds: int, task: Callable[..., Any], *args, **kwargs) -> None:
"""
每 seconds 秒执行
:param seconds: 间隔,秒
:param task: 定时执行的方法
:return: None
"""
schedule.every(seconds).seconds.do(task, *args, **kwargs)
def onEveryMinutes(self, minutes: int, task: Callable[..., Any], *args, **kwargs) -> None:
"""
每 minutes 分钟执行
:param minutes: 间隔,分钟
:param task: 定时执行的方法
:return: None
"""
schedule.every(minutes).minutes.do(task, *args, **kwargs)
def onEveryHours(self, hours: int, task: Callable[..., Any], *args, **kwargs) -> None:
"""
每 hours 小时执行
:param hours: 间隔,小时
:param task: 定时执行的方法
:return: None
"""
schedule.every(hours).hours.do(task, *args, **kwargs)
def onEveryDays(self, days: int, task: Callable[..., Any], *args, **kwargs) -> None:
"""
每 days 天执行
:param days: 间隔,天
:param task: 定时执行的方法
:return: None
"""
schedule.every(days).days.do(task, *args, **kwargs)
def onEveryTime(self, times: int, task: Callable[..., Any], *args, **kwargs) -> None:
"""
每天定时执行
:param times: 时间字符串列表,格式:
- For daily jobs -> HH:MM:SS or HH:MM
- For hourly jobs -> MM:SS or :MM
- For minute jobs -> :SS
:param task: 定时执行的方法
:return: None
例子: times=["10:30", "10:45", "11:00"]
"""
if not isinstance(times, list):
times = [times]
for t in times:
schedule.every(1).days.at(t).do(task, *args, **kwargs)
def runPendingJobs(self) -> None:
schedule.run_pending()
if __name__ == "__main__":
def printStr(s):
print(s)
job = Job()
job.onEverySeconds(59, printStr, "onEverySeconds 59")
job.onEveryMinutes(59, printStr, "onEveryMinutes 59")
job.onEveryHours(23, printStr, "onEveryHours 23")
job.onEveryDays(1, printStr, "onEveryDays 1")
job.onEveryTime("23:59", printStr, "onEveryTime 23:59")
while True:
job.runPendingJobs()
time.sleep(1)

71
main.py Normal file
View File

@@ -0,0 +1,71 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import signal
from argparse import ArgumentParser
from base.func_report_reminder import ReportReminder
from configuration import Config
from constants import ChatType
from robot import Robot, __version__
from wcferry import Wcf
def weather_report(robot: Robot) -> None:
"""模拟发送天气预报
"""
# 获取接收人
receivers = ["filehelper"]
# 获取天气,需要自己实现,可以参考 https://gitee.com/lch0821/WeatherScrapy 获取天气。
report = "这就是获取到的天气情况了"
for r in receivers:
robot.sendTextMsg(report, r)
# robot.sendTextMsg(report, r, "notify@all") # 发送消息并@所有人
def main(chat_type: int):
config = Config()
# 开启调试模式
wcf = Wcf(debug=True)
def handler(sig, frame):
wcf.cleanup() # 退出前清理环境
exit(0)
# 进程通信
signal.signal(signal.SIGINT, handler)
# 自定义函数功能robot
robot = Robot(config, wcf, chat_type)
# 终端日志
robot.LOG.info(f"WeChatRobot【{__version__}】成功启动···")
# 机器人=>文件传输助手启动发送测试消息
robot.sendTextMsg("机器人启动成功!", "filehelper")
# 接收消息
# robot.enableRecvMsg() # 可能会丢消息?
robot.enableReceivingMsg() # 加队列
# 每天 7 点发送天气预报
# robot.onEveryTime("07:00", weather_report, robot=robot)
# 每天 7:30 发送新闻
# robot.onEveryTime("07:30", robot.newsReport)
# 每天 16:30 提醒发日报周报月报
# robot.onEveryTime("16:30", ReportReminder.remind, robot=robot)
# 让机器人一直跑
robot.keepRunningAndBlockProcess()
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument('-c', type=int, default=0, help=f'选择模型参数序号: {ChatType.help_hint()}')
args = parser.parse_args()
print('args',args)
main(args)

17
requirements.txt Normal file
View File

@@ -0,0 +1,17 @@
chinese_calendar
lxml
openai>1.0.0
pandas
pyyaml
requests
schedule
pyhandytools
sparkdesk-api==1.3.0
wcferry>=39.0.10.0
websocket
pillow
jupyter_client
zhdate
ipykernel
google-generativeai
zhipuai

269
robot.py Normal file
View File

@@ -0,0 +1,269 @@
# -*- coding: utf-8 -*-
import logging
import re
import time
import xml.etree.ElementTree as ET
from queue import Empty
from threading import Thread
from base.func_zhipu import ZhiPu
from wcferry import Wcf, WxMsg
from base.func_bard import BardAssistant
from base.func_chatglm import ChatGLM
from base.func_chatgpt import ChatGPT
from base.func_chengyu import cy
from base.func_news import News
from base.func_tigerbot import TigerBot
from base.func_xinghuo_web import XinghuoWeb
from configuration import Config
from constants import ChatType
from job_mgmt import Job
__version__ = "39.0.10.1"
class Robot(Job):
"""个性化自己的机器人
"""
def __init__(self, config: Config, wcf: Wcf, chat_type: int) -> None:
self.wcf = wcf
self.config = config
self.LOG = logging.getLogger("Robot")
self.wxid = self.wcf.get_self_wxid()
# 获取所有联系人
self.allContacts = self.getAllContacts()
self.chat = None
# if ChatType.is_in_chat_types(chat_type):
# if chat_type == ChatType.TIGER_BOT.value and TigerBot.value_check(self.config.TIGERBOT):
# self.chat = TigerBot(self.config.TIGERBOT)
# elif chat_type == ChatType.CHATGPT.value and ChatGPT.value_check(self.config.CHATGPT):
# self.chat = ChatGPT(self.config.CHATGPT)
# elif chat_type == ChatType.XINGHUO_WEB.value and XinghuoWeb.value_check(self.config.XINGHUO_WEB):
# self.chat = XinghuoWeb(self.config.XINGHUO_WEB)
# elif chat_type == ChatType.CHATGLM.value and ChatGLM.value_check(self.config.CHATGLM):
# self.chat = ChatGLM(self.config.CHATGLM)
# elif chat_type == ChatType.BardAssistant.value and BardAssistant.value_check(self.config.BardAssistant):
# self.chat = BardAssistant(self.config.BardAssistant)
# elif chat_type == ChatType.ZhiPu.value and ZhiPu.value_check(self.config.ZHIPU):
# self.chat = ZhiPu(self.config.ZHIPU)
# else:
# self.LOG.warning("未配置模型")
# self.chat = None
# else:
# if TigerBot.value_check(self.config.TIGERBOT):
# self.chat = TigerBot(self.config.TIGERBOT)
# elif ChatGPT.value_check(self.config.CHATGPT):
# self.chat = ChatGPT(self.config.CHATGPT)
# elif XinghuoWeb.value_check(self.config.XINGHUO_WEB):
# self.chat = XinghuoWeb(self.config.XINGHUO_WEB)
# elif ChatGLM.value_check(self.config.CHATGLM):
# self.chat = ChatGLM(self.config.CHATGLM)
# elif BardAssistant.value_check(self.config.BardAssistant):
# self.chat = BardAssistant(self.config.BardAssistant)
# elif ZhiPu.value_check(self.config.ZhiPu):
# self.chat = ZhiPu(self.config.ZhiPu)
# else:
# self.LOG.warning("未配置模型")
# self.chat = None
# self.LOG.info(f"已选择: {self.chat}")
@staticmethod
def value_check(args: dict) -> bool:
if args:
return all(value is not None for key, value in args.items() if key != 'proxy')
return False
def toAt(self, msg: WxMsg) -> bool:
"""处理被 @ 消息
:param msg: 微信消息结构
:return: 处理状态,`True` 成功,`False` 失败
"""
return self.toChitchat(msg)
def toChengyu(self, msg: WxMsg) -> bool:
"""
处理成语查询/接龙消息
:param msg: 微信消息结构
:return: 处理状态,`True` 成功,`False` 失败
"""
status = False
texts = re.findall(r"^([#|?|])(.*)$", msg.content)
# [('#', '天天向上')]
if texts:
flag = texts[0][0]
text = texts[0][1]
if flag == "#": # 接龙
if cy.isChengyu(text):
rsp = cy.getNext(text)
if rsp:
self.sendTextMsg(rsp, msg.roomid)
status = True
elif flag in ["?", ""]: # 查词
if cy.isChengyu(text):
rsp = cy.getMeaning(text)
if rsp:
self.sendTextMsg(rsp, msg.roomid)
status = True
return status
def toChitchat(self, msg: WxMsg) -> bool:
"""闲聊,接入 ChatGPT
"""
if not self.chat: # 没接 ChatGPT固定回复
rsp = "你@我干嘛?"
else: # 接了 ChatGPT智能回复
q = re.sub(r"@.*?[\u2005|\s]", "", msg.content).replace(" ", "")
rsp = self.chat.get_answer(q, (msg.roomid if msg.from_group() else msg.sender))
if rsp:
# 回复群消息
if msg.from_group():
self.sendTextMsg(rsp, msg.roomid, msg.sender)
# 回复私聊
else:
self.sendTextMsg(rsp, msg.sender)
return True
else:
self.LOG.error(f"无法从 ChatGPT 获得答案")
return False
def processMsg(self, msg: WxMsg) -> None:
"""当接收到消息的时候,会调用本方法。如果不实现本方法,则打印原始消息。
此处可进行自定义发送的内容,如通过 msg.content 关键字自动获取当前天气信息,并发送到对应的群组@发送者
群号msg.roomid 微信IDmsg.sender 消息内容msg.content
content = "xx天气信息为"
receivers = msg.roomid
self.sendTextMsg(content, receivers, msg.sender)
"""
# 群聊消息
if msg.from_group():
# 如果在群里被 @
if msg.roomid not in self.config.GROUPS: # 不在配置的响应的群列表里,忽略
return
if msg.is_at(self.wxid): # 被@
self.toAt(msg)
else: # 其他消息
self.toChengyu(msg)
return # 处理完群聊信息,后面就不需要处理了
# 非群聊信息,按消息类型进行处理
if msg.type == 37: # 好友请求
self.autoAcceptFriendRequest(msg)
elif msg.type == 10000: # 系统信息
self.sayHiToNewFriend(msg)
elif msg.type == 0x01: # 文本消息
# 让配置加载更灵活,自己可以更新配置。也可以利用定时任务更新。
if msg.from_self():
if msg.content == "^更新$":
self.config.reload()
self.LOG.info("已更新")
else:
self.toChitchat(msg) # 闲聊
def onMsg(self, msg: WxMsg) -> int:
try:
self.LOG.info(msg) # 打印信息
self.processMsg(msg)
except Exception as e:
self.LOG.error(e)
return 0
def enableRecvMsg(self) -> None:
self.wcf.enable_recv_msg(self.onMsg)
def enableReceivingMsg(self) -> None:
def innerProcessMsg(wcf: Wcf):
while wcf.is_receiving_msg():
try:
msg = wcf.get_msg()
self.LOG.info(msg)
self.processMsg(msg)
except Empty:
continue # Empty message
except Exception as e:
self.LOG.error(f"Receiving message error: {e}")
self.wcf.enable_receiving_msg()
Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start()
def sendTextMsg(self, msg: str, receiver: str, at_list: str = "") -> None:
""" 发送消息
:param msg: 消息字符串
:param receiver: 接收人wxid或者群id
:param at_list: 要@的wxid, @所有人的wxid为notify@all
"""
# msg 中需要有 @ 名单中一样数量的 @
ats = ""
if at_list:
if at_list == "notify@all": # @所有人
ats = " @所有人"
else:
wxids = at_list.split(",")
for wxid in wxids:
# 根据 wxid 查找群昵称
ats += f" @{self.wcf.get_alias_in_chatroom(wxid, receiver)}"
# {msg}{ats} 表示要发送的消息内容后面紧跟@,例如 北京天气情况为xxx @张三
if ats == "":
self.LOG.info(f"To {receiver}: {msg}")
self.wcf.send_text(f"{msg}", receiver, at_list)
else:
self.LOG.info(f"To {receiver}: {ats}\r{msg}")
self.wcf.send_text(f"{ats}\n\n{msg}", receiver, at_list)
def getAllContacts(self) -> dict:
"""
获取联系人(包括好友、公众号、服务号、群成员……)
格式: {"wxid": "NickName"}
"""
contacts = self.wcf.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;")
return {contact["UserName"]: contact["NickName"] for contact in contacts}
def keepRunningAndBlockProcess(self) -> None:
"""
保持机器人运行,不让进程退出
"""
while True:
self.runPendingJobs()
time.sleep(1)
def autoAcceptFriendRequest(self, msg: WxMsg) -> None:
try:
xml = ET.fromstring(msg.content)
v3 = xml.attrib["encryptusername"]
v4 = xml.attrib["ticket"]
scene = int(xml.attrib["scene"])
self.wcf.accept_new_friend(v3, v4, scene)
except Exception as e:
self.LOG.error(f"同意好友出错:{e}")
def sayHiToNewFriend(self, msg: WxMsg) -> None:
nickName = re.findall(r"你已添加了(.*),现在可以开始聊天了。", msg.content)
if nickName:
# 添加了好友,更新好友列表
self.allContacts[msg.sender] = nickName[0]
self.sendTextMsg(f"Hi {nickName[0]},我自动通过了你的好友请求。", msg.sender)
def newsReport(self) -> None:
receivers = self.config.NEWS
if not receivers:
return
news = News().get_important_news()
for r in receivers:
self.sendTextMsg(news, r)