Compare commits

...

27 Commits

Author SHA1 Message Date
RockChinQ
45a10b4ac7 chore: release v3.2.4 2024-07-05 18:19:10 +08:00
RockChinQ
b5d33ef629 perf: 优化 pipeline 处理时的报错 2024-07-04 13:03:58 +08:00
RockChinQ
d3629916bf fix: user_notice 处理时为对齐为 MessageChain (#809) 2024-07-04 12:47:55 +08:00
RockChinQ
c5cb26d295 fix: GroupNormalMessageReceived事件设置 alter 无效 (#803) 2024-07-03 23:16:16 +08:00
RockChinQ
4b2785c5eb fix: QQ 官方 API 图片识别功能不正常 (#825) 2024-07-03 22:36:35 +08:00
RockChinQ
7ed190e6d2 doc: 删除广告 2024-07-03 17:50:58 +08:00
Junyan Qin
eac041cdd2 Merge pull request #834 from RockChinQ/feat/env-reminder
Feat: 添加启动信息阶段
2024-07-03 17:45:56 +08:00
RockChinQ
05527cfc01 feat: 添加 windows 下针对选择模式的提示 2024-07-03 17:44:10 +08:00
RockChinQ
61e2af4a14 feat: 添加启动信息阶段 2024-07-03 17:34:23 +08:00
RockChinQ
79804b6ecd chore: release 3.2.3 2024-06-26 10:55:21 +08:00
Junyan Qin
76434b2f4e Merge pull request #829 from RockChinQ/version/3.2.3
Release 3.2.3
2024-06-26 10:54:42 +08:00
RockChinQ
ec8bd4922e fix: 错误地resprule选择逻辑 (#810) 2024-06-26 10:37:08 +08:00
RockChinQ
4ffa773fac fix: 前缀响应时图片被错误地转换为文字 (#820) 2024-06-26 10:15:21 +08:00
Junyan Qin
ea8b7bc8aa Merge pull request #818 from Huoyuuu/master
fix: ensure content is string in chatcmpl call method
2024-06-24 17:12:46 +08:00
RockChinQ
39ce5646f6 perf: content元素拼接时使用换行符间隔 2024-06-24 17:04:50 +08:00
Huoyuuu
5092a82739 Update chatcmpl.py 2024-06-19 19:13:00 +08:00
Huoyuuu
3bba0b6d9a Merge pull request #1 from Huoyuuu/fix/issue-817-ensure-content-string
fix: ensure content is string in chatcmpl call method
2024-06-19 17:32:30 +08:00
Huoyuuu
7a19dd503d fix: ensure content is string in chatcmpl call method
fix: ensure content is string in chatcmpl call method

- Ensure user message content is a string instead of an array
- Updated `call` method in `chatcmpl.py` to guarantee content is a string
- Resolves compatibility issue with the yi-large model
2024-06-19 17:26:06 +08:00
RockChinQ
9e6a01fefd chore: release v3.2.2 2024-05-31 19:20:34 +08:00
RockChinQ
933471b4d9 perf: 启动失败时输出完整traceback (#799) 2024-05-31 15:37:56 +08:00
RockChinQ
f81808d239 perf: 添加JSON配置文件语法检查 (#796) 2024-05-29 21:11:21 +08:00
RockChinQ
96832b6f7d perf: 忽略空的 assistant content 消息 (#795) 2024-05-29 21:00:48 +08:00
Junyan Qin
e2eb0a84b0 Merge pull request #797 from RockChinQ/feat/context-truncater
Feat: 消息截断器
2024-05-29 20:38:14 +08:00
RockChinQ
c8eb2e3376 feat: 消息截断器 2024-05-29 20:34:49 +08:00
Junyan Qin
21fe5822f9 Merge pull request #794 from RockChinQ/perf/advanced-fixwin
Feat: fixwin限速支持设置窗口大小
2024-05-26 10:33:49 +08:00
RockChinQ
d49cc9a7a3 feat: fixwin限速支持设置窗口大小 (#791) 2024-05-26 10:29:10 +08:00
Junyan Qin
910d0bfae1 Update README.md 2024-05-25 12:27:27 +08:00
32 changed files with 392 additions and 62 deletions

View File

@@ -37,7 +37,10 @@ class JSONConfigFile(file_model.ConfigFile):
self.template_data = json.load(f) self.template_data = json.load(f)
with open(self.config_file_name, "r", encoding="utf-8") as 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}")
if completion: if completion:

View 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()

View 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()

View File

@@ -15,7 +15,7 @@ from ..command import cmdmgr
from ..plugin import manager as plugin_mgr from ..plugin import manager as plugin_mgr
from ..pipeline import pool from ..pipeline import pool
from ..pipeline import controller, stagemgr 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: class Application:
@@ -69,6 +69,8 @@ class Application:
ver_mgr: version_mgr.VersionManager = None ver_mgr: version_mgr.VersionManager = None
ann_mgr: announce_mgr.AnnouncementManager = None
proxy_mgr: proxy_mgr.ProxyManager = None proxy_mgr: proxy_mgr.ProxyManager = None
logger: logging.Logger = None logger: logging.Logger = None

View File

@@ -1,18 +1,21 @@
from __future__ import print_function from __future__ import print_function
import traceback
from . import app from . import app
from ..audit import identifier from ..audit import identifier
from . import stage 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 = [ stage_order = [
"LoadConfigStage", "LoadConfigStage",
"MigrationStage", "MigrationStage",
"SetupLoggerStage", "SetupLoggerStage",
"BuildAppStage" "BuildAppStage",
"ShowNotesStage"
] ]
@@ -27,6 +30,7 @@ async def make_app() -> app.Application:
for stage_name in stage_order: for stage_name in stage_order:
stage_cls = stage.preregistered_stages[stage_name] stage_cls = stage.preregistered_stages[stage_name]
stage_inst = stage_cls() stage_inst = stage_cls()
await stage_inst.run(ap) await stage_inst.run(ap)
await ap.initialize() await ap.initialize()
@@ -35,5 +39,8 @@ async def make_app() -> app.Application:
async def main(): async def main():
app_inst = await make_app() try:
await app_inst.run() app_inst = await make_app()
await app_inst.run()
except Exception as e:
traceback.print_exc()

View File

@@ -8,16 +8,3 @@ from ...config.impls import pymodule
load_python_module_config = config_mgr.load_python_module_config load_python_module_config = config_mgr.load_python_module_config
load_json_config = config_mgr.load_json_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

View File

@@ -73,6 +73,9 @@ class Query(pydantic.BaseModel):
resp_message_chain: typing.Optional[list[mirai.MessageChain]] = None resp_message_chain: typing.Optional[list[mirai.MessageChain]] = None
"""回复消息链从resp_messages包装而得""" """回复消息链从resp_messages包装而得"""
# ======= 内部保留 =======
current_stage: "pkg.pipeline.stagemgr.StageInstContainer" = None
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True

44
pkg/core/note.py Normal file
View 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

View File

View 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()

View 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

View File

@@ -53,12 +53,10 @@ class BuildAppStage(stage.BootingStage):
# 发送公告 # 发送公告
ann_mgr = announce.AnnouncementManager(ap) ann_mgr = announce.AnnouncementManager(ap)
await ann_mgr.show_announcements() ap.ann_mgr = ann_mgr
ap.query_pool = pool.QueryPool() ap.query_pool = pool.QueryPool()
await ap.ver_mgr.show_version_update()
plugin_mgr_inst = plugin_mgr.PluginManager(ap) plugin_mgr_inst = plugin_mgr.PluginManager(ap)
await plugin_mgr_inst.initialize() await plugin_mgr_inst.initialize()
ap.plugin_mgr = plugin_mgr_inst ap.plugin_mgr = plugin_mgr_inst

View File

@@ -5,7 +5,7 @@ import importlib
from .. import stage, app from .. import stage, app
from ...config import migration 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 ...config.migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion
from ...config.migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url from ...config.migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg
@stage.stage_class("MigrationStage") @stage.stage_class("MigrationStage")

View 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

View File

@@ -4,6 +4,8 @@ import asyncio
import typing import typing
import traceback import traceback
import mirai
from ..core import app, entities from ..core import app, entities
from . import entities as pipeline_entities from . import entities as pipeline_entities
from ..plugin import events from ..plugin import events
@@ -68,6 +70,17 @@ class Controller:
"""检查输出 """检查输出
""" """
if result.user_notice: 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( await self.ap.platform_mgr.send(
query.message_event, query.message_event,
result.user_notice, result.user_notice,
@@ -109,6 +122,8 @@ class Controller:
while i < len(self.ap.stage_mgr.stage_containers): while i < len(self.ap.stage_mgr.stage_containers):
stage_container = self.ap.stage_mgr.stage_containers[i] 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) result = stage_container.inst.process(query, stage_container.inst_name)
@@ -149,7 +164,7 @@ class Controller:
try: try:
await self._execute_from_stage(0, query) await self._execute_from_stage(0, query)
except Exception as e: 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()}") self.ap.logger.debug(f"Traceback: {traceback.format_exc()}")
# traceback.print_exc() # traceback.print_exc()
finally: finally:

View File

View 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
)

View 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

View 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

View File

@@ -62,9 +62,8 @@ class ChatMessageHandler(handler.MessageHandler):
) )
if event_ctx.event.alter is not None: if event_ctx.event.alter is not None:
query.message_chain = mirai.MessageChain([ # if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter
mirai.Plain(event_ctx.event.alter) query.user_message.content = event_ctx.event.alter
])
text_length = 0 text_length = 0

View File

@@ -1,18 +1,15 @@
# 固定窗口算法
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import time import time
from .. import algo from .. import algo
# 固定窗口算法
class SessionContainer: class SessionContainer:
wait_lock: asyncio.Lock wait_lock: asyncio.Lock
records: dict[int, int] records: dict[int, int]
"""访问记录key为每分钟的起始时间戳value为访问次数""" """访问记录key为每窗口长度的起始时间戳value为访问次数"""
def __init__(self): def __init__(self):
self.wait_lock = asyncio.Lock() self.wait_lock = asyncio.Lock()
@@ -47,30 +44,34 @@ class FixedWindowAlgo(algo.ReteLimitAlgo):
# 等待锁 # 等待锁
async with container.wait_lock: 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 = int(time.time())
# 获取当前分钟的起始时间戳 # 获取当前窗口的起始时间戳
now = now - now % 60 now = now - now % window_size
# 获取当前分钟的访问次数 # 获取当前窗口的访问次数
count = container.records.get(now, 0) 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 count >= limitation:
if self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'drop': if self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'drop':
return False return False
elif self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'wait': 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 = int(time.time())
now = now - now % 60 now = now - now % window_size
if now not in container.records: if now not in container.records:
container.records = {} container.records = {}

View File

@@ -44,8 +44,8 @@ class GroupRespondRuleCheckStage(stage.PipelineStage):
use_rule = rules['default'] use_rule = rules['default']
if str(query.launcher_id) in use_rule: if str(query.launcher_id) in rules:
use_rule = use_rule[str(query.launcher_id)] use_rule = rules[str(query.launcher_id)]
for rule_matcher in self.rule_matchers: # 任意一个匹配就放行 for rule_matcher in self.rule_matchers: # 任意一个匹配就放行
res = await rule_matcher.match(str(query.message_chain), query.message_chain, use_rule, query) res = await rule_matcher.match(str(query.message_chain), query.message_chain, use_rule, query)

View File

@@ -20,11 +20,14 @@ class PrefixRule(rule_model.GroupRespondRule):
for prefix in prefixes: for prefix in prefixes:
if message_text.startswith(prefix): 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( return entities.RuleJudgeResult(
matching=True, matching=True,
replacement=mirai.MessageChain([ replacement=message_chain,
mirai.Plain(message_text[len(prefix):])
]),
) )
return entities.RuleJudgeResult( return entities.RuleJudgeResult(

View File

@@ -13,6 +13,7 @@ from .respback import respback
from .wrapper import wrapper from .wrapper import wrapper
from .preproc import preproc from .preproc import preproc
from .ratelimit import ratelimit from .ratelimit import ratelimit
from .msgtrun import msgtrun
# 请求处理阶段顺序 # 请求处理阶段顺序
@@ -21,6 +22,7 @@ stage_order = [
"BanSessionCheckStage", # 封禁会话检查 "BanSessionCheckStage", # 封禁会话检查
"PreContentFilterStage", # 内容过滤前置阶段 "PreContentFilterStage", # 内容过滤前置阶段
"PreProcessor", # 预处理器 "PreProcessor", # 预处理器
"ConversationMessageTruncator", # 会话消息截断器
"RequireRateLimitOccupancy", # 请求速率限制占用 "RequireRateLimitOccupancy", # 请求速率限制占用
"MessageProcessor", # 处理器 "MessageProcessor", # 处理器
"ReleaseRateLimitOccupancy", # 释放速率限制占用 "ReleaseRateLimitOccupancy", # 释放速率限制占用

View File

@@ -71,7 +71,7 @@ class ResponseWrapper(stage.PipelineStage):
reply_text = '' reply_text = ''
if result.content is not None: # 有内容 if result.content: # 有内容
reply_text = str(result.get_content_mirai_message_chain()) reply_text = str(result.get_content_mirai_message_chain())
# ============= 触发插件事件 =============== # ============= 触发插件事件 ===============

View File

@@ -198,7 +198,6 @@ class OfficialMessageConverter(adapter_model.MessageConverter):
bot_account_id: int = 0, bot_account_id: int = 0,
) -> mirai.MessageChain: ) -> mirai.MessageChain:
yiri_msg_list = [] yiri_msg_list = []
# 存id # 存id
yiri_msg_list.append( yiri_msg_list.append(
@@ -218,7 +217,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter):
yiri_msg_list.append(mirai.At(target=mention.id)) yiri_msg_list.append(mirai.At(target=mention.id))
for attachment in message.attachments: 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)) yiri_msg_list.append(mirai.Image(url=attachment.url))
else: else:
logging.warning( logging.warning(

View File

@@ -102,9 +102,16 @@ class OpenAIChatCompletions(api.LLMAPIRequester):
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,
) -> llm_entities.Message: ) -> llm_entities.Message:
req_messages = [ # req_messages 仅用于类内,外部同步由 query.messages 进行 req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行
m.dict(exclude_none=True) for m in 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)
try: try:
return await self._closure(req_messages, model, funcs) return await self._closure(req_messages, model, funcs)

View File

@@ -4,6 +4,7 @@ import json
import typing import typing
import os import os
import base64 import base64
import logging
import pydantic import pydantic
import requests import requests
@@ -107,17 +108,20 @@ class AnnouncementManager:
async def show_announcements( async def show_announcements(
self self
): ) -> typing.Tuple[str, int]:
"""显示公告""" """显示公告"""
try: try:
announcements = await self.fetch_new() announcements = await self.fetch_new()
ann_text = ""
for ann in announcements: for ann in announcements:
self.ap.logger.info(f'[公告] {ann.time}: {ann.content}') ann_text += f"[公告] {ann.time}: {ann.content}\n"
if announcements: if announcements:
await self.ap.ctr_mgr.main.post_announcement_showed( await self.ap.ctr_mgr.main.post_announcement_showed(
ids=[item.id for item in announcements] ids=[item.id for item in announcements]
) )
return ann_text, logging.INFO
except Exception as e: except Exception as e:
self.ap.logger.warning(f'获取公告时出错: {e}') return f'获取公告时出错: {e}', logging.WARNING

View File

@@ -1 +1 @@
semantic_version = "v3.2.1" semantic_version = "v3.2.4"

View File

@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os import os
import typing
import logging
import time import time
import requests import requests
@@ -213,11 +215,11 @@ class VersionManager:
async def show_version_update( async def show_version_update(
self self
): ) -> typing.Tuple[str, int]:
try: try:
if await self.ap.ver_mgr.is_new_version_available(): if await self.ap.ver_mgr.is_new_version_available():
self.ap.logger.info("有新版本可用,请使用 !update 命令更新") return "有新版本可用,请使用管理员账号发送 !update 命令更新", logging.INFO
except Exception as e: except Exception as e:
self.ap.logger.warning(f"检查版本更新时出错: {e}") return f"检查版本更新时出错: {e}", logging.WARNING

View File

@@ -29,7 +29,16 @@
"strategy": "drop", "strategy": "drop",
"algo": "fixwin", "algo": "fixwin",
"fixwin": { "fixwin": {
"default": 60 "default": {
"window-size": 60,
"limit": 60
}
}
},
"msg-truncate": {
"method": "round",
"round": {
"max-round": 10
} }
} }
} }