优化: run_command 实时流式 + 审批浮窗修复 + 文档探索闭环(fetch_url+obscura引导+model_fetch兼容+prompt策略+厂商预设)

This commit is contained in:
lxy
2026-08-01 17:58:47 +08:00
parent 0e0c6862ba
commit d664bdc309
25 changed files with 1693 additions and 44 deletions
+37
View File
@@ -68,6 +68,16 @@
<label class="form-label">{{ $t('settings.labelName') }}</label>
<input v-model="providerForm.name" class="setting-input" :placeholder="$t('settings.phProviderName')" />
</div>
<!-- D:厂商预设下拉 选中自动填充 providerType/baseUrl/defaultModel(用户可改覆盖)
新建态独立"快捷选择";编辑态回填后切预设仍可覆盖(等价快捷填充),不阻塞手改
"自定义" 不预填任何字段 -->
<div class="form-field">
<label class="form-label">{{ $t('settings.labelPreset') }}</label>
<select v-model="providerForm.presetId" class="setting-select" @change="onPresetChange">
<option value="__custom">{{ $t('settings.presetCustom') }}</option>
<option v-for="p in PROVIDER_PRESETS" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<div class="form-field">
<label class="form-label">{{ $t('settings.labelProviderType') }}</label>
<select v-model="providerForm.providerType" class="setting-select">
@@ -136,6 +146,7 @@ import { reactive, ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { aiApi } from '@/api'
import type { AiProviderConfig, ModelConfig } from '@/api/types'
import { PROVIDER_PRESETS, CUSTOM_PRESET_ID, findPreset } from './providerPresets'
// ============================================================
// AI 提供商域 — 真实后端 CRUD + F-04c 负载均衡池(enabled / weight)
@@ -174,6 +185,10 @@ const providerForm = reactive({
baseUrl: '',
apiKey: '',
defaultModel: '',
// 项 D:厂商预设下拉当前值(__custom=自定义;命中预设 id 时仅作展示与回填源头,
// 实际字段值由 onPresetChange 写入;用户手改任一字段后 presetId 不自动复位,
// 避免编辑态抖动,提交时也不读 presetId —— 以表单实际字段值为准)。
presetId: CUSTOM_PRESET_ID,
saving: false,
// F-01 阶段6:模型拉取 + 列表
fetching: false,
@@ -205,6 +220,9 @@ function openProviderForm(p?: AiProviderConfig) {
providerForm.apiKey = '' // 编辑不回填 api_key(list 返回 mask,留空=不改,FR-S1)
providerForm.defaultModel = p.default_model
providerForm.providerType = p.provider_type || 'openai_compat'
// 项 D:编辑态不反推预设(避免用户已手改 baseUrl/model 却被下拉误标成某预设误导);
// 统一标"自定义",下拉仅作新建快捷填充入口。
providerForm.presetId = CUSTOM_PRESET_ID
// F-01 阶段6:回填已保存的 model_configs(深拷贝避免双向绑定污染列表源)
providerForm.models = (p.model_configs ?? []).map(m => ({ ...m, modalities: [...m.modalities], capabilities: [...m.capabilities] }))
} else {
@@ -214,6 +232,7 @@ function openProviderForm(p?: AiProviderConfig) {
providerForm.baseUrl = ''
providerForm.apiKey = ''
providerForm.defaultModel = ''
providerForm.presetId = CUSTOM_PRESET_ID
providerForm.models = []
}
providerForm.fetching = false
@@ -221,6 +240,24 @@ function openProviderForm(p?: AiProviderConfig) {
providerForm.visible = true
}
/**
* 项 D:厂商预设下拉变更 — 命中预设则覆盖填充 providerType/baseUrl/defaultModel。
*
* 设计取舍:
* - 覆盖而非合并:选预设=用户明确意图用某厂商端点,全量填入最省心;用户随后仍可手改任一字段。
* - 不动 name/apiKey:name 用户应自取别名(避免重名"DeepSeek"×N);apiKey 是密钥预设无可填。
* - 选"自定义"不动作(仅清字段是粗暴的;用户可能误点,留原值更安全)。
* - 不复位 presetId:用户手改 baseUrl 等后下拉仍标原预设——这无副作用(提交不读 presetId),
* 强行复位反而要监听每个字段 input,过度工程。
*/
function onPresetChange() {
const preset = findPreset(providerForm.presetId)
if (!preset) return // __custom 或未知 id:不动作
providerForm.providerType = preset.providerType
providerForm.baseUrl = preset.baseUrl
providerForm.defaultModel = preset.defaultModel
}
/**
* 测试连接按钮可用条件(P0-1):
* - baseUrl 必填(无论新建/编辑,表单值都要有)
@@ -0,0 +1,86 @@
/**
* 厂商预设数据(项 D:文档探索失败时兜底 + 配置体验)。
*
* 选中预设后表单自动填入 providerType / baseUrl / defaultModel;用户仍可手改覆盖。
* "自定义" 不预填(纯手填)。endpoints 均来自代码库实测核验,非猜测:
*
* - OpenAI openai_compat https://api.openai.com (openai_compat.rs:45 / presets/models.json)
* - GLM/智谱 openai_compat https://open.bigmodel.cn/api/paas/v4 (model_fetch_helpers.rs:178,实测端点)
* - DeepSeek openai_compat https://api.deepseek.com (openai_compat.rs:45 / model_fetch_helpers.rs:187)
* - SenseNova openai_compat https://api.sensenova.cn/compatible-mode/v1 (scripts/test_sensenova.py 实测,非 token 端点)
* - Anthropic anthropic https://api.anthropic.com (anthropic_compat.rs:51)
*
* chat_url/messages_url 智能拼接(见 openai_compat.rs chat_url / anthropic_compat.rs messages_url):
* - 仅域名(OpenAI/DeepSeek)→ 后端补 /v1/chat/completions
* - 已含版本段 GLM /v4、SenseNova /compatible-mode/v1 → 后端补 /chat/completions
* 故 baseUrl 既可填带 /v1 也可不填,均能工作;此处采用代码库注释示例形态(更接近用户文档预期)。
*/
/** providerType 字面量(对齐后端 ProviderType serde,与 ProviderPanel.vue 下拉 option value 一致) */
export type PresetProviderType = 'openai_compat' | 'anthropic'
export interface ProviderPreset {
/** 预设唯一标识(下拉 option value;__custom 为内置"自定义"项) */
id: string
/** 展示名(中英 i18n key 后缀,见 labelPreset.{id}) */
name: string
providerType: PresetProviderType
baseUrl: string
defaultModel: string
}
/**
* 预设列表(顺序即下拉展示顺序;高频国产厂商前置)。
*
* baseUrl 不带末尾斜杠(对齐后端 chat_url trim_end_matches('/') 兜底,但保持干净)。
* defaultModel 选各厂商最常用、最稳定的 chat 模型(用户可改)。
*/
export const PROVIDER_PRESETS: readonly ProviderPreset[] = [
{
id: 'glm',
name: 'GLM / 智谱',
providerType: 'openai_compat',
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
defaultModel: 'glm-4.6',
},
{
id: 'deepseek',
name: 'DeepSeek',
providerType: 'openai_compat',
baseUrl: 'https://api.deepseek.com',
defaultModel: 'deepseek-chat',
},
{
id: 'sensenova',
name: 'SenseNova / 商汤',
providerType: 'openai_compat',
baseUrl: 'https://api.sensenova.cn/compatible-mode/v1',
defaultModel: 'SenseNova-Turbo',
},
{
id: 'openai',
name: 'OpenAI',
providerType: 'openai_compat',
baseUrl: 'https://api.openai.com',
defaultModel: 'gpt-4o',
},
{
id: 'anthropic',
name: 'Anthropic / Claude',
providerType: 'anthropic',
baseUrl: 'https://api.anthropic.com',
defaultModel: 'claude-sonnet-4',
},
] as const
/** "自定义" 项标识(下拉内置项,选中不预填任何字段) */
export const CUSTOM_PRESET_ID = '__custom' as const
/**
* 按 id 查预设;找不到(含 CUSTOM_PRESET_ID)返回 null。
* 调用方:null → 不预填(自定义/编辑态不覆盖用户已填值)。
*/
export function findPreset(id: string): ProviderPreset | null {
if (id === CUSTOM_PRESET_ID) return null
return PROVIDER_PRESETS.find(p => p.id === id) ?? null
}