<template>
  <div class="softphone-page">
    <div class="softphone-layout">
      <section class="gateway-section">
        <div class="section-head">
          <div class="section-head-left">
            <div class="section-title">
              <i class="material-icons">route</i>
              <span>切换外呼线路</span>
            </div>
            <p class="section-desc">点击表格行选择线路，核对规则后点击「重连所选线路」生效</p>
          </div>
          <button type="button" class="btn-text" @click="loadGatewayList" :disabled="gatewayListLoading">
            <i class="material-icons">{{ gatewayListLoading ? 'hourglass_empty' : 'refresh' }}</i>
            刷新
          </button>
        </div>

        <div class="gateway-table-wrap">
          <el-table
            v-if="gatewayList.length || gatewayListLoading"
            v-loading="gatewayListLoading"
            :data="gatewayList"
            border
            stripe
            class="gateway-el-table"
            :row-class-name="gatewayRowClassName"
            @row-click="onGatewayRowClick"
            empty-text="暂无可用线路">
            <el-table-column width="52" align="center" label="">
              <template slot-scope="{ row }">
                <span class="radio-dot" :class="{ on: isGatewaySelected(row.id) }"></span>
              </template>
            </el-table-column>
            <el-table-column label="外呼线路" min-width="160">
              <template slot-scope="{ row }">
                <div class="line-name">{{ getGatewayDisplayTitle(row) }}</div>
                <div class="line-tags">
                  <el-tag v-if="getGatewayBadge(row)" size="mini" type="primary" effect="plain">{{ getGatewayBadge(row) }}</el-tag>
                  <el-tag v-if="isGatewayConnected(row.id)" size="mini" type="success" effect="plain">当前已连接</el-tag>
                </div>
              </template>
            </el-table-column>
            <el-table-column
              v-for="col in gatewayRuleColumns"
              :key="col"
              :label="col"
              min-width="120"
              class-name="wrap-cell">
              <template slot-scope="{ row }">
                <span class="cell-text">{{ getRuleCellValue(row, col) }}</span>
              </template>
            </el-table-column>
            <el-table-column
              :label="gatewayRateColumn"
              width="96"
              align="center"
              class-name="rate-cell-col">
              <template slot="header">
                <span class="rate-col-header">{{ gatewayRateColumn }}</span>
              </template>
              <template slot-scope="{ row }">
                <span class="cell-text rate-cell-value">{{ getRuleCellValue(row, gatewayRateColumn) }}</span>
              </template>
            </el-table-column>
          </el-table>
          <div v-else class="empty-tip">暂无可用线路</div>
        </div>

        <div class="section-foot">
          <span class="foot-hint" v-if="selectedGatewayId">
            已选：{{ selectedGatewayTitle }}
          </span>
          <span class="foot-hint foot-hint-muted" v-else>请先选择一条线路</span>
          <button type="button"
                  class="btn-primary"
                  :disabled="!selectedGatewayId || isGatewayReconnecting || callStatus !== 'idle'"
                  @click="reconnectWithSelectedGateway">
            <i class="material-icons">{{ isGatewayReconnecting ? 'hourglass_top' : 'sync' }}</i>
            {{ isGatewayReconnecting ? '重连中...' : '重连所选线路' }}
          </button>
        </div>
      </section>

      <aside class="dialer-column">
        <div class="dialer-panel">
          <div class="dialer-panel-head">
            <div class="dialer-panel-title">
              <i class="material-icons">phone_in_talk</i>
              <span>软电话</span>
            </div>
            <span class="dialer-panel-hint">{{ isInActiveCall ? '通话中，点击红色按钮挂断' : '输入号码后点击绿色拨号键' }}</span>
          </div>
          <section class="dialer-section">
            <div class="dialer" :class="{ 'is-locked': showInitMask }">
              <div class="dialer-mask" v-if="showInitMask">
                <div class="spinner"></div>
                <p>{{ initStatusText }}</p>
                <button v-if="initFailed" type="button" class="btn-primary btn-sm" @click="retryInit">重新连接</button>
              </div>
              <!-- 状态栏 -->
              <div class="status-bar">
                <div class="status-left">
                  <div class="user-avatar-dropdown" v-click-outside="closeDropdown">
                    <i class="material-icons user-avatar-icon"
                       :class="{ 'network-available': isRegistered, 'no-network': !isConnected }"
                       @click="toggleDropdown"
                       title="点击切换账号">account_circle</i>
                    <div class="dropdown-menu" v-show="dropdownVisible">
                      <div class="dropdown-group">
                        <a href="#" class="dropdown-item" v-for="(userProfile, userId) in userList" :key="userId" @click.prevent="switchAccount(userId)" :title="'切换到: ' + userProfile.note">
                          <i class="material-icons" v-if="currentUserId === userId">check</i>
                          <i class="material-icons" v-else style="visibility: hidden;">check</i>
                          {{ userProfile.note }}
                        </a>
                      </div>
                      <div class="dropdown-group">
                        <a href="#" class="dropdown-item" @click.prevent="openEditAccountDialog" title="编辑当前账号信息"><i class="material-icons">edit</i>编辑账号</a>
                        <a href="#" class="dropdown-item" @click.prevent="openAddAccountDialog" title="添加新的SIP账号"><i class="material-icons">add</i>添加账号</a>
                        <a href="#" class="dropdown-item" @click.prevent="openSettingsDialog" title="SIP通话设置"><i class="material-icons">settings</i>SIP设置</a>
                        <a href="#" class="dropdown-item" @click.prevent="confirmDeleteAccount" title="删除当前账号"><i class="material-icons">delete</i>删除账号</a>
                      </div>
                    </div>
                  </div>
                </div>
                <div class="status-center">
          <span class="display-user"
                :class="{ 'network-available': isRegistered }"
                :title="currentUserDisplay">{{ currentUserDisplay }}</span>
                </div>
                <div class="status-right">
                  <i class="material-icons network-icon"
                     :class="{ 'no-network': !isConnected, 'network-available': isConnected && isRegistered, 'network-connecting': isConnected && !isRegistered }"
                     :title="!isConnected ? '未连接' : (isRegistered ? '已注册' : '连接中')">signal_cellular_alt</i>
                </div>
              </div>

              <!-- 拨号显示屏 -->
              <div class="display-wrapper">
                <div class="dial-mode-selector" v-if="showPlaintextOption">
                  <label class="radio-label" title="使用明文号码直接拨号">
                    <input type="radio" value="plaintext" v-model="dialMode" @change="handleDialModeChange">
                    <span>明文</span>
                  </label>
                  <label class="radio-label" title="使用加密号码拨号（点击拨号时解密）">
                    <input type="radio" value="encrypted" v-model="dialMode" @change="handleDialModeChange">
                    <span>密文</span>
                  </label>
                </div>
                <input type="tel"
                       class="dialer-display"
                       :class="{ 'center-align': isContentFit, 'right-align': !isContentFit }"
                       id="display"
                       v-model="dialNumber"
                       @keypress="handleDisplayKeypress"
                       placeholder="输入电话号码"
                       ref="dialerInput"
                       :readonly="!canEditDialNumber"
                       title="输入要拨打的电话号码">
                <button type="button"
                        class="clear-number-btn"
                        @click="clearDialNumber"
                        :disabled="!dialNumber || !canEditDialNumber"
                        title="清空号码">
                  <i class="material-icons">backspace</i>
                </button>
              </div>

              <div class="container" v-show="province" title="来电归属地">
                <span class="province">{{ province }}</span>
              </div>
              <div class="container" title="通话时长">
                <span class="call-timer" v-show="callDuration !== '00:00'">{{ callDuration }}</span>
              </div>
              <div class="container decrypting-tip" v-show="isDecryptingForCall && dialMode === 'encrypted'" title="正在解密号码">
                <span>🔄 正在解密号码...</span>
              </div>

              <!-- 音量控制：始终可见 -->
              <div class="audio-panel">
                <div class="audio-row">
                  <button type="button" class="audio-mute-btn" :class="{ active: isSpeakerMuted }" @click="toggleMuteSpeaker" :title="isSpeakerMuted ? '取消扬声器静音' : '扬声器静音'">
                    <i class="material-icons">{{ isSpeakerMuted ? 'volume_off' : 'volume_up' }}</i>
                  </button>
                  <div class="audio-slider-wrap">
                    <span class="audio-label">扬声器</span>
                    <input type="range" min="0" max="100" step="1" :value="speakerVolumePercent" @input="onSpeakerVolumeInput" class="audio-slider" :disabled="isSpeakerMuted">
                    <span class="audio-value">{{ isSpeakerMuted ? '静音' : speakerVolumePercent + '%' }}</span>
                  </div>
                </div>
                <div class="audio-row">
                  <button type="button" class="audio-mute-btn" :class="{ active: isMicMuted }" @click="toggleMuteMic" :title="isMicMuted ? '取消麦克风静音' : '麦克风静音'">
                    <i class="material-icons">{{ isMicMuted ? 'mic_off' : 'mic' }}</i>
                  </button>
                  <div class="audio-slider-wrap">
                    <span class="audio-label">麦克风</span>
                    <input type="range" min="0" max="100" step="1" :value="micVolumePercent" @input="onMicVolumeInput" class="audio-slider" :disabled="isMicMuted">
                    <span class="audio-value">{{ isMicMuted ? '静音' : micVolumePercent + '%' }}</span>
                  </div>
                </div>
              </div>

              <div class="dialer-keypad">
                <button class="dialer-button"
                        v-for="digit in dialKeys"
                        :key="digit"
                        @click="onDigitClick(digit)"
                        :title="callStatus === 'talking' ? '发送DTMF: ' + digit : '输入: ' + digit">{{ digit }}</button>
              </div>

              <div class="call-buttons" :class="{ 'call-buttons-single': !showLeftButton && !showRightButton }">
                <button v-if="showLeftButton"
                        class="call-button call-left-button"
                        :class="{ normal: leftButtonNormal }"
                        @click="onLeftButtonClick"
                        :title="getLeftButtonTitle()">
                  <i class="material-icons">{{ leftButtonIcon }}</i>
                </button>
                <button class="call-button call-hangup-button"
                        :class="hangupButtonClass"
                        @click="onHangupClick"
                        :title="getHangupButtonTitle()"
                        :disabled="isHangupDisabled">
                  <i class="material-icons">{{ hangupButtonIcon }}</i>
                </button>
                <button v-if="showRightButton"
                        class="call-button call-right-button"
                        :class="{ normal: rightButtonNormal }"
                        @click="onRightButtonClick"
                        :title="getRightButtonTitle()">
                  <i class="material-icons">{{ rightButtonIcon }}</i>
                </button>
              </div>

              <div class="status-footer">
                <div class="status-footer-left">
                  <div class="status-bar-message"
                       :class="statusType"
                       v-if="statusText"
                       :title="statusText">{{ statusText }}</div>
                  <div class="reconnect-failed" v-if="reconnectFailed" title="重连超时，请尝试手动重新连接">
                    <span>重连超时</span>
                  </div>
                </div>
                <div class="version-ribbon" title="软电话版本">v1.0.0</div>
              </div>
            </div>
          </section>
        </div>
      </aside>
    </div>

    <!-- 账号模态框 -->
    <div id="phoneModal" class="modal" v-show="accountDialogVisible" @click.self="accountDialogVisible = false">
      <div class="modal-header">
        <i class="material-icons">{{ accountDialogTitle === '添加账号' ? 'add' : 'edit' }}</i>
        <span id="modalTitle">{{ accountDialogTitle }}</span>
      </div>
      <div class="modal-content">
        <form @submit.prevent="saveAccount">
          <div class="form-group">
            <input type="text" id="note" v-model="accountForm.note" placeholder="备注">
          </div>
          <div class="form-group">
            <input type="text" id="server" v-model="accountForm.server" placeholder="服务(ws://129.28.164.235:5066)" required>
          </div>
          <div class="form-group">
            <input type="text" id="username" v-model="accountForm.username" placeholder="用户名">
          </div>
          <div class="form-group">
            <input type="text" id="domain" v-model="accountForm.domain" placeholder="域名" required>
          </div>
          <div class="form-group">
            <input type="text" id="loginName" v-model="accountForm.loginName" placeholder="登录名" required>
          </div>
          <div class="form-group">
            <input :type="showPassword ? 'text' : 'password'" id="password" v-model="accountForm.password" placeholder="密码" required>
            <i class="material-icons password-toggle" @click="showPassword = !showPassword">{{ showPassword ? 'visibility_off' : 'visibility' }}</i>
          </div>
          <div class="form-group">
            <select id="transport" v-model="accountForm.transport">
              <option value="wss">Transport (WSS)</option>
              <option value="ws">Transport (WS)</option>
            </select>
          </div>
          <div class="form-buttons">
            <button type="button" class="cancel-button" @click="accountDialogVisible = false">取消</button>
            <button type="submit" class="add-button">保存</button>
          </div>
        </form>
      </div>
    </div>

    <!-- 设置模态框 -->
    <div id="settingModal" class="modal" v-show="settingsDialogVisible" @click.self="settingsDialogVisible = false">
      <div class="modal-header">
        <i class="material-icons">settings</i>
        <span>SIP 设置</span>
      </div>
      <div class="modal-content">
        <form @submit.prevent="saveSettings">
          <div class="form-group">
            <label class="form-label">
              User Agent
              <i class="material-icons info-icon" title="SIP 用户代理标识，用于服务器识别客户端类型。例如：JsSIP、Zoiper 等">info</i>
            </label>
            <input type="text" id="userAgent" v-model="settingsForm.userAgent" placeholder="例如: JsSIP" required>
          </div>
          <div class="form-group">
            <label class="form-label">
              Session Expires
              <i class="material-icons info-icon" title="SIP 会话的有效期（单位：秒）。超时后需要重新注册。建议值：180-600 秒。当前设置为 180 秒（3分钟），JsSIP 会自动在到期前发送 re-INVITE 刷新会话，防止通话被中间代理断开">info</i>
            </label>
            <input type="number" id="sessionExpires" v-model.number="settingsForm.sessionExpires" placeholder="例如: 180" required min="60">
          </div>
          <div class="form-group">
            <label class="form-label">
              Min Session Expires
              <i class="material-icons info-icon" title="服务器可接受的最小会话有效期（单位：秒）。必须小于 Session Expires。建议值：90-180 秒。当前设置为 90 秒，确保与 Session Expires 配合工作">info</i>
            </label>
            <input type="number" id="minSessionExpires" v-model.number="settingsForm.minSessionExpires" placeholder="例如: 90" required min="30">
          </div>
          <div class="form-group">
            <label class="form-label">
              启用 STUN
              <i class="material-icons info-icon" title="STUN 服务器用于 NAT 穿透，帮助在防火墙或复杂网络环境下建立连接。内网或直接连接可关闭">info</i>
            </label>
            <select id="stun" v-model="settingsForm.stun">
              <option :value="false" selected>否</option>
              <option :value="true">是</option>
            </select>
          </div>
          <div class="form-group">
            <label class="form-label">
              ICE Server
              <i class="material-icons info-icon" title="ICE 服务器地址，用于音视频媒体流穿透。格式：stun:server:port 或 turn:user:pass@server:port。例如：stun:stun.l.google.com:19302">info</i>
            </label>
            <input type="text" id="iceServer" v-model="settingsForm.iceServer" placeholder="例如: stun:stun.l.google.com:19302">
          </div>
          <div class="form-group">
            <label class="form-label">
              自动接听
              <i class="material-icons info-icon" title="收到来电时是否自动接听，无需手动操作。开启后适合客服场景">info</i>
            </label>
            <select id="autoAnswer" v-model="settingsForm.autoAnswer">
              <option :value="false" selected>否</option>
              <option :value="true">是</option>
            </select>
          </div>
          <div class="form-group">
            <label class="form-label">
              自动重连
              <i class="material-icons info-icon" title="WebSocket 或 SIP 连接断开后是否尝试自动重连。建议开启以应对网络波动">info</i>
            </label>
            <select id="reconnect" v-model="settingsForm.reconnect">
              <option :value="true" selected>是</option>
              <option :value="false">否</option>
            </select>
          </div>
          <div class="form-group" v-if="settingsForm.reconnect">
            <label class="form-label">
              重连间隔
              <i class="material-icons info-icon" title="每次重连尝试之间的等待时间（单位：秒）。建议值：10-30 秒，过短会增加服务器压力">info</i>
            </label>
            <input type="number" id="reconnectInterval" v-model.number="settingsForm.reconnectInterval" placeholder="例如: 15" min="5" max="300">
          </div>
          <div class="form-buttons">
            <button type="button" class="cancel-button" @click="settingsDialogVisible = false">取消</button>
            <button type="submit" class="add-button">保存</button>
          </div>
        </form>
      </div>
    </div>

  </div>
</template>

<script>
import { WebPhone, ProfileManager, checkMicrophonePermission, IPCC_DEFAULTS, JS_SIP_DEFAULTS } from '@/api/aiSipCall/softPhone.js';
import ccPhoneBarSocket from '@/assets/callCenterPhoneBarSdk/ccPhoneBarSocket.js';
import { EventList, VideoLevels, AgentStatusEnum } from '@/assets/callCenterPhoneBarSdk/constants.js';
import { myCallUser, getToolbarBasicParam } from '@/api/aiSipCall/aiSipCallUser.js';
import { callEndSyncByUuid, encryptMobile } from '@/api/aiSipCall/aiSipCallOutboundCdr.js';
import { remoteGatewayList } from '@/api/aiSipCall/aiSipCallGateway.js';

