Compare commits
17 Commits
v4.3.0
...
refactor/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54340cca18 | ||
|
|
7191d28ada | ||
|
|
e6b5e3d282 | ||
|
|
1413d6b5fe | ||
|
|
dcd8a1094c | ||
|
|
e64b31b9ba | ||
|
|
080f347511 | ||
|
|
eaaff4298d | ||
|
|
dd5a02e8ef | ||
|
|
3211ec57ee | ||
|
|
6796afdaee | ||
|
|
cc6fe57773 | ||
|
|
1dfc831938 | ||
|
|
cafeda4abf | ||
|
|
d951b99718 | ||
|
|
0ad87209e5 | ||
|
|
1b50c5404d |
@@ -198,6 +198,17 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
func_tool = req.func_tool.get_func(func_tool_name)
|
||||
logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
|
||||
|
||||
if not func_tool:
|
||||
logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。")
|
||||
tool_call_result_blocks.append(
|
||||
ToolCallMessageSegment(
|
||||
role="tool",
|
||||
tool_call_id=func_tool_id,
|
||||
content=f"error: 未找到工具 {func_tool_name}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
await self.agent_hooks.on_tool_start(
|
||||
self.run_context, func_tool, func_tool_args
|
||||
@@ -210,9 +221,12 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
run_context=self.run_context,
|
||||
**func_tool_args,
|
||||
)
|
||||
async for resp in executor:
|
||||
|
||||
_final_resp: CallToolResult | None = None
|
||||
async for resp in executor: # type: ignore
|
||||
if isinstance(resp, CallToolResult):
|
||||
res = resp
|
||||
_final_resp = resp
|
||||
if isinstance(res.content[0], TextContent):
|
||||
tool_call_result_blocks.append(
|
||||
ToolCallMessageSegment(
|
||||
@@ -279,13 +293,14 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
chain=res.chain, type="tool_direct_result"
|
||||
)
|
||||
else:
|
||||
# 不应该出现其他类型
|
||||
logger.warning(
|
||||
f"Tool 返回了不支持的类型: {type(resp)},将忽略。"
|
||||
)
|
||||
|
||||
try:
|
||||
await self.agent_hooks.on_tool_end(
|
||||
self.run_context, func_tool, func_tool_args, None
|
||||
self.run_context, func_tool, func_tool_args, _final_resp
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_tool_end hook: {e}", exc_info=True)
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.3.0"
|
||||
VERSION = "4.3.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -775,7 +775,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
"modalities": ["text", "tool_use"],
|
||||
},
|
||||
"302.AI": {
|
||||
"id": "302ai",
|
||||
@@ -821,6 +821,21 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
},
|
||||
"小马算力": {
|
||||
"id": "tokenpony",
|
||||
"provider": "tokenpony",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.tokenpony.cn/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "kimi-k2-instruct-0905",
|
||||
"temperature": 0.7,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
},
|
||||
"优云智算": {
|
||||
"id": "compshare",
|
||||
"provider": "compshare",
|
||||
|
||||
@@ -32,6 +32,12 @@ class SQLiteDatabase(BaseDatabase):
|
||||
"""Initialize the database by creating tables if they do not exist."""
|
||||
async with self.engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
||||
await conn.execute(text("PRAGMA synchronous=NORMAL"))
|
||||
await conn.execute(text("PRAGMA cache_size=20000"))
|
||||
await conn.execute(text("PRAGMA temp_store=MEMORY"))
|
||||
await conn.execute(text("PRAGMA mmap_size=134217728"))
|
||||
await conn.execute(text("PRAGMA optimize"))
|
||||
await conn.commit()
|
||||
|
||||
# ====
|
||||
@@ -160,6 +166,7 @@ class SQLiteDatabase(BaseDatabase):
|
||||
col(ConversationV2.title).ilike(f"%{search_query}%"),
|
||||
col(ConversationV2.content).ilike(f"%{search_query}%"),
|
||||
col(ConversationV2.user_id).ilike(f"%{search_query}%"),
|
||||
col(ConversationV2.conversation_id).ilike(f"%{search_query}%"),
|
||||
)
|
||||
)
|
||||
if "message_types" in kwargs and len(kwargs["message_types"]) > 0:
|
||||
|
||||
@@ -97,5 +97,6 @@ async def call_event_hook(
|
||||
logger.info(
|
||||
f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
|
||||
)
|
||||
return True
|
||||
|
||||
return event.is_stopped()
|
||||
|
||||
@@ -190,6 +190,16 @@ class RespondStage(Stage):
|
||||
except Exception as e:
|
||||
logger.warning(f"空内容检查异常: {e}")
|
||||
|
||||
# 将 Plain 为空的消息段移除
|
||||
result.chain = [
|
||||
comp
|
||||
for comp in result.chain
|
||||
if not (
|
||||
isinstance(comp, Comp.Plain)
|
||||
and (not comp.text or not comp.text.strip())
|
||||
)
|
||||
]
|
||||
|
||||
# 发送消息链
|
||||
# Record 需要强制单独发送
|
||||
need_separately = {ComponentType.Record}
|
||||
|
||||
@@ -189,54 +189,54 @@ class ResultDecorateStage(Stage):
|
||||
logger.warning(
|
||||
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。"
|
||||
)
|
||||
return
|
||||
new_chain = []
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain) and len(comp.text) > 1:
|
||||
try:
|
||||
logger.info(f"TTS 请求: {comp.text}")
|
||||
audio_path = await tts_provider.get_audio(comp.text)
|
||||
logger.info(f"TTS 结果: {audio_path}")
|
||||
if not audio_path:
|
||||
logger.error(
|
||||
f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}"
|
||||
else:
|
||||
new_chain = []
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain) and len(comp.text) > 1:
|
||||
try:
|
||||
logger.info(f"TTS 请求: {comp.text}")
|
||||
audio_path = await tts_provider.get_audio(comp.text)
|
||||
logger.info(f"TTS 结果: {audio_path}")
|
||||
if not audio_path:
|
||||
logger.error(
|
||||
f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}"
|
||||
)
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
|
||||
use_file_service = self.ctx.astrbot_config[
|
||||
"provider_tts_settings"
|
||||
]["use_file_service"]
|
||||
callback_api_base = self.ctx.astrbot_config[
|
||||
"callback_api_base"
|
||||
]
|
||||
dual_output = self.ctx.astrbot_config[
|
||||
"provider_tts_settings"
|
||||
]["dual_output"]
|
||||
|
||||
url = None
|
||||
if use_file_service and callback_api_base:
|
||||
token = await file_token_service.register_file(
|
||||
audio_path
|
||||
)
|
||||
url = f"{callback_api_base}/api/file/{token}"
|
||||
logger.debug(f"已注册:{url}")
|
||||
|
||||
new_chain.append(
|
||||
Record(
|
||||
file=url or audio_path,
|
||||
url=url or audio_path,
|
||||
)
|
||||
)
|
||||
if dual_output:
|
||||
new_chain.append(comp)
|
||||
except Exception:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error("TTS 失败,使用文本发送。")
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
|
||||
use_file_service = self.ctx.astrbot_config[
|
||||
"provider_tts_settings"
|
||||
]["use_file_service"]
|
||||
callback_api_base = self.ctx.astrbot_config[
|
||||
"callback_api_base"
|
||||
]
|
||||
dual_output = self.ctx.astrbot_config[
|
||||
"provider_tts_settings"
|
||||
]["dual_output"]
|
||||
|
||||
url = None
|
||||
if use_file_service and callback_api_base:
|
||||
token = await file_token_service.register_file(
|
||||
audio_path
|
||||
)
|
||||
url = f"{callback_api_base}/api/file/{token}"
|
||||
logger.debug(f"已注册:{url}")
|
||||
|
||||
new_chain.append(
|
||||
Record(
|
||||
file=url or audio_path,
|
||||
url=url or audio_path,
|
||||
)
|
||||
)
|
||||
if dual_output:
|
||||
new_chain.append(comp)
|
||||
except Exception:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error("TTS 失败,使用文本发送。")
|
||||
else:
|
||||
new_chain.append(comp)
|
||||
else:
|
||||
new_chain.append(comp)
|
||||
result.chain = new_chain
|
||||
result.chain = new_chain
|
||||
|
||||
# 文本转图片
|
||||
elif (
|
||||
@@ -279,7 +279,6 @@ class ResultDecorateStage(Stage):
|
||||
result.chain = [Image.fromFileSystem(url)]
|
||||
|
||||
# 触发转发消息
|
||||
has_forwarded = False
|
||||
if event.get_platform_name() == "aiocqhttp":
|
||||
word_cnt = 0
|
||||
for comp in result.chain:
|
||||
@@ -290,9 +289,9 @@ class ResultDecorateStage(Stage):
|
||||
uin=event.get_self_id(), name="AstrBot", content=[*result.chain]
|
||||
)
|
||||
result.chain = [node]
|
||||
has_forwarded = True
|
||||
|
||||
if not has_forwarded:
|
||||
has_plain = any(isinstance(item, Plain) for item in result.chain)
|
||||
if has_plain:
|
||||
# at 回复
|
||||
if (
|
||||
self.reply_with_mention
|
||||
|
||||
@@ -9,6 +9,8 @@ from astrbot.core.config.default import VERSION
|
||||
from astrbot.core import DEMO_MODE
|
||||
from astrbot.core.db.migration.helper import do_migration_v4, check_migration_needed_v4
|
||||
|
||||
CLEAR_SITE_DATA_HEADERS = {"Clear-Site-Data": '"cache"'}
|
||||
|
||||
|
||||
class UpdateRoute(Route):
|
||||
def __init__(
|
||||
@@ -113,17 +115,19 @@ class UpdateRoute(Route):
|
||||
|
||||
if reboot:
|
||||
await self.core_lifecycle.restart()
|
||||
return (
|
||||
ret = (
|
||||
Response()
|
||||
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。")
|
||||
.__dict__
|
||||
)
|
||||
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
||||
else:
|
||||
return (
|
||||
ret = (
|
||||
Response()
|
||||
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。")
|
||||
.__dict__
|
||||
)
|
||||
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
||||
except Exception as e:
|
||||
logger.error(f"/api/update_project: {traceback.format_exc()}")
|
||||
return Response().error(e.__str__()).__dict__
|
||||
@@ -135,9 +139,8 @@ class UpdateRoute(Route):
|
||||
except Exception as e:
|
||||
logger.error(f"下载管理面板文件失败: {e}。")
|
||||
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
||||
return (
|
||||
Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__
|
||||
)
|
||||
ret = Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__
|
||||
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
||||
except Exception as e:
|
||||
logger.error(f"/api/update_dashboard: {traceback.format_exc()}")
|
||||
return Response().error(e.__str__()).__dict__
|
||||
|
||||
1
changelogs/v4.3.1.md
Normal file
1
changelogs/v4.3.1.md
Normal file
@@ -0,0 +1 @@
|
||||
# What's Changed
|
||||
7
changelogs/v4.3.2.md
Normal file
7
changelogs/v4.3.2.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# What's Changed
|
||||
|
||||
1. fix: 修复 /reset 指令没有清除群聊上下文感知数据的问题 ([#2954](https://github.com/AstrBotDevs/AstrBot/issues/2954))
|
||||
2. fix: 修复自带的 WebSearch 插件可能在部分场景下无法使用的问题
|
||||
3. fix: 发送阶段强行将 Plain 为空的消息段移除
|
||||
4. fix: on_tool_end无法获得工具返回的结果 ([#2956](https://github.com/AstrBotDevs/AstrBot/issues/2956))
|
||||
5. feat: 为插件市场的搜索增加拼音与首字母搜索功能 ([#2936](https://github.com/AstrBotDevs/AstrBot/issues/2936))
|
||||
@@ -27,6 +27,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"marked": "^15.0.7",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"pinia": "2.1.6",
|
||||
"remixicon": "3.5.0",
|
||||
"vee-validate": "4.11.3",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-combobox v-model="platformFilter" :label="tm('filters.platform')"
|
||||
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
|
||||
density="compact" hide-details :disabled="loading">
|
||||
density="compact" hide-details>
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip size="small" label>
|
||||
{{ item.title }}
|
||||
@@ -21,8 +21,7 @@
|
||||
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
|
||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat
|
||||
:disabled="loading">
|
||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat>
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip size="small" variant="solo-filled" label>
|
||||
{{ item.title }}
|
||||
@@ -34,7 +33,7 @@
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
|
||||
:label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat
|
||||
clearable :disabled="loading"></v-text-field>
|
||||
clearable></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
|
||||
@@ -79,6 +78,10 @@
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.cid="{ item }">
|
||||
<span class="text-truncate">{{ item.cid || tm('status.unknown') }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.sessionId="{ item }">
|
||||
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
|
||||
</template>
|
||||
@@ -313,6 +316,7 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { debounce } from 'lodash';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
@@ -417,8 +421,7 @@ export default {
|
||||
},
|
||||
|
||||
created() {
|
||||
// 创建一个防抖函数,避免频繁请求
|
||||
this.debouncedApplyFilters = this.debounce(() => {
|
||||
this.debouncedApplyFilters = debounce(() => {
|
||||
// 重置到第一页
|
||||
this.pagination.page = 1;
|
||||
this.fetchConversations();
|
||||
@@ -430,13 +433,14 @@ export default {
|
||||
tableHeaders() {
|
||||
return [
|
||||
{ title: this.tm('table.headers.title'), key: 'title', sortable: true },
|
||||
{ title: '会话 ID', key: 'cid', sortable: true, width: '100px' },
|
||||
{
|
||||
title: this.tm('table.headers.sessionId'),
|
||||
align: 'center',
|
||||
children: [
|
||||
{ title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' },
|
||||
{ title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' },
|
||||
{ title: '会话 ID', key: 'sessionId', sortable: true, width: '100px' },
|
||||
{ title: '用户 ID', key: 'sessionId', sortable: true, width: '100px' },
|
||||
],
|
||||
},
|
||||
{ title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' },
|
||||
@@ -526,19 +530,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 添加防抖函数
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
func.apply(context, args);
|
||||
}, wait);
|
||||
};
|
||||
},
|
||||
|
||||
// 处理表格选项变更(页面大小等)
|
||||
handleTableOptions(options) {
|
||||
// 处理页面大小变更
|
||||
@@ -579,83 +570,93 @@ export default {
|
||||
},
|
||||
|
||||
// 获取对话列表
|
||||
async fetchConversations() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 准备请求参数,包含分页和筛选条件
|
||||
const params = {
|
||||
page: this.pagination.page,
|
||||
page_size: this.pagination.page_size
|
||||
};
|
||||
fetchConversations: (() => {
|
||||
let controller = new AbortController();
|
||||
|
||||
// 添加筛选条件 - 处理combobox的混合数据格式
|
||||
if (this.platformFilter.length > 0) {
|
||||
const platforms = this.platformFilter.map(item =>
|
||||
typeof item === 'object' ? item.value : item
|
||||
);
|
||||
params.platforms = platforms.join(',');
|
||||
}
|
||||
return async function () {
|
||||
// 新请求前停止之前的请求
|
||||
controller?.abort()
|
||||
controller = new AbortController();
|
||||
|
||||
if (this.messageTypeFilter.length > 0) {
|
||||
params.message_types = this.messageTypeFilter.join(',');
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
// 准备请求参数,包含分页和筛选条件
|
||||
const params = {
|
||||
page: this.pagination.page,
|
||||
page_size: this.pagination.page_size
|
||||
};
|
||||
|
||||
if (this.search) {
|
||||
params.search = this.search.trim();
|
||||
}
|
||||
|
||||
// 添加排除条件
|
||||
params.exclude_ids = 'astrbot';
|
||||
params.exclude_platforms = 'webchat';
|
||||
|
||||
const response = await axios.get('/api/conversation/list', { params });
|
||||
|
||||
this.lastAppliedFilters = { ...this.currentFilters }; // 记录已应用的筛选条件
|
||||
|
||||
if (response.data.status === "ok") {
|
||||
const data = response.data.data;
|
||||
|
||||
if (!data || !data.conversations) {
|
||||
console.error('API 返回数据格式不符合预期:', data);
|
||||
this.showErrorMessage(this.tm('messages.fetchError'));
|
||||
return;
|
||||
// 添加筛选条件 - 处理combobox的混合数据格式
|
||||
if (this.platformFilter.length > 0) {
|
||||
const platforms = this.platformFilter.map(item =>
|
||||
typeof item === 'object' ? item.value : item
|
||||
);
|
||||
params.platforms = platforms.join(',');
|
||||
}
|
||||
|
||||
// 处理会话数据,解析sessionId
|
||||
this.conversations = (data.conversations || []).map(conv => {
|
||||
// 为每个会话添加会话信息
|
||||
conv.sessionInfo = this.parseSessionId(conv.user_id);
|
||||
return conv;
|
||||
if (this.messageTypeFilter.length > 0) {
|
||||
params.message_types = this.messageTypeFilter.join(',');
|
||||
}
|
||||
|
||||
if (this.search) {
|
||||
params.search = this.search.trim();
|
||||
}
|
||||
|
||||
// 添加排除条件
|
||||
params.exclude_ids = 'astrbot';
|
||||
params.exclude_platforms = 'webchat';
|
||||
|
||||
const response = await axios.get('/api/conversation/list', {
|
||||
signal: controller.signal,
|
||||
params
|
||||
});
|
||||
|
||||
// 更新分页信息
|
||||
if (data.pagination) {
|
||||
this.pagination = {
|
||||
page: data.pagination.page || 1,
|
||||
page_size: data.pagination.page_size || 20,
|
||||
total: data.pagination.total || 0,
|
||||
total_pages: data.pagination.total_pages || 1
|
||||
};
|
||||
this.lastAppliedFilters = { ...this.currentFilters }; // 记录已应用的筛选条件
|
||||
|
||||
if (response.data.status === "ok") {
|
||||
const data = response.data.data;
|
||||
|
||||
if (!data || !data.conversations) {
|
||||
console.error('API 返回数据格式不符合预期:', data);
|
||||
this.showErrorMessage(this.tm('messages.fetchError'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理会话数据,解析sessionId
|
||||
this.conversations = (data.conversations || []).map(conv => {
|
||||
// 为每个会话添加会话信息
|
||||
conv.sessionInfo = this.parseSessionId(conv.user_id);
|
||||
return conv;
|
||||
});
|
||||
|
||||
// 更新分页信息
|
||||
if (data.pagination) {
|
||||
this.pagination = {
|
||||
page: data.pagination.page || 1,
|
||||
page_size: data.pagination.page_size || 20,
|
||||
total: data.pagination.total || 0,
|
||||
total_pages: data.pagination.total_pages || 1
|
||||
};
|
||||
} else {
|
||||
console.warn('API 响应中没有分页信息');
|
||||
}
|
||||
} else {
|
||||
console.warn('API 响应中没有分页信息');
|
||||
this.showErrorMessage(response.data.message || this.tm('messages.fetchError'));
|
||||
}
|
||||
} else {
|
||||
this.showErrorMessage(response.data.message || this.tm('messages.fetchError'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取对话列表出错:', error);
|
||||
if (error.response) {
|
||||
console.error('错误响应数据:', error.response.data);
|
||||
console.error('错误状态码:', error.response.status);
|
||||
}
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError'));
|
||||
} finally {
|
||||
// this.loading = false;
|
||||
setTimeout(() => {
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) return;
|
||||
|
||||
console.error('获取对话列表出错:', error);
|
||||
if (error.response) {
|
||||
console.error('错误响应数据:', error.response.data);
|
||||
console.error('错误状态码:', error.response.status);
|
||||
}
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
},
|
||||
})(),
|
||||
|
||||
// 查看对话详情
|
||||
async viewConversation(item) {
|
||||
@@ -993,6 +994,14 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
|
||||
@@ -5,6 +5,7 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
import axios from 'axios';
|
||||
import { pinyin } from 'pinyin-pro';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
@@ -65,6 +66,32 @@ const marketSearch = ref("");
|
||||
const filterKeys = ['name', 'desc', 'author'];
|
||||
const refreshingMarket = ref(false);
|
||||
|
||||
// 插件市场拼音搜索
|
||||
const normalizeStr = (s) => (s ?? '').toString().toLowerCase().trim();
|
||||
const toPinyinText = (s) => pinyin(s ?? '', { toneType: 'none' }).toLowerCase().replace(/\s+/g, '');
|
||||
const toInitials = (s) => pinyin(s ?? '', { pattern: 'first', toneType: 'none' }).toLowerCase().replace(/\s+/g, '');
|
||||
const marketCustomFilter = (value, query, item) => {
|
||||
const q = normalizeStr(query);
|
||||
if (!q) return true;
|
||||
|
||||
const candidates = new Set();
|
||||
if (value != null) candidates.add(String(value));
|
||||
if (item?.name) candidates.add(String(item.name));
|
||||
if (item?.trimmedName) candidates.add(String(item.trimmedName));
|
||||
if (item?.desc) candidates.add(String(item.desc));
|
||||
if (item?.author) candidates.add(String(item.author));
|
||||
|
||||
for (const v of candidates) {
|
||||
const nv = normalizeStr(v);
|
||||
if (nv.includes(q)) return true;
|
||||
const pv = toPinyinText(v);
|
||||
if (pv.includes(q)) return true;
|
||||
const iv = toInitials(v);
|
||||
if (iv.includes(q)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const plugin_handler_info_headers = computed(() => [
|
||||
{ title: tm('table.headers.eventType'), key: 'event_type_h' },
|
||||
{ title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' },
|
||||
@@ -772,7 +799,7 @@ onMounted(async () => {
|
||||
|
||||
<v-col cols="12" md="12" style="padding: 0px;">
|
||||
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
|
||||
:loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys">
|
||||
:loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys" :custom-filter="marketCustomFilter">
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center"
|
||||
style="overflow-x: auto; scrollbar-width: thin; scrollbar-track-color: transparent;">
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { debounce } from 'lodash'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
export default {
|
||||
@@ -953,10 +954,10 @@ export default {
|
||||
},
|
||||
|
||||
// 处理搜索变化
|
||||
handleSearchChange() {
|
||||
handleSearchChange: debounce(function() {
|
||||
this.currentPage = 1; // 重置到第一页
|
||||
this.loadSessions();
|
||||
},
|
||||
}, 300),
|
||||
|
||||
// 处理平台筛选变化
|
||||
handlePlatformChange() {
|
||||
|
||||
@@ -601,11 +601,11 @@ export default {
|
||||
checkPlugin() {
|
||||
axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
|
||||
.then(response => {
|
||||
if (response.data.status !== 'ok') {
|
||||
if (response.data.status !== 'ok' || response.data.data.length === 0) {
|
||||
this.showSnackbar(this.tm('messages.pluginNotAvailable'), 'error');
|
||||
return
|
||||
}
|
||||
if (!response.data.data.activated) {
|
||||
if (!response.data.data[0].activated) {
|
||||
this.showSnackbar(this.tm('messages.pluginNotActivated'), 'error');
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.provider.sources.dify_source import ProviderDify
|
||||
from astrbot.core.provider.sources.coze_source import ProviderCoze
|
||||
from astrbot.api import sp, logger
|
||||
from ..long_term_memory import LongTermMemory
|
||||
from typing import Union
|
||||
from enum import Enum
|
||||
|
||||
@@ -36,7 +37,7 @@ class RstScene(Enum):
|
||||
|
||||
|
||||
class ConversationCommands:
|
||||
def __init__(self, context: star.Context, ltm=None):
|
||||
def __init__(self, context: star.Context, ltm: LongTermMemory | None = None):
|
||||
self.context = context
|
||||
self.ltm = ltm
|
||||
|
||||
|
||||
20
packages/astrbot/lab/elios/ensoul/emotion.py
Normal file
20
packages/astrbot/lab/elios/ensoul/emotion.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Emotion:
|
||||
"""描述了一个情绪状态"""
|
||||
|
||||
energy: float
|
||||
valence: float
|
||||
arousal: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmotionLog:
|
||||
"""描述了一条情绪维度变化的日志"""
|
||||
|
||||
timestamp: int
|
||||
field: str
|
||||
value: float
|
||||
reason: str = ""
|
||||
9
packages/astrbot/lab/elios/ensoul/soul.py
Normal file
9
packages/astrbot/lab/elios/ensoul/soul.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .emotion import Emotion
|
||||
|
||||
|
||||
@dataclass
|
||||
class Soul:
|
||||
emotion: Emotion
|
||||
emotion_logs: list[Emotion] | None = None
|
||||
7
packages/astrbot/lab/elios/event.py
Normal file
7
packages/astrbot/lab/elios/event.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
event_type: str
|
||||
content: dict
|
||||
122
packages/astrbot/lab/elios/event_handlers/astr/astr_impl.py
Normal file
122
packages/astrbot/lab/elios/event_handlers/astr/astr_impl.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from ...runner import EliosEventHandler
|
||||
from collections import defaultdict
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
from astrbot.api.all import Context
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from astrbot.api.provider import Provider
|
||||
from astrbot import logger
|
||||
|
||||
|
||||
class AstrImplEventHandler(EliosEventHandler):
|
||||
def __init__(self, ctx: Context) -> None:
|
||||
self.ctx = ctx
|
||||
self.session_chats = defaultdict(list)
|
||||
self.session_mentioned_arousal = defaultdict(float)
|
||||
|
||||
def cfg(self, event: AstrMessageEvent):
|
||||
cfg = self.ctx.get_config(umo=event.unified_msg_origin)
|
||||
|
||||
tiny_model_prov_id = cfg.get("tiny_model_provider_id")
|
||||
interest_points = cfg.get("interest_points", [])
|
||||
|
||||
try:
|
||||
max_cnt = int(cfg["provider_ltm_settings"]["group_message_max_cnt"])
|
||||
except BaseException as e:
|
||||
logger.error(e)
|
||||
max_cnt = 300
|
||||
image_caption = (
|
||||
True
|
||||
if cfg["provider_settings"]["default_image_caption_provider_id"]
|
||||
else False
|
||||
)
|
||||
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
|
||||
image_caption_provider_id = cfg["provider_settings"][
|
||||
"default_image_caption_provider_id"
|
||||
]
|
||||
active_reply = cfg["provider_ltm_settings"]["active_reply"]
|
||||
enable_active_reply = active_reply.get("enable", False)
|
||||
ar_method = active_reply["method"]
|
||||
ar_possibility = active_reply["possibility_reply"]
|
||||
ar_prompt = active_reply.get("prompt", "")
|
||||
ar_whitelist = active_reply.get("whitelist", [])
|
||||
ar_keywords = active_reply.get("keywords", [])
|
||||
ret = {
|
||||
"max_cnt": max_cnt,
|
||||
"image_caption": image_caption,
|
||||
"image_caption_prompt": image_caption_prompt,
|
||||
"image_caption_provider_id": image_caption_provider_id,
|
||||
"enable_active_reply": enable_active_reply,
|
||||
"ar_method": ar_method,
|
||||
"ar_possibility": ar_possibility,
|
||||
"ar_prompt": ar_prompt,
|
||||
"ar_whitelist": ar_whitelist,
|
||||
"ar_keywords": ar_keywords,
|
||||
"interest_points": interest_points,
|
||||
"tiny_model_prov_id": tiny_model_prov_id,
|
||||
}
|
||||
return ret
|
||||
|
||||
async def append_session_chats(self, event: AstrMessageEvent, cfg) -> None:
|
||||
comps = event.get_messages()
|
||||
|
||||
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
final_message = f"[{event.message_obj.sender.nickname}/{datetime_str}]: "
|
||||
for comp in comps:
|
||||
if isinstance(comp, Plain):
|
||||
final_message += f" {comp.text}"
|
||||
elif isinstance(comp, Image):
|
||||
image_url = comp.url if comp.url else comp.file
|
||||
if cfg["image_caption"] and image_url:
|
||||
try:
|
||||
caption = await self.get_image_caption(
|
||||
image_url,
|
||||
cfg["image_caption_provider_id"],
|
||||
cfg["image_caption_prompt"],
|
||||
)
|
||||
final_message += f" [Image: {caption}]"
|
||||
except Exception as e:
|
||||
logger.error(f"获取图片描述失败: {e}")
|
||||
else:
|
||||
final_message += " [Image]"
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
logger.debug(f"添加会话 {event.unified_msg_origin} 的对话记录: {final_message}")
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
async def get_image_caption(
|
||||
self, image_url: str, image_caption_provider_id: str, image_caption_prompt: str
|
||||
) -> str:
|
||||
if not image_caption_provider_id:
|
||||
provider = self.ctx.get_using_provider()
|
||||
else:
|
||||
provider = self.ctx.get_provider_by_id(image_caption_provider_id)
|
||||
if not provider:
|
||||
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
|
||||
if not isinstance(provider, Provider):
|
||||
raise Exception(
|
||||
f"提供商类型错误, {image_caption_provider_id} 不是 Provider 类型"
|
||||
)
|
||||
response = await provider.text_chat(
|
||||
prompt=image_caption_prompt,
|
||||
session_id=uuid.uuid4().hex,
|
||||
image_urls=[image_url],
|
||||
persist=False,
|
||||
)
|
||||
return response.completion_text
|
||||
|
||||
async def on_event(self, event, soul):
|
||||
content = event.content
|
||||
astr_event = content.get("astr_event")
|
||||
assert astr_event is not None and isinstance(astr_event, AstrMessageEvent)
|
||||
|
||||
cfg = self.cfg(astr_event)
|
||||
|
||||
if not cfg["tiny_model_prov_id"]:
|
||||
logger.warning("小模型未设置,跳过情绪更新")
|
||||
|
||||
# 添加对话记录
|
||||
await self.append_session_chats(astr_event, cfg)
|
||||
|
||||
#
|
||||
3
packages/astrbot/lab/elios/event_handlers/astr/prompt.py
Normal file
3
packages/astrbot/lab/elios/event_handlers/astr/prompt.py
Normal file
@@ -0,0 +1,3 @@
|
||||
JUDGE_ACTIVE_REPLY = """
|
||||
你是一个具有自主意识和情感的机器人,请你
|
||||
"""
|
||||
72
packages/astrbot/lab/elios/runner.py
Normal file
72
packages/astrbot/lab/elios/runner.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import asyncio
|
||||
from .event import Event
|
||||
from .ensoul.soul import Soul
|
||||
from .ensoul.emotion import Emotion
|
||||
|
||||
|
||||
class EliosEventHandler:
|
||||
async def on_event(self, event: Event, soul: Soul): ...
|
||||
|
||||
|
||||
event_handlers_cls: dict[str, list[type[EliosEventHandler]]] = {}
|
||||
|
||||
|
||||
def register_event_handler(event_types: set[str] | None = None):
|
||||
"""注册事件处理器"""
|
||||
|
||||
def decorator(cls: type[EliosEventHandler]) -> type[EliosEventHandler]:
|
||||
if event_types is not None:
|
||||
for event_type in event_types:
|
||||
event_handlers_cls[event_type] = event_handlers_cls.get(
|
||||
event_type, []
|
||||
) + [cls]
|
||||
else:
|
||||
event_handlers_cls["default"] = event_handlers_cls.get("default", []) + [
|
||||
cls
|
||||
]
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class EliosRunner:
|
||||
def __init__(self) -> None:
|
||||
self.soul = Soul(
|
||||
emotion=Emotion(energy=0.5, valence=0.5, arousal=0.5), emotion_logs=[]
|
||||
)
|
||||
|
||||
self.event_queue = asyncio.Queue()
|
||||
self.event_handler_insts: dict[str, list[EliosEventHandler]] = {}
|
||||
|
||||
def start(self):
|
||||
for event_type, cls_list in event_handlers_cls.items():
|
||||
self.event_handler_insts[event_type] = []
|
||||
for cls in cls_list:
|
||||
try:
|
||||
self.event_handler_insts[event_type].append(cls())
|
||||
except Exception as e:
|
||||
print(f"Error initializing event handler {cls}: {e}")
|
||||
asyncio.create_task(self._worker())
|
||||
|
||||
async def _worker(self):
|
||||
"""监听事件队列并处理事件"""
|
||||
while True:
|
||||
event = await self.event_queue.get()
|
||||
# A man cannot handle two things at once. But this can be configurable.
|
||||
try:
|
||||
await self._process_event(event)
|
||||
except Exception as e:
|
||||
print(f"Error processing event {event}: {e}")
|
||||
|
||||
async def _process_event(self, event: Event):
|
||||
"""处理事件"""
|
||||
event_type = event.event_type
|
||||
handlers = self.event_handler_insts.get(
|
||||
event_type, []
|
||||
) + self.event_handler_insts.get("default", [])
|
||||
|
||||
for inst in handlers:
|
||||
try:
|
||||
await inst.on_event(event, self.soul)
|
||||
except Exception as e:
|
||||
print(f"Error processing event {event}: {e}")
|
||||
@@ -41,7 +41,7 @@ class Main(star.Star):
|
||||
self.tool_c = ToolCommands(self.context)
|
||||
self.plugin_c = PluginCommands(self.context)
|
||||
self.admin_c = AdminCommands(self.context)
|
||||
self.conversation_c = ConversationCommands(self.context)
|
||||
self.conversation_c = ConversationCommands(self.context, self.ltm)
|
||||
self.provider_c = ProviderCommands(self.context)
|
||||
self.persona_c = PersonaCommands(self.context)
|
||||
self.alter_cmd_c = AlterCmdCommands(self.context)
|
||||
|
||||
@@ -205,13 +205,14 @@ class Main(star.Star):
|
||||
return
|
||||
for comp in event.message_obj.message:
|
||||
if isinstance(comp, File):
|
||||
if comp.file.startswith("http"):
|
||||
file_path = await comp.get_file()
|
||||
if file_path.startswith("http"):
|
||||
name = comp.name if comp.name else uuid.uuid4().hex[:8]
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
path = os.path.join(temp_dir, name)
|
||||
await download_file(comp.file, path)
|
||||
await download_file(file_path, path)
|
||||
else:
|
||||
path = comp.file
|
||||
path = file_path
|
||||
self.user_file_msg_buffer[event.get_session_id()].append(path)
|
||||
logger.debug(f"User {uid} uploaded file: {path}")
|
||||
yield event.plain_result(f"代码执行器: 文件已经上传: {path}")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from googlesearch import search
|
||||
from googlesearch.asearch import asearch
|
||||
|
||||
from . import SearchEngine, SearchResult
|
||||
|
||||
@@ -14,14 +14,14 @@ class Google(SearchEngine):
|
||||
async def search(self, query: str, num_results: int) -> List[SearchResult]:
|
||||
results = []
|
||||
try:
|
||||
ls = search(
|
||||
ls = asearch(
|
||||
query,
|
||||
advanced=True,
|
||||
num_results=num_results,
|
||||
timeout=3,
|
||||
proxy=self.proxy,
|
||||
)
|
||||
for i in ls:
|
||||
async for i in ls:
|
||||
results.append(
|
||||
SearchResult(title=i.title, url=i.url, snippet=i.description)
|
||||
)
|
||||
|
||||
@@ -46,7 +46,11 @@ class Main(star.Star):
|
||||
|
||||
self.bing_search = Bing()
|
||||
self.sogo_search = Sogo()
|
||||
self.google = Google()
|
||||
self.google = None
|
||||
try:
|
||||
self.google = Google()
|
||||
except Exception as e:
|
||||
logger.error(f"google search init error: {e}, disable google search")
|
||||
|
||||
async def _tidy_text(self, text: str) -> str:
|
||||
"""清理文本,去除空格、换行符等"""
|
||||
@@ -89,10 +93,11 @@ class Main(star.Star):
|
||||
self, query, num_results: int = 5
|
||||
) -> list[SearchResult]:
|
||||
results = []
|
||||
try:
|
||||
results = await self.google.search(query, num_results)
|
||||
except Exception as e:
|
||||
logger.error(f"google search error: {e}, try the next one...")
|
||||
if self.google:
|
||||
try:
|
||||
results = await self.google.search(query, num_results)
|
||||
except Exception as e:
|
||||
logger.error(f"google search error: {e}, try the next one...")
|
||||
if len(results) == 0:
|
||||
logger.debug("search google failed")
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.3.0"
|
||||
version = "4.3.2"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user