新增: F-01阶段6 AiChat模型下拉+override穿透
This commit is contained in:
@@ -119,6 +119,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
max_iterations: usize,
|
||||
max_retries: usize,
|
||||
start_iteration: usize,
|
||||
model_override: Option<String>,
|
||||
) {
|
||||
// B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底)
|
||||
let mut guard = GeneratingGuard::new(session_arc.clone());
|
||||
@@ -174,6 +175,17 @@ pub(crate) async fn run_agentic_loop(
|
||||
};
|
||||
let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs)
|
||||
.unwrap_or_else(|| provider_config.default_model.clone());
|
||||
// F-01 阶段6: 用户指定模型 override 穿透(仅主对话生效,标题/扫描/灵感仍走路由)。
|
||||
// 兜底原则:override 非空且在该 provider model_configs 池中 → 用 override;否则用 resolved_model。
|
||||
// 绝不让 override 导致无模型(空/不在池 → 落回路由结果,行为不变)。
|
||||
let resolved_model = match model_override.as_deref() {
|
||||
Some(id) if !id.is_empty()
|
||||
&& provider_config.model_configs.iter().any(|m| m.model_id == id) =>
|
||||
{
|
||||
id.to_string()
|
||||
}
|
||||
_ => resolved_model,
|
||||
};
|
||||
let tool_defs = tools_arc.tool_definitions();
|
||||
// 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁
|
||||
// notify 同取一份 Arc 引用(B-260615-14):stream_llm select! 监听 notified() 即时唤醒
|
||||
@@ -684,6 +696,11 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s
|
||||
let max_iterations = state.agent_max_iterations.load(Ordering::SeqCst);
|
||||
// F-260616-07: 流式失败重试次数快照
|
||||
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
||||
// F-01 阶段6: 续跑沿用同一主对话的 model_override(审批续跑/达 max 续跑保持一致)。
|
||||
let model_override = {
|
||||
let session = state.ai_session.lock().await;
|
||||
session.model_override.clone()
|
||||
};
|
||||
|
||||
// 恢复循环前通知前端新建 assistant 消息:审批(通过/拒绝)后新一轮文本
|
||||
// 不应追加到发起工具调用的旧消息,用 AiAgentRound 隔开
|
||||
@@ -693,6 +710,6 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration).await;
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ pub async fn ai_regenerate(
|
||||
state: State<'_, AppState>,
|
||||
conversation_id: String,
|
||||
language: Option<String>,
|
||||
model_override: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||
|
||||
@@ -55,6 +56,8 @@ pub async fn ai_regenerate(
|
||||
session.agent_language = language.clone();
|
||||
// F-260616-11: 重生成 = 新生命周期起点,iteration 从头计数。
|
||||
session.iteration_used = 0;
|
||||
// F-01 阶段6: 记录用户指定模型 override(主对话专用,兜底见 run_agentic_loop)。
|
||||
session.model_override = model_override.clone();
|
||||
let popped = session.messages.pop_last_assistant_round();
|
||||
if !popped {
|
||||
// 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
|
||||
@@ -109,7 +112,7 @@ pub async fn ai_regenerate(
|
||||
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await;
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0, model_override).await;
|
||||
});
|
||||
|
||||
Ok("ok".to_string())
|
||||
@@ -137,6 +140,7 @@ pub async fn ai_chat_send(
|
||||
message: String,
|
||||
language: Option<String>,
|
||||
skill: Option<String>,
|
||||
model_override: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
// 获取活跃提供商(只读,失败可直接返回,不影响生成标志)
|
||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||
@@ -152,6 +156,9 @@ pub async fn ai_chat_send(
|
||||
session.agent_language = language.clone();
|
||||
// F-260616-11: 新对话生命周期 iteration 从头计数(累计计数器复位)。
|
||||
session.iteration_used = 0;
|
||||
// F-01 阶段6: 记录用户指定模型 override(主对话专用)。run_agentic_loop 兜底校验
|
||||
// (override 非空且在 provider model_configs 池中才用,否则落回路由结果)。
|
||||
session.model_override = model_override.clone();
|
||||
// F-260614-02 §5.2:纯技能调用(用户未填文本)时,落库 user content 改 /{skillname}
|
||||
// 作为技能调用标记(非伪造用户文本),让 title.rs summary_msgs 取到非空素材生成标题。
|
||||
// 非空 message 原样落库。
|
||||
@@ -219,7 +226,7 @@ pub async fn ai_chat_send(
|
||||
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await;
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0, model_override).await;
|
||||
});
|
||||
|
||||
Ok("ok".to_string())
|
||||
@@ -392,6 +399,7 @@ pub async fn ai_chat_edit(
|
||||
conversation_id: String,
|
||||
new_message: String,
|
||||
language: Option<String>,
|
||||
model_override: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||
|
||||
@@ -424,6 +432,8 @@ pub async fn ai_chat_edit(
|
||||
session.agent_language = language.clone();
|
||||
// F-260616-11: 编辑重生成 = 新生命周期起点,iteration 从头计数。
|
||||
session.iteration_used = 0;
|
||||
// F-01 阶段6: 记录用户指定模型 override(主对话专用)。
|
||||
session.model_override = model_override.clone();
|
||||
}
|
||||
|
||||
let _tool_defs = state.ai_tools.tool_definitions();
|
||||
@@ -480,6 +490,7 @@ pub async fn ai_chat_edit(
|
||||
max_iterations,
|
||||
max_retries,
|
||||
0,
|
||||
model_override,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
@@ -499,6 +510,7 @@ pub async fn ai_chat_force_send(
|
||||
message: String,
|
||||
language: Option<String>,
|
||||
skill: Option<String>,
|
||||
model_override: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
// 原子复位:清 generating + 清积压审批 + 置 stop_flag,与 ai_chat_stop 审批分支一致
|
||||
let old_conv_id = {
|
||||
@@ -521,7 +533,7 @@ pub async fn ai_chat_force_send(
|
||||
}
|
||||
// 复位完成后走 ai_chat_send 同款流程(内部会重新设 generating=true 并 spawn loop)
|
||||
// 直接内联而非递归调 ai_chat_send,避免 IPC 嵌套
|
||||
ai_chat_send(app, state, message, language, skill).await
|
||||
ai_chat_send(app, state, message, language, skill, model_override).await
|
||||
}
|
||||
|
||||
/// 停止当前 AI 生成
|
||||
@@ -946,6 +958,8 @@ pub async fn ai_conversation_create(
|
||||
// 启动即见 stop_flag=true 异常退出;agent_language 清空防新会话沿用旧会话语言设置。
|
||||
session.stop_flag.store(false, Ordering::SeqCst);
|
||||
session.agent_language = None;
|
||||
// F-01 阶段6: 清旧对话的 model_override,防新对话沿用上一次的「指定」模型。
|
||||
session.model_override = None;
|
||||
|
||||
Ok(serde_json::json!({ "id": id }))
|
||||
}
|
||||
@@ -1021,6 +1035,9 @@ pub async fn ai_conversation_switch(
|
||||
// 重启恢复链路:restore_pending_approvals(init 重建) → switchConversation(此处不清目标对话的)
|
||||
// → ai_pending_tool_calls 查询 → ai_approve 落库)
|
||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||
// F-01 阶段6: 切换对话清旧 override,防新对话沿用上一次的「指定」模型
|
||||
// (override 是单对话级 UI 选择,不跨对话持久化)。
|
||||
session.model_override = None;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"id": record.id,
|
||||
|
||||
@@ -6,13 +6,23 @@ import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProvid
|
||||
|
||||
export const aiApi = {
|
||||
/** 发送消息(非阻塞,通过 ai-chat-event 流式返回);skill 传技能名则注入其 SKILL.md */
|
||||
sendMessage(message: string, language?: string, skill?: string): Promise<string> {
|
||||
return invoke('ai_chat_send', { message, language: language || 'zh-CN', skill: skill || null })
|
||||
sendMessage(message: string, language?: string, skill?: string, modelOverride?: string | null): Promise<string> {
|
||||
return invoke('ai_chat_send', {
|
||||
message,
|
||||
language: language || 'zh-CN',
|
||||
skill: skill || null,
|
||||
modelOverride: modelOverride || null,
|
||||
})
|
||||
},
|
||||
|
||||
/** 强制发送(B-260616-02: 复位 generating 残留后走 send 同款流程) */
|
||||
forceSend(message: string, language?: string, skill?: string): Promise<string> {
|
||||
return invoke('ai_chat_force_send', { message, language: language || 'zh-CN', skill: skill || null })
|
||||
forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null): Promise<string> {
|
||||
return invoke('ai_chat_force_send', {
|
||||
message,
|
||||
language: language || 'zh-CN',
|
||||
skill: skill || null,
|
||||
modelOverride: modelOverride || null,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -20,8 +30,12 @@ export const aiApi = {
|
||||
* 后端:占用 generating → 弹出末尾 AI 回复(保留触发它的 user 消息)→ 重跑 agentic loop。
|
||||
* 前端消费:与 sendMessage 一样需本地 push 空气泡占位 + 置 streaming(见 useAiSend.regenerate)。
|
||||
*/
|
||||
regenerate(conversationId: string, language?: string): Promise<string> {
|
||||
return invoke('ai_regenerate', { conversationId, language: language || 'zh-CN' })
|
||||
regenerate(conversationId: string, language?: string, modelOverride?: string | null): Promise<string> {
|
||||
return invoke('ai_regenerate', {
|
||||
conversationId,
|
||||
language: language || 'zh-CN',
|
||||
modelOverride: modelOverride || null,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -30,8 +44,13 @@ export const aiApi = {
|
||||
* 前端消费:调 editMessage 前需移除被编辑消息之后的所有消息(视图截断)+ push 空气泡占位 + 置 streaming。
|
||||
* 中间 user 消息编辑被后端拒绝(只允许末条)。
|
||||
*/
|
||||
editMessage(conversationId: string, newMessage: string, language?: string): Promise<string> {
|
||||
return invoke('ai_chat_edit', { conversationId, newMessage, language: language || 'zh-CN' })
|
||||
editMessage(conversationId: string, newMessage: string, language?: string, modelOverride?: string | null): Promise<string> {
|
||||
return invoke('ai_chat_edit', {
|
||||
conversationId,
|
||||
newMessage,
|
||||
language: language || 'zh-CN',
|
||||
modelOverride: modelOverride || null,
|
||||
})
|
||||
},
|
||||
|
||||
/** 批准/拒绝工具调用 */
|
||||
|
||||
@@ -211,6 +211,42 @@
|
||||
⏳ {{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- F-01 阶段6: 模型选择器(自动/指定)。
|
||||
自动模式=路由器选(现状零变化);指定模式=用户从下拉选 model_id,override 穿透主对话。
|
||||
provider 无 model_configs(空池)时:下拉空 + 强制自动(toggle 禁用)。
|
||||
仅展示 active provider 的 enabled model_configs。 -->
|
||||
<div class="ai-model-picker" :title="modelPickerHint">
|
||||
<div class="ai-model-mode">
|
||||
<button
|
||||
type="button"
|
||||
class="ai-model-mode-btn"
|
||||
:class="{ 'ai-model-mode-btn--active': !isSpecifyMode }"
|
||||
:disabled="!enabledModels.length"
|
||||
@click="setAutoMode"
|
||||
>{{ $t('aiChat.autoMode') }}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ai-model-mode-btn"
|
||||
:class="{ 'ai-model-mode-btn--active': isSpecifyMode }"
|
||||
:disabled="!enabledModels.length"
|
||||
:title="enabledModels.length ? $t('aiChat.specifyModeHint') : $t('aiChat.noModels')"
|
||||
@click="setSpecifyMode"
|
||||
>{{ $t('aiChat.specifyMode') }}</button>
|
||||
</div>
|
||||
<select
|
||||
v-if="enabledModels.length"
|
||||
class="ai-model-select"
|
||||
:value="selectedModelId"
|
||||
:disabled="!isSpecifyMode"
|
||||
:title="isSpecifyMode ? '' : $t('aiChat.autoModeHint')"
|
||||
@change="onModelSelect($event)"
|
||||
>
|
||||
<option v-for="m in enabledModels" :key="m.model_id" :value="m.model_id">
|
||||
{{ modelOptionLabel(m) }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-else class="ai-model-empty">{{ $t('aiChat.noModels') }}</span>
|
||||
</div>
|
||||
<div class="ai-header-actions">
|
||||
<button class="ai-btn-icon" @click="confirmNewConversation" :title="$t('aiChat.newConversation')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||
@@ -978,8 +1014,11 @@ function detectMentionTrigger() {
|
||||
}
|
||||
|
||||
// UX-09:切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
|
||||
// F-01 阶段6: 同步清 modelOverride(后端 switchConversation/new 已清 session.model_override,
|
||||
// 前端 ref 不清则会与后端不一致——override 是单对话级 UI 选择,不跨对话持久化)。
|
||||
watch(() => store.state.activeConversationId, () => {
|
||||
if (editingMsgId.value) cancelEdit()
|
||||
store.modelOverride.value = null
|
||||
})
|
||||
|
||||
function selectSkill(s: SkillInfo) {
|
||||
@@ -1196,6 +1235,54 @@ const activeProviderName = computed(() => {
|
||||
return active?.name || store.state.providers[0]?.name || t('aiChat.notConfigured')
|
||||
})
|
||||
|
||||
// ── F-01 阶段6: 顶部模型选择器(自动/指定) ──
|
||||
// active provider 的 model_configs(用户在 Settings 拉取过的,仅 enabled 项)。
|
||||
// 空池(provider 未拉取/无 enabled 模型)时下拉为空 + 强制自动模式。
|
||||
const activeProviderRecord = computed(() =>
|
||||
store.state.providers.find(p => p.id === store.state.activeProvider)
|
||||
|| store.state.providers.find(p => p.is_default)
|
||||
|| store.state.providers[0]
|
||||
|| null,
|
||||
)
|
||||
const enabledModels = computed(() => {
|
||||
const p = activeProviderRecord.value
|
||||
if (!p?.model_configs) return []
|
||||
return p.model_configs.filter(m => m.enabled)
|
||||
})
|
||||
// 指定模式 = store.modelOverride 非 null(模块级 ref,发送链路读它透传后端)。
|
||||
const isSpecifyMode = computed(() => store.modelOverride.value !== null)
|
||||
const selectedModelId = computed(() => store.modelOverride.value || '')
|
||||
/** 当前选中模型的 model_id(指定模式);自动模式为空 */
|
||||
const modelPickerHint = computed(() => {
|
||||
if (!enabledModels.value.length) return t('aiChat.noModels')
|
||||
return isSpecifyMode.value
|
||||
? t('aiChat.specifyModeHint')
|
||||
: t('aiChat.autoModeHint')
|
||||
})
|
||||
/** 下拉选项 label:model_id + 4 维度简标(智力/价格),复用 settings i18n 标签字面 */
|
||||
function modelOptionLabel(m: import('../api/types').ModelConfig): string {
|
||||
const name = m.label || m.model_id
|
||||
const intel = t('settings.tagIntel.' + m.intelligence)
|
||||
const cost = t('settings.tagCost.' + m.cost_tier)
|
||||
return `${name} · ${intel} · ${cost}`
|
||||
}
|
||||
function setAutoMode() {
|
||||
store.modelOverride.value = null
|
||||
}
|
||||
function setSpecifyMode() {
|
||||
if (!enabledModels.value.length) return
|
||||
// 进入指定模式默认选第一个 enabled 模型(若已非空且在池中则保留)
|
||||
const cur = store.modelOverride.value
|
||||
const inPool = cur && enabledModels.value.some(m => m.model_id === cur)
|
||||
if (!inPool) {
|
||||
store.modelOverride.value = enabledModels.value[0]?.model_id || null
|
||||
}
|
||||
}
|
||||
function onModelSelect(e: Event) {
|
||||
const v = (e.target as HTMLSelectElement).value
|
||||
store.modelOverride.value = v || null
|
||||
}
|
||||
|
||||
// 当前视图是否正在生成(切走后台生成时光标不显示)
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.state.streaming && store.state.generatingConvId === store.state.activeConversationId
|
||||
@@ -2397,6 +2484,69 @@ body.ai-sidebar-resizing * {
|
||||
.ai-approval-badge:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ═══ F-01 阶段6: 模型选择器(自动/指定 toggle + 下拉) ═══ */
|
||||
/* 紧凑内联控件,header 中间区。空池时下拉隐藏 + toggle 禁用,强制自动。 */
|
||||
.ai-model-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
margin-right: 4px;
|
||||
padding-left: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.ai-model-mode {
|
||||
display: inline-flex;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-model-mode-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 10px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-model-mode-btn:hover:not(:disabled) {
|
||||
background: var(--df-bg-card);
|
||||
color: var(--df-text);
|
||||
}
|
||||
.ai-model-mode-btn--active {
|
||||
background: var(--df-accent-bg);
|
||||
color: var(--df-accent);
|
||||
}
|
||||
.ai-model-mode-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ai-model-select {
|
||||
min-width: 0;
|
||||
max-width: 180px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: var(--df-bg);
|
||||
color: var(--df-text);
|
||||
font-size: 10px;
|
||||
font-family: var(--df-font-mono);
|
||||
padding: 3px 6px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ai-model-select:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ai-model-empty {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-header-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { ref } from 'vue'
|
||||
import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
@@ -35,6 +36,22 @@ const QUEUE_LIMIT = 10
|
||||
/// 排队超时阈值(ms) — 超过后弹 confirm 让用户选择"强制发送"或继续等(B-260616-02)
|
||||
const QUEUE_TIMEOUT_MS = 30_000
|
||||
|
||||
/**
|
||||
* F-01 阶段6: 用户指定模型 override(主对话专用,模块级单例)。
|
||||
*
|
||||
* null=自动模式(路由器选);非空字符串=用户从下拉指定的 model_id。
|
||||
* 仅主对话(agentic)生效:doSend/sendMessage/regenerate/editMessage 透传给后端,
|
||||
* 标题/扫描/灵感等内部调用不读此字段(后端各自独立 IPC 不带 override)。
|
||||
*
|
||||
* 后端兜底(agentic.rs):override 非空且在该 provider model_configs 池中才用,
|
||||
* 否则落回路由结果——绝不让 override 导致无模型。
|
||||
*
|
||||
* 模块级(非组件级):AiChat.vue 顶部下拉读写此 ref,发送链路读此 ref 透传,
|
||||
* 单实例 AiChat 不需 props/inject 串联。切换对话时后端清 session.model_override,
|
||||
* 前端应同步清(modelOverride.value = null)保持 UI 与后端一致。
|
||||
*/
|
||||
const modelOverride = ref<string | null>(null)
|
||||
|
||||
// 审批计时器(startApprovalTimer/clearApprovalTimer/clearAllApprovalTimers)已下沉到 aiShared.ts
|
||||
// 打破 useAiEvents ↔ useAiSend 循环依赖。本模块 re-export 供组件使用。
|
||||
|
||||
@@ -80,10 +97,12 @@ async function doSend(text: string, skill?: string, force = false) {
|
||||
: raw
|
||||
try {
|
||||
// force=true 走 force_send IPC(复位残留 generating),否则走正常 send
|
||||
// F-01 阶段6: 透传 modelOverride(主对话专用,后端兜底校验池内才用)
|
||||
const override = modelOverride.value
|
||||
if (force) {
|
||||
await aiApi.forceSend(text.trim(), lang, skill)
|
||||
await aiApi.forceSend(text.trim(), lang, skill, override)
|
||||
} else {
|
||||
await aiApi.sendMessage(text.trim(), lang, skill)
|
||||
await aiApi.sendMessage(text.trim(), lang, skill, override)
|
||||
}
|
||||
} catch (e) {
|
||||
// IPC 失败(spawn 前/provider 配置错等):回滚 streaming 防光标卡死 + 移除 user 消息与空气泡占位;
|
||||
@@ -142,7 +161,7 @@ async function regenerate() {
|
||||
throw new Error(t('aiChat.regenerateUnavailable'))
|
||||
}
|
||||
try {
|
||||
await aiApi.regenerate(convId, lang)
|
||||
await aiApi.regenerate(convId, lang, modelOverride.value)
|
||||
} catch (e) {
|
||||
// IPC 失败:回滚 streaming + 移除占位气泡(旧回复后端已弹但前端先弹了,保持弹后态)
|
||||
state.streaming = false
|
||||
@@ -214,7 +233,7 @@ async function editMessage(newMessage: string) {
|
||||
throw new Error(t('aiChat.editNotLastUser'))
|
||||
}
|
||||
try {
|
||||
await aiApi.editMessage(convId, newMessage.trim(), lang)
|
||||
await aiApi.editMessage(convId, newMessage.trim(), lang, modelOverride.value)
|
||||
} catch (e) {
|
||||
// IPC 失败:回滚 streaming + 移除占位气泡(被截断的旧回复后端已标 truncated,前端已移除)
|
||||
state.streaming = false
|
||||
@@ -418,6 +437,8 @@ export function useAiSend() {
|
||||
stopChat,
|
||||
isQueueTimedOut,
|
||||
tryForceSend,
|
||||
// F-01 阶段6: 模型 override(主对话专用),顶部下拉读写
|
||||
modelOverride,
|
||||
// 审批计时器已下沉到 aiShared.ts,此处 re-export 供组件使用
|
||||
startApprovalTimer,
|
||||
clearApprovalTimer,
|
||||
|
||||
@@ -41,6 +41,14 @@ export default {
|
||||
providerNotConfigured: 'No AI provider configured. Please add one in settings.',
|
||||
notConfigured: 'Not configured',
|
||||
|
||||
// ── F-01 阶段6: model picker (auto/specify toggle + dropdown) ──
|
||||
autoMode: 'Auto',
|
||||
specifyMode: 'Specify',
|
||||
autoModeHint: 'Auto mode: router picks the best model per scenario',
|
||||
specifyModeHint: 'Specify mode: use the selected model, overriding the router (main chat only)',
|
||||
selectModel: 'Select model',
|
||||
noModels: 'No models available. Please fetch in settings.',
|
||||
|
||||
// ── Empty states (UX-2025-20: example prompt cards + no-provider guidance) ──
|
||||
emptyNoProvider: 'No AI provider configured',
|
||||
emptyNoProviderHint: 'Please add a provider in settings to start chatting',
|
||||
|
||||
@@ -41,6 +41,14 @@ export default {
|
||||
providerNotConfigured: '未配置 AI 提供商,请在设置中添加',
|
||||
notConfigured: '未配置',
|
||||
|
||||
// ── F-01 阶段6: 模型选择器(自动/指定 toggle + 下拉) ──
|
||||
autoMode: '自动',
|
||||
specifyMode: '指定',
|
||||
autoModeHint: '自动模式:路由器按场景自动选最优模型',
|
||||
specifyModeHint: '指定模式:使用下拉所选模型覆盖路由(仅主对话)',
|
||||
selectModel: '选择模型',
|
||||
noModels: '无可用模型,请在设置中拉取',
|
||||
|
||||
// ── 空状态(UX-2025-20:示例问题卡片 + 无 provider 引导) ──
|
||||
emptyNoProvider: '未配置 AI 提供商',
|
||||
emptyNoProviderHint: '请在设置中添加 Provider 后开始对话',
|
||||
|
||||
Reference in New Issue
Block a user