Compare commits
8 Commits
v4.3.2
...
refactor/ltm
| Author | SHA1 | Date | |
|---|---|---|---|
| 54340cca18 | |||
| 7191d28ada | |||
| e6b5e3d282 | |||
| 1413d6b5fe | |||
| dcd8a1094c | |||
| e64b31b9ba | |||
| 080f347511 | |||
| eaaff4298d |
@@ -775,7 +775,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
||||||
"custom_extra_body": {},
|
"custom_extra_body": {},
|
||||||
"modalities": ["text", "image", "tool_use"],
|
"modalities": ["text", "tool_use"],
|
||||||
},
|
},
|
||||||
"302.AI": {
|
"302.AI": {
|
||||||
"id": "302ai",
|
"id": "302ai",
|
||||||
@@ -821,6 +821,21 @@ CONFIG_METADATA_2 = {
|
|||||||
},
|
},
|
||||||
"custom_extra_body": {},
|
"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",
|
"id": "compshare",
|
||||||
"provider": "compshare",
|
"provider": "compshare",
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
"""Initialize the database by creating tables if they do not exist."""
|
"""Initialize the database by creating tables if they do not exist."""
|
||||||
async with self.engine.begin() as conn:
|
async with self.engine.begin() as conn:
|
||||||
await conn.run_sync(SQLModel.metadata.create_all)
|
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()
|
await conn.commit()
|
||||||
|
|
||||||
# ====
|
# ====
|
||||||
@@ -160,6 +166,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
col(ConversationV2.title).ilike(f"%{search_query}%"),
|
col(ConversationV2.title).ilike(f"%{search_query}%"),
|
||||||
col(ConversationV2.content).ilike(f"%{search_query}%"),
|
col(ConversationV2.content).ilike(f"%{search_query}%"),
|
||||||
col(ConversationV2.user_id).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:
|
if "message_types" in kwargs and len(kwargs["message_types"]) > 0:
|
||||||
|
|||||||
@@ -97,5 +97,6 @@ async def call_event_hook(
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
|
f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
|
||||||
)
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
return event.is_stopped()
|
return event.is_stopped()
|
||||||
|
|||||||
@@ -189,54 +189,54 @@ class ResultDecorateStage(Stage):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。"
|
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。"
|
||||||
)
|
)
|
||||||
return
|
else:
|
||||||
new_chain = []
|
new_chain = []
|
||||||
for comp in result.chain:
|
for comp in result.chain:
|
||||||
if isinstance(comp, Plain) and len(comp.text) > 1:
|
if isinstance(comp, Plain) and len(comp.text) > 1:
|
||||||
try:
|
try:
|
||||||
logger.info(f"TTS 请求: {comp.text}")
|
logger.info(f"TTS 请求: {comp.text}")
|
||||||
audio_path = await tts_provider.get_audio(comp.text)
|
audio_path = await tts_provider.get_audio(comp.text)
|
||||||
logger.info(f"TTS 结果: {audio_path}")
|
logger.info(f"TTS 结果: {audio_path}")
|
||||||
if not audio_path:
|
if not audio_path:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}"
|
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)
|
new_chain.append(comp)
|
||||||
continue
|
else:
|
||||||
|
|
||||||
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)
|
new_chain.append(comp)
|
||||||
else:
|
result.chain = new_chain
|
||||||
new_chain.append(comp)
|
|
||||||
result.chain = new_chain
|
|
||||||
|
|
||||||
# 文本转图片
|
# 文本转图片
|
||||||
elif (
|
elif (
|
||||||
@@ -279,7 +279,6 @@ class ResultDecorateStage(Stage):
|
|||||||
result.chain = [Image.fromFileSystem(url)]
|
result.chain = [Image.fromFileSystem(url)]
|
||||||
|
|
||||||
# 触发转发消息
|
# 触发转发消息
|
||||||
has_forwarded = False
|
|
||||||
if event.get_platform_name() == "aiocqhttp":
|
if event.get_platform_name() == "aiocqhttp":
|
||||||
word_cnt = 0
|
word_cnt = 0
|
||||||
for comp in result.chain:
|
for comp in result.chain:
|
||||||
@@ -290,9 +289,9 @@ class ResultDecorateStage(Stage):
|
|||||||
uin=event.get_self_id(), name="AstrBot", content=[*result.chain]
|
uin=event.get_self_id(), name="AstrBot", content=[*result.chain]
|
||||||
)
|
)
|
||||||
result.chain = [node]
|
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 回复
|
# at 回复
|
||||||
if (
|
if (
|
||||||
self.reply_with_mention
|
self.reply_with_mention
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from astrbot.core.config.default import VERSION
|
|||||||
from astrbot.core import DEMO_MODE
|
from astrbot.core import DEMO_MODE
|
||||||
from astrbot.core.db.migration.helper import do_migration_v4, check_migration_needed_v4
|
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):
|
class UpdateRoute(Route):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -113,17 +115,19 @@ class UpdateRoute(Route):
|
|||||||
|
|
||||||
if reboot:
|
if reboot:
|
||||||
await self.core_lifecycle.restart()
|
await self.core_lifecycle.restart()
|
||||||
return (
|
ret = (
|
||||||
Response()
|
Response()
|
||||||
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。")
|
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。")
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
|
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
||||||
else:
|
else:
|
||||||
return (
|
ret = (
|
||||||
Response()
|
Response()
|
||||||
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。")
|
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。")
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
|
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"/api/update_project: {traceback.format_exc()}")
|
logger.error(f"/api/update_project: {traceback.format_exc()}")
|
||||||
return Response().error(e.__str__()).__dict__
|
return Response().error(e.__str__()).__dict__
|
||||||
@@ -135,9 +139,8 @@ class UpdateRoute(Route):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"下载管理面板文件失败: {e}。")
|
logger.error(f"下载管理面板文件失败: {e}。")
|
||||||
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
||||||
return (
|
ret = Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__
|
||||||
Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__
|
return ret, 200, CLEAR_SITE_DATA_HEADERS
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"/api/update_dashboard: {traceback.format_exc()}")
|
logger.error(f"/api/update_dashboard: {traceback.format_exc()}")
|
||||||
return Response().error(e.__str__()).__dict__
|
return Response().error(e.__str__()).__dict__
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<v-col cols="12" sm="6" md="4">
|
<v-col cols="12" sm="6" md="4">
|
||||||
<v-combobox v-model="platformFilter" :label="tm('filters.platform')"
|
<v-combobox v-model="platformFilter" :label="tm('filters.platform')"
|
||||||
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
|
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
|
||||||
density="compact" hide-details :disabled="loading">
|
density="compact" hide-details>
|
||||||
<template v-slot:selection="{ item }">
|
<template v-slot:selection="{ item }">
|
||||||
<v-chip size="small" label>
|
<v-chip size="small" label>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
|
|
||||||
<v-col cols="12" sm="6" md="4">
|
<v-col cols="12" sm="6" md="4">
|
||||||
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
|
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
|
||||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat
|
chips multiple clearable variant="solo-filled" density="compact" hide-details flat>
|
||||||
:disabled="loading">
|
|
||||||
<template v-slot:selection="{ item }">
|
<template v-slot:selection="{ item }">
|
||||||
<v-chip size="small" variant="solo-filled" label>
|
<v-chip size="small" variant="solo-filled" label>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
<v-col cols="12" sm="12" md="4">
|
<v-col cols="12" sm="12" md="4">
|
||||||
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
|
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
|
||||||
:label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat
|
: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-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
|
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
|
||||||
@@ -79,6 +78,10 @@
|
|||||||
</v-chip>
|
</v-chip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.cid="{ item }">
|
||||||
|
<span class="text-truncate">{{ item.cid || tm('status.unknown') }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-slot:item.sessionId="{ item }">
|
<template v-slot:item.sessionId="{ item }">
|
||||||
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
|
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -313,6 +316,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
import { useCommonStore } from '@/stores/common';
|
import { useCommonStore } from '@/stores/common';
|
||||||
@@ -417,8 +421,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
// 创建一个防抖函数,避免频繁请求
|
this.debouncedApplyFilters = debounce(() => {
|
||||||
this.debouncedApplyFilters = this.debounce(() => {
|
|
||||||
// 重置到第一页
|
// 重置到第一页
|
||||||
this.pagination.page = 1;
|
this.pagination.page = 1;
|
||||||
this.fetchConversations();
|
this.fetchConversations();
|
||||||
@@ -430,13 +433,14 @@ export default {
|
|||||||
tableHeaders() {
|
tableHeaders() {
|
||||||
return [
|
return [
|
||||||
{ title: this.tm('table.headers.title'), key: 'title', sortable: true },
|
{ title: this.tm('table.headers.title'), key: 'title', sortable: true },
|
||||||
|
{ title: '会话 ID', key: 'cid', sortable: true, width: '100px' },
|
||||||
{
|
{
|
||||||
title: this.tm('table.headers.sessionId'),
|
title: this.tm('table.headers.sessionId'),
|
||||||
align: 'center',
|
align: 'center',
|
||||||
children: [
|
children: [
|
||||||
{ title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' },
|
{ 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: 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' },
|
{ 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) {
|
handleTableOptions(options) {
|
||||||
// 处理页面大小变更
|
// 处理页面大小变更
|
||||||
@@ -579,83 +570,93 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取对话列表
|
// 获取对话列表
|
||||||
async fetchConversations() {
|
fetchConversations: (() => {
|
||||||
this.loading = true;
|
let controller = new AbortController();
|
||||||
try {
|
|
||||||
// 准备请求参数,包含分页和筛选条件
|
|
||||||
const params = {
|
|
||||||
page: this.pagination.page,
|
|
||||||
page_size: this.pagination.page_size
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加筛选条件 - 处理combobox的混合数据格式
|
return async function () {
|
||||||
if (this.platformFilter.length > 0) {
|
// 新请求前停止之前的请求
|
||||||
const platforms = this.platformFilter.map(item =>
|
controller?.abort()
|
||||||
typeof item === 'object' ? item.value : item
|
controller = new AbortController();
|
||||||
);
|
|
||||||
params.platforms = platforms.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.messageTypeFilter.length > 0) {
|
this.loading = true;
|
||||||
params.message_types = this.messageTypeFilter.join(',');
|
try {
|
||||||
}
|
// 准备请求参数,包含分页和筛选条件
|
||||||
|
const params = {
|
||||||
|
page: this.pagination.page,
|
||||||
|
page_size: this.pagination.page_size
|
||||||
|
};
|
||||||
|
|
||||||
if (this.search) {
|
// 添加筛选条件 - 处理combobox的混合数据格式
|
||||||
params.search = this.search.trim();
|
if (this.platformFilter.length > 0) {
|
||||||
}
|
const platforms = this.platformFilter.map(item =>
|
||||||
|
typeof item === 'object' ? item.value : item
|
||||||
// 添加排除条件
|
);
|
||||||
params.exclude_ids = 'astrbot';
|
params.platforms = platforms.join(',');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理会话数据,解析sessionId
|
if (this.messageTypeFilter.length > 0) {
|
||||||
this.conversations = (data.conversations || []).map(conv => {
|
params.message_types = this.messageTypeFilter.join(',');
|
||||||
// 为每个会话添加会话信息
|
}
|
||||||
conv.sessionInfo = this.parseSessionId(conv.user_id);
|
|
||||||
return conv;
|
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
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新分页信息
|
this.lastAppliedFilters = { ...this.currentFilters }; // 记录已应用的筛选条件
|
||||||
if (data.pagination) {
|
|
||||||
this.pagination = {
|
if (response.data.status === "ok") {
|
||||||
page: data.pagination.page || 1,
|
const data = response.data.data;
|
||||||
page_size: data.pagination.page_size || 20,
|
|
||||||
total: data.pagination.total || 0,
|
if (!data || !data.conversations) {
|
||||||
total_pages: data.pagination.total_pages || 1
|
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 {
|
} else {
|
||||||
console.warn('API 响应中没有分页信息');
|
this.showErrorMessage(response.data.message || this.tm('messages.fetchError'));
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
this.showErrorMessage(response.data.message || this.tm('messages.fetchError'));
|
if (axios.isCancel(error)) return;
|
||||||
}
|
|
||||||
} catch (error) {
|
console.error('获取对话列表出错:', error);
|
||||||
console.error('获取对话列表出错:', error);
|
if (error.response) {
|
||||||
if (error.response) {
|
console.error('错误响应数据:', error.response.data);
|
||||||
console.error('错误响应数据:', error.response.data);
|
console.error('错误状态码:', error.response.status);
|
||||||
console.error('错误状态码:', error.response.status);
|
}
|
||||||
}
|
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError'));
|
||||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError'));
|
} finally {
|
||||||
} finally {
|
|
||||||
// this.loading = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}, 200);
|
}
|
||||||
}
|
}
|
||||||
},
|
})(),
|
||||||
|
|
||||||
// 查看对话详情
|
// 查看对话详情
|
||||||
async viewConversation(item) {
|
async viewConversation(item) {
|
||||||
@@ -993,6 +994,14 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-truncate {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* 动画 */
|
/* 动画 */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -345,6 +345,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -953,10 +954,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 处理搜索变化
|
// 处理搜索变化
|
||||||
handleSearchChange() {
|
handleSearchChange: debounce(function() {
|
||||||
this.currentPage = 1; // 重置到第一页
|
this.currentPage = 1; // 重置到第一页
|
||||||
this.loadSessions();
|
this.loadSessions();
|
||||||
},
|
}, 300),
|
||||||
|
|
||||||
// 处理平台筛选变化
|
// 处理平台筛选变化
|
||||||
handlePlatformChange() {
|
handlePlatformChange() {
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .emotion import Emotion
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Soul:
|
||||||
|
emotion: Emotion
|
||||||
|
emotion_logs: list[Emotion] | None = None
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Event:
|
||||||
|
event_type: str
|
||||||
|
content: dict
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
#
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
JUDGE_ACTIVE_REPLY = """
|
||||||
|
你是一个具有自主意识和情感的机器人,请你
|
||||||
|
"""
|
||||||
@@ -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}")
|
||||||
@@ -205,13 +205,14 @@ class Main(star.Star):
|
|||||||
return
|
return
|
||||||
for comp in event.message_obj.message:
|
for comp in event.message_obj.message:
|
||||||
if isinstance(comp, File):
|
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]
|
name = comp.name if comp.name else uuid.uuid4().hex[:8]
|
||||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||||
path = os.path.join(temp_dir, name)
|
path = os.path.join(temp_dir, name)
|
||||||
await download_file(comp.file, path)
|
await download_file(file_path, path)
|
||||||
else:
|
else:
|
||||||
path = comp.file
|
path = file_path
|
||||||
self.user_file_msg_buffer[event.get_session_id()].append(path)
|
self.user_file_msg_buffer[event.get_session_id()].append(path)
|
||||||
logger.debug(f"User {uid} uploaded file: {path}")
|
logger.debug(f"User {uid} uploaded file: {path}")
|
||||||
yield event.plain_result(f"代码执行器: 文件已经上传: {path}")
|
yield event.plain_result(f"代码执行器: 文件已经上传: {path}")
|
||||||
|
|||||||
Reference in New Issue
Block a user