mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e281fb61 | ||
|
|
dc625647eb | ||
|
|
66cf1b05be | ||
|
|
622cc89414 | ||
|
|
78d98c40b1 | ||
|
|
1c5f06d9a9 | ||
|
|
998fe5a980 | ||
|
|
8cad4089a7 | ||
|
|
48cc3656bd | ||
|
|
68ddb3a6e1 | ||
|
|
70583f5ba0 | ||
|
|
5bebe01dd0 | ||
|
|
4dd976c9c5 | ||
|
|
221b310485 | ||
|
|
dd1cec70c0 | ||
|
|
7656443b28 | ||
|
|
9d91c13b12 | ||
|
|
7c06141ce2 | ||
|
|
3dc413638b | ||
|
|
bdb8baeddd | ||
|
|
21966bfb69 | ||
|
|
e78c82e999 | ||
|
|
2bdc3468d1 | ||
|
|
987b3dc4ef | ||
|
|
45a10b4ac7 | ||
|
|
b5d33ef629 | ||
|
|
d3629916bf | ||
|
|
c5cb26d295 | ||
|
|
4b2785c5eb | ||
|
|
7ed190e6d2 | ||
|
|
eac041cdd2 | ||
|
|
05527cfc01 | ||
|
|
61e2af4a14 | ||
|
|
79804b6ecd | ||
|
|
76434b2f4e | ||
|
|
ec8bd4922e | ||
|
|
4ffa773fac | ||
|
|
ea8b7bc8aa | ||
|
|
39ce5646f6 | ||
|
|
5092a82739 | ||
|
|
3bba0b6d9a | ||
|
|
7a19dd503d | ||
|
|
9e6a01fefd | ||
|
|
933471b4d9 | ||
|
|
f81808d239 | ||
|
|
96832b6f7d | ||
|
|
e2eb0a84b0 | ||
|
|
c8eb2e3376 | ||
|
|
21fe5822f9 | ||
|
|
d49cc9a7a3 | ||
|
|
910d0bfae1 | ||
|
|
d6761949ca | ||
|
|
6afac1f593 | ||
|
|
4d1a270d22 | ||
|
|
a7888f5536 | ||
|
|
b9049e91cf | ||
|
|
7db56c8e77 | ||
|
|
50563cb957 | ||
|
|
18ae2299a7 | ||
|
|
7463e0aab9 | ||
|
|
c92d47bb95 | ||
|
|
0b1af7df91 | ||
|
|
a9104eb2da | ||
|
|
abbd15d5cc | ||
|
|
aadfa14d59 | ||
|
|
4cd10bbe25 | ||
|
|
1d4a6b71ab | ||
|
|
a7f830dd73 | ||
|
|
bae86ac05c | ||
|
|
a3706bfe21 | ||
|
|
91e23b8c11 | ||
|
|
37ef1c9fab | ||
|
|
6bc6f77af1 | ||
|
|
2c478ccc25 | ||
|
|
404e5492a3 | ||
|
|
d5b5d667a5 | ||
|
|
8807f02f36 | ||
|
|
269e561497 | ||
|
|
527ad81d38 | ||
|
|
972d3c18af | ||
|
|
3cbfc078fc | ||
|
|
fde6822b5c | ||
|
|
930321bcf1 | ||
|
|
c45931363a | ||
|
|
9c6491e5ee | ||
|
|
9bc248f5bc | ||
|
|
becac2fde5 | ||
|
|
1e1a103882 |
34
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
34
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -3,17 +3,6 @@ description: 报错或漏洞请使用这个模板创建,不使用此模板创
|
||||
title: "[Bug]: "
|
||||
labels: ["bug?"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 部署方式
|
||||
description: "主程序使用的部署方式"
|
||||
options:
|
||||
- 手动部署
|
||||
- 安装器部署
|
||||
- 一键安装包部署
|
||||
- Docker部署
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 消息平台适配器
|
||||
@@ -27,37 +16,24 @@ body:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: 系统环境
|
||||
description: 操作系统、系统架构、**主机地理位置**,地理位置最好写清楚,涉及网络问题排查。
|
||||
placeholder: 例如: CentOS x64 中国大陆、Windows11 美国
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Python环境
|
||||
description: 运行程序的Python版本
|
||||
placeholder: 例如: Python 3.10
|
||||
label: 运行环境
|
||||
description: 操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||
placeholder: 例如: CentOS x64 Python 3.10.3、Docker 的直接写 Docker 就行
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: QChatGPT版本
|
||||
description: QChatGPT版本号
|
||||
placeholder: 例如: v2.6.0,可以使用`!version`命令查看
|
||||
placeholder: 例如:v3.3.0,可以使用`!version`命令查看,或者到 pkg/utils/constants.py 查看
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 异常情况
|
||||
description: 完整描述异常情况,什么时候发生的、发生了什么,尽可能详细
|
||||
description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。**
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志信息
|
||||
description: 请提供完整的 **登录框架 和 QChatGPT控制台**的相关日志信息(若有),不提供日志信息**无法**为您排查问题,请尽可能详细
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 启用的插件
|
||||
|
||||
26
.github/pull_request_template.md
vendored
26
.github/pull_request_template.md
vendored
@@ -2,24 +2,16 @@
|
||||
|
||||
实现/解决/优化的内容:
|
||||
|
||||
### 事务
|
||||
## 检查清单
|
||||
|
||||
- [ ] 已阅读仓库[贡献指引](https://github.com/RockChinQ/QChatGPT/blob/master/CONTRIBUTING.md)
|
||||
- [ ] 已与维护者在issues或其他平台沟通此PR大致内容
|
||||
### PR 作者完成
|
||||
|
||||
## 以下内容可在起草PR后、合并PR前逐步完成
|
||||
- [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/QChatGPT/blob/master/CONTRIBUTING.md)了吗?
|
||||
- [ ] 与项目所有者沟通过了吗?
|
||||
|
||||
### 功能
|
||||
### 项目所有者完成
|
||||
|
||||
- [ ] 已编写完善的配置文件字段说明(若有新增)
|
||||
- [ ] 已编写面向用户的新功能说明(若有必要)
|
||||
- [ ] 已测试新功能或更改
|
||||
|
||||
### 兼容性
|
||||
|
||||
- [ ] 已处理版本兼容性
|
||||
- [ ] 已处理插件兼容问题
|
||||
|
||||
### 风险
|
||||
|
||||
可能导致或已知的问题:
|
||||
- [ ] 相关 issues 链接了吗?
|
||||
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗?
|
||||
- [ ] 依赖写到 requirements.txt 和 core/bootutils/deps.py 了吗
|
||||
- [ ] 文档编写了吗?
|
||||
10
README.md
10
README.md
@@ -2,11 +2,12 @@
|
||||
<p align="center">
|
||||
<img src="https://qchatgpt.rockchin.top/logo.png" alt="QChatGPT" width="180" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
# QChatGPT
|
||||
|
||||
<a href="https://trendshift.io/repositories/6187" target="_blank"><img src="https://trendshift.io/api/badge/repositories/6187" alt="RockChinQ%2FQChatGPT | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[](https://github.com/RockChinQ/QChatGPT/releases/latest)
|
||||
<a href="https://hub.docker.com/repository/docker/rockchin/qchatgpt">
|
||||
<img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull">
|
||||
@@ -18,8 +19,8 @@
|
||||
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=66-aWvn8cbP4c1ut_1YYkvvGVeEtyTH8&authKey=pTaKBK5C%2B8dFzQ4XlENf6MHTCLaHnlKcCRx7c14EeVVlpX2nRSaS8lJm8YeM4mCU&noverify=0&group_code=195992197">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/%E5%AE%98%E6%96%B9%E7%BE%A4-195992197-purple">
|
||||
</a>
|
||||
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nC80H57wmKPwRDLFeQrDDjVl81XuC21P&authKey=2wTUTfoQ5v%2BD4C5zfpuR%2BSPMDqdXgDXA%2FS2wHI1NxTfWIG%2B%2FqK08dgyjMMOzhXa9&noverify=0&group_code=248432104">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA%E7%BE%A4-248432104-purple">
|
||||
<a href="https://qm.qq.com/q/PClALFK242">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA%E7%BE%A4-619154800-purple">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/RockChinQ/QChatGPT" >
|
||||
<img src="https://codecov.io/gh/RockChinQ/QChatGPT/graph/badge.svg?token=pjxYIL2kbC"/>
|
||||
@@ -38,7 +39,8 @@
|
||||
|
||||
<a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a> |
|
||||
<a href="https://github.com/RockChinQ/qcg-tester">测试工程源码</a> |
|
||||
<a href="https://github.com/RockChinQ/qcg-center">遥测服务端源码</a> |
|
||||
<a href="https://github.com/the-lazy-me/QChatGPT-Wiki">官方文档储存库</a>
|
||||
|
||||
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
|
||||
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-0516.png" width="500px"/>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,6 @@ from .groups import plugin
|
||||
from ...core import app
|
||||
|
||||
|
||||
BACKEND_URL = "https://api.qchatgpt.rockchin.top/api/v2"
|
||||
|
||||
class V2CenterAPI:
|
||||
"""中央服务器 v2 API 交互类"""
|
||||
|
||||
@@ -23,7 +21,7 @@ class V2CenterAPI:
|
||||
plugin: plugin.V2PluginDataAPI = None
|
||||
"""插件 API 组"""
|
||||
|
||||
def __init__(self, ap: app.Application, basic_info: dict = None, runtime_info: dict = None):
|
||||
def __init__(self, ap: app.Application, backend_url: str, basic_info: dict = None, runtime_info: dict = None):
|
||||
"""初始化"""
|
||||
|
||||
logging.debug("basic_info: %s, runtime_info: %s", basic_info, runtime_info)
|
||||
@@ -31,7 +29,7 @@ class V2CenterAPI:
|
||||
apigroup.APIGroup._basic_info = basic_info
|
||||
apigroup.APIGroup._runtime_info = runtime_info
|
||||
|
||||
self.main = main.V2MainDataAPI(BACKEND_URL, ap)
|
||||
self.usage = usage.V2UsageDataAPI(BACKEND_URL, ap)
|
||||
self.plugin = plugin.V2PluginDataAPI(BACKEND_URL, ap)
|
||||
self.main = main.V2MainDataAPI(backend_url, ap)
|
||||
self.usage = usage.V2UsageDataAPI(backend_url, ap)
|
||||
self.plugin = plugin.V2PluginDataAPI(backend_url, ap)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from . import entities, operator, errors
|
||||
from ..config import manager as cfg_mgr
|
||||
|
||||
# 引入所有算子以便注册
|
||||
from .operators import func, plugin, default, reset, list as list_cmd, last, next, delc, resend, prompt, cmd, help, version, update
|
||||
from .operators import func, plugin, default, reset, list as list_cmd, last, next, delc, resend, prompt, cmd, help, version, update, ollama, model
|
||||
|
||||
|
||||
class CommandManager:
|
||||
|
||||
@@ -13,11 +13,16 @@ class CommandReturn(pydantic.BaseModel):
|
||||
"""命令返回值
|
||||
"""
|
||||
|
||||
text: typing.Optional[str]
|
||||
text: typing.Optional[str] = None
|
||||
"""文本
|
||||
"""
|
||||
|
||||
image: typing.Optional[mirai.Image]
|
||||
image: typing.Optional[mirai.Image] = None
|
||||
"""弃用"""
|
||||
|
||||
image_url: typing.Optional[str] = None
|
||||
"""图片链接
|
||||
"""
|
||||
|
||||
error: typing.Optional[errors.CommandError]= None
|
||||
"""错误
|
||||
|
||||
@@ -24,7 +24,7 @@ class DefaultOperator(operator.CommandOperator):
|
||||
|
||||
content = ""
|
||||
for msg in prompt.messages:
|
||||
content += f" {msg.role}: {msg.content}"
|
||||
content += f" {msg.readable_str()}\n"
|
||||
|
||||
reply_str += f"名称: {prompt.name}\n内容: \n{content}\n\n"
|
||||
|
||||
@@ -45,18 +45,18 @@ class DefaultSetOperator(operator.CommandOperator):
|
||||
context: entities.ExecuteContext
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供情景预设名称'))
|
||||
else:
|
||||
prompt_name = context.crt_params[0]
|
||||
|
||||
try:
|
||||
prompt = await self.ap.prompt_mgr.get_prompt_by_prefix(prompt_name)
|
||||
if prompt is None:
|
||||
yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: 未找到情景预设 {}".format(prompt_name)))
|
||||
else:
|
||||
context.session.use_prompt_name = prompt.name
|
||||
yield entities.CommandReturn(text=f"已设置当前会话默认情景预设为 {prompt_name}, !reset 后生效")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: "+str(e)))
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供情景预设名称'))
|
||||
else:
|
||||
prompt_name = context.crt_params[0]
|
||||
|
||||
try:
|
||||
prompt = await self.ap.prompt_mgr.get_prompt_by_prefix(prompt_name)
|
||||
if prompt is None:
|
||||
yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: 未找到情景预设 {}".format(prompt_name)))
|
||||
else:
|
||||
context.session.use_prompt_name = prompt.name
|
||||
yield entities.CommandReturn(text=f"已设置当前会话默认情景预设为 {prompt_name}, !reset 后生效")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: "+str(e)))
|
||||
|
||||
@@ -30,7 +30,7 @@ class LastOperator(operator.CommandOperator):
|
||||
context.session.using_conversation = context.session.conversations[index-1]
|
||||
time_str = context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
yield entities.CommandReturn(text=f"已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}")
|
||||
yield entities.CommandReturn(text=f"已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}")
|
||||
return
|
||||
else:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
||||
@@ -42,7 +42,7 @@ class ListOperator(operator.CommandOperator):
|
||||
using_conv_index = index
|
||||
|
||||
if index >= page * record_per_page and index < (page + 1) * record_per_page:
|
||||
content += f"{index} {time_str}: {conv.messages[0].content if len(conv.messages) > 0 else '无内容'}\n"
|
||||
content += f"{index} {time_str}: {conv.messages[0].readable_str() if len(conv.messages) > 0 else '无内容'}\n"
|
||||
index += 1
|
||||
|
||||
if content == '':
|
||||
@@ -51,6 +51,6 @@ class ListOperator(operator.CommandOperator):
|
||||
if context.session.using_conversation is None:
|
||||
content += "\n当前处于新会话"
|
||||
else:
|
||||
content += f"\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')}: {context.session.using_conversation.messages[0].content if len(context.session.using_conversation.messages) > 0 else '无内容'}"
|
||||
content += f"\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else '无内容'}"
|
||||
|
||||
yield entities.CommandReturn(text=f"第 {page + 1} 页 (时间倒序):\n{content}")
|
||||
|
||||
86
pkg/command/operators/model.py
Normal file
86
pkg/command/operators/model.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, cmdmgr, errors
|
||||
|
||||
@operator.operator_class(
|
||||
name="model",
|
||||
help='显示和切换模型列表',
|
||||
usage='!model\n!model show <模型名>\n!model set <模型名>',
|
||||
privilege=2
|
||||
)
|
||||
class ModelOperator(operator.CommandOperator):
|
||||
"""Model命令"""
|
||||
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
content = '模型列表:\n'
|
||||
|
||||
model_list = self.ap.model_mgr.model_list
|
||||
|
||||
for model in model_list:
|
||||
content += f"\n名称: {model.name}\n"
|
||||
content += f"请求器: {model.requester.name}\n"
|
||||
|
||||
content += f"\n当前对话使用模型: {context.query.use_model.name}\n"
|
||||
content += f"新对话默认使用模型: {self.ap.provider_cfg.data.get('model')}\n"
|
||||
|
||||
yield entities.CommandReturn(text=content.strip())
|
||||
|
||||
|
||||
@operator.operator_class(
|
||||
name="show",
|
||||
help='显示模型详情',
|
||||
privilege=2,
|
||||
parent_class=ModelOperator
|
||||
)
|
||||
class ModelShowOperator(operator.CommandOperator):
|
||||
"""Model Show命令"""
|
||||
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
model_name = context.crt_params[0]
|
||||
|
||||
model = None
|
||||
for _model in self.ap.model_mgr.model_list:
|
||||
if model_name == _model.name:
|
||||
model = _model
|
||||
break
|
||||
|
||||
if model is None:
|
||||
yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}"))
|
||||
else:
|
||||
content = f"模型详情\n"
|
||||
content += f"名称: {model.name}\n"
|
||||
if model.model_name is not None:
|
||||
content += f"请求模型名称: {model.model_name}\n"
|
||||
content += f"请求器: {model.requester.name}\n"
|
||||
content += f"密钥组: {model.token_mgr.provider}\n"
|
||||
content += f"支持视觉: {model.vision_supported}\n"
|
||||
content += f"支持工具: {model.tool_call_supported}\n"
|
||||
|
||||
yield entities.CommandReturn(text=content.strip())
|
||||
|
||||
@operator.operator_class(
|
||||
name="set",
|
||||
help='设置默认使用模型',
|
||||
privilege=2,
|
||||
parent_class=ModelOperator
|
||||
)
|
||||
class ModelSetOperator(operator.CommandOperator):
|
||||
"""Model Set命令"""
|
||||
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
model_name = context.crt_params[0]
|
||||
|
||||
model = None
|
||||
for _model in self.ap.model_mgr.model_list:
|
||||
if model_name == _model.name:
|
||||
model = _model
|
||||
break
|
||||
|
||||
if model is None:
|
||||
yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}"))
|
||||
else:
|
||||
self.ap.provider_cfg.data['model'] = model_name
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
yield entities.CommandReturn(text=f"已设置当前使用模型为 {model_name},重置会话以生效")
|
||||
121
pkg/command/operators/ollama.py
Normal file
121
pkg/command/operators/ollama.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import typing
|
||||
import traceback
|
||||
|
||||
import ollama
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(
|
||||
name="ollama",
|
||||
help="ollama平台操作",
|
||||
usage="!ollama\n!ollama show <模型名>\n!ollama pull <模型名>\n!ollama del <模型名>"
|
||||
)
|
||||
class OllamaOperator(operator.CommandOperator):
|
||||
async def execute(
|
||||
self, context: entities.ExecuteContext
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
try:
|
||||
content: str = '模型列表:\n'
|
||||
model_list: list = ollama.list().get('models', [])
|
||||
for model in model_list:
|
||||
content += f"名称: {model['name']}\n"
|
||||
content += f"修改时间: {model['modified_at']}\n"
|
||||
content += f"大小: {bytes_to_mb(model['size'])}MB\n\n"
|
||||
yield entities.CommandReturn(text=f"{content.strip()}")
|
||||
except ollama.ResponseError as e:
|
||||
yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常"))
|
||||
|
||||
|
||||
def bytes_to_mb(num_bytes):
|
||||
mb: float = num_bytes / 1024 / 1024
|
||||
return format(mb, '.2f')
|
||||
|
||||
|
||||
@operator.operator_class(
|
||||
name="show",
|
||||
help="ollama模型详情",
|
||||
privilege=2,
|
||||
parent_class=OllamaOperator
|
||||
)
|
||||
class OllamaShowOperator(operator.CommandOperator):
|
||||
async def execute(
|
||||
self, context: entities.ExecuteContext
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
content: str = '模型详情:\n'
|
||||
try:
|
||||
show: dict = ollama.show(model=context.crt_params[0])
|
||||
model_info: dict = show.get('model_info', {})
|
||||
ignore_show: str = 'too long to show...'
|
||||
|
||||
for key in ['license', 'modelfile']:
|
||||
show[key] = ignore_show
|
||||
|
||||
for key in ['tokenizer.chat_template.rag', 'tokenizer.chat_template.tool_use']:
|
||||
model_info[key] = ignore_show
|
||||
|
||||
content += json.dumps(show, indent=4)
|
||||
yield entities.CommandReturn(text=content.strip())
|
||||
except ollama.ResponseError as e:
|
||||
yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型详情,请确认 Ollama 服务正常"))
|
||||
|
||||
@operator.operator_class(
|
||||
name="pull",
|
||||
help="ollama模型拉取",
|
||||
privilege=2,
|
||||
parent_class=OllamaOperator
|
||||
)
|
||||
class OllamaPullOperator(operator.CommandOperator):
|
||||
async def execute(
|
||||
self, context: entities.ExecuteContext
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
try:
|
||||
model_list: list = ollama.list().get('models', [])
|
||||
if context.crt_params[0] in [model['name'] for model in model_list]:
|
||||
yield entities.CommandReturn(text="模型已存在")
|
||||
return
|
||||
except ollama.ResponseError as e:
|
||||
yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常"))
|
||||
return
|
||||
|
||||
on_progress: bool = False
|
||||
progress_count: int = 0
|
||||
try:
|
||||
for resp in ollama.pull(model=context.crt_params[0], stream=True):
|
||||
total: typing.Any = resp.get('total')
|
||||
if not on_progress:
|
||||
if total is not None:
|
||||
on_progress = True
|
||||
yield entities.CommandReturn(text=resp.get('status'))
|
||||
else:
|
||||
if total is None:
|
||||
on_progress = False
|
||||
|
||||
completed: typing.Any = resp.get('completed')
|
||||
if isinstance(completed, int) and isinstance(total, int):
|
||||
percentage_completed = (completed / total) * 100
|
||||
if percentage_completed > progress_count:
|
||||
progress_count += 10
|
||||
yield entities.CommandReturn(
|
||||
text=f"下载进度: {completed}/{total} ({percentage_completed:.2f}%)")
|
||||
except ollama.ResponseError as e:
|
||||
yield entities.CommandReturn(text=f"拉取失败: {e.error}")
|
||||
|
||||
|
||||
@operator.operator_class(
|
||||
name="del",
|
||||
help="ollama模型删除",
|
||||
privilege=2,
|
||||
parent_class=OllamaOperator
|
||||
)
|
||||
class OllamaDelOperator(operator.CommandOperator):
|
||||
async def execute(
|
||||
self, context: entities.ExecuteContext
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
try:
|
||||
ret: str = ollama.delete(model=context.crt_params[0])['status']
|
||||
except ollama.ResponseError as e:
|
||||
ret = f"{e.error}"
|
||||
yield entities.CommandReturn(text=ret)
|
||||
@@ -20,7 +20,7 @@ class VersionCommand(operator.CommandOperator):
|
||||
|
||||
try:
|
||||
if await self.ap.ver_mgr.is_new_version_available():
|
||||
reply_str += "\n\n有新版本可用, 使用 !update 更新"
|
||||
reply_str += "\n\n有新版本可用。"
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class JSONConfigFile(file_model.ConfigFile):
|
||||
else:
|
||||
raise ValueError("template_file_name or template_data must be provided")
|
||||
|
||||
async def load(self) -> dict:
|
||||
async def load(self, completion: bool=True) -> dict:
|
||||
|
||||
if not self.exists():
|
||||
await self.create()
|
||||
@@ -37,11 +37,16 @@ class JSONConfigFile(file_model.ConfigFile):
|
||||
self.template_data = json.load(f)
|
||||
|
||||
with open(self.config_file_name, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
try:
|
||||
cfg = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"配置文件 {self.config_file_name} 语法错误: {e}")
|
||||
|
||||
for key in self.template_data:
|
||||
if key not in cfg:
|
||||
cfg[key] = self.template_data[key]
|
||||
if completion:
|
||||
|
||||
for key in self.template_data:
|
||||
if key not in cfg:
|
||||
cfg[key] = self.template_data[key]
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class PythonModuleConfigFile(file_model.ConfigFile):
|
||||
async def create(self):
|
||||
shutil.copyfile(self.template_file_name, self.config_file_name)
|
||||
|
||||
async def load(self) -> dict:
|
||||
async def load(self, completion: bool=True) -> dict:
|
||||
module_name = os.path.splitext(os.path.basename(self.config_file_name))[0]
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
@@ -43,18 +43,19 @@ class PythonModuleConfigFile(file_model.ConfigFile):
|
||||
cfg[key] = getattr(module, key)
|
||||
|
||||
# 从模板模块文件中进行补全
|
||||
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
|
||||
module = importlib.import_module(module_name)
|
||||
if completion:
|
||||
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
for key in dir(module):
|
||||
if key.startswith('__'):
|
||||
continue
|
||||
for key in dir(module):
|
||||
if key.startswith('__'):
|
||||
continue
|
||||
|
||||
if not isinstance(getattr(module, key), allowed_types):
|
||||
continue
|
||||
if not isinstance(getattr(module, key), allowed_types):
|
||||
continue
|
||||
|
||||
if key not in cfg:
|
||||
cfg[key] = getattr(module, key)
|
||||
if key not in cfg:
|
||||
cfg[key] = getattr(module, key)
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ class ConfigManager:
|
||||
self.file = cfg_file
|
||||
self.data = {}
|
||||
|
||||
async def load_config(self):
|
||||
self.data = await self.file.load()
|
||||
async def load_config(self, completion: bool=True):
|
||||
self.data = await self.file.load(completion=completion)
|
||||
|
||||
async def dump_config(self):
|
||||
await self.file.save(self.data)
|
||||
@@ -30,7 +30,7 @@ class ConfigManager:
|
||||
self.file.save_sync(self.data)
|
||||
|
||||
|
||||
async def load_python_module_config(config_name: str, template_name: str) -> ConfigManager:
|
||||
async def load_python_module_config(config_name: str, template_name: str, completion: bool=True) -> ConfigManager:
|
||||
"""加载Python模块配置文件"""
|
||||
cfg_inst = pymodule.PythonModuleConfigFile(
|
||||
config_name,
|
||||
@@ -38,12 +38,12 @@ async def load_python_module_config(config_name: str, template_name: str) -> Con
|
||||
)
|
||||
|
||||
cfg_mgr = ConfigManager(cfg_inst)
|
||||
await cfg_mgr.load_config()
|
||||
await cfg_mgr.load_config(completion=completion)
|
||||
|
||||
return cfg_mgr
|
||||
|
||||
|
||||
async def load_json_config(config_name: str, template_name: str=None, template_data: dict=None) -> ConfigManager:
|
||||
async def load_json_config(config_name: str, template_name: str=None, template_data: dict=None, completion: bool=True) -> ConfigManager:
|
||||
"""加载JSON配置文件"""
|
||||
cfg_inst = json_file.JSONConfigFile(
|
||||
config_name,
|
||||
@@ -52,6 +52,6 @@ async def load_json_config(config_name: str, template_name: str=None, template_d
|
||||
)
|
||||
|
||||
cfg_mgr = ConfigManager(cfg_inst)
|
||||
await cfg_mgr.load_config()
|
||||
await cfg_mgr.load_config(completion=completion)
|
||||
|
||||
return cfg_mgr
|
||||
@@ -22,7 +22,7 @@ class ConfigFile(metaclass=abc.ABCMeta):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def load(self) -> dict:
|
||||
async def load(self, completion: bool=True) -> dict:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -9,13 +9,14 @@ from ..provider.session import sessionmgr as llm_session_mgr
|
||||
from ..provider.modelmgr import modelmgr as llm_model_mgr
|
||||
from ..provider.sysprompt import sysprompt as llm_prompt_mgr
|
||||
from ..provider.tools import toolmgr as llm_tool_mgr
|
||||
from ..provider import runnermgr
|
||||
from ..config import manager as config_mgr
|
||||
from ..audit.center import v2 as center_mgr
|
||||
from ..command import cmdmgr
|
||||
from ..plugin import manager as plugin_mgr
|
||||
from ..pipeline import pool
|
||||
from ..pipeline import controller, stagemgr
|
||||
from ..utils import version as version_mgr, proxy as proxy_mgr
|
||||
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
|
||||
|
||||
|
||||
class Application:
|
||||
@@ -33,6 +34,8 @@ class Application:
|
||||
|
||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
||||
|
||||
runner_mgr: runnermgr.RunnerManager = None
|
||||
|
||||
# ======= 配置管理器 =======
|
||||
|
||||
command_cfg: config_mgr.ConfigManager = None
|
||||
@@ -69,6 +72,8 @@ class Application:
|
||||
|
||||
ver_mgr: version_mgr.VersionManager = None
|
||||
|
||||
ann_mgr: announce_mgr.AnnouncementManager = None
|
||||
|
||||
proxy_mgr: proxy_mgr.ProxyManager = None
|
||||
|
||||
logger: logging.Logger = None
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import traceback
|
||||
|
||||
from . import app
|
||||
from ..audit import identifier
|
||||
from . import stage
|
||||
|
||||
# 引入启动阶段实现以便注册
|
||||
from .stages import load_config, setup_logger, build_app, migrate
|
||||
from .stages import load_config, setup_logger, build_app, migrate, show_notes
|
||||
|
||||
|
||||
stage_order = [
|
||||
"LoadConfigStage",
|
||||
"MigrationStage",
|
||||
"SetupLoggerStage",
|
||||
"BuildAppStage"
|
||||
"BuildAppStage",
|
||||
"ShowNotesStage"
|
||||
]
|
||||
|
||||
|
||||
@@ -27,6 +30,7 @@ async def make_app() -> app.Application:
|
||||
for stage_name in stage_order:
|
||||
stage_cls = stage.preregistered_stages[stage_name]
|
||||
stage_inst = stage_cls()
|
||||
|
||||
await stage_inst.run(ap)
|
||||
|
||||
await ap.initialize()
|
||||
@@ -35,5 +39,8 @@ async def make_app() -> app.Application:
|
||||
|
||||
|
||||
async def main():
|
||||
app_inst = await make_app()
|
||||
await app_inst.run()
|
||||
try:
|
||||
app_inst = await make_app()
|
||||
await app_inst.run()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -8,16 +8,3 @@ from ...config.impls import pymodule
|
||||
|
||||
load_python_module_config = config_mgr.load_python_module_config
|
||||
load_json_config = config_mgr.load_json_config
|
||||
|
||||
|
||||
async def override_config_manager(cfg_mgr: config_mgr.ConfigManager) -> list[str]:
|
||||
override_json = json.load(open("override.json", "r", encoding="utf-8"))
|
||||
overrided = []
|
||||
|
||||
config = cfg_mgr.data
|
||||
for key in override_json:
|
||||
if key in config:
|
||||
config[key] = override_json[key]
|
||||
overrided.append(key)
|
||||
|
||||
return overrided
|
||||
|
||||
@@ -14,6 +14,8 @@ required_deps = {
|
||||
"yaml": "pyyaml",
|
||||
"aiohttp": "aiohttp",
|
||||
"psutil": "psutil",
|
||||
"async_lru": "async-lru",
|
||||
"ollama": "ollama",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -67,12 +67,15 @@ class Query(pydantic.BaseModel):
|
||||
use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None
|
||||
"""使用的函数,由前置处理器阶段设置"""
|
||||
|
||||
resp_messages: typing.Optional[list[llm_entities.Message]] = []
|
||||
resp_messages: typing.Optional[list[llm_entities.Message]] | typing.Optional[list[mirai.MessageChain]] = []
|
||||
"""由Process阶段生成的回复消息对象列表"""
|
||||
|
||||
resp_message_chain: typing.Optional[mirai.MessageChain] = None
|
||||
resp_message_chain: typing.Optional[list[mirai.MessageChain]] = None
|
||||
"""回复消息链,从resp_messages包装而得"""
|
||||
|
||||
# ======= 内部保留 =======
|
||||
current_stage: "pkg.pipeline.stagemgr.StageInstContainer" = None
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from ..core import app
|
||||
from . import app
|
||||
|
||||
|
||||
preregistered_migrations: list[typing.Type[Migration]] = []
|
||||
30
pkg/core/migrations/m005_deepseek_cfg_completion.py
Normal file
30
pkg/core/migrations/m005_deepseek_cfg_completion.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("deepseek-config-completion", 5)
|
||||
class DeepseekConfigCompletionMigration(migration.Migration):
|
||||
"""OpenAI配置迁移
|
||||
"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移
|
||||
"""
|
||||
return 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester'] \
|
||||
or 'deepseek' not in self.ap.provider_cfg.data['keys']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移
|
||||
"""
|
||||
if 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['deepseek-chat-completions'] = {
|
||||
'base-url': 'https://api.deepseek.com',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
if 'deepseek' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['deepseek'] = []
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
19
pkg/core/migrations/m006_vision_config.py
Normal file
19
pkg/core/migrations/m006_vision_config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("vision-config", 6)
|
||||
class VisionConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return "enable-vision" not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if "enable-vision" not in self.ap.provider_cfg.data:
|
||||
self.ap.provider_cfg.data["enable-vision"] = False
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
20
pkg/core/migrations/m007_qcg_center_url.py
Normal file
20
pkg/core/migrations/m007_qcg_center_url.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("qcg-center-url-config", 7)
|
||||
class QCGCenterURLConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return "qcg-center-url" not in self.ap.system_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
if "qcg-center-url" not in self.ap.system_cfg.data:
|
||||
self.ap.system_cfg.data["qcg-center-url"] = "https://api.qchatgpt.rockchin.top/api/v2"
|
||||
|
||||
await self.ap.system_cfg.dump_config()
|
||||
29
pkg/core/migrations/m008_ad_fixwin_config_migrate.py
Normal file
29
pkg/core/migrations/m008_ad_fixwin_config_migrate.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("ad-fixwin-cfg-migration", 8)
|
||||
class AdFixwinConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return isinstance(
|
||||
self.ap.pipeline_cfg.data["rate-limit"]["fixwin"]["default"],
|
||||
int
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
for session_name in self.ap.pipeline_cfg.data["rate-limit"]["fixwin"]:
|
||||
|
||||
temp_dict = {
|
||||
"window-size": 60,
|
||||
"limit": self.ap.pipeline_cfg.data["rate-limit"]["fixwin"][session_name]
|
||||
}
|
||||
|
||||
self.ap.pipeline_cfg.data["rate-limit"]["fixwin"][session_name] = temp_dict
|
||||
|
||||
await self.ap.pipeline_cfg.dump_config()
|
||||
24
pkg/core/migrations/m009_msg_truncator_cfg.py
Normal file
24
pkg/core/migrations/m009_msg_truncator_cfg.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("msg-truncator-cfg-migration", 9)
|
||||
class MsgTruncatorConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'msg-truncate' not in self.ap.pipeline_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.pipeline_cfg.data['msg-truncate'] = {
|
||||
'method': 'round',
|
||||
'round': {
|
||||
'max-round': 10
|
||||
}
|
||||
}
|
||||
|
||||
await self.ap.pipeline_cfg.dump_config()
|
||||
23
pkg/core/migrations/m010_ollama_requester_config.py
Normal file
23
pkg/core/migrations/m010_ollama_requester_config.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("ollama-requester-config", 10)
|
||||
class MsgTruncatorConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'ollama-chat' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.provider_cfg.data['requester']['ollama-chat'] = {
|
||||
"base-url": "http://127.0.0.1:11434",
|
||||
"args": {},
|
||||
"timeout": 600
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
21
pkg/core/migrations/m011_command_prefix_config.py
Normal file
21
pkg/core/migrations/m011_command_prefix_config.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("command-prefix-config", 11)
|
||||
class CommandPrefixConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'command-prefix' not in self.ap.command_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.command_cfg.data['command-prefix'] = [
|
||||
"!", "!"
|
||||
]
|
||||
|
||||
await self.ap.command_cfg.dump_config()
|
||||
19
pkg/core/migrations/m012_runner_config.py
Normal file
19
pkg/core/migrations/m012_runner_config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("runner-config", 12)
|
||||
class RunnerConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'runner' not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.provider_cfg.data['runner'] = 'local-agent'
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
44
pkg/core/note.py
Normal file
44
pkg/core/note.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from . import app
|
||||
|
||||
preregistered_notes: list[typing.Type[LaunchNote]] = []
|
||||
|
||||
def note_class(name: str, number: int):
|
||||
"""注册一个启动信息
|
||||
"""
|
||||
def decorator(cls: typing.Type[LaunchNote]) -> typing.Type[LaunchNote]:
|
||||
cls.name = name
|
||||
cls.number = number
|
||||
preregistered_notes.append(cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class LaunchNote(abc.ABC):
|
||||
"""启动信息
|
||||
"""
|
||||
name: str
|
||||
|
||||
number: int
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
@abc.abstractmethod
|
||||
async def need_show(self) -> bool:
|
||||
"""判断当前环境是否需要显示此启动信息
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
"""生成启动信息
|
||||
"""
|
||||
pass
|
||||
0
pkg/core/notes/__init__.py
Normal file
0
pkg/core/notes/__init__.py
Normal file
20
pkg/core/notes/n001_classic_msgs.py
Normal file
20
pkg/core/notes/n001_classic_msgs.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import note, app
|
||||
|
||||
|
||||
@note.note_class("ClassicNotes", 1)
|
||||
class ClassicNotes(note.LaunchNote):
|
||||
"""经典启动信息
|
||||
"""
|
||||
|
||||
async def need_show(self) -> bool:
|
||||
return True
|
||||
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
|
||||
yield await self.ap.ann_mgr.show_announcements()
|
||||
|
||||
yield await self.ap.ver_mgr.show_version_update()
|
||||
21
pkg/core/notes/n002_selection_mode_on_windows.py
Normal file
21
pkg/core/notes/n002_selection_mode_on_windows.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from .. import note, app
|
||||
|
||||
|
||||
@note.note_class("SelectionModeOnWindows", 2)
|
||||
class SelectionModeOnWindows(note.LaunchNote):
|
||||
"""Windows 上的选择模式提示信息
|
||||
"""
|
||||
|
||||
async def need_show(self) -> bool:
|
||||
return os.name == 'nt'
|
||||
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
|
||||
yield """您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""", logging.INFO
|
||||
@@ -13,9 +13,9 @@ from ...provider.session import sessionmgr as llm_session_mgr
|
||||
from ...provider.modelmgr import modelmgr as llm_model_mgr
|
||||
from ...provider.sysprompt import sysprompt as llm_prompt_mgr
|
||||
from ...provider.tools import toolmgr as llm_tool_mgr
|
||||
from ...provider import runnermgr
|
||||
from ...platform import manager as im_mgr
|
||||
|
||||
|
||||
@stage.stage_class("BuildAppStage")
|
||||
class BuildAppStage(stage.BootingStage):
|
||||
"""构建应用阶段
|
||||
@@ -35,6 +35,7 @@ class BuildAppStage(stage.BootingStage):
|
||||
|
||||
center_v2_api = center_v2.V2CenterAPI(
|
||||
ap,
|
||||
backend_url=ap.system_cfg.data["qcg-center-url"],
|
||||
basic_info={
|
||||
"host_id": identifier.identifier["host_id"],
|
||||
"instance_id": identifier.identifier["instance_id"],
|
||||
@@ -53,12 +54,10 @@ class BuildAppStage(stage.BootingStage):
|
||||
|
||||
# 发送公告
|
||||
ann_mgr = announce.AnnouncementManager(ap)
|
||||
await ann_mgr.show_announcements()
|
||||
ap.ann_mgr = ann_mgr
|
||||
|
||||
ap.query_pool = pool.QueryPool()
|
||||
|
||||
await ap.ver_mgr.show_version_update()
|
||||
|
||||
plugin_mgr_inst = plugin_mgr.PluginManager(ap)
|
||||
await plugin_mgr_inst.initialize()
|
||||
ap.plugin_mgr = plugin_mgr_inst
|
||||
@@ -84,6 +83,10 @@ class BuildAppStage(stage.BootingStage):
|
||||
await llm_tool_mgr_inst.initialize()
|
||||
ap.tool_mgr = llm_tool_mgr_inst
|
||||
|
||||
runner_mgr_inst = runnermgr.RunnerManager(ap)
|
||||
await runner_mgr_inst.initialize()
|
||||
ap.runner_mgr = runner_mgr_inst
|
||||
|
||||
im_mgr_inst = im_mgr.PlatformManager(ap=ap)
|
||||
await im_mgr_inst.initialize()
|
||||
ap.platform_mgr = im_mgr_inst
|
||||
@@ -92,5 +95,6 @@ class BuildAppStage(stage.BootingStage):
|
||||
await stage_mgr.initialize()
|
||||
ap.stage_mgr = stage_mgr
|
||||
|
||||
|
||||
ctrl = controller.Controller(ap)
|
||||
ap.ctrl = ctrl
|
||||
|
||||
@@ -12,11 +12,11 @@ class LoadConfigStage(stage.BootingStage):
|
||||
async def run(self, ap: app.Application):
|
||||
"""启动
|
||||
"""
|
||||
ap.command_cfg = await config.load_json_config("data/config/command.json", "templates/command.json")
|
||||
ap.pipeline_cfg = await config.load_json_config("data/config/pipeline.json", "templates/pipeline.json")
|
||||
ap.platform_cfg = await config.load_json_config("data/config/platform.json", "templates/platform.json")
|
||||
ap.provider_cfg = await config.load_json_config("data/config/provider.json", "templates/provider.json")
|
||||
ap.system_cfg = await config.load_json_config("data/config/system.json", "templates/system.json")
|
||||
ap.command_cfg = await config.load_json_config("data/config/command.json", "templates/command.json", completion=False)
|
||||
ap.pipeline_cfg = await config.load_json_config("data/config/pipeline.json", "templates/pipeline.json", completion=False)
|
||||
ap.platform_cfg = await config.load_json_config("data/config/platform.json", "templates/platform.json", completion=False)
|
||||
ap.provider_cfg = await config.load_json_config("data/config/provider.json", "templates/provider.json", completion=False)
|
||||
ap.system_cfg = await config.load_json_config("data/config/system.json", "templates/system.json", completion=False)
|
||||
|
||||
ap.plugin_setting_meta = await config.load_json_config("plugins/plugins.json", "templates/plugin-settings.json")
|
||||
await ap.plugin_setting_meta.dump_config()
|
||||
|
||||
@@ -3,8 +3,10 @@ from __future__ import annotations
|
||||
import importlib
|
||||
|
||||
from .. import stage, app
|
||||
from ...config import migration
|
||||
from ...config.migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion
|
||||
from .. import migration
|
||||
from ..migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion
|
||||
from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg
|
||||
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config
|
||||
|
||||
|
||||
@stage.stage_class("MigrationStage")
|
||||
|
||||
28
pkg/core/stages/show_notes.py
Normal file
28
pkg/core/stages/show_notes.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import stage, app, note
|
||||
from ..notes import n001_classic_msgs, n002_selection_mode_on_windows
|
||||
|
||||
|
||||
@stage.stage_class("ShowNotesStage")
|
||||
class ShowNotesStage(stage.BootingStage):
|
||||
"""显示启动信息阶段
|
||||
"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
|
||||
# 排序
|
||||
note.preregistered_notes.sort(key=lambda x: x.number)
|
||||
|
||||
for note_cls in note.preregistered_notes:
|
||||
try:
|
||||
note_inst = note_cls(ap)
|
||||
if await note_inst.need_show():
|
||||
async for ret in note_inst.yield_note():
|
||||
if not ret:
|
||||
continue
|
||||
msg, level = ret
|
||||
if msg:
|
||||
ap.logger.log(level, msg)
|
||||
except Exception as e:
|
||||
continue
|
||||
@@ -8,7 +8,10 @@ from ...config import manager as cfg_mgr
|
||||
|
||||
@stage.stage_class('BanSessionCheckStage')
|
||||
class BanSessionCheckStage(stage.PipelineStage):
|
||||
"""访问控制处理阶段"""
|
||||
"""访问控制处理阶段
|
||||
|
||||
仅检查query中群号或个人号是否在访问控制列表中。
|
||||
"""
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
@@ -9,12 +9,24 @@ from ...core import entities as core_entities
|
||||
from ...config import manager as cfg_mgr
|
||||
from . import filter as filter_model, entities as filter_entities
|
||||
from .filters import cntignore, banwords, baiduexamine
|
||||
from ...provider import entities as llm_entities
|
||||
|
||||
|
||||
@stage.stage_class('PostContentFilterStage')
|
||||
@stage.stage_class('PreContentFilterStage')
|
||||
class ContentFilterStage(stage.PipelineStage):
|
||||
"""内容过滤阶段"""
|
||||
"""内容过滤阶段
|
||||
|
||||
前置:
|
||||
检查消息是否符合规则,不符合则拦截。
|
||||
改写:
|
||||
message_chain
|
||||
|
||||
后置:
|
||||
检查AI回复消息是否符合规则,可能进行改写,不符合则拦截。
|
||||
改写:
|
||||
query.resp_messages
|
||||
"""
|
||||
|
||||
filter_chain: list[filter_model.ContentFilter]
|
||||
|
||||
@@ -130,19 +142,34 @@ class ContentFilterStage(stage.PipelineStage):
|
||||
"""处理
|
||||
"""
|
||||
if stage_inst_name == 'PreContentFilterStage':
|
||||
|
||||
contain_non_text = False
|
||||
|
||||
for me in query.message_chain:
|
||||
if not isinstance(me, mirai.Plain):
|
||||
contain_non_text = True
|
||||
break
|
||||
|
||||
if contain_non_text:
|
||||
self.ap.logger.debug(f"消息中包含非文本消息,跳过内容过滤器检查。")
|
||||
return entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
|
||||
return await self._pre_process(
|
||||
str(query.message_chain).strip(),
|
||||
query
|
||||
)
|
||||
elif stage_inst_name == 'PostContentFilterStage':
|
||||
# 仅处理 query.resp_messages[-1].content 是 str 的情况
|
||||
if isinstance(query.resp_messages[-1].content, str):
|
||||
if isinstance(query.resp_messages[-1], llm_entities.Message) and isinstance(query.resp_messages[-1].content, str):
|
||||
return await self._post_process(
|
||||
query.resp_messages[-1].content,
|
||||
query
|
||||
)
|
||||
else:
|
||||
self.ap.logger.debug(f"resp_messages[-1] 不是 str 类型,跳过内容过滤器检查。")
|
||||
self.ap.logger.debug(f"resp_messages[-1] 不是 Message 类型或 query.resp_messages[-1].content 不是 str 类型,跳过内容过滤器检查。")
|
||||
return entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
|
||||
@@ -4,6 +4,8 @@ import enum
|
||||
|
||||
import pydantic
|
||||
|
||||
from ...provider import entities as llm_entities
|
||||
|
||||
|
||||
class ResultLevel(enum.Enum):
|
||||
"""结果等级"""
|
||||
@@ -38,7 +40,7 @@ class FilterResult(pydantic.BaseModel):
|
||||
"""
|
||||
|
||||
replacement: str
|
||||
"""替换后的消息
|
||||
"""替换后的文本消息
|
||||
|
||||
内容过滤器可以进行一些遮掩处理,然后把遮掩后的消息返回。
|
||||
若没有修改内容,也需要返回原消息。
|
||||
|
||||
@@ -5,6 +5,7 @@ import typing
|
||||
|
||||
from ...core import app
|
||||
from . import entities
|
||||
from ...provider import entities as llm_entities
|
||||
|
||||
|
||||
preregistered_filters: list[typing.Type[ContentFilter]] = []
|
||||
@@ -63,7 +64,7 @@ class ContentFilter(metaclass=abc.ABCMeta):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def process(self, message: str) -> entities.FilterResult:
|
||||
async def process(self, message: str=None, image_url=None) -> entities.FilterResult:
|
||||
"""处理消息
|
||||
|
||||
分为前后阶段,具体取决于 enable_stages 的值。
|
||||
@@ -71,6 +72,7 @@ class ContentFilter(metaclass=abc.ABCMeta):
|
||||
|
||||
Args:
|
||||
message (str): 需要检查的内容
|
||||
image_url (str): 要检查的图片的 URL
|
||||
|
||||
Returns:
|
||||
entities.FilterResult: 过滤结果,具体内容请查看 entities.FilterResult 类的文档
|
||||
|
||||
@@ -8,7 +8,7 @@ from ....config import manager as cfg_mgr
|
||||
|
||||
@filter_model.filter_class("ban-word-filter")
|
||||
class BanWordFilter(filter_model.ContentFilter):
|
||||
"""根据内容禁言"""
|
||||
"""根据内容过滤"""
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
@@ -4,6 +4,8 @@ import asyncio
|
||||
import typing
|
||||
import traceback
|
||||
|
||||
import mirai
|
||||
|
||||
from ..core import app, entities
|
||||
from . import entities as pipeline_entities
|
||||
from ..plugin import events
|
||||
@@ -68,6 +70,17 @@ class Controller:
|
||||
"""检查输出
|
||||
"""
|
||||
if result.user_notice:
|
||||
# 处理str类型
|
||||
|
||||
if isinstance(result.user_notice, str):
|
||||
result.user_notice = mirai.MessageChain(
|
||||
mirai.Plain(result.user_notice)
|
||||
)
|
||||
elif isinstance(result.user_notice, list):
|
||||
result.user_notice = mirai.MessageChain(
|
||||
*result.user_notice
|
||||
)
|
||||
|
||||
await self.ap.platform_mgr.send(
|
||||
query.message_event,
|
||||
result.user_notice,
|
||||
@@ -109,6 +122,8 @@ class Controller:
|
||||
|
||||
while i < len(self.ap.stage_mgr.stage_containers):
|
||||
stage_container = self.ap.stage_mgr.stage_containers[i]
|
||||
|
||||
query.current_stage = stage_container # 标记到 Query 对象里
|
||||
|
||||
result = stage_container.inst.process(query, stage_container.inst_name)
|
||||
|
||||
@@ -149,7 +164,7 @@ class Controller:
|
||||
try:
|
||||
await self._execute_from_stage(0, query)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f"处理请求时出错 query_id={query.query_id}: {e}")
|
||||
self.ap.logger.error(f"处理请求时出错 query_id={query.query_id} stage={query.current_stage.inst_name} : {e}")
|
||||
self.ap.logger.debug(f"Traceback: {traceback.format_exc()}")
|
||||
# traceback.print_exc()
|
||||
finally:
|
||||
|
||||
@@ -16,6 +16,9 @@ from ...config import manager as cfg_mgr
|
||||
@stage.stage_class("LongTextProcessStage")
|
||||
class LongTextProcessStage(stage.PipelineStage):
|
||||
"""长消息处理阶段
|
||||
|
||||
改写:
|
||||
- resp_message_chain
|
||||
"""
|
||||
|
||||
strategy_impl: strategy.LongTextStrategy
|
||||
@@ -31,18 +34,18 @@ class LongTextProcessStage(stage.PipelineStage):
|
||||
if os.name == "nt":
|
||||
use_font = "C:/Windows/Fonts/msyh.ttc"
|
||||
if not os.path.exists(use_font):
|
||||
self.ap.logger.warn("未找到字体文件,且无法使用Windows自带字体,更换为转发消息组件以发送长消息,您可以在config.py中调整相关设置。")
|
||||
self.ap.logger.warn("未找到字体文件,且无法使用Windows自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。")
|
||||
config['blob_message_strategy'] = "forward"
|
||||
else:
|
||||
self.ap.logger.info("使用Windows自带字体:" + use_font)
|
||||
config['font-path'] = use_font
|
||||
else:
|
||||
self.ap.logger.warn("未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在config.py中调整相关设置。")
|
||||
self.ap.logger.warn("未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。")
|
||||
|
||||
self.ap.platform_cfg.data['long-text-process']['strategy'] = "forward"
|
||||
except:
|
||||
traceback.print_exc()
|
||||
self.ap.logger.error("加载字体文件失败({}),更换为转发消息组件以发送长消息,您可以在config.py中调整相关设置。".format(use_font))
|
||||
self.ap.logger.error("加载字体文件失败({}),更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。".format(use_font))
|
||||
|
||||
self.ap.platform_cfg.data['long-text-process']['strategy'] = "forward"
|
||||
|
||||
@@ -59,15 +62,15 @@ class LongTextProcessStage(stage.PipelineStage):
|
||||
# 检查是否包含非 Plain 组件
|
||||
contains_non_plain = False
|
||||
|
||||
for msg in query.resp_message_chain:
|
||||
for msg in query.resp_message_chain[-1]:
|
||||
if not isinstance(msg, Plain):
|
||||
contains_non_plain = True
|
||||
break
|
||||
|
||||
if contains_non_plain:
|
||||
self.ap.logger.debug("消息中包含非 Plain 组件,跳过长消息处理。")
|
||||
elif len(str(query.resp_message_chain)) > self.ap.platform_cfg.data['long-text-process']['threshold']:
|
||||
query.resp_message_chain = MessageChain(await self.strategy_impl.process(str(query.resp_message_chain), query))
|
||||
elif len(str(query.resp_message_chain[-1])) > self.ap.platform_cfg.data['long-text-process']['threshold']:
|
||||
query.resp_message_chain[-1] = MessageChain(await self.strategy_impl.process(str(query.resp_message_chain[-1]), query))
|
||||
|
||||
return entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
|
||||
0
pkg/pipeline/msgtrun/__init__.py
Normal file
0
pkg/pipeline/msgtrun/__init__.py
Normal file
35
pkg/pipeline/msgtrun/msgtrun.py
Normal file
35
pkg/pipeline/msgtrun/msgtrun.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import stage, entities, stagemgr
|
||||
from ...core import entities as core_entities
|
||||
from . import truncator
|
||||
from .truncators import round
|
||||
|
||||
|
||||
@stage.stage_class("ConversationMessageTruncator")
|
||||
class ConversationMessageTruncator(stage.PipelineStage):
|
||||
"""会话消息截断器
|
||||
|
||||
用于截断会话消息链,以适应平台消息长度限制。
|
||||
"""
|
||||
trun: truncator.Truncator
|
||||
|
||||
async def initialize(self):
|
||||
use_method = self.ap.pipeline_cfg.data['msg-truncate']['method']
|
||||
|
||||
for trun in truncator.preregistered_truncators:
|
||||
if trun.name == use_method:
|
||||
self.trun = trun(self.ap)
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"未知的截断器: {use_method}")
|
||||
|
||||
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
|
||||
"""处理
|
||||
"""
|
||||
query = await self.trun.truncate(query)
|
||||
|
||||
return entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
56
pkg/pipeline/msgtrun/truncator.py
Normal file
56
pkg/pipeline/msgtrun/truncator.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import abc
|
||||
|
||||
from ...core import entities as core_entities, app
|
||||
|
||||
|
||||
preregistered_truncators: list[typing.Type[Truncator]] = []
|
||||
|
||||
|
||||
def truncator_class(
|
||||
name: str
|
||||
) -> typing.Callable[[typing.Type[Truncator]], typing.Type[Truncator]]:
|
||||
"""截断器类装饰器
|
||||
|
||||
Args:
|
||||
name (str): 截断器名称
|
||||
|
||||
Returns:
|
||||
typing.Callable[[typing.Type[Truncator]], typing.Type[Truncator]]: 装饰器
|
||||
"""
|
||||
def decorator(cls: typing.Type[Truncator]) -> typing.Type[Truncator]:
|
||||
assert issubclass(cls, Truncator)
|
||||
|
||||
cls.name = name
|
||||
|
||||
preregistered_truncators.append(cls)
|
||||
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class Truncator(abc.ABC):
|
||||
"""消息截断器基类
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def truncate(self, query: core_entities.Query) -> core_entities.Query:
|
||||
"""截断
|
||||
|
||||
一般只需要操作query.messages,也可以扩展操作query.prompt, query.user_message。
|
||||
请勿操作其他字段。
|
||||
"""
|
||||
pass
|
||||
0
pkg/pipeline/msgtrun/truncators/__init__.py
Normal file
0
pkg/pipeline/msgtrun/truncators/__init__.py
Normal file
32
pkg/pipeline/msgtrun/truncators/round.py
Normal file
32
pkg/pipeline/msgtrun/truncators/round.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import truncator
|
||||
from ....core import entities as core_entities
|
||||
|
||||
|
||||
@truncator.truncator_class("round")
|
||||
class RoundTruncator(truncator.Truncator):
|
||||
"""前文回合数阶段器
|
||||
"""
|
||||
|
||||
async def truncate(self, query: core_entities.Query) -> core_entities.Query:
|
||||
"""截断
|
||||
"""
|
||||
max_round = self.ap.pipeline_cfg.data['msg-truncate']['round']['max-round']
|
||||
|
||||
temp_messages = []
|
||||
|
||||
current_round = 0
|
||||
|
||||
# 从后往前遍历
|
||||
for msg in query.messages[::-1]:
|
||||
if current_round < max_round:
|
||||
temp_messages.append(msg)
|
||||
if msg.role == 'user':
|
||||
current_round += 1
|
||||
else:
|
||||
break
|
||||
|
||||
query.messages = temp_messages[::-1]
|
||||
|
||||
return query
|
||||
@@ -43,7 +43,7 @@ class QueryPool:
|
||||
message_event=message_event,
|
||||
message_chain=message_chain,
|
||||
resp_messages=[],
|
||||
resp_message_chain=None,
|
||||
resp_message_chain=[],
|
||||
adapter=adapter
|
||||
)
|
||||
self.queries.append(query)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import mirai
|
||||
|
||||
from .. import stage, entities, stagemgr
|
||||
from ...core import entities as core_entities
|
||||
from ...provider import entities as llm_entities
|
||||
@@ -9,6 +11,16 @@ from ...plugin import events
|
||||
@stage.stage_class("PreProcessor")
|
||||
class PreProcessor(stage.PipelineStage):
|
||||
"""请求预处理阶段
|
||||
|
||||
签出会话、prompt、上文、模型、内容函数。
|
||||
|
||||
改写:
|
||||
- session
|
||||
- prompt
|
||||
- messages
|
||||
- user_message
|
||||
- use_model
|
||||
- use_funcs
|
||||
"""
|
||||
|
||||
async def process(
|
||||
@@ -27,21 +39,42 @@ class PreProcessor(stage.PipelineStage):
|
||||
query.prompt = conversation.prompt.copy()
|
||||
query.messages = conversation.messages.copy()
|
||||
|
||||
query.user_message = llm_entities.Message(
|
||||
role='user',
|
||||
content=str(query.message_chain).strip()
|
||||
)
|
||||
|
||||
query.use_model = conversation.use_model
|
||||
|
||||
query.use_funcs = conversation.use_funcs
|
||||
query.use_funcs = conversation.use_funcs if query.use_model.tool_call_supported else None
|
||||
|
||||
|
||||
# 检查vision是否启用,没启用就删除所有图片
|
||||
if not self.ap.provider_cfg.data['enable-vision'] or not query.use_model.vision_supported:
|
||||
for msg in query.messages:
|
||||
if isinstance(msg.content, list):
|
||||
for me in msg.content:
|
||||
if me.type == 'image_url':
|
||||
msg.content.remove(me)
|
||||
|
||||
content_list = []
|
||||
|
||||
for me in query.message_chain:
|
||||
if isinstance(me, mirai.Plain):
|
||||
content_list.append(
|
||||
llm_entities.ContentElement.from_text(me.text)
|
||||
)
|
||||
elif isinstance(me, mirai.Image):
|
||||
if self.ap.provider_cfg.data['enable-vision'] and query.use_model.vision_supported:
|
||||
if me.url is not None:
|
||||
content_list.append(
|
||||
llm_entities.ContentElement.from_image_url(str(me.url))
|
||||
)
|
||||
|
||||
query.user_message = llm_entities.Message( # TODO 适配多模态输入
|
||||
role='user',
|
||||
content=content_list
|
||||
)
|
||||
# =========== 触发事件 PromptPreProcessing
|
||||
session = query.session
|
||||
|
||||
event_ctx = await self.ap.plugin_mgr.emit_event(
|
||||
event=events.PromptPreProcessing(
|
||||
session_name=f'{session.launcher_type.value}_{session.launcher_id}',
|
||||
session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||
default_prompt=query.prompt.messages,
|
||||
prompt=query.messages,
|
||||
query=query
|
||||
|
||||
@@ -3,13 +3,14 @@ from __future__ import annotations
|
||||
import typing
|
||||
import time
|
||||
import traceback
|
||||
import json
|
||||
|
||||
import mirai
|
||||
|
||||
from .. import handler
|
||||
from ... import entities
|
||||
from ....core import entities as core_entities
|
||||
from ....provider import entities as llm_entities
|
||||
from ....provider import entities as llm_entities, runnermgr
|
||||
from ....plugin import events
|
||||
|
||||
|
||||
@@ -41,12 +42,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
if event_ctx.event.reply is not None:
|
||||
mc = mirai.MessageChain(event_ctx.event.reply)
|
||||
|
||||
query.resp_messages.append(
|
||||
llm_entities.Message(
|
||||
role='plugin',
|
||||
content=mc,
|
||||
)
|
||||
)
|
||||
query.resp_messages.append(mc)
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
@@ -66,13 +62,8 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
)
|
||||
|
||||
if event_ctx.event.alter is not None:
|
||||
query.message_chain = mirai.MessageChain([
|
||||
mirai.Plain(event_ctx.event.alter)
|
||||
])
|
||||
|
||||
query.messages.append(
|
||||
query.user_message
|
||||
)
|
||||
# if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter
|
||||
query.user_message.content = event_ctx.event.alter
|
||||
|
||||
text_length = 0
|
||||
|
||||
@@ -80,7 +71,9 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
|
||||
try:
|
||||
|
||||
async for result in query.use_model.requester.request(query):
|
||||
runner = self.ap.runner_mgr.get_runner()
|
||||
|
||||
async for result in runner.run(query):
|
||||
query.resp_messages.append(result)
|
||||
|
||||
self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}')
|
||||
@@ -92,6 +85,9 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
|
||||
query.session.using_conversation.messages.append(query.user_message)
|
||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
||||
except Exception as e:
|
||||
|
||||
self.ap.logger.error(f'对话({query.query_id})请求失败: {str(e)}')
|
||||
@@ -104,8 +100,6 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
debug_notice=traceback.format_exc()
|
||||
)
|
||||
finally:
|
||||
query.session.using_conversation.messages.append(query.user_message)
|
||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
||||
|
||||
await self.ap.ctr_mgr.usage.post_query_record(
|
||||
session_type=query.session.launcher_type.value,
|
||||
@@ -115,4 +109,4 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
model_name=query.use_model.name,
|
||||
response_seconds=int(time.time() - start_time),
|
||||
retry_times=-1,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -48,12 +48,7 @@ class CommandHandler(handler.MessageHandler):
|
||||
if event_ctx.event.reply is not None:
|
||||
mc = mirai.MessageChain(event_ctx.event.reply)
|
||||
|
||||
query.resp_messages.append(
|
||||
llm_entities.Message(
|
||||
role='command',
|
||||
content=str(mc),
|
||||
)
|
||||
)
|
||||
query.resp_messages.append(mc)
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
@@ -80,9 +75,6 @@ class CommandHandler(handler.MessageHandler):
|
||||
session=session
|
||||
):
|
||||
if ret.error is not None:
|
||||
# query.resp_message_chain = mirai.MessageChain([
|
||||
# mirai.Plain(str(ret.error))
|
||||
# ])
|
||||
query.resp_messages.append(
|
||||
llm_entities.Message(
|
||||
role='command',
|
||||
@@ -96,18 +88,28 @@ class CommandHandler(handler.MessageHandler):
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
elif ret.text is not None:
|
||||
# query.resp_message_chain = mirai.MessageChain([
|
||||
# mirai.Plain(ret.text)
|
||||
# ])
|
||||
elif ret.text is not None or ret.image_url is not None:
|
||||
|
||||
content: list[llm_entities.ContentElement]= []
|
||||
|
||||
if ret.text is not None:
|
||||
content.append(
|
||||
llm_entities.ContentElement.from_text(ret.text)
|
||||
)
|
||||
|
||||
if ret.image_url is not None:
|
||||
content.append(
|
||||
llm_entities.ContentElement.from_image_url(ret.image_url)
|
||||
)
|
||||
|
||||
query.resp_messages.append(
|
||||
llm_entities.Message(
|
||||
role='command',
|
||||
content=ret.text,
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
|
||||
self.ap.logger.info(f'命令返回: {self.cut_str(ret.text)}')
|
||||
self.ap.logger.info(f'命令返回: {self.cut_str(str(content[0]))}')
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
|
||||
@@ -11,7 +11,13 @@ from ...config import manager as cfg_mgr
|
||||
|
||||
@stage.stage_class("MessageProcessor")
|
||||
class Processor(stage.PipelineStage):
|
||||
"""请求实际处理阶段"""
|
||||
"""请求实际处理阶段
|
||||
|
||||
通过命令处理器和聊天处理器处理消息。
|
||||
|
||||
改写:
|
||||
- resp_messages
|
||||
"""
|
||||
|
||||
cmd_handler: handler.MessageHandler
|
||||
|
||||
@@ -36,7 +42,9 @@ class Processor(stage.PipelineStage):
|
||||
self.ap.logger.info(f"处理 {query.launcher_type.value}_{query.launcher_id} 的请求({query.query_id}): {message_text}")
|
||||
|
||||
async def generator():
|
||||
if message_text.startswith('!') or message_text.startswith('!'):
|
||||
cmd_prefix = self.ap.command_cfg.data['command-prefix']
|
||||
|
||||
if any(message_text.startswith(prefix) for prefix in cmd_prefix):
|
||||
async for result in self.cmd_handler.handle(query):
|
||||
yield result
|
||||
else:
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
# 固定窗口算法
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from .. import algo
|
||||
|
||||
|
||||
# 固定窗口算法
|
||||
class SessionContainer:
|
||||
|
||||
wait_lock: asyncio.Lock
|
||||
|
||||
records: dict[int, int]
|
||||
"""访问记录,key为每分钟的起始时间戳,value为访问次数"""
|
||||
"""访问记录,key为每窗口长度的起始时间戳,value为访问次数"""
|
||||
|
||||
def __init__(self):
|
||||
self.wait_lock = asyncio.Lock()
|
||||
@@ -47,30 +44,34 @@ class FixedWindowAlgo(algo.ReteLimitAlgo):
|
||||
|
||||
# 等待锁
|
||||
async with container.wait_lock:
|
||||
|
||||
# 获取窗口大小和限制
|
||||
window_size = self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['window-size']
|
||||
limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['limit']
|
||||
|
||||
if session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']:
|
||||
window_size = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]['window-size']
|
||||
limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]['limit']
|
||||
|
||||
# 获取当前时间戳
|
||||
now = int(time.time())
|
||||
|
||||
# 获取当前分钟的起始时间戳
|
||||
now = now - now % 60
|
||||
# 获取当前窗口的起始时间戳
|
||||
now = now - now % window_size
|
||||
|
||||
# 获取当前分钟的访问次数
|
||||
# 获取当前窗口的访问次数
|
||||
count = container.records.get(now, 0)
|
||||
|
||||
limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']
|
||||
|
||||
if session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']:
|
||||
limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name]
|
||||
|
||||
# 如果访问次数超过了限制
|
||||
if count >= limitation:
|
||||
if self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'drop':
|
||||
return False
|
||||
elif self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'wait':
|
||||
# 等待下一分钟
|
||||
await asyncio.sleep(60 - time.time() % 60)
|
||||
# 等待下一窗口
|
||||
await asyncio.sleep(window_size - time.time() % window_size)
|
||||
|
||||
now = int(time.time())
|
||||
now = now - now % 60
|
||||
now = now - now % window_size
|
||||
|
||||
if now not in container.records:
|
||||
container.records = {}
|
||||
|
||||
@@ -11,7 +11,10 @@ from ...core import entities as core_entities
|
||||
@stage.stage_class("RequireRateLimitOccupancy")
|
||||
@stage.stage_class("ReleaseRateLimitOccupancy")
|
||||
class RateLimit(stage.PipelineStage):
|
||||
"""限速器控制阶段"""
|
||||
"""限速器控制阶段
|
||||
|
||||
不改写query,只检查是否需要限速。
|
||||
"""
|
||||
|
||||
algo: algo.ReteLimitAlgo
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class SendResponseBackStage(stage.PipelineStage):
|
||||
|
||||
await self.ap.platform_mgr.send(
|
||||
query.message_event,
|
||||
query.resp_message_chain,
|
||||
query.resp_message_chain[-1],
|
||||
adapter=query.adapter
|
||||
)
|
||||
|
||||
|
||||
@@ -14,9 +14,12 @@ from ...config import manager as cfg_mgr
|
||||
@stage.stage_class("GroupRespondRuleCheckStage")
|
||||
class GroupRespondRuleCheckStage(stage.PipelineStage):
|
||||
"""群组响应规则检查器
|
||||
|
||||
仅检查群消息是否符合规则。
|
||||
"""
|
||||
|
||||
rule_matchers: list[rule.GroupRespondRule]
|
||||
"""检查器实例"""
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化检查器
|
||||
@@ -31,7 +34,7 @@ class GroupRespondRuleCheckStage(stage.PipelineStage):
|
||||
|
||||
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
|
||||
|
||||
if query.launcher_type.value != 'group':
|
||||
if query.launcher_type.value != 'group': # 只处理群消息
|
||||
return entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
@@ -41,8 +44,8 @@ class GroupRespondRuleCheckStage(stage.PipelineStage):
|
||||
|
||||
use_rule = rules['default']
|
||||
|
||||
if str(query.launcher_id) in use_rule:
|
||||
use_rule = use_rule[str(query.launcher_id)]
|
||||
if str(query.launcher_id) in rules:
|
||||
use_rule = rules[str(query.launcher_id)]
|
||||
|
||||
for rule_matcher in self.rule_matchers: # 任意一个匹配就放行
|
||||
res = await rule_matcher.match(str(query.message_chain), query.message_chain, use_rule, query)
|
||||
|
||||
@@ -20,11 +20,14 @@ class PrefixRule(rule_model.GroupRespondRule):
|
||||
for prefix in prefixes:
|
||||
if message_text.startswith(prefix):
|
||||
|
||||
# 查找第一个plain元素
|
||||
for me in message_chain:
|
||||
if isinstance(me, mirai.Plain):
|
||||
me.text = me.text[len(prefix):]
|
||||
|
||||
return entities.RuleJudgeResult(
|
||||
matching=True,
|
||||
replacement=mirai.MessageChain([
|
||||
mirai.Plain(message_text[len(prefix):])
|
||||
]),
|
||||
replacement=message_chain,
|
||||
)
|
||||
|
||||
return entities.RuleJudgeResult(
|
||||
|
||||
@@ -13,21 +13,23 @@ from .respback import respback
|
||||
from .wrapper import wrapper
|
||||
from .preproc import preproc
|
||||
from .ratelimit import ratelimit
|
||||
from .msgtrun import msgtrun
|
||||
|
||||
|
||||
# 请求处理阶段顺序
|
||||
stage_order = [
|
||||
"GroupRespondRuleCheckStage",
|
||||
"BanSessionCheckStage",
|
||||
"PreContentFilterStage",
|
||||
"PreProcessor",
|
||||
"RequireRateLimitOccupancy",
|
||||
"MessageProcessor",
|
||||
"ReleaseRateLimitOccupancy",
|
||||
"PostContentFilterStage",
|
||||
"ResponseWrapper",
|
||||
"LongTextProcessStage",
|
||||
"SendResponseBackStage",
|
||||
"GroupRespondRuleCheckStage", # 群响应规则检查
|
||||
"BanSessionCheckStage", # 封禁会话检查
|
||||
"PreContentFilterStage", # 内容过滤前置阶段
|
||||
"PreProcessor", # 预处理器
|
||||
"ConversationMessageTruncator", # 会话消息截断器
|
||||
"RequireRateLimitOccupancy", # 请求速率限制占用
|
||||
"MessageProcessor", # 处理器
|
||||
"ReleaseRateLimitOccupancy", # 释放速率限制占用
|
||||
"PostContentFilterStage", # 内容过滤后置阶段
|
||||
"ResponseWrapper", # 响应包装器
|
||||
"LongTextProcessStage", # 长文本处理
|
||||
"SendResponseBackStage", # 发送响应
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,13 @@ from ...plugin import events
|
||||
|
||||
@stage.stage_class("ResponseWrapper")
|
||||
class ResponseWrapper(stage.PipelineStage):
|
||||
"""回复包装阶段
|
||||
|
||||
把回复的 message 包装成人类识读的形式。
|
||||
|
||||
改写:
|
||||
- resp_message_chain
|
||||
"""
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
@@ -25,78 +32,49 @@ class ResponseWrapper(stage.PipelineStage):
|
||||
) -> typing.AsyncGenerator[entities.StageProcessResult, None]:
|
||||
"""处理
|
||||
"""
|
||||
|
||||
if query.resp_messages[-1].role == 'command':
|
||||
query.resp_message_chain = mirai.MessageChain("[bot] "+query.resp_messages[-1].content)
|
||||
|
||||
# 如果 resp_messages[-1] 已经是 MessageChain 了
|
||||
if isinstance(query.resp_messages[-1], mirai.MessageChain):
|
||||
query.resp_message_chain.append(query.resp_messages[-1])
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
elif query.resp_messages[-1].role == 'plugin':
|
||||
if not isinstance(query.resp_messages[-1].content, mirai.MessageChain):
|
||||
query.resp_message_chain = mirai.MessageChain(query.resp_messages[-1].content)
|
||||
else:
|
||||
query.resp_message_chain = query.resp_messages[-1].content
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
else:
|
||||
|
||||
if query.resp_messages[-1].role == 'command':
|
||||
# query.resp_message_chain.append(mirai.MessageChain("[bot] "+query.resp_messages[-1].content))
|
||||
query.resp_message_chain.append(query.resp_messages[-1].get_content_mirai_message_chain(prefix_text='[bot] '))
|
||||
|
||||
if query.resp_messages[-1].role == 'assistant':
|
||||
result = query.resp_messages[-1]
|
||||
session = await self.ap.sess_mgr.get_session(query)
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
elif query.resp_messages[-1].role == 'plugin':
|
||||
# if not isinstance(query.resp_messages[-1].content, mirai.MessageChain):
|
||||
# query.resp_message_chain.append(mirai.MessageChain(query.resp_messages[-1].content))
|
||||
# else:
|
||||
# query.resp_message_chain.append(query.resp_messages[-1].content)
|
||||
query.resp_message_chain.append(query.resp_messages[-1].get_content_mirai_message_chain())
|
||||
|
||||
reply_text = ''
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
else:
|
||||
|
||||
if result.content is not None: # 有内容
|
||||
reply_text = result.content
|
||||
if query.resp_messages[-1].role == 'assistant':
|
||||
result = query.resp_messages[-1]
|
||||
session = await self.ap.sess_mgr.get_session(query)
|
||||
|
||||
# ============= 触发插件事件 ===============
|
||||
event_ctx = await self.ap.plugin_mgr.emit_event(
|
||||
event=events.NormalMessageResponded(
|
||||
launcher_type=query.launcher_type.value,
|
||||
launcher_id=query.launcher_id,
|
||||
sender_id=query.sender_id,
|
||||
session=session,
|
||||
prefix='',
|
||||
response_text=reply_text,
|
||||
finish_reason='stop',
|
||||
funcs_called=[fc.function.name for fc in result.tool_calls] if result.tool_calls is not None else [],
|
||||
query=query
|
||||
)
|
||||
)
|
||||
if event_ctx.is_prevented_default():
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.INTERRUPT,
|
||||
new_query=query
|
||||
)
|
||||
else:
|
||||
if event_ctx.event.reply is not None:
|
||||
|
||||
query.resp_message_chain = mirai.MessageChain(event_ctx.event.reply)
|
||||
reply_text = ''
|
||||
|
||||
else:
|
||||
if result.content: # 有内容
|
||||
reply_text = str(result.get_content_mirai_message_chain())
|
||||
|
||||
query.resp_message_chain = mirai.MessageChain([mirai.Plain(reply_text)])
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
|
||||
if result.tool_calls is not None: # 有函数调用
|
||||
|
||||
function_names = [tc.function.name for tc in result.tool_calls]
|
||||
|
||||
reply_text = f'调用函数 {".".join(function_names)}...'
|
||||
|
||||
query.resp_message_chain = mirai.MessageChain([mirai.Plain(reply_text)])
|
||||
|
||||
if self.ap.platform_cfg.data['track-function-calls']:
|
||||
|
||||
# ============= 触发插件事件 ===============
|
||||
event_ctx = await self.ap.plugin_mgr.emit_event(
|
||||
event=events.NormalMessageResponded(
|
||||
launcher_type=query.launcher_type.value,
|
||||
@@ -110,7 +88,6 @@ class ResponseWrapper(stage.PipelineStage):
|
||||
query=query
|
||||
)
|
||||
)
|
||||
|
||||
if event_ctx.is_prevented_default():
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.INTERRUPT,
|
||||
@@ -119,13 +96,56 @@ class ResponseWrapper(stage.PipelineStage):
|
||||
else:
|
||||
if event_ctx.event.reply is not None:
|
||||
|
||||
query.resp_message_chain = mirai.MessageChain(event_ctx.event.reply)
|
||||
query.resp_message_chain.append(mirai.MessageChain(event_ctx.event.reply))
|
||||
|
||||
else:
|
||||
|
||||
query.resp_message_chain = mirai.MessageChain([mirai.Plain(reply_text)])
|
||||
query.resp_message_chain.append(result.get_content_mirai_message_chain())
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
)
|
||||
|
||||
if result.tool_calls is not None: # 有函数调用
|
||||
|
||||
function_names = [tc.function.name for tc in result.tool_calls]
|
||||
|
||||
reply_text = f'调用函数 {".".join(function_names)}...'
|
||||
|
||||
query.resp_message_chain.append(mirai.MessageChain([mirai.Plain(reply_text)]))
|
||||
|
||||
if self.ap.platform_cfg.data['track-function-calls']:
|
||||
|
||||
event_ctx = await self.ap.plugin_mgr.emit_event(
|
||||
event=events.NormalMessageResponded(
|
||||
launcher_type=query.launcher_type.value,
|
||||
launcher_id=query.launcher_id,
|
||||
sender_id=query.sender_id,
|
||||
session=session,
|
||||
prefix='',
|
||||
response_text=reply_text,
|
||||
finish_reason='stop',
|
||||
funcs_called=[fc.function.name for fc in result.tool_calls] if result.tool_calls is not None else [],
|
||||
query=query
|
||||
)
|
||||
)
|
||||
|
||||
if event_ctx.is_prevented_default():
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.INTERRUPT,
|
||||
new_query=query
|
||||
)
|
||||
else:
|
||||
if event_ctx.event.reply is not None:
|
||||
|
||||
query.resp_message_chain.append(mirai.MessageChain(event_ctx.event.reply))
|
||||
|
||||
else:
|
||||
|
||||
query.resp_message_chain.append(mirai.MessageChain([mirai.Plain(reply_text)]))
|
||||
|
||||
yield entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=query
|
||||
)
|
||||
|
||||
@@ -146,9 +146,9 @@ class PlatformManager:
|
||||
if len(self.adapters) == 0:
|
||||
self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。')
|
||||
|
||||
async def send(self, event: mirai.MessageEvent, msg: mirai.MessageChain, adapter: msadapter.MessageSourceAdapter, check_quote=True, check_at_sender=True):
|
||||
async def send(self, event: mirai.MessageEvent, msg: mirai.MessageChain, adapter: msadapter.MessageSourceAdapter):
|
||||
|
||||
if check_at_sender and self.ap.platform_cfg.data['at-sender'] and isinstance(event, GroupMessage):
|
||||
if self.ap.platform_cfg.data['at-sender'] and isinstance(event, GroupMessage):
|
||||
|
||||
msg.insert(
|
||||
0,
|
||||
@@ -160,7 +160,7 @@ class PlatformManager:
|
||||
await adapter.reply_message(
|
||||
event,
|
||||
msg,
|
||||
quote_origin=True if self.ap.platform_cfg.data['quote-origin'] and check_quote else False
|
||||
quote_origin=True if self.ap.platform_cfg.data['quote-origin'] else False
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
|
||||
@@ -30,7 +30,16 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
msg_id = msg.id
|
||||
msg_time = msg.time
|
||||
elif type(msg) is mirai.Image:
|
||||
msg_list.append(aiocqhttp.MessageSegment.image(msg.path))
|
||||
arg = ''
|
||||
if msg.base64:
|
||||
arg = msg.base64
|
||||
msg_list.append(aiocqhttp.MessageSegment.image(f"base64://{arg}"))
|
||||
elif msg.url:
|
||||
arg = msg.url
|
||||
msg_list.append(aiocqhttp.MessageSegment.image(arg))
|
||||
elif msg.path:
|
||||
arg = msg.path
|
||||
msg_list.append(aiocqhttp.MessageSegment.image(arg))
|
||||
elif type(msg) is mirai.At:
|
||||
msg_list.append(aiocqhttp.MessageSegment.at(msg.target))
|
||||
elif type(msg) is mirai.AtAll:
|
||||
@@ -38,7 +47,16 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
elif type(msg) is mirai.Face:
|
||||
msg_list.append(aiocqhttp.MessageSegment.face(msg.face_id))
|
||||
elif type(msg) is mirai.Voice:
|
||||
msg_list.append(aiocqhttp.MessageSegment.record(msg.path))
|
||||
arg = ''
|
||||
if msg.base64:
|
||||
arg = msg.base64
|
||||
msg_list.append(aiocqhttp.MessageSegment.record(f"base64://{arg}"))
|
||||
elif msg.url:
|
||||
arg = msg.url
|
||||
msg_list.append(aiocqhttp.MessageSegment.record(arg))
|
||||
elif msg.path:
|
||||
arg = msg.path
|
||||
msg_list.append(aiocqhttp.MessageSegment.record(msg.path))
|
||||
elif type(msg) is forward.Forward:
|
||||
|
||||
for node in msg.node_list:
|
||||
|
||||
@@ -322,7 +322,7 @@ class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
|
||||
proxies=None
|
||||
)
|
||||
if resp.status_code == 403:
|
||||
raise Exception("go-cqhttp拒绝访问,请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配")
|
||||
raise Exception("go-cqhttp拒绝访问,请检查配置文件中nakuru适配器的配置")
|
||||
self.bot_account_id = int(resp.json()['data']['user_id'])
|
||||
except Exception as e:
|
||||
raise Exception("获取go-cqhttp账号信息失败, 请检查是否已启动go-cqhttp并配置正确")
|
||||
|
||||
@@ -198,7 +198,6 @@ class OfficialMessageConverter(adapter_model.MessageConverter):
|
||||
bot_account_id: int = 0,
|
||||
) -> mirai.MessageChain:
|
||||
yiri_msg_list = []
|
||||
|
||||
# 存id
|
||||
|
||||
yiri_msg_list.append(
|
||||
@@ -218,7 +217,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter):
|
||||
yiri_msg_list.append(mirai.At(target=mention.id))
|
||||
|
||||
for attachment in message.attachments:
|
||||
if attachment.content_type == "image":
|
||||
if attachment.content_type.startswith("image"):
|
||||
yiri_msg_list.append(mirai.Image(url=attachment.url))
|
||||
else:
|
||||
logging.warning(
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import typing
|
||||
import abc
|
||||
import pydantic
|
||||
import mirai
|
||||
|
||||
from . import events
|
||||
from ..provider.tools import entities as tools_entities
|
||||
@@ -165,11 +166,54 @@ class EventContext:
|
||||
}
|
||||
"""
|
||||
|
||||
# ========== 插件可调用的 API ==========
|
||||
|
||||
def add_return(self, key: str, ret):
|
||||
"""添加返回值"""
|
||||
if key not in self.__return_value__:
|
||||
self.__return_value__[key] = []
|
||||
self.__return_value__[key].append(ret)
|
||||
|
||||
async def reply(self, message_chain: mirai.MessageChain):
|
||||
"""回复此次消息请求
|
||||
|
||||
Args:
|
||||
message_chain (mirai.MessageChain): YiriMirai库的消息链,若用户使用的不是 YiriMirai 适配器,程序也能自动转换为目标消息链
|
||||
"""
|
||||
await self.host.ap.platform_mgr.send(
|
||||
event=self.event.query.message_event,
|
||||
msg=message_chain,
|
||||
adapter=self.event.query.adapter,
|
||||
)
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
target_id: str,
|
||||
message: mirai.MessageChain
|
||||
):
|
||||
"""主动发送消息
|
||||
|
||||
Args:
|
||||
target_type (str): 目标类型,`person`或`group`
|
||||
target_id (str): 目标ID
|
||||
message (mirai.MessageChain): YiriMirai库的消息链,若用户使用的不是 YiriMirai 适配器,程序也能自动转换为目标消息链
|
||||
"""
|
||||
await self.event.query.adapter.send_message(
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
message=message
|
||||
)
|
||||
|
||||
def prevent_postorder(self):
|
||||
"""阻止后续插件执行"""
|
||||
self.__prevent_postorder__ = True
|
||||
|
||||
def prevent_default(self):
|
||||
"""阻止默认行为"""
|
||||
self.__prevent_default__ = True
|
||||
|
||||
# ========== 以下是内部保留方法,插件不应调用 ==========
|
||||
|
||||
def get_return(self, key: str) -> list:
|
||||
"""获取key的所有返回值"""
|
||||
@@ -183,14 +227,6 @@ class EventContext:
|
||||
return self.__return_value__[key][0]
|
||||
return None
|
||||
|
||||
def prevent_default(self):
|
||||
"""阻止默认行为"""
|
||||
self.__prevent_default__ = True
|
||||
|
||||
def prevent_postorder(self):
|
||||
"""阻止后续插件执行"""
|
||||
self.__prevent_postorder__ = True
|
||||
|
||||
def is_prevented_default(self):
|
||||
"""是否阻止默认行为"""
|
||||
return self.__prevent_default__
|
||||
@@ -198,6 +234,7 @@ class EventContext:
|
||||
def is_prevented_postorder(self):
|
||||
"""是否阻止后序插件执行"""
|
||||
return self.__prevent_postorder__
|
||||
|
||||
|
||||
def __init__(self, host: APIHost, event: events.BaseEventModel):
|
||||
|
||||
|
||||
@@ -21,6 +21,39 @@ class ToolCall(pydantic.BaseModel):
|
||||
function: FunctionCall
|
||||
|
||||
|
||||
class ImageURLContentObject(pydantic.BaseModel):
|
||||
url: str
|
||||
|
||||
def __str__(self):
|
||||
return self.url[:128] + ('...' if len(self.url) > 128 else '')
|
||||
|
||||
|
||||
class ContentElement(pydantic.BaseModel):
|
||||
|
||||
type: str
|
||||
"""内容类型"""
|
||||
|
||||
text: typing.Optional[str] = None
|
||||
|
||||
image_url: typing.Optional[ImageURLContentObject] = None
|
||||
|
||||
def __str__(self):
|
||||
if self.type == 'text':
|
||||
return self.text
|
||||
elif self.type == 'image_url':
|
||||
return f'[图片]({self.image_url})'
|
||||
else:
|
||||
return '未知内容'
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
return cls(type='text', text=text)
|
||||
|
||||
@classmethod
|
||||
def from_image_url(cls, image_url: str):
|
||||
return cls(type='image_url', image_url=ImageURLContentObject(url=image_url))
|
||||
|
||||
|
||||
class Message(pydantic.BaseModel):
|
||||
"""消息"""
|
||||
|
||||
@@ -30,12 +63,9 @@ class Message(pydantic.BaseModel):
|
||||
name: typing.Optional[str] = None
|
||||
"""名称,仅函数调用返回时设置"""
|
||||
|
||||
content: typing.Optional[str] | typing.Optional[mirai.MessageChain] = None
|
||||
content: typing.Optional[list[ContentElement]] | typing.Optional[str] = None
|
||||
"""内容"""
|
||||
|
||||
function_call: typing.Optional[FunctionCall] = None
|
||||
"""函数调用,不再受支持,请使用tool_calls"""
|
||||
|
||||
tool_calls: typing.Optional[list[ToolCall]] = None
|
||||
"""工具调用"""
|
||||
|
||||
@@ -43,10 +73,47 @@ class Message(pydantic.BaseModel):
|
||||
|
||||
def readable_str(self) -> str:
|
||||
if self.content is not None:
|
||||
return str(self.content)
|
||||
elif self.function_call is not None:
|
||||
return f'{self.function_call.name}({self.function_call.arguments})'
|
||||
return str(self.role) + ": " + str(self.get_content_mirai_message_chain())
|
||||
elif self.tool_calls is not None:
|
||||
return f'调用工具: {self.tool_calls[0].id}'
|
||||
else:
|
||||
return '未知消息'
|
||||
|
||||
def get_content_mirai_message_chain(self, prefix_text: str="") -> mirai.MessageChain | None:
|
||||
"""将内容转换为 Mirai MessageChain 对象
|
||||
|
||||
Args:
|
||||
prefix_text (str): 首个文字组件的前缀文本
|
||||
"""
|
||||
|
||||
if self.content is None:
|
||||
return None
|
||||
elif isinstance(self.content, str):
|
||||
return mirai.MessageChain([mirai.Plain(prefix_text+self.content)])
|
||||
elif isinstance(self.content, list):
|
||||
mc = []
|
||||
for ce in self.content:
|
||||
if ce.type == 'text':
|
||||
mc.append(mirai.Plain(ce.text))
|
||||
elif ce.type == 'image_url':
|
||||
if ce.image_url.url.startswith("http"):
|
||||
mc.append(mirai.Image(url=ce.image_url.url))
|
||||
else: # base64
|
||||
|
||||
b64_str = ce.image_url.url
|
||||
|
||||
if b64_str.startswith("data:"):
|
||||
b64_str = b64_str.split(",")[1]
|
||||
|
||||
mc.append(mirai.Image(base64=b64_str))
|
||||
|
||||
# 找第一个文字组件
|
||||
if prefix_text:
|
||||
for i, c in enumerate(mc):
|
||||
if isinstance(c, mirai.Plain):
|
||||
mc[i] = mirai.Plain(prefix_text+c.text)
|
||||
break
|
||||
else:
|
||||
mc.insert(0, mirai.Plain(prefix_text))
|
||||
|
||||
return mirai.MessageChain(mc)
|
||||
|
||||
@@ -6,6 +6,8 @@ import typing
|
||||
from ...core import app
|
||||
from ...core import entities as core_entities
|
||||
from .. import entities as llm_entities
|
||||
from . import entities as modelmgr_entities
|
||||
from ..tools import entities as tools_entities
|
||||
|
||||
|
||||
preregistered_requesters: list[typing.Type[LLMAPIRequester]] = []
|
||||
@@ -33,20 +35,31 @@ class LLMAPIRequester(metaclass=abc.ABCMeta):
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def request(
|
||||
async def preprocess(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
"""请求API
|
||||
):
|
||||
"""预处理
|
||||
|
||||
在这里处理特定API对Query对象的兼容性问题。
|
||||
"""
|
||||
pass
|
||||
|
||||
对话前文可以从 query 对象中获取。
|
||||
可以多次yield消息对象。
|
||||
@abc.abstractmethod
|
||||
async def call(
|
||||
self,
|
||||
model: modelmgr_entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
) -> llm_entities.Message:
|
||||
"""调用API
|
||||
|
||||
Args:
|
||||
query (core_entities.Query): 本次请求的上下文对象
|
||||
model (modelmgr_entities.LLMModelInfo): 使用的模型信息
|
||||
messages (typing.List[llm_entities.Message]): 消息对象列表
|
||||
funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None.
|
||||
|
||||
Yields:
|
||||
pkg.provider.entities.Message: 返回消息对象
|
||||
Returns:
|
||||
llm_entities.Message: 返回消息对象
|
||||
"""
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@@ -11,6 +11,7 @@ from .. import api, entities, errors
|
||||
from ....core import entities as core_entities
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
from ....utils import image
|
||||
|
||||
|
||||
@api.requester_class("anthropic-messages")
|
||||
@@ -27,47 +28,76 @@ class AnthropicMessages(api.LLMAPIRequester):
|
||||
proxies=self.ap.proxy_mgr.get_forward_proxies()
|
||||
)
|
||||
|
||||
async def request(
|
||||
async def call(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
self.client.api_key = query.use_model.token_mgr.get_token()
|
||||
model: entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
) -> llm_entities.Message:
|
||||
self.client.api_key = model.token_mgr.get_token()
|
||||
|
||||
args = self.ap.provider_cfg.data['requester']['anthropic-messages']['args'].copy()
|
||||
args["model"] = query.use_model.name if query.use_model.model_name is None else query.use_model.model_name
|
||||
args["model"] = model.name if model.model_name is None else model.model_name
|
||||
|
||||
req_messages = [ # req_messages 仅用于类内,外部同步由 query.messages 进行
|
||||
m.dict(exclude_none=True) for m in query.prompt.messages if m.content.strip() != ""
|
||||
] + [m.dict(exclude_none=True) for m in query.messages]
|
||||
# 处理消息
|
||||
|
||||
# 删除所有 role=system & content='' 的消息
|
||||
req_messages = [
|
||||
m for m in req_messages if not (m["role"] == "system" and m["content"].strip() == "")
|
||||
]
|
||||
# system
|
||||
system_role_message = None
|
||||
|
||||
# 检查是否有 role=system 的消息,若有,改为 role=user,并在后面加一个 role=assistant 的消息
|
||||
system_role_index = []
|
||||
for i, m in enumerate(req_messages):
|
||||
if m["role"] == "system":
|
||||
system_role_index.append(i)
|
||||
m["role"] = "user"
|
||||
for i, m in enumerate(messages):
|
||||
if m.role == "system":
|
||||
system_role_message = m
|
||||
|
||||
if system_role_index:
|
||||
for i in system_role_index[::-1]:
|
||||
req_messages.insert(i + 1, {"role": "assistant", "content": "Okay, I'll follow."})
|
||||
messages.pop(i)
|
||||
break
|
||||
|
||||
# 忽略掉空消息,用户可能发送空消息,而上层未过滤
|
||||
req_messages = [
|
||||
m for m in req_messages if m["content"].strip() != ""
|
||||
]
|
||||
if isinstance(system_role_message, llm_entities.Message) \
|
||||
and isinstance(system_role_message.content, str):
|
||||
args['system'] = system_role_message.content
|
||||
|
||||
req_messages = []
|
||||
|
||||
for m in messages:
|
||||
if isinstance(m.content, str) and m.content.strip() != "":
|
||||
req_messages.append(m.dict(exclude_none=True))
|
||||
elif isinstance(m.content, list):
|
||||
# m.content = [
|
||||
# c for c in m.content if c.type == "text"
|
||||
# ]
|
||||
|
||||
# if len(m.content) > 0:
|
||||
# req_messages.append(m.dict(exclude_none=True))
|
||||
|
||||
msg_dict = m.dict(exclude_none=True)
|
||||
|
||||
for i, ce in enumerate(m.content):
|
||||
if ce.type == "image_url":
|
||||
alter_image_ele = {
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": await image.qq_image_url_to_base64(ce.image_url.url)
|
||||
}
|
||||
}
|
||||
msg_dict["content"][i] = alter_image_ele
|
||||
|
||||
req_messages.append(msg_dict)
|
||||
|
||||
args["messages"] = req_messages
|
||||
|
||||
try:
|
||||
# anthropic的tools处在beta阶段,sdk不稳定,故暂时不支持
|
||||
#
|
||||
# if funcs:
|
||||
# tools = await self.ap.tool_mgr.generate_tools_for_openai(funcs)
|
||||
|
||||
# if tools:
|
||||
# args["tools"] = tools
|
||||
|
||||
try:
|
||||
resp = await self.client.messages.create(**args)
|
||||
|
||||
yield llm_entities.Message(
|
||||
return llm_entities.Message(
|
||||
content=resp.content[0].text,
|
||||
role=resp.role
|
||||
)
|
||||
@@ -79,4 +109,4 @@ class AnthropicMessages(api.LLMAPIRequester):
|
||||
if 'model: ' in str(e):
|
||||
raise errors.RequesterError(f'模型无效: {e.message}')
|
||||
else:
|
||||
raise errors.RequesterError(f'请求地址无效: {e.message}')
|
||||
raise errors.RequesterError(f'请求地址无效: {e.message}')
|
||||
|
||||
@@ -3,16 +3,20 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import typing
|
||||
import json
|
||||
import base64
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import openai
|
||||
import openai.types.chat.chat_completion as chat_completion
|
||||
import httpx
|
||||
import aiohttp
|
||||
import async_lru
|
||||
|
||||
from .. import api, entities, errors
|
||||
from ....core import entities as core_entities, app
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
from ....utils import image
|
||||
|
||||
|
||||
@api.requester_class("openai-chat-completions")
|
||||
@@ -43,7 +47,6 @@ class OpenAIChatCompletions(api.LLMAPIRequester):
|
||||
self,
|
||||
args: dict,
|
||||
) -> chat_completion.ChatCompletion:
|
||||
self.ap.logger.debug(f"req chat_completion with args {args}")
|
||||
return await self.client.chat.completions.create(**args)
|
||||
|
||||
async def _make_msg(
|
||||
@@ -67,14 +70,22 @@ class OpenAIChatCompletions(api.LLMAPIRequester):
|
||||
args = self.requester_cfg['args'].copy()
|
||||
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
|
||||
|
||||
if use_model.tool_call_supported:
|
||||
if use_funcs:
|
||||
tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs)
|
||||
|
||||
if tools:
|
||||
args["tools"] = tools
|
||||
|
||||
# 设置此次请求中的messages
|
||||
messages = req_messages
|
||||
messages = req_messages.copy()
|
||||
|
||||
# 检查vision
|
||||
for msg in messages:
|
||||
if 'content' in msg and isinstance(msg["content"], list):
|
||||
for me in msg["content"]:
|
||||
if me["type"] == "image_url":
|
||||
me["image_url"]['url'] = await self.get_base64_str(me["image_url"]['url'])
|
||||
|
||||
args["messages"] = messages
|
||||
|
||||
# 发送请求
|
||||
@@ -84,73 +95,26 @@ class OpenAIChatCompletions(api.LLMAPIRequester):
|
||||
message = await self._make_msg(resp)
|
||||
|
||||
return message
|
||||
|
||||
async def call(
|
||||
self,
|
||||
model: entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
) -> llm_entities.Message:
|
||||
req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行
|
||||
for m in messages:
|
||||
msg_dict = m.dict(exclude_none=True)
|
||||
content = msg_dict.get("content")
|
||||
if isinstance(content, list):
|
||||
# 检查 content 列表中是否每个部分都是文本
|
||||
if all(isinstance(part, dict) and part.get("type") == "text" for part in content):
|
||||
# 将所有文本部分合并为一个字符串
|
||||
msg_dict["content"] = "\n".join(part["text"] for part in content)
|
||||
req_messages.append(msg_dict)
|
||||
|
||||
async def _request(
|
||||
self, query: core_entities.Query
|
||||
) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
"""请求"""
|
||||
|
||||
pending_tool_calls = []
|
||||
|
||||
req_messages = [ # req_messages 仅用于类内,外部同步由 query.messages 进行
|
||||
m.dict(exclude_none=True) for m in query.prompt.messages if m.content.strip() != ""
|
||||
] + [m.dict(exclude_none=True) for m in query.messages]
|
||||
|
||||
# req_messages.append({"role": "user", "content": str(query.message_chain)})
|
||||
|
||||
# 首次请求
|
||||
msg = await self._closure(req_messages, query.use_model, query.use_funcs)
|
||||
|
||||
yield msg
|
||||
|
||||
pending_tool_calls = msg.tool_calls
|
||||
|
||||
req_messages.append(msg.dict(exclude_none=True))
|
||||
|
||||
# 持续请求,只要还有待处理的工具调用就继续处理调用
|
||||
while pending_tool_calls:
|
||||
for tool_call in pending_tool_calls:
|
||||
try:
|
||||
func = tool_call.function
|
||||
|
||||
parameters = json.loads(func.arguments)
|
||||
|
||||
func_ret = await self.ap.tool_mgr.execute_func_call(
|
||||
query, func.name, parameters
|
||||
)
|
||||
|
||||
msg = llm_entities.Message(
|
||||
role="tool", content=json.dumps(func_ret, ensure_ascii=False), tool_call_id=tool_call.id
|
||||
)
|
||||
|
||||
yield msg
|
||||
|
||||
req_messages.append(msg.dict(exclude_none=True))
|
||||
except Exception as e:
|
||||
# 出错,添加一个报错信息到 req_messages
|
||||
err_msg = llm_entities.Message(
|
||||
role="tool", content=f"err: {e}", tool_call_id=tool_call.id
|
||||
)
|
||||
|
||||
yield err_msg
|
||||
|
||||
req_messages.append(
|
||||
err_msg.dict(exclude_none=True)
|
||||
)
|
||||
|
||||
# 处理完所有调用,继续请求
|
||||
msg = await self._closure(req_messages, query.use_model, query.use_funcs)
|
||||
|
||||
yield msg
|
||||
|
||||
pending_tool_calls = msg.tool_calls
|
||||
|
||||
req_messages.append(msg.dict(exclude_none=True))
|
||||
|
||||
async def request(self, query: core_entities.Query) -> AsyncGenerator[llm_entities.Message, None]:
|
||||
try:
|
||||
async for msg in self._request(query):
|
||||
yield msg
|
||||
return await self._closure(req_messages, model, funcs)
|
||||
except asyncio.TimeoutError:
|
||||
raise errors.RequesterError('请求超时')
|
||||
except openai.BadRequestError as e:
|
||||
@@ -163,6 +127,16 @@ class OpenAIChatCompletions(api.LLMAPIRequester):
|
||||
except openai.NotFoundError as e:
|
||||
raise errors.RequesterError(f'请求路径错误: {e.message}')
|
||||
except openai.RateLimitError as e:
|
||||
raise errors.RequesterError(f'请求过于频繁: {e.message}')
|
||||
raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}')
|
||||
except openai.APIError as e:
|
||||
raise errors.RequesterError(f'请求错误: {e.message}')
|
||||
|
||||
@async_lru.alru_cache(maxsize=128)
|
||||
async def get_base64_str(
|
||||
self,
|
||||
original_url: str,
|
||||
) -> str:
|
||||
|
||||
base64_image = await image.qq_image_url_to_base64(original_url)
|
||||
|
||||
return f"data:image/jpeg;base64,{base64_image}"
|
||||
|
||||
53
pkg/provider/modelmgr/apis/deepseekchatcmpl.py
Normal file
53
pkg/provider/modelmgr/apis/deepseekchatcmpl.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ....core import app
|
||||
|
||||
from . import chatcmpl
|
||||
from .. import api, entities, errors
|
||||
from ....core import entities as core_entities, app
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
|
||||
|
||||
@api.requester_class("deepseek-chat-completions")
|
||||
class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""Deepseek ChatCompletion API 请求器"""
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.requester_cfg = ap.provider_cfg.data['requester']['deepseek-chat-completions']
|
||||
self.ap = ap
|
||||
|
||||
async def _closure(
|
||||
self,
|
||||
req_messages: list[dict],
|
||||
use_model: entities.LLMModelInfo,
|
||||
use_funcs: list[tools_entities.LLMFunction] = None,
|
||||
) -> llm_entities.Message:
|
||||
self.client.api_key = use_model.token_mgr.get_token()
|
||||
|
||||
args = self.requester_cfg['args'].copy()
|
||||
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
|
||||
|
||||
if use_funcs:
|
||||
tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs)
|
||||
|
||||
if tools:
|
||||
args["tools"] = tools
|
||||
|
||||
# 设置此次请求中的messages
|
||||
messages = req_messages
|
||||
|
||||
# deepseek 不支持多模态,把content都转换成纯文字
|
||||
for m in messages:
|
||||
if 'content' in m and isinstance(m["content"], list):
|
||||
m["content"] = " ".join([c["text"] for c in m["content"]])
|
||||
|
||||
args["messages"] = messages
|
||||
|
||||
# 发送请求
|
||||
resp = await self._req(args)
|
||||
|
||||
# 处理请求结果
|
||||
message = await self._make_msg(resp)
|
||||
|
||||
return message
|
||||
@@ -3,7 +3,10 @@ from __future__ import annotations
|
||||
from ....core import app
|
||||
|
||||
from . import chatcmpl
|
||||
from .. import api
|
||||
from .. import api, entities, errors
|
||||
from ....core import entities as core_entities, app
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
|
||||
|
||||
@api.requester_class("moonshot-chat-completions")
|
||||
@@ -13,3 +16,41 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
def __init__(self, ap: app.Application):
|
||||
self.requester_cfg = ap.provider_cfg.data['requester']['moonshot-chat-completions']
|
||||
self.ap = ap
|
||||
|
||||
async def _closure(
|
||||
self,
|
||||
req_messages: list[dict],
|
||||
use_model: entities.LLMModelInfo,
|
||||
use_funcs: list[tools_entities.LLMFunction] = None,
|
||||
) -> llm_entities.Message:
|
||||
self.client.api_key = use_model.token_mgr.get_token()
|
||||
|
||||
args = self.requester_cfg['args'].copy()
|
||||
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
|
||||
|
||||
if use_funcs:
|
||||
tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs)
|
||||
|
||||
if tools:
|
||||
args["tools"] = tools
|
||||
|
||||
# 设置此次请求中的messages
|
||||
messages = req_messages
|
||||
|
||||
# deepseek 不支持多模态,把content都转换成纯文字
|
||||
for m in messages:
|
||||
if 'content' in m and isinstance(m["content"], list):
|
||||
m["content"] = " ".join([c["text"] for c in m["content"]])
|
||||
|
||||
# 删除空的
|
||||
messages = [m for m in messages if m["content"].strip() != ""]
|
||||
|
||||
args["messages"] = messages
|
||||
|
||||
# 发送请求
|
||||
resp = await self._req(args)
|
||||
|
||||
# 处理请求结果
|
||||
message = await self._make_msg(resp)
|
||||
|
||||
return message
|
||||
105
pkg/provider/modelmgr/apis/ollamachat.py
Normal file
105
pkg/provider/modelmgr/apis/ollamachat.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import typing
|
||||
from typing import Union, Mapping, Any, AsyncIterator
|
||||
|
||||
import async_lru
|
||||
import ollama
|
||||
|
||||
from .. import api, entities, errors
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
from ....core import app
|
||||
from ....utils import image
|
||||
|
||||
REQUESTER_NAME: str = "ollama-chat"
|
||||
|
||||
|
||||
@api.requester_class(REQUESTER_NAME)
|
||||
class OllamaChatCompletions(api.LLMAPIRequester):
|
||||
"""Ollama平台 ChatCompletion API请求器"""
|
||||
client: ollama.AsyncClient
|
||||
request_cfg: dict
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
super().__init__(ap)
|
||||
self.ap = ap
|
||||
self.request_cfg = self.ap.provider_cfg.data['requester'][REQUESTER_NAME]
|
||||
|
||||
async def initialize(self):
|
||||
os.environ['OLLAMA_HOST'] = self.request_cfg['base-url']
|
||||
self.client = ollama.AsyncClient(
|
||||
timeout=self.request_cfg['timeout']
|
||||
)
|
||||
|
||||
async def _req(self,
|
||||
args: dict,
|
||||
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
||||
return await self.client.chat(
|
||||
**args
|
||||
)
|
||||
|
||||
async def _closure(self, req_messages: list[dict], use_model: entities.LLMModelInfo,
|
||||
user_funcs: list[tools_entities.LLMFunction] = None) -> (
|
||||
llm_entities.Message):
|
||||
args: Any = self.request_cfg['args'].copy()
|
||||
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
|
||||
|
||||
messages: list[dict] = req_messages.copy()
|
||||
for msg in messages:
|
||||
if 'content' in msg and isinstance(msg["content"], list):
|
||||
text_content: list = []
|
||||
image_urls: list = []
|
||||
for me in msg["content"]:
|
||||
if me["type"] == "text":
|
||||
text_content.append(me["text"])
|
||||
elif me["type"] == "image_url":
|
||||
image_url = await self.get_base64_str(me["image_url"]['url'])
|
||||
image_urls.append(image_url)
|
||||
msg["content"] = "\n".join(text_content)
|
||||
msg["images"] = [url.split(',')[1] for url in image_urls]
|
||||
args["messages"] = messages
|
||||
|
||||
resp: Mapping[str, Any] | AsyncIterator[Mapping[str, Any]] = await self._req(args)
|
||||
message: llm_entities.Message = await self._make_msg(resp)
|
||||
return message
|
||||
|
||||
async def _make_msg(
|
||||
self,
|
||||
chat_completions: Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]) -> llm_entities.Message:
|
||||
message: Any = chat_completions.pop('message', None)
|
||||
if message is None:
|
||||
raise ValueError("chat_completions must contain a 'message' field")
|
||||
|
||||
message.update(chat_completions)
|
||||
ret_msg: llm_entities.Message = llm_entities.Message(**message)
|
||||
return ret_msg
|
||||
|
||||
async def call(
|
||||
self,
|
||||
model: entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
) -> llm_entities.Message:
|
||||
req_messages: list = []
|
||||
for m in messages:
|
||||
msg_dict: dict = m.dict(exclude_none=True)
|
||||
content: Any = msg_dict.get("content")
|
||||
if isinstance(content, list):
|
||||
if all(isinstance(part, dict) and part.get('type') == 'text' for part in content):
|
||||
msg_dict["content"] = "\n".join(part["text"] for part in content)
|
||||
req_messages.append(msg_dict)
|
||||
try:
|
||||
return await self._closure(req_messages, model)
|
||||
except asyncio.TimeoutError:
|
||||
raise errors.RequesterError('请求超时')
|
||||
|
||||
@async_lru.alru_cache(maxsize=128)
|
||||
async def get_base64_str(
|
||||
self,
|
||||
original_url: str,
|
||||
) -> str:
|
||||
base64_image: str = await image.qq_image_url_to_base64(original_url)
|
||||
return f"data:image/jpeg;base64,{base64_image}"
|
||||
@@ -21,5 +21,7 @@ class LLMModelInfo(pydantic.BaseModel):
|
||||
|
||||
tool_call_supported: typing.Optional[bool] = False
|
||||
|
||||
vision_supported: typing.Optional[bool] = False
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@@ -6,7 +6,7 @@ from . import entities
|
||||
from ...core import app
|
||||
|
||||
from . import token, api
|
||||
from .apis import chatcmpl, anthropicmsgs, moonshotchatcmpl
|
||||
from .apis import chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat
|
||||
|
||||
FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list"
|
||||
|
||||
@@ -37,7 +37,7 @@ class ModelManager:
|
||||
raise ValueError(f"无法确定模型 {name} 的信息,请在元数据中配置")
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
|
||||
# 初始化token_mgr, requester
|
||||
for k, v in self.ap.provider_cfg.data['keys'].items():
|
||||
self.token_mgrs[k] = token.TokenManager(k, v)
|
||||
@@ -83,7 +83,8 @@ class ModelManager:
|
||||
model_name=None,
|
||||
token_mgr=self.token_mgrs[model['token_mgr']],
|
||||
requester=self.requesters[model['requester']],
|
||||
tool_call_supported=model['tool_call_supported']
|
||||
tool_call_supported=model['tool_call_supported'],
|
||||
vision_supported=model['vision_supported']
|
||||
)
|
||||
break
|
||||
|
||||
@@ -95,13 +96,15 @@ class ModelManager:
|
||||
token_mgr = self.token_mgrs[model['token_mgr']] if 'token_mgr' in model else default_model_info.token_mgr
|
||||
requester = self.requesters[model['requester']] if 'requester' in model else default_model_info.requester
|
||||
tool_call_supported = model.get('tool_call_supported', default_model_info.tool_call_supported)
|
||||
vision_supported = model.get('vision_supported', default_model_info.vision_supported)
|
||||
|
||||
model_info = entities.LLMModelInfo(
|
||||
name=model['name'],
|
||||
model_name=model_name,
|
||||
token_mgr=token_mgr,
|
||||
requester=requester,
|
||||
tool_call_supported=tool_call_supported
|
||||
tool_call_supported=tool_call_supported,
|
||||
vision_supported=vision_supported
|
||||
)
|
||||
self.model_list.append(model_info)
|
||||
|
||||
|
||||
40
pkg/provider/runner.py
Normal file
40
pkg/provider/runner.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from ..core import app, entities as core_entities
|
||||
from . import entities as llm_entities
|
||||
|
||||
|
||||
preregistered_runners: list[typing.Type[RequestRunner]] = []
|
||||
|
||||
def runner_class(name: str):
|
||||
"""注册一个请求运行器
|
||||
"""
|
||||
def decorator(cls: typing.Type[RequestRunner]) -> typing.Type[RequestRunner]:
|
||||
cls.name = name
|
||||
preregistered_runners.append(cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class RequestRunner(abc.ABC):
|
||||
"""请求运行器
|
||||
"""
|
||||
name: str = None
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
"""运行请求
|
||||
"""
|
||||
pass
|
||||
27
pkg/provider/runnermgr.py
Normal file
27
pkg/provider/runnermgr.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from . import runner
|
||||
from ..core import app
|
||||
|
||||
from .runners import localagent
|
||||
|
||||
|
||||
class RunnerManager:
|
||||
|
||||
ap: app.Application
|
||||
|
||||
using_runner: runner.RequestRunner
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
for r in runner.preregistered_runners:
|
||||
if r.name == self.ap.provider_cfg.data['runner']:
|
||||
self.using_runner = r(self.ap)
|
||||
await self.using_runner.initialize()
|
||||
break
|
||||
|
||||
def get_runner(self) -> runner.RequestRunner:
|
||||
return self.using_runner
|
||||
0
pkg/provider/runners/__init__.py
Normal file
0
pkg/provider/runners/__init__.py
Normal file
70
pkg/provider/runners/localagent.py
Normal file
70
pkg/provider/runners/localagent.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import typing
|
||||
|
||||
from .. import runner
|
||||
from ...core import app, entities as core_entities
|
||||
from .. import entities as llm_entities
|
||||
|
||||
|
||||
@runner.runner_class("local-agent")
|
||||
class LocalAgentRunner(runner.RequestRunner):
|
||||
"""本地Agent请求运行器
|
||||
"""
|
||||
|
||||
async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
"""运行请求
|
||||
"""
|
||||
await query.use_model.requester.preprocess(query)
|
||||
|
||||
pending_tool_calls = []
|
||||
|
||||
req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message]
|
||||
|
||||
# 首次请求
|
||||
msg = await query.use_model.requester.call(query.use_model, req_messages, query.use_funcs)
|
||||
|
||||
yield msg
|
||||
|
||||
pending_tool_calls = msg.tool_calls
|
||||
|
||||
req_messages.append(msg)
|
||||
|
||||
# 持续请求,只要还有待处理的工具调用就继续处理调用
|
||||
while pending_tool_calls:
|
||||
for tool_call in pending_tool_calls:
|
||||
try:
|
||||
func = tool_call.function
|
||||
|
||||
parameters = json.loads(func.arguments)
|
||||
|
||||
func_ret = await self.ap.tool_mgr.execute_func_call(
|
||||
query, func.name, parameters
|
||||
)
|
||||
|
||||
msg = llm_entities.Message(
|
||||
role="tool", content=json.dumps(func_ret, ensure_ascii=False), tool_call_id=tool_call.id
|
||||
)
|
||||
|
||||
yield msg
|
||||
|
||||
req_messages.append(msg)
|
||||
except Exception as e:
|
||||
# 工具调用出错,添加一个报错信息到 req_messages
|
||||
err_msg = llm_entities.Message(
|
||||
role="tool", content=f"err: {e}", tool_call_id=tool_call.id
|
||||
)
|
||||
|
||||
yield err_msg
|
||||
|
||||
req_messages.append(err_msg)
|
||||
|
||||
# 处理完所有调用,再次请求
|
||||
msg = await query.use_model.requester.call(query.use_model, req_messages, query.use_funcs)
|
||||
|
||||
yield msg
|
||||
|
||||
pending_tool_calls = msg.tool_calls
|
||||
|
||||
req_messages.append(msg)
|
||||
@@ -9,11 +9,10 @@ from ...plugin import context as plugin_context
|
||||
|
||||
|
||||
class ToolManager:
|
||||
"""LLM工具管理器
|
||||
"""
|
||||
"""LLM工具管理器"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
self.all_functions = []
|
||||
@@ -22,35 +21,33 @@ class ToolManager:
|
||||
pass
|
||||
|
||||
async def get_function(self, name: str) -> entities.LLMFunction:
|
||||
"""获取函数
|
||||
"""
|
||||
"""获取函数"""
|
||||
for function in await self.get_all_functions():
|
||||
if function.name == name:
|
||||
return function
|
||||
return None
|
||||
|
||||
async def get_function_and_plugin(self, name: str) -> typing.Tuple[entities.LLMFunction, plugin_context.BasePlugin]:
|
||||
"""获取函数和插件
|
||||
"""
|
||||
|
||||
async def get_function_and_plugin(
|
||||
self, name: str
|
||||
) -> typing.Tuple[entities.LLMFunction, plugin_context.BasePlugin]:
|
||||
"""获取函数和插件"""
|
||||
for plugin in self.ap.plugin_mgr.plugins:
|
||||
for function in plugin.content_functions:
|
||||
if function.name == name:
|
||||
return function, plugin.plugin_inst
|
||||
return None, None
|
||||
|
||||
|
||||
async def get_all_functions(self) -> list[entities.LLMFunction]:
|
||||
"""获取所有函数
|
||||
"""
|
||||
"""获取所有函数"""
|
||||
all_functions: list[entities.LLMFunction] = []
|
||||
|
||||
|
||||
for plugin in self.ap.plugin_mgr.plugins:
|
||||
all_functions.extend(plugin.content_functions)
|
||||
|
||||
|
||||
return all_functions
|
||||
|
||||
async def generate_tools_for_openai(self, use_funcs: entities.LLMFunction) -> str:
|
||||
"""生成函数列表
|
||||
"""
|
||||
async def generate_tools_for_openai(self, use_funcs: list[entities.LLMFunction]) -> list:
|
||||
"""生成函数列表"""
|
||||
tools = []
|
||||
|
||||
for function in use_funcs:
|
||||
@@ -60,40 +57,71 @@ class ToolManager:
|
||||
"function": {
|
||||
"name": function.name,
|
||||
"description": function.description,
|
||||
"parameters": function.parameters
|
||||
}
|
||||
"parameters": function.parameters,
|
||||
},
|
||||
}
|
||||
tools.append(function_schema)
|
||||
|
||||
return tools
|
||||
|
||||
async def generate_tools_for_anthropic(
|
||||
self, use_funcs: list[entities.LLMFunction]
|
||||
) -> list:
|
||||
"""为anthropic生成函数列表
|
||||
|
||||
e.g.
|
||||
|
||||
[
|
||||
{
|
||||
"name": "get_stock_price",
|
||||
"description": "Get the current stock price for a given ticker symbol.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticker": {
|
||||
"type": "string",
|
||||
"description": "The stock ticker symbol, e.g. AAPL for Apple Inc."
|
||||
}
|
||||
},
|
||||
"required": ["ticker"]
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
tools = []
|
||||
|
||||
for function in use_funcs:
|
||||
if function.enable:
|
||||
function_schema = {
|
||||
"name": function.name,
|
||||
"description": function.description,
|
||||
"input_schema": function.parameters,
|
||||
}
|
||||
tools.append(function_schema)
|
||||
|
||||
return tools
|
||||
|
||||
async def execute_func_call(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
name: str,
|
||||
parameters: dict
|
||||
self, query: core_entities.Query, name: str, parameters: dict
|
||||
) -> typing.Any:
|
||||
"""执行函数调用
|
||||
"""
|
||||
"""执行函数调用"""
|
||||
|
||||
try:
|
||||
|
||||
function, plugin = await self.get_function_and_plugin(name)
|
||||
if function is None:
|
||||
return None
|
||||
|
||||
|
||||
parameters = parameters.copy()
|
||||
|
||||
parameters = {
|
||||
"query": query,
|
||||
**parameters
|
||||
}
|
||||
|
||||
parameters = {"query": query, **parameters}
|
||||
|
||||
return await function.func(plugin, **parameters)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}')
|
||||
self.ap.logger.error(f"执行函数 {name} 时发生错误: {e}")
|
||||
traceback.print_exc()
|
||||
return f'error occurred when executing function {name}: {e}'
|
||||
return f"error occurred when executing function {name}: {e}"
|
||||
finally:
|
||||
|
||||
plugin = None
|
||||
@@ -107,11 +135,11 @@ class ToolManager:
|
||||
|
||||
await self.ap.ctr_mgr.usage.post_function_record(
|
||||
plugin={
|
||||
'name': plugin.plugin_name,
|
||||
'remote': plugin.plugin_source,
|
||||
'version': plugin.plugin_version,
|
||||
'author': plugin.plugin_author
|
||||
"name": plugin.plugin_name,
|
||||
"remote": plugin.plugin_source,
|
||||
"version": plugin.plugin_version,
|
||||
"author": plugin.plugin_author,
|
||||
},
|
||||
function_name=function.name,
|
||||
function_description=function.description,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import typing
|
||||
import os
|
||||
import base64
|
||||
import logging
|
||||
|
||||
import pydantic
|
||||
import requests
|
||||
@@ -107,17 +108,20 @@ class AnnouncementManager:
|
||||
|
||||
async def show_announcements(
|
||||
self
|
||||
):
|
||||
) -> typing.Tuple[str, int]:
|
||||
"""显示公告"""
|
||||
try:
|
||||
announcements = await self.fetch_new()
|
||||
ann_text = ""
|
||||
for ann in announcements:
|
||||
self.ap.logger.info(f'[公告] {ann.time}: {ann.content}')
|
||||
ann_text += f"[公告] {ann.time}: {ann.content}\n"
|
||||
|
||||
if announcements:
|
||||
|
||||
await self.ap.ctr_mgr.main.post_announcement_showed(
|
||||
ids=[item.id for item in announcements]
|
||||
)
|
||||
|
||||
return ann_text, logging.INFO
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'获取公告时出错: {e}')
|
||||
return f'获取公告时出错: {e}', logging.WARNING
|
||||
|
||||
@@ -1 +1 @@
|
||||
semantic_version = "v3.1.0.4"
|
||||
semantic_version = "v3.3.0.1"
|
||||
|
||||
41
pkg/utils/image.py
Normal file
41
pkg/utils/image.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import base64
|
||||
import typing
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import ssl
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
async def qq_image_url_to_base64(
|
||||
image_url: str
|
||||
) -> str:
|
||||
"""将QQ图片URL转为base64
|
||||
|
||||
Args:
|
||||
image_url (str): QQ图片URL
|
||||
|
||||
Returns:
|
||||
str: base64编码
|
||||
"""
|
||||
parsed = urlparse(image_url)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
# Flatten the query dictionary
|
||||
query = {k: v[0] for k, v in query.items()}
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=False) as session:
|
||||
async with session.get(
|
||||
f"http://{parsed.netloc}{parsed.path}",
|
||||
params=query,
|
||||
ssl=ssl_context
|
||||
) as resp:
|
||||
resp.raise_for_status() # 检查HTTP错误
|
||||
file_bytes = await resp.read()
|
||||
|
||||
base64_str = base64.b64encode(file_bytes).decode()
|
||||
|
||||
return base64_str
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import typing
|
||||
import logging
|
||||
import time
|
||||
|
||||
import requests
|
||||
@@ -213,11 +215,11 @@ class VersionManager:
|
||||
|
||||
async def show_version_update(
|
||||
self
|
||||
):
|
||||
) -> typing.Tuple[str, int]:
|
||||
try:
|
||||
|
||||
if await self.ap.ver_mgr.is_new_version_available():
|
||||
self.ap.logger.info("有新版本可用,请使用 !update 命令更新")
|
||||
return "有新版本可用,请使用管理员账号发送 !update 命令更新", logging.INFO
|
||||
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f"检查版本更新时出错: {e}")
|
||||
return f"检查版本更新时出错: {e}", logging.WARNING
|
||||
|
||||
@@ -13,4 +13,6 @@ aiohttp
|
||||
pydantic
|
||||
websockets
|
||||
urllib3
|
||||
psutil
|
||||
psutil
|
||||
async-lru
|
||||
ollama
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"privilege": {}
|
||||
"privilege": {},
|
||||
"command-prefix": [
|
||||
"!",
|
||||
"!"
|
||||
]
|
||||
}
|
||||
@@ -4,23 +4,73 @@
|
||||
"name": "default",
|
||||
"requester": "openai-chat-completions",
|
||||
"token_mgr": "openai",
|
||||
"tool_call_supported": false
|
||||
"tool_call_supported": false,
|
||||
"vision_supported": false
|
||||
},
|
||||
{
|
||||
"name": "gpt-3.5-turbo-0125",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": false
|
||||
},
|
||||
{
|
||||
"name": "gpt-3.5-turbo",
|
||||
"tool_call_supported": true
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": false
|
||||
},
|
||||
{
|
||||
"name": "gpt-4",
|
||||
"tool_call_supported": true
|
||||
"name": "gpt-3.5-turbo-1106",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": false
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-turbo",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-turbo-2024-04-09",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-turbo-preview",
|
||||
"tool_call_supported": true
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-0125-preview",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-1106-preview",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4o",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-0613",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-32k",
|
||||
"tool_call_supported": true
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "gpt-4-32k-0613",
|
||||
"tool_call_supported": true,
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"model_name": "SparkDesk",
|
||||
@@ -33,32 +83,43 @@
|
||||
{
|
||||
"name": "claude-3-opus-20240229",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic"
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "claude-3-sonnet-20240229",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic"
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "claude-3-haiku-20240307",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic"
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true
|
||||
},
|
||||
{
|
||||
"name": "moonshot-v1-8k",
|
||||
"requester": "moonshot-chat-completions",
|
||||
"token_mgr": "moonshot"
|
||||
"token_mgr": "moonshot",
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "moonshot-v1-32k",
|
||||
"requester": "moonshot-chat-completions",
|
||||
"token_mgr": "moonshot"
|
||||
"token_mgr": "moonshot",
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "moonshot-v1-128k",
|
||||
"requester": "moonshot-chat-completions",
|
||||
"token_mgr": "moonshot"
|
||||
"token_mgr": "moonshot",
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "deepseek-chat",
|
||||
"requester": "deepseek-chat-completions",
|
||||
"token_mgr": "deepseek"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,12 +25,20 @@
|
||||
"api-key": "",
|
||||
"api-secret": ""
|
||||
},
|
||||
"submit-messages-tokens": 3072,
|
||||
"rate-limit": {
|
||||
"strategy": "drop",
|
||||
"algo": "fixwin",
|
||||
"fixwin": {
|
||||
"default": 60
|
||||
"default": {
|
||||
"window-size": 60,
|
||||
"limit": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
"msg-truncate": {
|
||||
"method": "round",
|
||||
"round": {
|
||||
"max-round": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"enable-chat": true,
|
||||
"enable-vision": true,
|
||||
"keys": {
|
||||
"openai": [
|
||||
"sk-1234567890"
|
||||
@@ -9,6 +10,9 @@
|
||||
],
|
||||
"moonshot": [
|
||||
"sk-1234567890"
|
||||
],
|
||||
"deepseek": [
|
||||
"sk-1234567890"
|
||||
]
|
||||
},
|
||||
"requester": {
|
||||
@@ -28,11 +32,22 @@
|
||||
"base-url": "https://api.moonshot.cn/v1",
|
||||
"args": {},
|
||||
"timeout": 120
|
||||
},
|
||||
"deepseek-chat-completions": {
|
||||
"base-url": "https://api.deepseek.com",
|
||||
"args": {},
|
||||
"timeout": 120
|
||||
},
|
||||
"ollama-chat": {
|
||||
"base-url": "http://127.0.0.1:11434",
|
||||
"args": {},
|
||||
"timeout": 600
|
||||
}
|
||||
},
|
||||
"model": "gpt-3.5-turbo",
|
||||
"prompt-mode": "normal",
|
||||
"prompt": {
|
||||
"default": ""
|
||||
}
|
||||
},
|
||||
"runner": "local-agent"
|
||||
}
|
||||
@@ -10,5 +10,6 @@
|
||||
"default": 1
|
||||
},
|
||||
"pipeline-concurrency": 20,
|
||||
"qcg-center-url": "https://api.qchatgpt.rockchin.top/api/v2",
|
||||
"help-message": "QChatGPT - 😎高稳定性、🧩支持插件、🌏实时联网的 ChatGPT QQ 机器人🤖\n链接:https://q.rkcn.top"
|
||||
}
|
||||
Reference in New Issue
Block a user