Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1759ca2ed | |||
| 912e40e7f0 | |||
| 2876c43387 | |||
| 464882f206 | |||
| 6736fb85c2 | |||
| 1f75255950 | |||
| a954e75547 | |||
| d2b9997620 | |||
| 36432c4361 | |||
| 36f0d1f0f9 | |||
| f65b268bb2 | |||
| fe06dfcca3 | |||
| bc9043bc3f | |||
| 430694aae9 | |||
| c643e3c093 | |||
| ff46eef3b2 | |||
| a0c364aa81 | |||
| 0e0f923a49 | |||
| f2d637b935 | |||
| 96e61a4a92 | |||
| e42c1b6da8 | |||
| 387bba093e | |||
| 123cf9cb11 |
@@ -1,18 +0,0 @@
|
|||||||
我需要让 Agent 能够在未来提醒自己去做某些事情,这样 Agent 能够主动地去完成一些任务,而不是等用户主动来下达命令。
|
|
||||||
|
|
||||||
你需要实现一个 CronJob 系统,允许 Agent 创建未来任务,并且在未来的某个时间点自动触发这些任务的执行.
|
|
||||||
|
|
||||||
CronJob 系统分为 BasicCronJob 和 ActiveAgentCronJob 两种类型。前者只是简单的提供一个定时任务功能(给插件用),而后者则允许 Agent 主动地去完成一些任务。BasicCronJob 不必多说,就是定时执行某个函数。对于 ActiveAgentCronJob,Agent 应该可以主动管理(比如通过Tool来管理)这些 CronJobs,当添加的时候,Agent 可以给 CronJob 捎一段文字,以说明未来的自己需要做什么事情。比如说,Agent 在听到用户 “每天早上都给我整理一份今日早报” 之后,应该可以创建 Cron Job,并且自己写脚本来完成这个任务,并且注册 cron job。Agent 给未来的自己捎去的信息应该只是呈现为一段文字,这样可以保持设计简约。当触发后, CronJobManager 会调用 MainAgent 的一轮循环,MainAgent 通过上下文知道这是一个定时任务触发的循环,从而执行相应的操作。
|
|
||||||
|
|
||||||
此外,我还有一个需求,后台长任务。需要给当前的 FunctionTool 类增加一个属性,is_background_task: bool = False,插件可以通过这个属性来声明这是一个异步任务。这是为了解决一些 Tool 需要长时间运行的问题,比如 Deep Search tool 需要长时间搜索网页内容、Sub Agent 需要长时间运行来完成一个复杂任务。
|
|
||||||
|
|
||||||
基于上面的讨论,我觉得,应该:
|
|
||||||
|
|
||||||
1. 需要给当前的 FunctionTool 类增加一个属性is_background_task: bool = False,tool runner 在执行这个 tool 的时候,如果发现是后台任务,就不等待结果返回,而是直接返回一个任务 ID (已经创建成功提示)的结果,tool runner 在后台继续执行这个任务。当任务完成之后,任务的结果回传给 MainAgent(其实就是再执行一次 main agent loop,但是上下文应该是最新的),并且 MainAgent 此时应该有 send_message_to_user 的工具,通过这个工具可以选择是否主动通知用户任务完成的结果。
|
|
||||||
2. 增加一个 CronJobManager 类,负责管理所有的定时任务。Agent 可以通过调用这个类的方法来创建、删除、修改定时任务。通过 cron expression 来定义触发条件。
|
|
||||||
3. CronJobManager 除了管理普通的定时任务(比如插件可能有一些自己的定时任务),还有一种特殊的任务类型,就是上面提到的主动型 Agent 任务。用户提需求,MainAgent 选择性地调用 CronJobManager 的方法来创建这些任务,并且在任务触发时,CronJobManager 的回调就是执行 MainAgent 的一轮循环(需要加 send_message_to_user tool),MainAgent 通过上下文知道这是一个定时任务触发的循环,从而执行相应的操作。
|
|
||||||
4. WebUI 需要增加 Cron Job 管理界面,用户可以在界面上查看、创建、修改、删除定时任务。对于主动型 Agent 任务,用户可以看到任务的描述、触发条件等信息。
|
|
||||||
5. 除此之外,现在的代码中已经有了 subagent 的管理。WebUI 可以创建 SubAgent,但是还没写完。除了结合上面我说的之外,你还需要将 SubAgent 与 Persona 结合起来——因为 Persona 是一个包含了 tool、skills、name、description 的完整体,所以 SubAgent 应该直接继承 Persona 的定义,而不是单独定义 SubAgent。SubAgent 本质上就是一个有特定角色和能力的 Persona!多么美妙的设计啊!
|
|
||||||
6. 为了实现大一统,is_background_task = True 的时候,后台任务也挂到 CronJobManager 上去管理,只不过这个是立即触发的任务,不需要等到未来某个时间点才触发罢了。
|
|
||||||
|
|
||||||
我希望设计尽可能简单,但是强大。
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
|
AstrBot 是一个易用、高性能的 AI Agentic 个人 / 群聊助手。可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -67,8 +67,6 @@ AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
陪伴与能力**从来不应该是**对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人——致敬[ATRI](https://zh.wikipedia.org/zh-cn/ATRI_-My_Dear_Moments-)。
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
#### Docker 部署(推荐 🥳)
|
#### Docker 部署(推荐 🥳)
|
||||||
@@ -268,6 +266,6 @@ pre-commit install
|
|||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
|
||||||
</div
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ class Main(star.Star):
|
|||||||
|
|
||||||
yield event.request_llm(
|
yield event.request_llm(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
func_tool_manager=self.context.get_llm_tool_manager(),
|
|
||||||
session_id=event.session_id,
|
session_id=event.session_id,
|
||||||
conversation=conv,
|
conversation=conv,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class Main(Star):
|
|||||||
if p_settings.get("empty_mention_waiting_need_reply", True):
|
if p_settings.get("empty_mention_waiting_need_reply", True):
|
||||||
try:
|
try:
|
||||||
# 尝试使用 LLM 生成更生动的回复
|
# 尝试使用 LLM 生成更生动的回复
|
||||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
# func_tools_mgr = self.context.get_llm_tool_manager()
|
||||||
|
|
||||||
# 获取用户当前的对话信息
|
# 获取用户当前的对话信息
|
||||||
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||||
@@ -76,7 +76,6 @@ class Main(Star):
|
|||||||
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
|
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
|
||||||
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
|
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
|
||||||
),
|
),
|
||||||
func_tool_manager=func_tools_mgr,
|
|
||||||
session_id=curr_cid,
|
session_id=curr_cid,
|
||||||
contexts=[],
|
contexts=[],
|
||||||
system_prompt="",
|
system_prompt="",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.13.2"
|
__version__ = "4.14.4"
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
if not llm_response.is_chunk and llm_response.usage:
|
if not llm_response.is_chunk and llm_response.usage:
|
||||||
# only count the token usage of the final response for computation purpose
|
# only count the token usage of the final response for computation purpose
|
||||||
self.stats.token_usage += llm_response.usage
|
self.stats.token_usage += llm_response.usage
|
||||||
|
if self.req.conversation:
|
||||||
|
self.req.conversation.token_usage = llm_response.usage.total
|
||||||
break # got final response
|
break # got final response
|
||||||
|
|
||||||
if not llm_resp_result:
|
if not llm_resp_result:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
from collections.abc import Coroutine
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from astrbot.api import sp
|
from astrbot.api import sp
|
||||||
@@ -114,6 +115,7 @@ class MainAgentBuildResult:
|
|||||||
agent_runner: AgentRunner
|
agent_runner: AgentRunner
|
||||||
provider_request: ProviderRequest
|
provider_request: ProviderRequest
|
||||||
provider: Provider
|
provider: Provider
|
||||||
|
reset_coro: Coroutine | None = None
|
||||||
|
|
||||||
|
|
||||||
def _select_provider(
|
def _select_provider(
|
||||||
@@ -837,8 +839,12 @@ async def build_main_agent(
|
|||||||
config: MainAgentBuildConfig,
|
config: MainAgentBuildConfig,
|
||||||
provider: Provider | None = None,
|
provider: Provider | None = None,
|
||||||
req: ProviderRequest | None = None,
|
req: ProviderRequest | None = None,
|
||||||
|
apply_reset: bool = True,
|
||||||
) -> MainAgentBuildResult | None:
|
) -> MainAgentBuildResult | None:
|
||||||
"""构建主对话代理(Main Agent),并且自动 reset。"""
|
"""构建主对话代理(Main Agent),并且自动 reset。
|
||||||
|
|
||||||
|
If apply_reset is False, will not call reset on the agent runner.
|
||||||
|
"""
|
||||||
provider = provider or _select_provider(event, plugin_context)
|
provider = provider or _select_provider(event, plugin_context)
|
||||||
if provider is None:
|
if provider is None:
|
||||||
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
|
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
|
||||||
@@ -955,7 +961,7 @@ async def build_main_agent(
|
|||||||
if action_type == "live":
|
if action_type == "live":
|
||||||
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
|
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
|
||||||
|
|
||||||
await agent_runner.reset(
|
reset_coro = agent_runner.reset(
|
||||||
provider=provider,
|
provider=provider,
|
||||||
request=req,
|
request=req,
|
||||||
run_context=AgentContextWrapper(
|
run_context=AgentContextWrapper(
|
||||||
@@ -973,8 +979,12 @@ async def build_main_agent(
|
|||||||
tool_schema_mode=config.tool_schema_mode,
|
tool_schema_mode=config.tool_schema_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if apply_reset:
|
||||||
|
await reset_coro
|
||||||
|
|
||||||
return MainAgentBuildResult(
|
return MainAgentBuildResult(
|
||||||
agent_runner=agent_runner,
|
agent_runner=agent_runner,
|
||||||
provider_request=req,
|
provider_request=req,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
|
reset_coro=reset_coro if not apply_reset else None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
|||||||
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
VERSION = "4.13.2"
|
VERSION = "4.14.4"
|
||||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||||
|
|
||||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ class CronJobManager:
|
|||||||
config = MainAgentBuildConfig(
|
config = MainAgentBuildConfig(
|
||||||
tool_call_timeout=3600,
|
tool_call_timeout=3600,
|
||||||
llm_safety_mode=False,
|
llm_safety_mode=False,
|
||||||
|
streaming_response=False,
|
||||||
)
|
)
|
||||||
req = ProviderRequest()
|
req = ProviderRequest()
|
||||||
conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx)
|
conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx)
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ class InternalAgentSubStage(Stage):
|
|||||||
event=event,
|
event=event,
|
||||||
plugin_context=self.ctx.plugin_manager.context,
|
plugin_context=self.ctx.plugin_manager.context,
|
||||||
config=build_cfg,
|
config=build_cfg,
|
||||||
|
apply_reset=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if build_result is None:
|
if build_result is None:
|
||||||
@@ -172,6 +173,7 @@ class InternalAgentSubStage(Stage):
|
|||||||
agent_runner = build_result.agent_runner
|
agent_runner = build_result.agent_runner
|
||||||
req = build_result.provider_request
|
req = build_result.provider_request
|
||||||
provider = build_result.provider
|
provider = build_result.provider
|
||||||
|
reset_coro = build_result.reset_coro
|
||||||
|
|
||||||
api_base = provider.provider_config.get("api_base", "")
|
api_base = provider.provider_config.get("api_base", "")
|
||||||
for host in decoded_blocked:
|
for host in decoded_blocked:
|
||||||
@@ -190,6 +192,10 @@ class InternalAgentSubStage(Stage):
|
|||||||
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
|
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# apply reset
|
||||||
|
if reset_coro:
|
||||||
|
await reset_coro
|
||||||
|
|
||||||
action_type = event.get_extra("action_type")
|
action_type = event.get_extra("action_type")
|
||||||
|
|
||||||
event.trace.record(
|
event.trace.record(
|
||||||
@@ -357,7 +363,8 @@ class InternalAgentSubStage(Stage):
|
|||||||
|
|
||||||
token_usage = None
|
token_usage = None
|
||||||
if runner_stats:
|
if runner_stats:
|
||||||
token_usage = runner_stats.token_usage.total
|
# token_usage = runner_stats.token_usage.total
|
||||||
|
token_usage = llm_response.usage.total if llm_response.usage else None
|
||||||
|
|
||||||
await self.conv_manager.update_conversation(
|
await self.conv_manager.update_conversation(
|
||||||
event.unified_msg_origin,
|
event.unified_msg_origin,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from time import time
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
|
from astrbot.core.agent.tool import ToolSet
|
||||||
from astrbot.core.db.po import Conversation
|
from astrbot.core.db.po import Conversation
|
||||||
from astrbot.core.message.components import (
|
from astrbot.core.message.components import (
|
||||||
At,
|
At,
|
||||||
@@ -355,6 +356,7 @@ class AstrMessageEvent(abc.ABC):
|
|||||||
self,
|
self,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
func_tool_manager=None,
|
func_tool_manager=None,
|
||||||
|
tool_set: ToolSet | None = None,
|
||||||
session_id: str = "",
|
session_id: str = "",
|
||||||
image_urls: list[str] | None = None,
|
image_urls: list[str] | None = None,
|
||||||
contexts: list | None = None,
|
contexts: list | None = None,
|
||||||
@@ -377,7 +379,7 @@ class AstrMessageEvent(abc.ABC):
|
|||||||
|
|
||||||
contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。
|
contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。
|
||||||
|
|
||||||
func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。
|
func_tool_manager: [Deprecated] 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。已过时,请使用 tool_set 参数代替。
|
||||||
|
|
||||||
conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
|
conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
|
||||||
|
|
||||||
@@ -393,7 +395,8 @@ class AstrMessageEvent(abc.ABC):
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
image_urls=image_urls,
|
image_urls=image_urls,
|
||||||
func_tool=func_tool_manager,
|
# func_tool=func_tool_manager,
|
||||||
|
func_tool=tool_set,
|
||||||
contexts=contexts,
|
contexts=contexts,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
conversation=conversation,
|
conversation=conversation,
|
||||||
|
|||||||
@@ -21,3 +21,6 @@ class PlatformMetadata:
|
|||||||
"""平台是否支持真实流式传输"""
|
"""平台是否支持真实流式传输"""
|
||||||
support_proactive_message: bool = True
|
support_proactive_message: bool = True
|
||||||
"""平台是否支持主动消息推送(非用户触发)"""
|
"""平台是否支持主动消息推送(非用户触发)"""
|
||||||
|
|
||||||
|
module_path: str | None = None
|
||||||
|
"""注册该适配器的模块路径,用于插件热重载时清理"""
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ def register_platform_adapter(
|
|||||||
if "id" not in default_config_tmpl:
|
if "id" not in default_config_tmpl:
|
||||||
default_config_tmpl["id"] = adapter_name
|
default_config_tmpl["id"] = adapter_name
|
||||||
|
|
||||||
|
# Get the module path of the class being decorated
|
||||||
|
module_path = cls.__module__
|
||||||
|
|
||||||
pm = PlatformMetadata(
|
pm = PlatformMetadata(
|
||||||
name=adapter_name,
|
name=adapter_name,
|
||||||
description=desc,
|
description=desc,
|
||||||
@@ -45,6 +48,7 @@ def register_platform_adapter(
|
|||||||
adapter_display_name=adapter_display_name,
|
adapter_display_name=adapter_display_name,
|
||||||
logo_path=logo_path,
|
logo_path=logo_path,
|
||||||
support_streaming_message=support_streaming_message,
|
support_streaming_message=support_streaming_message,
|
||||||
|
module_path=module_path,
|
||||||
)
|
)
|
||||||
platform_registry.append(pm)
|
platform_registry.append(pm)
|
||||||
platform_cls_map[adapter_name] = cls
|
platform_cls_map[adapter_name] = cls
|
||||||
@@ -52,3 +56,31 @@ def register_platform_adapter(
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str]:
|
||||||
|
"""根据模块路径前缀注销平台适配器。
|
||||||
|
|
||||||
|
在插件热重载时调用,用于清理该插件注册的所有平台适配器。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path_prefix: 模块路径前缀,如 "data.plugins.my_plugin"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
被注销的平台适配器名称列表
|
||||||
|
"""
|
||||||
|
unregistered = []
|
||||||
|
to_remove = []
|
||||||
|
|
||||||
|
for pm in platform_registry:
|
||||||
|
if pm.module_path and pm.module_path.startswith(module_path_prefix):
|
||||||
|
to_remove.append(pm)
|
||||||
|
unregistered.append(pm.name)
|
||||||
|
|
||||||
|
for pm in to_remove:
|
||||||
|
platform_registry.remove(pm)
|
||||||
|
if pm.name in platform_cls_map:
|
||||||
|
del platform_cls_map[pm.name]
|
||||||
|
logger.debug(f"平台适配器 {pm.name} 已注销 (来自模块 {pm.module_path})")
|
||||||
|
|
||||||
|
return unregistered
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
|
|||||||
class CustomFilterOr(CustomFilter):
|
class CustomFilterOr(CustomFilter):
|
||||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
|
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"CustomFilter lass can only operate with other CustomFilter.",
|
"CustomFilter class can only operate with other CustomFilter.",
|
||||||
)
|
)
|
||||||
self.filter1 = filter1
|
self.filter1 = filter1
|
||||||
self.filter2 = filter2
|
self.filter2 = filter2
|
||||||
@@ -51,7 +51,7 @@ class CustomFilterOr(CustomFilter):
|
|||||||
class CustomFilterAnd(CustomFilter):
|
class CustomFilterAnd(CustomFilter):
|
||||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
|
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"CustomFilter lass can only operate with other CustomFilter.",
|
"CustomFilter lass can only operate with other CustomFilter.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs):
|
|||||||
if args:
|
if args:
|
||||||
raise_error = args[0]
|
raise_error = args[0]
|
||||||
|
|
||||||
if not isinstance(custom_filter, CustomFilterAnd | CustomFilterOr):
|
if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
|
||||||
custom_filter = custom_filter(raise_error)
|
custom_filter = custom_filter(raise_error)
|
||||||
|
|
||||||
def decorator(awaitable):
|
def decorator(awaitable):
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import yaml
|
|||||||
from astrbot.core import logger, pip_installer, sp
|
from astrbot.core import logger, pip_installer, sp
|
||||||
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
||||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot.core.platform.register import unregister_platform_adapters_by_module
|
||||||
from astrbot.core.provider.register import llm_tools
|
from astrbot.core.provider.register import llm_tools
|
||||||
from astrbot.core.utils.astrbot_path import (
|
from astrbot.core.utils.astrbot_path import (
|
||||||
get_astrbot_config_path,
|
get_astrbot_config_path,
|
||||||
@@ -842,6 +843,18 @@ class PluginManager:
|
|||||||
for func_tool in to_remove:
|
for func_tool in to_remove:
|
||||||
llm_tools.func_list.remove(func_tool)
|
llm_tools.func_list.remove(func_tool)
|
||||||
|
|
||||||
|
# Unregister platform adapters registered by this plugin
|
||||||
|
# module_path is like "data.plugins.my_plugin.main", extract prefix like "data.plugins.my_plugin"
|
||||||
|
module_prefix = ".".join(plugin_module_path.split(".")[:-1])
|
||||||
|
if module_prefix:
|
||||||
|
unregistered_adapters = unregister_platform_adapters_by_module(
|
||||||
|
module_prefix
|
||||||
|
)
|
||||||
|
for adapter_name in unregistered_adapters:
|
||||||
|
logger.info(
|
||||||
|
f"移除了插件 {plugin_name} 的平台适配器 {adapter_name}",
|
||||||
|
)
|
||||||
|
|
||||||
if plugin is None:
|
if plugin is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,17 @@ class PluginRoute(Route):
|
|||||||
"display_name": plugin.display_name,
|
"display_name": plugin.display_name,
|
||||||
"logo": f"/api/file/{logo_url}" if logo_url else None,
|
"logo": f"/api/file/{logo_url}" if logo_url else None,
|
||||||
}
|
}
|
||||||
|
# 检查是否为全空的幽灵插件
|
||||||
|
if not any(
|
||||||
|
[
|
||||||
|
plugin.name,
|
||||||
|
plugin.author,
|
||||||
|
plugin.desc,
|
||||||
|
plugin.version,
|
||||||
|
plugin.display_name,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
continue
|
||||||
_plugin_resp.append(_t)
|
_plugin_resp.append(_t)
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
## What's Changed - BIG AND BEAUTIFUL VERSION
|
||||||
|
|
||||||
|
> 如果在之前版本使用了 Skill,这次更新之后**需要重新配置** Skill Runtime 相关选项。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- 🔥 新增未来任务系统(Future Tasks)。给 AstrBot 布置的未来任务,让 AstrBot 能够在某一时刻自动唤醒,帮你完成任务。详见 [主动任务](https://docs.astrbot.app/use/proactive-agent.html) 。(实验性) ([#4697](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||||
|
- 🔥 新增子代理(SubAgent)编排器。(实验性)([#4697](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||||
|
- 🔥 AstrBot 目前可以直接通过调用 tool 将图片 / 文件推送给用户,大大提高交互效果。
|
||||||
|
- 新增 Computer Use 运行时配置,以融合 Skill 和 Sandbox 配置 ([#4831](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||||
|
- 新增主题自定义功能,可设置主色与辅色
|
||||||
|
- 支持在配置页下人格对话框的编辑人格 ([#4826](https://github.com/AstrBotDevs/AstrBot/issues/4826))
|
||||||
|
- 支持开关 “追踪” 功能;支持在系统配置中设置是否将日志写入 log 文件 ([#4822](https://github.com/AstrBotDevs/AstrBot/issues/4822))
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- ‼️ 修复 ChatUI 图片、思考等显示异常问题。
|
||||||
|
- ‼️ 修复 Skill 上传到 Sandbox 后未自动解压导致 Agent 无法读取的问题。
|
||||||
|
- ‼️ 修复配置特定插件集时 MCP 工具被过滤的问题 ([#4825](https://github.com/AstrBotDevs/AstrBot/issues/4825))
|
||||||
|
- ‼️ 移除 ChatUI 自带的让 LLM 最后提出问题的 prompt ([#4824](https://github.com/AstrBotDevs/AstrBot/issues/4824))
|
||||||
|
- ‼️ 修复 WebUI 在上传 Skill 失败后仍显示成功消息的 bug ([#4768](https://github.com/AstrBotDevs/AstrBot/issues/4768))
|
||||||
|
- 修复 MCP 服务器无法重命名的问题 ([#4766](https://github.com/AstrBotDevs/AstrBot/issues/4766))
|
||||||
|
- 修复插件的 tool 无法在 WebUI 管理行为中看到来源的问题 ([#4776](https://github.com/AstrBotDevs/AstrBot/issues/4776))
|
||||||
|
- ‼️ 修复 skill-like 的 tool 模式下,调用 tool 失败的问题 ([#4775](https://github.com/AstrBotDevs/AstrBot/issues/4775))
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
|
||||||
|
- WebUI 整体 UI 效果优化
|
||||||
|
- 部分 Dialog 标题样式统一
|
||||||
|
|
||||||
|
## What's Changed (EN)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Introduce CronJob system with one-time tasks and enhanced dashboard management
|
||||||
|
- Add theme customization with primary & secondary color options
|
||||||
|
- Add computer-use runtime config for skills sandbox execution ([#4831](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||||
|
- Add edit button to persona selector dialog ([#4826](https://github.com/AstrBotDevs/AstrBot/issues/4826))
|
||||||
|
- Add trace logging toggle and configuration UI ([#4822](https://github.com/AstrBotDevs/AstrBot/issues/4822))
|
||||||
|
- Add proactive-messaging capability with cron-tool trigger
|
||||||
|
- Implement SubAgent orchestrator with configurable tool-management policies
|
||||||
|
- Support resolving sandbox file paths and auto-download when necessary
|
||||||
|
- Add embedded image & audio styles in MessagePartsRenderer
|
||||||
|
- Introduce i18n foundation
|
||||||
|
- Persist agent-interaction history
|
||||||
|
- Add user notifications for file-download success/removal
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Improve ghost-plugin detection accuracy
|
||||||
|
- Add error handling to prevent ghost-plugin crashes
|
||||||
|
- Prevent skills bundle from overwriting existing files
|
||||||
|
- Fix skills bundle unzip failure inside sandbox
|
||||||
|
- Fix MCP tools being filtered when specific plugin set configured ([#4825](https://github.com/AstrBotDevs/AstrBot/issues/4825))
|
||||||
|
- Merge ChatUI persona pop-up into default persona ([#4824](https://github.com/AstrBotDevs/AstrBot/issues/4824))
|
||||||
|
- Fix reasoning block style
|
||||||
|
- Add missing comma in truncate_and_compress hint
|
||||||
|
- Fix frontend still showing success message ([#4768](https://github.com/AstrBotDevs/AstrBot/issues/4768))
|
||||||
|
- Fix unable to rename MCP server ([#4766](https://github.com/AstrBotDevs/AstrBot/issues/4766))
|
||||||
|
- Remove leftover sandbox runtime handling in skill upload ([#4798](https://github.com/AstrBotDevs/AstrBot/issues/4798))
|
||||||
|
- Fix handler module path construction ([#4776](https://github.com/AstrBotDevs/AstrBot/issues/4776))
|
||||||
|
- Fix skill-like tool invocation error ([#4775](https://github.com/AstrBotDevs/AstrBot/issues/4775))
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- Runtime hints & refined UI in skills management
|
||||||
|
- Performance and UX improvements on cron-job page
|
||||||
|
- General WebUI performance boost
|
||||||
|
- Group tools by plugin in dropdown
|
||||||
|
- Consistent dialog titles with padding and text styles
|
||||||
|
- Code formatting unified (ruff format)
|
||||||
|
- Bump version to 4.13.2
|
||||||
|
|
||||||
|
### Others
|
||||||
|
- Remove obsolete reminder code
|
||||||
|
- Extract main-agent module for better architecture
|
||||||
|
- Merge AstrBot_skill branch changes
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## What's Changed - BIG AND BEAUTIFUL VERSION
|
||||||
|
|
||||||
|
hotfix of v4.14.0
|
||||||
|
|
||||||
|
fixes:
|
||||||
|
|
||||||
|
- 由 `event.request_llm()` 过时导致的群聊上下文感知-主动回复功能可能不可用的问题
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- 控制台页面新增调试提示和本地化文件 ([#4852](https://github.com/AstrBotDevs/AstrBot/pull/4852))
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复插件热重载时平台适配器未清理导致注册冲突的问题 ([#4859](https://github.com/AstrBotDevs/AstrBot/pull/4859))
|
||||||
|
|
||||||
|
### 其他
|
||||||
|
- 更新 ruff 版本至 0.15.0
|
||||||
|
- 新增 robots.txt ([#4847](https://github.com/AstrBotDevs/AstrBot/pull/4847))
|
||||||
|
|
||||||
|
## What's Changed (EN)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Add debug hint to console page and localization files ([#4852](https://github.com/AstrBotDevs/AstrBot/pull/4852))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fix platform adapter not being cleaned up during plugin hot reload, causing registration conflicts ([#4859](https://github.com/AstrBotDevs/AstrBot/pull/4859))
|
||||||
|
|
||||||
|
### Others
|
||||||
|
- Update ruff version to 0.15.0
|
||||||
|
- Add robots.txt ([#4847](https://github.com/AstrBotDevs/AstrBot/pull/4847))
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复 `on_llm_request` 钩子可能无法应用效果的问题
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复 token 统计错误的问题,修复在多轮 tool call 情况下或者其他极端情况下可能造成 tool 无限调用的问题。
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="keywords" content="AstrBot Soulter" />
|
<meta name="keywords" content="AstrBot Soulter" />
|
||||||
<meta name="description" content="AstrBot Dashboard" />
|
<meta name="description" content="AstrBot Dashboard" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Outfit&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Outfit&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markstream-vue": "^0.0.6",
|
"markstream-vue": "^0.0.6",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.52.2",
|
||||||
"pinia": "2.1.6",
|
"pinia": "2.1.6",
|
||||||
"pinyin-pro": "^3.26.0",
|
"pinyin-pro": "^3.26.0",
|
||||||
"remixicon": "3.5.0",
|
"remixicon": "3.5.0",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
@@ -11,5 +11,8 @@
|
|||||||
"mirrorLabel": "Force PyPI repository URL (optional)",
|
"mirrorLabel": "Force PyPI repository URL (optional)",
|
||||||
"mirrorHint": "Force PyPI repository URL > Config item `PyPI Repository Address`",
|
"mirrorHint": "Force PyPI repository URL > Config item `PyPI Repository Address`",
|
||||||
"installButton": "Install"
|
"installButton": "Install"
|
||||||
|
},
|
||||||
|
"debugHint": {
|
||||||
|
"text": "Debug logs can be enabled in \"Configuration File → System → Console Log Level\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"cron": "Cron",
|
"cron": "Cron",
|
||||||
|
"session": "Session ID",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Next Run",
|
||||||
"lastRun": "Last Run",
|
"lastRun": "Last Run",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
|
|||||||
@@ -11,5 +11,8 @@
|
|||||||
"mirrorLabel": "强制 PyPI 软件仓库链接(可选)",
|
"mirrorLabel": "强制 PyPI 软件仓库链接(可选)",
|
||||||
"mirrorHint": "强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`",
|
"mirrorHint": "强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`",
|
||||||
"installButton": "安装"
|
"installButton": "安装"
|
||||||
|
},
|
||||||
|
"debugHint": {
|
||||||
|
"text": "Debug 日志需要在「配置文件 → 系统 → 控制台日志级别」中开启"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"name": "名称",
|
"name": "名称",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"cron": "Cron",
|
"cron": "Cron",
|
||||||
|
"session": "会话 ID",
|
||||||
"nextRun": "下一次执行",
|
"nextRun": "下一次执行",
|
||||||
"lastRun": "最近执行",
|
"lastRun": "最近执行",
|
||||||
"note": "说明",
|
"note": "说明",
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
"nameHint": "建议使用英文小写+下划线,且全局唯一",
|
"nameHint": "建议使用英文小写+下划线,且全局唯一",
|
||||||
"providerLabel": "Chat Provider(可选)",
|
"providerLabel": "Chat Provider(可选)",
|
||||||
"providerHint": "留空表示跟随全局默认 provider。",
|
"providerHint": "留空表示跟随全局默认 provider。",
|
||||||
"personaLabel": "选择 Persona",
|
"personaLabel": "选择人格设定",
|
||||||
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。",
|
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。在人格设定页管理和新建人格。",
|
||||||
"descriptionLabel": "对主 LLM 的描述(用于决定是否 handoff)",
|
"descriptionLabel": "对主 LLM 的描述(用于决定是否 handoff)",
|
||||||
"descriptionHint": "这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
|
"descriptionHint": "这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,18 @@ const { tm } = useModuleI18n('features/console');
|
|||||||
<div style="height: 100%;">
|
<div style="height: 100%;">
|
||||||
<div
|
<div
|
||||||
style="background-color: var(--v-theme-surface); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
|
style="background-color: var(--v-theme-surface); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
|
||||||
<h4>{{ tm('title') }}</h4>
|
<div>
|
||||||
|
<h4>{{ tm('title') }}</h4>
|
||||||
|
<v-alert
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
class="mt-2"
|
||||||
|
style="max-width: 600px;"
|
||||||
|
>
|
||||||
|
{{ tm('debugHint.text') }}
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="autoScrollEnabled"
|
v-model="autoScrollEnabled"
|
||||||
@@ -111,4 +122,4 @@ export default {
|
|||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.2s ease-in-out;
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
<div class="text-caption text-medium-emphasis">{{ item.timezone || tm('table.timezoneLocal') }}</div>
|
<div class="text-caption text-medium-emphasis">{{ item.timezone || tm('table.timezoneLocal') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #item.session="{ item }">
|
||||||
|
<div>{{ item.session || tm('table.notAvailable') }}</div>
|
||||||
|
</template>
|
||||||
<template #item.next_run_time="{ item }">{{ formatTime(item.next_run_time) }}</template>
|
<template #item.next_run_time="{ item }">{{ formatTime(item.next_run_time) }}</template>
|
||||||
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
|
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
|
||||||
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
||||||
@@ -129,6 +132,7 @@ const headers = computed(() => [
|
|||||||
{ title: tm('table.headers.name'), key: 'name', minWidth: '200px' },
|
{ title: tm('table.headers.name'), key: 'name', minWidth: '200px' },
|
||||||
{ title: tm('table.headers.type'), key: 'type', width: 110 },
|
{ title: tm('table.headers.type'), key: 'type', width: 110 },
|
||||||
{ title: tm('table.headers.cron'), key: 'cron_expression', minWidth: '160px' },
|
{ title: tm('table.headers.cron'), key: 'cron_expression', minWidth: '160px' },
|
||||||
|
{ title: tm('table.headers.session'), key: 'session', minWidth: '200px' },
|
||||||
{ title: tm('table.headers.nextRun'), key: 'next_run_time', minWidth: '160px' },
|
{ title: tm('table.headers.nextRun'), key: 'next_run_time', minWidth: '160px' },
|
||||||
{ title: tm('table.headers.lastRun'), key: 'last_run_at', minWidth: '160px' },
|
{ title: tm('table.headers.lastRun'), key: 'last_run_at', minWidth: '160px' },
|
||||||
{ title: tm('table.headers.note'), key: 'note', minWidth: '220px' },
|
{ title: tm('table.headers.note'), key: 'note', minWidth: '220px' },
|
||||||
@@ -163,7 +167,11 @@ async function loadJobs() {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/cron/jobs')
|
const res = await axios.get('/api/cron/jobs')
|
||||||
if (res.data.status === 'ok') {
|
if (res.data.status === 'ok') {
|
||||||
jobs.value = Array.isArray(res.data.data) ? res.data.data : []
|
const data = Array.isArray(res.data.data) ? res.data.data : []
|
||||||
|
jobs.value = data.map((job: any) => ({
|
||||||
|
...job,
|
||||||
|
session: job?.payload?.session || job?.session || ''
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
toast(res.data.message || tm('messages.loadFailed'), 'error')
|
toast(res.data.message || tm('messages.loadFailed'), 'error')
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "AstrBot"
|
name = "AstrBot"
|
||||||
version = "4.13.2"
|
version = "4.14.4"
|
||||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -69,14 +69,14 @@ dev = [
|
|||||||
"pytest>=8.4.1",
|
"pytest>=8.4.1",
|
||||||
"pytest-asyncio>=1.1.0",
|
"pytest-asyncio>=1.1.0",
|
||||||
"pytest-cov>=6.2.1",
|
"pytest-cov>=6.2.1",
|
||||||
"ruff>=0.12.8",
|
"ruff>=0.15.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
astrbot = "astrbot.cli.__main__:cli"
|
astrbot = "astrbot.cli.__main__:cli"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py"]
|
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py", "tests"]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ ignore = [
|
|||||||
"F405",
|
"F405",
|
||||||
"E501",
|
"E501",
|
||||||
"ASYNC230", # TODO: handle ASYNC230 in AstrBot
|
"ASYNC230", # TODO: handle ASYNC230 in AstrBot
|
||||||
|
"ASYNC240", # TODO: handle ASYNC240 in AstrBot
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
|
|||||||
Executable
+253
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Auto-generate changelog from git commits using LLM.
|
||||||
|
Usage: python scripts/generate_changelog.py [--version VERSION]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_tag():
|
||||||
|
"""Get the latest git tag."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "describe", "--tags", "--abbrev=0"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_commits_since_tag(tag):
|
||||||
|
"""Get all commit messages since the specified tag."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "log", f"{tag}..HEAD", "--pretty=format:%H|%s|%b"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
commits = []
|
||||||
|
for line in result.stdout.strip().split("\n"):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split("|", 2)
|
||||||
|
if len(parts) >= 2:
|
||||||
|
commit_hash = parts[0]
|
||||||
|
subject = parts[1]
|
||||||
|
body = parts[2] if len(parts) > 2 else ""
|
||||||
|
commits.append({"hash": commit_hash[:7], "subject": subject, "body": body})
|
||||||
|
return commits
|
||||||
|
|
||||||
|
|
||||||
|
def extract_issue_number(text):
|
||||||
|
"""Extract issue number from commit message."""
|
||||||
|
# Match #1234 or (#1234)
|
||||||
|
match = re.search(r"#(\d+)", text)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm_for_changelog(commits, version):
|
||||||
|
"""Call LLM to generate changelog from commits."""
|
||||||
|
try:
|
||||||
|
# Try to use OpenAI API or other LLM providers
|
||||||
|
import openai
|
||||||
|
|
||||||
|
# Build prompt
|
||||||
|
commits_text = "\n".join([f"- {c['subject']}" for c in commits])
|
||||||
|
|
||||||
|
prompt = f"""Based on the following git commit messages, generate a changelog document in BOTH Chinese and English.
|
||||||
|
|
||||||
|
Commit messages:
|
||||||
|
{commits_text}
|
||||||
|
|
||||||
|
Please organize the changes into these categories:
|
||||||
|
- 新增 (New Features)
|
||||||
|
- 修复 (Bug Fixes)
|
||||||
|
- 优化 (Improvements)
|
||||||
|
- 其他 (Others)
|
||||||
|
|
||||||
|
Format requirements:
|
||||||
|
1. Start with Chinese version under "## What's Changed"
|
||||||
|
2. Follow with English version under "## What's Changed (EN)"
|
||||||
|
3. Use markdown format with proper bullet points
|
||||||
|
4. Keep descriptions concise and user-friendly
|
||||||
|
5. If a commit mentions an issue number (#1234), include it in the format ([#1234](https://github.com/AstrBotDevs/AstrBot/issues/1234))
|
||||||
|
|
||||||
|
Example format:
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- 支持某某功能 ([#1234](https://github.com/AstrBotDevs/AstrBot/issues/1234))
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复某某问题
|
||||||
|
|
||||||
|
## What's Changed (EN)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Add support for something ([#1234](https://github.com/AstrBotDevs/AstrBot/issues/1234))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fix something
|
||||||
|
"""
|
||||||
|
|
||||||
|
client = openai.OpenAI(
|
||||||
|
api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
|
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=os.getenv("OPENAI_MODEL", "gpt-4"),
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a helpful assistant that generates well-structured changelogs.",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
temperature=0.3,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"Warning: openai package not installed. Install it with: pip install openai"
|
||||||
|
)
|
||||||
|
return generate_simple_changelog(commits)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to call LLM API: {e}")
|
||||||
|
print("Falling back to simple changelog generation...")
|
||||||
|
return generate_simple_changelog(commits)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_simple_changelog(commits):
|
||||||
|
"""Generate a simple changelog without LLM."""
|
||||||
|
sections = {
|
||||||
|
"feat": ("新增", "New Features", []),
|
||||||
|
"fix": ("修复", "Bug Fixes", []),
|
||||||
|
"perf": ("优化", "Improvements", []),
|
||||||
|
"docs": ("文档", "Documentation", []),
|
||||||
|
"refactor": ("重构", "Refactoring", []),
|
||||||
|
"test": ("测试", "Tests", []),
|
||||||
|
"chore": ("其他", "Chore", []),
|
||||||
|
"other": ("其他", "Others", []),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Categorize commits by conventional commit type
|
||||||
|
for commit in commits:
|
||||||
|
subject = commit["subject"]
|
||||||
|
issue_num = extract_issue_number(subject)
|
||||||
|
issue_link = (
|
||||||
|
f" ([#{issue_num}](https://github.com/AstrBotDevs/AstrBot/issues/{issue_num}))"
|
||||||
|
if issue_num
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Detect conventional commit type
|
||||||
|
matched = False
|
||||||
|
for prefix in ["feat", "fix", "perf", "docs", "refactor", "test", "chore"]:
|
||||||
|
if subject.lower().startswith(f"{prefix}:") or subject.lower().startswith(
|
||||||
|
f"{prefix}("
|
||||||
|
):
|
||||||
|
# Remove prefix for display
|
||||||
|
clean_subject = re.sub(
|
||||||
|
r"^[a-z]+(\([^)]+\))?:\s*", "", subject, flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
sections[prefix][2].append(f"- {clean_subject}{issue_link}")
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
sections["other"][2].append(f"- {subject}{issue_link}")
|
||||||
|
|
||||||
|
# Build Chinese version
|
||||||
|
changelog_zh = "## What's Changed\n\n"
|
||||||
|
for section_key in ["feat", "fix", "perf", "docs", "refactor", "test", "other"]:
|
||||||
|
zh_title, _, items = sections[section_key]
|
||||||
|
if items:
|
||||||
|
changelog_zh += f"### {zh_title}\n\n"
|
||||||
|
changelog_zh += "\n".join(items) + "\n\n"
|
||||||
|
|
||||||
|
# Build English version
|
||||||
|
changelog_en = "## What's Changed (EN)\n\n"
|
||||||
|
for section_key in ["feat", "fix", "perf", "docs", "refactor", "test", "other"]:
|
||||||
|
_, en_title, items = sections[section_key]
|
||||||
|
if items:
|
||||||
|
changelog_en += f"### {en_title}\n\n"
|
||||||
|
changelog_en += "\n".join(items) + "\n\n"
|
||||||
|
|
||||||
|
return changelog_zh + changelog_en
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate changelog from git commits")
|
||||||
|
parser.add_argument(
|
||||||
|
"--version", help="Version number for the changelog (e.g., v4.13.3)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use-llm",
|
||||||
|
action="store_true",
|
||||||
|
help="Use LLM to generate changelog (requires OpenAI API key)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Get latest tag
|
||||||
|
try:
|
||||||
|
latest_tag = get_latest_tag()
|
||||||
|
print(f"Latest tag: {latest_tag}")
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print("Error: No tags found in repository")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get commits since tag
|
||||||
|
commits = get_commits_since_tag(latest_tag)
|
||||||
|
if not commits:
|
||||||
|
print(f"No commits found since {latest_tag}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print(f"Found {len(commits)} commits since {latest_tag}")
|
||||||
|
|
||||||
|
# Determine version
|
||||||
|
if args.version:
|
||||||
|
version = args.version
|
||||||
|
else:
|
||||||
|
# Auto-increment patch version
|
||||||
|
match = re.match(r"v(\d+)\.(\d+)\.(\d+)", latest_tag)
|
||||||
|
if match:
|
||||||
|
major, minor, patch = map(int, match.groups())
|
||||||
|
version = f"v{major}.{minor}.{patch + 1}"
|
||||||
|
else:
|
||||||
|
print(f"Warning: Could not parse version from tag {latest_tag}")
|
||||||
|
version = "vX.X.X"
|
||||||
|
|
||||||
|
print(f"Generating changelog for {version}...")
|
||||||
|
|
||||||
|
# Generate changelog
|
||||||
|
if args.use_llm:
|
||||||
|
changelog_content = call_llm_for_changelog(commits, version)
|
||||||
|
else:
|
||||||
|
changelog_content = generate_simple_changelog(commits)
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
changelog_dir = Path(__file__).parent.parent / "changelogs"
|
||||||
|
changelog_dir.mkdir(exist_ok=True)
|
||||||
|
changelog_file = changelog_dir / f"{version}.md"
|
||||||
|
|
||||||
|
with open(changelog_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(changelog_content)
|
||||||
|
|
||||||
|
print(f"\n✓ Changelog generated: {changelog_file}")
|
||||||
|
print("\nPreview:")
|
||||||
|
print("=" * 80)
|
||||||
|
print(changelog_content)
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user