修复: 模型停用无效根因 + 上下文归位/压缩守卫
- 模型停用无效根因: get_active_provider 两条路径均不校验 enabled(显式活跃直返/默认只挑 is_default) → 收敛为启用集合解析,无启用 provider 明确报错 - 压缩/标题兜底模型不校验 enabled → 新增 resolve_fallback_model 纯函数(title 复用同一实现,单一源无双轨) - 上下文多夹层归位修复: gather_tool_responses 聚拢该头全部响应(此前第二个夹层之后的响应仍被判未闭合剥除) - 压缩后台任务加内容版本哨兵(取首条消息 id,否掉 len:压缩期间 loop 正常 append 会致守卫永不生效) + panic 兜底复位标志 - 902 超时类失败与其它 Fatal 区分停靠文案(点继续可续跑) - 子代理事件按 conv 改挂(route_scope) + 运行期事件推父会话视图 - 飞书 13a 真根因: run_stream_refresher 死代码(提前 drop)+静默钟被心跳重置 → 帧通道+仅内容事件重置 - 项目详情文件查看: F22 目录不存在误显非 Git 仓库 / F24 模块为仓库子目录时 git 徽标全失效(scope_git_path 按 show-prefix 剥前缀) - 更新徽章通道错配(Tauri 事件 vs window CustomEvent 同名不同通道,徽章此前永不触发) - ProjectCard 卡片根 keydown 顶替内嵌按钮(补 e.target 守卫) - i18n 坏键 ai.aiTool.rejectedHint → aiTool.rejectedHint(双缺会渲染裸 key) - miniapp 三处: 会话列表刷新漏网/看门狗节流致新一轮无兜底/newConversation 漏清看门狗
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
class="update-badge-btn"
|
||||
:class="{ 'is-active': expanded }"
|
||||
:title="$t('settings.updateBadgeTitle')"
|
||||
:aria-expanded="expanded"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<span class="update-badge-dot"></span>
|
||||
@@ -67,6 +68,15 @@ const installError = ref('')
|
||||
let unlistenAvailable: UnlistenFn | null = null
|
||||
let unlistenProgress: UnlistenFn | null = null
|
||||
|
||||
/** window CustomEvent 形态的更新通知:生产者 useUpdateCheck 用浏览器事件广播(非 Tauri emit),
|
||||
* 与 Tauri 监听同名不同通道,故单列此处理器接住它。 */
|
||||
function onUpdateAvailable(e: Event) {
|
||||
const detail = (e as CustomEvent<UpdateAvailablePayload>).detail
|
||||
if (!detail) return
|
||||
info.value = detail
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
unlistenAvailable = await listen<UpdateAvailablePayload>('app-update-available', (e) => {
|
||||
@@ -76,6 +86,10 @@ onMounted(async () => {
|
||||
} catch {
|
||||
/* 静默 */
|
||||
}
|
||||
// 通道对齐(2026-10-10 实证):生产者 useUpdateCheck 走的是**浏览器 window CustomEvent**
|
||||
// (同名 'app-update-available'),与上面的 Tauri listen 不是同一通道 → 徽章此前永不触发。
|
||||
// 保留 Tauri 监听(后端将来若改走 emit 亦可用),同时挂 window 监听接住现有生产者。
|
||||
window.addEventListener('app-update-available', onUpdateAvailable as EventListener)
|
||||
try {
|
||||
unlistenProgress = await listen<{ stage: string; downloaded: number; total: number | null }>(
|
||||
'app-update-progress',
|
||||
@@ -93,6 +107,7 @@ onUnmounted(() => {
|
||||
unlistenProgress?.()
|
||||
unlistenAvailable = null
|
||||
unlistenProgress = null
|
||||
window.removeEventListener('app-update-available', onUpdateAvailable as EventListener)
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<template>
|
||||
<div class="project-card" :class="{ 'project-card--list': view === 'list' }" @click="router.push('/projects/' + project.id)">
|
||||
<div
|
||||
class="project-card"
|
||||
:class="{ 'project-card--list': view === 'list' }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="openProject"
|
||||
@keydown.enter.prevent="onCardKey"
|
||||
@keydown.space.prevent="onCardKey"
|
||||
>
|
||||
<div class="card-top">
|
||||
<div class="card-title-row">
|
||||
<h2 class="card-name">{{ project.name }}</h2>
|
||||
</div>
|
||||
<div class="card-top-actions">
|
||||
<span class="stage-badge" :class="stageClass(project.status)">{{ $t(statusLabel(project.status)) }}</span>
|
||||
<button class="card-del-btn" :title="$t('projects.archiveTitle')" @click.stop="emit('archive', project)">📦</button>
|
||||
<button class="card-del-btn" :title="$t('projects.deleteTitle')" @click.stop="emit('delete', project)">🗑</button>
|
||||
<button class="card-del-btn" :title="$t('projects.archiveTitle')" :aria-label="$t('projects.archiveTitle')" @click.stop="emit('archive', project)">📦</button>
|
||||
<button class="card-del-btn" :title="$t('projects.deleteTitle')" :aria-label="$t('projects.deleteTitle')" @click.stop="emit('delete', project)">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,6 +82,20 @@ const emit = defineEmits<{
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 整卡点击进详情:可点根元素用 role/tabindex 兜底(卡内嵌套删除/归档按钮,不能整体换 <button>),
|
||||
// 键盘与鼠标复用同一 handler
|
||||
function openProject() {
|
||||
router.push('/projects/' + props.project.id)
|
||||
}
|
||||
|
||||
/** 卡片根键盘触发:仅当事件源就是根本身才开详情——卡内嵌套的归档/删除/展开按钮聚焦后按
|
||||
* Enter/Space 会冒泡到此,原实现(.prevent 直接调 openProject)会把「删除」顶替成「跳转详情」
|
||||
* 并取消按钮自身激活(2026-10-10 a11y 对抗核验实测缺陷)。 */
|
||||
function onCardKey(e: KeyboardEvent) {
|
||||
if (e.target !== e.currentTarget) return
|
||||
openProject()
|
||||
}
|
||||
|
||||
// 工程数(异步拉取,展示在卡片底部)
|
||||
const moduleCount = ref(0)
|
||||
// UX走查 P3-6: 加载态标记,模板据此显骨架占位而非 0(成功/失败都收 loading,失败仍显 0 属既有约定)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { aiApi } from '@/api'
|
||||
import { state } from '@/stores/ai'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
||||
import { findToolCall, removePendingApprovalById } from './aiShared'
|
||||
import { bumpToolEpoch, findToolCall, removePendingApprovalById } from './aiShared'
|
||||
import { dispatchApprovalIPC } from './useToolApproval'
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,7 @@ async function approveToolCall(toolCallId: string, approved: boolean, decision?:
|
||||
tc.status = isDeny ? 'rejected' : 'running'
|
||||
}
|
||||
if (isDeny) {
|
||||
tc.result = t('ai.aiTool.rejectedHint')
|
||||
tc.result = t('aiTool.rejectedHint')
|
||||
}
|
||||
}
|
||||
// 同步从待审批集合移除(含 per-conv 集合;不等后端 AiApprovalResult 事件来移,防 loading 卡死)
|
||||
@@ -108,6 +108,7 @@ async function approveToolCall(toolCallId: string, approved: boolean, decision?:
|
||||
if (tc) {
|
||||
tc.status = 'completed'
|
||||
tc.result = t('ai.approvalNotDelivered', { error: errMsg })
|
||||
bumpToolEpoch()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user