Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions astrbot/dashboard/routes/session_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,37 +328,87 @@ async def batch_delete_session_rule(self):

请求体:
{
"umos": ["平台:消息类型:会话ID", ...] // umo 列表
"umos": ["平台:消息类型:会话ID", ...], // 可选
"scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围
"group_id": "分组ID", // 当 scope 为 custom_group 时必填
"rule_key": "session_service_config" | ... (可选,不传则删除所有规则)
}
"""

try:
data = await request.get_json()
umos = data.get("umos", [])
scope = data.get("scope", "")
group_id = data.get("group_id", "")
rule_key = data.get("rule_key")

# 如果指定了 scope,获取符合条件的所有 umo
if scope and not umos:
# 如果是自定义分组
if scope == "custom_group":
if not group_id:
return Response().error("请指定分组 ID").__dict__
groups = self._get_groups()
if group_id not in groups:
return Response().error(f"分组 '{group_id}' 不存在").__dict__
umos = groups[group_id].get("umos", [])
else:
async with self.db_helper.get_db() as session:
session: AsyncSession
result = await session.execute(
select(ConversationV2.user_id).distinct()
)
all_umos = [row[0] for row in result.fetchall()]

if scope == "group":
umos = [
u
for u in all_umos
if ":group:" in u.lower() or ":groupmessage:" in u.lower()
]
elif scope == "private":
umos = [
u
for u in all_umos
if ":private:" in u.lower() or ":friend" in u.lower()
]
elif scope == "all":
umos = all_umos

if not umos:
return Response().error("缺少必要参数: umos").__dict__
return Response().error("缺少必要参数: umos 或有效的 scope").__dict__

if not isinstance(umos, list):
return Response().error("参数 umos 必须是数组").__dict__

if rule_key and rule_key not in AVAILABLE_SESSION_RULE_KEYS:
return Response().error(f"不支持的规则键: {rule_key}").__dict__
Comment on lines +384 to +385
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider validating scope values explicitly to produce clearer errors for invalid input.

An unsupported scope (e.g. a typo) currently just leaves umos empty and triggers the generic "缺少必要参数: umos 或有效的 scope" error, which hides that scope itself was invalid. Please validate scope against the allowed set and return a specific error (e.g. "不支持的 scope 值") before expanding it into umos so invalid scopes aren’t treated the same as missing umos.

Suggested implementation:

                    elif scope == "all":
                        umos = all_umos

            # 显式校验 scope,避免无效 scope 静默失败
            allowed_scopes = {"all"}
            if scope and scope not in allowed_scopes:
                return Response().error("不支持的 scope 值").__dict__

            if not umos:
                return Response().error("缺少必要参数: umos 或有效的 scope").__dict__

To fully align this change with the rest of the function:

  1. Update allowed_scopes to include all currently支持的 scope 值(例如如果上文有 if scope == "online", if scope == "selected" 等分支,请将这些字符串也加入到 allowed_scopes 集合中),确保不会误报合法 scope 为无效。
  2. If there is a centralized definition of allowed scopes elsewhere in the codebase (e.g. a constant like AVAILABLE_SESSION_SCOPES), replace the hardcoded allowed_scopes set with that constant to keep things DRY and consistent.


# 批量删除
deleted_count = 0
success_count = 0
failed_umos = []
for umo in umos:
try:
await sp.clear_async("umo", umo)
deleted_count += 1
if rule_key:
await sp.session_remove(umo, rule_key)
else:
await sp.clear_async("umo", umo)
success_count += 1
except Exception as e:
logger.error(f"删除 umo {umo} 的规则失败: {e!s}")
failed_umos.append(umo)

message = f"已删除 {success_count} 条规则"
if rule_key:
message = f"已删除 {success_count} 条 {rule_key} 规则"

if failed_umos:
return (
Response()
.ok(
{
"message": f"已删除 {deleted_count} 条规则,{len(failed_umos)} 条删除失败",
"deleted_count": deleted_count,
"message": f"{message},{len(failed_umos)} 条删除失败",
"success_count": success_count,
"failed_umos": failed_umos,
}
)
Expand All @@ -369,8 +419,8 @@ async def batch_delete_session_rule(self):
Response()
.ok(
{
"message": f"已删除 {deleted_count} 条规则",
"deleted_count": deleted_count,
"message": message,
"success_count": success_count,
}
)
.__dict__
Expand Down
97 changes: 68 additions & 29 deletions dashboard/src/views/SessionManagementPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
</v-select>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-select v-model="batchChatProvider" :items="chatProviderOptions" item-title="label" item-value="value"
<v-select v-model="batchChatProvider" :items="batchChatProviderOptions" item-title="label" item-value="value"
:label="tm('batchOperations.chatProvider')" hide-details clearable variant="solo-filled" flat density="comfortable">
</v-select>
</v-col>
Expand Down Expand Up @@ -584,9 +584,9 @@ export default {

// Provider 配置
providerConfig: {
chat_completion: null,
speech_to_text: null,
text_to_speech: null,
chat_completion: '__astrbot_follow_config__',
speech_to_text: '__astrbot_follow_config__',
text_to_speech: '__astrbot_follow_config__',
},

// 插件配置
Expand Down Expand Up @@ -671,7 +671,7 @@ export default {

chatProviderOptions() {
return [
{ label: this.tm('provider.followConfig'), value: null },
{ label: this.tm('provider.followConfig'), value: '__astrbot_follow_config__' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Extract the follow-config sentinel string into a shared constant to avoid duplication and typo risk.

The '__astrbot_follow_config__' literal appears in several places (defaults, options, checks). Please define a single shared constant (e.g. const FOLLOW_CONFIG_SENTINEL = '__astrbot_follow_config__' or a data/computed property) and reference it everywhere to avoid typos and simplify future changes.

Suggested implementation:

const FOLLOW_CONFIG_SENTINEL = '__astrbot_follow_config__'

        chat_completion: FOLLOW_CONFIG_SENTINEL,
        speech_to_text: FOLLOW_CONFIG_SENTINEL,
        text_to_speech: FOLLOW_CONFIG_SENTINEL,
      },

      // 插件配置

    chatProviderOptions() {
      return [
        { label: this.tm('provider.followConfig'), value: FOLLOW_CONFIG_SENTINEL },
        ...this.availableChatProviders.map(p => ({
          label: `${p.name} (${p.model})`,
          value: p.id

    sttProviderOptions() {
      return [
        { label: this.tm('provider.followConfig'), value: FOLLOW_CONFIG_SENTINEL },
        ...this.availableSttProviders.map(p => ({
          label: `${p.name} (${p.model})`,
          value: p.id

  1. Place the const FOLLOW_CONFIG_SENTINEL = '__astrbot_follow_config__' declaration at the top of the <script> block (or just after existing imports) to match your project's conventions; the SEARCH/REPLACE block above assumes this snippet is near the top of the script, but you may need to move the constant accordingly.
  2. Search the remainder of SessionManagementPage.vue for any other occurrences of '__astrbot_follow_config__' (e.g. in watchers, computed properties, validation logic) and replace them with FOLLOW_CONFIG_SENTINEL as well.
  3. If this sentinel is used in other files (e.g. shared stores or components), consider moving FOLLOW_CONFIG_SENTINEL into a shared constants module (e.g. src/constants/providers.ts) and importing it here and elsewhere to fully centralize the value.

...this.availableChatProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
Expand All @@ -681,7 +681,7 @@ export default {

sttProviderOptions() {
return [
{ label: this.tm('provider.followConfig'), value: null },
{ label: this.tm('provider.followConfig'), value: '__astrbot_follow_config__' },
...this.availableSttProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
Expand All @@ -691,7 +691,27 @@ export default {

ttsProviderOptions() {
return [
{ label: this.tm('provider.followConfig'), value: null },
{ label: this.tm('provider.followConfig'), value: '__astrbot_follow_config__' },
...this.availableTtsProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
}))
]
},

batchChatProviderOptions() {
return [
{ label: this.tm('provider.followConfig'), value: '__astrbot_follow_config__' },
...this.availableChatProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
}))
]
},

batchTtsProviderOptions() {
return [
{ label: this.tm('provider.followConfig'), value: '__astrbot_follow_config__' },
...this.availableTtsProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
Expand Down Expand Up @@ -914,9 +934,9 @@ export default {

// 初始化 Provider 配置
this.providerConfig = {
chat_completion: this.editingRules['provider_perf_chat_completion'] || null,
speech_to_text: this.editingRules['provider_perf_speech_to_text'] || null,
text_to_speech: this.editingRules['provider_perf_text_to_speech'] || null,
chat_completion: this.editingRules['provider_perf_chat_completion'] || '__astrbot_follow_config__',
speech_to_text: this.editingRules['provider_perf_speech_to_text'] || '__astrbot_follow_config__',
text_to_speech: this.editingRules['provider_perf_text_to_speech'] || '__astrbot_follow_config__',
}

// 初始化插件配置
Expand Down Expand Up @@ -997,7 +1017,7 @@ export default {

for (const type of providerTypes) {
const value = this.providerConfig[type]
if (value) {
if (value && value !== '__astrbot_follow_config__') {
// 有值时更新
updateTasks.push(
axios.post('/api/session/update-rule', {
Expand All @@ -1007,7 +1027,7 @@ export default {
})
)
} else if (this.editingRules[`provider_perf_${type}`]) {
// 选择了"跟随配置文件"(null)且之前有配置,则删除
// 选择了"跟随配置文件" (__astrbot_follow_config__) 且之前有配置,则删除
deleteTasks.push(
axios.post('/api/session/delete-rule', {
umo: this.selectedUmo.umo,
Expand Down Expand Up @@ -1035,9 +1055,10 @@ export default {
this.rulesList.push(item)
}
for (const type of providerTypes) {
if (this.providerConfig[type]) {
item.rules[`provider_perf_${type}`] = this.providerConfig[type]
this.editingRules[`provider_perf_${type}`] = this.providerConfig[type]
const val = this.providerConfig[type]
if (val && val !== '__astrbot_follow_config__') {
item.rules[`provider_perf_${type}`] = val
this.editingRules[`provider_perf_${type}`] = val
} else {
// 删除本地数据
delete item.rules[`provider_perf_${type}`]
Expand Down Expand Up @@ -1354,23 +1375,41 @@ export default {
}

if (this.batchChatProvider !== null) {
tasks.push(axios.post('/api/session/batch-update-provider', {
scope,
umos,
group_id: groupId,
provider_type: 'chat_completion',
provider_id: this.batchChatProvider || null
}))
if (this.batchChatProvider === '__astrbot_follow_config__') {
tasks.push(axios.post('/api/session/batch-delete-rule', {
scope,
umos,
group_id: groupId,
rule_key: 'provider_perf_chat_completion'
}))
} else {
tasks.push(axios.post('/api/session/batch-update-provider', {
scope,
umos,
group_id: groupId,
provider_type: 'chat_completion',
provider_id: this.batchChatProvider
}))
}
}

if (this.batchTtsProvider !== null) {
tasks.push(axios.post('/api/session/batch-update-provider', {
scope,
umos,
group_id: groupId,
provider_type: 'text_to_speech',
provider_id: this.batchTtsProvider || null
}))
if (this.batchTtsProvider === '__astrbot_follow_config__') {
tasks.push(axios.post('/api/session/batch-delete-rule', {
scope,
umos,
group_id: groupId,
rule_key: 'provider_perf_text_to_speech'
}))
} else {
tasks.push(axios.post('/api/session/batch-update-provider', {
scope,
umos,
group_id: groupId,
provider_type: 'text_to_speech',
provider_id: this.batchTtsProvider
}))
}
}

if (tasks.length === 0) {
Expand Down
Loading