Compare commits

...

6 Commits

Author SHA1 Message Date
Soulter 8faaa4b2be feat: add error handling for disabled sandbox runtime in get_booter function 2026-03-12 00:28:45 +08:00
Soulter 7f5bd942b3 feat: add video message support and enhance message type descriptions in SendMessageToUserTool 2026-03-12 00:26:56 +08:00
Soulter e254caf82d fix(docs): add official developer group ID to multiple language READMEs and enhance regex description in config metadata 2026-03-11 21:26:11 +08:00
Soulter 7efcd242d6 fix(docs): update edit link patterns and remove obsolete repository reference 2026-03-11 17:42:42 +08:00
JIANG Zijun 5d811d3949 fix: Persist Discord pre-ack emoji config across restart by adding missing default key (#6031)
* Initial plan

* fix: add discord default platform_specific pre-ack config

Co-authored-by: Jzjerry <20167827+Jzjerry@users.noreply.github.com>

* Delete tests/unit/test_config.py

we don't need to add tests

* fix: use 🤔 as default discord pre-ack emoji

Co-authored-by: Jzjerry <20167827+Jzjerry@users.noreply.github.com>

* add back old test config

* doc: discord pre-ack-emoji doc

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Jzjerry <20167827+Jzjerry@users.noreply.github.com>
2026-03-11 16:41:08 +08:00
Flartiny 8e6aaee10c fix(webui): unify search input clear behavior (#6017)
* fix(webui): unify search input clear behavior

* fix: centralize search input normalization
2026-03-11 15:14:16 +08:00
27 changed files with 116 additions and 35 deletions
+2 -1
View File
@@ -234,7 +234,8 @@ pre-commit install
- Group 7: 743746109
- Group 8: 1030353265
- Developer Group: 975206796
- Developer Group(Chit-chat): 975206796
- Developer Group(Formal): 1039761811
### Discord Server
+1
View File
@@ -222,6 +222,7 @@ pre-commit install
- Groupe 5 : 822130018
- Groupe 6 : 753075035
- Groupe développeurs : 975206796
- Groupe développeurs (officiel) : 1039761811
### Serveur Discord
+1
View File
@@ -223,6 +223,7 @@ pre-commit install
- 5群: 822130018
- 6群: 753075035
- 開発者群: 975206796
- 開発者群(正式): 1039761811
### Discord サーバー
+1
View File
@@ -222,6 +222,7 @@ pre-commit install
- Группа 5: 822130018
- Группа 6: 753075035
- Группа разработчиков: 975206796
- Группа разработчиков (официальная): 1039761811
### Сервер Discord
+2 -1
View File
@@ -225,7 +225,8 @@ pre-commit install
- 6 群:753075035
- 7 群:743746109
- 8 群:1030353265
- 開發者群:975206796
- 開發者群(闲聊吹水)975206796
- 開發者群(正式):1039761811
### Discord 群組
+2 -1
View File
@@ -226,7 +226,8 @@ pre-commit install
- 6 群:753075035
- 7 群:743746109
- 8 群:1030353265
- 开发者群:975206796
- 开发者群(偏闲聊吹水)975206796
- 开发者群(正式):1039761811
### Discord 频道
+14 -1
View File
@@ -204,7 +204,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
"type": "string",
"description": (
"Component type. One of: "
"plain, image, record, file, mention_user"
"plain, image, record, video, file, mention_user. Record is voice message."
),
},
"text": {
@@ -320,6 +320,19 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
components.append(Comp.Record.fromURL(url=url))
else:
return f"error: messages[{idx}] must include path or url for record component."
elif msg_type == "video":
path = msg.get("path")
url = msg.get("url")
if path:
(
local_path,
file_from_sandbox,
) = await self._resolve_path_from_sandbox(context, path)
components.append(Comp.Video.fromFileSystem(path=local_path))
elif url:
components.append(Comp.Video.fromURL(url=url))
else:
return f"error: messages[{idx}] must include path or url for video component."
elif msg_type == "file":
path = msg.get("path")
url = msg.get("url")
+6
View File
@@ -422,6 +422,12 @@ async def get_booter(
) -> ComputerBooter:
config = context.get_config(umo=session_id)
runtime = config.get("provider_settings", {}).get("computer_use_runtime", "local")
if runtime == "local":
return get_local_booter()
elif runtime == "none":
raise RuntimeError("Sandbox runtime is disabled by configuration.")
sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
booter_type = sandbox_cfg.get("booter", "shipyard_neo")
+3
View File
@@ -219,6 +219,9 @@ DEFAULT_CONFIG = {
"telegram": {
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
},
"discord": {
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
},
},
"wake_prefix": ["/"],
"log_level": "INFO",
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import { normalizeTextInput } from '@/utils/inputValue';
const { tm } = useModuleI18n('features/command');
@@ -52,6 +53,7 @@ const statusItems = [
{ title: tm('filters.disabled'), value: 'disabled' },
{ title: tm('filters.conflict'), value: 'conflict' }
];
</script>
<template>
@@ -108,10 +110,11 @@ const statusItems = [
<div style="min-width: 200px; max-width: 350px; flex: 1; border: 1px solid #B9B9B9; border-radius: 16px;">
<v-text-field
:model-value="searchQuery"
@update:model-value="emit('update:searchQuery', $event)"
@update:model-value="emit('update:searchQuery', normalizeTextInput($event))"
density="compact"
:label="tm('search.placeholder')"
prepend-inner-icon="mdi-magnify"
clearable
variant="solo-filled"
flat
hide-details
@@ -3,6 +3,7 @@
*/
import { ref, computed, type Ref } from 'vue';
import type { CommandItem, FilterState } from '../types';
import { normalizeTextInput } from '@/utils/inputValue';
export function useCommandFilters(commands: Ref<CommandItem[]>) {
// 过滤状态
@@ -95,7 +96,7 @@ export function useCommandFilters(commands: Ref<CommandItem[]>) {
* 过滤后的指令列表(支持层级结构)
*/
const filteredCommands = computed(() => {
const query = searchQuery.value.toLowerCase();
const query = normalizeTextInput(searchQuery.value).toLowerCase();
const conflictCmds: CommandItem[] = [];
const normalCmds: CommandItem[] = [];
@@ -184,4 +185,3 @@ export function useCommandFilters(commands: Ref<CommandItem[]>) {
isGroupExpanded
};
}
@@ -15,6 +15,7 @@
import { computed, onActivated, onMounted, ref, watch} from 'vue';
import axios from 'axios';
import { useModuleI18n } from '@/i18n/composables';
import { normalizeTextInput } from '@/utils/inputValue';
// Composables
import { useComponentData } from './composables/useComponentData';
@@ -83,7 +84,7 @@ const {
} = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed')));
const filteredTools = computed(() => {
const query = toolSearch.value.trim().toLowerCase();
const query = normalizeTextInput(toolSearch.value).trim().toLowerCase();
if (!query) return tools.value;
return tools.value.filter(tool =>
tool.name?.toLowerCase().includes(query) ||
@@ -253,7 +254,8 @@ watch(viewMode, async (mode) => {
<div class="d-flex flex-wrap align-center ga-3 mb-4">
<div style="min-width: 240px; max-width: 380px; flex: 1;">
<v-text-field
v-model="toolSearch"
:model-value="toolSearch"
@update:model-value="toolSearch = normalizeTextInput($event)"
prepend-inner-icon="mdi-magnify"
:label="tmTool('functionTools.search')"
variant="outlined"
@@ -7,6 +7,7 @@
v-model="modelSearchProxy"
density="compact"
prepend-inner-icon="mdi-magnify"
clearable
hide-details
variant="solo-filled"
flat
@@ -161,6 +162,7 @@
<script setup>
import { computed } from 'vue'
import { normalizeTextInput } from '@/utils/inputValue'
const props = defineProps({
entries: {
@@ -222,7 +224,7 @@ const emit = defineEmits([
const modelSearchProxy = computed({
get: () => props.modelSearch,
set: (val) => emit('update:modelSearch', val)
set: (val) => emit('update:modelSearch', normalizeTextInput(val))
})
const isProviderTesting = (providerId) => props.testingProviders.includes(providerId)
@@ -2,6 +2,7 @@ import { ref, computed, onMounted, nextTick, watch } from 'vue'
import axios from 'axios'
import { getProviderIcon } from '@/utils/providerUtils'
import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog'
import { normalizeTextInput } from '@/utils/inputValue'
export interface UseProviderSourcesOptions {
defaultTab?: string
@@ -157,7 +158,7 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
})
const filteredMergedModelEntries = computed(() => {
const term = modelSearch.value.trim().toLowerCase()
const term = normalizeTextInput(modelSearch.value).trim().toLowerCase()
if (!term) return mergedModelEntries.value
return mergedModelEntries.value.filter((entry: any) => {
@@ -873,7 +873,8 @@
]
},
"regex": {
"description": "Segmentation Regular Expression"
"description": "Segmentation Regular Expression",
"hint": "Used to identify split points with a regular expression. Prefer patterns that match separators."
},
"split_words": {
"description": "Split Word List",
@@ -876,7 +876,8 @@
]
},
"regex": {
"description": "分段正则表达式"
"description": "分段正则表达式",
"hint": "用于按正则规则识别分段点。建议使用能匹配分隔符的表达式。"
},
"split_words": {
"description": "分段词列表",
+2
View File
@@ -0,0 +1,2 @@
export const normalizeTextInput = (value: unknown): string =>
typeof value === 'string' ? value : '';
+7 -1
View File
@@ -13,9 +13,11 @@
</v-select>
<v-text-field
class="config-search-input"
v-model="configSearchKeyword"
:model-value="configSearchKeyword"
@update:model-value="onConfigSearchInput"
prepend-inner-icon="mdi-magnify"
:label="tm('search.placeholder')"
clearable
hide-details
density="compact"
rounded="md"
@@ -211,6 +213,7 @@ import {
useConfirmDialog
} from '@/utils/confirmDialog';
import UnsavedChangesConfirmDialog from '@/components/config/UnsavedChangesConfirmDialog.vue';
import { normalizeTextInput } from '@/utils/inputValue';
export default {
name: 'ConfigPage',
@@ -419,6 +422,9 @@ export default {
},
methods: {
onConfigSearchInput(value) {
this.configSearchKeyword = normalizeTextInput(value);
},
extractConfigTypeFromHash(hash) {
const rawHash = String(hash || '');
const lastHashIndex = rawHash.lastIndexOf('#');
+10 -4
View File
@@ -353,10 +353,11 @@
<v-window-item value="search">
<div class="search-container pa-4">
<v-form @submit.prevent="searchKnowledgeBase" class="d-flex align-center">
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')"
<v-text-field :model-value="searchQuery"
@update:model-value="onSearchQueryInput" :label="tm('search.queryLabel')"
append-icon="mdi-magnify" variant="outlined" class="flex-grow-1 me-2"
@click:append="searchKnowledgeBase" @keyup.enter="searchKnowledgeBase"
:placeholder="tm('search.queryPlaceholder')" hide-details></v-text-field>
:placeholder="tm('search.queryPlaceholder')" hide-details clearable></v-text-field>
<v-select v-model="topK" :items="[3, 5, 10, 20]"
:label="tm('search.resultCountLabel')" variant="outlined"
@@ -434,6 +435,7 @@
import axios from 'axios';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import { useModuleI18n } from '@/i18n/composables';
import { normalizeTextInput } from '@/utils/inputValue';
export default {
name: 'KnowledgeBase',
@@ -580,6 +582,9 @@ export default {
this.getProviderList();
},
methods: {
onSearchQueryInput(value) {
this.searchQuery = normalizeTextInput(value);
},
getSelectedGitHubProxy() {
if (typeof window === "undefined" || !window.localStorage) return "";
return localStorage.getItem("githubProxyRadioValue") === "1"
@@ -903,7 +908,8 @@ export default {
},
searchKnowledgeBase() {
if (!this.searchQuery.trim()) {
const query = normalizeTextInput(this.searchQuery).trim();
if (!query) {
this.showSnackbar(this.tm('messages.pleaseEnterSearchContent'), 'warning');
return;
}
@@ -914,7 +920,7 @@ export default {
axios.get(`/api/plug/alkaid/kb/collection/search`, {
params: {
collection_name: this.currentKB.collection_name,
query: this.searchQuery,
query,
top_k: this.topK
}
})
+19 -8
View File
@@ -37,10 +37,12 @@
<h3>{{ tm('search.title') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div>
<v-text-field v-model="searchMemoryUserId" :label="tm('search.userIdLabel')" variant="outlined" density="compact" hide-details
class="mb-2"></v-text-field>
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')" variant="outlined" density="compact" hide-details
@keyup.enter="searchMemory" class="mb-2"></v-text-field>
<v-text-field :model-value="searchMemoryUserId"
@update:model-value="onSearchMemoryUserIdInput" :label="tm('search.userIdLabel')" variant="outlined" density="compact" hide-details
class="mb-2" clearable></v-text-field>
<v-text-field :model-value="searchQuery"
@update:model-value="onSearchQueryInput" :label="tm('search.queryLabel')" variant="outlined" density="compact" hide-details
@keyup.enter="searchMemory" class="mb-2" clearable></v-text-field>
<v-btn color="info" @click="searchMemory" :loading="isSearching" variant="tonal">
<v-icon start>mdi-text-search</v-icon>
{{ tm('search.searchButton') }}
@@ -254,6 +256,7 @@
import axios from 'axios';
// import * as d3 from "d3"; // npm install d3
import { useModuleI18n } from '@/i18n/composables';
import { normalizeTextInput } from '@/utils/inputValue';
export default {
name: 'LongTermMemory',
@@ -336,9 +339,16 @@ export default {
this.searchResults = [];
},
methods: {
onSearchMemoryUserIdInput(value) {
this.searchMemoryUserId = normalizeTextInput(value);
},
onSearchQueryInput(value) {
this.searchQuery = normalizeTextInput(value);
},
// 添加搜索记忆方法
searchMemory() {
if (!this.searchQuery.trim()) {
const query = normalizeTextInput(this.searchQuery).trim();
if (!query) {
this.$toast.warning(this.tm('messages.searchQueryRequired'));
return;
}
@@ -349,12 +359,13 @@ export default {
// 构建查询参数
const params = {
query: this.searchQuery
query
};
// 如果有选择用户ID,也加入查询参数
if (this.searchMemoryUserId) {
params.user_id = this.searchMemoryUserId;
const normalizedUserId = normalizeTextInput(this.searchMemoryUserId).trim();
if (normalizedUserId) {
params.user_id = normalizedUserId;
}
axios.get('/api/plug/alkaid/ltm/graph/search', { params })
@@ -3,6 +3,7 @@ import PluginSortControl from "@/components/extension/PluginSortControl.vue";
import ExtensionCard from "@/components/shared/ExtensionCard.vue";
import StyledMenu from "@/components/shared/StyledMenu.vue";
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
import { normalizeTextInput } from "@/utils/inputValue";
const props = defineProps({
state: {
@@ -164,10 +165,12 @@ const {
<div class="d-flex align-center flex-wrap ml-auto" style="gap: 8px">
<v-text-field
v-model="pluginSearch"
:model-value="pluginSearch"
@update:model-value="pluginSearch = normalizeTextInput($event)"
density="compact"
:label="tm('search.placeholder')"
prepend-inner-icon="mdi-magnify"
clearable
variant="solo-filled"
flat
hide-details
@@ -3,6 +3,7 @@ import MarketPluginCard from "@/components/extension/MarketPluginCard.vue";
import PluginSortControl from "@/components/extension/PluginSortControl.vue";
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
import { computed } from "vue";
import { normalizeTextInput } from "@/utils/inputValue";
const props = defineProps({
state: {
@@ -212,11 +213,13 @@ const marketSortItems = computed(() => [
</div>
<v-text-field
v-model="marketSearch"
:model-value="marketSearch"
@update:model-value="marketSearch = normalizeTextInput($event)"
class="ml-auto"
density="compact"
:label="tm('search.marketPlaceholder')"
prepend-inner-icon="mdi-magnify"
clearable
variant="solo-filled"
flat
hide-details
+2 -2
View File
@@ -245,7 +245,7 @@ export default defineConfig({
next: '下一篇'
},
editLink: {
pattern: 'https://github.com/AstrBotdevs/AstrBot-docs/edit/v4/:path',
pattern: 'https://github.com/AstrBotdevs/AstrBot/edit/master/docs/:path',
text: '发现文档有问题?在 GitHub 上编辑此页',
},
logo: '/logo_prod.png',
@@ -484,7 +484,7 @@ export default defineConfig({
next: 'Next'
},
editLink: {
pattern: 'https://github.com/AstrBotdevs/AstrBot-docs/edit/v4/:path',
pattern: 'https://github.com/AstrBotdevs/AstrBot/edit/master/docs/:path',
text: 'Edit this page on GitHub',
},
logo: '/logo_prod.png',
-2
View File
@@ -14,8 +14,6 @@ Welcome to submit Issues or Pull Requests:
- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot)
- [AstrBotDevs/AstrBot-Docs](https://github.com/AstrBotDevs/AstrBot-docs)
### Tencent QQ Groups
> - All groups are available to join. If you find that the group size is below the limit, please feel free to join.
+8
View File
@@ -128,6 +128,9 @@ The default AstrBot configuration is as follows:
"telegram": {
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
},
"discord": {
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
},
},
"wake_prefix": ["/"],
"log_level": "INFO",
@@ -511,6 +514,11 @@ When enabled, AstrBot sends a pre-reply emoji before requesting the LLM to infor
- `enable`: Whether to enable pre-reply emojis for Telegram messages. Default is `false`.
- `emojis`: List of pre-reply emojis. Default is `["✍️"]`. Telegram only supports a fixed set of reactions; refer to [reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9).
##### discord
- `enable`: Whether to enable pre-reply emojis for Discord messages. Default is `false`.
- `emojis`: List of pre-reply emojis. Default is `["🤔"]`. Refer to [Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ).
### `wake_prefix`
Wake prefix. Default is `/`. When a message starts with `/`, AstrBot is awakened.
-2
View File
@@ -29,8 +29,6 @@ https://discord.gg/PxgzhmxJ
- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot)
- [AstrBotDevs/AstrBot-Docs](https://github.com/AstrBotDevs/AstrBot-docs)
## 成为 AstrBot 组织成员
欢迎加入我们!
+9 -1
View File
@@ -128,6 +128,9 @@ AstrBot 默认配置如下:
"telegram": {
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
},
"discord": {
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
},
},
"wake_prefix": ["/"],
"log_level": "INFO",
@@ -506,11 +509,16 @@ AstrBot WebUI 配置。
- `enable`: 是否启用飞书消息预回复表情。默认为 `false`
- `emojis`: 预回复的表情列表。默认为 `["Typing"]`。表情枚举名参考:[表情文案说明](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce)
#### telegram
##### telegram
- `enable`: 是否启用 Telegram 消息预回复表情。默认为 `false`
- `emojis`: 预回复的表情列表。默认为 `["✍️"]`。Telegram 仅支持固定反应集合,参考:[reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9)
##### discord
- `enable`: 是否启用 Discord 消息预回复表情。默认为 `false`
- `emojis`: 预回复的表情列表。默认为 `["🤔"]`。Discord反应支持参考:[Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ)
### `wake_prefix`
唤醒前缀。默认为 `/`。当消息以 `/` 开头时,AstrBot 会被唤醒。