Compare commits

...

29 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 31aae304c2 refactor: extract helper method to reduce code duplication
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-06 03:16:02 +00:00
copilot-swe-agent[bot] c6118409d0 feat: deactivate built-in web_search tools when disabled to allow MCP tools
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-06 03:12:51 +00:00
copilot-swe-agent[bot] f009e09894 Initial plan 2026-02-06 03:08:02 +00:00
Soulter fc2a67188f docs: update watashiwakoseinodesukara
Removed duplicate text and added a new image.
2026-02-05 23:08:14 +08:00
boushi1111 d69592aaa8 fix: TypeError when MCP schema type is a list (#4867)
* Fix TypeError when MCP schema type is a list

Fixes crash in Gemini native tools with VRChat MCP.

* Refactor: avoid modifying schema in place per feedback

* Fix formatting and cleanup comments
2026-02-05 22:51:29 +08:00
Dt8333 f3397f6f08 fix: pyright lint (#4874)
* feat: 将 MessageSession 的 platform_id 改为 init=False,实例化时无需传入

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* refactor: 将 isinstance 检查改为元组、将默认模型值设为空字符串、将类型注解改为 Any 并导入

* refactor: 为 _serialize_job 增加返回类型注解 dict

* fix: 使用 cast 获取百度 AIP 的 msg 并对 psutil_addr 引入 type: ignore

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* refactor: 引入 _AddrWithPort 协议并替换 conn.laddr 的 cast

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* fix: 在构建 AstrBotMessage 时对 ctx.channel 可能为 None 进行兜底处理

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

---------

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>
2026-02-05 21:54:12 +08:00
LIghtJUNction be92e4f395 feat: systemd support (#4880) 2026-02-05 21:52:21 +08:00
Soulter 912e40e7f0 chore: delete unused file 2026-02-05 10:40:53 +08:00
Xican 2876c43387 fix: 修复特定提供商导致的定时任务执行失败的问题 (#4872)
* fix: 修复特定提供商导致的定时任务执行失败的问题

* ruff format

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-05 10:14:31 +08:00
Soulter 464882f206 chore: bump version to 4.14.4 2026-02-04 23:21:08 +08:00
Soulter 6736fb85c2 fix: conversation token usage calculate wrongly and fix tool call infinitely (#4869) 2026-02-04 23:18:32 +08:00
Soulter 1f75255950 chore: bump version to 4.14.3 2026-02-04 20:31:19 +08:00
Soulter a954e75547 fix: add apply_reset parameter to build_main_agent and handle coroutine reset in InternalAgentSubStage 2026-02-04 20:25:31 +08:00
advent259141 d2b9997620 chore: bump version to 4.14.2 2026-02-04 17:42:41 +08:00
Gao Jinzhe 36432c4361 fix: 修复插件热重载时平台适配器未清理导致注册冲突的问题 (#4859) 2026-02-04 15:06:03 +08:00
圣达生物多 36f0d1f0f9 feat: add debug hint to console page and localization files (#4852) 2026-02-04 15:02:15 +08:00
Anima-IGCenter f65b268bb2 chore: create robots.txt (#4847) 2026-02-04 15:00:08 +08:00
Raven95676 fe06dfcca3 fix: update ruff version to 0.15.0 and add ASYNC240 to ignore list 2026-02-04 11:45:59 +08:00
Soulter bc9043bc3f fix: update ruff exclude list to include tests directory 2026-02-04 10:08:48 +08:00
Soulter 430694aae9 chore: update readme 2026-02-04 10:05:35 +08:00
Soulter c643e3c093 chore: ruff format 2026-02-03 23:40:23 +08:00
Soulter ff46eef3b2 chore: bump version to 4.14.1 2026-02-03 23:35:21 +08:00
Soulter a0c364aa81 fix: active reply function does not work caused by event.request_llm() outdated 2026-02-03 23:34:42 +08:00
Anima-IGCenter 0e0f923a49 chore(seo): prevent indexing with noindex, nofollow (#4844) 2026-02-03 23:19:25 +08:00
Soulter f2d637b935 fix: downgrade monaco-editor to version 0.52.2 2026-02-03 22:12:29 +08:00
Soulter 96e61a4a92 chore: bump version to 4.14.0 2026-02-03 22:08:29 +08:00
香草味的纳西妲喵 e42c1b6da8 fix: add error handling to avoid ghost plugins (#4836)
* fix: add error handling to avoid ghost plugins

Add null checks to filter out incomplete plugin metadata objects that would appear as ghost plugins in the API response.

This fix ensures that plugins with all null key fields (name, author, desc, version, display_name) are not included in the plugin list response, preventing ghost plugins from appearing in the UI.

Issue: #4833

* fix: improve ghost plugin detection logic for better accuracy

---------

Co-authored-by: Soulter <905617992@qq.com>
2026-02-03 20:40:47 +08:00
Soulter 387bba093e fix: missing 2 required positional arguments: 'filter1' and 'filter2' (#4840)
fixes: #4777
2026-02-03 20:37:18 +08:00
Soulter 123cf9cb11 docs: revise README.md for clarity and feature updates (#4839)
Updated project description and added details about deployment and features.
2026-02-03 20:24:10 +08:00
44 changed files with 598 additions and 58 deletions
-18
View File
@@ -1,18 +0,0 @@
我需要让 Agent 能够在未来提醒自己去做某些事情,这样 Agent 能够主动地去完成一些任务,而不是等用户主动来下达命令。
你需要实现一个 CronJob 系统,允许 Agent 创建未来任务,并且在未来的某个时间点自动触发这些任务的执行.
CronJob 系统分为 BasicCronJob 和 ActiveAgentCronJob 两种类型。前者只是简单的提供一个定时任务功能(给插件用),而后者则允许 Agent 主动地去完成一些任务。BasicCronJob 不必多说,就是定时执行某个函数。对于 ActiveAgentCronJobAgent 应该可以主动管理(比如通过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 = Falsetool 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 上去管理,只不过这个是立即触发的任务,不需要等到未来某个时间点才触发罢了。
我希望设计尽可能简单,但是强大。
+20 -2
View File
@@ -34,7 +34,7 @@
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
</div>
AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主流即时通讯软件,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建生产可用的 AI 应用。
AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
@@ -50,6 +50,23 @@ AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主
7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
8. 🌐 国际化(i18n)支持。
<br>
<table align="center">
<tr align="center">
<th>💙 角色扮演 & 情感陪伴</th>
<th>✨ 主动式 Agent</th>
<th>🚀 通用 Agentic 能力</th>
<th>🧩 900+ 社区插件</th>
</tr>
<tr>
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
</tr>
</table>
## 快速开始
#### Docker 部署(推荐 🥳)
@@ -247,8 +264,9 @@ pre-commit install
<div align="center">
_陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。_
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div
-1
View File
@@ -77,7 +77,6 @@ class Main(star.Star):
yield event.request_llm(
prompt=prompt,
func_tool_manager=self.context.get_llm_tool_manager(),
session_id=event.session_id,
conversation=conv,
)
@@ -49,7 +49,7 @@ class Main(Star):
if p_settings.get("empty_mention_waiting_need_reply", True):
try:
# 尝试使用 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(
@@ -76,7 +76,6 @@ class Main(Star):
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
),
func_tool_manager=func_tools_mgr,
session_id=curr_cid,
contexts=[],
system_prompt="",
@@ -49,6 +49,24 @@ class Main(star.Star):
self.sogo_search = Sogo()
self.baidu_initialized = False
# Deactivate built-in web search tools if web_search is disabled
# This allows MCP to provide custom web_search tools
websearch_enable = (provider_settings or {}).get("web_search", False)
if not websearch_enable:
self._set_tools_active(False)
def _set_tools_active(self, active: bool) -> None:
"""Set the active status of all built-in web search tools.
Args:
active: True to activate tools, False to deactivate them
"""
func_tool_mgr = self.context.get_llm_tool_manager()
for tool_name in self.TOOLS:
tool = func_tool_mgr.get_func(tool_name)
if tool:
tool.active = active
async def _tidy_text(self, text: str) -> str:
"""清理文本,去除空格、换行符等"""
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
@@ -394,6 +412,10 @@ class Main(star.Star):
websearch_enable = prov_settings.get("web_search", False)
provider = prov_settings.get("websearch_provider", "default")
# Globally activate/deactivate built-in web search tools based on config
# This allows MCP to provide custom web_search tools when built-in is disabled
self._set_tools_active(websearch_enable)
tool_set = req.func_tool
if isinstance(tool_set, FunctionToolManager):
req.func_tool = tool_set.get_full_tool_set()
+1 -1
View File
@@ -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:
# only count the token usage of the final response for computation purpose
self.stats.token_usage += llm_response.usage
if self.req.conversation:
self.req.conversation.token_usage = llm_response.usage.total
break # got final response
if not llm_resp_result:
+12 -2
View File
@@ -246,8 +246,18 @@ class ToolSet:
result = {}
if "type" in schema and schema["type"] in supported_types:
result["type"] = schema["type"]
# Avoid side effects by not modifying the original schema
origin_type = schema.get("type")
target_type = origin_type
# Compatibility fix: Gemini API expects 'type' to be a string (enum),
# but standard JSON Schema (MCP) allows lists (e.g. ["string", "null"]).
# We fallback to the first non-null type.
if isinstance(origin_type, list):
target_type = next((t for t in origin_type if t != "null"), "string")
if target_type in supported_types:
result["type"] = target_type
if "format" in schema and schema["format"] in supported_formats.get(
result["type"],
set(),
+12 -2
View File
@@ -7,6 +7,7 @@ import datetime
import json
import os
import zoneinfo
from collections.abc import Coroutine
from dataclasses import dataclass, field
from astrbot.api import sp
@@ -114,6 +115,7 @@ class MainAgentBuildResult:
agent_runner: AgentRunner
provider_request: ProviderRequest
provider: Provider
reset_coro: Coroutine | None = None
def _select_provider(
@@ -837,8 +839,12 @@ async def build_main_agent(
config: MainAgentBuildConfig,
provider: Provider | None = None,
req: ProviderRequest | None = None,
apply_reset: bool = True,
) -> 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)
if provider is None:
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
@@ -955,7 +961,7 @@ async def build_main_agent(
if action_type == "live":
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
await agent_runner.reset(
reset_coro = agent_runner.reset(
provider=provider,
request=req,
run_context=AgentContextWrapper(
@@ -973,8 +979,12 @@ async def build_main_agent(
tool_schema_mode=config.tool_schema_mode,
)
if apply_reset:
await reset_coro
return MainAgentBuildResult(
agent_runner=agent_runner,
provider_request=req,
provider=provider,
reset_coro=reset_coro if not apply_reset else None,
)
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
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")
WEBHOOK_SUPPORTED_PLATFORMS = [
+1
View File
@@ -310,6 +310,7 @@ class CronJobManager:
config = MainAgentBuildConfig(
tool_call_timeout=3600,
llm_safety_mode=False,
streaming_response=False,
)
req = ProviderRequest()
conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx)
@@ -1,5 +1,7 @@
"""使用此功能应该先 pip install baidu-aip"""
from typing import Any, cast
from aip import AipContentCensor
from . import ContentSafetyStrategy
@@ -23,7 +25,8 @@ class BaiduAipStrategy(ContentSafetyStrategy):
count = len(res["data"])
parts = [f"百度审核服务发现 {count} 处违规:\n"]
for i in res["data"]:
parts.append(f"{i['msg']}\n")
# 百度 AIP 返回结构是动态 dict;类型检查时 i 可能被推断为序列,转成 dict 后用 get 取字段
parts.append(f"{cast(dict[str, Any], i).get('msg', '')}\n")
parts.append("\n判断结果:" + res["conclusion"])
info = "".join(parts)
return False, info
@@ -164,6 +164,7 @@ class InternalAgentSubStage(Stage):
event=event,
plugin_context=self.ctx.plugin_manager.context,
config=build_cfg,
apply_reset=False,
)
if build_result is None:
@@ -172,6 +173,7 @@ class InternalAgentSubStage(Stage):
agent_runner = build_result.agent_runner
req = build_result.provider_request
provider = build_result.provider
reset_coro = build_result.reset_coro
api_base = provider.provider_config.get("api_base", "")
for host in decoded_blocked:
@@ -190,6 +192,10 @@ class InternalAgentSubStage(Stage):
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
return
# apply reset
if reset_coro:
await reset_coro
action_type = event.get_extra("action_type")
event.trace.record(
@@ -357,7 +363,8 @@ class InternalAgentSubStage(Stage):
token_usage = None
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(
event.unified_msg_origin,
+5 -2
View File
@@ -8,6 +8,7 @@ from time import time
from typing import Any
from astrbot import logger
from astrbot.core.agent.tool import ToolSet
from astrbot.core.db.po import Conversation
from astrbot.core.message.components import (
At,
@@ -355,6 +356,7 @@ class AstrMessageEvent(abc.ABC):
self,
prompt: str,
func_tool_manager=None,
tool_set: ToolSet | None = None,
session_id: str = "",
image_urls: list[str] | None = None,
contexts: list | None = None,
@@ -377,7 +379,7 @@ class AstrMessageEvent(abc.ABC):
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 请求,并且结果将会被记录到对话中。
@@ -393,7 +395,8 @@ class AstrMessageEvent(abc.ABC):
prompt=prompt,
session_id=session_id,
image_urls=image_urls,
func_tool=func_tool_manager,
# func_tool=func_tool_manager,
func_tool=tool_set,
contexts=contexts,
system_prompt=system_prompt,
conversation=conversation,
+2 -2
View File
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from astrbot.core.platform.message_type import MessageType
@@ -13,7 +13,7 @@ class MessageSession:
"""平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。"""
message_type: MessageType
session_id: str
platform_id: str | None = None
platform_id: str = field(init=False)
def __str__(self):
return f"{self.platform_id}:{self.message_type.value}:{self.session_id}"
@@ -21,3 +21,6 @@ class PlatformMetadata:
"""平台是否支持真实流式传输"""
support_proactive_message: bool = True
"""平台是否支持主动消息推送(非用户触发)"""
module_path: str | None = None
"""注册该适配器的模块路径,用于插件热重载时清理"""
+32
View File
@@ -37,6 +37,9 @@ def register_platform_adapter(
if "id" not in default_config_tmpl:
default_config_tmpl["id"] = adapter_name
# Get the module path of the class being decorated
module_path = cls.__module__
pm = PlatformMetadata(
name=adapter_name,
description=desc,
@@ -45,6 +48,7 @@ def register_platform_adapter(
adapter_display_name=adapter_display_name,
logo_path=logo_path,
support_streaming_message=support_streaming_message,
module_path=module_path,
)
platform_registry.append(pm)
platform_cls_map[adapter_name] = cls
@@ -52,3 +56,31 @@ def register_platform_adapter(
return cls
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
@@ -444,9 +444,20 @@ class DiscordPlatformAdapter(Platform):
logger.warning(f"[Discord] 指令 '{cmd_name}' defer 失败: {e}")
# 2. 构建 AstrBotMessage
channel = ctx.channel
abm = AstrBotMessage()
abm.type = self._get_message_type(ctx.channel, ctx.guild_id)
abm.group_id = self._get_channel_id(ctx.channel)
if channel is not None:
abm.type = self._get_message_type(channel, ctx.guild_id)
abm.group_id = self._get_channel_id(channel)
else:
# 防守式兜底:channel 取不到时,仍能根据 guild_id/channel_id 推断会话信息
abm.type = (
MessageType.GROUP_MESSAGE
if ctx.guild_id is not None
else MessageType.FRIEND_MESSAGE
)
abm.group_id = str(ctx.channel_id)
abm.message_str = message_str_for_filter
abm.sender = MessageMember(
user_id=str(ctx.author.id),
@@ -63,7 +63,7 @@ class ProviderFishAudioTTSAPI(TTSProvider):
self.headers = {
"Authorization": f"Bearer {self.chosen_api_key}",
}
self.set_model(provider_config.get("model", None))
self.set_model(provider_config.get("model", ""))
async def _get_reference_id_by_character(self, character: str) -> str | None:
"""获取角色的reference_id
+3 -3
View File
@@ -37,9 +37,9 @@ class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
class CustomFilterOr(CustomFilter):
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
super().__init__()
if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
raise ValueError(
"CustomFilter lass can only operate with other CustomFilter.",
"CustomFilter class can only operate with other CustomFilter.",
)
self.filter1 = filter1
self.filter2 = filter2
@@ -51,7 +51,7 @@ class CustomFilterOr(CustomFilter):
class CustomFilterAnd(CustomFilter):
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
super().__init__()
if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
raise ValueError(
"CustomFilter lass can only operate with other CustomFilter.",
)
+1 -1
View File
@@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs):
if args:
raise_error = args[0]
if not isinstance(custom_filter, CustomFilterAnd | CustomFilterOr):
if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
custom_filter = custom_filter(raise_error)
def decorator(awaitable):
+13
View File
@@ -15,6 +15,7 @@ import yaml
from astrbot.core import logger, pip_installer, sp
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
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.utils.astrbot_path import (
get_astrbot_config_path,
@@ -842,6 +843,18 @@ class PluginManager:
for func_tool in to_remove:
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:
return
+1 -1
View File
@@ -23,7 +23,7 @@ class CronRoute(Route):
]
self.register_routes()
def _serialize_job(self, job):
def _serialize_job(self, job) -> dict:
data = job.model_dump() if hasattr(job, "model_dump") else job.__dict__
for k in ["created_at", "updated_at", "last_run_at", "next_run_time"]:
if isinstance(data.get(k), datetime):
+2 -1
View File
@@ -4,6 +4,7 @@ import asyncio
import os
import traceback
import uuid
from typing import Any
import aiofiles
from quart import request
@@ -75,7 +76,7 @@ class KnowledgeBaseRoute(Route):
}
def _set_task_result(
self, task_id: str, status: str, result: any = None, error: str | None = None
self, task_id: str, status: str, result: Any = None, error: str | None = None
) -> None:
self.upload_tasks[task_id] = {
"status": status,
+11
View File
@@ -315,6 +315,17 @@ class PluginRoute(Route):
"display_name": plugin.display_name,
"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)
return (
Response()
+7 -3
View File
@@ -2,14 +2,13 @@ import asyncio
import logging
import os
import socket
from typing import cast
from typing import Protocol, cast
import jwt
import psutil
from flask.json.provider import DefaultJSONProvider
from hypercorn.asyncio import serve
from hypercorn.config import Config as HyperConfig
from psutil._common import addr as psutil_addr
from quart import Quart, g, jsonify, request
from quart.logging import default_handler
@@ -29,6 +28,11 @@ from .routes.session_management import SessionManagementRoute
from .routes.subagent import SubAgentRoute
from .routes.t2i import T2iRoute
class _AddrWithPort(Protocol):
port: int
APP: Quart
@@ -168,7 +172,7 @@ class AstrBotDashboard:
"""获取占用端口的进程详细信息"""
try:
for conn in psutil.net_connections(kind="inet"):
if cast(psutil_addr, conn.laddr).port == port:
if cast(_AddrWithPort, conn.laddr).port == port:
try:
process = psutil.Process(conn.pid)
# 获取详细信息
+72
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
## What's Changed - BIG AND BEAUTIFUL VERSION
hotfix of v4.14.0
fixes:
- 由 `event.request_llm()` 过时导致的群聊上下文感知-主动回复功能可能不可用的问题
+23
View File
@@ -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))
+4
View File
@@ -0,0 +1,4 @@
## What's Changed
### 修复
- 修复 `on_llm_request` 钩子可能无法应用效果的问题
+4
View File
@@ -0,0 +1,4 @@
## What's Changed
### 修复
- 修复 token 统计错误的问题,修复在多轮 tool call 情况下或者其他极端情况下可能造成 tool 无限调用的问题。
+1
View File
@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="keywords" content="AstrBot Soulter" />
<meta name="description" content="AstrBot Dashboard" />
<meta name="robots" content="noindex, nofollow" />
<link
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"
+1 -1
View File
@@ -30,7 +30,7 @@
"markdown-it": "^14.1.0",
"markstream-vue": "^0.0.6",
"mermaid": "^11.12.2",
"monaco-editor": "^0.55.1",
"monaco-editor": "^0.52.2",
"pinia": "2.1.6",
"pinyin-pro": "^3.26.0",
"remixicon": "3.5.0",
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
@@ -11,5 +11,8 @@
"mirrorLabel": "Force PyPI repository URL (optional)",
"mirrorHint": "Force PyPI repository URL > Config item `PyPI Repository Address`",
"installButton": "Install"
},
"debugHint": {
"text": "Debug logs can be enabled in \"Configuration File → System → Console Log Level\""
}
}
}
@@ -22,6 +22,7 @@
"name": "Name",
"type": "Type",
"cron": "Cron",
"session": "Session ID",
"nextRun": "Next Run",
"lastRun": "Last Run",
"note": "Note",
@@ -11,5 +11,8 @@
"mirrorLabel": "强制 PyPI 软件仓库链接(可选)",
"mirrorHint": "强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`",
"installButton": "安装"
},
"debugHint": {
"text": "Debug 日志需要在「配置文件 → 系统 → 控制台日志级别」中开启"
}
}
}
@@ -22,6 +22,7 @@
"name": "名称",
"type": "类型",
"cron": "Cron",
"session": "会话 ID",
"nextRun": "下一次执行",
"lastRun": "最近执行",
"note": "说明",
@@ -35,8 +35,8 @@
"nameHint": "建议使用英文小写+下划线,且全局唯一",
"providerLabel": "Chat Provider(可选)",
"providerHint": "留空表示跟随全局默认 provider。",
"personaLabel": "选择 Persona",
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。",
"personaLabel": "选择人格设定",
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。在人格设定页管理和新建人格。",
"descriptionLabel": "对主 LLM 的描述(用于决定是否 handoff",
"descriptionHint": "这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
},
+13 -2
View File
@@ -10,7 +10,18 @@ const { tm } = useModuleI18n('features/console');
<div style="height: 100%;">
<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;">
<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">
<v-switch
v-model="autoScrollEnabled"
@@ -111,4 +122,4 @@ export default {
.fade-in {
animation: fadeIn 0.2s ease-in-out;
}
</style>
</style>
+9 -1
View File
@@ -48,6 +48,9 @@
<div class="text-caption text-medium-emphasis">{{ item.timezone || tm('table.timezoneLocal') }}</div>
</div>
</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.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</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.type'), key: 'type', width: 110 },
{ 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.lastRun'), key: 'last_run_at', minWidth: '160px' },
{ title: tm('table.headers.note'), key: 'note', minWidth: '220px' },
@@ -163,7 +167,11 @@ async function loadJobs() {
try {
const res = await axios.get('/api/cron/jobs')
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 {
toast(res.data.message || tm('messages.loadFailed'), 'error')
}
+4 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.13.2"
version = "4.14.4"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.10"
@@ -69,14 +69,14 @@ dev = [
"pytest>=8.4.1",
"pytest-asyncio>=1.1.0",
"pytest-cov>=6.2.1",
"ruff>=0.12.8",
"ruff>=0.15.0",
]
[project.scripts]
astrbot = "astrbot.cli.__main__:cli"
[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
target-version = "py310"
@@ -97,6 +97,7 @@ ignore = [
"F405",
"E501",
"ASYNC230", # TODO: handle ASYNC230 in AstrBot
"ASYNC240", # TODO: handle ASYNC240 in AstrBot
]
[tool.pyright]
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=AstrBot Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/.local/share/astrbot
ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }'
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=default.target
+253
View File
@@ -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()