mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-03 02:27:09 +08:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16324e7283 | ||
|
|
9f7e2e1572 | ||
|
|
857ce1d530 | ||
|
|
be0d72775d | ||
|
|
7832a2495b | ||
|
|
0506b7f735 | ||
|
|
4c0b7942f0 | ||
|
|
651c840c4a | ||
|
|
2a351ca415 | ||
|
|
49b7106d71 | ||
|
|
8bf633f539 | ||
|
|
0f8efcb4b0 | ||
|
|
c567641c5c | ||
|
|
bdc3820382 | ||
|
|
33a69a7907 | ||
|
|
a4d0e9bbc3 | ||
|
|
afc753e1d2 | ||
|
|
e641a41224 | ||
|
|
79305c0632 | ||
|
|
ef2ce3f09d | ||
|
|
71c18c04fc | ||
|
|
cf84e57f81 | ||
|
|
9421d44579 | ||
|
|
5cd2ae8cc8 | ||
|
|
22d67b3a59 | ||
|
|
e102cbb8c4 | ||
|
|
d90eeb7ee4 | ||
|
|
1989d53031 | ||
|
|
04ef0907b4 | ||
|
|
517b43561c | ||
|
|
ccb8c7227f | ||
|
|
9fbfeeb04f | ||
|
|
8b753a5a1f | ||
|
|
d25cab0627 | ||
|
|
84da0a8a35 | ||
|
|
6f665cffba | ||
|
|
aea8ac2e97 | ||
|
|
8418fa7b45 | ||
|
|
9cc4d0ee07 | ||
|
|
da60831c44 | ||
|
|
0773174a20 | ||
|
|
70e007d8ca | ||
|
|
fcc4d02c2f | ||
|
|
f4a5f00593 | ||
|
|
1170ed6566 |
@@ -40,11 +40,12 @@ DEMO视频:https://cdn.link-ai.tech/doc/cow_demo.mp4
|
||||
|
||||
**企业服务和产品咨询** 可联系产品顾问:
|
||||
|
||||
<img width="160" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/github-product-consult.png">
|
||||
<img width="160" src="https://cdn.link-ai.tech/consultant-s.jpg">
|
||||
|
||||
<br>
|
||||
|
||||
# 🏷 更新日志
|
||||
>**2024.10.31:** [1.7.3版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.3) 程序稳定性提升、数据库功能、Claude模型优化、linkai插件优化、离线通知
|
||||
|
||||
>**2024.09.26:** [1.7.2版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.2) 和 [1.7.1版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.1) 文心,讯飞等模型优化、o1 模型、快速安装和管理脚本
|
||||
|
||||
@@ -143,6 +144,7 @@ pip3 install -r requirements-optional.txt
|
||||
{
|
||||
"model": "gpt-3.5-turbo", # 模型名称, 支持 gpt-3.5-turbo, gpt-4, gpt-4-turbo, wenxin, xunfei, glm-4, claude-3-haiku, moonshot
|
||||
"open_ai_api_key": "YOUR API KEY", # 如果使用openAI模型则填入上面创建的 OpenAI API KEY
|
||||
"open_ai_api_base": "https://api.openai.com/v1", # OpenAI接口代理地址
|
||||
"proxy": "", # 代理客户端的ip和端口,国内环境开启代理的需要填写该项,如 "127.0.0.1:7890"
|
||||
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
|
||||
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
|
||||
@@ -274,7 +276,7 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
volumes:
|
||||
- ./config.json:/app/plugins/config.json
|
||||
```
|
||||
|
||||
**注**:采用docker方式部署的详细教程可以参考:[docker部署CoW项目](https://www.wangpc.cc/ai/docker-deploy-cow/)
|
||||
### 4. Railway部署
|
||||
|
||||
> Railway 每月提供5刀和最多500小时的免费额度。 (07.11更新: 目前大部分账号已无法免费部署)
|
||||
|
||||
2
app.py
2
app.py
@@ -27,7 +27,7 @@ def sigterm_handler_wrap(_signo):
|
||||
|
||||
def start_channel(channel_name: str):
|
||||
channel = channel_factory.create_channel(channel_name)
|
||||
if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework",
|
||||
if channel_name in ["wx", "wxy", "terminal", "wechatmp","web", "wechatmp_service", "wechatcom_app", "wework",
|
||||
const.FEISHU, const.DINGTALK]:
|
||||
PluginManager().load_plugins()
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class ChatGPTSession(Session):
|
||||
def num_tokens_from_messages(messages, model):
|
||||
"""Returns the number of tokens used by a list of messages."""
|
||||
|
||||
if model in ["wenxin", "xunfei", const.GEMINI]:
|
||||
if model in ["wenxin", "xunfei"] or model.startswith(const.GEMINI):
|
||||
return num_tokens_by_character(messages)
|
||||
|
||||
import tiktoken
|
||||
|
||||
@@ -8,12 +8,12 @@ import anthropic
|
||||
|
||||
from bot.bot import Bot
|
||||
from bot.openai.open_ai_image import OpenAIImage
|
||||
from bot.chatgpt.chat_gpt_session import ChatGPTSession
|
||||
from bot.gemini.google_gemini_bot import GoogleGeminiBot
|
||||
from bot.baidu.baidu_wenxin_session import BaiduWenxinSession
|
||||
from bot.session_manager import SessionManager
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from common.log import logger
|
||||
from common import const
|
||||
from config import conf
|
||||
|
||||
user_session = dict()
|
||||
@@ -23,17 +23,14 @@ user_session = dict()
|
||||
class ClaudeAPIBot(Bot, OpenAIImage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
proxy = conf().get("proxy", None)
|
||||
base_url = conf().get("open_ai_api_base", None) # 复用"open_ai_api_base"参数作为base_url
|
||||
self.claudeClient = anthropic.Anthropic(
|
||||
api_key=conf().get("claude_api_key")
|
||||
api_key=conf().get("claude_api_key"),
|
||||
proxies=proxy if proxy else None,
|
||||
base_url=base_url if base_url else None
|
||||
)
|
||||
openai.api_key = conf().get("open_ai_api_key")
|
||||
if conf().get("open_ai_api_base"):
|
||||
openai.api_base = conf().get("open_ai_api_base")
|
||||
proxy = conf().get("proxy")
|
||||
if proxy:
|
||||
openai.proxy = proxy
|
||||
|
||||
self.sessions = SessionManager(ChatGPTSession, model=conf().get("model") or "text-davinci-003")
|
||||
self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "text-davinci-003")
|
||||
|
||||
def reply(self, query, context=None):
|
||||
# acquire reply content
|
||||
@@ -76,14 +73,14 @@ class ClaudeAPIBot(Bot, OpenAIImage):
|
||||
reply = Reply(ReplyType.ERROR, retstring)
|
||||
return reply
|
||||
|
||||
def reply_text(self, session: ChatGPTSession, retry_count=0):
|
||||
def reply_text(self, session: BaiduWenxinSession, retry_count=0):
|
||||
try:
|
||||
actual_model = self._model_mapping(conf().get("model"))
|
||||
response = self.claudeClient.messages.create(
|
||||
model=actual_model,
|
||||
max_tokens=1024,
|
||||
# system=conf().get("system"),
|
||||
messages=GoogleGeminiBot.filter_messages(session.messages)
|
||||
max_tokens=4096,
|
||||
system=conf().get("character_desc", ""),
|
||||
messages=session.messages
|
||||
)
|
||||
# response = openai.Completion.create(prompt=str(session), **self.args)
|
||||
res_content = response.content[0].text.strip().replace("<|endoftext|>", "")
|
||||
@@ -97,7 +94,7 @@ class ClaudeAPIBot(Bot, OpenAIImage):
|
||||
}
|
||||
except Exception as e:
|
||||
need_retry = retry_count < 2
|
||||
result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
|
||||
result = {"total_tokens": 0, "completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
|
||||
if isinstance(e, openai.error.RateLimitError):
|
||||
logger.warn("[CLAUDE_API] RateLimitError: {}".format(e))
|
||||
result["content"] = "提问太快啦,请休息一下再问我吧"
|
||||
@@ -125,11 +122,11 @@ class ClaudeAPIBot(Bot, OpenAIImage):
|
||||
|
||||
def _model_mapping(self, model) -> str:
|
||||
if model == "claude-3-opus":
|
||||
return "claude-3-opus-20240229"
|
||||
return const.CLAUDE_3_OPUS
|
||||
elif model == "claude-3-sonnet":
|
||||
return "claude-3-sonnet-20240229"
|
||||
return const.CLAUDE_3_SONNET
|
||||
elif model == "claude-3-haiku":
|
||||
return "claude-3-haiku-20240307"
|
||||
return const.CLAUDE_3_HAIKU
|
||||
elif model == "claude-3.5-sonnet":
|
||||
return "claude-3-5-sonnet-20240620"
|
||||
return const.CLAUDE_35_SONNET
|
||||
return model
|
||||
|
||||
@@ -21,6 +21,9 @@ def create_channel(channel_type) -> Channel:
|
||||
elif channel_type == "terminal":
|
||||
from channel.terminal.terminal_channel import TerminalChannel
|
||||
ch = TerminalChannel()
|
||||
elif channel_type == 'web':
|
||||
from channel.web.web_channel import WebChannel
|
||||
ch = WebChannel()
|
||||
elif channel_type == "wechatmp":
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
ch = WechatMPChannel(passive_reply=True)
|
||||
|
||||
@@ -337,24 +337,27 @@ class ChatChannel(Channel):
|
||||
while True:
|
||||
with self.lock:
|
||||
session_ids = list(self.sessions.keys())
|
||||
for session_id in session_ids:
|
||||
for session_id in session_ids:
|
||||
with self.lock:
|
||||
context_queue, semaphore = self.sessions[session_id]
|
||||
if semaphore.acquire(blocking=False): # 等线程处理完毕才能删除
|
||||
if not context_queue.empty():
|
||||
context = context_queue.get()
|
||||
logger.debug("[chat_channel] consume context: {}".format(context))
|
||||
future: Future = handler_pool.submit(self._handle, context)
|
||||
future.add_done_callback(self._thread_pool_callback(session_id, context=context))
|
||||
if semaphore.acquire(blocking=False): # 等线程处理完毕才能删除
|
||||
if not context_queue.empty():
|
||||
context = context_queue.get()
|
||||
logger.debug("[chat_channel] consume context: {}".format(context))
|
||||
future: Future = handler_pool.submit(self._handle, context)
|
||||
future.add_done_callback(self._thread_pool_callback(session_id, context=context))
|
||||
with self.lock:
|
||||
if session_id not in self.futures:
|
||||
self.futures[session_id] = []
|
||||
self.futures[session_id].append(future)
|
||||
elif semaphore._initial_value == semaphore._value + 1: # 除了当前,没有任务再申请到信号量,说明所有任务都处理完毕
|
||||
elif semaphore._initial_value == semaphore._value + 1: # 除了当前,没有任务再申请到信号量,说明所有任务都处理完毕
|
||||
with self.lock:
|
||||
self.futures[session_id] = [t for t in self.futures[session_id] if not t.done()]
|
||||
assert len(self.futures[session_id]) == 0, "thread pool error"
|
||||
del self.sessions[session_id]
|
||||
else:
|
||||
semaphore.release()
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
semaphore.release()
|
||||
time.sleep(0.2)
|
||||
|
||||
# 取消session_id对应的所有任务,只能取消排队的消息和已提交线程池但未执行的任务
|
||||
def cancel_session(self, session_id):
|
||||
|
||||
7
channel/web/README.md
Normal file
7
channel/web/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Web channel
|
||||
使用SSE(Server-Sent Events,服务器推送事件)实现,提供了一个默认的网页。也可以自己实现加入api
|
||||
|
||||
#使用方法
|
||||
- 在配置文件中channel_type填入web即可
|
||||
- 访问地址 http://localhost:9899
|
||||
- port可以在配置项 web_port中设置
|
||||
165
channel/web/chat.html
Normal file
165
channel/web/chat.html
Normal file
@@ -0,0 +1,165 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chat</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh; /* 占据所有高度 */
|
||||
margin: 0;
|
||||
/* background-color: #f8f9fa; */
|
||||
}
|
||||
#chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
flex: 1; /* 使聊天容器占据剩余空间 */
|
||||
}
|
||||
#messages {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
border-bottom: 1px solid #ccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 5px 0; /* 间隔 */
|
||||
padding: 10px 15px; /* 内边距 */
|
||||
border-radius: 15px; /* 圆角 */
|
||||
max-width: 80%; /* 限制最大宽度 */
|
||||
min-width: 80px; /* 设置最小宽度 */
|
||||
min-height: 40px; /* 设置最小高度 */
|
||||
word-wrap: break-word; /* 自动换行 */
|
||||
position: relative; /* 时间戳定位 */
|
||||
display: inline-block; /* 内容自适应宽度 */
|
||||
box-sizing: border-box; /* 包括内边距和边框 */
|
||||
flex-shrink: 0; /* 禁止高度被压缩 */
|
||||
word-wrap: break-word; /* 自动换行,防止单行过长 */
|
||||
white-space: normal; /* 允许正常换行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bot {
|
||||
background-color: #f1f1f1; /* 灰色背景 */
|
||||
color: black; /* 黑色字体 */
|
||||
align-self: flex-start; /* 左对齐 */
|
||||
margin-right: auto; /* 确保消息靠左 */
|
||||
text-align: left; /* 内容左对齐 */
|
||||
}
|
||||
|
||||
.user {
|
||||
background-color: #2bc840; /* 蓝色背景 */
|
||||
align-self: flex-end; /* 右对齐 */
|
||||
margin-left: auto; /* 确保消息靠右 */
|
||||
text-align: left; /* 内容左对齐 */
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.8em; /* 时间戳字体大小 */
|
||||
color: rgba(0, 0, 0, 0.5); /* 半透明黑色 */
|
||||
margin-bottom: 5px; /* 时间戳下方间距 */
|
||||
display: block; /* 时间戳独占一行 */
|
||||
}
|
||||
#input-container {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
#input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
#send {
|
||||
padding: 10px;
|
||||
border: none;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#send:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chat-container">
|
||||
<div id="messages"></div>
|
||||
<div id="input-container">
|
||||
<input type="text" id="input" placeholder="输入消息..." />
|
||||
<button id="send">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const input = document.getElementById('input');
|
||||
const sendButton = document.getElementById('send');
|
||||
|
||||
// 生成唯一的 user_id
|
||||
const userId = 'user_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// 连接 SSE
|
||||
const eventSource = new EventSource(`/sse/${userId}`);
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message bot';
|
||||
const timestamp = new Date(message.timestamp).toLocaleTimeString(); // 假设消息中有时间戳
|
||||
messageDiv.innerHTML = `<div class="timestamp">${timestamp}</div>${message.content}`; // 显示时间
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight; // 滚动到底部
|
||||
};
|
||||
|
||||
sendButton.onclick = function() {
|
||||
sendMessage();
|
||||
};
|
||||
|
||||
input.addEventListener('keypress', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
sendMessage();
|
||||
event.preventDefault(); // 防止换行
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const userMessage = input.value;
|
||||
if (userMessage) {
|
||||
const timestamp = new Date().toISOString(); // 获取当前时间戳
|
||||
fetch('/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId, message: userMessage, timestamp: timestamp }) // 发送时间戳
|
||||
});
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message user';
|
||||
const userTimestamp = new Date().toLocaleTimeString(); // 获取当前时间
|
||||
messageDiv.innerHTML = `<div class="timestamp">${userTimestamp}</div>${userMessage}`; // 显示时间
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight; // 滚动到底部
|
||||
input.value = ''; // 清空输入框
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
204
channel/web/web_channel.py
Normal file
204
channel/web/web_channel.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import sys
|
||||
import time
|
||||
import web
|
||||
import json
|
||||
from queue import Queue
|
||||
from bridge.context import *
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from channel.chat_channel import ChatChannel, check_prefix
|
||||
from channel.chat_message import ChatMessage
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from config import conf
|
||||
import os
|
||||
|
||||
|
||||
class WebMessage(ChatMessage):
|
||||
def __init__(
|
||||
self,
|
||||
msg_id,
|
||||
content,
|
||||
ctype=ContextType.TEXT,
|
||||
from_user_id="User",
|
||||
to_user_id="Chatgpt",
|
||||
other_user_id="Chatgpt",
|
||||
):
|
||||
self.msg_id = msg_id
|
||||
self.ctype = ctype
|
||||
self.content = content
|
||||
self.from_user_id = from_user_id
|
||||
self.to_user_id = to_user_id
|
||||
self.other_user_id = other_user_id
|
||||
|
||||
|
||||
@singleton
|
||||
class WebChannel(ChatChannel):
|
||||
NOT_SUPPORT_REPLYTYPE = [ReplyType.VOICE]
|
||||
_instance = None
|
||||
|
||||
# def __new__(cls):
|
||||
# if cls._instance is None:
|
||||
# cls._instance = super(WebChannel, cls).__new__(cls)
|
||||
# return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.message_queues = {} # 为每个用户存储一个消息队列
|
||||
self.msg_id_counter = 0 # 添加消息ID计数器
|
||||
|
||||
def _generate_msg_id(self):
|
||||
"""生成唯一的消息ID"""
|
||||
self.msg_id_counter += 1
|
||||
return str(int(time.time())) + str(self.msg_id_counter)
|
||||
|
||||
def send(self, reply: Reply, context: Context):
|
||||
try:
|
||||
if reply.type == ReplyType.IMAGE:
|
||||
from PIL import Image
|
||||
|
||||
image_storage = reply.content
|
||||
image_storage.seek(0)
|
||||
img = Image.open(image_storage)
|
||||
print("<IMAGE>")
|
||||
img.show()
|
||||
elif reply.type == ReplyType.IMAGE_URL:
|
||||
import io
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
img_url = reply.content
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
image_storage = io.BytesIO()
|
||||
for block in pic_res.iter_content(1024):
|
||||
image_storage.write(block)
|
||||
image_storage.seek(0)
|
||||
img = Image.open(image_storage)
|
||||
print(img_url)
|
||||
img.show()
|
||||
else:
|
||||
print(reply.content)
|
||||
|
||||
# 获取用户ID,如果没有则使用默认值
|
||||
# user_id = getattr(context.get("session", None), "session_id", "default_user")
|
||||
user_id = context["receiver"]
|
||||
# 确保用户有对应的消息队列
|
||||
if user_id not in self.message_queues:
|
||||
self.message_queues[user_id] = Queue()
|
||||
|
||||
# 将消息放入对应用户的队列
|
||||
message_data = {
|
||||
"type": str(reply.type),
|
||||
"content": reply.content,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.message_queues[user_id].put(message_data)
|
||||
logger.debug(f"Message queued for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in send method: {e}")
|
||||
raise
|
||||
|
||||
def sse_handler(self, user_id):
|
||||
"""
|
||||
Handle Server-Sent Events (SSE) for real-time communication.
|
||||
"""
|
||||
web.header('Content-Type', 'text/event-stream')
|
||||
web.header('Cache-Control', 'no-cache')
|
||||
web.header('Connection', 'keep-alive')
|
||||
|
||||
# 确保用户有消息队列
|
||||
if user_id not in self.message_queues:
|
||||
self.message_queues[user_id] = Queue()
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
# 发送心跳
|
||||
yield f": heartbeat\n\n"
|
||||
|
||||
# 非阻塞方式获取消息
|
||||
if not self.message_queues[user_id].empty():
|
||||
message = self.message_queues[user_id].get_nowait()
|
||||
yield f"data: {json.dumps(message)}\n\n"
|
||||
time.sleep(0.5)
|
||||
except Exception as e:
|
||||
logger.error(f"SSE Error: {e}")
|
||||
break
|
||||
finally:
|
||||
# 清理资源
|
||||
if user_id in self.message_queues:
|
||||
# 只有当队列为空时才删除
|
||||
if self.message_queues[user_id].empty():
|
||||
del self.message_queues[user_id]
|
||||
|
||||
def post_message(self):
|
||||
"""
|
||||
Handle incoming messages from users via POST request.
|
||||
"""
|
||||
try:
|
||||
data = web.data() # 获取原始POST数据
|
||||
json_data = json.loads(data)
|
||||
user_id = json_data.get('user_id', 'default_user')
|
||||
prompt = json_data.get('message', '')
|
||||
except json.JSONDecodeError:
|
||||
return json.dumps({"status": "error", "message": "Invalid JSON"})
|
||||
except Exception as e:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
if not prompt:
|
||||
return json.dumps({"status": "error", "message": "No message provided"})
|
||||
|
||||
try:
|
||||
msg_id = self._generate_msg_id()
|
||||
context = self._compose_context(ContextType.TEXT, prompt, msg=WebMessage(msg_id,
|
||||
prompt,
|
||||
from_user_id=user_id,
|
||||
other_user_id = user_id
|
||||
))
|
||||
context["isgroup"] = False
|
||||
# context["session"] = web.storage(session_id=user_id)
|
||||
|
||||
if not context:
|
||||
return json.dumps({"status": "error", "message": "Failed to process message"})
|
||||
|
||||
self.produce(context)
|
||||
return json.dumps({"status": "success", "message": "Message received"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
return json.dumps({"status": "error", "message": "Internal server error"})
|
||||
|
||||
def chat_page(self):
|
||||
"""Serve the chat HTML page."""
|
||||
file_path = os.path.join(os.path.dirname(__file__), 'chat.html') # 使用绝对路径
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
def startup(self):
|
||||
logger.setLevel("WARN")
|
||||
print("\nWeb Channel is running. Send POST requests to /message to send messages.")
|
||||
|
||||
urls = (
|
||||
'/sse/(.+)', 'SSEHandler', # 修改路由以接收用户ID
|
||||
'/message', 'MessageHandler',
|
||||
'/chat', 'ChatHandler',
|
||||
)
|
||||
port = conf().get("web_port", 9899)
|
||||
app = web.application(urls, globals(), autoreload=False)
|
||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||
|
||||
|
||||
class SSEHandler:
|
||||
def GET(self, user_id):
|
||||
return WebChannel().sse_handler(user_id)
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
def POST(self):
|
||||
return WebChannel().post_message()
|
||||
|
||||
|
||||
class ChatHandler:
|
||||
def GET(self):
|
||||
return WebChannel().chat_page()
|
||||
@@ -20,7 +20,7 @@ from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.time_check import time_checker
|
||||
from common.utils import convert_webp_to_png
|
||||
from common.utils import convert_webp_to_png, remove_markdown_symbol
|
||||
from config import conf, get_appdata_dir
|
||||
from lib import itchat
|
||||
from lib.itchat.content import *
|
||||
@@ -213,9 +213,11 @@ class WechatChannel(ChatChannel):
|
||||
def send(self, reply: Reply, context: Context):
|
||||
receiver = context["receiver"]
|
||||
if reply.type == ReplyType.TEXT:
|
||||
reply.content = remove_markdown_symbol(reply.content)
|
||||
itchat.send(reply.content, toUserName=receiver)
|
||||
logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
reply.content = remove_markdown_symbol(reply.content)
|
||||
itchat.send(reply.content, toUserName=receiver)
|
||||
logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
|
||||
@@ -55,6 +55,16 @@ class WechatMessage(ChatMessage):
|
||||
self.ctype = ContextType.EXIT_GROUP
|
||||
self.content = itchat_msg["Content"]
|
||||
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0]
|
||||
|
||||
elif any(note_patpat in itchat_msg["Content"] for note_patpat in notes_patpat): # 若有任何在notes_patpat列表中的字符串出现在NOTE中:
|
||||
self.ctype = ContextType.PATPAT
|
||||
self.content = itchat_msg["Content"]
|
||||
if "拍了拍我" in itchat_msg["Content"]: # 识别中文
|
||||
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0]
|
||||
elif "tickled my" in itchat_msg["Content"] or "tickled me" in itchat_msg["Content"]:
|
||||
self.actual_user_nickname = re.findall(r'^(.*?)(?:tickled my|tickled me)', itchat_msg["Content"])[0]
|
||||
else:
|
||||
raise NotImplementedError("Unsupported note message: " + itchat_msg["Content"])
|
||||
|
||||
elif "你已添加了" in itchat_msg["Content"]: #通过好友请求
|
||||
self.ctype = ContextType.ACCEPT_FRIEND
|
||||
@@ -62,11 +72,6 @@ class WechatMessage(ChatMessage):
|
||||
elif any(note_patpat in itchat_msg["Content"] for note_patpat in notes_patpat): # 若有任何在notes_patpat列表中的字符串出现在NOTE中:
|
||||
self.ctype = ContextType.PATPAT
|
||||
self.content = itchat_msg["Content"]
|
||||
if is_group:
|
||||
if "拍了拍我" in itchat_msg["Content"]: # 识别中文
|
||||
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0]
|
||||
elif ("tickled my" in itchat_msg["Content"] or "tickled me" in itchat_msg["Content"]):
|
||||
self.actual_user_nickname = re.findall(r'^(.*?)(?:tickled my|tickled me)', itchat_msg["Content"])[0]
|
||||
else:
|
||||
raise NotImplementedError("Unsupported note message: " + itchat_msg["Content"])
|
||||
elif itchat_msg["Type"] == ATTACHMENT:
|
||||
|
||||
@@ -17,7 +17,7 @@ from channel.wechatcom.wechatcomapp_client import WechatComAppClient
|
||||
from channel.wechatcom.wechatcomapp_message import WechatComAppMessage
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.utils import compress_imgfile, fsize, split_string_by_utf8_length, convert_webp_to_png
|
||||
from common.utils import compress_imgfile, fsize, split_string_by_utf8_length, convert_webp_to_png, remove_markdown_symbol
|
||||
from config import conf, subscribe_msg
|
||||
from voice.audio_convert import any_to_amr, split_audio
|
||||
|
||||
@@ -52,7 +52,7 @@ class WechatComAppChannel(ChatChannel):
|
||||
def send(self, reply: Reply, context: Context):
|
||||
receiver = context["receiver"]
|
||||
if reply.type in [ReplyType.TEXT, ReplyType.ERROR, ReplyType.INFO]:
|
||||
reply_text = reply.content
|
||||
reply_text = remove_markdown_symbol(reply.content)
|
||||
texts = split_string_by_utf8_length(reply_text, MAX_UTF8_LEN)
|
||||
if len(texts) > 1:
|
||||
logger.info("[wechatcom] text too long, split into {} parts".format(len(texts)))
|
||||
|
||||
@@ -19,7 +19,7 @@ from channel.wechatmp.common import *
|
||||
from channel.wechatmp.wechatmp_client import WechatMPClient
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.utils import split_string_by_utf8_length
|
||||
from common.utils import split_string_by_utf8_length, remove_markdown_symbol
|
||||
from config import conf
|
||||
from voice.audio_convert import any_to_mp3, split_audio
|
||||
|
||||
@@ -81,7 +81,7 @@ class WechatMPChannel(ChatChannel):
|
||||
receiver = context["receiver"]
|
||||
if self.passive_reply:
|
||||
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
|
||||
reply_text = reply.content
|
||||
reply_text = remove_markdown_symbol(reply.content)
|
||||
logger.info("[wechatmp] text cached, receiver {}\n{}".format(receiver, reply_text))
|
||||
self.cache_dict[receiver].append(("text", reply_text))
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
|
||||
@@ -59,6 +59,8 @@ LINKAI_4o = "linkai-4o"
|
||||
GEMINI_PRO = "gemini-1.0-pro"
|
||||
GEMINI_15_flash = "gemini-1.5-flash"
|
||||
GEMINI_15_PRO = "gemini-1.5-pro"
|
||||
GEMINI_20_flash_exp = "gemini-2.0-flash-exp"
|
||||
|
||||
|
||||
GLM_4 = "glm-4"
|
||||
GLM_4_PLUS = "glm-4-plus"
|
||||
@@ -69,6 +71,17 @@ GLM_4_0520 = "glm-4-0520"
|
||||
GLM_4_AIR = "glm-4-air"
|
||||
GLM_4_AIRX = "glm-4-airx"
|
||||
|
||||
|
||||
CLAUDE_3_OPUS = "claude-3-opus-latest"
|
||||
CLAUDE_3_OPUS_0229 = "claude-3-opus-20240229"
|
||||
|
||||
CLAUDE_35_SONNET = "claude-3-5-sonnet-latest" # 带 latest 标签的模型名称,会不断更新指向最新发布的模型
|
||||
CLAUDE_35_SONNET_1022 = "claude-3-5-sonnet-20241022" # 带具体日期的模型名称,会固定为该日期发布的模型
|
||||
CLAUDE_35_SONNET_0620 = "claude-3-5-sonnet-20240620"
|
||||
CLAUDE_3_SONNET = "claude-3-sonnet-20240229"
|
||||
|
||||
CLAUDE_3_HAIKU = "claude-3-haiku-20240307"
|
||||
|
||||
MODEL_LIST = [
|
||||
GPT35, GPT35_0125, GPT35_1106, "gpt-3.5-turbo-16k",
|
||||
O1, O1_MINI, GPT_4o, GPT_4O_0806, GPT_4o_MINI, GPT4_TURBO, GPT4_TURBO_PREVIEW, GPT4_TURBO_01_25, GPT4_TURBO_11_06, GPT4, GPT4_32k, GPT4_06_13, GPT4_32k_06_13,
|
||||
@@ -76,8 +89,8 @@ MODEL_LIST = [
|
||||
XUNFEI,
|
||||
ZHIPU_AI, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS, GLM_4_0520, GLM_4_AIR, GLM_4_AIRX,
|
||||
MOONSHOT, MiniMax,
|
||||
GEMINI, GEMINI_PRO, GEMINI_15_flash, GEMINI_15_PRO,
|
||||
"claude", "claude-3-haiku", "claude-3-sonnet", "claude-3-opus", "claude-3-opus-20240229", "claude-3.5-sonnet",
|
||||
GEMINI, GEMINI_PRO, GEMINI_15_flash, GEMINI_15_PRO,GEMINI_20_flash_exp,
|
||||
CLAUDE_3_OPUS, CLAUDE_3_OPUS_0229, CLAUDE_35_SONNET, CLAUDE_35_SONNET_1022, CLAUDE_35_SONNET_0620, CLAUDE_3_SONNET, CLAUDE_3_HAIKU, "claude", "claude-3-haiku", "claude-3-sonnet", "claude-3-opus", "claude-3.5-sonnet",
|
||||
"moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k",
|
||||
QWEN, QWEN_TURBO, QWEN_PLUS, QWEN_MAX,
|
||||
LINKAI_35, LINKAI_4_TURBO, LINKAI_4o
|
||||
|
||||
@@ -2,7 +2,7 @@ from bridge.context import Context, ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from common.log import logger
|
||||
from linkai import LinkAIClient, PushMsg
|
||||
from config import conf, pconf, plugin_config, available_setting
|
||||
from config import conf, pconf, plugin_config, available_setting, write_plugin_config
|
||||
from plugins import PluginManager
|
||||
import time
|
||||
|
||||
@@ -51,10 +51,10 @@ class ChatClient(LinkAIClient):
|
||||
local_config["voice_reply_voice"] = False
|
||||
|
||||
if config.get("admin_password"):
|
||||
if not plugin_config.get("Godcmd"):
|
||||
plugin_config["Godcmd"] = {"password": config.get("admin_password"), "admin_users": []}
|
||||
if not pconf("Godcmd"):
|
||||
write_plugin_config({"Godcmd": {"password": config.get("admin_password"), "admin_users": []} })
|
||||
else:
|
||||
plugin_config["Godcmd"]["password"] = config.get("admin_password")
|
||||
pconf("Godcmd")["password"] = config.get("admin_password")
|
||||
PluginManager().instances["GODCMD"].reload()
|
||||
|
||||
if config.get("group_app_map") and pconf("linkai"):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from PIL import Image
|
||||
from common.log import logger
|
||||
@@ -68,3 +69,10 @@ def convert_webp_to_png(webp_image):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to convert WEBP to PNG: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def remove_markdown_symbol(text: str):
|
||||
# 移除markdown格式,目前先移除**
|
||||
if not text:
|
||||
return text
|
||||
return re.sub(r'\*\*(.*?)\*\*', r'\1', text)
|
||||
|
||||
@@ -179,6 +179,7 @@ available_setting = {
|
||||
"Minimax_api_key": "",
|
||||
"Minimax_group_id": "",
|
||||
"Minimax_base_url": "",
|
||||
"web_port": 9899,
|
||||
}
|
||||
|
||||
|
||||
@@ -341,6 +342,14 @@ def write_plugin_config(pconf: dict):
|
||||
for k in pconf:
|
||||
plugin_config[k.lower()] = pconf[k]
|
||||
|
||||
def remove_plugin_config(name: str):
|
||||
"""
|
||||
移除待重新加载的插件全局配置
|
||||
:param name: 待重载的插件名
|
||||
"""
|
||||
global plugin_config
|
||||
plugin_config.pop(name.lower(), None)
|
||||
|
||||
|
||||
def pconf(plugin_name: str) -> dict:
|
||||
"""
|
||||
|
||||
@@ -313,7 +313,7 @@ class Godcmd(Plugin):
|
||||
except Exception as e:
|
||||
ok, result = False, "你没有设置私有GPT模型"
|
||||
elif cmd == "reset":
|
||||
if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI, const.QWEN, const.GEMINI, const.ZHIPU_AI]:
|
||||
if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI, const.QWEN, const.GEMINI, const.ZHIPU_AI, const.CLAUDEAPI]:
|
||||
bot.sessions.clear_session(session_id)
|
||||
if Bridge().chat_bots.get(bottype):
|
||||
Bridge().chat_bots.get(bottype).sessions.clear_session(session_id)
|
||||
@@ -477,7 +477,7 @@ class Godcmd(Plugin):
|
||||
return model
|
||||
|
||||
def reload(self):
|
||||
gconf = plugin_config[self.name]
|
||||
gconf = pconf(self.name)
|
||||
if gconf:
|
||||
if gconf.get("password"):
|
||||
self.password = gconf["password"]
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
|
||||
如果不想创建 `plugins/linkai/config.json` 配置,可以直接通过 `$linkai sum open` 指令开启该功能。
|
||||
|
||||
也可以通过私聊(全局 `config.json` 中的 `linkai_app_code`)或者群聊绑定(通过`group_app_map`参数配置)的应用来开启该功能:在LinkAI平台 [应用配置](https://link-ai.tech/console/factory) 里添加并开启**内容总结**插件。
|
||||
|
||||
#### 使用
|
||||
|
||||
功能开启后,向机器人发送 **文件**、 **分享链接卡片**、**图片** 即可生成摘要,进一步可以与文件或链接的内容进行多轮对话。如果需要关闭某种类型的内容总结,设置 `summary`配置中的type字段即可。
|
||||
|
||||
@@ -9,7 +9,7 @@ from common.expired_dict import ExpiredDict
|
||||
from common import const
|
||||
import os
|
||||
from .utils import Util
|
||||
from config import plugin_config
|
||||
from config import plugin_config, conf
|
||||
|
||||
|
||||
@plugins.register(
|
||||
@@ -28,7 +28,7 @@ class LinkAI(Plugin):
|
||||
# 未加载到配置,使用模板中的配置
|
||||
self.config = self._load_config_template()
|
||||
if self.config:
|
||||
self.mj_bot = MJBot(self.config.get("midjourney"))
|
||||
self.mj_bot = MJBot(self.config.get("midjourney"), self._fetch_group_app_code)
|
||||
self.sum_config = {}
|
||||
if self.config:
|
||||
self.sum_config = self.config.get("summary")
|
||||
@@ -56,7 +56,8 @@ class LinkAI(Plugin):
|
||||
return
|
||||
if context.type != ContextType.IMAGE:
|
||||
_send_info(e_context, "正在为你加速生成摘要,请稍后")
|
||||
res = LinkSummary().summary_file(file_path)
|
||||
app_code = self._fetch_app_code(context)
|
||||
res = LinkSummary().summary_file(file_path, app_code)
|
||||
if not res:
|
||||
if context.type != ContextType.IMAGE:
|
||||
_set_reply_text("因为神秘力量无法获取内容,请稍后再试吧", e_context, level=ReplyType.TEXT)
|
||||
@@ -74,7 +75,8 @@ class LinkAI(Plugin):
|
||||
if not LinkSummary().check_url(context.content):
|
||||
return
|
||||
_send_info(e_context, "正在为你加速生成摘要,请稍后")
|
||||
res = LinkSummary().summary_url(context.content)
|
||||
app_code = self._fetch_app_code(context)
|
||||
res = LinkSummary().summary_url(context.content, app_code)
|
||||
if not res:
|
||||
_set_reply_text("因为神秘力量无法获取文章内容,请稍后再试吧~", e_context, level=ReplyType.TEXT)
|
||||
return
|
||||
@@ -169,7 +171,7 @@ class LinkAI(Plugin):
|
||||
return
|
||||
|
||||
if len(cmd) == 3 and cmd[1] == "sum" and (cmd[2] == "open" or cmd[2] == "close"):
|
||||
# 知识库开关指令
|
||||
# 总结对话开关指令
|
||||
if not Util.is_admin(e_context):
|
||||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
||||
return
|
||||
@@ -192,14 +194,36 @@ class LinkAI(Plugin):
|
||||
return
|
||||
|
||||
def _is_summary_open(self, context) -> bool:
|
||||
if not self.sum_config or not self.sum_config.get("enabled"):
|
||||
return False
|
||||
if context.kwargs.get("isgroup") and not self.sum_config.get("group_enabled"):
|
||||
return False
|
||||
support_type = self.sum_config.get("type") or ["FILE", "SHARING"]
|
||||
if context.type.name not in support_type and context.type.name != "TEXT":
|
||||
return False
|
||||
return True
|
||||
# 获取远程应用插件状态
|
||||
remote_enabled = False
|
||||
if context.kwargs.get("isgroup"):
|
||||
# 群聊场景只查询群对应的app_code
|
||||
group_name = context.get("msg").from_user_nickname
|
||||
app_code = self._fetch_group_app_code(group_name)
|
||||
if app_code:
|
||||
if context.type.name in ["FILE", "SHARING"]:
|
||||
remote_enabled = Util.fetch_app_plugin(app_code, "内容总结")
|
||||
else:
|
||||
# 非群聊场景使用全局app_code
|
||||
app_code = conf().get("linkai_app_code")
|
||||
if app_code:
|
||||
if context.type.name in ["FILE", "SHARING"]:
|
||||
remote_enabled = Util.fetch_app_plugin(app_code, "内容总结")
|
||||
|
||||
# 基础条件:总开关开启且消息类型符合要求
|
||||
base_enabled = (
|
||||
self.sum_config
|
||||
and self.sum_config.get("enabled")
|
||||
and (context.type.name in (
|
||||
self.sum_config.get("type") or ["FILE", "SHARING"]) or context.type.name == "TEXT")
|
||||
)
|
||||
|
||||
# 群聊:需要满足(总开关和群开关)或远程插件开启
|
||||
if context.kwargs.get("isgroup"):
|
||||
return (base_enabled and self.sum_config.get("group_enabled")) or remote_enabled
|
||||
|
||||
# 非群聊:只需要满足总开关或远程插件开启
|
||||
return base_enabled or remote_enabled
|
||||
|
||||
# LinkAI 对话任务处理
|
||||
def _is_chat_task(self, e_context: EventContext):
|
||||
@@ -230,6 +254,19 @@ class LinkAI(Plugin):
|
||||
app_code = group_mapping.get(group_name) or group_mapping.get("ALL_GROUP")
|
||||
return app_code
|
||||
|
||||
def _fetch_app_code(self, context) -> str:
|
||||
"""
|
||||
根据主配置或者群聊名称获取对应的应用code,优先获取群聊配置的应用code
|
||||
:param context: 上下文
|
||||
:return: 应用code
|
||||
"""
|
||||
app_code = conf().get("linkai_app_code")
|
||||
if context.kwargs.get("isgroup"):
|
||||
# 群聊场景只查询群对应的app_code
|
||||
group_name = context.get("msg").from_user_nickname
|
||||
app_code = self._fetch_group_app_code(group_name)
|
||||
return app_code
|
||||
|
||||
def get_help_text(self, verbose=False, **kwargs):
|
||||
trigger_prefix = _get_trigger_prefix()
|
||||
help_text = "用于集成 LinkAI 提供的知识库、Midjourney绘画、文档总结、联网搜索等能力。\n\n"
|
||||
@@ -254,7 +291,7 @@ class LinkAI(Plugin):
|
||||
plugin_conf = json.load(f)
|
||||
plugin_conf["midjourney"]["enabled"] = False
|
||||
plugin_conf["summary"]["enabled"] = False
|
||||
plugin_config["linkai"] = plugin_conf
|
||||
write_plugin_config({"linkai": plugin_conf})
|
||||
return plugin_conf
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
@@ -10,6 +10,7 @@ from bridge.context import ContextType
|
||||
from plugins import EventContext, EventAction
|
||||
from .utils import Util
|
||||
|
||||
|
||||
INVALID_REQUEST = 410
|
||||
NOT_FOUND_ORIGIN_IMAGE = 461
|
||||
NOT_FOUND_TASK = 462
|
||||
@@ -67,10 +68,11 @@ class MJTask:
|
||||
|
||||
# midjourney bot
|
||||
class MJBot:
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, fetch_group_app_code):
|
||||
self.base_url = conf().get("linkai_api_base", "https://api.link-ai.tech") + "/v1/img/midjourney"
|
||||
self.headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
|
||||
self.config = config
|
||||
self.fetch_group_app_code = fetch_group_app_code
|
||||
self.tasks = {}
|
||||
self.temp_dict = {}
|
||||
self.tasks_lock = threading.Lock()
|
||||
@@ -98,7 +100,7 @@ class MJBot:
|
||||
return TaskType.VARIATION
|
||||
elif cmd_list[0].lower() == f"{trigger_prefix}mjr":
|
||||
return TaskType.RESET
|
||||
elif context.type == ContextType.IMAGE_CREATE and self.config.get("use_image_create_prefix") and self.config.get("enabled"):
|
||||
elif context.type == ContextType.IMAGE_CREATE and self.config.get("use_image_create_prefix") and self._is_mj_open(context):
|
||||
return TaskType.GENERATE
|
||||
|
||||
def process_mj_task(self, mj_type: TaskType, e_context: EventContext):
|
||||
@@ -129,8 +131,8 @@ class MJBot:
|
||||
self._set_reply_text(f"Midjourney绘画已{tips_text}", e_context, level=ReplyType.INFO)
|
||||
return
|
||||
|
||||
if not self.config.get("enabled"):
|
||||
logger.warn("Midjourney绘画未开启,请查看 plugins/linkai/config.json 中的配置")
|
||||
if not self._is_mj_open(context):
|
||||
logger.warn("Midjourney绘画未开启,请查看 plugins/linkai/config.json 中的配置,或者在LinkAI平台 应用中添加/打开”MJ“插件")
|
||||
self._set_reply_text(f"Midjourney绘画未开启", e_context, level=ReplyType.INFO)
|
||||
return
|
||||
|
||||
@@ -409,6 +411,25 @@ class MJBot:
|
||||
result.append(task)
|
||||
return result
|
||||
|
||||
def _is_mj_open(self, context) -> bool:
|
||||
# 获取远程应用插件状态
|
||||
remote_enabled = False
|
||||
if context.kwargs.get("isgroup"):
|
||||
# 群聊场景只查询群对应的app_code
|
||||
group_name = context.get("msg").from_user_nickname
|
||||
app_code = self.fetch_group_app_code(group_name)
|
||||
if app_code:
|
||||
remote_enabled = Util.fetch_app_plugin(app_code, "Midjourney")
|
||||
else:
|
||||
# 非群聊场景使用全局app_code
|
||||
app_code = conf().get("linkai_app_code")
|
||||
if app_code:
|
||||
remote_enabled = Util.fetch_app_plugin(app_code, "Midjourney")
|
||||
|
||||
# 本地配置
|
||||
base_enabled = self.config.get("enabled")
|
||||
|
||||
return base_enabled or remote_enabled
|
||||
|
||||
def _send(channel, reply: Reply, context, retry_cnt=0):
|
||||
try:
|
||||
|
||||
@@ -9,20 +9,26 @@ class LinkSummary:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def summary_file(self, file_path: str):
|
||||
def summary_file(self, file_path: str, app_code: str):
|
||||
file_body = {
|
||||
"file": open(file_path, "rb"),
|
||||
"name": file_path.split("/")[-1],
|
||||
"name": file_path.split("/")[-1]
|
||||
}
|
||||
body = {
|
||||
"app_code": app_code
|
||||
}
|
||||
url = self.base_url() + "/v1/summary/file"
|
||||
res = requests.post(url, headers=self.headers(), files=file_body, timeout=(5, 300))
|
||||
logger.info(f"[LinkSum] file summary, app_code={app_code}")
|
||||
res = requests.post(url, headers=self.headers(), files=file_body, data=body, timeout=(5, 300))
|
||||
return self._parse_summary_res(res)
|
||||
|
||||
def summary_url(self, url: str):
|
||||
def summary_url(self, url: str, app_code: str):
|
||||
url = html.unescape(url)
|
||||
body = {
|
||||
"url": url
|
||||
"url": url,
|
||||
"app_code": app_code
|
||||
}
|
||||
logger.info(f"[LinkSum] url summary, app_code={app_code}")
|
||||
res = requests.post(url=self.base_url() + "/v1/summary/url", headers=self.headers(), json=body, timeout=(5, 180))
|
||||
return self._parse_summary_res(res)
|
||||
|
||||
@@ -48,7 +54,7 @@ class LinkSummary:
|
||||
def _parse_summary_res(self, res):
|
||||
if res.status_code == 200:
|
||||
res = res.json()
|
||||
logger.debug(f"[LinkSum] url summary, res={res}")
|
||||
logger.debug(f"[LinkSum] summary result, res={res}")
|
||||
if res.get("code") == 200:
|
||||
data = res.get("data")
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import requests
|
||||
from common.log import logger
|
||||
from config import global_config
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from plugins.event import EventContext, EventAction
|
||||
|
||||
from config import conf
|
||||
|
||||
class Util:
|
||||
@staticmethod
|
||||
@@ -26,3 +28,23 @@ class Util:
|
||||
reply = Reply(level, content)
|
||||
e_context["reply"] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
|
||||
@staticmethod
|
||||
def fetch_app_plugin(app_code: str, plugin_name: str) -> bool:
|
||||
try:
|
||||
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
|
||||
# do http request
|
||||
base_url = conf().get("linkai_api_base", "https://api.link-ai.tech")
|
||||
params = {"app_code": app_code}
|
||||
res = requests.get(url=base_url + "/v1/app/info", params=params, headers=headers, timeout=(5, 10))
|
||||
if res.status_code == 200:
|
||||
plugins = res.json().get("data").get("plugins")
|
||||
for plugin in plugins:
|
||||
if plugin.get("name") and plugin.get("name") == plugin_name:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"[LinkAI] find app info exception, res={res}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import json
|
||||
from config import pconf, plugin_config, conf
|
||||
from config import pconf, plugin_config, conf, write_plugin_config
|
||||
from common.log import logger
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ class Plugin:
|
||||
plugin_conf = json.load(f)
|
||||
|
||||
# 写入全局配置内存
|
||||
plugin_config[self.name] = plugin_conf
|
||||
write_plugin_config({self.name: plugin_conf})
|
||||
logger.debug(f"loading plugin config, plugin_name={self.name}, conf={plugin_conf}")
|
||||
return plugin_conf
|
||||
|
||||
def save_config(self, config: dict):
|
||||
try:
|
||||
plugin_config[self.name] = config
|
||||
write_plugin_config({self.name: config})
|
||||
# 写入全局配置
|
||||
global_config_path = "./plugins/config.json"
|
||||
if os.path.exists(global_config_path):
|
||||
|
||||
@@ -9,7 +9,7 @@ import sys
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.sorted_dict import SortedDict
|
||||
from config import conf, write_plugin_config
|
||||
from config import conf, remove_plugin_config, write_plugin_config
|
||||
|
||||
from .event import *
|
||||
|
||||
@@ -151,6 +151,8 @@ class PluginManager:
|
||||
self.disable_plugin(name)
|
||||
failed_plugins.append(name)
|
||||
continue
|
||||
if name in self.instances:
|
||||
self.instances[name].handlers.clear()
|
||||
self.instances[name] = instance
|
||||
for event in instance.handlers:
|
||||
if event not in self.listening_plugins:
|
||||
@@ -161,10 +163,13 @@ class PluginManager:
|
||||
|
||||
def reload_plugin(self, name: str):
|
||||
name = name.upper()
|
||||
remove_plugin_config(name)
|
||||
if name in self.instances:
|
||||
for event in self.listening_plugins:
|
||||
if name in self.listening_plugins[event]:
|
||||
self.listening_plugins[event].remove(name)
|
||||
if name in self.instances:
|
||||
self.instances[name].handlers.clear()
|
||||
del self.instances[name]
|
||||
self.activate_plugins()
|
||||
return True
|
||||
|
||||
@@ -180,6 +180,7 @@ class Role(Plugin):
|
||||
e_context["reply"] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
else:
|
||||
e_context["context"]["generate_breaked_by"] = EventAction.BREAK
|
||||
prompt = self.roleplays[sessionid].action(content)
|
||||
e_context["context"].type = ContextType.TEXT
|
||||
e_context["context"].content = prompt
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
"url": "https://github.com/lanvent/plugin_summary.git",
|
||||
"desc": "总结聊天记录的插件"
|
||||
},
|
||||
"timetask": {
|
||||
"url": "https://github.com/haikerapples/timetask.git",
|
||||
"desc": "一款定时任务系统的插件"
|
||||
},
|
||||
"Apilot": {
|
||||
"url": "https://github.com/6vision/Apilot.git",
|
||||
"desc": "通过api直接查询早报、热榜、快递、天气等实用信息的插件"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openai==0.27.8
|
||||
HTMLParser>=0.0.2
|
||||
PyQRCode>=1.2.1
|
||||
qrcode>=7.4.2
|
||||
PyQRCode==1.2.1
|
||||
qrcode==7.4.2
|
||||
requests>=2.28.2
|
||||
chardet>=5.1.0
|
||||
Pillow
|
||||
|
||||
Reference in New Issue
Block a user