/**
 * AI 对话运行时 Store
 * 将流式对话状态提升到全局，并为每个 conversation 单独维护消息缓冲，
 * 这样页面切换或切换其他对话时，后台推理仍能持续写回正确的会话。
 */

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { usePromptStore } from '@/stores/prompt'
import { useSessionStore } from '@/stores/session'
import { useSettingsStore } from '@/stores/settings'
import { useDataService, useAIService, useLLMService } from '@/services'
import type { AIMessage as PersistedAIMessage } from '@/services/ai/types'
import { useAssistantStore } from '@/stores/assistant'
import { useSkillStore } from '@/stores/skill'
import { useLLMStore } from '@/stores/llm'
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '@electron/shared/types'
import { useAgentStreamService } from '@/services/ai-stream/service'
import { buildSerializablePreprocessConfig, shouldEnsureDesensitizeRulesBeforeSerialize } from './aiPreprocessConfig'
import type { ChartPayload, ChatEvidencePayload } from '@openchatlab/core'
import { extractToolResultText, truncateToolResultText } from '@openchatlab/core'
import {
  createRenderOnlyToolPendingBlock,
  extractChartPayloads,
  finishRenderOnlyToolResultBlocks,
  isRenderOnlyTool,
  toChartContentBlocks,
  toRenderOnlyToolErrorBlock,
} from './aiChatChartBlocks'
import { extractEvidencePayload, toEvidenceContentBlock } from './aiChatEvidenceBlocks'
import {
  appendPlanDraftDelta,
  removePlanDraftBlocks,
  replacePlanDraftWithPlan,
  type PlanBlockStatus,
  type PlanContentBlock,
  type PlanDraftContentBlock,
} from '@/services/ai/planBlocks'
import { toSerializableContentBlocks } from './aiChatContentBlocks'

// 工具调用记录
export interface ToolCallRecord {
  name: string
  displayName: string
  status: 'running' | 'done' | 'error'
  timestamp: number
  /** 工具调用参数（如搜索关键词等） */
  params?: Record<string, unknown>
}

export interface ToolBlockContent {
  name: string
  displayName: string
  status: 'running' | 'done' | 'error'
  params?: Record<string, unknown>
  durationMs?: number
  /** Runtime-only tool row used while a render-only tool is still generating its visible block. */
  transient?: boolean
  /** Provider-issued tool call id; replayed verbatim so multi-turn requests stay cache-stable */
  toolCallId?: string
  /** Truncated tool result text persisted for history replay */
  result?: string
  /** Full safe text result shown to the user; history replay continues to use result. */
  displayResult?: string
  /** Whether the tool execution failed (from the agent runtime, not UI render errors) */
  isError?: boolean
}

export interface MentionedMemberContext {
  memberId: number
  platformId: string
  displayName: string
  aliases: string[]
  mentionText: string
}

// 内容块类型（用于 AI 消息的流式混合渲染）
export type ContentBlock =
  | { type: 'text'; text: string }
  | { type: 'think'; tag: string; text: string; durationMs?: number }
  | { type: 'chart'; chart: ChartPayload }
  | { type: 'evidence'; evidence: ChatEvidencePayload }
  | PlanContentBlock
  | PlanDraftContentBlock
  | {
      type: 'tool'
      tool: ToolBlockContent
    }
  | { type: 'skill'; skillId: string; skillName: string }
  | { type: 'error'; error: SerializedErrorInfo }
  | {
      type: 'summary_meta'
      bufferBoundaryTimestamp: number
      compressedMessageCount: number
    }

// 消息类型
export interface ChatMessage {
  id: string
  role: 'user' | 'assistant' | 'summary'
  content: string
  timestamp: number
  parentId?: string | null
  dataSource?: {
    toolsUsed: string[]
    toolRounds: number
  }
  /** @deprecated 使用 contentBlocks 替代 */
  toolCalls?: ToolCallRecord[]
  /** AI 消息的内容块数组（按时序排列的文本和工具调用） */
  contentBlocks?: ContentBlock[]
  isStreaming?: boolean
}

// 搜索结果消息类型（保留用于数据源面板）
export interface SourceMessage {
  id: number
  senderName: string
  senderPlatformId: string
  content: string
  timestamp: number
  type: number
}

// 工具状态类型
export interface ToolStatus {
  name: string
  displayName: string
  status: 'running' | 'done' | 'error'
  result?: unknown
}

interface OwnerInfo {
  platformId: string
  displayName: string
}

interface AIChatBuffer {
  messages: ChatMessage[]
  sourceMessages: SourceMessage[]
  currentKeywords: string[]
  assistantId: string | null
  loaded: boolean
  sessionTokenUsage?: TokenUsage
}

export interface AIChatSessionState {
  sessionId: string
  sessionName: string
  chatType: 'group' | 'private'
  locale: string
  timeFilter?: { startTs: number; endTs: number }
  selectedAssistantId: string | null
  messages: ChatMessage[]
  sourceMessages: SourceMessage[]
  currentKeywords: string[]
  isLoadingSource: boolean
  isAIThinking: boolean
  currentAIChatId: string | null
  currentToolStatus: ToolStatus | null
  toolsUsedInCurrentRound: string[]
  sessionTokenUsage: TokenUsage
  agentStatus: AgentRuntimeStatus | null
  ownerInfo?: OwnerInfo
  ownerInfoInitialized: boolean
  isAborted: boolean
  currentRequestId: string
  currentAgentRequestId: string
  aiChatBuffers: Record<string, AIChatBuffer>
}

export interface AIBackgroundTask {
  requestId: string
  chatKey: string
  sessionId: string
  sessionName: string
  chatType: 'group' | 'private'
  aiChatId: string | null
  questionPreview: string
  startedAt: number
}

export interface EnsureAIChatSessionParams {
  sessionId: string
  sessionName: string
  chatType: 'group' | 'private'
  locale: string
  timeFilter?: { startTs: number; endTs: number }
}

export interface SendMessageResult {
  success: boolean
  reason?: 'busy' | 'empty' | 'no_config' | 'error' | 'aborted'
  activeTask?: AIBackgroundTask | null
}

const DRAFT_AI_CHAT_KEY = '__draft__'

/**
 * 创建对话时前端已经知道 locale，因此默认助手在这里选择即可。
 */
function getDefaultGeneralAssistantId(locale: string): 'general_cn' | 'general_en' | 'general_ja' {
  if (locale.startsWith('en')) return 'general_en'
  if (locale.startsWith('ja')) return 'general_ja'
  return 'general_cn'
}

function buildTimeFilterKey(timeFilter?: { startTs: number; endTs: number }): string {
  if (!timeFilter) return 'all'
  return `${timeFilter.startTs}_${timeFilter.endTs}`
}

export function buildAIChatKey(params: {
  sessionId: string
  chatType: 'group' | 'private'
  timeFilter?: { startTs: number; endTs: number }
}): string {
  return `${params.sessionId}:${params.chatType}:${buildTimeFilterKey(params.timeFilter)}`
}

function createEmptyTokenUsage(): TokenUsage {
  return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
}

function toTokenUsage(data: { [K in keyof TokenUsage]?: number }): TokenUsage {
  return {
    promptTokens: data.promptTokens ?? 0,
    completionTokens: data.completionTokens ?? 0,
    totalTokens: data.totalTokens ?? 0,
    cacheReadTokens: data.cacheReadTokens ?? 0,
    cacheWriteTokens: data.cacheWriteTokens ?? 0,
  }
}

function normalizeSerializedError(error: unknown): SerializedErrorInfo {
  if (error && typeof error === 'object') {
    const record = error as Record<string, unknown>
    return {
      name: typeof record.name === 'string' ? record.name : null,
      message: typeof record.message === 'string' ? record.message : '未知错误',
      stack: typeof record.stack === 'string' ? record.stack : null,
      statusCode: typeof record.statusCode === 'number' ? record.statusCode : null,
      url: typeof record.url === 'string' ? record.url : null,
      responseBody: typeof record.responseBody === 'string' ? record.responseBody : null,
      responseHeaders:
        record.responseHeaders && typeof record.responseHeaders === 'object'
          ? (record.responseHeaders as Record<string, string>)
          : null,
      requestBody: typeof record.requestBody === 'string' ? record.requestBody : null,
      cause: typeof record.cause === 'string' ? record.cause : null,
      provider: typeof record.provider === 'string' ? record.provider : null,
      friendlyMessage: typeof record.friendlyMessage === 'string' ? record.friendlyMessage : null,
    }
  }
  return { name: null, message: error ? String(error) : '未知错误', stack: null }
}

