diff --git a/astrbot/cli/__init__.py b/astrbot/cli/__init__.py
index ea674c5c5..454a2edae 100644
--- a/astrbot/cli/__init__.py
+++ b/astrbot/cli/__init__.py
@@ -1 +1 @@
-__version__ = "4.8.0"
+__version__ = "4.9.0"
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index 6e7f919fd..3c805cf99 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
-VERSION = "4.8.0"
+VERSION = "4.9.0"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [
diff --git a/astrbot/core/log.py b/astrbot/core/log.py
index 376f5ffd6..806ebcebb 100644
--- a/astrbot/core/log.py
+++ b/astrbot/core/log.py
@@ -24,6 +24,7 @@ import asyncio
import logging
import os
import sys
+import time
from asyncio import Queue
from collections import deque
@@ -148,7 +149,7 @@ class LogQueueHandler(logging.Handler):
self.log_broker.publish(
{
"level": record.levelname,
- "time": record.asctime,
+ "time": time.time(),
"data": log_entry,
},
)
diff --git a/changelogs/v4.9.0.md b/changelogs/v4.9.0.md
new file mode 100644
index 000000000..aeccdb006
--- /dev/null
+++ b/changelogs/v4.9.0.md
@@ -0,0 +1,19 @@
+## What's Changed
+
+### 新增
+
+- 支持自定义插件源。
+- 支持飞书(Lark)的 Webhook 模式(将事件推送至开发者服务器)。
+- 支持 “禁用自带指令” 快捷配置项,启用后将禁用所有 AstrBot 自带指令。入口: WebUI -> 配置文件 -> 平台配置。
+
+### 优化
+
+- 从 WebUI 移除了开发版本渠道。
+- 当试图测试"Agent Runner"时,提示前往配置文件页测试。
+- WebUI 列表项支持批量粘贴、回车创建项目。
+
+### 修复
+
+- Gemini API 部分调用失败的问题。
+- WebUI 插件安装加载 Dialog 关闭按钮在手机端下显示异常的问题。
+- 部分情况下,WebUI 日志显示不全的问题。
\ No newline at end of file
diff --git a/dashboard/src/assets/images/loading-seio.webp b/dashboard/src/assets/images/loading-seio.webp
new file mode 100644
index 000000000..62e159f98
Binary files /dev/null and b/dashboard/src/assets/images/loading-seio.webp differ
diff --git a/dashboard/src/components/shared/ConsoleDisplayer.vue b/dashboard/src/components/shared/ConsoleDisplayer.vue
index d3fa591e4..7d6759dfd 100644
--- a/dashboard/src/components/shared/ConsoleDisplayer.vue
+++ b/dashboard/src/components/shared/ConsoleDisplayer.vue
@@ -1,6 +1,7 @@
@@ -24,8 +25,6 @@ import { storeToRefs } from 'pinia';
export default {
name: 'ConsoleDisplayer',
data() {
- const commonStore = useCommonStore();
- const { log_cache } = storeToRefs(commonStore);
return {
autoScroll: true, // 默认开启自动滚动
logColorAnsiMap: {
@@ -38,7 +37,6 @@ export default {
'\u001b[32m': 'color: #00FF00;', // green
'default': 'color: #FFFFFF;'
},
- logCache: log_cache,
historyNum_: -1,
logLevels: ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
selectedLevels: [0, 1, 2, 3, 4], // 默认选中所有级别
@@ -48,7 +46,17 @@ export default {
'WARNING': 'amber',
'ERROR': 'red',
'CRITICAL': 'purple'
- }
+ },
+ lastProcessedTime: 0, // 记录最后处理的日志时间戳
+ localLogCache: [], // 本地日志缓存
+ }
+ },
+ computed: {
+ commonStore() {
+ return useCommonStore();
+ },
+ logCache() {
+ return this.commonStore.log_cache;
}
},
props: {
@@ -63,13 +71,39 @@ export default {
},
watch: {
logCache: {
- handler(val) {
- const lastLog = val[this.logCache.length - 1];
- if (lastLog && this.isLevelSelected(lastLog.level)) {
- this.printLog(lastLog.data);
+ handler(newVal) {
+ // 基于 timestamp 处理新增的日志
+ if (newVal && newVal.length > 0) {
+ // 确保 DOM 已经准备好
+ this.$nextTick(() => {
+ // 合并到本地缓存并按时间排序
+ const newLogs = newVal.filter(log => log.time > this.lastProcessedTime);
+
+ if (newLogs.length > 0) {
+ this.localLogCache.push(...newLogs);
+ // 按时间戳排序
+ this.localLogCache.sort((a, b) => a.time - b.time);
+
+ // 只保留最新的 log_cache_max_len 条
+ if (this.localLogCache.length > this.commonStore.log_cache_max_len) {
+ this.localLogCache.splice(0, this.localLogCache.length - this.commonStore.log_cache_max_len);
+ }
+
+ // 显示新日志
+ newLogs.forEach(logItem => {
+ if (this.isLevelSelected(logItem.level)) {
+ this.printLog(logItem.data);
+ }
+ });
+
+ // 更新最后处理时间
+ this.lastProcessedTime = Math.max(...newLogs.map(log => log.time));
+ }
+ });
}
},
- deep: true
+ deep: true,
+ immediate: false
},
selectedLevels: {
handler() {
@@ -78,14 +112,37 @@ export default {
deep: true
}
},
- mounted() {
- if (this.logCache.length === 0) {
- this.delayInit()
- } else {
- this.init()
- }
+ async mounted() {
+ // 请求历史日志
+ await this.fetchLogHistory();
+
+ // 等待 DOM 准备好后,显示历史日志
+ this.$nextTick(() => {
+ if (this.localLogCache.length > 0) {
+ this.localLogCache.forEach(logItem => {
+ if (this.isLevelSelected(logItem.level)) {
+ this.printLog(logItem.data);
+ }
+ });
+ // 更新最后处理时间
+ this.lastProcessedTime = Math.max(...this.localLogCache.map(log => log.time));
+ }
+ });
},
methods: {
+ async fetchLogHistory() {
+ try {
+ const res = await axios.get('/api/log-history');
+ if (res.data.data.logs && res.data.data.logs.length > 0) {
+ this.localLogCache = [...res.data.data.logs];
+ // 按时间戳排序
+ this.localLogCache.sort((a, b) => a.time - b.time);
+ }
+ } catch (err) {
+ console.error('Failed to fetch log history:', err);
+ }
+ },
+
getLevelColor(level) {
return this.levelColors[level] || 'grey';
},
@@ -101,41 +158,22 @@ export default {
},
refreshDisplay() {
- // 清空现有的显示
const termElement = document.getElementById('term');
if (termElement) {
termElement.innerHTML = '';
- }
-
- // 重新显示符合筛选条件的日志
- this.init();
- },
-
- delayInit() {
- if (this.logCache.length === 0) {
- setTimeout(() => {
- this.delayInit()
- }, 500)
- } else {
- this.init()
- }
- },
-
- init() {
- this.historyNum_ = parseInt(this.historyNum)
- let i = 0
- for (let log of this.logCache) {
- if (this.isLevelSelected(log.level)) { // 只显示选中级别的日志
- if (this.historyNum_ != -1 && i >= this.logCache.length - this.historyNum_) {
- this.printLog(log.data)
- ++i
- } else if (this.historyNum_ == -1) {
- this.printLog(log.data)
- }
+
+ // 重新显示所有符合筛选条件的日志
+ if (this.localLogCache && this.localLogCache.length > 0) {
+ this.localLogCache.forEach(logItem => {
+ if (this.isLevelSelected(logItem.level)) {
+ this.printLog(logItem.data);
+ }
+ });
}
}
},
+
toggleAutoScroll() {
this.autoScroll = !this.autoScroll;
},
@@ -143,6 +181,11 @@ export default {
printLog(log) {
// append 一个 span 标签到 term,block 的方式
let ele = document.getElementById('term')
+ if (!ele) {
+ console.warn('term element not found, skipping log print');
+ return;
+ }
+
let span = document.createElement('pre')
let style = this.logColorAnsiMap['default']
for (let key in this.logColorAnsiMap) {
diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js
index fa8dde58b..b8e905216 100644
--- a/dashboard/src/stores/common.js
+++ b/dashboard/src/stores/common.js
@@ -16,21 +16,6 @@ export const useCommonStore = defineStore({
}),
actions: {
async createEventSource() {
-
- const fetchLogHistory = async () => {
- try {
- const res = await axios.get('/api/log-history');
- if (res.data.data.logs) {
- this.log_cache.push(...res.data.data.logs);
- } else {
- this.log_cache = [];
- }
- } catch (err) {
- console.error('Failed to fetch log history:', err);
- }
- };
- await fetchLogHistory();
-
if (this.eventSource) {
return
}
@@ -54,25 +39,9 @@ export const useCommonStore = defineStore({
const reader = response.body.getReader();
const decoder = new TextDecoder();
-
- let incompleteLine = ""; // 用于存储不完整的行
-
- const handleIncompleteLine = (line) => {
- incompleteLine += line;
- // if can parse as JSON, return it
- try {
- const data_json = JSON.parse(incompleteLine);
- incompleteLine = ""; // 清空不完整行
- return data_json;
- } catch (e) {
- return null;
- }
- }
+ let bufferedText = '';
const processStream = ({ done, value }) => {
- // get bytes length
- const bytesLength = value ? value.byteLength : 0;
- console.log(`Received ${bytesLength} bytes from live log`);
if (done) {
console.log('SSE stream closed');
setTimeout(() => {
@@ -82,44 +51,41 @@ export const useCommonStore = defineStore({
return;
}
- const text = decoder.decode(value);
- const lines = text.split('\n\n');
- lines.forEach(line => {
- if (!line.trim()) {
+ // Accumulate partial chunks; SSE data may split JSON across reads.
+ const text = decoder.decode(value, { stream: true });
+ bufferedText += text;
+
+ // Split completed events; keep the trailing partial in buffer.
+ const segments = bufferedText.split('\n\n');
+ bufferedText = segments.pop() || '';
+
+ segments.forEach(segment => {
+ const line = segment.trim();
+ if (!line.startsWith('data: ')) {
return;
}
- if (line.startsWith('data:')) {
- const data = line.substring(5).trim();
- // {"type":"log","data":"[2021-08-01 00:00:00] INFO: Hello, world!"}
- let data_json = {}
- try {
- data_json = JSON.parse(data);
- } catch (e) {
- console.warn('Invalid JSON:', data);
- // 尝试处理不完整的行
- const parsedData = handleIncompleteLine(data);
- if (parsedData) {
- data_json = parsedData;
- } else {
- return; // 如果无法解析,跳过当前行
- }
+
+ const logLine = line.replace('data: ', '').trim();
+ if (!logLine) {
+ return;
+ }
+
+ try {
+ const logObject = JSON.parse(logLine);
+ // give a uuid if not exists
+ if (!logObject.uuid) {
+ logObject.uuid = crypto.randomUUID();
}
- if (data_json.type === 'log') {
- this.log_cache.push(data_json);
- if (this.log_cache.length > this.log_cache_max_len) {
- this.log_cache.shift();
- }
- }
- } else {
- const parsedData = handleIncompleteLine(line);
- if (parsedData && parsedData.type === 'log') {
- this.log_cache.push(parsedData);
- if (this.log_cache.length > this.log_cache_max_len) {
- this.log_cache.shift();
- }
+ this.log_cache.push(logObject);
+ // Limit log cache size
+ if (this.log_cache.length > this.log_cache_max_len) {
+ this.log_cache.splice(0, this.log_cache.length - this.log_cache_max_len);
}
+ } catch (err) {
+ console.warn('Failed to parse SSE log line, skipping:', err, logLine);
}
});
+
return reader.read().then(processStream);
};
diff --git a/pyproject.toml b/pyproject.toml
index a260f55d2..6badf6d19 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
-version = "4.8.0"
+version = "4.9.0"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.10"