const IPCC_CONFIG = IPCC_DEFAULTS;
const JS_SIP_CONFIG = JS_SIP_DEFAULTS;
const VOLUME_CONFIG = { DEFAULT: 0.8, HIDE_DELAY: 1000 };
const UI_STATE = { IDLE: 'idle', RINGING: 'ringing', TALKING: 'talking' };
const SYNC_DELAY_MS = 5000;
const ACTIVE_CALL_STORAGE_KEY = 'softphone_active_call_v1';
const IPCC_TOKEN_CACHE_KEY = 'softphone_ipcc_token_v2';
const IPCC_GATEWAY_RELEASE_DELAY_MS = IPCC_CONFIG.GATEWAY_RELEASE_DELAY_MS;
const IPCC_TOKEN_TTL_MS = 60 * 60 * 1000;
/** 外呼桥接保护：分机接通→客户振铃/接通，手机号网关在此期间会发611 */
const OUTBOUND_BRIDGE_PROTECT_MS = 45000;
const GATEWAY_LINE_RULES = [
  {
    match: ['芳华未来助手', '芳华未来', '02386990189'],
    title: '润天023',
    badge: '推荐线路',
    theme: 'future',
    rules: [
      { label: '主叫显示', value: '芳华未来助手' },
      { label: '主叫号码', value: '02386990189' },
      { label: '外呼频率', value: '同一个被叫号码24h内不能拨打超过3次' },
      { label: '呼叫间隔', value: '连续发起呼叫需间隔1分钟' },
      { label: '呼叫盲区', value: '新疆、西藏、北京' },
      { label: '参考接通率', value: '—' }
    ]
  },
  {
    match: ['润天售后', '润天售后外呼', '手机外呼'],
    title: '手机外呼线路',
    badge: '备用线路',
    theme: 'shuntian',
    rules: [
      { label: '主叫显示', value: '无' },
      { label: '主叫号码', value: '手机号' },
      { label: '外呼频率', value: '无' },
      { label: '呼叫间隔', value: '无' },
      { label: '呼叫盲区', value: '北京' },
      { label: '参考接通率', value: '—' }
    ]
  },
  {
    match: ['runtian4002', '4001100202'],
    title: '润天4002',
    theme: 'runtian4002',
    rules: [
      { label: '主叫显示', value: '芳华未来助手' },
      { label: '主叫号码', value: '4001100202' },
      { label: '外呼频率', value: '同一个被叫号码24h内不能拨打超过3次' },
      { label: '呼叫间隔', value: '无' },
      { label: '呼叫盲区', value: '新疆、西藏、北京' },
      { label: '参考接通率', value: '—' }
    ]
  },
  {
    match: ['runtian400', '4001035321'],
    title: '润天4001',
    theme: 'runtian400',
    rules: [
      { label: '主叫显示', value: '芳华未来助手' },
      { label: '主叫号码', value: '4001035321' },
      { label: '外呼频率', value: '同一个被叫号码24h内不能拨打超过3次' },
      { label: '呼叫间隔', value: '无' },
      { label: '呼叫盲区', value: '新疆、西藏、北京' },
      { label: '参考接通率', value: '—' }
    ]
  }
];

const GATEWAY_RULE_COLUMNS = ['主叫显示', '主叫号码', '外呼频率', '呼叫间隔', '呼叫盲区'];
const GATEWAY_RATE_COLUMN = '参考接通率';

