新增: aichat前端体验四件与队列残留收口

- R4 代码块语言角标:MessageList decorateCodeHtml 捕获 language-xxx 输出 data-lang+CSS ::before 左上角标,hover 显,无语言裸块不渲染
- R6 工具结果路径可复制:ToolResultBody 五处 file-bar 的 path span→button(read_file/list_directory/search_files/grep/patch_file),点击复制完整路径+✓/✕ 1.2s 反馈+execCommand 降级链
- I6 重新生成 UI 入口:MessageItem user 气泡 hover 补重新生成按钮(canRegenerate=isLastUser&&非流式),复用 handleRetry→store.regenerate 链路
- V4 浮窗审批 High 风险二次确认:useToolApproval 导出 isHighRiskTool/highRiskConfirmText,ApprovalPopup.onApprove 前置 ConfirmDialog,数字键守卫不绕确认
- 9-28#5 两处全局清队列残留:useAiEvents/useAiLifecycleEvents 无 convId 的 AiError 分支改保留队列+console.warn(防误伤其他会话排队消息)
- i18n zh/en 补 clickToCopy 键;裸 @ 改 @ 转义(vue-i18n linked-message 前缀吞噬)
This commit is contained in:
lxy
2026-10-05 00:35:03 +08:00
parent 83e86cd6b7
commit c10f0e46f0
11 changed files with 161 additions and 35 deletions
+3 -1
View File
@@ -225,10 +225,12 @@ export function handleEvent(event: AiChatEvent) {
if (event.type === 'AiCompleted') {
emit('ai-drain-queue', { conversationId: event.conversation_id })
} else if (event.type === 'AiError') {
// 9-28#5 收口:无 convId 的 AiError 属异常防御路径(后端 emit 必带),清全部队列
// 会误伤其他会话排队消息,改保留 + console.warn 留痕
if (event.conversation_id) {
state.queue = state.queue.filter(q => q.conversationId !== event.conversation_id)
} else {
state.queue = []
console.warn('[ai] AiError 缺 conversation_id,跳过清队列(防误伤其他会话排队)')
}
}
} else if (event.type === 'AiTextDelta' && convId && switchingConvs.has(convId)) {
+3 -2
View File
@@ -208,8 +208,9 @@ export function handleLifecycleEvent(event: AiChatEvent): boolean {
state.queue = state.queue.filter(q => q.conversationId !== event.conversation_id)
notifyQueueCleared(dropped)
} else {
notifyQueueCleared(state.queue.length)
state.queue = []
// 9-28#5 收口:无 convId 的 AiError 属异常防御路径(后端 emit 必带),清全部队列
// 会误伤其他会话排队消息,改保留 + console.warn 留痕(不再弹「已清 N 条」误导用户)
console.warn('[ai] AiError 缺 conversation_id,跳过清队列(防误伤其他会话排队)')
}
state.messages.push({
id: `err-${nextMsgId()}` as MessageId,
+21 -9
View File
@@ -9,6 +9,7 @@
*/
import { ref, reactive, computed, watch, onBeforeUnmount, type Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import i18n from '@/i18n'
import { useConfirm } from '@/composables/useConfirm'
import { aiApi } from '@/api'
import { getApprovalRemainingMs } from './aiShared'
@@ -47,8 +48,9 @@ export function syncHighRiskTools(tools: { name: string; risk: string }[]): void
if (high.length > 0) registryHighRisk = new Set(high)
}
/** 审批二次确认门控:优先注册表投影,未拉到元数据时退兜底名单 */
function isHighRiskTool(name: string): boolean {
/** 审批二次确认门控:优先注册表投影,未拉到元数据时退兜底名单。
* V4 导出:系统浮窗 ApprovalPopup 的批准路径与 ToolCard 同门槛复用。 */
export function isHighRiskTool(name: string): boolean {
return (registryHighRisk ?? HIGH_RISK_TOOLS_FALLBACK).has(name)
}
@@ -80,6 +82,21 @@ export function dispatchApprovalIPC(
return aiApi.approve(tc.id, approved)
}
/** V4:High 风险工具二次确认文案(模块级,供 ApprovalPopup 在无组件上下文复用;
* i18n 全局实例取 t,与 useI18n 同源 locale) */
export function highRiskConfirmText(name: string): string {
// i18n.global 整体访问触发 vue-i18n 泛型递归 TS2589,先断言窄化再取 t
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const g = i18n.global as any
const gt = g.t as (key: string) => string
if (name === 'run_command') return gt('aiTool.confirmHighExec')
if (name === 'http_request') return gt('aiTool.confirmHighHttp')
if (name === 'delete_task' || name === 'delete_project' || name === 'purge_project' || name === 'delete_file' || name === 'restore_project') {
return gt('aiTool.confirmHighDelete')
}
return gt('aiTool.confirmHighGeneric')
}
/** useToolApproval emit 签名 — 补全类型替代 any */
interface ToolApprovalEmit {
(event: 'approve', payload: { id: string; approved: boolean; decision?: 'once' | 'session' | 'always' | 'deny' }): void
@@ -145,14 +162,9 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
_approveToastTimer = setTimeout(() => { approveToast.visible = false }, 3000)
}
/** High 风险工具的二次确认文案(按操作类型) */
/** High 风险工具的二次确认文案(按操作类型;V4 提升为模块级导出,浮窗复用) */
function highRiskConfirmMsg(name: string): string {
if (name === 'run_command') return t('aiTool.confirmHighExec')
if (name === 'http_request') return t('aiTool.confirmHighHttp')
if (name === 'delete_task' || name === 'delete_project' || name === 'purge_project' || name === 'delete_file' || name === 'restore_project') {
return t('aiTool.confirmHighDelete')
}
return t('aiTool.confirmHighGeneric')
return highRiskConfirmText(name)
}
/**