修复: @任务联想源隔离 store.tasks 污染 + DirAuthDialog 拒绝二次确认 + 看板销账(AC弱模型机制 3/5 已落地核对)
This commit is contained in:
@@ -346,6 +346,8 @@ function detectMentionTrigger() {
|
||||
mentionStart.value = i
|
||||
mentionOpen.value = true
|
||||
mentionIndex.value = 0
|
||||
// P1-g: 用户首次进入 @ 联想即懒加载全量任务候选(幂等,缓存判重;不在 computed 副作用内调用)
|
||||
void ensureTaskSuggestions()
|
||||
return
|
||||
}
|
||||
// 形如 a@b:前字符非空白 → 不是 mention 触发符,继续向前扫也只会找到更早的 @,
|
||||
@@ -403,6 +405,19 @@ const pendingMentionSpans = ref<MentionSpan[]>([])
|
||||
const MENTION_TASK_LIMIT = 50
|
||||
const projectTasksCache = ref<Map<string, TaskRecord[]>>(new Map())
|
||||
|
||||
// P1-g: @任务联想候选源隔离 store.tasks(反映 Tasks 视图筛选+分页子集)。
|
||||
// 懒加载全量未删任务(taskApi.list() 无参=全量)+ 缓存;首次 @ 触发拉取,失败空数组占位防反复重试。
|
||||
const taskSuggestionCache = ref<TaskRecord[] | null>(null)
|
||||
async function ensureTaskSuggestions(): Promise<void> {
|
||||
if (taskSuggestionCache.value) return
|
||||
try {
|
||||
taskSuggestionCache.value = await taskApi.list()
|
||||
} catch (e) {
|
||||
taskSuggestionCache.value = []
|
||||
console.error('[ChatInput] 加载任务联想失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉取指定项目的任务并缓存(已缓存则跳过);失败静默降级为空数组(不阻塞 enrichment 渲染) */
|
||||
async function ensureProjectTasks(projectId: string): Promise<void> {
|
||||
if (projectTasksCache.value.has(projectId)) return
|
||||
@@ -564,7 +579,7 @@ const mentionQuery = computed(() => {
|
||||
const mentionItems = computed<MentionItem[]>(() => {
|
||||
const q = mentionQuery.value.trim().toLowerCase()
|
||||
const projects = (projectsStore.projects || []) as ProjectRecord[]
|
||||
const tasks = (projectsStore.tasks || []) as TaskRecord[]
|
||||
const tasks = (taskSuggestionCache.value ?? []) as TaskRecord[]
|
||||
const match = (s: string) => !q || s.toLowerCase().includes(q)
|
||||
const projectItems: MentionItem[] = projects
|
||||
.filter(p => match(p.name) || match(p.description || ''))
|
||||
@@ -606,7 +621,7 @@ const mentionItems = computed<MentionItem[]>(() => {
|
||||
/** 联想候选总数(未过滤,用于区分"无匹配" vs "空") */
|
||||
const mentionTotal = computed(() =>
|
||||
((projectsStore.projects || []) as ProjectRecord[]).length
|
||||
+ ((projectsStore.tasks || []) as TaskRecord[]).length
|
||||
+ (taskSuggestionCache.value?.length ?? 0)
|
||||
+ ((projectsStore.ideas || []) as IdeaRecord[]).length,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
>{{ $t('aiChat.dirAuthDeny') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- P1-e-c: deny 拒绝二次确认(拒绝不可逆——工具返 Err、loop 续跑不再重试;对齐 ToolCard/ApprovalPopup) -->
|
||||
<ConfirmDialog
|
||||
:visible="confirmState.visible"
|
||||
:msg="confirmState.msg"
|
||||
:danger-label="$t('common.confirm')"
|
||||
@result="answerConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -45,6 +52,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { pendingDirAuths } from '../../composables/ai/useAiPendingState'
|
||||
import { aiApi } from '../../api'
|
||||
import { useConfirm } from '../../composables/useConfirm'
|
||||
import ConfirmDialog from '../../components/ConfirmDialog.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toast', payload: { msg: string; type: 'error' | 'warning' | 'info' }): void
|
||||
@@ -53,6 +62,9 @@ const emit = defineEmits<{
|
||||
const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// P1-e-c: deny 拒绝二次确认统一弹层(useConfirm 状态机 + ConfirmDialog 组件,同 ToolCard/ApprovalPopup)
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
// F-260620 根治(授权挂起无声卡死):挂起态弹窗显示只依赖 isGenerating(conv),不依赖 streaming。
|
||||
// streaming 会被流式看门狗超时清(useAiStream onStreamTimeout),而 AiDirAuthRequired 挂起常发生在
|
||||
// 首轮慢响应(看门狗已超时清 streaming)之后——此时 streaming=false 但后端 generating=true、
|
||||
@@ -87,6 +99,15 @@ const dirAuthRefs = ref<HTMLElement[]>([])
|
||||
async function handleAuthorize(id: string, decision: 'once' | 'session' | 'always' | 'deny'): Promise<void> {
|
||||
if (actingIds.has(id)) return
|
||||
actingIds.add(id)
|
||||
// P1-e-c: deny 拒绝前二次确认(拒绝不可逆——工具返 Err、loop 续跑不再重试;与 ToolCard onDeny 同款)。
|
||||
// 放 actingIds.add 之后、IPC 之前;取消则删 actingIds 复位处理中态(保持 pending 可重新决策),不调用 IPC。
|
||||
if (decision === 'deny') {
|
||||
const ok = await confirmDialog(t('aiChat.dirAuthDenyConfirm'))
|
||||
if (!ok) {
|
||||
actingIds.delete(id)
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
await aiApi.authorizeDir(id, decision)
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user