优化: aichat 体验收尾(useAiEvents拆分4子域 + AIC-FIX P1队列/mutex/缓存 + i18n残留抽取)

This commit is contained in:
lxy
2026-08-08 20:38:33 +08:00
parent 4092a8d5bb
commit 65c0f6b2b6
21 changed files with 882 additions and 913 deletions
+2 -1
View File
@@ -114,7 +114,8 @@ import { ref, computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { useProjectStore } from '../../stores/project'
import { getConvState, textIdle } from '../../composables/ai/useAiEvents'
import { getConvState } from '../../composables/ai/useAiEvents'
import { textIdle } from '../../composables/ai/useAiPendingState'
import { extractImageUrlParts } from '../../composables/ai/utils'
import { uploadFile } from '../../composables/ai/fileUpload'
import { open } from '@tauri-apps/plugin-dialog'
+1 -1
View File
@@ -43,7 +43,7 @@
import { reactive, computed, watch, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { pendingDirAuths } from '../../composables/ai/useAiEvents'
import { pendingDirAuths } from '../../composables/ai/useAiPendingState'
import { aiApi } from '../../api'
const emit = defineEmits<{
+1 -1
View File
@@ -25,7 +25,7 @@
import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { pendingHelp } from '../../composables/ai/useAiEvents'
import { pendingHelp } from '../../composables/ai/useAiPendingState'
// toast 经 emit 转父(保持单一 toast 源,与 MaxRoundsCard 一致)
const emit = defineEmits<{
+2 -1
View File
@@ -27,7 +27,8 @@
import { ref, computed, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { pendingMaxRounds, getConvState } from '../../composables/ai/useAiEvents'
import { getConvState } from '../../composables/ai/useAiEvents'
import { pendingMaxRounds } from '../../composables/ai/useAiPendingState'
import { aiApi } from '../../api'
const emit = defineEmits<{
+10 -10
View File
@@ -901,7 +901,7 @@ defineExpose({
:aria-expanded="tokenPopoverMsgId === item.msg.id"
@click="toggleTokenPopover(item.msg, $event)"
>
<span>{{ formatTokens(tokenInOf(item.msg)) }} in<template v-if="tokenEstimated(item.msg)"><span class="ai-token-est">(估算)</span></template></span>
<span>{{ formatTokens(tokenInOf(item.msg)) }} in<template v-if="tokenEstimated(item.msg)"><span class="ai-token-est">({{ $t('aiChat.estimated') }})</span></template></span>
<span class="ai-token-sep">·</span>
<span>{{ formatTokens(tokenCacheOf(item.msg)) }} cache</span>
<span class="ai-token-sep">·</span>
@@ -917,34 +917,34 @@ defineExpose({
class="ai-token-popover"
@click.stop
>
<div class="ai-token-popover-title">Token 用量详情<template v-if="tokenEstimated(item.msg)"><span class="ai-token-est">(估算)</span></template></div>
<div class="ai-token-popover-title">{{ $t('aiChat.tokenDetailTitle') }}<template v-if="tokenEstimated(item.msg)"><span class="ai-token-est">({{ $t('aiChat.estimated') }})</span></template></div>
<div class="ai-token-popover-row">
<span class="ai-token-popover-label">输入(未命中,全价)</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenInLabel') }}</span>
<span class="ai-token-popover-val">{{ formatTokens(tokenInOf(item.msg)) }} ({{ tokenInOf(item.msg) }})</span>
</div>
<div class="ai-token-popover-row">
<span class="ai-token-popover-label">缓存命中(低价)</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenCacheLabel') }}</span>
<span class="ai-token-popover-val">{{ formatTokens(tokenCacheOf(item.msg)) }} ({{ tokenCacheOf(item.msg) }})</span>
</div>
<div class="ai-token-popover-row">
<span class="ai-token-popover-label">输出</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenOutLabel') }}</span>
<span class="ai-token-popover-val">{{ formatTokens(tokenOutOf(item.msg)) }} ({{ tokenOutOf(item.msg) }})</span>
</div>
<div v-if="tokenReasonOf(item.msg) > 0" class="ai-token-popover-row">
<span class="ai-token-popover-label">思考(reasoning)</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenReasonLabel') }}</span>
<span class="ai-token-popover-val">{{ formatTokens(tokenReasonOf(item.msg)) }} ({{ tokenReasonOf(item.msg) }})</span>
</div>
<div v-if="cacheHitRate(item.msg) != null" class="ai-token-popover-row">
<span class="ai-token-popover-label">缓存命中率</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenCacheRateLabel') }}</span>
<span class="ai-token-popover-val">{{ cacheHitRate(item.msg) }}%</span>
</div>
<div v-if="item.msg.model" class="ai-token-popover-row">
<span class="ai-token-popover-label">模型</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenModelLabel') }}</span>
<span class="ai-token-popover-val">{{ item.msg.model }}</span>
</div>
<div v-if="tokenEstimated(item.msg)" class="ai-token-popover-row">
<span class="ai-token-popover-label">用量说明</span>
<span class="ai-token-popover-val">prompt_tokens 缺失,按估算展示</span>
<span class="ai-token-popover-label">{{ $t('aiChat.tokenUsageNoteLabel') }}</span>
<span class="ai-token-popover-val">{{ $t('aiChat.tokenUsageNote') }}</span>
</div>
</div>
</div>
+12 -8
View File
@@ -172,8 +172,8 @@
<div class="ai-context-section-title">{{ $t('aiChat.contextSystemPrompt') }}</div>
<div class="ai-context-section-body">
<div class="ai-context-row">
<span class="ai-context-label">目标({{ contextInfo.goals.length }})</span>
<span class="ai-context-value">{{ contextInfo.goals.join('; ') || '无' }}</span>
<span class="ai-context-label">{{ $t('aiChat.goalsCount', { n: contextInfo.goals.length }) }}</span>
<span class="ai-context-value">{{ contextInfo.goals.join('; ') || $t('aiChat.none') }}</span>
</div>
<div class="ai-context-row">
<span class="ai-context-label">{{ $t('aiChat.contextEnvironment') }}</span>
@@ -190,8 +190,8 @@
<div class="ai-context-section-title">{{ $t('aiChat.contextEnrichment') }}</div>
<div class="ai-context-section-body">
<div class="ai-context-row">
<span class="ai-context-label">@项目</span>
<span class="ai-context-value">{{ contextInfo.projectNames || '无' }}</span>
<span class="ai-context-label">{{ $t('aiChat.projectLabel') }}</span>
<span class="ai-context-value">{{ contextInfo.projectNames || $t('aiChat.none') }}</span>
</div>
</div>
</div>
@@ -325,9 +325,13 @@ function onModelSelect(e: Event) {
store.modelOverride.value = v || null
}
// model
// :/(,)
watch(() => store.state.activeConversationId, () => {
store.modelOverride.value = enabledModels.value[0]?.model_id || null
const current = store.modelOverride.value
const valid = current && enabledModels.value.some(m => m.model_id === current)
if (!valid) {
store.modelOverride.value = enabledModels.value[0]?.model_id || null
}
})
// provider override (BUG-2026-07-07:provider header ):
@@ -372,7 +376,7 @@ function fmtTime(ts: number): string {
const yest = new Date(now)
yest.setDate(yest.getDate() - 1)
if (d.getFullYear() === yest.getFullYear() && d.getMonth() === yest.getMonth() && d.getDate() === yest.getDate()) {
return `昨天 ${hm}`
return `${t('aiChat.yesterday')} ${hm}`
}
//
return `${pad(d.getMonth()+1)}-${pad(d.getDate())} ${hm}`
@@ -481,7 +485,7 @@ const contextInfo = computed<ContextInfo>(() => {
goals: conv?.pinned_goals ?? [],
summaryCount: summaryMsgs.value.length,
osInfo: 'Windows 11 / PowerShell',
projectNames: Array.from(projectLabels).join(', ') || '无',
projectNames: Array.from(projectLabels).join(', ') || t('aiChat.none'),
}
})
+21 -21
View File
@@ -3,10 +3,10 @@
<div class="dep-graph__toolbar">
<span class="dep-graph__title">{{ $t('dependencyGraph.title') }}</span>
<div class="dep-graph__actions">
<button v-if="modules.length >= 2" class="btn btn-ghost btn-sm" @click="showAddDep = true">+ 依赖</button>
<button v-if="dependencies.length > 0" class="btn btn-ghost btn-sm" @click="checkCycles">🔍 环检测</button>
<button class="btn btn-ghost btn-sm" @click="exportPNG">📷 导出</button>
<button class="btn btn-ghost btn-sm" @click="fitContent">适应内容</button>
<button v-if="modules.length >= 2" class="btn btn-ghost btn-sm" @click="showAddDep = true">{{ $t('dependencyGraph.addDep') }}</button>
<button v-if="dependencies.length > 0" class="btn btn-ghost btn-sm" @click="checkCycles">{{ $t('dependencyGraph.checkCycles') }}</button>
<button class="btn btn-ghost btn-sm" @click="exportPNG">{{ $t('dependencyGraph.exportImage') }}</button>
<button class="btn btn-ghost btn-sm" @click="fitContent">{{ $t('dependencyGraph.fitContent') }}</button>
<button class="btn btn-ghost btn-sm" @click="zoomIn">+</button>
<button class="btn btn-ghost btn-sm" @click="zoomOut">-</button>
</div>
@@ -36,32 +36,32 @@
<!-- 添加依赖弹窗 -->
<div v-if="showAddDep" class="dep-graph__modal-overlay" @click.self="showAddDep = false">
<div class="dep-graph__modal">
<h3>添加工程依赖</h3>
<h3>{{ $t('dependencyGraph.addDepTitle') }}</h3>
<div class="dep-graph__modal-field">
<label>源工程(依赖方)</label>
<label>{{ $t('dependencyGraph.sourceLabel') }}</label>
<select v-model="depFrom" class="dep-graph__modal-input">
<option v-for="m in modules" :key="m.id" :value="m.id">{{ m.name }}</option>
</select>
</div>
<div class="dep-graph__modal-field">
<label>目标工程(被依赖)</label>
<label>{{ $t('dependencyGraph.targetLabel') }}</label>
<select v-model="depTo" class="dep-graph__modal-input">
<option v-for="m in modules" :key="m.id" :value="m.id">{{ m.name }}</option>
</select>
</div>
<div class="dep-graph__modal-field">
<label>依赖类型</label>
<label>{{ $t('dependencyGraph.depTypeLabel') }}</label>
<select v-model="depType" class="dep-graph__modal-input">
<option value="library">类库</option>
<option value="api">API 调用</option>
<option value="mq">消息队列</option>
<option value="shared">共享资源</option>
<option value="custom">自定义</option>
<option value="library">{{ $t('dependencyGraph.typeLibrary') }}</option>
<option value="api">{{ $t('dependencyGraph.typeApi') }}</option>
<option value="mq">{{ $t('dependencyGraph.typeMq') }}</option>
<option value="shared">{{ $t('dependencyGraph.typeShared') }}</option>
<option value="custom">{{ $t('dependencyGraph.typeCustom') }}</option>
</select>
</div>
<div class="dep-graph__modal-actions">
<button class="btn btn-ghost btn-sm" @click="showAddDep = false">取消</button>
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo" @click="onAddDep">确认</button>
<button class="btn btn-ghost btn-sm" @click="showAddDep = false">{{ $t('common.cancel') }}</button>
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo" @click="onAddDep">{{ $t('dependencyGraph.confirm') }}</button>
</div>
</div>
</div>
@@ -257,13 +257,13 @@ async function checkCycles() {
cycleNodes.value = new Set(cycles)
renderGraph()
if (cycles.length > 0) {
Message.warning(`检测到 ${cycles.length} 个环节点(已红框高亮)`)
Message.warning(t('dependencyGraph.cycleWarning', { n: cycles.length }))
} else {
Message.success('未检测到环形依赖')
Message.success(t('dependencyGraph.noCycle'))
}
} catch (e) {
console.error('[DependencyGraph] 环检测失败:', e)
Message.error('环检测失败')
Message.error(t('dependencyGraph.cycleError'))
}
}
@@ -286,17 +286,17 @@ async function exportPNG() {
async function confirmRemoveDep(dep: ModuleDependencyRecord) {
const fromName = modules.value.find(m => m.id === dep.from_module_id)?.name ?? dep.from_module_id
const toName = modules.value.find(m => m.id === dep.to_module_id)?.name ?? dep.to_module_id
const ok = await confirmDialog(`确定删除「${fromName}${toName}」的依赖吗?此操作不可撤销。`, t('common.delete'))
const ok = await confirmDialog(t('dependencyGraph.deleteConfirm', { from: fromName, to: toName }), t('common.delete'))
if (!ok) return
try {
await moduleApi.removeModuleDependency(dep.id)
Message.info('依赖已删除')
Message.info(t('dependencyGraph.deleteSuccess'))
await loadModules()
renderGraph()
bindEdgeDeleteButtons()
} catch (e) {
console.error('[DependencyGraph] 删除依赖失败:', e)
Message.error('删除依赖失败')
Message.error(t('dependencyGraph.deleteError'))
}
}