Skip to content

Commit d672f1f

Browse files
author
AstrBot Local
committed
merge: sync upstream/master and keep upstream qqofficial implementation
2 parents 2a61b24 + a21bb5b commit d672f1f

49 files changed

Lines changed: 4951 additions & 45 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

astrbot/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.19.5"
1+
__version__ = "4.20.0"

astrbot/core/config/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
77

8-
VERSION = "4.19.5"
8+
VERSION = "4.20.0"
99
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
1010

1111
WEBHOOK_SUPPORTED_PLATFORMS = [

astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ async def send_streaming(self, generator, use_fallback: bool = False):
120120
self.send_buffer.chain.extend(chain.chain)
121121

122122
# 节流:按时间间隔发送中间分片
123-
current_time = asyncio.get_event_loop().time()
123+
current_time = asyncio.get_running_loop().time()
124124
if current_time - last_edit_time >= throttle_interval:
125125
ret = cast(
126126
message.Message,
@@ -130,7 +130,7 @@ async def send_streaming(self, generator, use_fallback: bool = False):
130130
ret_id = self._extract_response_message_id(ret)
131131
if ret_id is not None:
132132
stream_payload["id"] = ret_id
133-
last_edit_time = asyncio.get_event_loop().time()
133+
last_edit_time = asyncio.get_running_loop().time()
134134
self.send_buffer = None # 清空已发送的分片,避免下次重复发送旧内容
135135

136136
if isinstance(source, botpy.message.C2CMessage):

astrbot/core/tools/cron_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
3030
"properties": {
3131
"cron_expression": {
3232
"type": "string",
33-
"description": "Cron expression defining recurring schedule (e.g., '0 8 * * *').",
33+
"description": "Cron expression defining recurring schedule (e.g., '0 8 * * *' or '0 23 * * mon-fri'). Prefer named weekdays like 'mon-fri' or 'sat,sun' instead of numeric day-of-week ranges such as '1-5' to avoid ambiguity across cron implementations.",
3434
},
3535
"run_at": {
3636
"type": "string",

astrbot/dashboard/routes/backup.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,17 @@ async def download_backup(self):
977977
if not jwt_secret:
978978
return Response().error("服务器配置错误").__dict__
979979

980-
jwt.decode(token, jwt_secret, algorithms=["HS256"])
980+
# Verify JWT token with strict security options
981+
jwt.decode(
982+
token,
983+
jwt_secret,
984+
algorithms=["HS256"],
985+
options={
986+
"require": ["exp"], # Require expiration claim
987+
"verify_signature": True, # Explicitly verify signature
988+
"verify_exp": True, # Verify expiration
989+
},
990+
)
981991
except jwt.ExpiredSignatureError:
982992
return Response().error("Token 已过期,请刷新页面后重试").__dict__
983993
except jwt.InvalidTokenError:

changelogs/v4.20.0.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
## What's Changed
2+
3+
### 新增
4+
5+
- 新增俄语翻译([#6081](https://github.com/AstrBotDevs/AstrBot/pull/6081))。
6+
- QQ 官方 Bot 新增文件、语音、视频消息支持(含 WebSocket 模式)([#6063](https://github.com/AstrBotDevs/AstrBot/pull/6063))。
7+
8+
### 优化
9+
10+
- 优化 QQ 官方 Bot 的流式消息投递可靠性与主动媒体发送能力([#6131](https://github.com/AstrBotDevs/AstrBot/pull/6131))。
11+
- 优化边界场景下 booter 选择逻辑与消息发送工具([#6064](https://github.com/AstrBotDevs/AstrBot/pull/6064))。
12+
13+
### 修复
14+
15+
- 修复 Dashboard README 对话框锚点导航失效([#6083](https://github.com/AstrBotDevs/AstrBot/pull/6083))。
16+
- 优先使用具名 weekday 的 cron 示例,避免歧义([#6091](https://github.com/AstrBotDevs/AstrBot/pull/6091))。
17+
- 修复插件市场安装后状态未及时刷新的问题([#6124](https://github.com/AstrBotDevs/AstrBot/pull/6124))。
18+
- 修复插件依赖安装逻辑:仅安装缺失依赖([#6088](https://github.com/AstrBotDevs/AstrBot/pull/6088))。
19+
- 移除 Telegram 适配器中已废弃的 `normalize_whitespace` 参数([#6044](https://github.com/AstrBotDevs/AstrBot/pull/6044))。
20+
- 修复 Windows 本地 skill 文件读取问题([#6028](https://github.com/AstrBotDevs/AstrBot/pull/6028))。
21+
- 修复 Discord pre-ack emoji 配置重启后不持久化的问题([#6031](https://github.com/AstrBotDevs/AstrBot/pull/6031))。
22+
- 统一 WebUI 搜索框清空行为([#6017](https://github.com/AstrBotDevs/AstrBot/pull/6017))。
23+
- 优化插件依赖自动安装流程与 Dashboard 安装体验([#5954](https://github.com/AstrBotDevs/AstrBot/pull/5954))。
24+
25+
26+
### 文档
27+
28+
- 新增 Astrbook 和玖帕喵社区链接([#6135](https://github.com/AstrBotDevs/AstrBot/pull/6135))。
29+
- 修正文档 `docker.md``napcat.md` 中的拼写错误([#6048](https://github.com/AstrBotDevs/AstrBot/pull/6048))。
30+
- 在多语言 README 中补充官方开发群号,并改进配置元数据中的正则说明。
31+
- 更新编辑链接模式并移除过时仓库引用。
32+
33+
---
34+
35+
## What's Changed (EN)
36+
37+
### New Features
38+
39+
- Added Russian translation support ([#6081](https://github.com/AstrBotDevs/AstrBot/pull/6081)).
40+
- Added file, voice, and video message support for QQ Official Bot (including WebSocket mode) ([#6063](https://github.com/AstrBotDevs/AstrBot/pull/6063)).
41+
42+
### Improvements
43+
44+
- Improved streaming message delivery reliability and proactive media sending for QQ Official API ([#6131](https://github.com/AstrBotDevs/AstrBot/pull/6131)).
45+
- Optimized booter selection logic in edge cases and message sending tooling ([#6064](https://github.com/AstrBotDevs/AstrBot/pull/6064)).
46+
47+
### Bug Fixes
48+
49+
- Fixed broken README dialog anchor navigation in the Dashboard ([#6083](https://github.com/AstrBotDevs/AstrBot/pull/6083)).
50+
- Preferred named weekday cron examples to reduce ambiguity ([#6091](https://github.com/AstrBotDevs/AstrBot/pull/6091)).
51+
- Fixed plugin market install-state refresh after installation ([#6124](https://github.com/AstrBotDevs/AstrBot/pull/6124)).
52+
- Fixed plugin dependency installation logic to install only missing packages ([#6088](https://github.com/AstrBotDevs/AstrBot/pull/6088)).
53+
- Removed deprecated `normalize_whitespace` parameter in the Telegram adapter ([#6044](https://github.com/AstrBotDevs/AstrBot/pull/6044)).
54+
- Fixed local skill file reading issues on Windows ([#6028](https://github.com/AstrBotDevs/AstrBot/pull/6028)).
55+
- Fixed Discord pre-ack emoji config not being persisted across restarts ([#6031](https://github.com/AstrBotDevs/AstrBot/pull/6031)).
56+
- Unified WebUI search input clear behavior ([#6017](https://github.com/AstrBotDevs/AstrBot/pull/6017)).
57+
- Improved plugin dependency auto-install flow and Dashboard installation experience ([#5954](https://github.com/AstrBotDevs/AstrBot/pull/5954)).
58+
59+
### Documentation
60+
61+
- Added Astrbook and Jiupa Miao community links ([#6135](https://github.com/AstrBotDevs/AstrBot/pull/6135)).
62+
- Fixed typos in `docker.md` and `napcat.md` ([#6048](https://github.com/AstrBotDevs/AstrBot/pull/6048)).
63+
- Added official developer group IDs to multilingual READMEs and improved regex description in config metadata.
64+
- Updated edit-link patterns and removed obsolete repository references.

dashboard/src/components/shared/ReadmeDialog.vue

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ const loading = ref(false);
4848
const isEmpty = ref(false);
4949
const copyFeedbackTimer = ref(null);
5050
const lastRequestId = ref(0);
51+
const scrollContainer = ref(null);
52+
53+
function slugifyHeading(text, slugCounts) {
54+
const base = (text || "")
55+
.trim()
56+
.toLowerCase()
57+
.normalize("NFKD")
58+
.replace(/[\u0300-\u036f]/g, "")
59+
.replace(/[^\p{Letter}\p{Number}\s-]/gu, "")
60+
.replace(/\s+/g, "-")
61+
.replace(/-+/g, "-");
62+
63+
if (!base) return "";
64+
65+
const count = slugCounts.get(base) || 0;
66+
slugCounts.set(base, count + 1);
67+
return count === 0 ? base : `${base}-${count}`;
68+
}
5169
5270
onUnmounted(() => {
5371
if (copyFeedbackTimer.value) clearTimeout(copyFeedbackTimer.value);
@@ -153,6 +171,18 @@ const renderedHtml = computed(() => {
153171
// 3. 后处理方案:完全隔离,安全性最高
154172
const tempDiv = document.createElement("div");
155173
tempDiv.innerHTML = cleanHtml;
174+
175+
const slugCounts = new Map();
176+
tempDiv.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((heading) => {
177+
if (heading.id) {
178+
slugCounts.set(heading.id, (slugCounts.get(heading.id) || 0) + 1);
179+
return;
180+
}
181+
182+
const slug = slugifyHeading(heading.textContent, slugCounts);
183+
if (slug) heading.id = slug;
184+
});
185+
156186
tempDiv.querySelectorAll("a").forEach((link) => {
157187
const href = link.getAttribute("href");
158188
// 强制所有外部链接使用安全的 _blank 策略
@@ -251,18 +281,35 @@ watch(
251281
252282
function handleContainerClick(event) {
253283
const btn = event.target.closest(".copy-code-btn");
254-
if (!btn) return;
255-
const code = btn.closest(".code-block-wrapper")?.querySelector("code");
256-
if (code) {
257-
if (navigator.clipboard?.writeText) {
258-
navigator.clipboard
259-
.writeText(code.textContent)
260-
.then(() => showCopyFeedback(btn, true))
261-
.catch(() => tryFallbackCopy(code.textContent, btn));
262-
} else {
263-
tryFallbackCopy(code.textContent, btn);
284+
if (btn) {
285+
const code = btn.closest(".code-block-wrapper")?.querySelector("code");
286+
if (code) {
287+
if (navigator.clipboard?.writeText) {
288+
navigator.clipboard
289+
.writeText(code.textContent)
290+
.then(() => showCopyFeedback(btn, true))
291+
.catch(() => tryFallbackCopy(code.textContent, btn));
292+
} else {
293+
tryFallbackCopy(code.textContent, btn);
294+
}
264295
}
296+
return;
265297
}
298+
299+
const anchor = event.target.closest('a[href^="#"]');
300+
if (!anchor) return;
301+
302+
const rawHref = anchor.getAttribute("href");
303+
const targetId = rawHref ? decodeURIComponent(rawHref.slice(1)) : "";
304+
if (!targetId) return;
305+
306+
const target = scrollContainer.value?.querySelector(
307+
`#${CSS.escape(targetId)}`,
308+
);
309+
if (!target) return;
310+
311+
event.preventDefault();
312+
target.scrollIntoView({ behavior: "smooth", block: "start" });
266313
}
267314
268315
function tryFallbackCopy(text, btn) {
@@ -326,7 +373,7 @@ const showActionArea = computed(() => {
326373
<v-icon>mdi-close</v-icon>
327374
</v-btn>
328375
</v-card-title>
329-
<v-card-text style="overflow-y: auto">
376+
<v-card-text ref="scrollContainer" style="overflow-y: auto">
330377
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
331378
<v-btn
332379
v-if="modeConfig.showGithubButton && repoUrl"
@@ -436,6 +483,7 @@ const showActionArea = computed(() => {
436483
margin-bottom: 16px;
437484
font-weight: 600;
438485
line-height: 1.25;
486+
scroll-margin-top: 12px;
439487
}
440488
441489
:deep(.markdown-body h1) {

dashboard/src/i18n/composables.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const translations = ref<Record<string, any>>({});
1111
*/
1212
export async function initI18n(locale: Locale = 'zh-CN') {
1313
currentLocale.value = locale;
14-
14+
1515
// 加载静态翻译数据
1616
loadTranslations(locale);
1717
}
@@ -50,7 +50,7 @@ export function useI18n() {
5050
const t = (key: string, params?: Record<string, string | number>): string => {
5151
const keys = key.split('.');
5252
let value: any = translations.value;
53-
53+
5454
// 遍历键路径
5555
for (const k of keys) {
5656
if (value && typeof value === 'object' && k in value) {
@@ -61,35 +61,35 @@ export function useI18n() {
6161
return `[MISSING: ${key}]`;
6262
}
6363
}
64-
64+
6565
if (typeof value !== 'string') {
6666
console.warn(`Translation value is not string: ${key}`, value);
6767
// 返回带括号的键名,便于在开发时识别类型错误的翻译
6868
return `[INVALID: ${key}]`;
6969
}
70-
70+
7171
// 此时value确定是string类型
7272
let result: string = value;
73-
73+
7474
// 处理参数插值
7575
if (params) {
7676
result = result.replace(/\{(\w+)\}/g, (match: string, paramKey: string) => {
7777
return params[paramKey]?.toString() || match;
7878
});
7979
}
80-
80+
8181
return result;
8282
};
83-
83+
8484
// 切换语言
8585
const setLocale = async (newLocale: Locale) => {
8686
if (newLocale !== currentLocale.value) {
8787
currentLocale.value = newLocale;
8888
loadTranslations(newLocale);
89-
89+
9090
// 保存到localStorage
9191
localStorage.setItem('astrbot-locale', newLocale);
92-
92+
9393
// 触发自定义事件,通知相关页面重新加载配置数据
9494
// 这是因为插件适配器的 i18n 数据是通过后端 API 注入的,
9595
// 需要根据 Accept-Language 头重新获取
@@ -98,16 +98,16 @@ export function useI18n() {
9898
}));
9999
}
100100
};
101-
101+
102102
// 获取当前语言
103103
const locale = computed(() => currentLocale.value);
104-
104+
105105
// 获取可用语言列表
106-
const availableLocales: Locale[] = ['zh-CN', 'en-US'];
107-
106+
const availableLocales: Locale[] = ['zh-CN', 'en-US', 'ru-RU'];
107+
108108
// 检查是否已加载
109109
const isLoaded = computed(() => Object.keys(translations.value).length > 0);
110-
110+
111111
return {
112112
t,
113113
locale,
@@ -122,13 +122,13 @@ export function useI18n() {
122122
*/
123123
export function useModuleI18n(moduleName: string) {
124124
const { t } = useI18n();
125-
125+
126126
const tm = (key: string, params?: Record<string, string | number>): string => {
127127
// 将斜杠转换为点号以匹配嵌套对象结构
128128
const normalizedModuleName = moduleName.replace(/\//g, '.');
129129
return t(`${normalizedModuleName}.${key}`, params);
130130
};
131-
131+
132132
// 获取原始翻译值(可能是字符串、数组或对象)
133133
const getRaw = (key: string): any => {
134134
const normalizedModuleName = moduleName.replace(/\//g, '.');
@@ -143,10 +143,10 @@ export function useModuleI18n(moduleName: string) {
143143
return null;
144144
}
145145
}
146-
146+
147147
return value;
148148
};
149-
149+
150150
return { tm, getRaw };
151151
}
152152

@@ -155,20 +155,21 @@ export function useModuleI18n(moduleName: string) {
155155
*/
156156
export function useLanguageSwitcher() {
157157
const { locale, setLocale, availableLocales } = useI18n();
158-
158+
159159
const languageOptions = computed(() => [
160160
{ value: 'zh-CN', label: '简体中文', flag: '🇨🇳' },
161-
{ value: 'en-US', label: 'English', flag: '🇺🇸' }
161+
{ value: 'en-US', label: 'English', flag: '🇺🇸' },
162+
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' }
162163
]);
163-
164+
164165
const currentLanguage = computed(() => {
165166
return languageOptions.value.find(lang => lang.value === locale.value);
166167
});
167-
168+
168169
const switchLanguage = async (newLocale: Locale) => {
169170
await setLocale(newLocale);
170171
};
171-
172+
172173
return {
173174
locale,
174175
languageOptions,
@@ -220,9 +221,9 @@ function deepMerge(target: Record<string, any>, source: Record<string, any>) {
220221
export async function setupI18n() {
221222
// 从localStorage获取保存的语言设置
222223
const savedLocale = localStorage.getItem('astrbot-locale') as Locale;
223-
const initialLocale = savedLocale && ['zh-CN', 'en-US'].includes(savedLocale)
224-
? savedLocale
224+
const initialLocale = savedLocale && ['zh-CN', 'en-US', 'ru-RU'].includes(savedLocale)
225+
? savedLocale
225226
: 'zh-CN';
226-
227+
227228
await initI18n(initialLocale);
228229
}

dashboard/src/i18n/locales/en-US/features/config-metadata.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
},
7979
"persona": {
8080
"description": "Persona",
81+
"hint": "Set the default persona for AI conversations. Personas can be managed in the Persona tab.",
8182
"provider_settings": {
8283
"default_personality": {
8384
"description": "Default Persona"

0 commit comments

Comments
 (0)