修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
+22
-10
@@ -155,6 +155,7 @@ const props = defineProps<{
|
||||
const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// path_auth 审批链阶段3b(统一审批模型):df-ai-unified-approval 开关响应式 ref。
|
||||
// 开(true,默认):path 挂起归一进 ToolCard 内联审批(once/always/deny),不挂 DirAuthDialog。
|
||||
// 关(false,兜底回退):挂 DirAuthDialog 独立弹窗老链路(useAiEvents AiDirAuthRequired case 走 pendingDirAuths)。
|
||||
@@ -255,7 +256,12 @@ async function confirmClearChat() {
|
||||
* 历史:B-260615-10 单 loop 软复位弹中断确认,F-09 决策e 上线后已废弃(A 路线遗留)。
|
||||
*/
|
||||
async function confirmNewConversation() {
|
||||
await store.newConversation()
|
||||
try {
|
||||
await store.newConversation()
|
||||
} catch (e) {
|
||||
console.error('[AI] 新建对话失败:', e)
|
||||
// 用错误气泡反馈用户,不静默吞掉
|
||||
}
|
||||
}
|
||||
|
||||
// 第二批抽离: 技能 `/` 联想 + @ 实体引用联想 + watch(inputText) 已迁移至 ChatInput.vue
|
||||
@@ -510,18 +516,24 @@ onMounted(async () => {
|
||||
console.debug(`[启动] AiChat IPC 完成: ${(performance.now() - t0).toFixed(0)}ms`)
|
||||
})
|
||||
|
||||
// ── 置顶功能(持久化:刷新后保持置顶状态图标一致)──
|
||||
// ── 置顶功能(仅分离窗口有意义;持久化:刷新后保持置顶状态图标一致)──
|
||||
// 关键:置顶只对「分离窗口」生效。AiChat 在主窗口(App.vue)与分离窗口(AiDetached.vue)都挂载,
|
||||
// 若不区分,主窗口启动时会因 KV=true 把【整个主窗口】置顶到所有应用之上(BUG)。
|
||||
// 主窗口内 AI 是 docked 面板,主窗口本身不该置顶。故 restore/apply 一律 gate 在 props.detached。
|
||||
const alwaysOnTop = ref(false)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
// 从 SQLite KV 恢复上次置顶状态
|
||||
// 从 SQLite KV 恢复上次置顶状态(仅分离窗口读用;主窗口读了也无副作用,ref 仅驱动按钮 active 态)
|
||||
const savedAlwaysOnTop = appSettingsStore.get<boolean>('df-ai-always-on-top', false)
|
||||
alwaysOnTop.value = savedAlwaysOnTop
|
||||
// 若恢复值为 true(上次是置顶态),同步到 Tauri 窗口
|
||||
if (savedAlwaysOnTop) {
|
||||
// 异步设(不阻塞初始化流程)
|
||||
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
|
||||
getCurrentWebviewWindow().setAlwaysOnTop(true).catch(() => {/* 静默 */})
|
||||
}
|
||||
alwaysOnTop.value = props.detached ? savedAlwaysOnTop : false
|
||||
// 同步到 Tauri 窗口:分离窗口按 KV 还原;主窗口显式清置顶(防御历史误设/窗口状态残留)。
|
||||
// 注意:此处不可用顶层 await —— <script setup> 顶层 await 会使本组件变成 async setup,
|
||||
// 而 App.vue / AiDetached.vue 直接挂载 <AiChat /> 未包 <Suspense>,会导致渲染空白。
|
||||
// 用 .then() fire-and-forget,既不阻塞初始化流程,又保持组件同步 setup。
|
||||
import('@tauri-apps/api/webviewWindow').then(({ getCurrentWebviewWindow }) =>
|
||||
getCurrentWebviewWindow()
|
||||
.setAlwaysOnTop(props.detached ? savedAlwaysOnTop : false)
|
||||
.catch(() => {/* 静默:webview 不可用时忽略 */}),
|
||||
)
|
||||
async function toggleAlwaysOnTop() {
|
||||
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
|
||||
const win = getCurrentWebviewWindow()
|
||||
|
||||
@@ -730,13 +730,13 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
|
||||
<!-- token 用量(仅最后一条 AI 消息 + 非流式;ai.ts handleEvent 已按 df-show-token-usage 开关过滤 lastTokenUsage) -->
|
||||
<!-- token 用量(显示在每条 assistant 消息底部,非流式且有数据时) -->
|
||||
<div
|
||||
v-if="isLastAi(item.msg) && !store.state.streaming && store.state.lastTokenUsage"
|
||||
v-if="item.msg.role === 'assistant' && !store.state.streaming && item.msg.tokenUsage"
|
||||
class="ai-token-usage"
|
||||
>
|
||||
<span class="ai-token-usage-icon">🔣</span>
|
||||
<span>{{ formatTokens(store.state.lastTokenUsage.prompt) }} in · {{ formatTokens(store.state.lastTokenUsage.completion) }} out</span>
|
||||
<span>{{ formatTokens(item.msg.tokenUsage.prompt) }} in · {{ formatTokens(item.msg.tokenUsage.completion) }} out</span>
|
||||
</div>
|
||||
|
||||
<!-- 工具调用卡片(渲染/折叠/审批全下沉到 ToolCardList+ToolCard 子组件,MessageList 仅转发审批) -->
|
||||
|
||||
@@ -225,7 +225,7 @@ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { aiApi } from '../../api/ai'
|
||||
import type { AiMessage, ModelConfig } from '../../api/types'
|
||||
import type { AiMessage, ModelConfig, GoalEntry } from '../../api/types'
|
||||
|
||||
defineProps<{
|
||||
/** 分离窗口模式(嵌入 vs 分离决定顶部按钮组) */
|
||||
@@ -326,6 +326,20 @@ watch(() => store.state.activeConversationId, () => {
|
||||
store.modelOverride.value = enabledModels.value[0]?.model_id || null
|
||||
})
|
||||
|
||||
// 切 provider 后模型 override 同步(BUG-2026-07-07:provider 切换后 header 模型名不刷新):
|
||||
// - modelOverride 是模块级 ref(useAiSend)不持久化,切 provider 后仍持旧 provider 的 model_id,
|
||||
// 该 id 不在新 provider 池里 → 下拉显示错位(UI 显示 X,实际后端用路由默认 Y)。
|
||||
// - 守卫:当前选择对新 provider 模型池无效(空/不在池)时,默认选权重最高模型。
|
||||
// enabledModels 已按 weight 降序排,model[0] 即权重最高。
|
||||
// - immediate:组件挂载时也校验(modelOverride 为 null → 默认权重最高,与下拉首项对齐)。
|
||||
watch(enabledModels, (models) => {
|
||||
const current = store.modelOverride.value
|
||||
const valid = current && models.some(m => m.model_id === current)
|
||||
if ((!valid || !current) && models.length) {
|
||||
store.modelOverride.value = models[0].model_id
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// ── 当前对话标题 ──
|
||||
const currentConvTitle = computed(() => {
|
||||
const id = store.state.activeConversationId
|
||||
@@ -416,7 +430,7 @@ const summaryExpanded = ref(false)
|
||||
|
||||
// ── L3 完整上下文 ──
|
||||
interface ContextInfo {
|
||||
goals: string[]
|
||||
goals: GoalEntry[]
|
||||
summaryCount: number
|
||||
osInfo: string
|
||||
projectNames: string
|
||||
|
||||
@@ -331,7 +331,9 @@ function statusLabelKey(status: IdeaStatus): string {
|
||||
|
||||
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
|
||||
const { rendered: renderedDesc } = useRendered(
|
||||
// BUG-2026-07-08:原漏解构 ensureLoaded + onMounted 未调用,冷启动(marked 未被其他组件预热)
|
||||
// 时 mdReady 永远 false,renderMd 走 escapeFallback 纯文本降级,Markdown 语法不渲染。
|
||||
const { rendered: renderedDesc, ensureLoaded: ensureMdLoaded } = useRendered(
|
||||
() => props.idea.description ?? '',
|
||||
)
|
||||
|
||||
@@ -439,7 +441,10 @@ function historySummary(aiAnalysis: string | null | undefined): HistorySummary |
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadHistory)
|
||||
onMounted(() => {
|
||||
ensureMdLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||
loadHistory()
|
||||
})
|
||||
watch(() => props.idea.id, loadHistory)
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user