export default {
  name: 'SoftPhone',
  directives: {
    'click-outside': {
      bind(el, binding, vnode) {
        el.clickOutsideEvent = function(event) {
          if (!(el === event.target || el.contains(event.target))) {
            vnode.context[binding.expression]();
          }
        };
        document.body.addEventListener('click', el.clickOutsideEvent);
      },
      unbind(el) {
        document.body.removeEventListener('click', el.clickOutsideEvent);
      }
    }
  },
  data() {
    return {
      dialNumber: '',
      callDuration: '00:00',
      province: '',
      isContentFit: true,
      isRegistered: false,
      isConnected: false,
      callStatus: UI_STATE.IDLE,
      speakerVolume: VOLUME_CONFIG.DEFAULT,
      micVolume: VOLUME_CONFIG.DEFAULT,
      isSpeakerMuted: false,
      isMicMuted: false,
      showLeftButton: false,
      showRightButton: false,
      leftButtonNormal: false,
      rightButtonNormal: false,
      rightButtonHangup: false,
      callDirection: '',
      dropdownVisible: false,
      userList: {},
      currentUserId: '',
      currentUserDisplay: '',
      accountDialogVisible: false,
      accountDialogTitle: '添加账号',
      isEditMode: false,
      editingUserId: null,
      accountForm: {
        note: '',
        server: JS_SIP_CONFIG.SERVER,
        username: '',
        domain: JS_SIP_CONFIG.DOMAIN,
        loginName: '',
        password: '',
        transport: JS_SIP_CONFIG.TRANSPORT
      },
      showPassword: false,
      settingsDialogVisible: false,
      settingsForm: {
        userAgent: 'JsSIP',
        sessionExpires: 180,
        minSessionExpires: 90,
        stun: false,
        iceServer: '',
        autoAnswer: true,
        reconnect: true,
        reconnectInterval: 15
      },
      dialKeys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'],
      phone: null,
      profileManager: null,
      statusText: '',
      statusType: 'info',
      statusTimerId: null,
      isReconnecting: false,
      reconnectFailed: false,
      ccPhoneBar: null,
      ccSocketConnected: false,
      ccSocketFailed: false,
      ccConnectingPromise: null,
      ccConnectingResolve: null,
      ccConnectingReject: null,
      isCallingReady: false,
      isOnHold: false,
      _lastSetStatusTime: 0,
      _setStatusDebounceMs: 2000,
      _isSettingStatus: false,
      currentCallUuid: '',
      callUuidMap: {},
      _isHangingUp: false,
      _hangupLockTimer: null,
      _pendingSyncTimer: null,
      _syncTimers: {},
      _syncingUuids: {},
      _endedCallUuids: {},
      _ipccReconnectTimer: null,
      _ipccReconnecting: false,
      _ipccRecovering: false,
      micPermissionGranted: true,
      _pageHideHandler: null,
      dialMode: 'encrypted',
      isDecryptingForCall: false,
      systemReady: false,
      initFailed: false,
      initStatusText: '正在初始化...',
      gatewayList: [],
      selectedGatewayId: null,
      currentConnectedGatewayIds: [],
      gatewayListLoading: false,
      isGatewayReconnecting: false,
      // 高频操作控制
      _isCalling: false,
      _callLockTimer: null,
      _callTimeoutTimer: null,
      _canMakeCallDelay: 1000,
      _audioHealthTimer: null,
      _wakeLock: null,
      _visibilityHandler: null,
      _remoteHangupTimer: null,
      _lastAsrAt: 0,
      _lastCallTimerTickAt: 0,
      _lastIpccCallSignalAt: 0,
      _ccEventsBound: false,
      _mediaInactiveSince: 0,
      _ccConnectingTimeoutId: null,
      _ccConnectStartedAt: 0,
      _ipccCallConfig: null,
      _ipccConnectedAt: 0,
      _completingHangup: false,
      _callEstablishedAt: 0,
      _bridgeStartedAt: 0,
      _suppressIpccHangupUntil: 0,
      _postCallSettlingUntil: 0,
      _readyStatusSent: false,
      _initInProgress: false,
      _readyRestoreTimer: null,
      _ipccReconnectFailCount: 0
    };
  },
  computed: {
    showPlaintextOption() {
      return process.env.VUE_APP_SIP_ENABLE_PLAINTEXT_DIAL === 'true';
    },
    hangupButtonIcon() {
      return this.isInActiveCall ? 'call_end' : 'phone';
    },
    /** 是否处于活跃通话（含外呼振铃/桥接，避免 idle 却显示绿色拨号键） */
    isInActiveCall() {
      if (this.callStatus !== UI_STATE.IDLE) return true;
      if (this._isCalling || this._isHangingUp) return true;
      if (this.currentCallUuid) return true;
      if (this.phone?.hasActiveCall?.()) return true;
      if (this.phone?.hasSipSession?.()) return true;
      if (this.ccPhoneBar?.getCallConnected?.()) return true;
      if (this.ccPhoneBar?.getActiveCallUuid?.()) return true;
      return false;
    },
    hangupButtonClass() {
      if (this.isDecryptingForCall) return 'disabled';
      if (this.isInActiveCall) return 'hangup';
      if (this.canUseDialer && !this._isCalling) return 'call-ready';
      return 'disabled';
    },
    leftButtonIcon() {
      if (this.callStatus === 'ringing') return 'phone';
      if (this.callStatus === 'talking') {
        if (this.ccPhoneBar && this.ccSocketConnected) {
          return this.isOnHold ? 'play_arrow' : 'pause';
        }
        return this.phone && this.phone.IsOnHold() ? 'play_arrow' : 'pause';
      }
      return '';
    },
    rightButtonIcon() {
      if (this.callStatus === 'talking') return 'call_split';
      return '';
    },
    isHangupDisabled() {
      if (this.isDecryptingForCall) return true;
      if (this.isInActiveCall) return false;
      if (!this.canUseDialer || this._isCalling) return true;
      return false;
    },
    /** 空闲且未在拨号/通话时才能编辑或清空号码 */
    canEditDialNumber() {
      return !this.isInActiveCall && !this.isDecryptingForCall;
    },
    /** 空闲时可发起外呼；通话中始终可操作挂断/键盘 */
    canUseDialer() {
      if (this.isInActiveCall) return true;
      return this.systemReady && this.isRegistered && this.ccSocketConnected &&
        this.isCallingReady && !this.isReconnecting && !this.isGatewayReconnecting;
    },
    /** 仅初始化阶段显示遮罩，通话中不遮挡 */
    showInitMask() {
      return !this.isInActiveCall && (!this.systemReady || this.isGatewayReconnecting);
    },
    isSystemReady() {
      return this.canUseDialer;
    },
    speakerVolumePercent() {
      return Math.round(this.speakerVolume * 100);
    },
    micVolumePercent() {
      return Math.round(this.micVolume * 100);
    },
    gatewayRuleColumns() {
      return GATEWAY_RULE_COLUMNS;
    },
    gatewayRateColumn() {
      return GATEWAY_RATE_COLUMN;
    },
    isGatewayLineConnected() {
      return this.ccSocketConnected && this.isRegistered && this.currentConnectedGatewayIds.length > 0;
    },
    selectedGatewayTitle() {
      if (!this.selectedGatewayId) return '';
      const gw = this.gatewayList.find(g => String(g.id) === String(this.selectedGatewayId));
      return gw ? this.getGatewayDisplayTitle(gw) : '未知线路';
    }
  },
  watch: {
    speakerVolume(val) {
      if (this.phone) this.phone.SetSpeaker(this.isSpeakerMuted, val);
    },
    micVolume(val) {
      if (this.phone) this.phone.SetMicPhone(this.isMicMuted, val);
    },
    dialNumber() {
      this.updateContentAlignment();
    }
  },
  async mounted() {
    this.profileManager = new ProfileManager();
    const profile = this.profileManager.getProfile();
    this.userList = profile.users || {};
    this.currentUserId = profile.user || '';
    if (this.currentUserId && this.userList[this.currentUserId]) {
      this.currentUserDisplay = this.userList[this.currentUserId].note || this.userList[this.currentUserId].user;
    }
    const settings = this.profileManager.getSettings();
    this.settingsForm = this._mapSettingsToForm(settings);
    if (this.profileManager.getSettings().auto_answer === undefined) {
      this.profileManager.updateSettings({ auto_answer: true });
      this.settingsForm.autoAnswer = true;
    }
    this.speakerVolume = profile.speaker_volume !== undefined ? profile.speaker_volume : 0.8;
    this.micVolume = profile.mic_volume !== undefined ? profile.mic_volume : 0.8;
    this.isSpeakerMuted = profile.speaker_paused || false;
    this.isMicMuted = profile.mic_paused || false;
    this._pageHideHandler = (event) => {
      if (event && event.persisted) return;
      this._releaseMicrophoneResources();
    };
    window.addEventListener('pagehide', this._pageHideHandler);
    await this._checkMicrophoneOnEnter();
    await this._ensureCleanIpccState(false);
    await this.initCCAndStart();
    await this.loadGatewayList();
    this._processStaleSyncs();
    this._visibilityHandler = this._handleVisibilityChange.bind(this);
    document.addEventListener('visibilitychange', this._visibilityHandler);
    window.addEventListener('beforeunload', this.handleBeforeUnload);
  },
  beforeDestroy() {
    if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
    this.destroyAllConnections();
    window.removeEventListener('beforeunload', this.handleBeforeUnload);
    if (this._pageHideHandler) window.removeEventListener('pagehide', this._pageHideHandler);
    if (this._visibilityHandler) document.removeEventListener('visibilitychange', this._visibilityHandler);
    this.clearAllTimers();
    this.removeEventListeners();
    this.stopAudioHealthCheck();
    this._releaseWakeLock();
    this._clearAllSyncTimers();
  },
  methods: {
    _mapSettingsToForm(settings) {
      return {
        userAgent: settings.user_agent || JS_SIP_CONFIG.USER_AGENT,
        sessionExpires: settings.session_expires || JS_SIP_CONFIG.SESSION_EXPIRES,
        minSessionExpires: settings.min_session_expires || JS_SIP_CONFIG.MIN_SESSION_EXPIRES,
        stun: settings.stun || false,
        iceServer: settings.ice_server || '',
        autoAnswer: settings.auto_answer !== undefined ? settings.auto_answer : true,
        reconnect: settings.reconnect !== undefined ? settings.reconnect : true,
        reconnectInterval: settings.reconnect_interval || JS_SIP_CONFIG.RECONNECT_INTERVAL
      };
    },
    getLeftButtonTitle() {
      if (this.callStatus === 'ringing') return '接听来电';
      if (this.callStatus === 'talking') return this.isOnHold ? '恢复通话' : '保持通话';
      return '';
    },
    getHangupButtonTitle() {
      if (this.isDecryptingForCall) return '解密中...';
      if (this.isInActiveCall) return '结束通话';
      if (this.isSystemReady && !this._isCalling) return '发起外呼';
      if (this._isCalling) return '呼叫中...';
      if (this.initFailed) return '连接失败';
      return '连接中，请稍候...';
    },
    getRightButtonTitle() {
      if (this.callStatus === 'talking') return '呼叫转移';
      return '';
    },
    getHangupButtonClass() {
      return this.hangupButtonClass;
    },
    updateContentAlignment() {
      this.$nextTick(() => {
        const inputEl = this.$refs.dialerInput;
        if (!inputEl) return;
        this.isContentFit = inputEl.scrollWidth <= inputEl.clientWidth;
      });
    },
    scrollInputToEnd() {
      const inputEl = this.$refs.dialerInput;
      if (!inputEl) return;
      this.$nextTick(() => {
        inputEl.focus();
        inputEl.setSelectionRange(this.dialNumber.length, this.dialNumber.length);
        inputEl.scrollLeft = inputEl.scrollWidth - inputEl.clientWidth;
      });
    },
    clearDialNumber() {
      if (!this.dialNumber || !this.canEditDialNumber) return;
      this.dialNumber = '';
      this.$nextTick(() => {
        const inputEl = this.$refs.dialerInput;
        if (inputEl) {
          inputEl.focus();
          this.updateContentAlignment();
        }
      });
    },
    showStatus(text, type = 'info') {
      let displayText = text;
      if (type === 'error' && text) {
        if (text.length > 30) displayText = text.substring(0, 30) + '...';
        displayText = displayText.replace(/wss?:\/\/[^/\s]+/g, '***');
        displayText = displayText.replace(/password=\w+/i, 'password=***');
      }
      if (this._isInCallStatusLocked() && !this._isAllowedInCallStatus(displayText)) {
        this._refreshInCallStatusIndicator();
        return;
      }
      if (this.statusTimerId) clearTimeout(this.statusTimerId);
      this.statusText = displayText;
      this.statusType = type;
      if (text === '就绪') return;
      if (this.callStatus === UI_STATE.TALKING) return;
      this.statusTimerId = setTimeout(() => {
        if (this.callStatus === UI_STATE.TALKING) {
          this._refreshInCallStatusIndicator();
          return;
        }
        if (this.isRegistered && this.ccSocketConnected && this.isCallingReady) {
          this.statusText = '就绪';
          this.statusType = 'success';
        } else {
          this.statusText = '';
        }
        this.statusTimerId = null;
      }, 10000);
    },
    _isInCallStatusLocked() {
      return this.callStatus === UI_STATE.TALKING || this.callStatus === UI_STATE.RINGING;
    },
    _isAllowedInCallStatus(text) {
      const t = String(text || '');
      return ['通话中', '已暂停通话', '振铃', '客户振铃', '拨号', '线路恢复', '已挂机', '对方已挂机', '通话已结束'].some((s) => t.includes(s));
    },
    _refreshInCallStatusIndicator() {
      if (this.statusTimerId) {
        clearTimeout(this.statusTimerId);
        this.statusTimerId = null;
      }
      if (this._ipccRecovering && (this.callStatus === UI_STATE.TALKING || this.callStatus === UI_STATE.RINGING)) {
        this.statusText = this.callStatus === UI_STATE.TALKING ? '通话中（线路恢复中）' : '振铃中（线路恢复中）';
        this.statusType = 'warn';
        return;
      }
      if (this.callStatus === UI_STATE.TALKING) {
        this.statusText = this.isOnHold ? '已暂停通话' : (this._isMobileGatewayLine() ? '通话中（音频在手机）' : '通话中');
        this.statusType = this.isOnHold ? 'warn' : 'success';
        return;
      }
      if (this.callStatus === UI_STATE.RINGING) {
        this.statusText = this.callDirection === 'outbound' ? '客户振铃中...' : '振铃中...';
        this.statusType = 'info';
      }
    },
    _promoteToTalkingIfNeeded() {
      if (this.callStatus === UI_STATE.TALKING) {
        this._refreshInCallStatusIndicator();
        return;
      }
      this.callStatus = UI_STATE.TALKING;
      this._callEstablishedAt = Date.now();
      this.showLeftButton = true;
      this.showRightButton = true;
      this.leftButtonNormal = true;
      this.rightButtonNormal = true;
      if (this.phone?.startCallTimer && this.callDuration === '00:00') {
        this.phone.startCallTimer();
      }
      this._refreshInCallStatusIndicator();
      this.startAudioHealthCheck();
    },
    async initCCAndStart() {
      if (this._initInProgress) return;
      this._initInProgress = true;
      this.systemReady = false;
      this.initFailed = false;
      this.ccSocketConnected = false;
      this.ccSocketFailed = false;
      this.isCallingReady = false;
      this.initStatusText = '正在连接呼叫中心...';
      try {
        await this.ensureCCSocketConnect();
        this.initStatusText = '等待坐席就绪...';
        await this.waitForAgentReady(IPCC_CONFIG.AGENT_READY_TIMEOUT_MS);
        this.initStatusText = '正在注册 SIP...';
        await this.startPhone();
        await this.waitForRegistration(IPCC_CONFIG.SIP_REGISTER_TIMEOUT_MS);
        if (!this.isRegistered || !this.ccSocketConnected || !this.isCallingReady) {
          throw new Error('系统未完全就绪，请稍后重试');
        }
        this.systemReady = true;
        this.initFailed = false;
        this.initStatusText = '';
        this.showStatus('就绪', 'success');
      } catch (err) {
        console.error('[连接流程] 初始化失败:', err.message);
        this.systemReady = false;
        this.initFailed = true;
        this.ccSocketFailed = true;
        this.initStatusText = '连接失败: ' + (err.message || '未知错误');
        this.showStatus(`初始化失败: ${err.message}`, 'error');
        if (err.message.includes('分机号')) {
          this.$message({
            message: '未配置分机号，请联系管理员在【系统管理】-【员工管理】页面绑定 SIP 角色',
            type: 'warning',
            duration: 5000,
            showClose: true
          });
        }
      } finally {
        this._initInProgress = false;
      }
    },
    async retryInit() {
      this.initStatusText = '正在重新连接...';
      this.initFailed = false;
      await this._ensureCleanIpccState(true);
      await this.initCCAndStart();
      if (this.systemReady) await this.loadGatewayList();
    },
    waitForAgentReady(timeout = IPCC_CONFIG.AGENT_READY_TIMEOUT_MS) {
      return new Promise((resolve, reject) => {
        if (this.isCallingReady) { resolve(); return; }
        const startTime = Date.now();
        const checkInterval = setInterval(() => {
          if (this.isCallingReady) { clearInterval(checkInterval); resolve(); }
          else if (Date.now() - startTime > timeout) { clearInterval(checkInterval); reject(new Error('坐席状态未就绪')); }
        }, 100);
      });
    },
    ensureCCSocketConnect() {
      if (this.ccSocketConnected && !this._ipccRecovering) return Promise.resolve();
      if (this.ccConnectingPromise) return this.ccConnectingPromise;
      if (this.ccPhoneBar) {
        return this._reconnectExistingIpcc();
      }
      this.ccConnectingPromise = new Promise((resolve, reject) => {
        this.ccConnectingResolve = resolve;
        this.ccConnectingReject = reject;
        this._doConnectCCSocket();
      });
      return this.ccConnectingPromise;
    },
    _clearCcConnectingState() {
      if (this._ccConnectingTimeoutId) {
        clearTimeout(this._ccConnectingTimeoutId);
        this._ccConnectingTimeoutId = null;
      }
      this.ccConnectingResolve = null;
      this.ccConnectingReject = null;
      this.ccConnectingPromise = null;
      this._ipccReconnecting = false;
      this._ccConnectStartedAt = 0;
    },
    _finishCcConnecting(success, err) {
      const resolve = this.ccConnectingResolve;
      const reject = this.ccConnectingReject;
      if (this._ccConnectingTimeoutId) {
        clearTimeout(this._ccConnectingTimeoutId);
        this._ccConnectingTimeoutId = null;
      }
      this.ccConnectingResolve = null;
      this.ccConnectingReject = null;
      this.ccConnectingPromise = null;
      this._ipccReconnecting = false;
      this._ccConnectStartedAt = 0;
      if (success && resolve) resolve();
      else if (!success && reject) reject(err || new Error('IPCC连接失败'));
    },
    _getGatewayCacheKey(extNum, gatewayId) {
      const gw = gatewayId ? String(gatewayId) : 'default';
      return `${extNum}:${gw}`;
    },
    _readIpccTokenStore() {
      try {
        const raw = sessionStorage.getItem(IPCC_TOKEN_CACHE_KEY);
        if (!raw) return { entries: {} };
        const store = JSON.parse(raw);
        return store?.entries ? store : { entries: {} };
      } catch (e) {
        return { entries: {} };
      }
    },
    _writeIpccTokenStore(store) {
      try {
        sessionStorage.setItem(IPCC_TOKEN_CACHE_KEY, JSON.stringify(store));
      } catch (e) {}
    },
    _getIpccTokenCache(extNum, gatewayId) {
      if (!extNum) return null;
      const key = this._getGatewayCacheKey(extNum, gatewayId);
      const entry = this._readIpccTokenStore().entries[key];
      if (!entry?.callConfig?.loginToken || !entry.expiresAt) return null;
      if (Date.now() > entry.expiresAt) {
        this._clearIpccTokenCache(extNum, gatewayId);
        return null;
      }
      return entry;
    },
    _saveIpccTokenCache(extNum, gatewayId, callConfig) {
      if (!extNum || !callConfig) return;
      const store = this._readIpccTokenStore();
      const key = this._getGatewayCacheKey(extNum, gatewayId);
      store.entries[key] = {
        callConfig,
        expiresAt: Date.now() + IPCC_TOKEN_TTL_MS
      };
      this._writeIpccTokenStore(store);
    },
    _clearIpccTokenCache(extNum, gatewayId) {
      try {
        if (!extNum) {
          sessionStorage.removeItem(IPCC_TOKEN_CACHE_KEY);
          return;
        }
        const store = this._readIpccTokenStore();
        if (gatewayId != null && gatewayId !== '') {
          delete store.entries[this._getGatewayCacheKey(extNum, gatewayId)];
        } else {
          Object.keys(store.entries).forEach((k) => {
            if (k.startsWith(`${extNum}:`)) delete store.entries[k];
          });
        }
        this._writeIpccTokenStore(store);
      } catch (e) {}
    },
    _buildCallConfig(configData, extNum) {
      return {
        useDefaultUi: false,
        loginToken: configData.loginToken,
        ipccServer: IPCC_CONFIG.SERVER_PROD,
        gatewayList: configData.gatewayList,
        gatewayEncrypted: false,
        extPassword: configData.encryptPsw,
        extnum: extNum,
        opnum: configData.opNum || configData.userName,
        enableWss: true,
        enableHeartBeat: true,
        heartBeatIntervalSecs: IPCC_CONFIG.HEARTBEAT_INTERVAL
      };
    },
    async _loadIpccCallConfig(extNum, myGateway, forceRefresh = false) {
      const gatewayKey = myGateway ? String(myGateway) : 'default';
      if (!forceRefresh) {
        const cached = this._getIpccTokenCache(extNum, gatewayKey);
        if (cached?.callConfig) {
          const remainMin = Math.round((cached.expiresAt - Date.now()) / 60000);
          return cached.callConfig;
        }
      }
      const basicRes = await getToolbarBasicParam({ extNum, myGateway });
      if (basicRes.code !== 0) throw new Error(basicRes.message || '获取配置失败');
      const configData = basicRes.data;
      if (!configData.loginToken) throw new Error('登录令牌无效');
      const callConfig = this._buildCallConfig(configData, extNum);
      this._saveIpccTokenCache(extNum, gatewayKey, callConfig);
      return callConfig;
    },
    async _ensureCleanIpccState(force = false) {
      if (this.isInActiveCall) {
        console.warn('[IPCC] 通话中，跳过连接清理');
        return;
      }
      const needCleanup = force || this.ccPhoneBar || this.ccConnectingPromise ||
        this._ipccReconnecting || (this.ccSocketConnected && !this.systemReady);
      if (!needCleanup) return;
      if (this._ipccReconnectTimer) {
        clearTimeout(this._ipccReconnectTimer);
        this._ipccReconnectTimer = null;
      }
      if (this.ccConnectingPromise) {
        this._finishCcConnecting(false, new Error('连接重置'));
      }
      this._ipccRecovering = false;
      this._ipccReconnecting = false;
      if (this.phone) {
        try { this.phone.destroy(); } catch (e) {}
        this.phone = null;
      }
      if (this.ccPhoneBar) {
        this._forceReleaseIpccCallState();
        await this._gracefulDisconnectIpcc(IPCC_CONFIG.DISCONNECT_WAIT_MS || 2000);
        this.ccPhoneBar = null;
      }
      this._ccEventsBound = false;
      this.ccSocketConnected = false;
      this.isCallingReady = false;
      this.isRegistered = false;
      await new Promise((r) => setTimeout(r, force ? IPCC_GATEWAY_RELEASE_DELAY_MS : 200));
    },
    _isIpccMediaActive() {
      if (!this.ccPhoneBar || !this.ccSocketConnected) return false;
      return this._ipcCallSessionActive();
    },
    /** IPCC 侧是否仍有通话会话（不含 ASR 推断，避免误判挂机） */
    _ipcCallSessionActive() {
      if (!this.ccPhoneBar) return false;
      if (this.ccPhoneBar.getCallConnected && this.ccPhoneBar.getCallConnected()) return true;
      if (this.ccPhoneBar.getActiveCallUuid && this.ccPhoneBar.getActiveCallUuid()) return true;
      return false;
    },
    _touchIpccCallSignal() {
      this._lastIpccCallSignalAt = Date.now();
    },
    /** 手机线路：IPCC callConnected 易 stale，结合 SIP/ASR/计时器判断是否已挂机 */
    _shouldTreatMobileLineAsEnded() {
      if (!this._isMobileGatewayLine()) return false;
      if (this.callStatus !== UI_STATE.TALKING && this.callStatus !== UI_STATE.RINGING) return false;
      if (!this._ipcCallSessionActive()) return true;
      if (this.callStatus === UI_STATE.RINGING) return false;
      const sipAlive = this.phone?.hasActiveCall?.() || this.phone?.hasSipSession?.();
      const asrRecent = this._lastAsrAt && Date.now() - this._lastAsrAt < 3000;
      const timerStale = this._lastCallTimerTickAt && Date.now() - this._lastCallTimerTickAt > 3500;
      if (timerStale && !asrRecent) return true;
      if (!sipAlive && !asrRecent) return true;
      return false;
    },
    _syncMobileRemoteHangupIfNeeded(reason) {
      if (!this._shouldTreatMobileLineAsEnded()) return false;
      if (this._completingHangup || (this._isPostCallSettling() && this._isHangingUp)) return false;
      console.warn('[手机线路] 远端已挂机，同步电脑:', reason);
      this._forceReleaseIpccCallState();
      this._completeCallHangup('ipcc', '对方已挂机');
      return true;
    },
    /** IPCC 608 话后整理 = 服务端已挂机，需同步桌面 UI */
    _handleAgentPostCallStatus(statusCode) {
      if (statusCode !== AgentStatusEnum.FILL_FORM) return;
      if (this.callStatus === UI_STATE.IDLE && !this.isInActiveCall) return;
      if (this._completingHangup) return;
      this._forceReleaseIpccCallState();
      this._completeCallHangup('ipcc', '已挂机');
    },
    _scheduleMobileRemoteHangupSync(reason, delayMs = 600) {
      if (!this._isMobileGatewayLine()) return;
      if (this.callStatus !== UI_STATE.TALKING && this.callStatus !== UI_STATE.RINGING) return;
      this._scheduleRemoteHangupCheck(reason, delayMs);
    },
    /** 外呼桥接开始：手机号网关在分机接通→客户接通期间会提前发611，需保护 */
    _startOutboundBridgeProtect(reason = '') {
      const now = Date.now();
      this._bridgeStartedAt = now;
      this._suppressIpccHangupUntil = now + OUTBOUND_BRIDGE_PROTECT_MS;
      if (this.ccPhoneBar && typeof this.ccPhoneBar.setCallConnected === 'function') {
        this.ccPhoneBar.setCallConnected(true);
      }
      if (reason) console.debug('[桥接保护] 启动', reason, OUTBOUND_BRIDGE_PROTECT_MS + 'ms');
    },
    /** 仅外呼振铃桥接阶段：SIP 可能短暂断开，需等 IPCC 确认 */
    _isRingingBridgeProtected() {
      if (this.callStatus !== UI_STATE.RINGING || !this.currentCallUuid) return false;
      if (!this.ccPhoneBar || !this.ccSocketConnected) return false;
      if (this._suppressIpccHangupUntil && Date.now() < this._suppressIpccHangupUntil) return true;
      if (this._bridgeStartedAt && Date.now() - this._bridgeStartedAt < OUTBOUND_BRIDGE_PROTECT_MS) return true;
      if (this.ccPhoneBar.getCallConnected && this.ccPhoneBar.getCallConnected()) return true;
      if (this._callEstablishedAt && Date.now() - this._callEstablishedAt < 5000) return true;
      return this._ipcCallSessionActive();
    },
    _forceReleaseIpccCallState() {
      if (!this.ccPhoneBar) return;
      this.ccPhoneBar.clearActiveCallUuid();
      if (typeof this.ccPhoneBar.setCallConnected === 'function') {
        this.ccPhoneBar.setCallConnected(false);
      }
      if (typeof this.ccPhoneBar.resetOutboundLock === 'function') {
        this.ccPhoneBar.resetOutboundLock();
      }
    },
    _isPostCallSettling() {
      return !!(this._postCallSettlingUntil && Date.now() < this._postCallSettlingUntil);
    },
    _beginPostCallSettling(ms = 8000) {
      this._postCallSettlingUntil = Date.now() + ms;
    },
    _tryMarkSystemReady() {
      const ipccOk = this.ccSocketConnected && this.isCallingReady;
      const sipOk = this.isRegistered || this._isMobileGatewayLine();
      if (ipccOk && sipOk) {
        this.systemReady = true;
        this.initFailed = false;
        this.showStatus('就绪', 'success');
        return true;
      }
      if (ipccOk && !this.isRegistered && this.phone?.ua && !this.phone.isReconnecting) {
        try {
          if (!this.phone.ua.isRegistered()) this.phone.Register();
        } catch (e) {}
      }
      return false;
    },
    _restoreReadyAfterCall() {
      if (this._readyRestoreTimer) {
        clearTimeout(this._readyRestoreTimer);
        this._readyRestoreTimer = null;
      }
      this._resetOutboundLocks();
      this.currentCallUuid = '';
      this._callEstablishedAt = 0;
      this._bridgeStartedAt = 0;
      this._suppressIpccHangupUntil = 0;
      this._forceReleaseIpccCallState();
      this._readyStatusSent = false;
      const deadline = Date.now() + (IPCC_CONFIG.POST_CALL_READY_DEADLINE_MS || 3000);
      const tick = () => {
        this._readyRestoreTimer = null;
        if (this.callStatus !== UI_STATE.IDLE) return;
        if (this._tryMarkSystemReady()) return;
        if (!this._readyStatusSent && this.ccPhoneBar && this.ccSocketConnected && !this.isCallingReady) {
          this._readyStatusSent = true;
          try {
            if (typeof this.ccPhoneBar.setStatusBusy === 'function') {
              this.ccPhoneBar.setStatusBusy();
            } else {
              this._safeSetStatus(AgentStatusEnum.BUSY, '话后恢复就绪');
            }
          } catch (e) {}
        }
        if (this._tryMarkSystemReady()) return;
        if (Date.now() >= deadline) {
          if (this.ccSocketConnected && this.isCallingReady) {
            this.systemReady = true;
            this.initFailed = false;
            this.showStatus('就绪', 'success');
            return;
          }
          if (!this.ccSocketConnected) {
            console.warn('[就绪恢复] IPCC 未连接，触发快速重连');
            this._scheduleIpccReconnect(false);
          }
          return;
        }
        this._readyRestoreTimer = setTimeout(tick, IPCC_CONFIG.READY_RESTORE_INTERVAL_MS || 200);
      };
      this._readyRestoreTimer = setTimeout(tick, IPCC_CONFIG.READY_RESTORE_INITIAL_MS || 50);
    },
    async _gracefulDisconnectIpcc(waitMs) {
      if (!this.ccPhoneBar) return;
      const disconnectWait = waitMs || IPCC_CONFIG.DISCONNECT_WAIT_MS || 2000;
      try {
        if (typeof this.ccPhoneBar.forceDisconnect === 'function') {
          this.ccPhoneBar.forceDisconnect();
          await new Promise((r) => setTimeout(r, Math.min(disconnectWait, 1200)));
        } else if (typeof this.ccPhoneBar.disconnectAndWait === 'function') {
          await this.ccPhoneBar.disconnectAndWait(disconnectWait);
        } else {
          this.ccPhoneBar.disconnect();
          await new Promise((r) => setTimeout(r, Math.min(disconnectWait, 1200)));
        }
      } catch (e) {}
      await new Promise((r) => setTimeout(r, IPCC_GATEWAY_RELEASE_DELAY_MS));
    },
    _getIpccConnectTimeout(isReconnect = false) {
      return isReconnect
        ? (IPCC_CONFIG.RECONNECT_CONNECT_TIMEOUT || 5000)
        : (IPCC_CONFIG.CONNECT_TIMEOUT || 8000);
    },
    _shouldIgnoreLoginConflict() {
      if (!this.ccPhoneBar) return false;
      const sdkConnected = this.ccPhoneBar.getIsConnected && this.ccPhoneBar.getIsConnected();
      if (!this.ccSocketConnected || !sdkConnected) return false;
      if (this._ipccConnectedAt && Date.now() - this._ipccConnectedAt < 10000) return true;
      if (this.isCallingReady || this.systemReady) return true;
      return false;
    },
    async _reconnectExistingIpcc() {
      if (this._ipccReconnecting && this.ccConnectingPromise) {
        return this.ccConnectingPromise;
      }
      this._ipccReconnecting = true;
      this._ccConnectStartedAt = Date.now();
      this.ccConnectingPromise = new Promise((resolve, reject) => {
        this.ccConnectingResolve = resolve;
        this.ccConnectingReject = reject;
        try {
          if (!this._ccEventsBound) {
            this._bindCCEvents();
            this._ccEventsBound = true;
          }
          if (this.ccPhoneBar.resetKickedState) this.ccPhoneBar.resetKickedState();
          if (typeof this.ccPhoneBar.reconnect === 'function') {
            this.ccPhoneBar.reconnect();
          } else {
            this.ccPhoneBar.connect();
          }
          this._ccConnectingTimeoutId = setTimeout(() => {
            if (!this.ccSocketConnected) {
              this._finishCcConnecting(false, new Error('IPCC重连超时'));
            }
          }, this._getIpccConnectTimeout(true));
        } catch (err) {
          this._finishCcConnecting(false, err);
        }
      });
      try {
        await this.ccConnectingPromise;
      } catch (err) {
        throw err;
      }
    },
    _scheduleIpccReconnect(duringCall, extraDelayMs = 0) {
      if (!duringCall && this.callStatus === UI_STATE.IDLE &&
        this.ccSocketConnected && this.isCallingReady && !this._ipccRecovering) {
        return;
      }
      if (this._ipccReconnectTimer) {
        if (duringCall) return;
        clearTimeout(this._ipccReconnectTimer);
        this._ipccReconnectTimer = null;
      }
      if (this._ipccReconnecting) return;
      if (this.ccConnectingPromise) {
        const elapsed = this._ccConnectStartedAt ? Date.now() - this._ccConnectStartedAt : 0;
        const reconnectTimeout = this._getIpccConnectTimeout(true);
        if (elapsed < reconnectTimeout + 500) return;
        console.warn('[IPCC] 连接 Promise 超时未结束，强制清理后重试');
        this._finishCcConnecting(false, new Error('连接状态超时'));
      }
      if (this._ipccReconnectTimer) clearTimeout(this._ipccReconnectTimer);
      const delay = (duringCall
        ? (IPCC_CONFIG.RECONNECT_DELAY_IN_CALL_MS || 200)
        : (IPCC_CONFIG.RECONNECT_DELAY_MS || 300)) + (extraDelayMs || 0);
      if (!duringCall && this.callStatus === UI_STATE.IDLE) {
        this.showStatus('正在快速重连...', 'warn');
      }
      this._ipccReconnectTimer = setTimeout(async () => {
        this._ipccReconnectTimer = null;
        if (this.ccSocketConnected && !this._ipccRecovering) return;
        try {
          if (this.ccPhoneBar) {
            await this._reconnectExistingIpcc();
          } else {
            await this.ensureCCSocketConnect();
          }
          this._ipccRecovering = false;
          if (this.callStatus === UI_STATE.TALKING || this.callStatus === UI_STATE.RINGING) {
            this._refreshInCallStatusIndicator();
          } else if (this.callStatus === UI_STATE.IDLE) {
            this.showStatus('就绪', 'success');
          }
        } catch (err) {
          if (!duringCall) {
            console.error('[IPCC] 自动重连失败:', err.message || err);
          } else {
            console.debug('[IPCC] 通话中后台重连失败:', err.message || err);
          }
          if (!duringCall && this.callStatus === UI_STATE.IDLE && this.ccPhoneBar) {
            try {
              const extRes = await myCallUser();
              if (extRes.code === 200 && extRes.data?.extNum) {
                const { extNum, gatewayIds: myGateway } = extRes.data;
                const callConfig = await this._loadIpccCallConfig(extNum, myGateway, true);
                this.ccPhoneBar.initConfig(callConfig);
                await this._reconnectExistingIpcc();
                this._ipccRecovering = false;
                if (this.callStatus === UI_STATE.TALKING || this.callStatus === UI_STATE.RINGING) {
                  this._refreshInCallStatusIndicator();
                } else {
                  this.showStatus('就绪', 'success');
                }
                return;
              }
            } catch (retryErr) {
              console.error('[IPCC] 刷新 token 后重连仍失败:', retryErr.message || retryErr);
            }
          }
          if (duringCall && this.callStatus !== UI_STATE.IDLE) {
            if (this._ipccReconnectFailCount == null) this._ipccReconnectFailCount = 0;
            this._ipccReconnectFailCount += 1;
            const backoff = this._ipccReconnectFailCount <= 8
              ? 0
              : Math.min(5000, 350 * (this._ipccReconnectFailCount - 8));
            this._scheduleIpccReconnect(true, backoff);
          } else if (this.callStatus === UI_STATE.IDLE) {
            console.warn('[IPCC] 空闲态重连失败，请刷新页面');
            this.showStatus('重连失败，请刷新页面', 'error');
          }
        }
      }, delay);
    },
    _ensureCallTracking() {
      if (!this.callUuidMap) this.callUuidMap = {};
      if (!this._endedCallUuids) this._endedCallUuids = {};
      if (!this._syncingUuids) this._syncingUuids = {};
      if (!this._syncTimers) this._syncTimers = {};
    },
    _syncCallUuidToSdk(uuid) {
      if (uuid && this.ccPhoneBar && typeof this.ccPhoneBar.setActiveCallUuid === 'function') {
        this.ccPhoneBar.setActiveCallUuid(uuid);
      }
    },
    _isCallNotExistsError(msg) {
      const text = msg?.msg || '';
      return text.includes('通话不存在') || text.includes('不存在');
    },
    _persistActiveCallSnapshot() {
      if (!this.currentCallUuid || this.callStatus === UI_STATE.IDLE) return;
      try {
        sessionStorage.setItem(ACTIVE_CALL_STORAGE_KEY, JSON.stringify({
          uuid: this.currentCallUuid,
          callStatus: this.callStatus,
          callDirection: this.callDirection,
          dialNumber: this.dialNumber,
          callUuidMapEntry: this.callUuidMap[this.currentCallUuid] || null,
          savedAt: Date.now()
        }));
      } catch (e) {}
    },
    _restoreActiveCallSnapshot() {
      try {
        const raw = sessionStorage.getItem(ACTIVE_CALL_STORAGE_KEY);
        if (!raw) return;
        const snap = JSON.parse(raw);
        if (!snap?.uuid) return;
        if (!this.currentCallUuid) this.currentCallUuid = snap.uuid;
        if (snap.callStatus && this.callStatus === UI_STATE.IDLE) {
          this.callStatus = snap.callStatus;
        }
        if (snap.callDirection && !this.callDirection) this.callDirection = snap.callDirection;
        if (snap.callUuidMapEntry && !this.callUuidMap[snap.uuid]) {
          this.callUuidMap[snap.uuid] = snap.callUuidMapEntry;
        }
        this._syncCallUuidToSdk(this.currentCallUuid);
      } catch (e) {}
    },
    _clearActiveCallSnapshot() {
      try { sessionStorage.removeItem(ACTIVE_CALL_STORAGE_KEY); } catch (e) {}
    },
    _markCallEnded(callUuid) {
      this._ensureCallTracking();
      if (callUuid) this._endedCallUuids[callUuid] = Date.now();
    },
    _isCallAlreadyEnded(callUuid) {
      if (!callUuid) return this.callStatus === UI_STATE.IDLE;
      return !!(this._endedCallUuids?.[callUuid] || this.callUuidMap[callUuid]?.synced);
    },
    _handleCallSessionNotFound(msg) {
      const callUuid = this.currentCallUuid;
      if (this._isPostCallSettling() || this._completingHangup) {
        console.debug('[IPCC] 话后稳定期，忽略会话不存在', msg?.msg || '');
        return;
      }
      if (this._isCallAlreadyEnded(callUuid) || this.callStatus === UI_STATE.IDLE) {
        console.debug('[IPCC] 通话已结束/已同步，忽略会话不存在', callUuid || '');
        return;
      }
      if (this._isHangingUp) {
        console.debug('[IPCC] 主动挂机中，忽略会话不存在');
        return;
      }
      console.warn('[IPCC] 服务端无通话会话，视为远端已挂机:', msg?.msg || '');
      this._completeCallHangup('ipcc', '对方已挂机');
    },
    _ipcCallAlive() {
      if (this._ipcCallSessionActive()) return true;
      if (this._ipccRecovering && this._lastAsrAt && Date.now() - this._lastAsrAt < 5000) return true;
      return false;
    },
    /** 当前是否为手机号主叫网关（音频在手机，桌面以 IPCC 为准） */
    _isMobileGatewayLine() {
      const gwId = this.currentConnectedGatewayIds[0] || this.selectedGatewayId;
      if (!gwId) return false;
      const gw = this.gatewayList.find((g) => String(g.id) === String(gwId));
      return this.getGatewayRuleInfo(gw)?.theme === 'shuntian';
    },
    _isRemoteCallAlive() {
      if (!this.ccPhoneBar || !this.ccSocketConnected) return false;
      if (this._ipccRecovering && this._ipcCallSessionActive()) return true;
      if (this._isMobileGatewayLine()) {
        if (this._shouldTreatMobileLineAsEnded()) return false;
        return this._ipcCallSessionActive();
      }
      return this._ipcCallSessionActive() && this.callStatus === UI_STATE.TALKING;
    },
    _abortOutboundAttempt(statusText, type = 'warn') {
      if (this._callTimeoutTimer) {
        clearTimeout(this._callTimeoutTimer);
        this._callTimeoutTimer = null;
      }
      this._releaseCallLock();
      this._resetCallState();
      this._forceReleaseIpccCallState();
      if (this.phone) {
        try { this.phone.Terminate(); } catch (e) {}
      }
      this.showStatus(statusText, type);
      this._restoreReadyAfterCall();
    },
    _stopCallUiImmediately() {
      if (this.phone?.stopCallTimer) this.phone.stopCallTimer();
      if (this.phone?.clearIceIssueTimer) this.phone.clearIceIssueTimer();
      this.callDuration = '00:00';
    },
    _cancelRemoteHangupCheck() {
      if (this._remoteHangupTimer) {
        clearTimeout(this._remoteHangupTimer);
        this._remoteHangupTimer = null;
      }
    },
    _scheduleRemoteHangupCheck(reason, delayMs = 8000) {
      if (this.callStatus !== UI_STATE.TALKING && this.callStatus !== UI_STATE.RINGING) return;
      if (this._remoteHangupTimer) return;
      const delay = this.callStatus === UI_STATE.TALKING ? Math.min(delayMs, 1500) : delayMs;
      this._remoteHangupTimer = setTimeout(() => {
        this._remoteHangupTimer = null;
        if (this.callStatus === UI_STATE.IDLE) return;
        if (this._isMobileGatewayLine() && this._syncMobileRemoteHangupIfNeeded(reason)) return;
        if (this._ipccRecovering && this._ipcCallAlive()) return;
        const sipAlive = this.phone?.hasActiveCall?.() || this.phone?.hasSipSession?.();
        if (sipAlive) return;
        if (this.callStatus === UI_STATE.RINGING && this._isRingingBridgeProtected()) {
          this._scheduleRemoteHangupCheck(reason, 2000);
          return;
        }
        if (this._isRemoteCallAlive()) return;
        this._completeCallHangup('sip', '对方已挂机');
      }, delay);
    },
    _completeCallHangup(source, statusText) {
      const callUuid = this.currentCallUuid;
      if (this.callStatus === UI_STATE.IDLE && !this.isInActiveCall) return;
      if (callUuid && this._endedCallUuids?.[callUuid]) return;
      if (this._completingHangup) return;
      if (source === 'user') {
        this._beginPostCallSettling(8000);
      }
      this._completingHangup = true;
      try {
        this._suppressIpccHangupUntil = 0;
        this._bridgeStartedAt = 0;
        this._stopCallUiImmediately();
        this._cancelRemoteHangupCheck();
        if (this._isHangingUp) this._clearHangupLock();
        if (source !== 'ipcc' && this.ccPhoneBar && this.ccSocketConnected) {
          try { this.ccPhoneBar.hangup(); } catch (e) {}
        } else if (source === 'ipcc') {
          this._forceReleaseIpccCallState();
        }
        if (this.phone) {
          if (typeof this.phone.markIntentionalHangup === 'function') {
            this.phone.markIntentionalHangup();
          }
          try { this.phone.Terminate(); } catch (e) {}
        }
        this.showStatus(statusText, 'info');
        this._onCallEnded(callUuid);
      } finally {
        this._completingHangup = false;
      }
    },
    _finalizeRemoteHangup(reason) {
      if (this.callStatus === UI_STATE.TALKING) {
        console.warn('[挂机] 通话中忽略非 IPCC 挂机信号:', reason);
        return;
      }
      if (this.callStatus === UI_STATE.RINGING && this._isRingingBridgeProtected()) {
        console.debug('[挂机] 振铃桥接受保护，忽略:', reason);
        return;
      }
      if (this._suppressIpccHangupUntil && Date.now() < this._suppressIpccHangupUntil) {
        console.debug('[挂机] 桥接保护期内，忽略:', reason);
        return;
      }
      this._completeCallHangup('ipcc', '通话已结束');
    },
    _finishHangupLocally(callUuid) {
      if (this._hangupLockTimer) clearTimeout(this._hangupLockTimer);
      this._clearHangupLock();
      this._resetOutboundLocks();
      this._stopCallUiImmediately();
      if (this.phone) {
        try { this.phone.Terminate(); } catch (e) {}
      }
      if (this.ccPhoneBar) this.ccPhoneBar.clearActiveCallUuid();
      this.showStatus('已挂机', 'info');
      this._onCallEnded(callUuid);
    },
    async _doConnectCCSocket(forceRefreshToken = false) {
      try {
        const extRes = await myCallUser();
        if (extRes.code !== 200 || !extRes.data || !extRes.data.extNum) throw new Error('未查询到分机号信息');
        const { extNum, extPass, gatewayIds: myGateway } = extRes.data;
        this.setupDefaultAccount(extNum, extPass);
        if (myGateway) {
          this.currentConnectedGatewayIds = String(myGateway).split(',').map(id => id.trim()).filter(id => id);
          this._syncSelectedGatewayFromConnected();
        }
        const callConfig = await this._loadIpccCallConfig(extNum, myGateway, forceRefreshToken);
        this._ipccCallConfig = callConfig;
        if (!this.ccPhoneBar) {
          this.ccPhoneBar = new ccPhoneBarSocket();
          this.ccPhoneBar.initConfig(callConfig);
          this._bindCCEvents();
          this._ccEventsBound = true;
        } else if (forceRefreshToken) {
          this.ccPhoneBar.initConfig(callConfig);
          if (this.ccPhoneBar.resetKickedState) this.ccPhoneBar.resetKickedState();
        }
        if (!forceRefreshToken) {
          const cached = this._getIpccTokenCache(extNum, myGateway || 'default');
          if (cached) {
            await new Promise((r) => setTimeout(r, 150));
          }
        }
        this._ccConnectStartedAt = Date.now();
        this.ccPhoneBar.connect();
        this._ccConnectingTimeoutId = setTimeout(() => {
          if (!this.ccSocketConnected) {
            this._finishCcConnecting(false, new Error('连接超时'));
          }
        }, this._getIpccConnectTimeout(false));
      } catch (err) {
        this._finishCcConnecting(false, err);
        throw err;
      }
    },
    _bindCCEvents() {
      this._unbindCCEvents();
      this.ccPhoneBar.on(EventList.WS_CONNECTED, () => {
        this.ccSocketConnected = true;
        this.ccSocketFailed = false;
        this._ipccConnectedAt = Date.now();
        this.initFailed = false;
        if (this.callStatus !== UI_STATE.IDLE || this._ipccRecovering) {
          this._restoreActiveCallSnapshot();
          if (this.ccPhoneBar) this.ccPhoneBar.restoreCallSession();
        }
        this._ipccRecovering = false;
        this._ipccReconnectFailCount = 0;
        if (this.callStatus === UI_STATE.TALKING) {
          this._refreshInCallStatusIndicator();
        } else if (this.callStatus === UI_STATE.RINGING) {
          this._refreshInCallStatusIndicator();
        }
        if (this.ccConnectingResolve) this._finishCcConnecting(true);
        if (this.callStatus === UI_STATE.IDLE && !this.isCallingReady && this.ccPhoneBar) {
          setTimeout(() => {
            if (this.ccSocketConnected && !this.isCallingReady && this.callStatus === UI_STATE.IDLE) {
              try { this.ccPhoneBar.setStatusBusy(); } catch (e) {}
            }
          }, IPCC_CONFIG.STATUS_BUSY_DELAY_MS || 300);
        }
      });
      this.ccPhoneBar.on(EventList.WS_DISCONNECTED, () => {
        const inCall = this.callStatus !== UI_STATE.IDLE;
        if (!inCall) {
          console.warn('[IPCC] WebSocket断开');
        }
        this.ccSocketConnected = false;
        if (inCall) {
          this._persistActiveCallSnapshot();
          this._cancelRemoteHangupCheck();
          this._ipccRecovering = true;
          this._refreshInCallStatusIndicator();
          this._scheduleIpccReconnect(true);
          return;
        }
        this.isCallingReady = false;
        this.showStatus('连接断开，正在快速重连...', 'warn');
        this._scheduleIpccReconnect(false);
      });
      this.ccPhoneBar.on(EventList.USER_LOGIN_ON_OTHER_DEVICE, () => {
        if (this._shouldIgnoreLoginConflict()) {
          console.warn('[IPCC] 忽略误报201，当前 IPCC 连接正常');
          if (this.ccPhoneBar.resetKickedState) this.ccPhoneBar.resetKickedState();
          return;
        }
        console.error('[IPCC] 该坐席已在其他设备登录');
        if (this._ipccReconnectTimer) {
          clearTimeout(this._ipccReconnectTimer);
          this._ipccReconnectTimer = null;
        }
        this._finishCcConnecting(false, new Error('该坐席已在其他设备登录，请关闭其他页面后刷新'));
        this._ipccRecovering = false;
        this.ccSocketConnected = false;
        this.isCallingReady = false;
        this.systemReady = false;
        this.initFailed = true;
        this.initStatusText = '该坐席已在其他设备登录';
        this.showStatus('该坐席已在其他设备登录，请关闭其他页面后点击重试', 'error');
      });
      this.ccPhoneBar.on(EventList.STATUS_CHANGED, (msg) => {
        if (msg?.object) {
          const statusCode = msg.object.status;
          this._handleAgentPostCallStatus(statusCode);
          const readyStatuses = [
            AgentStatusEnum.BUSY, AgentStatusEnum.BUSY_REST, AgentStatusEnum.BUSY_MEETING,
            AgentStatusEnum.BUSY_TRAINING, AgentStatusEnum.FILL_FORM, AgentStatusEnum.FREE
          ];
          const wasReady = this.isCallingReady;
          this.isCallingReady = readyStatuses.includes(statusCode);
          if (wasReady !== this.isCallingReady) {
            if (this.isCallingReady && !this.isRegistered && !this.isInActiveCall) {
              this.showStatus('等待SIP注册...', 'info');
            }
          }
          if (this.isCallingReady && this.isRegistered && this.ccSocketConnected &&
            this.callStatus === UI_STATE.IDLE) {
            this.systemReady = true;
            this.initFailed = false;
          }
        }
      });
      this.ccPhoneBar.on(EventList.REQUEST_ARGS_ERROR, (msg) => {
        const errText = msg?.msg || '';
        if (errText.includes('method not support')) {
          console.debug('[IPCC] 服务端不支持的方法，已忽略:', errText);
          return;
        }
        if (this._isCallNotExistsError(msg)) {
          if (this._isCallAlreadyEnded(this.currentCallUuid)) {
            console.debug('[IPCC] 通话已结束，忽略参数错误:', errText);
            return;
          }
          this._handleCallSessionNotFound(msg);
          return;
        }
        if (this._ipccRecovering && this.callStatus !== UI_STATE.IDLE) {
          console.warn('[IPCC] 通话恢复中收到参数错误，忽略:', errText);
          return;
        }
        console.error('[IPCC] 请求参数错误');
        this.showStatus('请求参数错误', 'error');
        if (!this.ccSocketConnected && this.ccConnectingReject) this.ccConnectingReject(new Error('请求参数错误'));
      });
      this.ccPhoneBar.on('call_session_not_found', (msg) => {
        this._handleCallSessionNotFound(msg);
      });
      this.ccPhoneBar.on(EventList.ASR_RESULT_GENERATE, () => {
        this._lastAsrAt = Date.now();
        this._touchIpccCallSignal();
        this._cancelRemoteHangupCheck();
        if (this.callStatus === UI_STATE.RINGING && this.isInActiveCall) {
          this._promoteToTalkingIfNeeded();
        }
        if (this._ipccRecovering && this.callStatus === UI_STATE.TALKING) {
          this._ipccRecovering = false;
          this._refreshInCallStatusIndicator();
        }
      });
      this.ccPhoneBar.on(EventList.ASR_PROCESS_END_CUSTOMER, () => {
        if (this._isMobileGatewayLine() && this.callStatus === UI_STATE.TALKING) {
          console.debug('[IPCC] 手机线路 ASR 结束，复核是否已挂机');
          this._scheduleMobileRemoteHangupSync('asr_customer_end', 800);
          return;
        }
        console.debug('[IPCC] ASR 客户侧片段结束');
      });
      this.ccPhoneBar.on(EventList.OUTBOUND_FINISHED, () => {
        if (this.callStatus === UI_STATE.IDLE) {
          console.debug('[IPCC] 611(外呼任务结束)，空闲态忽略');
          return;
        }
        if (this._isMobileGatewayLine() && this.callStatus === UI_STATE.TALKING) {
          console.debug('[IPCC] 手机线路 611，复核是否已挂机');
          this._scheduleMobileRemoteHangupSync('611_talking', 500);
          return;
        }
        console.debug('[IPCC] 611(外呼任务结束)，不参与挂机，当前态=', this.callStatus);
      });
      this.ccPhoneBar.on(EventList.SERVER_ERROR, (msg) => {
        console.error('[IPCC] 服务器错误');
        this.showStatus('服务器错误', 'error');
        if (!this.ccSocketConnected && this.ccConnectingReject) this.ccConnectingReject(new Error('服务器错误'));
      });
      this.ccPhoneBar.on(EventList.OUTBOUND_START, (msg) => {
        if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
        this.callDirection = 'outbound';
        this.callStatus = UI_STATE.RINGING;
        this._startOutboundBridgeProtect('OUTBOUND_START');
        this.showLeftButton = false;
        this.showRightButton = false;
        this.showStatus('拨号中...', 'info');
        if (this._isCalling) this._releaseCallLock();
        if (msg?.object?.uuid) {
          const outboundUuid = msg.object.uuid;
          this.currentCallUuid = outboundUuid;
          this._syncCallUuidToSdk(outboundUuid);
          this.callUuidMap[outboundUuid] = { startTime: Date.now(), phoneNumber: this.dialNumber, status: 'outbound_start', synced: false };
          this._persistActiveCallSnapshot();
        } else console.warn('[UUID] 外呼开始消息中未获取到UUID');
      });
      this.ccPhoneBar.on(EventList.CALLEE_RINGING, () => {
        this.callStatus = UI_STATE.RINGING;
        if (this.callDirection === 'outbound') {
          this._startOutboundBridgeProtect('CALLEE_RINGING');
        }
        this.showLeftButton = false;
        this.showRightButton = false;
        this.rightButtonHangup = false;
        if (this.callDirection === 'inbound') {
          this.showLeftButton = true;
        }
        this.showStatus('振铃中...', 'info');
        if (this.currentCallUuid && this.callUuidMap[this.currentCallUuid]) this.callUuidMap[this.currentCallUuid].status = 'ringing';
        this._releaseCallLock();
        this.startAudioHealthCheck();
      });
      this.ccPhoneBar.on(EventList.CALLEE_BUSY, () => {
        console.warn('[IPCC] 被叫忙或拒接');
        this._abortOutboundAttempt('对方忙或拒接', 'warn');
      });
      this.ccPhoneBar.on(EventList.CALLER_BUSY, () => {
        console.warn('[IPCC] 分机忙');
        this._abortOutboundAttempt('分机忙，请稍后再试', 'warn');
      });
      this.ccPhoneBar.on(EventList.CALLER_RESPOND_TIMEOUT, () => {
        if (this._isRingingBridgeProtected()) return;
        this._abortOutboundAttempt('分机未应答', 'warn');
      });
      this.ccPhoneBar.on(EventList.EXTENSION_OFF_LINE, () => {
        if (this._isPostCallSettling() && this._isHangingUp) return;
        if (this.callStatus === UI_STATE.IDLE && !this.isInActiveCall) return;
        if (this._isMobileGatewayLine() &&
          (this.callStatus === UI_STATE.TALKING || this.callStatus === UI_STATE.RINGING)) {
          console.warn('[IPCC] 手机线路分机离线，同步挂机');
          this._forceReleaseIpccCallState();
          this._completeCallHangup('ipcc', '对方已挂机');
          return;
        }
        if (this._ipcCallSessionActive()) {
          this._completeCallHangup('ipcc', '分机已离线');
        }
      });
      const applyTalkingState = (msg) => {
        if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
        if (msg?.object?.uuid) {
          const realUuid = msg.object.uuid;
          if (!this.currentCallUuid || this.currentCallUuid !== realUuid) {
            if (this.currentCallUuid && this.currentCallUuid !== realUuid) {
              if (this.callUuidMap[this.currentCallUuid]) {
                this.callUuidMap[realUuid] = this.callUuidMap[this.currentCallUuid];
                delete this.callUuidMap[this.currentCallUuid];
              }
            } else if (!this.currentCallUuid) {
              this.callUuidMap[realUuid] = { startTime: Date.now(), phoneNumber: this.dialNumber, status: 'answered', synced: false };
            }
            this.currentCallUuid = realUuid;
          }
        } else console.warn('[UUID] 接听消息中未获取到UUID');
        this._syncCallUuidToSdk(this.currentCallUuid);
        this.callStatus = UI_STATE.TALKING;
        this._callEstablishedAt = Date.now();
        this._lastCallTimerTickAt = Date.now();
        this._touchIpccCallSignal();
        this._suppressIpccHangupUntil = Date.now() + 3000;
        if (this.phone?.startCallTimer) {
          this.phone.stopCallTimer();
          this.phone.startCallTimer();
        }
        this.showLeftButton = true;
        this.showRightButton = true;
        this.leftButtonNormal = true;
        this.rightButtonNormal = true;
        this.showStatus('通话中', 'success');
        this._refreshInCallStatusIndicator();
        if (this.phone) {
          this.phone.ensureLocalStream()
            .then(() => {
              this.phone.SetSpeaker(this.isSpeakerMuted, this.speakerVolume);
              this.phone.SetMicPhone(this.isMicMuted, this.micVolume);
              this.phone.resumeRemoteAudio();
            })
            .catch(() => console.warn('[麦克风] 通话中不可用'));
        }
        this._acquireWakeLock();
        this.startAudioHealthCheck();
        if (this.currentCallUuid && this.callUuidMap[this.currentCallUuid]) {
          this.callUuidMap[this.currentCallUuid].status = 'answered';
          this.callUuidMap[this.currentCallUuid].answerTime = Date.now();
        }
        this._persistActiveCallSnapshot();
      };
      this.ccPhoneBar.on(EventList.CALLER_ANSWERED, (msg) => {
        if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
        if (msg?.object?.uuid) {
          this.currentCallUuid = msg.object.uuid;
          if (!this.callUuidMap[msg.object.uuid]) {
            this.callUuidMap[msg.object.uuid] = { startTime: Date.now(), phoneNumber: this.dialNumber, status: 'caller_answered', synced: false };
          }
        }
        if (this.callDirection === 'outbound') {
          this.callStatus = UI_STATE.RINGING;
          this._startOutboundBridgeProtect('CALLER_ANSWERED');
          this.showLeftButton = false;
          this.showRightButton = false;
          this.showStatus('客户振铃中...', 'info');
          this.startAudioHealthCheck();
          return;
        }
        applyTalkingState(msg);
      });
      this.ccPhoneBar.on(EventList.CALLEE_ANSWERED, (msg) => {
        applyTalkingState(msg);
      });
      const handleCallHangup = (msg) => {
        if (this._completingHangup) return;
        if (this.currentCallUuid && this._endedCallUuids?.[this.currentCallUuid]) return;
        this._postCallSettlingUntil = 0;
        this._suppressIpccHangupUntil = 0;
        this._bridgeStartedAt = 0;
        this._completeCallHangup('ipcc', msg?.msg?.includes('分机') ? '对方已挂机' : '已挂机');
      };
      this.ccPhoneBar.on(EventList.CALLER_HANGUP, handleCallHangup);
      this.ccPhoneBar.on(EventList.CALLEE_HANGUP, handleCallHangup);
      this.ccPhoneBar.on(EventList.CUSTOMER_CHANNEL_HOLD, () => {
        this.isOnHold = true;
        this._refreshInCallStatusIndicator();
      });
      this.ccPhoneBar.on(EventList.CUSTOMER_CHANNEL_UNHOLD, () => {
        this.isOnHold = false;
        this._refreshInCallStatusIndicator();
      });
    },
    _unbindCCEvents() {
      if (!this.ccPhoneBar) return;
      this.ccPhoneBar.off(EventList.WS_CONNECTED);
      this.ccPhoneBar.off(EventList.WS_DISCONNECTED);
      this.ccPhoneBar.off(EventList.USER_LOGIN_ON_OTHER_DEVICE);
      this.ccPhoneBar.off(EventList.STATUS_CHANGED);
      this.ccPhoneBar.off(EventList.REQUEST_ARGS_ERROR);
      this.ccPhoneBar.off('call_session_not_found');
      this.ccPhoneBar.off(EventList.ASR_RESULT_GENERATE);
      this.ccPhoneBar.off(EventList.ASR_PROCESS_END_CUSTOMER);
      this.ccPhoneBar.off(EventList.OUTBOUND_FINISHED);
      this.ccPhoneBar.off(EventList.SERVER_ERROR);
      this.ccPhoneBar.off(EventList.OUTBOUND_START);
      this.ccPhoneBar.off(EventList.CALLEE_RINGING);
      this.ccPhoneBar.off(EventList.CALLEE_BUSY);
      this.ccPhoneBar.off(EventList.CALLER_BUSY);
      this.ccPhoneBar.off(EventList.CALLER_RESPOND_TIMEOUT);
      this.ccPhoneBar.off(EventList.EXTENSION_OFF_LINE);
      this.ccPhoneBar.off(EventList.CALLER_ANSWERED);
      this.ccPhoneBar.off(EventList.CALLEE_ANSWERED);
      this.ccPhoneBar.off(EventList.CALLER_HANGUP);
      this.ccPhoneBar.off(EventList.CALLEE_HANGUP);
      this.ccPhoneBar.off(EventList.CUSTOMER_CHANNEL_HOLD);
      this.ccPhoneBar.off(EventList.CUSTOMER_CHANNEL_UNHOLD);
    },
    setupDefaultAccount(extNum, extPass) {
      if (!extNum || !extPass) return;
      extNum = String(extNum).trim();
      extPass = String(extPass).trim();
      let existingUserId = null;
      for (const [id, user] of Object.entries(this.userList)) {
        if (user.user === extNum) { existingUserId = id; break; }
      }
      if (existingUserId) {
        const updatedProfile = {
          ...this.userList[existingUserId],
          note: extNum, display_name: extNum, password: extPass, user: extNum,
          domain: JS_SIP_CONFIG.DOMAIN, server: JS_SIP_CONFIG.SERVER, transport: JS_SIP_CONFIG.TRANSPORT
        };
        this.profileManager.updateUser(existingUserId, updatedProfile);
        if (this.currentUserId !== existingUserId) { this.profileManager.switchUser(existingUserId); this.currentUserId = existingUserId; }
      } else {
        const newProfile = { note: extNum, user: extNum, domain: JS_SIP_CONFIG.DOMAIN, password: extPass, display_name: extNum, server: JS_SIP_CONFIG.SERVER, transport: JS_SIP_CONFIG.TRANSPORT };
        this.profileManager.addUser(newProfile);
        const updatedProfile = this.profileManager.getProfile();
        this.userList = updatedProfile.users;
        for (const [id, user] of Object.entries(this.userList)) {
          if (user.user === extNum) { this.currentUserId = id; this.profileManager.switchUser(id); break; }
        }
      }
      if (this.currentUserId && this.userList[this.currentUserId]) this.currentUserDisplay = this.userList[this.currentUserId].note || this.userList[this.currentUserId].user;
      const profile = this.profileManager.getProfile();
      this.speakerVolume = profile.speaker_volume ?? VOLUME_CONFIG.DEFAULT;
      this.micVolume = profile.mic_volume ?? VOLUME_CONFIG.DEFAULT;
      this.isSpeakerMuted = profile.speaker_paused || false;
      this.isMicMuted = profile.mic_paused || false;
    },
    async startPhone() {
      if (!this.ccSocketConnected) { this.showStatus('jsSip未连接', 'error'); return; }
      const userProfile = this.profileManager.getCurrentUserProfile();
      if (!userProfile) { this.showStatus('jsSip无可用账号', 'warn'); return; }
      if (!userProfile.user || !userProfile.domain || !userProfile.password) { this.showStatus('jsSip账号配置错误', 'error'); return; }
      if (this.phone) { this.phone.destroy(); this.phone = null; }
      const settings = this.profileManager.getSettings();
      this.phone = new WebPhone(userProfile, settings);
      this.phone.On('OnRegister', this.onRegisterEvent);
      this.phone.On('OnRing', this.onRing);
      this.phone.On('OnAnswered', this.onAnswered);
      this.phone.On('OnSessionClosed', this.onSessionClosed);
      this.phone.On('OnIceMediaIssue', this.onIceMediaIssue);
      this.phone.On('OnCallTimer', this.onCallTimer);
      this.phone.On('OnStatusMessage', this.onStatusMessage);
      this.phone.On('OnReconnectStatus', this.onReconnectStatus);
      this.phone.Start(settings.reconnect);
      this.phone.SetSpeaker(this.isSpeakerMuted, this.speakerVolume);
      this.phone.SetMicPhone(this.isMicMuted, this.micVolume);
    },
    waitForRegistration(timeout = IPCC_CONFIG.SIP_REGISTER_TIMEOUT_MS) {
      return new Promise((resolve, reject) => {
        if (this.isRegistered) { resolve(); return; }
        const startTime = Date.now();
        const checkInterval = setInterval(() => {
          if (this.isRegistered) { clearInterval(checkInterval); clearTimeout(timeoutId); resolve(); }
          else if (Date.now() - startTime > timeout) { clearInterval(checkInterval); clearTimeout(timeoutId); reject(new Error('SIP注册超时')); }
        }, 100);
        const timeoutId = setTimeout(() => { clearInterval(checkInterval); if (!this.isRegistered) reject(new Error('SIP注册超时')); }, timeout);
      });
    },
    onStatusMessage({ type, text }) {
      if (this.isInActiveCall) {
        this._refreshInCallStatusIndicator();
        return;
      }
      this.showStatus(text, type);
    },
    onReconnectStatus({ isReconnecting, failed }) {
      this.isReconnecting = isReconnecting;
      this.reconnectFailed = failed;
      if (this.isInActiveCall) {
        this._refreshInCallStatusIndicator();
        return;
      }
      if (failed) this.showStatus('jsSip连接超时', 'error');
      else if (isReconnecting) this.showStatus('jsSip重连中...', 'info');
    },
    onRegisterEvent(event) {
      this.isRegistered = event.registered;
      this.isConnected = event.registered;
      if (event.registered) {
        this.currentUserDisplay = this.userList[this.currentUserId]?.note || this.userList[this.currentUserId]?.user || '';
        if (this.ccSocketConnected && this.isCallingReady) {
          this.systemReady = true;
          this.initFailed = false;
          this.initStatusText = '';
        }
        if (this.isInActiveCall) {
          this._refreshInCallStatusIndicator();
        } else {
          this.showStatus('就绪', 'success');
        }
        if (this.ccPhoneBar && this.ccSocketConnected) {
          setTimeout(() => { this._safeSetStatus(AgentStatusEnum.BUSY, 'SIP注册完成'); }, IPCC_CONFIG.STATUS_BUSY_DELAY_MS || 300);
        }
      } else if (!this.isReconnecting && !this.isInActiveCall) {
        this.showStatus('jsSip未注册', 'warn');
      }
    },
    onRing(event) {
      if (event.outgoing || this.callDirection === 'outbound' || this._isCalling) {
        this.showLeftButton = false;
        this.showRightButton = false;
        return;
      }
      this.callDirection = 'inbound';
      this.callStatus = UI_STATE.RINGING;
      this.province = `${event.province || ''}${event.city ? '-' + event.city : ''}`;
      this.showLeftButton = true;
      this.showRightButton = false;
      this.showStatus('来电振铃中...', 'info');
    },
    onAnswered(outgoing) {
      if (this.phone) {
        this.phone.SetSpeaker(this.isSpeakerMuted, this.speakerVolume);
        this.phone.SetMicPhone(this.isMicMuted, this.micVolume);
        this.phone.resumeRemoteAudio();
      }
      if (this.callDirection === 'outbound') {
        if (this.callStatus !== UI_STATE.TALKING) {
          this.callStatus = UI_STATE.RINGING;
          this.showStatus('客户振铃中...', 'info');
        }
        return;
      }
      this.callStatus = UI_STATE.TALKING;
      if (this.phone?.startCallTimer) {
        this.phone.stopCallTimer();
        this.phone.startCallTimer();
      }
      this.showLeftButton = true;
      this.showRightButton = true;
      this.leftButtonNormal = true;
      this.rightButtonNormal = true;
      this.rightButtonHangup = false;
      this.showStatus('通话中', 'success');
      this._acquireWakeLock();
      this.startAudioHealthCheck();
    },
    _onCallEnded(callUuid) {
      this._cancelRemoteHangupCheck();
      this._ensureCallTracking();
      if (callUuid && this._endedCallUuids[callUuid]) {
        this._restoreReadyAfterCall();
        return;
      }
      if (callUuid) this._endedCallUuids[callUuid] = Date.now();
      if (callUuid && this.callUuidMap[callUuid]) {
        this.callUuidMap[callUuid].status = 'ended';
        this.callUuidMap[callUuid].endTime = Date.now();
      }
      this._resetCallState();
      this._resetOutboundLocks();
      if (this.phone) {
        try { this.phone.releaseLocalStream(); } catch (e) {}
      }
      if (callUuid) this._scheduleCallEndSync(callUuid);
      this._clearActiveCallSnapshot();
      this._restoreReadyAfterCall();
    },
    _resetOutboundLocks() {
      this._releaseCallLock();
      if (this.ccPhoneBar && typeof this.ccPhoneBar.resetOutboundLock === 'function') {
        this.ccPhoneBar.resetOutboundLock();
      }
    },
    _resetCallState() {
      if (this.phone?.stopCallTimer) {
        try { this.phone.stopCallTimer(); } catch (e) {}
      }
      if (this.callStatus !== UI_STATE.IDLE) {
        this.callStatus = UI_STATE.IDLE;
        this.isOnHold = false;
        this.callDirection = '';
        this.province = '';
        this.callDuration = '00:00';
        this.showLeftButton = false;
        this.showRightButton = false;
        this.stopAudioHealthCheck();
        this._releaseWakeLock();
      }
      this.currentCallUuid = '';
      this._callEstablishedAt = 0;
      this._bridgeStartedAt = 0;
      this._suppressIpccHangupUntil = 0;
      this._ipccReconnectFailCount = 0;
    },
    onSessionClosed(payload) {
      this._ensureCallTracking();
      if (this._isHangingUp || this._completingHangup || payload?.intentional || this._isPostCallSettling()) return;
      const callUuid = this.currentCallUuid;
      if (callUuid && this._endedCallUuids[callUuid]) return;
      if (this.callStatus === UI_STATE.TALKING || this.callDuration !== '00:00') {
        if (this._isMobileGatewayLine()) {
          if (this._syncMobileRemoteHangupIfNeeded('sip_session_closed')) return;
          this._scheduleMobileRemoteHangupSync('mobile_sip_end', 400);
          return;
        }
        if (this._isRemoteCallAlive()) {
          console.debug('[SIP] SIP结束但IPCC仍活跃，继续通话');
          return;
        }
        console.warn('[SIP] SIP会话结束，同步双挂');
        this._completeCallHangup('sip', '对方已挂机');
        return;
      }
      if (this.callStatus === UI_STATE.RINGING) {
        if (this._isRingingBridgeProtected()) {
          this._scheduleRemoteHangupCheck('sip_ringing_bridge', 2500);
          return;
        }
        if (this._isRemoteCallAlive()) return;
        this._completeCallHangup('sip', '对方已挂机');
        return;
      }
      if (this.phone?.hasActiveCall?.()) return;
      this._stopCallUiImmediately();
      this.showStatus('已挂机', 'info');
      this._onCallEnded(callUuid);
    },
    onIceMediaIssue({ state, timedOut } = {}) {
      if (this.callStatus !== UI_STATE.TALKING) return;
      const sipAlive = this.phone?.hasActiveCall?.() || this.phone?.hasSipSession?.();
      if (sipAlive) {
        if (this.phone) this.phone.resumeRemoteAudio();
        return;
      }
      if (this._isRemoteCallAlive()) return;
      if (this._isMobileGatewayLine() && this._syncMobileRemoteHangupIfNeeded('ice_media')) return;
      console.warn('[SIP] 通话中ICE异常且无SIP会话，同步双挂:', state);
      if (timedOut || state === 'failed' || state === 'disconnected') {
        this._completeCallHangup('sip', '对方已挂机');
      }
    },
    onCallTimer(time) {
      this.callDuration = time;
      this._lastCallTimerTickAt = Date.now();
    },
    // 手动解密并拨号
    async makeCall() {
      if (!this.canUseDialer) { this.showStatus('系统未就绪，请等待连接完成', 'warn'); return; }
      if (this._isCalling) { this.showStatus('正在呼叫中...', 'warn'); return; }
      if (!this.ccPhoneBar || !this.ccSocketConnected) { this.showStatus('IPCC未连接', 'error'); return; }
      if (!this.isRegistered) { this.showStatus('SIP未注册，请稍后重试', 'warn'); return; }
      if (!this.isCallingReady) { this.showStatus('坐席未就绪', 'warn'); return; }
      if (!this.dialNumber || this.dialNumber.trim().length < 3) { this.showStatus('请输入正确的号码', 'warn'); return; }
      if (this.callStatus !== UI_STATE.IDLE) { this.showStatus('请先结束当前通话', 'warn'); return; }

      let phoneNumber;
      if (this.dialMode === 'encrypted') {
        if (this.isDecryptingForCall) { this.showStatus('正在解密号码，请稍候...', 'warn'); return; }
        // 开始解密
        this.isDecryptingForCall = true;
        try {
          phoneNumber = await this.decryptPhoneNumberForCall(this.dialNumber.trim());
          if (!phoneNumber) throw new Error('解密结果为空');
        } catch (err) {
          console.error('[解密] 失败:', err);
          this.showStatus('解密失败', 'error');
          this.isDecryptingForCall = false;
          // 确保释放呼叫锁
          this._releaseCallLock();
          return;
        } finally {
          this.isDecryptingForCall = false;
        }
      } else {
        phoneNumber = this.dialNumber.trim();
      }

      // 继续拨号
      this._resetOutboundLocks();
      if (this.ccPhoneBar) {
        this.ccPhoneBar.clearActiveCallUuid();
        if (typeof this.ccPhoneBar.setCallConnected === 'function') {
          this.ccPhoneBar.setCallConnected(false);
        }
      }
      this._isCalling = true;
      this.callDirection = 'outbound';
      this.callStatus = UI_STATE.RINGING;
      this.showStatus('拨号中...', 'info');
      if (!this.profileManager.getSettings().auto_answer) {
        this.profileManager.updateSettings({ auto_answer: true });
      }
      if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
      this._callTimeoutTimer = setTimeout(() => {
        if (this._isCalling) {
          console.warn('[外呼] 组件级超时：15秒未收到响应');
          this._abortOutboundAttempt('外呼超时，请重试', 'warn');
        }
      }, 15000);
      if (this.phone) {
        try {
          await this.phone.ensureLocalStream();
          this.phone.SetMicPhone(this.isMicMuted, this.micVolume);
          this.micPermissionGranted = true;
        } catch (e) {
          this._abortOutboundAttempt('请授权麦克风', 'error');
          return;
        }
      }
      try {
        const started = this.ccPhoneBar.call(phoneNumber, 'audio', VideoLevels.HD.levelId);
        if (started === false) {
          this._abortOutboundAttempt('外呼被拒绝，请稍后重试', 'warn');
        }
      } catch (err) {
        console.error('[外呼] 调用失败:', err);
        this._abortOutboundAttempt('外呼失败', 'error');
      }
    },
    // 解密方法，带超时控制（5秒）
    decryptPhoneNumberForCall(phoneNumber) {
      const timeoutMs = 5000;
      return Promise.race([
        (async () => {
          const combined = phoneNumber + this.generateRandom();
          const response = await encryptMobile({ data: combined });
          if (response.code !== 200) throw new Error(response.message || '解密接口返回错误');
          const privateKey = process.env.VUE_APP_SIP_PHONE_ENCRYPT_PRIVATE_KEY;
          if (!privateKey) throw new Error('解密配置缺失');
          const resultData = response.data.slice(0, -6);
          const decrypted = this.xorDecrypt(resultData, privateKey);
          if (!decrypted) throw new Error('解密结果无效');
          return decrypted;
        })(),
        new Promise((_, reject) => setTimeout(() => reject(new Error('解密超时（5秒）')), timeoutMs))
      ]);
    },
    generateRandom() { return Math.floor(100000 + Math.random() * 900000).toString(); },
    xorDecrypt(base64Str, privateKey) {
      try {
        const cleaned = base64Str.replace(/\s/g, '');
        const binary = atob(cleaned);
        const keyBytes = privateKey.split('').map(ch => ch.charCodeAt(0));
        let result = '';
        for (let i = 0; i < binary.length; i++) result += String.fromCharCode(binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length]);
        return result;
      } catch(e) { return ''; }
    },
    handleDialModeChange() {
      // 切换模式时清除解密状态
      this.isDecryptingForCall = false;
    },
    _releaseCallLock() {
      if (this._callLockTimer) clearTimeout(this._callLockTimer);
      if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
      this._isCalling = false;
    },
    _releaseCallLockAndAllowNext() {
      if (this._callLockTimer) clearTimeout(this._callLockTimer);
      if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
      // 延迟重置，避免连续快速点击
      this._callLockTimer = setTimeout(() => {
        this._isCalling = false;
        this._callLockTimer = null;
      }, this._canMakeCallDelay);
    },
    _forceResetCallState() {
      if (this.callStatus !== UI_STATE.IDLE) {
        console.warn('[强制重置] 清空调通状态');
        this.callStatus = UI_STATE.IDLE;
        this.isOnHold = false;
        this.callDirection = '';
        this.province = '';
        this.callDuration = '00:00';
        this.showLeftButton = false;
        this.showRightButton = false;
        this.stopAudioHealthCheck();
        this._releaseWakeLock();
        if (this.phone) {
          try { this.phone.Terminate(); } catch (e) {}
        }
      }
      this._isCalling = false;
      this._isHangingUp = false;
      if (this._hangupLockTimer) clearTimeout(this._hangupLockTimer);
    },
    endCall() {
      if (this._isHangingUp) return;
      if (this.callStatus === UI_STATE.IDLE && !this.isInActiveCall) {
        this.showStatus('当前无通话', 'warn');
        return;
      }
      this._isHangingUp = true;
      this._beginPostCallSettling(8000);
      this._completeCallHangup('user', '已挂机');
    },
    _clearHangupLock() { this._isHangingUp = false; this._clearHangupLockTimer(); },
    _clearHangupLockTimer() { if (this._hangupLockTimer) { clearTimeout(this._hangupLockTimer); this._hangupLockTimer = null; } },
    onDigitClick(digit) {
      if (this.callStatus === 'talking' && this.phone) { this.phone.SendDTMF(digit); this.phone.PlayDtmfTone(digit); }
      this.dialNumber += digit;
      this.scrollInputToEnd();
    },
    handleDisplayKeypress(event) {
      const key = event.key;
      if ((key >= '0' && key <= '9') || key === '*' || key === '#') {
        if (this.callStatus === 'talking' && this.phone) { this.phone.SendDTMF(key); this.phone.PlayDtmfTone(key); }
        this.dialNumber += key;
        this.scrollInputToEnd();
        event.preventDefault();
      } else if (key === 'Enter') { this.onHangupClick(); event.preventDefault(); }
      else if (key === 'Backspace') {
        if (!this.canEditDialNumber) {
          event.preventDefault();
          return;
        }
        event.preventDefault();
        this.clearDialNumber();
      }
      else event.preventDefault();
    },
    onHangupClick() {
      if (this.callStatus !== UI_STATE.IDLE) this.endCall();
      else this.makeCall();
    },
    onLeftButtonClick() {
      if (this.callStatus === UI_STATE.RINGING) { if (this.phone) this.phone.Answer(); }
      else if (this.callStatus === UI_STATE.TALKING) {
        if (this.ccPhoneBar && this.ccSocketConnected) {
          try { if (this.isOnHold) this.ccPhoneBar.unHoldCall(); else this.ccPhoneBar.holdCall(); }
          catch (err) { this.showStatus('操作失败', 'error'); }
        } else if (this.phone) { try { this.phone.ToggleHold(); } catch (err) { this.showStatus('操作失败', 'error'); } }
      }
    },
    onRightButtonClick() {
      if (this.callStatus === UI_STATE.TALKING) this.showStatus('呼叫转移功能暂未实现', 'info');
    },
    toggleMuteMic() {
      if (!this.phone) return;
      this.isMicMuted = !this.isMicMuted;
      this.phone.SetMicPhone(this.isMicMuted, this.micVolume);
      if (this.phone.session) {
        const sessionMuted = this.phone.isMicMuted();
        if (this.isMicMuted !== sessionMuted) this.phone.ToggleMicPhone();
      }
      this.profileManager.updateSettings({ mic_paused: this.isMicMuted, mic_volume: this.micVolume });
    },
    toggleMuteSpeaker() {
      if (!this.phone) return;
      this.isSpeakerMuted = !this.isSpeakerMuted;
      this.phone.SetSpeaker(this.isSpeakerMuted, this.speakerVolume);
      this.profileManager.updateSettings({ speaker_paused: this.isSpeakerMuted, speaker_volume: this.speakerVolume });
    },
    onSpeakerVolumeInput(event) {
      const pct = parseInt(event.target.value, 10);
      this.speakerVolume = pct / 100;
      this.isSpeakerMuted = false;
      if (this.phone) this.phone.SetSpeaker(false, this.speakerVolume);
      this.profileManager.updateSettings({ speaker_volume: this.speakerVolume, speaker_paused: false });
    },
    onMicVolumeInput(event) {
      const pct = parseInt(event.target.value, 10);
      this.micVolume = pct / 100;
      this.isMicMuted = false;
      if (this.phone) {
        this.phone.SetMicPhone(false, this.micVolume);
        if (this.phone.session && this.phone.isMicMuted()) this.phone.ToggleMicPhone();
      }
      this.profileManager.updateSettings({ mic_volume: this.micVolume, mic_paused: false });
    },
    toggleDropdown() { this.dropdownVisible = !this.dropdownVisible; },
    closeDropdown() { this.dropdownVisible = false; },
    async switchAccount(userId) {
      if (userId === this.currentUserId) { this.closeDropdown(); return; }
      this.profileManager.switchUser(userId);
      this.currentUserId = userId;
      const user = this.userList[userId];
      this.currentUserDisplay = user.note || user.user;
      if (this.phone) { this.phone.destroy(); this.phone = null; }
      if (!this.ccSocketConnected) { this.showStatus('呼叫中心连接已断开，请重试', 'error'); return; }
      await this.startPhone();
      this.closeDropdown();
    },
    openAddAccountDialog() {
      this.isEditMode = false;
      this.editingUserId = null;
      this.accountDialogTitle = '添加账号';
      this.accountForm = { note: '', server: JS_SIP_CONFIG.SERVER, username: '', domain: JS_SIP_CONFIG.DOMAIN, loginName: '', password: '', transport: JS_SIP_CONFIG.TRANSPORT };
      this.showPassword = false;
      this.accountDialogVisible = true;
    },
    openEditAccountDialog() {
      const profile = this.profileManager.getCurrentUserProfile();
      if (!profile) return;
      this.isEditMode = true;
      this.editingUserId = this.currentUserId;
      this.accountDialogTitle = '编辑账号';
      this.accountForm = {
        note: profile.note || profile.display_name,
        server: profile.server,
        username: profile.display_name,
        domain: profile.domain,
        loginName: profile.user,
        password: profile.password,
        transport: profile.transport || 'wss'
      };
      this.showPassword = false;
      this.accountDialogVisible = true;
    },
    async saveAccount() {
      try {
        if (!this.accountForm.loginName || !this.accountForm.domain || !this.accountForm.password) { this.$message.warning('登录名、域名和密码为必填项'); return; }
        if (this.isEditMode && this.editingUserId) {
          const updatedProfile = {
            note: this.accountForm.note, server: this.accountForm.server, display_name: this.accountForm.username,
            password: this.accountForm.password, transport: this.accountForm.transport,
            user: this.accountForm.loginName, domain: this.accountForm.domain
          };
          this.profileManager.updateUser(this.editingUserId, updatedProfile);
          this.showStatus('账号已更新', 'success');
        } else {
          const profile = {
            note: this.accountForm.note, user: this.accountForm.loginName, domain: this.accountForm.domain,
            password: this.accountForm.password, display_name: this.accountForm.username,
            server: this.accountForm.server, transport: this.accountForm.transport
          };
          this.profileManager.addUser(profile);
          this.showStatus('账号已添加', 'success');
        }
        this.userList = this.profileManager.getProfile().users;
        this.currentUserId = this.profileManager.getProfile().user;
        if (this.currentUserId && this.userList[this.currentUserId]) this.currentUserDisplay = this.userList[this.currentUserId].note || this.userList[this.currentUserId].user;
        this.accountDialogVisible = false;
        if (this.phone) { this.phone.destroy(); this.phone = null; }
        if (this.ccSocketConnected) await this.startPhone();
        else await this.initCCAndStart();
      } catch (err) { this.$message.error(err.message); this.showStatus(err.message, 'error'); }
    },
    async confirmDeleteAccount() {
      this.$confirm('删除账号将清除本地配置，确认删除？', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
        .then(async () => {
          this.profileManager.deleteCurrentUser();
          const newProfile = this.profileManager.getProfile();
          this.userList = newProfile.users || {};
          this.currentUserId = newProfile.user || '';
          this.currentUserDisplay = (this.currentUserId && this.userList[this.currentUserId]) ? (this.userList[this.currentUserId].note || this.userList[this.currentUserId].user) : '';
          if (this.phone) { this.phone.destroy(); this.phone = null; }
          if (this.ccSocketConnected && this.currentUserId) await this.startPhone();
          this.showStatus('账号已删除', 'info');
        }).catch(() => {});
    },
    openSettingsDialog() {
      this.settingsForm = this._mapSettingsToForm(this.profileManager.getSettings());
      this.settingsDialogVisible = true;
      this.closeDropdown();
    },
    async saveSettings() {
      this.profileManager.updateSettings({
        user_agent: this.settingsForm.userAgent, session_expires: this.settingsForm.sessionExpires,
        min_session_expires: this.settingsForm.minSessionExpires, stun: this.settingsForm.stun,
        ice_server: this.settingsForm.iceServer, auto_answer: this.settingsForm.autoAnswer,
        reconnect: this.settingsForm.reconnect, reconnect_interval: this.settingsForm.reconnectInterval
      });
      this.settingsDialogVisible = false;
      if (this.phone) { this.phone.destroy(); this.phone = null; }
      if (this.ccSocketConnected) await this.startPhone();
      this.showStatus('设置已保存', 'success');
    },
    destroyAllConnections() {
      if (this.isInActiveCall) {
        if (this.phone) {
          try { this.phone.Terminate(); } catch (e) {}
        }
        if (this.ccPhoneBar && this.ccSocketConnected) {
          try { this.ccPhoneBar.hangup(); } catch (e) {}
        }
      }
      this.clearAllTimers();
      this.stopAudioHealthCheck();
      this._clearAllSyncTimers();
      this._releaseMicrophoneResources();
      if (this.phone) { try { this.phone.destroy(); } catch(e) {} this.phone = null; }
      if (this.ccPhoneBar) {
        try {
          this._forceReleaseIpccCallState();
          if (typeof this.ccPhoneBar.forceDisconnect === 'function') {
            this.ccPhoneBar.forceDisconnect();
          } else {
            this.ccPhoneBar.disconnect();
          }
        } catch (e) {}
        this.ccPhoneBar = null;
      }
      this._ccEventsBound = false;
      this.callUuidMap = {};
      this._syncingUuids = {};
      this._syncTimers = {};
      if (!this._endedCallUuids) this._endedCallUuids = {};
      this.currentCallUuid = '';
      this.resetAllStates();
    },
    _safeSetStatus(status, reason = '未知') {
      const now = Date.now();
      if (now - this._lastSetStatusTime < this._setStatusDebounceMs) { console.warn(`[防抖] ${reason} - 跳过`); return; }
      if (this._isSettingStatus) { console.warn(`[防抖] ${reason} - 执行中`); return; }
      this._isSettingStatus = true;
      this._lastSetStatusTime = now;
      try { this.ccPhoneBar.setStatus(status); } catch (err) { this.showStatus('设置状态失败', 'error'); }
      finally { setTimeout(() => { this._isSettingStatus = false; }, 100); }
    },
    clearAllTimers() {
      if (this._readyRestoreTimer) {
        clearTimeout(this._readyRestoreTimer);
        this._readyRestoreTimer = null;
      }
      if (this._ipccReconnectTimer) clearTimeout(this._ipccReconnectTimer);
      if (this.statusTimerId) clearTimeout(this.statusTimerId);
      if (this._pendingSyncTimer) clearTimeout(this._pendingSyncTimer);
      if (this._hangupLockTimer) clearTimeout(this._hangupLockTimer);
      if (this._callLockTimer) clearTimeout(this._callLockTimer);
      if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
      this._cancelRemoteHangupCheck();
    },
    resetAllStates() {
      this.ccSocketConnected = false; this.ccSocketFailed = false; this.isCallingReady = false;
      this.isReconnecting = false; this.reconnectFailed = false; this.isConnected = false; this.isRegistered = false;
      this.callStatus = UI_STATE.IDLE; this.isOnHold = false; this.dropdownVisible = false;
      this.accountDialogVisible = false; this.settingsDialogVisible = false;
      this.isDecryptingForCall = false;
      this._isHangingUp = false; this._isCalling = false;
      this._clearHangupLockTimer();
      this.dialNumber = '';
    },
    removeEventListeners() {
      if (this.phone) {
        try {
          this.phone.Off('OnRegister', this.onRegisterEvent);
          this.phone.Off('OnRing', this.onRing); this.phone.Off('OnAnswered', this.onAnswered);
          this.phone.Off('OnSessionClosed', this.onSessionClosed);
          this.phone.Off('OnIceMediaIssue', this.onIceMediaIssue);
          this.phone.Off('OnCallTimer', this.onCallTimer);
          this.phone.Off('OnStatusMessage', this.onStatusMessage); this.phone.Off('OnReconnectStatus', this.onReconnectStatus);
        } catch(e) {}
      }
    },
    handleBeforeUnload() {
      this.destroyAllConnections();
    },
    async _checkMicrophoneOnEnter() {
      if (!navigator.mediaDevices?.getUserMedia) {
        this.micPermissionGranted = false;
        this.showStatus('当前浏览器不支持麦克风', 'error');
        return;
      }
      this.micPermissionGranted = await checkMicrophonePermission();
      if (!this.micPermissionGranted) {
        this.showStatus('麦克风权限被拒绝，首次通话时将再次请求', 'warn');
      }
    },
    _releaseMicrophoneResources() {
      if (this.phone) {
        try { this.phone.releaseLocalStream(); } catch (e) {}
      }
    },
    _scheduleCallEndSync(callUuid) {
      if (!callUuid) return;
      callUuid = String(callUuid);
      this._ensureCallTracking();
      const record = this.callUuidMap[callUuid];
      if (record?.synced || record?.syncScheduled) return;
      if (this._syncingUuids[callUuid]) return;
      if (this._syncTimers[callUuid]) return;
      if (record) record.syncScheduled = true;
      const pendingSyncs = this._getPendingSyncs();
      if (!pendingSyncs.some(s => s.uuid === callUuid)) {
        pendingSyncs.push({ uuid: callUuid, scheduledAt: Date.now(), executeAfter: Date.now() + SYNC_DELAY_MS, retryCount: 0 });
        this._savePendingSyncs(pendingSyncs);
      }
      this._syncTimers[callUuid] = setTimeout(() => {
        delete this._syncTimers[callUuid];
        this._executeCallEndSync(callUuid);
      }, SYNC_DELAY_MS);
    },
    _executeCallEndSync(callUuid) {
      if (!callUuid) return;
      callUuid = String(callUuid);
      this._ensureCallTracking();
      const record = this.callUuidMap[callUuid];
      if (record?.synced) return;
      if (this._syncingUuids[callUuid]) return;
      this._syncingUuids[callUuid] = true;
      callEndSyncByUuid({ uuid: callUuid }).then(() => {
        this._markCallSyncSuccess(callUuid);
      }).catch((err) => {
        const msg = err?.message || err?.msg || String(err || '');
        if (this._isDuplicateSyncError(msg) || this._isSyncNotFoundError(msg)) {
          this._markCallSyncSuccess(callUuid);
          return;
        }
        delete this._syncingUuids[callUuid];
        if (this.callUuidMap[callUuid]) this.callUuidMap[callUuid].syncScheduled = false;
        const pendingSyncs = this._getPendingSyncs();
        const task = pendingSyncs.find(s => s.uuid === callUuid);
        if (task) {
          task.retryCount = (task.retryCount || 0) + 1;
          if (task.retryCount >= 3) this._removePendingSync(callUuid);
          else {
            task.executeAfter = Date.now() + task.retryCount * SYNC_DELAY_MS;
            this._savePendingSyncs(pendingSyncs);
            if (!this._syncTimers[callUuid]) {
              this._syncTimers[callUuid] = setTimeout(() => {
                delete this._syncTimers[callUuid];
                this._executeCallEndSync(callUuid);
              }, task.retryCount * SYNC_DELAY_MS);
            }
          }
        } else if (!this._syncTimers[callUuid]) {
          this._scheduleCallEndSync(callUuid);
        }
      });
    },
    _markCallSyncSuccess(callUuid) {
      if (!callUuid) return;
      callUuid = String(callUuid);
      this._ensureCallTracking();
      if (this.callUuidMap[callUuid]) {
        this.callUuidMap[callUuid].synced = true;
        this.callUuidMap[callUuid].syncScheduled = false;
      }
      this._markCallEnded(callUuid);
      delete this._syncingUuids[callUuid];
      this._removePendingSync(callUuid);
      if (this.ccPhoneBar) this.ccPhoneBar.clearActiveCallUuid();
      this._cleanupCallUuidMap(20);
    },
    _isDuplicateSyncError(msg) {
      return /duplicate|PRIMARY|SQLIntegrityConstraintViolation|已存在|重复/i.test(msg);
    },
    _isSyncNotFoundError(msg) {
      return /不存在|not found|找不到|未找到/i.test(msg);
    },
    _clearAllSyncTimers() {
      if (!this._syncTimers) {
        this._syncTimers = {};
        return;
      }
      Object.keys(this._syncTimers).forEach((uuid) => {
        clearTimeout(this._syncTimers[uuid]);
      });
      this._syncTimers = {};
    },
    _getPendingSyncs() { try { const data = sessionStorage.getItem('_sip_pending_syncs'); return data ? JSON.parse(data) : []; } catch(e) { return []; } },
    _savePendingSyncs(list) { try { sessionStorage.setItem('_sip_pending_syncs', JSON.stringify(list)); } catch(e) {} },
    _removePendingSync(callUuid) { const filtered = this._getPendingSyncs().filter(s => s.uuid !== callUuid); this._savePendingSyncs(filtered); },
    _processStaleSyncs() {
      this._ensureCallTracking();
      const pendingSyncs = this._getPendingSyncs();
      if (pendingSyncs.length === 0) return;
      const now = Date.now();
      const remaining = [];
      for (const task of pendingSyncs) {
        if (!task?.uuid) continue;
        const uuid = String(task.uuid);
        if ((task.retryCount || 0) >= 3) continue;
        if (this._endedCallUuids?.[uuid] && this.callUuidMap?.[uuid]?.synced) continue;
        const record = this.callUuidMap?.[uuid];
        if (record?.synced) continue;
        if (now >= task.executeAfter) this._scheduleCallEndSync(uuid);
        else {
          remaining.push({ ...task, uuid });
          if (!this._syncTimers[uuid]) {
            this._syncTimers[uuid] = setTimeout(() => {
              delete this._syncTimers[uuid];
              this._scheduleCallEndSync(uuid);
            }, Math.max(0, task.executeAfter - now));
          }
        }
      }
      this._savePendingSyncs(remaining);
    },
    _cleanupCallUuidMap(maxKeep = 20) {
      this._ensureCallTracking();
      const keys = Object.keys(this.callUuidMap);
      if (keys.length > maxKeep) {
        const sorted = keys.sort((a,b) => (this.callUuidMap[a]?.startTime||0) - (this.callUuidMap[b]?.startTime||0));
        sorted.slice(0, keys.length - maxKeep).forEach(k => delete this.callUuidMap[k]);
      }
    },
    startAudioHealthCheck() {
      this.stopAudioHealthCheck();
      const intervalMs = this._isMobileGatewayLine() ? 1000 : 2000;
      this._audioHealthTimer = setInterval(() => {
        if ((this.callStatus !== UI_STATE.TALKING && this.callStatus !== UI_STATE.RINGING) || !this.phone) {
          this.stopAudioHealthCheck();
          return;
        }
        if (this._isHangingUp || this._completingHangup) return;
        if (this._isMobileGatewayLine() && this._syncMobileRemoteHangupIfNeeded('health_poll')) return;
        const sipAlive = this.phone.hasActiveCall() || this.phone.hasSipSession();
        if (!sipAlive) {
          if (this._isMobileGatewayLine()) {
            if (this._syncMobileRemoteHangupIfNeeded('health_sip_gone')) return;
          } else if (this._isRemoteCallAlive()) return;
          if (this.callStatus === UI_STATE.TALKING || this.callDuration !== '00:00') {
            console.warn('[挂机检测] SIP已无会话，同步双挂');
            this._completeCallHangup('sip', '对方已挂机');
            return;
          }
          if (this._isRingingBridgeProtected()) {
            console.debug('[挂机检测] 振铃桥接中SIP无会话，继续等待IPCC');
            return;
          }
          console.warn('[挂机检测] 双通道均无活跃媒体，同步双挂');
          this._completeCallHangup('sip', '对方已挂机');
          return;
        }
        if (this.callStatus === UI_STATE.TALKING) {
          if (!this._isMobileGatewayLine() && this.phone) {
            this.phone.ensureLocalStream()
              .then(() => {
                this.phone.SetMicPhone(this.isMicMuted, this.micVolume);
                this.phone.resumeRemoteAudio();
              })
              .catch(() => {});
          } else if (this.phone) {
            this.phone.resumeRemoteAudio();
          }
        }
      }, intervalMs);
    },
    stopAudioHealthCheck() {
      if (this._audioHealthTimer) {
        clearInterval(this._audioHealthTimer);
        this._audioHealthTimer = null;
      }
    },
    async _acquireWakeLock() {
      if (!('wakeLock' in navigator)) return;
      try {
        if (this._wakeLock) return;
        this._wakeLock = await navigator.wakeLock.request('screen');
        this._wakeLock.addEventListener('release', () => { this._wakeLock = null; });
      } catch (e) {
        this._wakeLock = null;
      }
    },
    _releaseWakeLock() {
      if (this._wakeLock) {
        this._wakeLock.release().catch(() => {});
        this._wakeLock = null;
      }
    },
    _handleVisibilityChange() {
      if (document.visibilityState !== 'visible') return;
      if (this.callStatus !== UI_STATE.TALKING || !this.phone) return;
      this.phone.resumeRemoteAudio();
      if (this._wakeLock === null) this._acquireWakeLock();
    },
    // ========== 网关切换相关 ==========
    getGatewayRuleInfo(gateway) {
      if (!gateway) return null;
      const text = `${gateway.gwName || ''}${gateway.gwDesc || ''}${gateway.caller || ''}`;
      let matched = null;
      let bestLen = 0;
      GATEWAY_LINE_RULES.forEach((rule) => {
        rule.match.forEach((key) => {
          if (text.includes(key) && key.length > bestLen) {
            matched = rule;
            bestLen = key.length;
          }
        });
      });
      return matched;
    },
    getGatewayBadge(gateway) {
      const info = this.getGatewayRuleInfo(gateway);
      return info && info.badge ? info.badge : '';
    },
    getGatewayDisplayTitle(gateway) {
      if (!gateway) return '未知线路';
      const info = this.getGatewayRuleInfo(gateway);
      if (info) return info.title;
      return gateway.gwName || gateway.gwDesc || `线路 ${gateway.id}`;
    },
    getRuleCellValue(gateway, label) {
      if (label === GATEWAY_RATE_COLUMN) {
        const rate = gateway?.connectRate ?? gateway?.realConnectRate ?? gateway?.conntectRate;
        if (rate != null && rate !== '') {
          const num = Number(rate);
          return Number.isFinite(num) ? `${num}%` : String(rate);
        }
      }
      const info = this.getGatewayRuleInfo(gateway);
      if (info) {
        const item = info.rules.find(r => r.label === label);
        if (item) return item.value;
      }
      return '—';
    },
    _syncSelectedGatewayFromConnected() {
      const connectedId = this.currentConnectedGatewayIds[0];
      if (connectedId) {
        this.selectedGatewayId = connectedId;
        return;
      }
      if (!this.selectedGatewayId && this.gatewayList.length) {
        this.selectedGatewayId = this.gatewayList[0].id;
      }
    },
    async loadGatewayList() {
      if (this.gatewayListLoading) return;
      this.gatewayListLoading = true;
      try {
        const response = await remoteGatewayList({
          pageNum: 1,
          pageSize: 50,
          isAICall: false,
          params: { purposes: [1, 3] }
        });
        this.gatewayList = response.rows || [];
        this._syncSelectedGatewayFromConnected();
      } catch (error) {
        this.showStatus('获取线路失败', 'error');
      } finally {
        this.gatewayListLoading = false;
      }
    },
    async reconnectWithSelectedGateway() {
      if (!this.selectedGatewayId) {
        this.$message.warning('请先选择一条线路');
        return;
      }
      if (this.callStatus !== UI_STATE.IDLE) {
        this.$message.warning('通话中无法切换线路');
        return;
      }
      if (this.isGatewayReconnecting) return;
      const selected = this.gatewayList.find(gw => String(gw.id) === String(this.selectedGatewayId));
      if (!selected) {
        this.$message.error('选中的线路不存在，请刷新列表');
        return;
      }
      const isSame = this.isGatewayConnected(this.selectedGatewayId);
      if (isSame) this.$message.info('将使用当前线路重新连接');
      this.isGatewayReconnecting = true;
      try {
        await this._ensureCleanIpccState(true);
        this.showStatus('准备重新连接...', 'info');
        this.showStatus('重新连接中...', 'info');
        await this.initCCAndStartWithGateway(String(this.selectedGatewayId), { forceRefreshToken: true });
        this.systemReady = true;
        this.initFailed = false;
        this.showStatus('线路重连成功', 'success');
      } catch (err) {
        this.showStatus('线路重连失败', 'error');
        this.$message.error('线路重连失败: ' + (err.message || '未知错误'));
      } finally {
        this.isGatewayReconnecting = false;
      }
    },
    isGatewaySelected(gatewayId) { return this.selectedGatewayId && String(this.selectedGatewayId) === String(gatewayId); },
    selectGateway(gatewayId) { this.selectedGatewayId = gatewayId; },
    onGatewayRowClick(row) { if (row && row.id != null) this.selectGateway(row.id); },
    gatewayRowClassName({ row }) {
      const classes = [];
      if (this.isGatewaySelected(row.id)) classes.push('row-selected');
      if (this.isGatewayConnected(row.id)) classes.push('row-connected');
      const info = this.getGatewayRuleInfo(row);
      if (info && info.theme) classes.push('row-theme-' + info.theme);
      return classes.join(' ');
    },
    isGatewayConnected(gatewayId) { return gatewayId && this.currentConnectedGatewayIds.includes(String(gatewayId)); },
    async cleanupExistingConnections() {
      if (this.callStatus !== UI_STATE.IDLE) {
        console.warn('[清理] 通话中，跳过连接清理');
        return;
      }
      await this._ensureCleanIpccState(true);
      if (this.statusTimerId) clearTimeout(this.statusTimerId);
      this.callStatus = UI_STATE.IDLE;
      if (this.ccConnectingPromise) {
        this._finishCcConnecting(false, new Error('用户主动重置'));
      }
      this.dialNumber = ''; this.province = ''; this.callDuration = '00:00'; this.showLeftButton = false; this.showRightButton = false;
      this.leftButtonNormal = false; this.rightButtonNormal = false; this.rightButtonHangup = false; this.callDirection = ''; this.dropdownVisible = false;
      this._isCalling = false; if (this._callLockTimer) clearTimeout(this._callLockTimer); if (this._callTimeoutTimer) clearTimeout(this._callTimeoutTimer);
      this.stopAudioHealthCheck();
      this._releaseWakeLock();
      this.systemReady = false;
    },
    async initCCAndStartWithGateway(selectedGatewayId, options = {}) {
      const forceRefreshToken = options.forceRefreshToken !== false;
      this.systemReady = false;
      this.ccSocketConnected = false; this.ccSocketFailed = false; this.isCallingReady = false;
      try {
        await this.ensureCCSocketConnectWithGateway(selectedGatewayId, forceRefreshToken);
        await this.waitForAgentReady(IPCC_CONFIG.AGENT_READY_TIMEOUT_MS);
        await this.startPhone();
        await this.waitForRegistration(IPCC_CONFIG.SIP_REGISTER_TIMEOUT_MS);
        if (!this.isRegistered || !this.ccSocketConnected || !this.isCallingReady) {
          throw new Error('线路重连后系统未就绪');
        }
        this.systemReady = true;
      } catch (err) {
        this.ccSocketFailed = true;
        this.systemReady = false;
        this.showStatus(`初始化失败: ${err.message}`, 'error');
        if (err.message.includes('分机号')) this.$message({ message: '未配置分机号，请联系管理员绑定 SIP 角色', type: 'warning', duration: 5000, showClose: true });
        throw err;
      }
    },
    async ensureCCSocketConnectWithGateway(selectedGatewayId, forceRefreshToken = true) {
      if (this.ccSocketConnected && this.isGatewayConnected(selectedGatewayId) && !forceRefreshToken) {
        return Promise.resolve();
      }
      if (this.ccConnectingPromise) return this.ccConnectingPromise;
      this.ccConnectingPromise = new Promise((resolve, reject) => {
        this.ccConnectingResolve = resolve;
        this.ccConnectingReject = reject;
        this._doConnectCCSocketWithGateway(selectedGatewayId, forceRefreshToken);
      });
      try {
        await this.ccConnectingPromise;
        if (selectedGatewayId) {
          this.currentConnectedGatewayIds = String(selectedGatewayId).split(',').map(id => id.trim()).filter(id => id);
          this._syncSelectedGatewayFromConnected();
        }
      } catch (err) {
        throw err;
      }
    },
    async _doConnectCCSocketWithGateway(selectedGatewayId, forceRefreshToken = true) {
      try {
        const extRes = await myCallUser();
        if (extRes.code !== 200 || !extRes.data || !extRes.data.extNum) throw new Error('未查询到分机号信息');
        const { extNum, extPass } = extRes.data;
        this.setupDefaultAccount(extNum, extPass);
        const gatewayKey = selectedGatewayId ? String(selectedGatewayId) : 'default';
        if (forceRefreshToken) {
          this._clearIpccTokenCache(extNum, gatewayKey);
        }
        const callConfig = await this._loadIpccCallConfig(extNum, selectedGatewayId, forceRefreshToken);
        this._ipccCallConfig = callConfig;
        if (!this.ccPhoneBar) {
          this.ccPhoneBar = new ccPhoneBarSocket();
          this.ccPhoneBar.initConfig(callConfig);
          this._bindCCEvents();
          this._ccEventsBound = true;
        } else {
          this.ccPhoneBar.initConfig(callConfig);
          if (this.ccPhoneBar.resetKickedState) this.ccPhoneBar.resetKickedState();
        }
        this._ccConnectStartedAt = Date.now();
        this.ccPhoneBar.connect();
        this._ccConnectingTimeoutId = setTimeout(() => {
          if (!this.ccSocketConnected) {
            this._finishCcConnecting(false, new Error('连接超时'));
          }
        }, IPCC_CONFIG.CONNECT_TIMEOUT);
      } catch (err) {
        this._finishCcConnecting(false, err);
        throw err;
      }
    }
  }
};
</script>