function toRuntimeMessage(msg: PersistedAIMessage): ChatMessage {
  return {
    id: msg.id,
    role: msg.role,
    content: msg.content,
    timestamp: msg.timestamp * 1000,
    parentId: msg.parentId,
    contentBlocks: msg.contentBlocks as ContentBlock[] | undefined,
  }
}

function createAIChatBuffer(assistantId: string | null = null): AIChatBuffer {
  return {
    messages: [],
    sourceMessages: [],
    currentKeywords: [],
    assistantId,
    loaded: false,
  }
}

function createSessionState(params: EnsureAIChatSessionParams): AIChatSessionState {
  const draftBuffer = createAIChatBuffer(null)
  return {
    sessionId: params.sessionId,
    sessionName: params.sessionName,
    chatType: params.chatType,
    locale: params.locale,
    timeFilter: params.timeFilter,
    selectedAssistantId: null,
    messages: draftBuffer.messages,
    sourceMessages: draftBuffer.sourceMessages,
    currentKeywords: draftBuffer.currentKeywords,
    isLoadingSource: false,
    isAIThinking: false,
    currentAIChatId: null,
    currentToolStatus: null,
    toolsUsedInCurrentRound: [],
    sessionTokenUsage: createEmptyTokenUsage(),
    agentStatus: null,
    ownerInfo: undefined,
    ownerInfoInitialized: false,
    isAborted: false,
    currentRequestId: '',
    currentAgentRequestId: '',
    aiChatBuffers: {
      [DRAFT_AI_CHAT_KEY]: draftBuffer,
    },
  }
}

function getDisplayedBufferKey(state: AIChatSessionState): string {
  return state.currentAIChatId ?? DRAFT_AI_CHAT_KEY
}

