<template>
  <div class="auto-reply-page">
    <div class="page-header">
      <div>
        <h2>自动客服</h2>
        <p>当前接管的企微账号将作为自动客服账号处理客户消息</p>
      </div>
      <div class="header-actions">
        <button class="primary-btn" @click="handleStart" :disabled="busy">一键开启</button>
        <button class="ghost-btn" @click="handleDisable" :disabled="busy">关闭</button>
      </div>
    </div>

    <div class="status-grid">
      <div class="metric">
        <span>状态</span>
        <strong :class="status.enabled ? 'ok' : 'muted'">{{ status.enabled ? '已开启' : '未开启' }}</strong>
      </div>
      <div class="metric">
        <span>知识文件</span>
        <strong>{{ status.knowledgeFileCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>知识片段</span>
        <strong>{{ status.knowledgeChunkCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>向量片段</span>
        <strong>{{ status.embeddingChunkCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>今日回复</span>
        <strong>{{ status.todayReplied || 0 }}</strong>
      </div>
      <div class="metric">
        <span>今日转人工</span>
        <strong>{{ status.todayHandoff || 0 }}</strong>
      </div>
      <div class="metric">
        <span>今日忽略</span>
        <strong>{{ status.todayIgnored || 0 }}</strong>
      </div>
      <div class="metric">
        <span>最近总耗时</span>
        <strong>{{ formatDuration(status.lastTotalDurationMs) }}</strong>
      </div>
      <div class="metric">
        <span>内部联系人</span>
        <strong>{{ status.internalContactCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>外部联系人</span>
        <strong>{{ status.externalContactCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>身份缓存</span>
        <strong :class="status.identityRefreshing ? 'muted' : ''">{{ status.identityRefreshing ? '刷新中' : formatUnixTime(status.identityLastRefreshAt) }}</strong>
      </div>
      <div class="metric">
        <span>协同等待中</span>
        <strong>{{ status.collaborationWaitingCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>AI 接管中</span>
        <strong>{{ status.collaborationTakeoverCount || 0 }}</strong>
      </div>
      <div class="metric">
        <span>今日 AI 补充</span>
        <strong>{{ status.todayCollaborationSupplemented || 0 }}</strong>
      </div>
      <div class="metric">
        <span>今日无人接管</span>
        <strong>{{ status.todayCollaborationTakeovers || 0 }}</strong>
      </div>
    </div>
    <div v-if="message && messageType !== 'error'" class="inline-message" :class="messageType">{{ message }}</div>
    <div v-if="reasonEntries.length" class="reason-row">
      <span v-for="item in reasonEntries" :key="item.reason">{{ reasonLabel(item.reason) }} {{ item.count }}</span>
    </div>

    <nav class="section-nav" aria-label="自动客服模块导航">
      <button
        v-for="item in sectionNavItems"
        :key="item.id"
        type="button"
        class="section-nav-btn"
        @click="scrollToSection(item.id)"
      >
        {{ item.label }}
      </button>
    </nav>

    <section id="auto-section-listen" class="auto-section">
      <div class="section-header">
        <div class="section-title">
          <span class="section-index">01</span>
          <h3>监听策略</h3>
        </div>
        <div class="section-meta">
          <span>{{ form.enabled ? '已启用' : '未启用' }}</span>
          <span>{{ form.listen.groupTriggerMode === 'all' ? '群聊全量' : '@ 触发' }}</span>
        </div>
      </div>
      <div v-if="sectionAlerts('listen').length" class="section-alerts">
        <div v-for="alert in sectionAlerts('listen')" :key="alert" class="section-alert">{{ alert }}</div>
      </div>
      <div class="form-grid">
        <label class="check-row">
          <input type="checkbox" v-model="form.enabled">
          <span>启用自动客服</span>
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.listen.enablePrivateChat">
          <span>处理私聊</span>
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.listen.enableGroupChat">
          <span>处理群聊</span>
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.listen.ignoreSelfMessage">
          <span>忽略自己发送的消息</span>
        </label>
        <label>
          <span>群聊触发</span>
          <select v-model="form.listen.groupTriggerMode">
            <option value="mention_only">仅 @ 当前账号</option>
            <option value="all">所有群消息</option>
          </select>
        </label>
        <label>
          <span>去重秒数</span>
          <input type="number" v-model.number="form.listen.deduplicateSeconds" min="30">
        </label>
        <label>
          <span>冷却秒数</span>
          <input type="number" v-model.number="form.replyPolicy.cooldownSeconds" min="0">
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.collaboration.enabled">
          <span>人机协同模式</span>
        </label>
        <label>
          <span>协同等待人工秒数</span>
          <input type="number" v-model.number="form.collaboration.humanWaitSeconds" min="5" max="3600">
        </label>
        <label>
          <span>协同人工回复后判断延迟</span>
          <input type="number" v-model.number="form.collaboration.afterHumanReplyDelaySeconds" min="0" max="120">
        </label>
        <label>
          <span>AI 接管静默退出秒数</span>
          <input type="number" v-model.number="form.collaboration.takeoverIdleExitSeconds" min="30" max="7200">
        </label>
      </div>
      <div class="panel-actions">
        <button class="ghost-btn" @click="saveConfig('listen')" :disabled="busy">保存监听策略</button>
      </div>
    </section>

    <section id="auto-section-ai" class="auto-section">
      <div class="section-header">
        <div class="section-title">
          <span class="section-index">02</span>
          <h3>AI 配置</h3>
        </div>
        <div class="section-meta">
          <span>{{ form.ai.provider === 'local' ? '本地模型' : '兼容接口' }}</span>
          <span>文本：{{ form.ai.model || '未配置' }}</span>
          <span>图片：{{ form.ai.visionModel || defaultVisionModel }}</span>
          <span>语音：{{ form.ai.audioModel || '未配置' }}</span>
        </div>
      </div>
      <div v-if="sectionAlerts('ai').length" class="section-alerts">
        <div v-for="alert in sectionAlerts('ai')" :key="alert" class="section-alert">{{ alert }}</div>
      </div>
      <div class="ai-config-groups">
        <div class="config-subsection">
          <div class="config-subsection-header">
            <strong>文本回复</strong>
            <span>用于知识库回答、普通问候和兜底回复</span>
          </div>
          <div class="form-grid">
            <label>
              <span>接口类型</span>
              <select v-model="form.ai.provider">
                <option value="openai_compatible">OpenAI 兼容接口</option>
                <option value="local">本地模型 Ollama</option>
              </select>
            </label>
            <label>
              <span>Base URL</span>
              <input v-model="form.ai.baseUrl" placeholder="https://api.example.com/v1">
            </label>
            <label>
              <span>API Key</span>
              <input v-model="form.ai.apiKey" type="password" placeholder="可为空">
            </label>
            <label>
              <span>文本模型</span>
              <input v-model="form.ai.model" placeholder="qwen-turbo">
            </label>
            <label class="wide">
              <span>AI 身份提示词</span>
              <textarea v-model="form.ai.systemPrompt" rows="3" placeholder="你是一名灵泽万川的智能客服。"></textarea>
            </label>
            <label>
              <span>Max Tokens</span>
              <input type="number" v-model.number="form.ai.maxTokens" min="64" max="4000">
            </label>
            <label>
              <span>Temperature</span>
              <input type="number" step="0.1" v-model.number="form.ai.temperature" min="0" max="2">
            </label>
            <label>
              <span>超时秒数</span>
              <input type="number" v-model.number="form.ai.timeoutSeconds" min="5">
            </label>
            <label class="check-row">
              <input type="checkbox" v-model="form.ai.enableThinking">
              <span>开启模型思考</span>
            </label>
            <label>
              <span>AI回复的详细程度</span>
              <select v-model="form.ai.replyDetail">
                <option value="concise">简洁</option>
                <option value="medium">中等</option>
                <option value="detailed">详细</option>
              </select>
            </label>
          </div>
        </div>

        <div class="config-subsection">
          <div class="config-subsection-header">
            <strong>图片识别</strong>
            <span>用于客户发送图片、表情和视频封面时先识别内容；默认复用文本回复的 Base URL / API Key</span>
          </div>
          <div class="form-grid">
            <label>
              <span>图片识别模型</span>
              <input v-model="form.ai.visionModel" placeholder="qwen3-vl-plus">
            </label>
          </div>
          <button type="button" class="advanced-toggle" @click="showVisionAdvanced = !showVisionAdvanced">
            {{ showVisionAdvanced ? '收起高级设置' : '高级设置' }}
          </button>
          <div v-if="showVisionAdvanced" class="form-grid advanced-grid">
            <label>
              <span>图片 Base URL</span>
              <input v-model="form.ai.visionBaseUrl" placeholder="留空复用主 Base URL">
            </label>
            <label>
              <span>图片 API Key</span>
              <input v-model="form.ai.visionApiKey" type="password" placeholder="留空复用主 API Key">
            </label>
            <div v-if="visionApiKeyLooksLikeUrl" class="field-warning">图片 API Key 当前看起来是 URL，保存时会清空并复用主 API Key。</div>
          </div>
        </div>

        <div class="config-subsection">
          <div class="config-subsection-header">
            <strong>语音识别</strong>
            <span>用于客户发送语音时转写文本；默认复用文本回复的 Base URL / API Key</span>
          </div>
          <div class="form-grid">
            <label>
              <span>语音模式</span>
              <select v-model="form.ai.audioMode">
                <option value="auto">自动识别</option>
                <option value="openai_audio_chat">OpenAI 兼容音频聊天</option>
                <option value="dashscope_paraformer">百炼 Paraformer</option>
                <option value="local_openai_transcription">本地 OpenAI 转写</option>
                <option value="custom_http">自定义 HTTP</option>
              </select>
            </label>
            <label>
              <span>语音模型</span>
              <input v-model="form.ai.audioModel" placeholder="qwen3-asr-flash">
            </label>
          </div>
          <button type="button" class="advanced-toggle" @click="showAudioAdvanced = !showAudioAdvanced">
            {{ showAudioAdvanced ? '收起高级设置' : '高级设置' }}
          </button>
          <div v-if="showAudioAdvanced" class="form-grid advanced-grid">
            <label>
              <span>语音 Base URL</span>
              <input v-model="form.ai.audioBaseUrl" placeholder="留空复用主 Base URL">
            </label>
            <label>
              <span>语音 API Key</span>
              <input v-model="form.ai.audioApiKey" type="password" placeholder="留空复用主 API Key">
            </label>
            <div v-if="audioApiKeyLooksLikeUrl" class="field-warning">语音 API Key 当前看起来是 URL，保存时会清空并复用主 API Key。</div>
          </div>
        </div>
      </div>
      <div class="panel-actions">
        <button class="ghost-btn" @click="saveConfig('ai')" :disabled="busy">保存配置</button>
        <button class="ghost-btn" @click="testAI" :disabled="busy">测试 AI</button>
      </div>
    </section>

    <section id="auto-section-knowledge" class="auto-section">
      <div class="section-header">
        <div class="section-title">
          <span class="section-index">03</span>
          <h3>知识库</h3>
        </div>
        <div class="section-meta">
          <span>{{ status.knowledgeFileCount || 0 }} 个文件</span>
          <span>{{ status.knowledgeChunkCount || 0 }} 个片段</span>
        </div>
      </div>
      <div v-if="sectionAlerts('knowledge').length" class="section-alerts">
        <div v-for="alert in sectionAlerts('knowledge')" :key="alert" class="section-alert">{{ alert }}</div>
      </div>
      <div class="form-grid">
        <label class="wide">
          <span>目录</span>
          <input v-model="form.knowledge.directory" placeholder="config/knowledge">
        </label>
        <label>
          <span>TopK</span>
          <input type="number" v-model.number="form.knowledge.topK" min="1">
        </label>
        <label>
          <span>最低分数</span>
          <input type="number" step="0.05" v-model.number="form.knowledge.minScore" min="0" max="1">
        </label>
        <label>
          <span>检索模式</span>
          <select v-model="form.retrieval.retrievalMode">
            <option value="hybrid_rerank">混合检索 + 重排序</option>
            <option value="keyword">仅关键词检索</option>
          </select>
        </label>
        <label>
          <span>Embedding 模型</span>
          <input v-model="form.retrieval.embeddingModel" placeholder="text-embedding-v4">
          <small style="color: #666; font-size: 12px; margin-top: 4px; display: block;">
            用于文本向量化，例如：text-embedding-v4, text-embedding-v3
          </small>
        </label>
        <label>
          <span>Embedding 维度</span>
          <input type="number" v-model.number="form.retrieval.embeddingDimensions" min="128">
        </label>
        <label>
          <span>Rerank 模型</span>
          <input v-model="form.retrieval.rerankModel" placeholder="qwen3-rerank">
          <small style="color: #666; font-size: 12px; margin-top: 4px; display: block;">
            用于结果重排序，例如：qwen3-rerank, gte-rerank-v2
          </small>
        </label>
        <label>
          <span>召回 TopK</span>
          <input type="number" v-model.number="form.retrieval.recallTopK" min="1">
        </label>
        <label>
          <span>重排 TopK</span>
          <input type="number" v-model.number="form.retrieval.rerankTopK" min="1">
        </label>
        <label>
          <span>最终 TopK</span>
          <input type="number" v-model.number="form.retrieval.finalTopK" min="1">
        </label>
      </div>
      <div class="form-grid">
        <label class="check-row">
          <input type="checkbox" v-model="form.materials.autoSendEnabled">
          <span>命中素材后直接发送图片/视频/文件</span>
        </label>
        <label>
          <span>素材目录</span>
          <input v-model="form.materials.directory" placeholder="config/materials">
        </label>
        <label>
          <span>素材索引</span>
          <input v-model="form.materials.indexPath" placeholder="config/materials/materials.json">
        </label>
        <label>
          <span>每次最多发送</span>
          <input type="number" v-model.number="form.materials.maxPerReply" min="1" max="5">
        </label>
        <label>
          <span>回复风格</span>
          <select v-model="form.replyStyle">
            <option value="natural_professional">自然专业</option>
            <option value="concise_direct">简洁直接</option>
            <option value="warm_service">热情服务</option>
          </select>
        </label>
      </div>
      <div class="tag-row">
        <span v-for="ext in form.knowledge.supportedExtensions" :key="ext">{{ ext }}</span>
        <span>检索：{{ status.retrievalMode || form.retrieval.retrievalMode }}</span>
        <span>Embedding：{{ status.embeddingModel || form.retrieval.embeddingModel }}</span>
      </div>
      <div class="panel-actions">
        <button class="ghost-btn" @click="saveConfig('knowledge')" :disabled="busy">保存配置</button>
        <button class="ghost-btn" @click="syncMaterials" :disabled="busy">同步素材库</button>
        <button class="ghost-btn" @click="rebuildKnowledge" :disabled="busy">重建索引</button>
      </div>
      <div v-if="status.knowledgeFailedFiles && status.knowledgeFailedFiles.length" class="failed-list">
        <div v-for="item in status.knowledgeFailedFiles" :key="item">{{ item }}</div>
      </div>
    </section>

    <section id="auto-section-handoff" class="auto-section">
      <div class="section-header">
        <div class="section-title">
          <span class="section-index">04</span>
          <h3>人工接管</h3>
        </div>
        <div class="section-meta">
          <span>{{ form.handoff.humanUserId ? '已配置同事' : '未配置同事' }}</span>
          <span>{{ form.handoff.sendHumanCardToCustomer ? '客户转人工时发名片' : '不发名片' }}</span>
        </div>
      </div>
      <div v-if="sectionAlerts('handoff').length" class="section-alerts">
        <div v-for="alert in sectionAlerts('handoff')" :key="alert" class="section-alert">{{ alert }}</div>
      </div>
      <div class="form-grid">
        <label>
          <span>人工同事</span>
          <div class="handoff-human-combobox">
            <input
              v-model="handoffHumanQuery"
              class="handoff-human-search"
              placeholder="输入姓名、拼音、userID 模糊搜索"
              @focus="startHandoffHumanEditing"
              @input="handleHandoffHumanInput"
              @keydown.enter.prevent="previewFirstHandoffHuman"
              @keydown.esc.prevent="closeHandoffHumanDropdown"
            >
            <div v-if="handoffHumanDropdownOpen" class="handoff-human-dropdown">
              <button
                v-for="item in handoffHumanOptions"
                :key="item.userId"
                type="button"
                class="handoff-human-option"
                :class="{ selected: pendingHandoffHumanId === item.userId }"
                @mousedown.prevent="previewHandoffHuman(item)"
              >
                <strong>{{ identityOptionName(item) }}</strong>
                <span>{{ item.userId }}</span>
                <small>{{ identitySourceLabel(item.source) }}</small>
              </button>
              <div v-if="handoffHumanOptions.length === 0" class="handoff-human-empty">
                {{ handoffHumanSelectHint }}
              </div>
            </div>
          </div>
          <div class="select-action-row">
            <button type="button" class="mini-btn" @click="confirmHandoffHuman" :disabled="!canConfirmHandoffHuman">确认选择</button>
            <button type="button" class="mini-btn" @click="clearHandoffHuman">清空</button>
          </div>
          <small class="field-hint">{{ selectedHandoffHumanSummary }}</small>
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.handoff.includeKnowledgeHits">
          <span>附带知识库候选</span>
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.handoff.sendHumanCardToCustomer">
          <span>客户要求人工时发送人工名片给客户</span>
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.handoff.sendCustomerCardToHuman">
          <span>同时发送客户名片给人工</span>
        </label>
        <label>
          <span>转人工关键词</span>
          <input v-model="manualTriggerKeywordsText" placeholder="人工、客服、转人工、真人">
        </label>
        <label class="wide">
          <span>客户转人工说明</span>
          <input v-model="form.handoff.customerHandoffNotice">
        </label>
        <label class="wide">
          <span>通知模板</span>
          <textarea v-model="form.handoff.messageTemplate" rows="8" :placeholder="defaultHandoffTemplateHint"></textarea>
          <small class="field-hint">留空时会按私聊/群聊自动使用上面的默认模板；填写后则使用你自定义的模板。</small>
        </label>
      </div>
      <div class="panel-actions">
        <button class="ghost-btn" @click="saveConfig('handoff')" :disabled="busy">保存配置</button>
        <button class="ghost-btn" @click="testHandoff" :disabled="busy">测试私信</button>
      </div>
    </section>

    <section id="auto-section-identity" class="auto-section">
      <div class="section-header">
        <div class="section-title">
          <span class="section-index">05</span>
          <h3>身份识别</h3>
        </div>
        <div class="section-meta">
          <span>内部 {{ status.internalContactCount || 0 }}</span>
          <span>外部 {{ status.externalContactCount || 0 }}</span>
          <span>缓存 {{ status.identityInitializing ? '初始化中' : (status.identityRefreshing ? '刷新中' : formatUnixTime(status.identityLastRefreshAt)) }}</span>
        </div>
      </div>
      <div v-if="sectionAlerts('identity').length" class="section-alerts">
        <div v-for="alert in sectionAlerts('identity')" :key="alert" class="section-alert">{{ alert }}</div>
      </div>
      <div class="form-grid">
        <label>
          <span>未知身份处理</span>
          <select v-model="form.identity.unknownPolicy">
            <option value="customer">按客户处理</option>
            <option value="ignore">忽略未知身份</option>
            <option value="internal">按内部员工处理</option>
          </select>
        </label>
        <label>
          <span>未知身份转人工</span>
          <select v-model="form.identity.unknownHandoffPolicy">
            <option value="hold">先拦截并核验</option>
            <option value="allow">允许转人工</option>
          </select>
        </label>
        <label>
          <span>刷新间隔分钟</span>
          <input type="number" v-model.number="form.identity.refreshIntervalMinutes" min="5">
        </label>
        <label>
          <span>每页数量</span>
          <input type="number" v-model.number="form.identity.pageSize" min="20" max="500">
        </label>
        <label class="check-row">
          <input type="checkbox" v-model="form.identity.refreshOnStart">
          <span>启动时刷新内外部联系人</span>
        </label>
        <label class="wide">
          <span>内部员工拦截回复</span>
          <input v-model="form.identity.internalNoHandoffReply">
        </label>
        <label class="wide">
          <span>未知身份转人工拦截回复</span>
          <input v-model="form.identity.unknownNoHandoffReply">
        </label>
        <div class="identity-editor wide">
          <div class="identity-editor-header">
            <strong>内部成员同步群</strong>
            <span>选择企业总群后，群成员会作为内部员工自动同步</span>
          </div>
          <div class="identity-picker-row">
            <input v-model="internalGroupQuery" placeholder="搜索总群名称或 conversationId">
            <select v-model="selectedInternalGroupId" :disabled="internalGroupFilteredOptions.length === 0">
              <option value="">{{ groupSelectHint() }}</option>
              <option v-for="item in internalGroupFilteredOptions" :key="item.conversationId" :value="item.conversationId">
                {{ groupOptionLabel(item) }}
              </option>
            </select>
            <button type="button" class="ghost-btn" @click="addSelectedInternalGroup" :disabled="!canAddInternalGroup">添加</button>
          </div>
          <div v-if="groupOptions.length === 0" class="field-hint">先点击“刷新群列表”，再选择企业总群；未选择总群时只同步企微联系人列表。</div>
          <div v-if="internalSelectedGroups.length" class="identity-selected-list">
            <div v-for="item in internalSelectedGroups" :key="item.conversationId" class="identity-selected-row group-selected-row">
              <strong>{{ item.name || '未命名群聊' }}</strong>
              <span class="identity-user-id">{{ item.conversationId }}</span>
              <span>{{ item.source || '群列表' }}</span>
              <span></span>
              <button type="button" class="ghost-btn small-btn" @click="removeInternalGroup(item.conversationId)">删除</button>
            </div>
          </div>
          <div v-else class="identity-empty">暂无内部成员同步群</div>
        </div>
        <div class="identity-sync-grid wide">
          <div class="identity-sync-panel">
            <div class="identity-editor-header">
              <strong>内部成员</strong>
              <span>{{ identityOptions.internal.length }} 人，来自企微通讯录和内部成员同步群</span>
            </div>
            <input v-model="syncedInternalQuery" class="identity-search-input" placeholder="搜索内部成员名称或 userID">
            <div v-if="internalSyncedContacts.length" class="identity-sync-list">
              <div v-for="item in internalSyncedContacts" :key="item.userId" class="identity-sync-row">
                <strong>{{ item.name || '未命名联系人' }}</strong>
                <span class="identity-user-id">{{ item.userId }}</span>
                <span>{{ identitySourceLabel(item.source) }}</span>
                <span>{{ formatUnixTime(item.lastSeenAt) }}</span>
              </div>
            </div>
            <div v-else class="identity-empty">暂无同步到的内部成员</div>
          </div>
          <div class="identity-sync-panel">
            <div class="identity-editor-header">
              <strong>外部客户</strong>
              <span>{{ identityOptions.external.length }} 人 / {{ externalCustomerGroups.length }} 个账号分组</span>
            </div>
            <input v-model="syncedExternalQuery" class="identity-search-input" placeholder="搜索外部客户名称或 userID">
            <div v-if="externalCustomerGroups.length" class="identity-sync-list external-group-list">
              <details v-for="group in externalCustomerGroups" :key="group.key" class="identity-external-group" open>
                <summary class="identity-external-summary">
                  <strong>{{ group.label }}</strong>
                  <span>{{ group.count }} 个客户</span>
                </summary>
                <div v-for="item in group.items" :key="externalContactRowKey(item)" class="identity-sync-row">
                  <strong>{{ item.name || '未命名联系人' }}</strong>
                  <span class="identity-user-id">{{ item.userId }}</span>
                  <span>{{ identitySourceLabel(item.source) }}</span>
                  <span>{{ formatUnixTime(item.lastSeenAt) }}</span>
                </div>
              </details>
            </div>
            <div v-else class="identity-empty">暂无同步到的外部客户</div>
          </div>
        </div>
        <details class="identity-advanced wide">
          <summary>高级补充：手动指定联系人 userID</summary>
          <div class="identity-advanced-body">
            <div class="identity-editor">
              <div class="identity-editor-header">
                <strong>手动内部员工</strong>
                <span>仅用于企微接口暂未同步到的特殊联系人</span>
              </div>
              <div class="identity-picker-row">
                <input v-model="internalIdentityQuery" placeholder="搜索内部员工名称或 userID">
                <select v-model="selectedInternalUserId" :disabled="internalFilteredOptions.length === 0">
                  <option value="">{{ identitySelectHint('internal') }}</option>
                  <option v-for="item in internalFilteredOptions" :key="item.userId" :value="item.userId">
                    {{ identityOptionLabel(item) }}
                  </option>
                </select>
                <button type="button" class="ghost-btn" @click="addSelectedIdentity('internal')" :disabled="!canAddIdentity('internal')">添加</button>
              </div>
              <div v-if="internalSelectedContacts.length" class="identity-selected-list">
                <div v-for="item in internalSelectedContacts" :key="item.userId" class="identity-selected-row">
                  <strong>{{ item.name || '未填写名称' }}</strong>
                  <span class="identity-user-id">{{ item.userId }}</span>
                  <span>{{ identitySourceLabel(item.source) }}</span>
                  <input
                    v-if="item.needsLabel"
                    :value="identityManualLabelDraft('internal', item.userId)"
                    placeholder="补充名称"
                    @input="setIdentityLabelDraft('internal', item.userId, $event.target.value)"
                    @blur="commitIdentityLabelDraft('internal', item.userId)"
                    @keydown.enter.prevent="commitIdentityLabelDraft('internal', item.userId)"
                  >
                  <span v-else></span>
                  <button type="button" class="ghost-btn small-btn" @click="removeIdentity('internal', item.userId)">删除</button>
                </div>
              </div>
              <div v-else class="identity-empty">暂无手动内部员工</div>
            </div>
            <div class="identity-editor">
              <div class="identity-editor-header">
                <strong>手动外部客户</strong>
                <span>仅用于企微接口暂未同步到的特殊联系人</span>
              </div>
              <div class="identity-picker-row">
                <input v-model="externalIdentityQuery" placeholder="搜索外部客户名称或 userID">
                <select v-model="selectedExternalUserId" :disabled="externalFilteredOptions.length === 0">
                  <option value="">{{ identitySelectHint('external') }}</option>
                  <option v-for="item in externalFilteredOptions" :key="item.userId" :value="item.userId">
                    {{ identityOptionLabel(item) }}
                  </option>
                </select>
                <button type="button" class="ghost-btn" @click="addSelectedIdentity('external')" :disabled="!canAddIdentity('external')">添加</button>
              </div>
              <div v-if="externalSelectedContacts.length" class="identity-selected-list">
                <div v-for="item in externalSelectedContacts" :key="item.userId" class="identity-selected-row">
                  <strong>{{ item.name || '未填写名称' }}</strong>
                  <span class="identity-user-id">{{ item.userId }}</span>
                  <span>{{ identitySourceLabel(item.source) }}</span>
                  <input
                    v-if="item.needsLabel"
                    :value="identityManualLabelDraft('external', item.userId)"
                    placeholder="补充名称"
                    @input="setIdentityLabelDraft('external', item.userId, $event.target.value)"
                    @blur="commitIdentityLabelDraft('external', item.userId)"
                    @keydown.enter.prevent="commitIdentityLabelDraft('external', item.userId)"
                  >
                  <span v-else></span>
                  <button type="button" class="ghost-btn small-btn" @click="removeIdentity('external', item.userId)">删除</button>
                </div>
              </div>
              <div v-else class="identity-empty">暂无手动外部客户</div>
            </div>
            <label>
              <span>批量粘贴内部员工 ID</span>
              <textarea v-model="internalUserIdsText" rows="3" placeholder="每行一个 user_id，例如 1688855899845302"></textarea>
            </label>
            <label>
              <span>批量粘贴外部客户 ID</span>
              <textarea v-model="externalUserIdsText" rows="3" placeholder="每行一个 user_id"></textarea>
            </label>
          </div>
        </details>
      </div>
      <div class="tag-row">
        <span>内部：{{ status.internalContactCount || 0 }}</span>
        <span>外部：{{ status.externalContactCount || 0 }}</span>
        <span>群列表：{{ status.identityGroupOptionCount || groupOptions.length || 0 }}</span>
        <span>同步群成员：{{ status.internalGroupMemberLastSyncCount || 0 }}</span>
        <span>同步群刷新：{{ formatUnixTime(status.internalGroupMemberLastSyncAt) }}</span>
        <span>上次刷新：{{ formatUnixTime(status.identityLastRefreshAt) }}</span>
        <span>最近响应：{{ status.identityLastResponseType || '-' }} / {{ status.identityLastResponseCount || 0 }}</span>
        <span>查询中：{{ status.identityLookupInFlight || 0 }}</span>
        <span>账号作用域：{{ status.identityScope || '-' }}</span>
      </div>
      <div class="panel-actions">
        <button class="ghost-btn" @click="saveConfig('identity')" :disabled="busy">保存配置</button>
        <button class="ghost-btn" @click="refreshContacts" :disabled="busy || status.identityRefreshing">刷新联系人</button>
        <button class="ghost-btn" @click="refreshGroups" :disabled="busy">刷新群列表</button>
        <button class="ghost-btn" @click="syncInternalGroups" :disabled="busy || status.identityRefreshing || !form.identity.internalGroupConversationIds.length">同步内部成员群</button>
      </div>
    </section>

    <section id="auto-section-records" class="auto-section">
      <div class="section-header">
        <div class="section-title">
          <span class="section-index">06</span>
          <h3>最近处理</h3>
        </div>
        <div class="section-meta">
          <span>{{ (status.lastMessages || []).length }} 条记录</span>
          <span>{{ recordPageSummary }}</span>
          <span>今日回复 {{ status.todayReplied || 0 }}</span>
        </div>
      </div>
      <div v-if="sectionAlerts('records').length" class="section-alerts">
        <div v-for="alert in sectionAlerts('records')" :key="alert" class="section-alert">{{ alert }}</div>
      </div>
      <div class="record-table">
        <div class="record-row header">
          <span>时间</span>
          <span>动作</span>
          <span>来源</span>
          <span>企微</span>
          <span>客户</span>
          <span>身份</span>
          <span>问题</span>
          <span>原因</span>
          <span>名片</span>
          <span>AI 耗时</span>
          <span>检索分</span>
          <span>来源</span>
        </div>
        <div v-for="record in pagedRecords" :key="record.id" class="record-row">
          <span>{{ record.time }}</span>
          <span>{{ actionLabel(record.action) }}</span>
          <span>{{ record.source }}</span>
          <span class="truncate">{{ formatRecordClient(record) }}</span>
          <span>{{ record.fromNickName || record.fromWxId }}</span>
          <span>{{ identityLabel(record.senderIdentity, record.identitySource) }}</span>
          <span class="truncate">{{ record.question }}</span>
          <span class="truncate">{{ record.reason }}</span>
          <span class="truncate">{{ record.cardStatus || '-' }}</span>
          <span>{{ formatDuration(record.aiDurationMs) }}</span>
          <span>{{ formatScores(record) }}</span>
          <span class="truncate">{{ record.usedKnowledgeSources || '-' }}</span>
        </div>
        <div v-if="!status.lastMessages || status.lastMessages.length === 0" class="empty">暂无处理记录</div>
      </div>
      <div v-if="recordTotalCount > 0" class="record-pagination">
        <button class="ghost-btn small-btn" type="button" @click="goRecordPage(1)" :disabled="recordCurrentPage <= 1">首页</button>
        <button class="ghost-btn small-btn" type="button" @click="prevRecordPage" :disabled="recordCurrentPage <= 1">上一页</button>
        <span>{{ recordPageSummary }}，显示 {{ recordStartIndex }}-{{ recordEndIndex }} / {{ recordTotalCount }}</span>
        <button class="ghost-btn small-btn" type="button" @click="nextRecordPage" :disabled="recordCurrentPage >= recordTotalPages">下一页</button>
        <button class="ghost-btn small-btn" type="button" @click="goRecordPage(recordTotalPages)" :disabled="recordCurrentPage >= recordTotalPages">末页</button>
      </div>
    </section>
  </div>
</template>

<script setup>
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import {
  GetAutoReplyConfig,
  SaveAutoReplyConfig,
  SetAutoReplyEnabled,
  GetAutoReplyStatus,
  RebuildKnowledgeIndex,
  SyncAutoReplyMaterials,
  RefreshAutoReplyContacts,
  GetAutoReplyIdentityOptions,
  RefreshAutoReplyGroups,
  GetAutoReplyGroupOptions,
  SyncAutoReplyInternalGroups,
  TestAIConnection,
  TestHumanHandoff,
  SendWxWorkData,
  LogFrontend
} from '../../wailsjs/go/main/App.js'

const busy = ref(false)
const message = ref('')
const messageType = ref('success')
const scopedMessages = ref({})
const status = ref({})
const showVisionAdvanced = ref(false)
const showAudioAdvanced = ref(false)
const identityOptions = ref({ internal: [], external: [] })
const groupOptions = ref([])
const internalIdentityQuery = ref('')
const externalIdentityQuery = ref('')
const syncedInternalQuery = ref('')
const syncedExternalQuery = ref('')
const internalGroupQuery = ref('')
const selectedInternalUserId = ref('')
const selectedExternalUserId = ref('')
const selectedInternalGroupId = ref('')
const handoffHumanQuery = ref('')
const handoffHumanDropdownOpen = ref(false)
const pendingHandoffHumanId = ref('')
const handoffHumanEditing = ref(false)
const identityLabelDrafts = reactive({ internal: {}, external: {} })
const recordCurrentPage = ref(1)
const recordPageSize = 20
const defaultVisionModel = 'qwen3-vl-plus'
let identityOptionsKey = ''
let currentIdentityScopeKey = ''
let timer = null

const form = reactive(defaultConfig())
const sectionNavItems = [
  { id: 'listen', label: '监听策略' },
  { id: 'ai', label: 'AI 配置' },
  { id: 'knowledge', label: '知识库' },
  { id: 'handoff', label: '人工接管' },
  { id: 'identity', label: '身份识别' },
  { id: 'records', label: '最近处理' }
]
const sectionScopes = new Set(sectionNavItems.map(item => item.id))
const scopedMessageTimers = {}
const defaultHandoffTemplateHint = `客户问题待处理

客户：{{customerName}}
客户ID：{{fromWxId}}
来源：私聊
时间：{{messageTime}}
问题：{{question}}
原因：{{reason}}
会话ID：{{conversationId}}

群聊问题待处理

群聊：{{groupName}}
客户：{{customerName}}
客户ID：{{fromWxId}}
来源：群聊
时间：{{messageTime}}
问题：{{question}}
原因：{{reason}}
会话ID：{{conversationId}}`
const reasonEntries = computed(() => {
  return Object.entries(status.value.reasonCounts || {})
    .filter(([, count]) => Number(count) > 0)
    .map(([reason, count]) => ({ reason, count }))
    .slice(0, 8)
})
const recordTotalCount = computed(() => (status.value.lastMessages || []).length)
const recordTotalPages = computed(() => Math.max(1, Math.ceil(recordTotalCount.value / recordPageSize)))
const recordStartIndex = computed(() => {
  if (recordTotalCount.value <= 0) return 0
  return (recordCurrentPage.value - 1) * recordPageSize + 1
})
const recordEndIndex = computed(() => Math.min(recordCurrentPage.value * recordPageSize, recordTotalCount.value))
const recordPageSummary = computed(() => {
  if (recordTotalCount.value <= 0) return '第 0 / 0 页'
  return `第 ${recordCurrentPage.value} / ${recordTotalPages.value} 页`
})
const pagedRecords = computed(() => {
  const records = status.value.lastMessages || []
  const start = (recordCurrentPage.value - 1) * recordPageSize
  return records.slice(start, start + recordPageSize)
})
const manualTriggerKeywordsText = computed({
  get() {
    return (form.handoff.manualTriggerKeywords || []).join('、')
  },
  set(value) {
    form.handoff.manualTriggerKeywords = splitKeywordText(value)
  }
})
const internalFilteredOptions = computed(() => {
  return filterIdentityOptions(identityOptions.value.internal || [], internalIdentityQuery.value, form.identity.internalUserIds || [])
})
const externalFilteredOptions = computed(() => {
  return filterIdentityOptions(identityOptions.value.external || [], externalIdentityQuery.value, form.identity.externalUserIds || [])
})
const internalGroupFilteredOptions = computed(() => {
  return filterGroupOptions(groupOptions.value || [], internalGroupQuery.value, form.identity.internalGroupConversationIds || [])
})
const internalSyncedContacts = computed(() => {
  return filterSyncedIdentityOptions(identityOptions.value.internal || [], syncedInternalQuery.value)
})
const externalCustomerGroups = computed(() => {
  return groupExternalContacts(identityOptions.value.external || [], syncedExternalQuery.value)
})
const handoffHumanOptions = computed(() => {
  return filterHandoffHumanOptions(handoffHumanCandidateOptions.value, handoffHumanQuery.value)
})
const handoffHumanCandidateOptions = computed(() => {
  return mergeIdentityOptionLists([
    ...(identityOptions.value.internal || []),
    ...(identityOptions.value.observed || [])
  ])
})
const handoffHumanSelectHint = computed(() => {
  const allOptions = handoffHumanCandidateOptions.value
  if (!allOptions.length) return '请先刷新联系人'
  return handoffHumanOptions.value.length ? '选择同事' : '未同步到，请刷新联系人或同步内部群'
})
const pendingHandoffHuman = computed(() => findHandoffHumanOption(pendingHandoffHumanId.value))
const canConfirmHandoffHuman = computed(() => {
  const pendingId = String(pendingHandoffHumanId.value || '').trim()
  return pendingId && pendingId !== String(form.handoff.humanUserId || '').trim()
})
const selectedHandoffHumanSummary = computed(() => {
  const pendingId = String(pendingHandoffHumanId.value || '').trim()
  if (pendingId) {
    const option = pendingHandoffHuman.value
    const label = option ? identityOptionLabel(option) : pendingId
    return `待确认：${label}`
  }
  const userId = String(form.handoff.humanUserId || '').trim()
  if (!userId) return '选择后系统会自动保存同事账号，私聊会话由后台自动推导'
  const option = findHandoffHumanOption(userId)
  const name = String(option?.name || '').trim()
  return name ? `已选择：${name} / ${userId}` : `已选择：${userId}，后台会自动推导私聊会话`
})
const internalSelectedGroups = computed(() => selectedInternalGroups())
const internalSelectedContacts = computed(() => selectedIdentityContacts('internal'))
const externalSelectedContacts = computed(() => selectedIdentityContacts('external'))
const visionApiKeyLooksLikeUrl = computed(() => looksLikeUrl(form.ai?.visionApiKey || ''))
const audioApiKeyLooksLikeUrl = computed(() => looksLikeUrl(form.ai?.audioApiKey || ''))
const internalUserIdsText = computed({
  get() {
    return (form.identity.internalUserIds || []).join('\n')
  },
  set(value) {
    setIdentityIDs('internal', splitIdText(value))
  }
})
const externalUserIdsText = computed({
  get() {
    return (form.identity.externalUserIds || []).join('\n')
  },
  set(value) {
    setIdentityIDs('external', splitIdText(value))
  }
})

function knownSectionScope(scope) {
  const id = String(scope || '').trim()
  return sectionScopes.has(id) ? id : ''
}

function normalizeSectionScope(scope) {
  return knownSectionScope(scope) || 'records'
}

function textIncludesAny(text, keywords) {
  return keywords.some(keyword => text.includes(keyword))
}

function classifyErrorScope(text) {
  const raw = String(text || '').trim()
  if (!raw) return 'records'
  const lower = raw.toLowerCase()
  if (
    textIncludesAny(raw, ['联系人身份', '身份查询', '未知身份拦截回复失败', '内部员工拦截回复失败']) ||
    textIncludesAny(lower, ['identity'])
  ) {
    return 'identity'
  }
  if (textIncludesAny(raw, ['AI请求失败', 'AI 请求失败', 'AI 测试失败']) || textIncludesAny(lower, ['ai '])) {
    return 'ai'
  }
  if (textIncludesAny(raw, ['转人工发送失败', '测试私信失败', '人工名片', '客户名片', '客户说明', '人工客服'])) {
    return 'handoff'
  }
  if (
    textIncludesAny(raw, ['知识库', '知识索引', '向量召回', '重排序', '重建失败', '索引', '检索']) ||
    textIncludesAny(lower, ['embedding', 'rerank'])
  ) {
    return 'knowledge'
  }
  if (textIncludesAny(raw, ['开启失败', '关闭失败', '保存失败', '加载自动客服配置失败', '重载', '监听', '启动'])) {
    return 'listen'
  }
  return 'records'
}

function classifyStatusErrorScope(text, scope) {
  return knownSectionScope(scope) || classifyErrorScope(text)
}

function addUniqueAlert(alerts, text) {
  const normalized = String(text || '').trim()
  if (!normalized) return
  const index = alerts.findIndex(item => item === normalized || item.includes(normalized) || normalized.includes(item))
  if (index < 0) {
    alerts.push(normalized)
  } else if (normalized.length > alerts[index].length) {
    alerts[index] = normalized
  }
}

function sectionAlerts(sectionId) {
  const scope = normalizeSectionScope(sectionId)
  const alerts = []
  addUniqueAlert(alerts, scopedMessages.value[scope])
  if (scope === 'identity') {
    addUniqueAlert(alerts, status.value.identityRefreshError)
    addUniqueAlert(alerts, status.value.internalGroupMemberSyncError)
  }
  const lastError = String(status.value.lastError || '').trim()
  if (lastError && classifyStatusErrorScope(lastError, status.value.lastErrorScope) === scope) {
    addUniqueAlert(alerts, lastError)
  }
  return alerts
}

function setScopedMessage(scope, text) {
  const target = normalizeSectionScope(scope)
  const normalized = String(text || '').trim()
  if (!normalized) return
  scopedMessages.value = { ...scopedMessages.value, [target]: normalized }
  if (scopedMessageTimers[target]) {
    clearTimeout(scopedMessageTimers[target])
  }
  scopedMessageTimers[target] = setTimeout(() => {
    if (scopedMessages.value[target] !== normalized) return
    const next = { ...scopedMessages.value }
    delete next[target]
    scopedMessages.value = next
    scopedMessageTimers[target] = null
  }, 5000)
}

function defaultConfig() {
  return {
    enabled: false,
    listen: {
      enablePrivateChat: true,
      enableGroupChat: true,
      groupTriggerMode: 'mention_only',
      ignoreSelfMessage: true,
      deduplicateSeconds: 300
    },
    knowledge: {
      directory: 'config/knowledge',
      indexPath: 'config/knowledge/index.json',
      supportedExtensions: ['.md', '.txt', '.csv', '.xlsx', '.docx', '.pdf'],
      topK: 8,
      minScore: 0.40,
      autoRebuildOnStart: false
    },
    retrieval: {
      retrievalMode: 'hybrid_rerank',
      embeddingIndexPath: 'config/knowledge/embedding_index.json',
      embeddingModel: 'text-embedding-v4',
      embeddingDimensions: 512,
      rerankModel: 'qwen3-rerank',
      recallTopK: 50,
      rerankTopK: 30,
      finalTopK: 8
    },
    materials: {
      directory: 'config/materials',
      indexPath: 'config/materials/materials.json',
      autoSendEnabled: true,
      maxPerReply: 2
    },
    collaboration: {
      enabled: false,
      humanWaitSeconds: 180,
      afterHumanReplyDelaySeconds: 3,
      takeoverIdleExitSeconds: 300,
      supplementTarget: 'customer',
      engineerReturnPolicy: 'review'
    },
    replyStyle: 'natural_professional',
    ai: {
      provider: 'openai_compatible',
      baseUrl: '',
      apiKey: '',
      model: 'qwen-turbo',
      systemPrompt: '你是一名企业微信智能客服。',
      visionModel: defaultVisionModel,
      visionBaseUrl: '',
      visionApiKey: '',
      audioProvider: 'auto',
      audioMode: 'openai_audio_chat',
      audioModel: 'qwen3-asr-flash',
      audioBaseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
      audioApiKey: '',
      timeoutSeconds: 20,
      enableThinking: false,
      replyDetail: 'detailed',
      temperature: 0,
      maxTokens: 700
    },
    handoff: {
      humanUserId: '',
      humanConversationId: '',
      messageTemplate: '',
      customerHandoffNotice: '已为您通知人工客服添加您的好友，请稍等。若 2 分钟内仍未收到好友申请，请点击上方名片主动添加人工客服。',
      includeKnowledgeHits: true,
      sendHumanCardToCustomer: true,
      sendCustomerCardToHuman: true,
      cardTriggerMode: 'manual_keywords',
      manualTriggerKeywords: ['人工', '客服', '转人工', '人工客服', '真人', '真人客服'],
      cardKeywords: ['人工', '客服', '转人工', '人工客服', '真人', '真人客服']
    },
    identity: {
      unknownPolicy: 'customer',
      unknownHandoffPolicy: 'hold',
      refreshOnStart: true,
      refreshIntervalMinutes: 30,
      pageSize: 200,
      internalNoHandoffReply: '内部员工消息不触发转人工，如需协助请直接联系对应同事。',
      unknownNoHandoffReply: '正在核验联系人身份，暂不触发转人工。如需协助请直接联系对应同事。',
      internalUserIds: [],
      externalUserIds: [],
      internalGroupConversationIds: [],
      internalGroupConversationIdsByScope: {},
      internalUserLabels: {},
      externalUserLabels: {}
    },
    replyPolicy: {
      unknownAnswerToken: 'NO_ANSWER',
      maxQuestionLength: 1000,
      cooldownSeconds: 3,
      sensitiveKeywords: ['人工', '客服', '投诉', '退款', '退货', '合同', '发票', '赔偿', '价格审批']
    }
  }
}

function splitIdText(value) {
  return String(value || '')
    .split(/[,\n，、;\s]+/)
    .map(item => item.trim())
    .filter(Boolean)
}

function splitKeywordText(value) {
  return String(value || '')
    .split(/[,\n，、;\s]+/)
    .map(item => item.trim())
    .filter(Boolean)
}

function uniqueIds(items) {
  const seen = new Set()
  const result = []
  for (const item of items || []) {
    const id = String(item || '').trim()
    if (!id || seen.has(id)) continue
    seen.add(id)
    result.push(id)
  }
  return result
}

function robotUserIds() {
  return uniqueIds(status.value?.robotUserIds || [])
}

function isRobotUserId(userId) {
  const id = String(userId || '').trim()
  return Boolean(id && robotUserIds().includes(id))
}

function uniqueNonRobotIds(items) {
  return uniqueIds(items).filter(id => !isRobotUserId(id))
}

function sanitizeConfiguredIdentityIDs() {
  form.identity.internalUserIds = uniqueNonRobotIds(form.identity.internalUserIds || [])
  form.identity.externalUserIds = uniqueNonRobotIds(form.identity.externalUserIds || [])
  for (const labels of [form.identity.internalUserLabels, form.identity.externalUserLabels]) {
    if (!labels) continue
    for (const userId of robotUserIds()) {
      delete labels[userId]
    }
  }
}

function internalGroupIDs() {
  return form.identity.internalGroupConversationIds || []
}

function setInternalGroupIDs(ids) {
  const next = uniqueIds(ids)
  form.identity.internalGroupConversationIds = next
  const scope = activeIdentityScope()
  if (scope) {
    if (!form.identity.internalGroupConversationIdsByScope || Array.isArray(form.identity.internalGroupConversationIdsByScope)) {
      form.identity.internalGroupConversationIdsByScope = {}
    }
    form.identity.internalGroupConversationIdsByScope[scope] = next
  }
}

function activeIdentityScope() {
  return String(status.value?.identityScope || '').trim()
}

function syncInternalGroupsForCurrentScope() {
  const scope = activeIdentityScope()
  if (!scope || currentIdentityScopeKey === scope) return
  if (!form.identity.internalGroupConversationIdsByScope || Array.isArray(form.identity.internalGroupConversationIdsByScope)) {
    form.identity.internalGroupConversationIdsByScope = {}
  }
  const scoped = uniqueIds(form.identity.internalGroupConversationIdsByScope[scope] || [])
  const legacy = uniqueIds(form.identity.internalGroupConversationIds || [])
  if (!scoped.length && legacy.length && !form.identity.internalGroupConversationIdsByScope[scope]) {
    form.identity.internalGroupConversationIdsByScope[scope] = legacy
    form.identity.internalGroupConversationIds = legacy
  } else {
    form.identity.internalGroupConversationIds = scoped
  }
  currentIdentityScopeKey = scope
}

function commitInternalGroupsForCurrentScope() {
  const scope = activeIdentityScope()
  if (!scope) return
  if (!form.identity.internalGroupConversationIdsByScope || Array.isArray(form.identity.internalGroupConversationIdsByScope)) {
    form.identity.internalGroupConversationIdsByScope = {}
  }
  form.identity.internalGroupConversationIdsByScope[scope] = uniqueIds(form.identity.internalGroupConversationIds || [])
}

function identityIDs(kind) {
  return kind === 'internal' ? (form.identity.internalUserIds || []) : (form.identity.externalUserIds || [])
}

function identityLabels(kind) {
  ensureIdentityLabelMaps()
  return kind === 'internal' ? form.identity.internalUserLabels : form.identity.externalUserLabels
}

function identityDraftMap(kind) {
  return kind === 'internal' ? identityLabelDrafts.internal : identityLabelDrafts.external
}

function hasIdentityLabelDraft(kind, userId) {
  return Object.prototype.hasOwnProperty.call(identityDraftMap(kind), userId)
}

function identityOptionsFor(kind) {
  return kind === 'internal' ? (identityOptions.value.internal || []) : (identityOptions.value.external || [])
}

function ensureIdentityLabelMaps() {
  if (!form.identity.internalUserLabels || Array.isArray(form.identity.internalUserLabels)) {
    form.identity.internalUserLabels = {}
  }
  if (!form.identity.externalUserLabels || Array.isArray(form.identity.externalUserLabels)) {
    form.identity.externalUserLabels = {}
  }
}

function findIdentityOption(kind, userId) {
  return identityOptionsFor(kind).find(item => String(item.userId || '').trim() === userId)
}

function findHandoffHumanOption(userId) {
  const id = String(userId || '').trim()
  if (!id) return null
  return handoffHumanCandidateOptions.value.find(item => String(item.userId || '').trim() === id) || null
}

function syncHandoffHumanQueryFromSelection(force = false) {
  if (!force && (handoffHumanEditing.value || pendingHandoffHumanId.value)) {
    return
  }
  const userId = String(form.handoff?.humanUserId || '').trim()
  if (!userId) {
    handoffHumanQuery.value = ''
    return
  }
  const option = findHandoffHumanOption(userId)
  handoffHumanQuery.value = option ? identityOptionLabel(option) : userId
}

function identityQuery(kind) {
  return String(kind === 'internal' ? internalIdentityQuery.value : externalIdentityQuery.value || '').trim()
}

function selectedIdentityId(kind) {
  return String(kind === 'internal' ? selectedInternalUserId.value : selectedExternalUserId.value || '').trim()
}

function filteredIdentityOptions(kind) {
  return kind === 'internal' ? internalFilteredOptions.value : externalFilteredOptions.value
}

function pendingIdentityOption(kind) {
  const selectedId = selectedIdentityId(kind)
  if (selectedId) return findIdentityOption(kind, selectedId)

  const query = identityQuery(kind)
  if (!query) return null

  const queryLower = query.toLowerCase()
  const exact = identityOptionsFor(kind).find(item => {
    const userId = String(item.userId || '').trim()
    const name = String(item.name || '').trim()
    return userId.toLowerCase() === queryLower || name.toLowerCase() === queryLower
  })
  if (exact) return exact

  const matches = filteredIdentityOptions(kind)
  return matches.length === 1 ? matches[0] : null
}

function pendingIdentityId(kind) {
  const selectedId = selectedIdentityId(kind)
  if (selectedId) return selectedId
  const option = pendingIdentityOption(kind)
  return String(option?.userId || identityQuery(kind)).trim()
}

function canAddIdentity(kind) {
  const userId = pendingIdentityId(kind)
  return Boolean(userId && !identityIDs(kind).includes(userId))
}

function clearIdentityPicker(kind) {
  if (kind === 'internal') {
    selectedInternalUserId.value = ''
    internalIdentityQuery.value = ''
  } else {
    selectedExternalUserId.value = ''
    externalIdentityQuery.value = ''
  }
}

function setIdentityIDs(kind, ids) {
  const next = uniqueNonRobotIds(ids)
  if (kind === 'internal') {
    form.identity.internalUserIds = next
  } else {
    form.identity.externalUserIds = next
  }
  syncIdentityLabels(kind)
}

function syncIdentityLabels(kind) {
  ensureIdentityLabelMaps()
  const ids = new Set(identityIDs(kind))
  const current = identityLabels(kind)
  const next = {}
  for (const userId of ids) {
    const cached = findIdentityOption(kind, userId)
    const savedLabel = String(current[userId] || '').trim()
    const cachedLabel = hasIdentityLabelDraft(kind, userId) ? '' : String(cached?.name || '').trim()
    const label = savedLabel || cachedLabel
    if (label) next[userId] = label
  }
  if (kind === 'internal') {
    form.identity.internalUserLabels = next
  } else {
    form.identity.externalUserLabels = next
  }
}

function syncAllIdentityLabels() {
  syncIdentityLabels('internal')
  syncIdentityLabels('external')
}

function filterIdentityOptions(options, query, selectedIds) {
  const selected = new Set(uniqueIds(selectedIds))
  const q = String(query || '').trim().toLowerCase()
  return (options || [])
    .filter(item => {
      const userId = String(item.userId || '').trim()
      if (!userId || selected.has(userId) || isRobotUserId(userId)) return false
      if (!q) return true
      const name = String(item.name || '').toLowerCase()
      return userId.toLowerCase().includes(q) || name.includes(q)
    })
    .slice(0, 80)
}

function filterHandoffHumanOptions(options, query) {
  const q = String(query || '').trim().toLowerCase()
  const selectedId = String(form.handoff?.humanUserId || '').trim()
  return (options || [])
    .map(item => ({ item, score: handoffHumanMatchScore(item, q, selectedId) }))
    .filter(entry => entry.score >= 0)
    .sort((a, b) => {
      if (a.score !== b.score) return b.score - a.score
      return identityOptionName(a.item).localeCompare(identityOptionName(b.item), 'zh-Hans-CN')
    })
    .map(entry => entry.item)
    .slice(0, 80)
}

function handoffHumanMatchScore(item, query, selectedId) {
      const userId = String(item.userId || '').trim()
  if (!userId || isRobotUserId(userId)) return -1
  let score = userId === selectedId ? 20 : 0
  if (!query) return score
      const name = String(item.name || '').toLowerCase()
      const source = String(item.source || '').toLowerCase()
      const accountName = String(item.sourceAccountName || '').toLowerCase()
      const accountUserId = String(item.sourceAccountUserId || '').toLowerCase()
      const clientId = String(item.clientId || '').toLowerCase()
  const userIdLower = userId.toLowerCase()
  const fields = [name, userIdLower, source, accountName, accountUserId, clientId].filter(Boolean)
  if (name === query || userIdLower === query) return score + 100
  if (fields.some(field => field.startsWith(query))) return score + 80
  if (fields.some(field => field.includes(query))) return score + 60
  if (fields.some(field => fuzzyIncludes(field, query))) return score + 35
  return -1
}

function fuzzyIncludes(text, query) {
  const target = String(text || '').toLowerCase()
  const pattern = String(query || '').toLowerCase()
  if (!target || !pattern) return false
  let pos = 0
  for (const ch of pattern) {
    pos = target.indexOf(ch, pos)
    if (pos < 0) return false
    pos += ch.length
  }
  return true
}

function mergeIdentityOptionLists(items) {
  const byUserId = new Map()
  for (const item of items || []) {
    const userId = String(item?.userId || '').trim()
    if (!userId || isRobotUserId(userId)) continue
    const current = byUserId.get(userId)
    if (!current || shouldReplaceIdentityOption(current, item)) {
      byUserId.set(userId, item)
    }
  }
  return [...byUserId.values()]
}

function shouldReplaceIdentityOption(current, next) {
  const currentName = String(current?.name || '').trim()
  const nextName = String(next?.name || '').trim()
  if (!currentName && nextName) return true
  const rank = source => ({
    internal_cache: 5,
    internal_group_member: 4,
    configured_human: 4,
    single_info: 3,
    observed_message: 2
  }[String(source || '').trim()] || 1)
  return rank(next?.source) > rank(current?.source)
}

function filterSyncedIdentityOptions(options, query) {
  const q = String(query || '').trim().toLowerCase()
  return (options || [])
    .filter(item => {
      const userId = String(item.userId || '').trim()
      if (!userId || isRobotUserId(userId)) return false
      if (!q) return true
      const name = String(item.name || '').toLowerCase()
      const source = String(item.source || '').toLowerCase()
      const accountName = String(item.sourceAccountName || '').toLowerCase()
      const accountUserId = String(item.sourceAccountUserId || '').toLowerCase()
      const clientId = String(item.clientId || '').toLowerCase()
      return userId.toLowerCase().includes(q) ||
        name.includes(q) ||
        source.includes(q) ||
        accountName.includes(q) ||
        accountUserId.includes(q) ||
        clientId.includes(q)
    })
    .slice(0, 200)
}

function groupExternalContacts(options, query) {
  const groups = new Map()
  for (const item of filterSyncedIdentityOptions(options, query)) {
    const key = sourceAccountKey(item)
    if (!groups.has(key)) {
      groups.set(key, { key, label: sourceAccountLabelFromItem(item), count: 0, items: [] })
    }
    const group = groups.get(key)
    group.items.push(item)
    group.count += 1
  }
  return [...groups.values()]
    .map(group => ({
      ...group,
      items: group.items.sort(compareIdentityOptions)
    }))
    .sort((a, b) => {
      if (b.count !== a.count) return b.count - a.count
      return a.label.localeCompare(b.label)
    })
}

function sourceAccountKey(item) {
  const userId = String(item?.sourceAccountUserId || '').trim()
  if (userId) return `user:${userId}`
  const clientId = Number(item?.clientId || 0)
  if (clientId) return `client:${clientId}`
  return 'unknown'
}

function sourceAccountLabelFromItem(item) {
  const name = String(item?.sourceAccountName || '').trim()
  if (name && !/^client\s+\d+$/i.test(name)) return name
  const userId = String(item?.sourceAccountUserId || '').trim()
  if (userId) return userId
  const clientId = Number(item?.clientId || 0)
  if (clientId) return `client ${clientId}`
  const scope = String(item?.scope || '').trim()
  if (scope) return scope
  return '账号待识别'
}

function externalContactRowKey(item) {
  return `${Number(item?.clientId || 0)}:${String(item?.userId || '').trim()}`
}

function compareIdentityOptions(a, b) {
  const leftName = String(a?.name || '').trim().toLowerCase()
  const rightName = String(b?.name || '').trim().toLowerCase()
  if (leftName && rightName && leftName !== rightName) return leftName.localeCompare(rightName)
  if (leftName && !rightName) return -1
  if (!leftName && rightName) return 1
  return String(a?.userId || '').localeCompare(String(b?.userId || ''))
}

function totalGroupCandidates(options) {
  const usable = (options || []).filter(item => {
    const conversationId = String(item.conversationId || '').trim()
    return conversationId && Number(item.memberCount || 0) !== 1
  })
  const groupsByClient = new Map()
  for (const item of usable) {
    const clientKey = String(Number(item.clientId || 0))
    if (!groupsByClient.has(clientKey)) groupsByClient.set(clientKey, [])
    groupsByClient.get(clientKey).push(item)
  }
  const result = []
  for (const items of groupsByClient.values()) {
    const maxMembers = items.reduce((max, item) => Math.max(max, Number(item.memberCount || 0)), 0)
    result.push(...(maxMembers <= 0 ? items : items.filter(item => Number(item.memberCount || 0) === maxMembers)))
  }
  return result
}

function filterGroupOptions(options, query, selectedIds) {
  const selected = new Set(uniqueIds(selectedIds))
  const q = String(query || '').trim().toLowerCase()
  return totalGroupCandidates(options)
    .filter(item => {
      const conversationId = String(item.conversationId || '').trim()
      if (!conversationId || selected.has(conversationId)) return false
      if (!q) return true
      const name = String(item.name || '').toLowerCase()
      return conversationId.toLowerCase().includes(q) || name.includes(q)
    })
    .slice(0, 80)
}

function findGroupOption(conversationId) {
  return (groupOptions.value || []).find(item => String(item.conversationId || '').trim() === conversationId)
}

function selectedInternalGroups() {
  return uniqueIds(internalGroupIDs()).map(conversationId => {
    const cached = findGroupOption(conversationId)
    return {
      conversationId,
      name: String(cached?.name || '').trim(),
      source: String(cached?.source || '').trim() || 'group_list'
    }
  })
}

const canAddInternalGroup = computed(() => {
  const id = String(selectedInternalGroupId.value || internalGroupQuery.value || '').trim()
  return Boolean(id && !internalGroupIDs().includes(id))
})

function addSelectedInternalGroup() {
  const id = String(selectedInternalGroupId.value || internalGroupQuery.value || '').trim()
  if (!id || internalGroupIDs().includes(id)) return
  setInternalGroupIDs([...internalGroupIDs(), id])
  selectedInternalGroupId.value = ''
  internalGroupQuery.value = ''
}

function removeInternalGroup(conversationId) {
  setInternalGroupIDs(internalGroupIDs().filter(item => item !== conversationId))
}

function groupOptionLabel(item) {
  const name = String(item?.name || '').trim() || '未命名群聊'
  return `${name} / ${item.conversationId}`
}

function groupSelectHint() {
  if (!groupOptions.value.length) return '请先刷新群列表'
  return internalGroupFilteredOptions.value.length ? '选择总群' : '没有匹配的群'
}

function selectedIdentityContacts(kind) {
  const labels = identityLabels(kind)
  return uniqueIds(identityIDs(kind)).map(userId => {
    const cached = findIdentityOption(kind, userId)
    const cachedName = String(cached?.name || '').trim()
    const label = String(labels[userId] || '').trim()
    const hasDraft = hasIdentityLabelDraft(kind, userId)
    const name = label || (hasDraft ? '' : cachedName)
    return {
      userId,
      name,
      source: cached?.source || (label ? 'manual_label' : 'manual'),
      needsLabel: hasDraft || !name
    }
  })
}

function identityOptionLabel(item) {
  const name = String(item?.name || '').trim() || '未命名联系人'
  return `${name} / ${item.userId}`
}

function identityOptionName(item) {
  return String(item?.name || '').trim() || String(item?.userId || '').trim() || '未命名联系人'
}

function setHandoffHuman(userId) {
  const id = String(userId || '').trim()
  form.handoff.humanUserId = id
  form.handoff.humanConversationId = ''
  const option = findHandoffHumanOption(id)
  handoffHumanQuery.value = option ? identityOptionLabel(option) : id
  pendingHandoffHumanId.value = ''
  handoffHumanEditing.value = false
  handoffHumanDropdownOpen.value = false
}

function clearHandoffHuman() {
  form.handoff.humanUserId = ''
  form.handoff.humanConversationId = ''
  handoffHumanQuery.value = ''
  pendingHandoffHumanId.value = ''
  handoffHumanEditing.value = false
  handoffHumanDropdownOpen.value = false
}

function startHandoffHumanEditing() {
  handoffHumanEditing.value = true
  handoffHumanDropdownOpen.value = true
}

function closeHandoffHumanDropdown() {
  handoffHumanDropdownOpen.value = false
}

function handleHandoffHumanInput() {
  handoffHumanEditing.value = true
  pendingHandoffHumanId.value = ''
  handoffHumanDropdownOpen.value = true
}

function previewHandoffHuman(item) {
  const id = String(item?.userId || '').trim()
  if (!id) return
  pendingHandoffHumanId.value = id
  handoffHumanQuery.value = identityOptionLabel(item)
  handoffHumanEditing.value = true
  handoffHumanDropdownOpen.value = false
}

function previewFirstHandoffHuman() {
  if (handoffHumanOptions.value.length > 0) {
    previewHandoffHuman(handoffHumanOptions.value[0])
  }
}

function confirmHandoffHuman() {
  const id = String(pendingHandoffHumanId.value || '').trim()
  if (!id) return
  setHandoffHuman(id)
}

function identitySelectHint(kind) {
  const options = kind === 'internal' ? internalFilteredOptions.value : externalFilteredOptions.value
  const allOptions = identityOptionsFor(kind)
  if (!allOptions.length) return '请先刷新联系人'
  return options.length ? '选择联系人' : '没有匹配的联系人'
}

function identitySourceLabel(source) {
  return {
    internal_cache: '企微联系人',
    external_cache: '企微联系人',
    observed_message: '消息观察',
    internal_group_member: '内部成员群',
    single_info: '单人查询',
    manual_label: '手动备注',
    manual: '手动输入'
  }[source] || source || '手动输入'
}

function identityManualLabel(kind, userId) {
  return identityLabels(kind)[userId] || ''
}

function identityManualLabelDraft(kind, userId) {
  const drafts = identityDraftMap(kind)
  if (hasIdentityLabelDraft(kind, userId)) {
    return drafts[userId]
  }
  return identityManualLabel(kind, userId)
}

function setIdentityLabelDraft(kind, userId, value) {
  identityDraftMap(kind)[userId] = String(value || '')
}

function clearIdentityLabelDraft(kind, userId) {
  delete identityDraftMap(kind)[userId]
}

function commitIdentityLabelDraft(kind, userId) {
  const text = String(identityManualLabelDraft(kind, userId) || '').trim()
  setIdentityLabel(kind, userId, text)
  clearIdentityLabelDraft(kind, userId)
}

function commitAllIdentityLabelDrafts() {
  for (const kind of ['internal', 'external']) {
    const ids = new Set(identityIDs(kind))
    for (const userId of Object.keys(identityDraftMap(kind))) {
      if (ids.has(userId)) {
        commitIdentityLabelDraft(kind, userId)
      } else {
        clearIdentityLabelDraft(kind, userId)
      }
    }
  }
}

function setIdentityLabel(kind, userId, value) {
  ensureIdentityLabelMaps()
  const labels = { ...identityLabels(kind) }
  const text = String(value || '').trim()
  if (text) {
    labels[userId] = text
  } else {
    delete labels[userId]
  }
  if (kind === 'internal') {
    form.identity.internalUserLabels = labels
  } else {
    form.identity.externalUserLabels = labels
  }
  syncIdentityLabels(kind)
}

function addSelectedIdentity(kind) {
  const userId = pendingIdentityId(kind)
  if (!userId) return
  const option = pendingIdentityOption(kind) || findIdentityOption(kind, userId)
  setIdentityIDs(kind, [...identityIDs(kind), userId])
  if (option?.name) {
    setIdentityLabel(kind, userId, option.name)
    clearIdentityLabelDraft(kind, userId)
  }
  clearIdentityPicker(kind)
}

function removeIdentity(kind, userId) {
  clearIdentityLabelDraft(kind, userId)
  setIdentityIDs(kind, identityIDs(kind).filter(item => item !== userId))
}

function clampRecordPage() {
  if (recordTotalCount.value <= 0) {
    recordCurrentPage.value = 1
    return
  }
  if (recordCurrentPage.value < 1) recordCurrentPage.value = 1
  if (recordCurrentPage.value > recordTotalPages.value) recordCurrentPage.value = recordTotalPages.value
}

function goRecordPage(page) {
  recordCurrentPage.value = Number(page) || 1
  clampRecordPage()
}

function prevRecordPage() {
  goRecordPage(recordCurrentPage.value - 1)
}

function nextRecordPage() {
  goRecordPage(recordCurrentPage.value + 1)
}

function normalizeIdentityOptions(data) {
  return {
    internal: normalizeIdentityOptionList(data?.internal),
    external: normalizeIdentityOptionList(data?.external),
    observed: normalizeIdentityOptionList(data?.observed)
  }
}

function normalizeGroupOptions(items) {
  return (Array.isArray(items) ? items : [])
    .map(item => ({
      conversationId: String(item?.conversationId || '').trim(),
      name: String(item?.name || '').trim(),
      source: String(item?.source || '').trim(),
      clientId: Number(item?.clientId || 0),
      lastSeenAt: Number(item?.lastSeenAt || 0),
      memberCount: Number(item?.memberCount || 0)
    }))
    .filter(item => item.conversationId && item.memberCount !== 1)
}

function normalizeIdentityOptionList(items) {
  return (Array.isArray(items) ? items : [])
    .map(item => ({
      userId: String(item?.userId || '').trim(),
      name: String(item?.name || '').trim(),
      source: String(item?.source || '').trim(),
      clientId: Number(item?.clientId || 0),
      scope: String(item?.scope || '').trim(),
      lastSeenAt: Number(item?.lastSeenAt || 0),
      sourceAccountUserId: String(item?.sourceAccountUserId || '').trim(),
      sourceAccountName: String(item?.sourceAccountName || '').trim()
    }))
    .filter(item => item.userId && !isRobotUserId(item.userId))
}

function cleanupIdentityLabels() {
  commitInternalGroupsForCurrentScope()
  sanitizeConfiguredIdentityIDs()
  form.handoff.humanConversationId = ''
  syncAllIdentityLabels()
}

function normalizeHandoffBeforeSave() {
  if (!form.handoff) return
  form.handoff.manualTriggerKeywords = uniqueIds(form.handoff.manualTriggerKeywords || [])
  if (!form.handoff.manualTriggerKeywords.length) {
    form.handoff.manualTriggerKeywords = uniqueIds(defaultConfig().handoff.manualTriggerKeywords || [])
  }
  form.handoff.cardKeywords = [...form.handoff.manualTriggerKeywords]
  form.handoff.cardTriggerMode = 'manual_keywords'
}

function mergeForm(cfg) {
  const defaults = defaultConfig()
  Object.assign(form, defaults, cfg || {})
  form.listen = Object.assign(defaults.listen, cfg?.listen || {})
  form.knowledge = Object.assign(defaults.knowledge, cfg?.knowledge || {})
  form.retrieval = Object.assign(defaults.retrieval, cfg?.retrieval || {})
  form.materials = Object.assign(defaults.materials, cfg?.materials || {})
  form.collaboration = Object.assign(defaults.collaboration, cfg?.collaboration || {})
  form.ai = Object.assign(defaults.ai, cfg?.ai || {})
  form.replyStyle = cfg?.replyStyle || defaults.replyStyle || 'natural_professional'
  normalizeVisionModelConfig()
  form.handoff = Object.assign(defaults.handoff, cfg?.handoff || {})
  if (!Array.isArray(form.handoff.manualTriggerKeywords) || form.handoff.manualTriggerKeywords.length === 0) {
    form.handoff.manualTriggerKeywords = uniqueIds([
      ...(defaults.handoff.manualTriggerKeywords || []),
      ...(form.handoff.cardKeywords || [])
    ])
  }
  form.handoff.manualTriggerKeywords = uniqueIds([
    ...(form.handoff.manualTriggerKeywords || []),
    ...(form.handoff.cardKeywords || [])
  ])
  form.handoff.cardKeywords = [...form.handoff.manualTriggerKeywords]
  form.handoff.cardTriggerMode = 'manual_keywords'
  syncHandoffHumanQueryFromSelection(true)
  form.identity = Object.assign(defaults.identity, cfg?.identity || {})
  form.replyPolicy = Object.assign(defaults.replyPolicy, cfg?.replyPolicy || {})
  ensureIdentityLabelMaps()
  form.identity.internalUserIds = uniqueNonRobotIds(form.identity.internalUserIds || [])
  form.identity.externalUserIds = uniqueNonRobotIds(form.identity.externalUserIds || [])
  form.identity.internalGroupConversationIds = uniqueIds(form.identity.internalGroupConversationIds || [])
  if (!form.identity.internalGroupConversationIdsByScope || Array.isArray(form.identity.internalGroupConversationIdsByScope)) {
    form.identity.internalGroupConversationIdsByScope = {}
  }
  currentIdentityScopeKey = ''
  syncAllIdentityLabels()
}

function looksLikeUrl(value) {
  return /^https?:\/\//i.test(String(value || '').trim())
}

function isVisionCapableModelName(model) {
  const name = String(model || '').trim().toLowerCase()
  return name.includes('vl') || name.includes('vision') || name.includes('qvq') || name.includes('omni')
}

function isLikelyTextOnlyQwenModel(model) {
  const name = String(model || '').trim().toLowerCase()
  if (!name || isVisionCapableModelName(name)) return false
  if (['qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-long'].includes(name)) return true
  return name.startsWith('qwen') &&
    ['turbo', 'plus', 'max', 'long', 'coder', 'math', 'instruct'].some(token => name.includes(token))
}

function normalizeVisionModelConfig() {
  if (!form.ai) return
  const visionModel = String(form.ai.visionModel || '').trim()
  const textModel = String(form.ai.model || '').trim()
  if (!visionModel ||
    (visionModel.toLowerCase() === textModel.toLowerCase() && !isVisionCapableModelName(visionModel)) ||
    isLikelyTextOnlyQwenModel(visionModel)) {
    form.ai.visionModel = defaultVisionModel
  }
}

function normalizeAIConfigBeforeSave() {
  if (!form.ai) return
  if (!String(form.ai.systemPrompt || '').trim()) form.ai.systemPrompt = '你是一名企业微信智能客服。'
  normalizeVisionModelConfig()
  if (looksLikeUrl(form.ai.visionApiKey)) {
    form.ai.visionApiKey = ''
    notify('图片 API Key 误填为 URL，已清空并复用主 API Key', 'warn', 'ai')
  }
  if (!form.ai.audioProvider) form.ai.audioProvider = 'auto'
  if (!form.ai.audioMode) form.ai.audioMode = 'openai_audio_chat'
  if (!form.ai.audioModel) form.ai.audioModel = 'qwen3-asr-flash'
  if (!form.ai.audioBaseUrl) form.ai.audioBaseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
  if (!['concise', 'medium', 'detailed'].includes(form.ai.replyDetail)) form.ai.replyDetail = 'detailed'
  if (looksLikeUrl(form.ai.audioApiKey)) {
    form.ai.audioApiKey = ''
    notify('语音 API Key 误填为 URL，已清空并复用主 API Key', 'warn', 'ai')
  }
}

function validateModelConfig() {
  if (!form.retrieval) return null

  const embeddingModel = String(form.retrieval.embeddingModel || '').trim().toLowerCase()
  const rerankModel = String(form.retrieval.rerankModel || '').trim().toLowerCase()

  // 检测 Embedding 模型字段是否填写了 Rerank 模型
  if (embeddingModel && (embeddingModel.includes('rerank') || embeddingModel.includes('gte-rerank') || embeddingModel.includes('bge-rerank'))) {
    return `配置错误：Embedding 模型字段不能填写 Rerank 模型（${form.retrieval.embeddingModel}）。请使用 text-embedding-v4 或 text-embedding-v3 等 Embedding 模型。`
  }

  // 检测 Rerank 模型字段是否填写了 Embedding 模型
  if (rerankModel && (rerankModel.includes('embedding') || rerankModel.includes('text-embedding'))) {
    return `配置错误：Rerank 模型字段不能填写 Embedding 模型（${form.retrieval.rerankModel}）。请使用 qwen3-rerank 或 gte-rerank-v2 等 Rerank 模型。`
  }

  return null
}

async function loadConfig() {
  try {
    const cfg = await GetAutoReplyConfig()
    mergeForm(cfg)
  } catch (err) {
    notify(`加载自动客服配置失败: ${err.message || err}`, 'error', 'listen')
  }
}

async function loadStatus() {
  try {
    const result = await GetAutoReplyStatus()
    if (result?.success && result.data) {
      status.value = result.data
      clampRecordPage()
      syncInternalGroupsForCurrentScope()
      sanitizeConfiguredIdentityIDs()
      const nextKey = [
        result.data.internalContactCount || 0,
        result.data.externalContactCount || 0,
        result.data.identityGroupOptionCount || 0,
        result.data.internalGroupMemberLastSyncAt || 0,
        result.data.identityLastResponseAt || 0,
        result.data.identityLastRefreshAt || 0
      ].join(':')
      if (nextKey !== identityOptionsKey) {
        identityOptionsKey = nextKey
        await loadIdentityOptions(true)
        await loadGroupOptions(true)
      }
    }
  } catch (err) {
    LogFrontend('warn', `加载自动客服状态失败: ${err.message || err}`)
  }
}

async function loadIdentityOptions(silent = true) {
  try {
    const result = await GetAutoReplyIdentityOptions()
    if (result?.success && result.data) {
      identityOptions.value = normalizeIdentityOptions(result.data)
      syncHandoffHumanQueryFromSelection()
      syncAllIdentityLabels()
    } else if (!silent) {
      notify(`加载联系人下拉失败: ${result?.message || ''}`, 'error', 'identity')
    }
  } catch (err) {
    LogFrontend('warn', `加载联系人下拉失败: ${err.message || err}`)
    if (!silent) notify(`加载联系人下拉失败: ${err.message || err}`, 'error', 'identity')
  }
}

async function loadGroupOptions(silent = true) {
  try {
    const result = await GetAutoReplyGroupOptions()
    if (result?.success && result.data) {
      groupOptions.value = normalizeGroupOptions(result.data)
    } else if (!silent) {
      notify(`加载群列表失败: ${result?.message || ''}`, 'error', 'identity')
    }
  } catch (err) {
    LogFrontend('warn', `加载群列表失败: ${err.message || err}`)
    if (!silent) notify(`加载群列表失败: ${err.message || err}`, 'error', 'identity')
  }
}

async function saveConfig(scope = 'listen', silentSuccess = false) {
  const ownsBusy = !busy.value
  if (ownsBusy) busy.value = true
  try {
    commitAllIdentityLabelDrafts()
    cleanupIdentityLabels()
    normalizeHandoffBeforeSave()
    normalizeAIConfigBeforeSave()

    // 验证模型配置
    const validationError = validateModelConfig()
    if (validationError) {
      notify(validationError, 'error', scope)
      return false
    }

    const result = await SaveAutoReplyConfig(JSON.stringify(form))
    const ok = Array.isArray(result) ? result[0] : Boolean(result)
    const msg = Array.isArray(result) ? result[1] : ''
    if (!ok) {
      notify(`保存失败: ${msg || '未知错误'}`, 'error', scope)
      await loadStatus()
      return false
    }
    if (!silentSuccess) {
      notify('自动客服配置已保存', 'success')
    }
    await loadStatus()
    return true
  } catch (err) {
    notify(`保存失败: ${err.message || err}`, 'error', scope)
    return false
  } finally {
    if (ownsBusy) busy.value = false
  }
}

async function handleStart() {
  busy.value = true
  try {
    form.enabled = true
    commitAllIdentityLabelDrafts()
    cleanupIdentityLabels()
    normalizeHandoffBeforeSave()
    await SaveAutoReplyConfig(JSON.stringify(form))
    await SetAutoReplyEnabled(true)
    await SendWxWorkData('0', JSON.stringify({ type: 10000, data: {} }))
    notify('自动客服已开启，正在监听当前接管账号', 'success')
    await loadStatus()
  } catch (err) {
    notify(`开启失败: ${err.message || err}`, 'error', 'listen')
  } finally {
    busy.value = false
  }
}

async function handleDisable() {
  busy.value = true
  try {
    form.enabled = false
    commitAllIdentityLabelDrafts()
    cleanupIdentityLabels()
    normalizeHandoffBeforeSave()
    await SaveAutoReplyConfig(JSON.stringify(form))
    await SetAutoReplyEnabled(false)
    notify('自动客服已关闭', 'success')
    await loadStatus()
  } catch (err) {
    notify(`关闭失败: ${err.message || err}`, 'error', 'listen')
  } finally {
    busy.value = false
  }
}

async function rebuildKnowledge() {
  busy.value = true
  try {
    if (!(await saveConfig('knowledge', true))) return
    const result = await RebuildKnowledgeIndex()
    notify(result?.success ? '知识库索引已重建' : `重建失败: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'knowledge')
    await loadStatus()
  } catch (err) {
    notify(`重建失败: ${err.message || err}`, 'error', 'knowledge')
  } finally {
    busy.value = false
  }
}

async function syncMaterials() {
  busy.value = true
  try {
    if (!(await saveConfig('knowledge', true))) return
    const result = await SyncAutoReplyMaterials()
    const data = result?.data || {}
    const summary = `新增 ${data.added || 0} 个，删除 ${data.removed || 0} 个，共 ${data.total || 0} 个`
    notify(result?.success ? `素材库已同步：${summary}` : `同步素材库失败: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'knowledge')
    await loadStatus()
  } catch (err) {
    notify(`同步素材库失败: ${err.message || err}`, 'error', 'knowledge')
  } finally {
    busy.value = false
  }
}

async function refreshContacts() {
  busy.value = true
  try {
    if (!(await saveConfig('identity', true))) return
    const result = await RefreshAutoReplyContacts()
    notify(result?.success ? '联系人身份缓存已开始刷新' : `刷新联系人失败: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'identity')
    await loadStatus()
    await loadIdentityOptions(true)
    setTimeout(() => loadIdentityOptions(true), 2000)
    setTimeout(() => loadIdentityOptions(true), 6000)
  } catch (err) {
    notify(`刷新联系人失败: ${err.message || err}`, 'error', 'identity')
  } finally {
    busy.value = false
  }
}

async function refreshGroups() {
  busy.value = true
  try {
    if (!(await saveConfig('identity', true))) return
    const result = await RefreshAutoReplyGroups()
    notify(result?.success ? '群列表已开始刷新' : `刷新群列表失败: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'identity')
    await loadStatus()
    await loadGroupOptions(true)
    setTimeout(() => loadGroupOptions(true), 2000)
    setTimeout(() => loadGroupOptions(true), 6000)
  } catch (err) {
    notify(`刷新群列表失败: ${err.message || err}`, 'error', 'identity')
  } finally {
    busy.value = false
  }
}

async function syncInternalGroups() {
  busy.value = true
  try {
    if (!(await saveConfig('identity', true))) return
    const result = await SyncAutoReplyInternalGroups()
    notify(result?.success ? '内部总群成员已开始同步' : `同步总群成员失败: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'identity')
    await loadStatus()
    await loadIdentityOptions(true)
    setTimeout(() => loadIdentityOptions(true), 2000)
    setTimeout(() => loadIdentityOptions(true), 6000)
  } catch (err) {
    notify(`同步总群成员失败: ${err.message || err}`, 'error', 'identity')
  } finally {
    busy.value = false
  }
}

async function testAI() {
  busy.value = true
  try {
    if (!(await saveConfig('ai', true))) return
    const result = await TestAIConnection()
    const summary = result?.data?.rawSummary || result?.data?.answer || ''
    const duration = formatDuration(result?.data?.durationMs)
    notify(result?.success ? `AI 连接正常（${duration}）：${summary}` : `AI 测试失败（${duration}）: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'ai')
  } catch (err) {
    notify(`AI 测试失败: ${err.message || err}`, 'error', 'ai')
  } finally {
    busy.value = false
  }
}

async function testHandoff() {
  busy.value = true
  try {
    if (!(await saveConfig('handoff', true))) return
    const result = await TestHumanHandoff()
    notify(result?.success ? '测试私信已发送' : `测试私信失败: ${result?.message || ''}`, result?.success ? 'success' : 'error', 'handoff')
  } catch (err) {
    notify(`测试私信失败: ${err.message || err}`, 'error', 'handoff')
  } finally {
    busy.value = false
  }
}

function notify(text, type = 'success', scope = '') {
  const normalized = String(text || '').trim()
  if (!normalized) return
  if (type === 'error') {
    setScopedMessage(scope || classifyErrorScope(normalized), normalized)
    return
  }
  message.value = normalized
  messageType.value = type
  setTimeout(() => {
    if (message.value === normalized) message.value = ''
  }, 5000)
}

function actionLabel(action) {
  return {
    replied: '已回复',
    handoff: '转人工',
    ignored: '已忽略',
    failed: '失败'
  }[action] || action
}

function reasonLabel(reason) {
  return {
    self_message: '自己消息',
    handoff_conversation: '人工会话',
    missing_group_name: '群名兜底',
    greeting_replied: '问候回复',
    internal_employee_no_handoff: '内部不转人工',
    identity_unknown_as_customer: '未知按客户',
    identity_unknown_no_handoff: '未知不转人工',
    identity_lookup_started: '身份查询已发起',
    identity_lookup_failed: '身份查询失败',
    human_card_sent: '人工名片已发',
    customer_card_sent: '客户名片已发',
    customer_notice_sent: '客户说明已发',
    human_card_failed: '人工名片失败',
    customer_card_failed: '客户名片失败',
    customer_notice_failed: '客户说明失败',
    duplicate: '重复消息',
    cooldown: '冷却中'
  }[reason] || reason
}

function formatDuration(value) {
  const ms = Number(value || 0)
  if (!Number.isFinite(ms) || ms <= 0) return '-'
  if (ms < 1000) return `${ms}ms`
  return `${(ms / 1000).toFixed(1)}s`
}

function formatUnixTime(value) {
  const seconds = Number(value || 0)
  if (!Number.isFinite(seconds) || seconds <= 0) return '-'
  const date = new Date(seconds * 1000)
  const h = String(date.getHours()).padStart(2, '0')
  const m = String(date.getMinutes()).padStart(2, '0')
  return `${h}:${m}`
}

function identityLabel(identity, source) {
  if (identity === 'internal') return '内部'
  if (identity === 'external') return '客户'
  if (source === 'identity_unknown_as_customer') return '未知/客户'
  if (source === 'identity_unknown_ignored') return '未知/忽略'
  if (source === 'manual_internal') return '手动内部'
  if (source === 'manual_external') return '手动客户'
  if (source === 'single_info') return identity === 'internal' ? '内部/单人' : '客户/单人'
  return identity || '-'
}

function formatRecordClient(record) {
  const clientId = record?.clientId ? `client ${record.clientId}` : ''
  const userId = String(record?.userId || record?.robotId || '').trim()
  if (clientId && userId) return `${clientId} / ${userId}`
  return clientId || userId || '-'
}

function formatScores(record) {
  const parts = []
  if (Number(record.keywordScore || 0) > 0) parts.push(`K ${Number(record.keywordScore).toFixed(3)}`)
  if (Number(record.vectorScore || 0) > 0) parts.push(`V ${Number(record.vectorScore).toFixed(3)}`)
  if (Number(record.rerankScore || 0) > 0) parts.push(`R ${Number(record.rerankScore).toFixed(3)}`)
  return parts.length ? parts.join(' / ') : '-'
}

function scrollToSection(id) {
  const target = document.getElementById(`auto-section-${id}`)
  if (target) {
    target.scrollIntoView({ behavior: 'smooth', block: 'start' })
  }
}

onMounted(async () => {
  await loadConfig()
  await loadStatus()
  await loadIdentityOptions(true)
  await loadGroupOptions(true)
  timer = setInterval(async () => {
    await loadStatus()
    await loadIdentityOptions(true)
  }, 5000)
})

onUnmounted(() => {
  if (timer) clearInterval(timer)
  for (const scopedTimer of Object.values(scopedMessageTimers)) {
    if (scopedTimer) clearTimeout(scopedTimer)
  }
})
</script>

<style scoped>
.auto-reply-page {
  color: #18282d;
  padding: 18px;
  background: #f5f7f8;
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin-bottom: 16px;
}

.page-header h2 {
  margin: 0 0 6px;
  color: #18282d;
  font-size: 24px;
}

.page-header p {
  margin: 0;
  color: #667085;
  font-size: 13px;
}

.header-actions,
.panel-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.primary-btn,
.ghost-btn {
  min-height: 36px;
  border-radius: 6px;
  border: 1px solid #16706d;
  padding: 0 16px;
  font-weight: 700;
  cursor: pointer;
}

.primary-btn {
  background: #16706d;
  color: #fff;
}

.ghost-btn {
  background: #fff;
  color: #16706d;
}

button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

.metric {
  background: #fff;
  border: 1px solid #dfe5e8;
  border-radius: 8px;
}

.metric {
  padding: 14px;
}

.metric span {
  display: block;
  color: #667085;
  font-size: 12px;
  margin-bottom: 8px;
}

.metric strong {
  font-size: 22px;
}

.metric .ok {
  color: #16706d;
}

.metric .muted {
  color: #8a94a6;
}

.section-nav {
  position: sticky;
  top: 0;
  z-index: 8;
  display: flex;
  gap: 8px;
  overflow-x: auto;
  margin: 0 0 22px;
  padding: 10px;
  border: 1px solid #d7e1e4;
  border-radius: 8px;
  background: rgba(245, 247, 248, 0.96);
  box-shadow: 0 8px 18px rgba(15, 31, 35, 0.06);
}

.section-nav-btn {
  flex: 0 0 auto;
  min-height: 34px;
  border: 1px solid #c9d9dc;
  border-radius: 6px;
  padding: 0 13px;
  background: #fff;
  color: #1d4e52;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.section-nav-btn:hover {
  border-color: #16706d;
  background: #eef9f7;
}

.auto-section {
  position: relative;
  margin: 0 0 26px;
  padding: 0 18px 18px;
  border: 1px solid #d7e1e4;
  border-left: 5px solid #16706d;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 12px 26px rgba(15, 31, 35, 0.05);
  scroll-margin-top: 76px;
}

.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin: 0 -18px 16px;
  padding: 15px 18px;
  border-bottom: 1px solid #e5ecef;
  border-radius: 8px 8px 0 0;
  background: #f9fbfc;
}

.section-title {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.section-title h3 {
  color: #18282d;
  font-size: 17px;
  margin: 0;
  line-height: 1.35;
}

.section-index {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 6px;
  color: #fff;
  background: #16706d;
  font-size: 13px;
  font-weight: 800;
}

.section-meta {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
}

.section-meta span {
  border: 1px solid #d7e1e4;
  border-radius: 6px;
  padding: 4px 8px;
  color: #4d5f66;
  background: #fff;
  font-size: 12px;
  white-space: nowrap;
}

.section-alerts {
  display: grid;
  gap: 8px;
  margin: -4px 0 16px;
}

.section-alert {
  border-radius: 4px;
  padding: 10px 12px;
  background: #fdecec;
  color: #9b1c1c;
  font-size: 13px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px 16px;
  margin-bottom: 14px;
}

.ai-config-groups {
  display: grid;
  gap: 14px;
}

.config-subsection {
  border: 1px solid #dce7ea;
  border-radius: 6px;
  padding: 14px;
  background: #fbfcfd;
}

.config-subsection .form-grid {
  margin-bottom: 0;
}

.advanced-grid {
  margin-top: 12px;
}

.config-subsection-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 12px;
}

.config-subsection-header strong {
  color: #18282d;
  font-size: 14px;
}

.config-subsection-header span {
  color: #667085;
  font-size: 12px;
  line-height: 1.45;
}

.advanced-toggle {
  margin-top: 12px;
  min-height: 32px;
  border: 1px solid #c9d9dc;
  border-radius: 6px;
  padding: 0 12px;
  background: #fff;
  color: #1d4e52;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.advanced-toggle:hover {
  border-color: #16706d;
  background: #eef9f7;
}

.form-grid label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  color: #475467;
  font-size: 13px;
}

.form-grid input,
.form-grid select,
.form-grid textarea {
  border: 1px solid #cfd8dc;
  border-radius: 6px;
  min-height: 36px;
  padding: 8px 10px;
  font-size: 13px;
  font-family: inherit;
}

.select-action-row {
  display: flex;
  gap: 8px;
}

.handoff-human-combobox {
  position: relative;
}

.handoff-human-search {
  width: 100%;
}

.handoff-human-dropdown {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 260px;
  overflow: auto;
  border: 1px solid #cfd8dc;
  border-radius: 6px;
  background: #fff;
  box-shadow: 0 12px 28px rgba(16, 24, 40, 0.16);
}

.handoff-human-option {
  width: 100%;
  border: 0;
  border-bottom: 1px solid #eef2f4;
  padding: 9px 10px;
  background: transparent;
  color: #1f2f35;
  text-align: left;
  cursor: pointer;
}

.handoff-human-option:hover,
.handoff-human-option.selected {
  background: #eef9f7;
}

.handoff-human-option strong,
.handoff-human-option span,
.handoff-human-option small {
  display: block;
}

.handoff-human-option span,
.handoff-human-option small,
.handoff-human-empty {
  color: #667085;
  font-size: 12px;
}

.handoff-human-empty {
  padding: 10px;
}

.select-action-row select {
  flex: 1;
  min-width: 0;
}

.mini-btn {
  min-height: 36px;
  border: 1px solid #cfd8dc;
  border-radius: 6px;
  padding: 0 12px;
  background: #fff;
  color: #1d4e52;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.mini-btn:hover {
  border-color: #16706d;
  background: #eef9f7;
}

.form-grid textarea {
  min-height: 110px;
  resize: vertical;
}

.wide {
  grid-column: 1 / -1;
}

.check-row {
  flex-direction: row !important;
  align-items: center;
}

.check-row input {
  width: 16px;
  min-height: 16px;
}

.field-hint {
  color: #5f6f82;
  font-size: 12px;
  line-height: 1.5;
}

.field-warning {
  grid-column: 1 / -1;
  border: 1px solid #f5c26b;
  border-radius: 4px;
  padding: 9px 10px;
  color: #7a4b00;
  background: #fff7e6;
  font-size: 13px;
  line-height: 1.45;
}

.identity-editor {
  border: 1px solid #d7e1e4;
  border-radius: 6px;
  padding: 12px;
  background: #f8fafb;
}

.identity-editor-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 10px;
}

.identity-editor-header strong {
  color: #18282d;
  font-size: 14px;
}

.identity-editor-header span,
.identity-empty {
  color: #667085;
  font-size: 12px;
}

.identity-picker-row {
  display: grid;
  grid-template-columns: minmax(180px, 1fr) minmax(220px, 1.4fr) auto;
  gap: 10px;
  align-items: center;
}

.identity-picker-row input,
.identity-picker-row select {
  border: 1px solid #cfd8dc;
  border-radius: 6px;
  min-height: 36px;
  padding: 8px 10px;
  font-size: 13px;
  font-family: inherit;
}

.identity-selected-list {
  display: grid;
  gap: 8px;
  margin-top: 10px;
}

.identity-selected-row {
  display: grid;
  grid-template-columns: minmax(120px, 1fr) minmax(170px, 1.2fr) 90px minmax(140px, 1fr) auto;
  gap: 8px;
  align-items: center;
  border: 1px solid #e6ecef;
  border-radius: 6px;
  padding: 8px;
  background: #fff;
}

.identity-selected-row strong {
  color: #18282d;
}

.identity-selected-row span,
.identity-user-id {
  color: #5f6f82;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.identity-selected-row input {
  border: 1px solid #cfd8dc;
  border-radius: 6px;
  min-height: 32px;
  padding: 6px 8px;
  font-size: 12px;
}

.identity-sync-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.identity-sync-panel {
  border: 1px solid #d7e1e4;
  border-radius: 6px;
  padding: 12px;
  background: #fff;
  min-width: 0;
}

.identity-search-input {
  border: 1px solid #cfd8dc;
  border-radius: 6px;
  min-height: 36px;
  padding: 8px 10px;
  font-size: 13px;
  font-family: inherit;
  width: 100%;
}

.identity-sync-list {
  display: grid;
  gap: 6px;
  margin-top: 10px;
  max-height: 360px;
  overflow: auto;
}

.external-group-list {
  gap: 10px;
}

.identity-external-group {
  border: 1px solid rgba(72, 214, 218, 0.22);
  border-radius: 6px;
  padding: 8px;
  background: rgba(6, 18, 22, 0.96);
  box-shadow: inset 0 0 0 1px rgba(72, 214, 218, 0.06);
}

.identity-external-summary {
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  gap: 10px;
  color: #effbfd;
  font-size: 13px;
  list-style-position: inside;
  align-items: center;
  padding: 2px 0 6px;
}

.identity-external-summary::-webkit-details-marker {
  display: none;
}

.identity-external-summary span {
  color: #8fb7bf;
  font-size: 12px;
  white-space: nowrap;
}

.identity-external-group .identity-sync-row {
  border-bottom-color: rgba(72, 214, 218, 0.12);
}

.identity-external-group .identity-sync-row strong {
  color: #f2feff;
}

.identity-external-group .identity-sync-row span {
  color: #9eb9c0;
}

.identity-sync-row {
  display: grid;
  grid-template-columns: minmax(110px, 1fr) minmax(150px, 1.2fr) 86px 52px;
  gap: 8px;
  align-items: center;
  border-bottom: 1px solid #edf2f4;
  padding: 7px 0;
  min-width: 0;
}

.identity-sync-row strong,
.identity-sync-row span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.identity-sync-row strong {
  color: #18282d;
}

.identity-sync-row span {
  color: #5f6f82;
  font-size: 12px;
}

.identity-advanced {
  border: 1px solid #d7e1e4;
  border-radius: 6px;
  padding: 10px 12px;
  background: #f8fafb;
}

.identity-advanced summary {
  cursor: pointer;
  color: #18343a;
  font-weight: 700;
}

.identity-advanced-body {
  display: grid;
  gap: 12px;
  margin-top: 12px;
}

.identity-advanced-body label {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.small-btn {
  min-height: 30px;
  padding: 0 10px;
  font-size: 12px;
}

.tag-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 14px;
}

.tag-row span {
  border: 1px solid #d7e1e4;
  border-radius: 6px;
  padding: 4px 8px;
  color: #44515f;
  background: #f8fafb;
}

.inline-message {
  border-radius: 4px;
  padding: 10px 12px;
  margin-bottom: 12px;
  background: #eaf7ee;
  color: #17663a;
}

.inline-message.error {
  background: #fdecec;
  color: #9b1c1c;
}

.reason-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
}

.reason-row span {
  border: 1px solid #d7e1e4;
  border-radius: 6px;
  padding: 4px 8px;
  color: #44515f;
  background: #f8fafb;
  font-size: 12px;
}

.failed-list {
  color: #9b1c1c;
  font-size: 12px;
  line-height: 1.6;
}

.record-table {
  overflow-x: auto;
}

.record-pagination {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  flex-wrap: wrap;
  padding-top: 12px;
  color: #667085;
  font-size: 12px;
}

.record-row {
  display: grid;
  grid-template-columns: 150px 80px 70px 150px 130px 90px minmax(180px, 1fr) minmax(180px, 1fr) minmax(150px, 1fr) 80px 150px minmax(180px, 1fr);
  gap: 10px;
  min-width: 1740px;
  padding: 9px 0;
  border-bottom: 1px solid #eef1f4;
  font-size: 13px;
}

.record-row.header {
  color: #667085;
  font-weight: 600;
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.empty {
  color: #667085;
  padding: 20px 0;
}

@media (max-width: 760px) {
  .auto-reply-page {
    padding: 12px;
  }

  .section-nav {
    margin-bottom: 18px;
    padding: 8px;
  }

  .auto-section {
    padding: 0 12px 14px;
    scroll-margin-top: 70px;
  }

  .section-header {
    align-items: flex-start;
    flex-direction: column;
    margin: 0 -12px 14px;
    padding: 13px 12px;
  }

  .section-meta {
    justify-content: flex-start;
  }

  .identity-picker-row,
  .identity-selected-row,
  .identity-sync-grid,
  .identity-sync-row {
    grid-template-columns: 1fr;
  }
}

/* Command center skin */
.auto-reply-page {
  color: var(--cmd-text);
  border-color: var(--cmd-line);
  background: transparent;
  box-shadow: none;
}

.page-header h2,
.section-title h3,
.config-subsection-header strong,
.identity-editor-header strong,
.identity-sync-row strong,
.identity-selected-row strong,
.identity-advanced summary {
  color: var(--cmd-text);
}

.page-header p,
.metric span,
.section-meta span,
.config-subsection-header span,
.form-grid label,
.field-hint,
.identity-editor-header span,
.identity-empty,
.identity-sync-row span,
.identity-selected-row span,
.identity-user-id,
.empty {
  color: var(--cmd-text-soft);
}

.metric,
.auto-section,
.config-subsection,
.identity-editor,
.identity-sync-panel,
.identity-advanced,
.section-nav,
.identity-selected-row {
  border-color: var(--cmd-line);
  background: var(--cmd-panel);
  box-shadow: var(--cmd-shadow);
}

.metric {
  position: relative;
  overflow: hidden;
}

.metric::before,
.auto-section::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: linear-gradient(90deg, var(--cmd-cyan), transparent);
}

.metric strong {
  color: var(--cmd-text);
  text-shadow: 0 0 18px rgba(72, 240, 220, 0.12);
}

.metric .ok {
  color: var(--cmd-green);
}

.section-nav {
  background: rgba(7, 18, 23, 0.96);
  backdrop-filter: blur(12px);
}

.section-nav-btn {
  color: var(--cmd-text-soft);
  border-color: rgba(72, 240, 220, 0.24);
  background: rgba(6, 17, 22, 0.92);
}

.section-nav-btn:hover {
  color: var(--cmd-cyan);
  border-color: rgba(72, 240, 220, 0.68);
  background: rgba(72, 240, 220, 0.1);
}

.auto-section {
  border-left-color: var(--cmd-cyan);
  background: rgba(8, 20, 26, 0.92);
}

.section-header {
  border-bottom-color: rgba(72, 240, 220, 0.16);
  background: linear-gradient(90deg, rgba(72, 240, 220, 0.1), rgba(99, 168, 255, 0.045));
}

.section-index {
  color: #031316;
  background: linear-gradient(135deg, var(--cmd-cyan), var(--cmd-blue));
  box-shadow: 0 0 18px rgba(72, 240, 220, 0.22);
}

.section-meta span,
.tag-row span,
.reason-row span {
  border-color: rgba(72, 240, 220, 0.2);
  color: var(--cmd-text-soft);
  background: rgba(7, 18, 23, 0.82);
}

.section-alert,
.field-warning,
.failed-list {
  color: var(--cmd-red);
}

.section-alert {
  background: rgba(255, 107, 125, 0.1);
  border: 1px solid rgba(255, 107, 125, 0.28);
}

.field-warning {
  border-color: rgba(255, 209, 102, 0.34);
  color: var(--cmd-amber);
  background: rgba(255, 209, 102, 0.1);
}

.config-subsection,
.identity-editor,
.identity-sync-panel,
.identity-advanced,
.identity-selected-row {
  background: rgba(7, 18, 23, 0.78);
}

.form-grid input,
.form-grid select,
.form-grid textarea,
.identity-picker-row input,
.identity-picker-row select,
.identity-selected-row input,
.identity-search-input {
  color: var(--cmd-text);
  border-color: rgba(72, 240, 220, 0.24);
  background: rgba(5, 15, 20, 0.94);
}

.handoff-human-dropdown {
  border-color: rgba(72, 240, 220, 0.28);
  background: rgba(5, 15, 20, 0.98);
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.38);
}

.handoff-human-option {
  color: var(--cmd-text);
  border-bottom-color: rgba(72, 240, 220, 0.12);
}

.handoff-human-option:hover,
.handoff-human-option.selected {
  background: rgba(72, 240, 220, 0.12);
}

.handoff-human-option span,
.handoff-human-option small,
.handoff-human-empty {
  color: var(--cmd-text-soft);
}

.form-grid input:focus,
.form-grid select:focus,
.form-grid textarea:focus,
.identity-picker-row input:focus,
.identity-picker-row select:focus,
.identity-selected-row input:focus,
.identity-search-input:focus {
  outline: none;
  border-color: rgba(72, 240, 220, 0.68);
  box-shadow: 0 0 18px rgba(72, 240, 220, 0.12);
}

.identity-sync-row,
.record-row {
  border-bottom-color: rgba(72, 240, 220, 0.14);
}

.record-row.header {
  color: var(--cmd-text);
}

.inline-message {
  color: var(--cmd-green);
  border: 1px solid rgba(93, 242, 167, 0.34);
  background: rgba(93, 242, 167, 0.1);
}

.inline-message.error {
  color: var(--cmd-red);
  border-color: rgba(255, 107, 125, 0.38);
  background: rgba(255, 107, 125, 0.1);
}

.primary-btn {
  color: #031316;
  border-color: rgba(72, 240, 220, 0.82);
  background: linear-gradient(135deg, var(--cmd-cyan), var(--cmd-blue));
  box-shadow: 0 0 20px rgba(72, 240, 220, 0.16);
}

.ghost-btn,
.mini-btn,
.advanced-toggle {
  color: var(--cmd-cyan);
  border-color: rgba(72, 240, 220, 0.38);
  background: rgba(9, 23, 29, 0.84);
}
</style>