<style scoped>
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');

.softphone-page {
  width: 100%;
  min-height: calc(100vh - 84px);
  padding: 28px 24px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f0f2f5;
}

.softphone-layout {
  display: flex;
  align-items: stretch;
  gap: 24px;
  width: 100%;
  max-width: 1520px;
}

.material-icons { font-size: 20px; vertical-align: middle; }

.gateway-section {
  flex: 2.2;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  overflow: hidden;
}

.dialer-column {
  flex: 1;
  min-width: 300px;
  max-width: 340px;
  display: flex;
  flex-shrink: 0;
}

.dialer-panel {
  width: 100%;
  display: flex;
  flex-direction: column;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  overflow: hidden;
}

.dialer-panel-head {
  padding: 14px 16px 12px;
  border-bottom: 1px solid #ebeef5;
  background: linear-gradient(180deg, #fafcff 0%, #fff 100%);
}
.dialer-panel-title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 4px;
}
.dialer-panel-title .material-icons { color: #409eff; font-size: 20px; }
.dialer-panel-hint { font-size: 12px; color: #909399; }

.section-head, .section-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 20px;
  flex-shrink: 0;
}
.section-head {
  border-bottom: 1px solid #ebeef5;
  background: linear-gradient(180deg, #f0f7ff 0%, #fff 100%);
}
.section-head-left { flex: 1; min-width: 0; }
.section-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 6px;
}
.section-title .material-icons { color: #409eff; font-size: 22px; }
.section-desc {
  margin: 0;
  font-size: 12px;
  color: #909399;
  line-height: 1.5;
}
.section-foot {
  border-top: 1px solid #ebeef5;
  background: #fafafa;
}
.foot-hint {
  flex: 1;
  font-size: 13px;
  color: #303133;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.foot-hint-muted { color: #909399; font-weight: 400; }
.btn-text {
  border: 1px solid #dcdfe6;
  background: #fff;
  color: #606266;
  border-radius: 4px;
  padding: 6px 12px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  transition: all .2s;
}
.btn-text:hover:not(:disabled) { color: #409eff; border-color: #c6e2ff; background: #ecf5ff; }
.btn-text:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary {
  border: none;
  background: #409eff;
  color: #fff;
  border-radius: 4px;
  padding: 9px 20px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 500;
  transition: background .2s;
}
.btn-primary:hover:not(:disabled) { background: #66b1ff; }
.btn-primary:disabled { opacity: .6; cursor: not-allowed; background: #a0cfff; }
.btn-sm { padding: 6px 14px; font-size: 12px; }

.gateway-table-wrap {
  flex: 1;
  min-height: 280px;
  max-height: calc(100vh - 260px);
  overflow-y: auto;
  overflow-x: hidden;
  padding: 16px 20px;
}
.empty-tip {
  padding: 60px 16px;
  text-align: center;
  color: #909399;
  font-size: 14px;
}

.gateway-el-table { width: 100%; }
.gateway-el-table ::v-deep .el-table__header-wrapper th {
  background: #f5f7fa !important;
  color: #303133;
  font-weight: 600;
  font-size: 13px;
  padding: 12px 0;
}
.gateway-el-table ::v-deep .el-table__header-wrapper th .cell {
  white-space: normal;
  word-break: break-word;
  line-height: 1.4;
  padding: 0 10px;
}
.gateway-el-table ::v-deep .el-table__body td {
  padding: 14px 0;
  font-size: 13px;
  color: #606266;
  vertical-align: top;
  height: auto;
}
.gateway-el-table ::v-deep .el-table__body tr {
  height: auto;
}
.gateway-el-table ::v-deep .el-table__body td .cell {
  white-space: normal !important;
  word-break: break-word;
  overflow: visible !important;
  text-overflow: clip !important;
  line-height: 1.55;
  padding: 0 10px;
}
.gateway-el-table ::v-deep .el-table__row { cursor: pointer; }
.gateway-el-table ::v-deep .el-table__row.row-selected td { background: #ecf5ff !important; }
.gateway-el-table ::v-deep .el-table__row.row-connected td:first-child { box-shadow: inset 3px 0 0 #67c23a; }
.gateway-el-table ::v-deep .el-table__row.row-selected.row-connected td:first-child { box-shadow: inset 3px 0 0 #409eff; }
.gateway-el-table ::v-deep .el-table__row:hover > td { background: #f5f7fa !important; }
.gateway-el-table ::v-deep .el-table__row.row-selected:hover > td { background: #ecf5ff !important; }
.gateway-el-table ::v-deep .rate-cell-col .cell {
  justify-content: center;
  text-align: center;
}
.rate-col-header {
  color: #f56c6c;
  font-weight: 600;
}
.rate-cell-value {
  color: #606266;
  font-weight: 500;
}

.line-name { font-weight: 600; color: #303133; margin-bottom: 6px; line-height: 1.45; word-break: break-word; }
.line-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.line-tags .el-tag { margin: 0; }
.cell-text {
  display: block;
  line-height: 1.55;
  word-break: break-word;
  white-space: normal;
}
.radio-dot {
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid #dcdfe6;
  border-radius: 50%;
  transition: all .15s;
  vertical-align: middle;
}
.radio-dot.on { border-color: #409eff; background: #409eff; box-shadow: inset 0 0 0 3px #fff; }

.dialer-section { width: 100%; display: flex; justify-content: center; padding: 8px 0 12px; }
.dialer {
  width: 100%;
  max-width: 300px;
  background: transparent;
  border-radius: 0;
  border: none;
  box-shadow: none;
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.dialer.is-locked .dialer-keypad, .dialer.is-locked .call-buttons { pointer-events: none; opacity: .5; }
.dialer-mask { position: absolute; inset: 0; z-index: 10; background: rgba(255,255,255,.94); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; }
.dialer-mask p { margin: 0; font-size: 13px; color: #64748b; text-align: center; padding: 0 20px; }
.spinner { width: 32px; height: 32px; border: 3px solid #e2e8f0; border-top-color: #6366f1; border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.status-bar { display: flex; align-items: center; height: 42px; padding: 0 12px; border-bottom: 1px solid #ebeef5; background: #fafafa; border-radius: 6px 6px 0 0; margin: 0 12px; }
.status-left, .status-right { flex: 0 0 auto; display: flex; align-items: center; gap: 2px; }
.status-center { flex: 1; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.display-user { font-size: 13px; font-weight: 500; color: #475569; }
.network-available { color: #22c55e; }
.no-network { color: #ef4444; }
.network-connecting { color: #f59e0b; animation: pulse 1.5s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }

.user-avatar-dropdown { position: relative; cursor: pointer; }
.dropdown-menu { position: absolute; top: 100%; left: 0; min-width: 150px; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 8px 24px rgba(15,23,42,.12); padding: 4px; z-index: 20; }
.dropdown-group { margin-top: 4px; }
.dropdown-item { display: flex; align-items: center; gap: 8px; padding: 7px 10px; color: #475569; text-decoration: none; font-size: 13px; border-radius: 6px; }
.dropdown-item:hover { background: #f1f5f9; color: #6366f1; }

.display-wrapper { display: flex; align-items: center; margin: 16px 14px 0; gap: 4px; position: relative; }
.dial-mode-selector { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px; font-size: 11px; color: #94a3b8; }
.radio-label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; }
.dialer-display { flex: 1; min-width: 0; height: 40px; border: none; border-bottom: 2px solid #e2e8f0; font-size: 22px; outline: none; background: transparent; color: #1e293b; }
.dialer-display:focus { border-bottom-color: #6366f1; }
.dialer-display.center-align { text-align: center; }
.dialer-display.right-align { text-align: right; }
.clear-number-btn {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border: none;
  border-radius: 10px;
  background: #f1f5f9;
  color: #64748b;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background .15s, color .15s, transform .1s;
}
.clear-number-btn .material-icons { font-size: 20px; }
.clear-number-btn:hover:not(:disabled) { background: #e2e8f0; color: #475569; }
.clear-number-btn:active:not(:disabled) { transform: scale(0.94); }
.clear-number-btn:disabled { opacity: .35; cursor: not-allowed; }
.container { display: flex; justify-content: center; margin: 2px 0; }
.call-timer, .province { font-size: 12px; color: #94a3b8; }
.decrypting-tip { color: #f59e0b; font-size: 11px; }

/* 音量面板 */
.audio-panel {
  margin: 8px 12px 4px;
  padding: 10px 12px;
  background: #f5f7fa;
  border: 1px solid #ebeef5;
  border-radius: 6px;
}
.audio-row { display: flex; align-items: center; gap: 8px; }
.audio-row + .audio-row { margin-top: 8px; }
.audio-mute-btn {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: #fff;
  color: #64748b;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.audio-mute-btn.active { background: #fef2f2; border-color: #fecaca; color: #ef4444; }
.audio-slider-wrap { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0; }
.audio-label { flex-shrink: 0; font-size: 11px; color: #64748b; width: 42px; }
.audio-slider { flex: 1; min-width: 0; height: 4px; accent-color: #6366f1; cursor: pointer; }
.audio-slider:disabled { opacity: .4; cursor: not-allowed; }
.audio-value { flex-shrink: 0; font-size: 11px; color: #94a3b8; width: 36px; text-align: right; }

.dialer-keypad { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; padding: 10px 22px; }
.dialer-button { aspect-ratio: 1; max-width: 54px; margin: 0 auto; width: 100%; border-radius: 50%; border: 1px solid #e2e8f0; background: #f8fafc; font-size: 20px; color: #334155; cursor: pointer; }
.dialer-button:active { transform: scale(.96); background: #e2e8f0; }

.call-buttons { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; padding: 4px 24px 10px; }
.call-buttons-single { grid-template-columns: 1fr; }
.call-button { width: 50px; height: 50px; margin: 0 auto; border: none; border-radius: 50%; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(15,23,42,.12); }
.call-button.hangup { background: #ef4444; }
.call-button.call-ready { background: #22c55e; }
.call-button.disabled { background: #e2e8f0; color: #94a3b8; cursor: not-allowed; box-shadow: none; }
.call-button.normal { background: #f1f5f9; color: #475569; box-shadow: none; }

.status-footer { display: flex; justify-content: space-between; padding: 6px 10px 8px; border-top: 1px solid #f8fafc; }
.status-footer-left { display: flex; align-items: center; gap: 6px; overflow: hidden; flex: 1; }
.status-bar-message { font-size: 11px; padding: 2px 8px; border-radius: 999px; color: #fff; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-bar-message.error { background: #ef4444; }
.status-bar-message.success { background: #22c55e; }
.status-bar-message.warn { background: #f59e0b; }
.status-bar-message.info { background: #6366f1; }
.reconnect-failed { font-size: 11px; background: #ef4444; color: #fff; padding: 2px 8px; border-radius: 999px; }
.version-ribbon { font-size: 10px; color: #cbd5e1; font-family: monospace; }

.modal { position: fixed; z-index: 10001; left: 50%; top: 50%; transform: translate(-50%,-50%); width: 340px; max-height: 80vh; overflow-y: auto; background: #fff; border-radius: 12px; box-shadow: 0 16px 40px rgba(15,23,42,.18); }
.modal-header { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
.modal-content { padding: 16px; }
.form-group { margin-bottom: 12px; position: relative; }
.form-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #64748b; margin-bottom: 4px; }
.info-icon { font-size: 15px; color: #cbd5e1; cursor: help; }
.form-group input, .form-group select { width: 100%; padding: 8px 10px; border: 1px solid #e2e8f0; border-radius: 8px; box-sizing: border-box; font-size: 13px; background: #f8fafc; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #6366f1; background: #fff; }
.form-group .password-toggle { position: absolute; right: 10px; top: 30px; cursor: pointer; color: #94a3b8; }
.form-buttons { display: flex; justify-content: center; gap: 40px; margin-top: 16px; }
.form-buttons button { width: 35%; padding: 8px; border: none; border-radius: 8px; color: #fff; cursor: pointer; }
.form-buttons .add-button { background: #22c55e; }
.form-buttons .cancel-button { background: #94a3b8; }

@media (max-width: 1100px) {
  .softphone-page { padding: 16px; align-items: flex-start; min-height: auto; }
  .softphone-layout { flex-direction: column; gap: 16px; }
  .gateway-section { width: 100%; flex: none; }
  .gateway-table-wrap { max-height: none; }
  .dialer-column { width: 100%; max-width: none; min-width: 0; }
  .dialer-section { padding: 8px 12px 16px; }
}
</style>