export const useAIChatStore = defineStore('aiChatRuntime', () => {
  const sessionStates = ref<Record<string, AIChatSessionState>>({})
  const activeTask = ref<AIBackgroundTask | null>(null)

  const promptStore = usePromptStore()
  const sessionStore = useSessionStore()
  const settingsStore = useSettingsStore()
  const assistantStore = useAssistantStore()
  const skillStore = useSkillStore()
  const llmStore = useLLMStore()
  const { aiGlobalSettings } = storeToRefs(promptStore)

  let pendingFocusReturn = false

  function generateId(prefix: string): string {
    return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
  }

  function ensureSessionState(params: EnsureAIChatSessionParams): { chatKey: string; state: AIChatSessionState } {
    const chatKey = buildAIChatKey(params)
    const existing = sessionStates.value[chatKey]

    if (existing) {
      existing.sessionName = params.sessionName
      existing.chatType = params.chatType
      existing.locale = params.locale
      existing.timeFilter = params.timeFilter
      return { chatKey, state: existing }
    }

    const state = createSessionState(params)
    sessionStates.value[chatKey] = state
    // 新建会话状态后，必须返回 store 中的响应式代理对象；
    // 否则首屏会绑定到原始对象，点击后要切页回来才会看到更新。
    const reactiveState = sessionStates.value[chatKey]
    void ensureOwnerInfo(chatKey)
    return { chatKey, state: reactiveState }
  }

  function getSessionState(chatKey: string): AIChatSessionState | null {
    return sessionStates.value[chatKey] ?? null
  }

  function getActiveTaskState(): AIChatSessionState | null {
    if (!activeTask.value) return null
    return getSessionState(activeTask.value.chatKey)
  }

  function getOrCreateBuffer(
    state: AIChatSessionState,
    bufferKey: string,
    assistantId: string | null = null
  ): AIChatBuffer {
    if (!state.aiChatBuffers[bufferKey]) {
      state.aiChatBuffers[bufferKey] = createAIChatBuffer(assistantId)
    }
    return state.aiChatBuffers[bufferKey]
  }

  /**
   * 将当前 UI 绑定到某个 conversation buffer。
   * 这里只切换显示，不会影响后台正在推理的 buffer。
   */
  function bindDisplayedBuffer(state: AIChatSessionState, bufferKey: string): void {
    // 保存当前对话的 token 使用量
    const currentKey = state.currentAIChatId ?? DRAFT_AI_CHAT_KEY
    const currentBuffer = state.aiChatBuffers[currentKey]
    if (currentBuffer) {
      currentBuffer.sessionTokenUsage = { ...state.sessionTokenUsage }
    }

    const buffer = getOrCreateBuffer(state, bufferKey)
    state.currentAIChatId = bufferKey === DRAFT_AI_CHAT_KEY ? null : bufferKey
    state.messages = buffer.messages
    state.sourceMessages = buffer.sourceMessages
    state.currentKeywords = buffer.currentKeywords
    state.selectedAssistantId = buffer.assistantId
    state.sessionTokenUsage = buffer.sessionTokenUsage ? { ...buffer.sessionTokenUsage } : createEmptyTokenUsage()
    state.agentStatus = null
  }

  function renameBufferKey(state: AIChatSessionState, fromKey: string, toKey: string): AIChatBuffer {
    const buffer = getOrCreateBuffer(state, fromKey)
    state.aiChatBuffers[toKey] = buffer
    if (fromKey !== toKey) {
      delete state.aiChatBuffers[fromKey]
    }
    if (state.currentAIChatId === null && fromKey === DRAFT_AI_CHAT_KEY) {
      state.currentAIChatId = toKey
    }
    return buffer
  }

  function applySessionAssistantSelection(chatKey: string): void {
    const state = getSessionState(chatKey)
    if (!state) return

    if (state.selectedAssistantId) {
      assistantStore.selectAssistant(state.selectedAssistantId)
    } else {
      assistantStore.clearSelection()
    }
  }

  async function ensureOwnerInfo(chatKey: string): Promise<void> {
    const state = getSessionState(chatKey)
    if (!state) return

    const session = sessionStore.sessions.find((item) => item.id === state.sessionId)
    const ownerId = session?.ownerId

    if (!ownerId) {
      state.ownerInfo = undefined
      state.ownerInfoInitialized = true
      return
    }

    if (state.ownerInfoInitialized && state.ownerInfo?.platformId === ownerId) {
      return
    }

    try {
      const members = await useDataService().getMembers(state.sessionId)
      const ownerMember = members.find((member) => member.platformId === ownerId)
      state.ownerInfo = ownerMember
        ? {
            platformId: ownerId,
            displayName: ownerMember.groupNickname || ownerMember.accountName || ownerId,
          }
        : {
            platformId: ownerId,
            displayName: ownerId,
          }
      state.ownerInfoInitialized = true
    } catch (error) {
      console.error('[AI] 获取 Owner 信息失败:', error)
      state.ownerInfo = undefined
      state.ownerInfoInitialized = true
    }
  }

  function setActiveTaskMeta(
    chatKey: string,
    content: string,
    requestId: string,
    aiChatId: string | null = null
  ): void {
    const state = getSessionState(chatKey)
    if (!state) return

    activeTask.value = {
      requestId,
      chatKey,
      sessionId: state.sessionId,
      sessionName: state.sessionName,
      chatType: state.chatType,
      aiChatId,
      questionPreview: content.trim().slice(0, 80),
      startedAt: Date.now(),
    }
  }

  function clearActiveTask(chatKey: string, requestId?: string): void {
    if (!activeTask.value) return
    if (activeTask.value.chatKey !== chatKey) return
    if (requestId && activeTask.value.requestId !== requestId) return
    activeTask.value = null
  }

  /**
   * 会话创建成功后，把后台任务绑定到真实 aiChatId。
   * 单独抽成 helper，避免在长 async 流程里触发异常的类型缩窄。
   */
  function updateActiveTaskAIChatId(chatKey: string, aiChatId: string): void {
    if (!activeTask.value) return
    if (activeTask.value.chatKey !== chatKey) return
    activeTask.value.aiChatId = aiChatId
  }

  function buildFallbackAgentStatus(state: AIChatSessionState): AgentRuntimeStatus {
    return {
      phase: 'preparing',
      round: 0,
      toolsUsed: state.toolsUsedInCurrentRound.length,
      contextTokens: 0,
      totalUsage: { ...state.sessionTokenUsage },
      updatedAt: Date.now(),
    }
  }

  function setAgentPhase(
    state: AIChatSessionState,
    phase: AgentRuntimeStatus['phase'],
    extra?: Partial<AgentRuntimeStatus>
  ): void {
    const base = state.agentStatus ? { ...state.agentStatus } : buildFallbackAgentStatus(state)
    state.agentStatus = {
      ...base,
      ...extra,
      phase,
      updatedAt: Date.now(),
    }
  }

  function selectAssistantForSession(chatKey: string, assistantId: string): boolean {
    const state = getSessionState(chatKey)
    if (!state || state.isAIThinking) return false

    const buffer = getOrCreateBuffer(state, getDisplayedBufferKey(state), assistantId)
    buffer.assistantId = assistantId
    state.selectedAssistantId = assistantId
    assistantStore.selectAssistant(assistantId)
    return true
  }

  async function loadAIChat(chatKey: string, aiChatId: string): Promise<boolean> {
    const state = getSessionState(chatKey)
    if (!state) return false

    try {
      const conversation = await useAIService().getAIChat(aiChatId)
      const buffer = getOrCreateBuffer(state, aiChatId, conversation?.assistantId ?? null)

      if (!buffer.loaded) {
        const [history, tokenUsage] = await Promise.all([
          useAIService().getMessages(aiChatId),
          useAIService().getAIChatTokenUsage(aiChatId),
        ])
        buffer.messages.splice(0, buffer.messages.length, ...history.map((msg) => toRuntimeMessage(msg)))
        buffer.sourceMessages.splice(0, buffer.sourceMessages.length)
        buffer.currentKeywords.splice(0, buffer.currentKeywords.length)
        buffer.sessionTokenUsage = toTokenUsage(tokenUsage)
        buffer.loaded = true
      }

      buffer.assistantId = conversation?.assistantId ?? buffer.assistantId ?? null
      bindDisplayedBuffer(state, aiChatId)
      applySessionAssistantSelection(chatKey)
      return true
    } catch (error) {
      console.error('[AI] 加载对话历史失败：', error)
      return false
    }
  }

  function focusAIChat(chatKey: string, aiChatId: string | null): boolean {
    const state = getSessionState(chatKey)
    if (!state) return false

    const bufferKey = aiChatId ?? DRAFT_AI_CHAT_KEY
    if (!state.aiChatBuffers[bufferKey]) {
      return false
    }

    bindDisplayedBuffer(state, bufferKey)
    applySessionAssistantSelection(chatKey)
    return true
  }

  function focusActiveTaskAIChat(): boolean {
    if (!activeTask.value) return false
    pendingFocusReturn = true
    return focusAIChat(activeTask.value.chatKey, activeTask.value.aiChatId)
  }

  /**
   * 每次 ChatExplorer 挂载时调用。
   * 如果存在有效的记忆助手则直接进入对应助手，否则回到助手选择页。
   * 从浮动任务条返回（pendingFocusReturn）时跳过重置以保留对话状态。
   */
  async function resetToSelectorOnEnter(chatKey: string): Promise<void> {
    if (pendingFocusReturn) {
      pendingFocusReturn = false
      return
    }
    const state = getSessionState(chatKey)
    if (!state || state.isAIThinking) return

    if (!assistantStore.isLoaded) {
      await assistantStore.loadAssistants()
    }

    if (!state.selectedAssistantId) {
      const defaultId = getDefaultGeneralAssistantId(state.locale)
      selectAssistantForSession(chatKey, defaultId)
    }
    startNewAIChat(chatKey)
  }

  function startNewAIChat(chatKey: string, welcomeMessage?: string): boolean {
    const state = getSessionState(chatKey)
    if (!state || state.isAIThinking) return false

    const draftBuffer = createAIChatBuffer(state.selectedAssistantId)
    state.aiChatBuffers[DRAFT_AI_CHAT_KEY] = draftBuffer
    bindDisplayedBuffer(state, DRAFT_AI_CHAT_KEY)
    state.currentToolStatus = null
    state.toolsUsedInCurrentRound = []
    state.isLoadingSource = false
    state.sessionTokenUsage = createEmptyTokenUsage()
    state.agentStatus = null
    state.isAborted = false
    state.currentRequestId = ''
    state.currentAgentRequestId = ''

    if (welcomeMessage) {
      draftBuffer.messages.push({
        id: generateId('welcome'),
        role: 'assistant',
        content: welcomeMessage,
        timestamp: Date.now(),
      })
    }

    return true
  }

  async function loadMoreSourceMessages(): Promise<void> {
    // Agent 模式下暂不支持加载更多
  }

  async function updateMaxMessages(): Promise<void> {
    // Agent 模式下由工具自行控制
  }

  interface StreamBlockHelpers {
    updateAIMessage: (updates: Partial<ChatMessage>) => void
    appendTextToBlocks: (text: string) => void
    appendThinkToBlocks: (text: string, tag?: string, durationMs?: number) => void
    appendChartsToBlocks: (charts: ChartPayload[]) => void
    appendEvidenceToBlocks: (evidence: ChatEvidencePayload) => void
    appendPlanDraftToBlocks: (delta: string) => void
    appendPlanToBlocks: (plan: PlanContentBlock) => void
    removePlanDraftsFromBlocks: () => void
    updatePlanBlockStatus: (status: PlanBlockStatus) => void
    appendErrorToBlocks: (error: SerializedErrorInfo) => void
    addToolBlock: (toolName: string, params?: Record<string, unknown>, toolCallId?: string) => void
    addRenderOnlyToolPendingBlock: (toolName: string, params?: Record<string, unknown>, toolCallId?: string) => void
    updateRenderOnlyToolResult: (
      toolName: string,
      toolCallId: string | undefined,
      charts: ChartPayload[],
      errorBlock: ReturnType<typeof toRenderOnlyToolErrorBlock>,
      toolFailed?: boolean
    ) => void
    updateToolBlockStatus: (
      toolName: string,
      status: 'done' | 'error',
      completion?: { toolCallId?: string; result?: string; displayResult?: string; isError?: boolean }
    ) => void
  }

  function createStreamBlockHelpers(targetBuffer: AIChatBuffer, getAiMessageIndex: () => number): StreamBlockHelpers {
    const updateAIMessage = (updates: Partial<ChatMessage>) => {
      const idx = getAiMessageIndex()
      targetBuffer.messages[idx] = { ...targetBuffer.messages[idx], ...updates }
    }

    const appendTextToBlocks = (text: string) => {
      if (!text) return
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      const lastBlock = blocks[blocks.length - 1]
      if (text.trim().length === 0 && (!lastBlock || lastBlock.type !== 'text')) return
      if (lastBlock && lastBlock.type === 'text') {
        lastBlock.text += text
      } else {
        blocks.push({ type: 'text', text })
      }
      updateAIMessage({ contentBlocks: [...blocks], content: targetBuffer.messages[idx].content + text })
    }

    const appendThinkToBlocks = (text: string, tag?: string, durationMs?: number) => {
      if (!text && durationMs === undefined) return
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      const thinkTag = tag || 'think'
      const lastBlock = blocks[blocks.length - 1]
      let targetBlock: ContentBlock | undefined = lastBlock
      if (lastBlock && lastBlock.type === 'think' && lastBlock.tag === thinkTag) {
        lastBlock.text += text
      } else if (text.trim().length > 0) {
        targetBlock = { type: 'think', tag: thinkTag, text }
        blocks.push(targetBlock)
      } else if (durationMs !== undefined) {
        for (let index = blocks.length - 1; index >= 0; index--) {
          const block = blocks[index]
          if (block.type === 'think' && block.tag === thinkTag) {
            targetBlock = block
            break
          }
        }
      }
      if (durationMs !== undefined && targetBlock && targetBlock.type === 'think') {
        targetBlock.durationMs = durationMs
      }
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const appendChartsToBlocks = (charts: ChartPayload[]) => {
      if (charts.length === 0) return
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      blocks.push(...toChartContentBlocks(charts))
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const appendEvidenceToBlocks = (evidence: ChatEvidencePayload) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      blocks.push(toEvidenceContentBlock(evidence))
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const appendPlanDraftToBlocks = (delta: string) => {
      if (!delta) return
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      updateAIMessage({ contentBlocks: [...appendPlanDraftDelta(blocks, delta)] })
    }

    const appendPlanToBlocks = (plan: PlanContentBlock) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      updateAIMessage({ contentBlocks: [...replacePlanDraftWithPlan(blocks, plan)] })
    }

    const removePlanDraftsFromBlocks = () => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      updateAIMessage({ contentBlocks: [...removePlanDraftBlocks(blocks)] })
    }

    const updatePlanBlockStatus = (status: PlanBlockStatus) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      for (let index = blocks.length - 1; index >= 0; index--) {
        const block = blocks[index]
        if (block.type === 'plan') {
          block.status = status
          break
        }
      }
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const appendErrorToBlocks = (error: SerializedErrorInfo) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      blocks.push({ type: 'error', error })
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const addToolBlock = (toolName: string, params?: Record<string, unknown>, toolCallId?: string) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      blocks.push({
        type: 'tool',
        tool: { name: toolName, displayName: toolName, status: 'running', params, toolCallId },
      })
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const addRenderOnlyToolPendingBlock = (toolName: string, params?: Record<string, unknown>, toolCallId?: string) => {
      const pendingBlock = createRenderOnlyToolPendingBlock(toolName, params, toolCallId)
      if (!pendingBlock) return
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      blocks.push(pendingBlock)
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    const updateRenderOnlyToolResult = (
      toolName: string,
      toolCallId: string | undefined,
      charts: ChartPayload[],
      errorBlock: ReturnType<typeof toRenderOnlyToolErrorBlock>
    ) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []

      updateAIMessage({
        contentBlocks: finishRenderOnlyToolResultBlocks(blocks, toolName, toolCallId, charts, errorBlock),
      })
    }

    const updateToolBlockStatus = (
      toolName: string,
      status: 'done' | 'error',
      completion?: { toolCallId?: string; result?: string; displayResult?: string; isError?: boolean }
    ) => {
      const idx = getAiMessageIndex()
      const blocks = targetBuffer.messages[idx].contentBlocks || []
      const isRunningTool = (block: ContentBlock): block is Extract<ContentBlock, { type: 'tool' }> =>
        block.type === 'tool' && block.tool.status === 'running'
      // 优先按 toolCallId 精确匹配（支持同名工具并行调用），找不到再按名称回退
      let target: Extract<ContentBlock, { type: 'tool' }> | undefined
      if (completion?.toolCallId) {
        for (let index = blocks.length - 1; index >= 0; index--) {
          const block = blocks[index]
          if (isRunningTool(block) && block.tool.toolCallId === completion.toolCallId) {
            target = block
            break
          }
        }
      }
      if (!target) {
        for (let index = blocks.length - 1; index >= 0; index--) {
          const block = blocks[index]
          if (isRunningTool(block) && block.tool.name === toolName) {
            target = block
            break
          }
        }
      }
      if (target) {
        target.tool.status = status
        if (completion?.result !== undefined) target.tool.result = completion.result
        if (completion?.displayResult !== undefined) target.tool.displayResult = completion.displayResult
        if (completion?.isError !== undefined) target.tool.isError = completion.isError
      }
      updateAIMessage({ contentBlocks: [...blocks] })
    }

    return {
      updateAIMessage,
      appendTextToBlocks,
      appendThinkToBlocks,
      appendChartsToBlocks,
      appendEvidenceToBlocks,
      appendPlanDraftToBlocks,
      appendPlanToBlocks,
      removePlanDraftsFromBlocks,
      updatePlanBlockStatus,
      appendErrorToBlocks,
      addToolBlock,
      addRenderOnlyToolPendingBlock,
      updateRenderOnlyToolResult,
      updateToolBlockStatus,
    }
  }

  async function sendMessage(
    chatKey: string,
    content: string,
    options?: { mentionedMembers?: MentionedMemberContext[] }
  ): Promise<SendMessageResult> {
    const state = getSessionState(chatKey)
    if (!state) {
      return { success: false, reason: 'error' }
    }

    if (!content.trim()) {
      return { success: false, reason: 'empty' }
    }

    if (state.isAIThinking || activeTask.value) {
      return { success: false, reason: 'busy', activeTask: activeTask.value }
    }

    const thisRequestId = generateId('req')
    const initialBufferKey = getDisplayedBufferKey(state)
    let resolvedAIChatId = initialBufferKey === DRAFT_AI_CHAT_KEY ? null : initialBufferKey
    const targetBuffer = getOrCreateBuffer(state, initialBufferKey, state.selectedAssistantId)
    // 在 try 外部声明，以便 catch 块能正确引用当前轮次的用户消息
    let currentUserMessage: ChatMessage | undefined
    let lastDoneUsage: TokenUsage | undefined

    targetBuffer.assistantId = state.selectedAssistantId
    targetBuffer.loaded = true

    setActiveTaskMeta(chatKey, content, thisRequestId, resolvedAIChatId)
    applySessionAssistantSelection(chatKey)
    void ensureOwnerInfo(chatKey)

    const currentSkillId = skillStore.activeSkillId
    const currentSkillName = skillStore.activeSkill?.name
    const autoSkillEnabled = aiGlobalSettings.value.enableAutoSkill ?? true
    const chartAutoMode = autoSkillEnabled ? (aiGlobalSettings.value.chartAutoMode ?? 'suggest') : 'explicit'
    const currentMentionedMembers = (options?.mentionedMembers ?? []).map((member) => ({
      memberId: member.memberId,
      platformId: member.platformId,
      displayName: member.displayName,
      aliases: [...member.aliases],
      mentionText: member.mentionText,
    }))

    state.isAIThinking = true
    state.isLoadingSource = true
    state.currentToolStatus = null
    state.toolsUsedInCurrentRound = []
    state.agentStatus = null
    state.isAborted = false
    state.currentRequestId = thisRequestId
    state.currentAgentRequestId = ''

    try {
      const hasConfig = await useLLMService().hasConfig()
      if (state.isAborted) {
        clearActiveTask(chatKey, thisRequestId)
        return { success: false, reason: 'aborted' }
      }

      if (state.currentRequestId !== thisRequestId) {
        clearActiveTask(chatKey, thisRequestId)
        return { success: false, reason: 'busy', activeTask: activeTask.value }
      }

      if (!hasConfig) {
        targetBuffer.messages.push({
          id: generateId('error'),
          role: 'assistant',
          content: '⚠️ 请先配置 AI 服务。点击左下角「设置」按钮前往「模型配置Tab」进行配置。',
          timestamp: Date.now(),
        })
        clearActiveTask(chatKey, thisRequestId)
        return { success: false, reason: 'no_config' }
      }

      const userMessage: ChatMessage = {
        id: generateId('user'),
        role: 'user',
        content,
        timestamp: Date.now(),
        toolCalls: [],
      }
      currentUserMessage = userMessage
      targetBuffer.messages.push(userMessage)

      const aiMessage: ChatMessage = {
        id: generateId('ai'),
        role: 'assistant',
        content: '',
        timestamp: Date.now(),
        isStreaming: true,
        contentBlocks: [],
      }
      if (currentSkillId && currentSkillName) {
        aiMessage.contentBlocks!.push({
          type: 'skill',
          skillId: currentSkillId,
          skillName: currentSkillName,
        })
      }
      targetBuffer.messages.push(aiMessage)
      let aiMessageIndex = targetBuffer.messages.length - 1
      let hasStreamError = false

      const {
        updateAIMessage,
        appendTextToBlocks,
        appendThinkToBlocks,
        appendChartsToBlocks,
        appendEvidenceToBlocks,
        appendPlanDraftToBlocks,
        appendPlanToBlocks,
        removePlanDraftsFromBlocks,
        updatePlanBlockStatus,
        appendErrorToBlocks,
        addToolBlock,
        addRenderOnlyToolPendingBlock,
        updateRenderOnlyToolResult,
        updateToolBlockStatus,
      } = createStreamBlockHelpers(targetBuffer, () => aiMessageIndex)

      const currentAssistantId = targetBuffer.assistantId ?? getDefaultGeneralAssistantId(state.locale)
      if (!resolvedAIChatId) {
        const title = content.slice(0, 50) + (content.length > 50 ? '...' : '')
        const conversation = await useAIService().createAIChat(state.sessionId, title, currentAssistantId)
        if (state.isAborted) {
          updateAIMessage({ isStreaming: false })
          clearActiveTask(chatKey, thisRequestId)
          return { success: false, reason: 'aborted' }
        }

        if (state.currentRequestId !== thisRequestId) {
          updateAIMessage({ isStreaming: false })
          clearActiveTask(chatKey, thisRequestId)
          return { success: false, reason: 'busy', activeTask: activeTask.value }
        }

        resolvedAIChatId = conversation.id
        renameBufferKey(state, DRAFT_AI_CHAT_KEY, conversation.id)
        targetBuffer.assistantId = currentAssistantId
        updateActiveTaskAIChatId(chatKey, conversation.id)
      }

      const context = {
        sessionId: state.sessionId,
        aiChatId: resolvedAIChatId,
        timeFilter: state.timeFilter ? { startTs: state.timeFilter.startTs, endTs: state.timeFilter.endTs } : undefined,
        maxMessagesLimit: aiGlobalSettings.value.maxMessagesPerRequest,
        ownerInfo: state.ownerInfo
          ? { platformId: state.ownerInfo.platformId, displayName: state.ownerInfo.displayName }
          : undefined,
        mentionedMembers: currentMentionedMembers.length > 0 ? currentMentionedMembers : undefined,
        preprocessConfig: await buildReadySerializablePreprocessConfig(),
        searchContextBefore: aiGlobalSettings.value.searchContextBefore,
        searchContextAfter: aiGlobalSettings.value.searchContextAfter,
      }

      const { requestId: agentReqId, promise: agentPromise } = useAgentStreamService().runStream(
        {
          userMessage: content,
          sessionId: state.sessionId,
          aiChatId: resolvedAIChatId,
          timeFilter: context.timeFilter,
          maxMessagesLimit: context.maxMessagesLimit,
          ownerInfo: context.ownerInfo,
          mentionedMembers: context.mentionedMembers,
          preprocessConfig: context.preprocessConfig,
          chatType: state.chatType,
          locale: state.locale,
          assistantId: currentAssistantId,
          skillId: currentSkillId,
          enableAutoSkill: !currentSkillId ? autoSkillEnabled : undefined,
          chartAutoMode: !currentSkillId ? chartAutoMode : undefined,
          compressionConfig: {
            enabled: aiGlobalSettings.value.contextCompression?.enabled ?? false,
            tokenThresholdPercent: aiGlobalSettings.value.contextCompression?.tokenThresholdPercent ?? 75,
            bufferSizePercent: aiGlobalSettings.value.contextCompression?.bufferSizePercent ?? 20,
            maxToolResultPercent: aiGlobalSettings.value.contextCompression?.maxToolResultPercent ?? 50,
          },
          thinkingLevel: (() => {
            const cfg = llmStore.defaultAssistant
            if (!cfg?.configId || !cfg?.modelId) return undefined
            return promptStore.getThinkingLevel(cfg.configId, cfg.modelId)
          })(),
        },
        (chunk) => {
          if (state.isAborted || thisRequestId !== state.currentRequestId) {
            return
          }

          switch (chunk.type) {
            case 'content':
              if (chunk.content) {
                state.currentToolStatus = null
                appendTextToBlocks(chunk.content)
              }
              break

            case 'think':
              if (chunk.content) {
                appendThinkToBlocks(chunk.content, chunk.thinkTag)
              } else if (chunk.thinkDurationMs !== undefined) {
                appendThinkToBlocks('', chunk.thinkTag, chunk.thinkDurationMs)
              }
              break

            case 'tool_start':
              if (chunk.toolName) {
                const toolParams = chunk.toolParams as Record<string, unknown> | undefined
                state.currentToolStatus = {
                  name: chunk.toolName,
                  displayName: chunk.toolName,
                  status: 'running',
                }
                state.toolsUsedInCurrentRound.push(chunk.toolName)
                if (isRenderOnlyTool(chunk.toolName)) {
                  addRenderOnlyToolPendingBlock(chunk.toolName, toolParams, chunk.toolCallId)
                } else {
                  addToolBlock(chunk.toolName, toolParams, chunk.toolCallId)
                }
              }
              break

            case 'tool_result':
              if (chunk.toolName) {
                const charts = extractChartPayloads(chunk.toolResult)
                const renderOnlyError = toRenderOnlyToolErrorBlock(chunk.toolName, chunk.toolResult)
                if (isRenderOnlyTool(chunk.toolName)) {
                  updateRenderOnlyToolResult(chunk.toolName, chunk.toolCallId, charts, renderOnlyError)
                } else {
                  appendChartsToBlocks(charts)
                }
                const evidence = extractEvidencePayload(chunk.toolResult)
                if (evidence) {
                  appendEvidenceToBlocks(evidence)
                }
                if (renderOnlyError) {
                  if (!isRenderOnlyTool(chunk.toolName)) {
                    appendErrorToBlocks(renderOnlyError.error)
                  }
                }
                const toolFailed = renderOnlyError !== null || chunk.toolIsError === true
                if (state.currentToolStatus?.name === chunk.toolName) {
                  state.currentToolStatus = {
                    ...state.currentToolStatus,
                    status: toolFailed ? 'error' : 'done',
                  }
                }
                if (!isRenderOnlyTool(chunk.toolName)) {
                  const resultText = extractToolResultText(chunk.toolResult)
                  updateToolBlockStatus(chunk.toolName, toolFailed ? 'error' : 'done', {
                    toolCallId: chunk.toolCallId,
                    result: truncateToolResultText(resultText),
                    displayResult: resultText,
                    isError: chunk.toolIsError,
                  })
                }
              }
              state.isLoadingSource = false
              break

            case 'plan_delta':
              if (chunk.planDelta) {
                appendPlanDraftToBlocks(chunk.planDelta)
              }
              break

            case 'plan':
              if (chunk.plan) {
                appendPlanToBlocks(chunk.plan)
                updatePlanBlockStatus('executing')
              }
              break

            case 'plan_skipped':
              removePlanDraftsFromBlocks()
              break

            case 'status':
              if (chunk.status && (!state.agentStatus || chunk.status.updatedAt >= state.agentStatus.updatedAt)) {
                state.agentStatus = chunk.status
              }
              break

            case 'compression_done':
              if (chunk.compressionResult) {
                const summaryMsg: ChatMessage = {
                  id: `summary-${Date.now()}`,
                  role: 'summary',
                  content: chunk.compressionResult.summaryContent,
                  timestamp: chunk.compressionResult.timestamp,
                }
                const insertIdx = Math.max(0, targetBuffer.messages.length - 1)
                targetBuffer.messages.splice(insertIdx, 0, summaryMsg)
                aiMessageIndex++
              }
              break

            case 'done':
              state.currentToolStatus = null
              if (!hasStreamError) updatePlanBlockStatus('done')
              if (chunk.usage) {
                lastDoneUsage = { ...chunk.usage }
                state.sessionTokenUsage = {
                  promptTokens: state.sessionTokenUsage.promptTokens + chunk.usage.promptTokens,
                  completionTokens: state.sessionTokenUsage.completionTokens + chunk.usage.completionTokens,
                  totalTokens: state.sessionTokenUsage.totalTokens + chunk.usage.totalTokens,
                  cacheReadTokens: state.sessionTokenUsage.cacheReadTokens + chunk.usage.cacheReadTokens,
                  cacheWriteTokens: state.sessionTokenUsage.cacheWriteTokens + chunk.usage.cacheWriteTokens,
                }
              }
              setAgentPhase(state, 'completed', chunk.usage ? { totalUsage: chunk.usage } : undefined)
              break

            case 'error':
              removePlanDraftsFromBlocks()
              updatePlanBlockStatus('skipped')
              if (state.currentToolStatus) {
                state.currentToolStatus = {
                  ...state.currentToolStatus,
                  status: 'error',
                }
                updateToolBlockStatus(state.currentToolStatus.name, 'error')
              }
              if (!hasStreamError) {
                hasStreamError = true
                const blocks = targetBuffer.messages[aiMessageIndex].contentBlocks || []
                blocks.push({
                  type: 'error',
                  error: normalizeSerializedError(chunk.error),
                })
                updateAIMessage({ contentBlocks: [...blocks], isStreaming: false })
              }
              setAgentPhase(state, 'error')
              break
          }
        }
      )

      state.currentAgentRequestId = agentReqId
      setActiveTaskMeta(chatKey, content, agentReqId, resolvedAIChatId)

      const result = await agentPromise
      if (state.isAborted) {
        clearActiveTask(chatKey, agentReqId)
        return { success: false, reason: 'aborted' }
      }

      if (thisRequestId !== state.currentRequestId) {
        clearActiveTask(chatKey, agentReqId)
        return { success: false, reason: 'busy', activeTask: activeTask.value }
      }

      if (result.success && result.result) {
        targetBuffer.messages[aiMessageIndex] = {
          ...targetBuffer.messages[aiMessageIndex],
          dataSource: {
            toolsUsed: result.result.toolsUsed,
            toolRounds: result.result.toolRounds,
          },
          isStreaming: false,
        }

        const savedMessages = await saveAIChatMessages(
          resolvedAIChatId,
          userMessage,
          targetBuffer.messages[aiMessageIndex],
          lastDoneUsage
        )
        if (savedMessages) {
          Object.assign(userMessage, savedMessages.userMessage)
          targetBuffer.messages[aiMessageIndex] = {
            ...targetBuffer.messages[aiMessageIndex],
            ...savedMessages.assistantMessage,
            isStreaming: false,
          }
        }
      } else if (!hasStreamError) {
        const blocks = targetBuffer.messages[aiMessageIndex].contentBlocks || []
        blocks.push({
          type: 'error',
          error: normalizeSerializedError(result.error),
        })
        targetBuffer.messages[aiMessageIndex] = {
          ...targetBuffer.messages[aiMessageIndex],
          contentBlocks: [...blocks],
          isStreaming: false,
        }
        const savedMessages = await saveAIChatMessages(
          resolvedAIChatId,
          userMessage,
          targetBuffer.messages[aiMessageIndex],
          lastDoneUsage
        )
        if (savedMessages) {
          Object.assign(userMessage, savedMessages.userMessage)
          targetBuffer.messages[aiMessageIndex] = {
            ...targetBuffer.messages[aiMessageIndex],
            ...savedMessages.assistantMessage,
            isStreaming: false,
          }
        }
      } else {
        const savedMessages = await saveAIChatMessages(
          resolvedAIChatId,
          userMessage,
          targetBuffer.messages[aiMessageIndex],
          lastDoneUsage
        )
        if (savedMessages) {
          Object.assign(userMessage, savedMessages.userMessage)
          targetBuffer.messages[aiMessageIndex] = {
            ...targetBuffer.messages[aiMessageIndex],
            ...savedMessages.assistantMessage,
            isStreaming: false,
          }
        }
      }

      return { success: true }
    } catch (error) {
      console.error('[AI] 处理失败：', error)
      state.agentStatus = null

      const lastMessage = targetBuffer.messages[targetBuffer.messages.length - 1]
      if (lastMessage && lastMessage.role === 'assistant') {
        const errInfo: SerializedErrorInfo = {
          name: error instanceof Error ? error.name : null,
          message: error instanceof Error ? error.message : String(error),
          stack: error instanceof Error ? (error.stack ?? null) : null,
        }
        const blocks = lastMessage.contentBlocks || []
        blocks.push({ type: 'error', error: errInfo })
        lastMessage.contentBlocks = [...blocks]
        lastMessage.isStreaming = false

        // 优先使用当前轮次的用户消息，避免多轮对话取到第一条历史消息
        const userMsg = currentUserMessage || targetBuffer.messages.findLast((m) => m.role === 'user')
        if (userMsg) {
          await saveAIChatMessages(resolvedAIChatId, userMsg, lastMessage, lastDoneUsage)
        }
      }

      return { success: false, reason: 'error' }
    } finally {
      state.isAIThinking = false
      state.isLoadingSource = false
      state.currentToolStatus = null
      state.isAborted = false
      state.currentRequestId = ''
      state.currentAgentRequestId = ''
      clearActiveTask(chatKey)
    }
  }

  async function saveAIChatMessages(
    aiChatId: string | null,
    userMsg: ChatMessage,
    aiMsg: ChatMessage,
    tokenUsage?: TokenUsage
  ): Promise<{ userMessage: ChatMessage; assistantMessage: ChatMessage } | null> {
    try {
      if (!aiChatId) {
        return null
      }

      const savedUserMessage = await useAIService().addMessage(aiChatId, 'user', userMsg.content)
      const serializableContentBlocks = toSerializableContentBlocks(aiMsg.contentBlocks)
      const savedAssistantMessage = await useAIService().addMessage(
        aiChatId,
        'assistant',
        aiMsg.content,
        undefined,
        undefined,
        serializableContentBlocks,
        tokenUsage
      )
      return {
        userMessage: toRuntimeMessage(savedUserMessage),
        assistantMessage: toRuntimeMessage(savedAssistantMessage),
      }
    } catch (error) {
      console.error('[AI] 保存对话失败：', error)
      return null
    }
  }

  async function buildReadySerializablePreprocessConfig() {
    const preprocessConfig = settingsStore.aiPreprocessConfig
    if (shouldEnsureDesensitizeRulesBeforeSerialize(preprocessConfig)) {
      await settingsStore.ensureDesensitizeRules()
    }

    return buildSerializablePreprocessConfig(settingsStore.aiPreprocessConfig)
  }

  function normalizeMentionLookupText(value: string): string {
    return value
      .trim()
      .replace(/^[\s"'“”‘’([{<]+|[\s"'“”‘’)\]}>.,!?;:，。！？；：、]+$/g, '')
      .toLocaleLowerCase()
  }

  async function resolveMentionedMembersFromContent(
    state: AIChatSessionState,
    content: string
  ): Promise<MentionedMemberContext[]> {
    const mentionTokens = new Set<string>()
    for (const match of content.matchAll(/@([^\s@]+)/g)) {
      const token = normalizeMentionLookupText(match[1] ?? '')
      if (token) {
        mentionTokens.add(token)
      }
    }

    if (mentionTokens.size === 0) {
      return []
    }

    try {
      const members = await useDataService().getMembers(state.sessionId)
      const displayNameCounts = new Map<string, number>()
      members.forEach((member) => {
        const displayName = member.groupNickname || member.accountName || member.platformId
        displayNameCounts.set(displayName, (displayNameCounts.get(displayName) ?? 0) + 1)
      })

      const candidates = members.map((member) => {
        const displayName = member.groupNickname || member.accountName || member.platformId
        const insertName =
          (displayNameCounts.get(displayName) ?? 0) > 1 ? `${displayName}·${member.platformId}` : displayName
        const aliases = [...member.aliases]
        const lookupValues = [
          displayName,
          member.groupNickname || '',
          member.accountName || '',
          member.platformId,
          insertName,
          ...aliases,
        ]
          .map(normalizeMentionLookupText)
          .filter(Boolean)

        return {
          memberId: member.id,
          platformId: member.platformId,
          displayName,
          aliases,
          mentionText: `@${insertName}`,
          lookupValues,
        }
      })

      const selected: MentionedMemberContext[] = []
      const selectedIds = new Set<number>()
      for (const token of mentionTokens) {
        const candidate = candidates.find(
          (item) => !selectedIds.has(item.memberId) && item.lookupValues.includes(token)
        )
        if (!candidate) continue

        selectedIds.add(candidate.memberId)
        selected.push({
          memberId: candidate.memberId,
          platformId: candidate.platformId,
          displayName: candidate.displayName,
          aliases: candidate.aliases,
          mentionText: candidate.mentionText,
        })
      }

      return selected
    } catch (error) {
      console.error('[AI] Failed to resolve mentioned members for edited message:', error)
      return []
    }
  }

  async function editMessageAndRegenerate(
    chatKey: string,
    messageId: string,
    newContent: string,
    options?: { overwriteSubsequent?: boolean }
  ): Promise<SendMessageResult> {
    const state = getSessionState(chatKey)
    const content = newContent.trim()
    if (!state || !state.currentAIChatId) return { success: false, reason: 'error' }
    if (!content) return { success: false, reason: 'empty' }
    if (state.isAIThinking || activeTask.value) {
      return { success: false, reason: 'busy', activeTask: activeTask.value }
    }

    const overwriteAll = options?.overwriteSubsequent ?? false
    const targetBuffer = getOrCreateBuffer(state, state.currentAIChatId, state.selectedAssistantId)
    const editIndex = targetBuffer.messages.findIndex((message) => message.id === messageId)
    const originalMessage = targetBuffer.messages[editIndex]
    if (!originalMessage || originalMessage.role !== 'user' || originalMessage.isStreaming) {
      return { success: false, reason: 'error' }
    }
    if (originalMessage.content.trim() === content) {
      return { success: false, reason: 'empty' }
    }

    if (overwriteAll) {
      return editAndOverwriteAll(chatKey, state, targetBuffer, editIndex, originalMessage, content)
    }
    return editCurrentRoundOnly(chatKey, state, targetBuffer, editIndex, originalMessage, content)
  }

  async function editAndOverwriteAll(
    chatKey: string,
    state: AIChatSessionState,
    targetBuffer: AIChatBuffer,
    editIndex: number,
    originalMessage: ChatMessage,
    content: string
  ): Promise<SendMessageResult> {
    try {
      const hasConfig = await useLLMService().hasConfig()
      if (!hasConfig) {
        return { success: false, reason: 'no_config' }
      }
      if (state.isAborted) {
        return { success: false, reason: 'aborted' }
      }
      if (state.isAIThinking || activeTask.value) {
        return { success: false, reason: 'busy', activeTask: activeTask.value }
      }

      await useAIService().deleteMessagesFrom(state.currentAIChatId!, originalMessage.id)
      targetBuffer.messages.splice(editIndex, targetBuffer.messages.length - editIndex)
      return sendMessage(chatKey, content)
    } catch (error) {
      console.error('[AI] edit and overwrite failed:', error)
      return { success: false, reason: 'error' }
    }
  }

  async function editCurrentRoundOnly(
    chatKey: string,
    state: AIChatSessionState,
    targetBuffer: AIChatBuffer,
    editIndex: number,
    originalMessage: ChatMessage,
    content: string
  ): Promise<SendMessageResult> {
    const thisRequestId = generateId('req')
    let lastDoneUsage: TokenUsage | undefined
    let hasStreamError = false

    setActiveTaskMeta(chatKey, content, thisRequestId, state.currentAIChatId)
    applySessionAssistantSelection(chatKey)
    void ensureOwnerInfo(chatKey)

    state.isAIThinking = true
    state.isLoadingSource = true
    state.currentToolStatus = null
    state.toolsUsedInCurrentRound = []
    state.agentStatus = null
    state.isAborted = false
    state.currentRequestId = thisRequestId
    state.currentAgentRequestId = ''

    const oldAiResponse = targetBuffer.messages[editIndex + 1]
    const hasOldAiResponse = oldAiResponse?.role === 'assistant'
    const subsequentMessages = hasOldAiResponse
      ? targetBuffer.messages.slice(editIndex + 2)
      : targetBuffer.messages.slice(editIndex + 1)

    const aiPlaceholder: ChatMessage = {
      id: generateId('ai'),
      role: 'assistant',
      content: '',
      timestamp: Date.now(),
      isStreaming: true,
      contentBlocks: [],
    }

    const currentSkillId = skillStore.activeSkillId
    const currentSkillName = skillStore.activeSkill?.name
    if (currentSkillId && currentSkillName) {
      aiPlaceholder.contentBlocks!.push({ type: 'skill', skillId: currentSkillId, skillName: currentSkillName })
    }

    const editedUserMessage: ChatMessage = {
      ...originalMessage,
      content,
    }
    const removeCount = hasOldAiResponse ? 2 : 1
    targetBuffer.messages.splice(editIndex, removeCount, editedUserMessage, aiPlaceholder)
    const aiMessageIndex = editIndex + 1

    const restoreOriginal = () => {
      targetBuffer.messages.splice(editIndex, targetBuffer.messages.length - editIndex, originalMessage)
      if (hasOldAiResponse) {
        targetBuffer.messages.splice(editIndex + 1, 0, oldAiResponse)
      }
      targetBuffer.messages.push(...subsequentMessages)
    }

    const {
      updateAIMessage,
      appendTextToBlocks,
      appendThinkToBlocks,
      appendChartsToBlocks,
      appendEvidenceToBlocks,
      appendPlanDraftToBlocks,
      appendPlanToBlocks,
      removePlanDraftsFromBlocks,
      updatePlanBlockStatus,
      appendErrorToBlocks,
      addToolBlock,
      addRenderOnlyToolPendingBlock,
      updateRenderOnlyToolResult,
      updateToolBlockStatus,
    } = createStreamBlockHelpers(targetBuffer, () => aiMessageIndex)

    try {
      const hasConfig = await useLLMService().hasConfig()
      if (!hasConfig) {
        restoreOriginal()
        clearActiveTask(chatKey, thisRequestId)
        return { success: false, reason: 'no_config' }
      }

      const currentAssistantId = targetBuffer.assistantId ?? getDefaultGeneralAssistantId(state.locale)
      const currentMentionedMembers = await resolveMentionedMembersFromContent(state, content)
      if (state.isAborted) {
        restoreOriginal()
        clearActiveTask(chatKey, thisRequestId)
        return { success: false, reason: 'aborted' }
      }
      if (thisRequestId !== state.currentRequestId) {
        restoreOriginal()
        clearActiveTask(chatKey, thisRequestId)
        return { success: false, reason: 'busy', activeTask: activeTask.value }
      }

      const autoSkillEnabled = aiGlobalSettings.value.enableAutoSkill ?? true
      const chartAutoMode = autoSkillEnabled ? (aiGlobalSettings.value.chartAutoMode ?? 'suggest') : 'explicit'
      const context = {
        sessionId: state.sessionId,
        aiChatId: state.currentAIChatId!,
        historyLeafMessageId: originalMessage.parentId ?? null,
        timeFilter: state.timeFilter ? { startTs: state.timeFilter.startTs, endTs: state.timeFilter.endTs } : undefined,
        maxMessagesLimit: aiGlobalSettings.value.maxMessagesPerRequest,
        ownerInfo: state.ownerInfo
          ? { platformId: state.ownerInfo.platformId, displayName: state.ownerInfo.displayName }
          : undefined,
        mentionedMembers: currentMentionedMembers.length > 0 ? currentMentionedMembers : undefined,
        preprocessConfig: await buildReadySerializablePreprocessConfig(),
        searchContextBefore: aiGlobalSettings.value.searchContextBefore,
        searchContextAfter: aiGlobalSettings.value.searchContextAfter,
      }

      const { requestId: agentReqId, promise: agentPromise } = useAgentStreamService().runStream(
        {
          userMessage: content,
          sessionId: state.sessionId,
          aiChatId: state.currentAIChatId!,
          historyLeafMessageId: originalMessage.parentId ?? null,
          timeFilter: context.timeFilter,
          maxMessagesLimit: context.maxMessagesLimit,
          ownerInfo: context.ownerInfo,
          mentionedMembers: context.mentionedMembers,
          preprocessConfig: context.preprocessConfig,
          chatType: state.chatType,
          locale: state.locale,
          assistantId: currentAssistantId,
          skillId: currentSkillId,
          enableAutoSkill: !currentSkillId ? autoSkillEnabled : undefined,
          chartAutoMode: !currentSkillId ? chartAutoMode : undefined,
          compressionConfig: {
            enabled: aiGlobalSettings.value.contextCompression?.enabled ?? false,
            tokenThresholdPercent: aiGlobalSettings.value.contextCompression?.tokenThresholdPercent ?? 75,
            bufferSizePercent: aiGlobalSettings.value.contextCompression?.bufferSizePercent ?? 20,
            maxToolResultPercent: aiGlobalSettings.value.contextCompression?.maxToolResultPercent ?? 50,
          },
          thinkingLevel: (() => {
            const cfg = llmStore.defaultAssistant
            if (!cfg?.configId || !cfg?.modelId) return undefined
            return promptStore.getThinkingLevel(cfg.configId, cfg.modelId)
          })(),
        },
        (chunk) => {
          if (state.isAborted || thisRequestId !== state.currentRequestId) return
          switch (chunk.type) {
            case 'content':
              state.currentToolStatus = null
              appendTextToBlocks(chunk.content || '')
              break
            case 'think':
              if (chunk.content) appendThinkToBlocks(chunk.content, chunk.thinkTag)
              else if (chunk.thinkDurationMs !== undefined)
                appendThinkToBlocks('', chunk.thinkTag, chunk.thinkDurationMs)
              break
            case 'tool_start':
              if (chunk.toolName) {
                const toolParams = chunk.toolParams as Record<string, unknown> | undefined
                state.currentToolStatus = { name: chunk.toolName, displayName: chunk.toolName, status: 'running' }
                state.toolsUsedInCurrentRound.push(chunk.toolName)
                if (isRenderOnlyTool(chunk.toolName)) {
                  addRenderOnlyToolPendingBlock(chunk.toolName, toolParams, chunk.toolCallId)
                } else {
                  addToolBlock(chunk.toolName, toolParams, chunk.toolCallId)
                }
              }
              break
            case 'tool_result':
              if (chunk.toolName) {
                const charts = extractChartPayloads(chunk.toolResult)
                const renderOnlyError = toRenderOnlyToolErrorBlock(chunk.toolName, chunk.toolResult)
                if (isRenderOnlyTool(chunk.toolName)) {
                  updateRenderOnlyToolResult(chunk.toolName, chunk.toolCallId, charts, renderOnlyError)
                } else {
                  appendChartsToBlocks(charts)
                }
                const evidence = extractEvidencePayload(chunk.toolResult)
                if (evidence) {
                  appendEvidenceToBlocks(evidence)
                }
                if (renderOnlyError) {
                  if (!isRenderOnlyTool(chunk.toolName)) {
                    appendErrorToBlocks(renderOnlyError.error)
                  }
                }
                if (!isRenderOnlyTool(chunk.toolName)) {
                  const resultText = extractToolResultText(chunk.toolResult)
                  updateToolBlockStatus(
                    chunk.toolName,
                    renderOnlyError !== null || chunk.toolIsError === true ? 'error' : 'done',
                    {
                      toolCallId: chunk.toolCallId,
                      result: truncateToolResultText(resultText),
                      displayResult: resultText,
                      isError: chunk.toolIsError,
                    }
                  )
                }
              }
              state.currentToolStatus = null
              state.isLoadingSource = false
              break
            case 'plan_delta':
              if (chunk.planDelta) appendPlanDraftToBlocks(chunk.planDelta)
              break
            case 'plan':
              if (chunk.plan) {
                appendPlanToBlocks(chunk.plan)
                updatePlanBlockStatus('executing')
              }
              break
            case 'plan_skipped':
              removePlanDraftsFromBlocks()
              break
            case 'status':
              if (chunk.status && (!state.agentStatus || chunk.status.updatedAt >= state.agentStatus.updatedAt)) {
                state.agentStatus = chunk.status
              }
              break
            case 'done':
              state.currentToolStatus = null
              if (!hasStreamError) updatePlanBlockStatus('done')
              if (chunk.usage) lastDoneUsage = { ...chunk.usage }
              setAgentPhase(state, 'completed', chunk.usage ? { totalUsage: chunk.usage } : undefined)
              break
            case 'error': {
              removePlanDraftsFromBlocks()
              updatePlanBlockStatus('skipped')
              hasStreamError = true
              if (state.currentToolStatus) updateToolBlockStatus(state.currentToolStatus.name, 'error')
              const blocks = targetBuffer.messages[aiMessageIndex].contentBlocks || []
              blocks.push({ type: 'error', error: normalizeSerializedError(chunk.error) })
              updateAIMessage({ contentBlocks: [...blocks], isStreaming: false })
              setAgentPhase(state, 'error')
              break
            }
          }
        }
      )

      state.currentAgentRequestId = agentReqId
      setActiveTaskMeta(chatKey, content, agentReqId, state.currentAIChatId)

      const result = await agentPromise
      if (state.isAborted) {
        restoreOriginal()
        clearActiveTask(chatKey, agentReqId)
        return { success: false, reason: 'aborted' }
      }
      if (thisRequestId !== state.currentRequestId) {
        restoreOriginal()
        clearActiveTask(chatKey, agentReqId)
        return { success: false, reason: 'busy', activeTask: activeTask.value }
      }

      if (result.success && result.result) {
        updateAIMessage({
          dataSource: { toolsUsed: result.result.toolsUsed, toolRounds: result.result.toolRounds },
          isStreaming: false,
        })
      } else if (!hasStreamError) {
        const blocks = targetBuffer.messages[aiMessageIndex].contentBlocks || []
        blocks.push({ type: 'error', error: normalizeSerializedError(result.error) })
        updateAIMessage({ contentBlocks: [...blocks], isStreaming: false })
      }

      if (!result.success) {
        restoreOriginal()
        return { success: false, reason: 'error' }
      }

      await useAIService().updateMessageContent(originalMessage.id, content)
      if (hasOldAiResponse) {
        await useAIService().deleteAndRelinkMessage(state.currentAIChatId!, oldAiResponse.id)
      }

      const serializableContentBlocks = toSerializableContentBlocks(targetBuffer.messages[aiMessageIndex].contentBlocks)
      const savedAiMsg = await useAIService().insertMessageAfter(
        state.currentAIChatId!,
        originalMessage.id,
        'assistant',
        targetBuffer.messages[aiMessageIndex].content,
        serializableContentBlocks,
        lastDoneUsage
      )
      targetBuffer.messages[aiMessageIndex] = {
        ...toRuntimeMessage(savedAiMsg),
        dataSource: targetBuffer.messages[aiMessageIndex].dataSource,
        isStreaming: false,
      }
      targetBuffer.messages[editIndex] = {
        ...targetBuffer.messages[editIndex],
        content,
      }

      const nextMsgIndex = aiMessageIndex + 1
      if (nextMsgIndex < targetBuffer.messages.length) {
        targetBuffer.messages[nextMsgIndex] = {
          ...targetBuffer.messages[nextMsgIndex],
          parentId: savedAiMsg.id,
        }
      }

      targetBuffer.sessionTokenUsage = toTokenUsage(await useAIService().getAIChatTokenUsage(state.currentAIChatId!))
      state.sessionTokenUsage = { ...targetBuffer.sessionTokenUsage }
      return { success: true }
    } catch (error) {
      if (state.isAborted) {
        restoreOriginal()
        return { success: false, reason: 'aborted' }
      }
      console.error('[AI] edit and regenerate failed:', error)
      const blocks = targetBuffer.messages[aiMessageIndex]?.contentBlocks || []
      blocks.push({
        type: 'error',
        error: {
          name: error instanceof Error ? error.name : null,
          message: error instanceof Error ? error.message : String(error),
          stack: error instanceof Error ? (error.stack ?? null) : null,
        },
      })
      if (targetBuffer.messages[aiMessageIndex]) {
        updateAIMessage({ contentBlocks: [...blocks], isStreaming: false })
      }
      return { success: false, reason: 'error' }
    } finally {
      state.isAIThinking = false
      state.isLoadingSource = false
      state.currentToolStatus = null
      state.isAborted = false
      state.currentRequestId = ''
      state.currentAgentRequestId = ''
      clearActiveTask(chatKey)
    }
  }

  async function stopGeneration(chatKey: string): Promise<boolean> {
    const state = getSessionState(chatKey)
    if (!state || !state.isAIThinking) return false

    state.isAborted = true
    state.isAIThinking = false
    state.isLoadingSource = false
    state.currentToolStatus = null
    setAgentPhase(state, 'aborted')

    // 停止时优先定位真实仍在流式写入的会话缓冲，而不是当前页面正在查看的缓冲。
    const runningBufferKey =
      activeTask.value?.chatKey === chatKey
        ? (activeTask.value.aiChatId ?? DRAFT_AI_CHAT_KEY)
        : getDisplayedBufferKey(state)
    const runningBuffer = state.aiChatBuffers[runningBufferKey]
    const lastMessage = runningBuffer ? runningBuffer.messages[runningBuffer.messages.length - 1] : undefined
    if (lastMessage && lastMessage.role === 'assistant' && lastMessage.isStreaming) {
      lastMessage.isStreaming = false
      lastMessage.content += '\n\n_（已停止生成）_'
    }

    if (state.currentAgentRequestId) {
      try {
        await useAgentStreamService().abort(state.currentAgentRequestId)
      } catch (error) {
        console.error('[AI] 中止 Agent 请求失败:', error)
      }
    }

    state.currentRequestId = ''
    state.currentAgentRequestId = ''
    clearActiveTask(chatKey)
    return true
  }

  async function stopActiveTask(): Promise<boolean> {
    if (!activeTask.value) return false
    return stopGeneration(activeTask.value.chatKey)
  }

  return {
    sessionStates,
    activeTask,
    ensureSessionState,
    getSessionState,
    getActiveTaskState,
    applySessionAssistantSelection,
    selectAssistantForSession,
    loadAIChat,
    focusAIChat,
    focusActiveTaskAIChat,
    resetToSelectorOnEnter,
    startNewAIChat,
    loadMoreSourceMessages,
    updateMaxMessages,
    sendMessage,
    editMessageAndRegenerate,
    stopGeneration,
    stopActiveTask,
  }
})
