Skip to content

[Bug] 新建Provider未保存时删除导致400和界面残留 #9262

Description

@Fronut

What happened / 发生了什么

在提供商源(Provider Source)管理页面,新建一个源后不保存、直接删除,会同时出现两个异常:

  1. Toast 报错Request failed with code 400
  2. 右侧面板残留:左侧列表中该源已消失,但右侧仍渲染着该源的配置表单

Reproduce / 如何复现?

  1. 进入 配置 → 提供商 页面,选中一个提供商类型(如 openai_chat_completion
  2. 点击左侧面板的「添加提供商源」,新建一个源
  3. 不做任何修改,不点保存
  4. 在左侧列表中,对刚新建的源点击「删除」并确认
  5. 观察:
    • 顶部弹出红色 Toast:Request failed with code 400
    • 左侧列表中该源消失
    • 右侧仍然显示该源的配置面板

AstrBot version, deployment method (e.g., Windows Docker Desktop deployment), provider used, and messaging platform used. / AstrBot 版本、部署方式(如 Windows Docker Desktop 部署)、使用的提供商、使用的消息平台适配器

4.26.5 Windows Desktop

OS

Windows

Logs / 报错日志

Image 新建一个Provider Image 然后立刻点击删除 Image 报错400,且没有得到正确清理

场景验证

场景 结果
新建 → 输入 API key → 保存 → 删除 ✅ 正常,右侧面板消失
新建 → 不动 → 直接删除 ❌ 400 错误 + 右侧残留
上述 Bug 发生后 → 在残留面板输入 API key → 点保存 源在后端被创建,左侧面板重新出现,无报错

根因分析

useProviderSources.tsaddProviderSource()(第 424 行)

新建源时仅操作本地状态,不调 API 持久化

function addProviderSource(templateKey: string) {
    const newSource = { ... }
    providerSources.value.push(newSource)           // 只加到本地数组
    selectedProviderSource.value = newSource         // 选中新源 → 右侧面板渲染
    isSourceModified.value = true                   // 标记已修改
    // ⚠️ 未调用 API,后端不知道有这个源
}

useProviderSources.tsdeleteProviderSource()(第 451 行)

删除时假设源一定在后端存在,API 调用和本地清理耦合在同一个 try 块:

async function deleteProviderSource(source) {
    try {
        await providerApi.deleteSource(source.id)         // ① API 调用 — 400 抛出
        // ⬇ 以下全部跳过 ⬇
        providers.value = providers.value.filter(...)     // ② 未执行
        providerSources.value = providerSources.value.filter(...) // ③ 未执行
        if (selectedProviderSource.value?.id === source.id) {
            selectedProviderSource.value = null           // ④ 未执行!核心问题
        }
        showMessage('删除成功')
    } catch (error) {
        showMessage(error.message, 'error')               // ⑤ 仅显示错误
    } finally {
        await loadConfig()                                // ⑥ 重新拉取数据
    }
}

执行顺序:

deleteProviderSource() 被调用
  → providerApi.deleteSource(source.id) → 后端查不到该源 → ValueError → HTTP 400
  → 进入 catch:弹出「Request failed with code 400」
  → 进入 finally:loadConfig() → 从后端拉 provider_sources → 该源不在其中
  → providerSources.value 被覆盖为后端数据 → 左侧列表正确(无该源)
  → 但 selectedProviderSource 仍指向已被"删除"的幽灵对象 → v-if="selectedProviderSource" 为 true
  → 右侧面板残留显示

后端 config_service.py:1357-1361 也确认了:删除不存在的源会抛 ValueError

async def delete_provider_source(self, source_id: str) -> None:
    sources = self.config.get("provider_sources", [])
    next_sources = [source for source in sources if source.get("id") != source_id]
    if len(next_sources) == len(sources):
        raise ValueError(f"Provider source {source_id} not found")  # → 400

触发条件总结

条件 说明
时机 addProviderSource() 之后、saveProviderSource() 之前
判断标志 isSourceModified.value === true — 源仅在本地,未持久化

影响范围

  • 用户体验:错误提示让用户以为操作失败,但实际上源确实从列表中消失了,右侧残留的配置面板造成混淆
  • 数据一致性selectedProviderSource 成为悬空引用,后续操作(如保存)可能意外复活该源
  • 频率:新建源后立即后悔想删除,是自然的操作路径,不是边缘场景

修复建议

方案 A(最小改动):在 catch 中补上本地清理

catch (error: any) {
    showMessage(error.message || '删除失败', 'error')
    // 即使 API 失败,也清理本地状态
    if (selectedProviderSource.value?.id === source.id) {
        selectedProviderSource.value = null
        selectedProviderSourceOriginalId.value = null
        editableProviderSource.value = null
    }
}

方案 B(推荐):判断源是否已持久化,未持久化的直接本地清理

async function deleteProviderSource(source: any) {
    const confirmed = await askForConfirmation(...)
    if (!confirmed) return

    // 如果源从未保存到后端,直接清理本地状态
    if (isSourceModified.value && selectedProviderSource.value?.id === source.id) {
        providerSources.value = providerSources.value.filter(s => s.id !== source.id)
        selectedProviderSource.value = null
        selectedProviderSourceOriginalId.value = null
        editableProviderSource.value = null
        isSourceModified.value = false
        showMessage('已取消新建')
        return
    }

    try {
        await providerApi.deleteSource(source.id)
        // ... 现有清理逻辑
    } catch (error) { ... }
}

推荐方案 B,因为它从根源避免了不必要的 API 调用,且语义更清晰:新建未保存 = 取消,删除已保存 = 删后端。

Are you willing to submit a PR? / 你愿意提交 PR 吗?

  • Yes!

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:webuiThe bug / feature is about webui(dashboard) of astrbot.bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions