lmx 4 jam lalu
induk
melakukan
1cbc0df7fa

+ 142 - 0
src/api/company/inboundCallManage.js

@@ -0,0 +1,142 @@
+import request from '@/utils/request'
+
+/**
+ * 查询呼入大模型配置列表
+ * @param {Object} query 查询参数
+ */
+export function listInboundLlm(query) {
+  return request({
+    url: '/company/inboundCallManage/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 新增呼入大模型配置
+ * @param {Object} data 配置数据
+ */
+export function addInboundLlm(data) {
+  return request({
+    url: '/company/inboundCallManage',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 修改呼入大模型配置
+ * @param {Object} data 配置数据
+ */
+export function updateInboundLlm(data) {
+  return request({
+    url: '/company/inboundCallManage',
+    method: 'put',
+    data: data
+  })
+}
+
+/**
+ * 删除呼入大模型配置
+ * @param {String} ids 配置ID,多个用逗号分隔
+ */
+export function delInboundLlm(ids) {
+  return request({
+    url: '/company/inboundCallManage/' + ids,
+    method: 'delete'
+  })
+}
+
+/**
+ * 获取大模型账户下拉列表
+ */
+export function listLlmAccount() {
+  return request({
+    url: '/company/inboundCallManage/llmAccountList',
+    method: 'get'
+  })
+}
+
+/**
+ * 校验被叫号码是否唯一
+ */
+export function checkCallee(id, callee) {
+  return request({
+    url: '/company/inboundCallManage/checkCallee',
+    method: 'get',
+    params: { id, callee }
+  })
+}
+
+/**
+ * 获取ASR提供商列表
+ */
+export function listAsrProvider() {
+  return request({
+    url: '/company/inboundCallManage/asrProviderList',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取TTS音色来源列表
+ */
+export function listVoiceSource() {
+  return request({
+    url: '/company/inboundCallManage/voiceSourceList',
+    method: 'get'
+  })
+}
+
+/**
+ * 根据音色来源获取音色列表
+ */
+export function listVoiceBySource(voiceSource) {
+  return request({
+    url: '/company/inboundCallManage/voiceList',
+    method: 'get',
+    params: { voiceSource }
+  })
+}
+
+/**
+ * 获取业务组列表
+ */
+export function listBizGroup() {
+  return request({
+    url: '/company/inboundCallManage/bizGroupList',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取出局网关列表
+ */
+export function listGateway() {
+  return request({
+    url: '/company/inboundCallManage/gatewayList',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取IVR列表
+ */
+export function listIvr() {
+  return request({
+    url: '/company/inboundCallManage/ivrList',
+    method: 'get'
+  })
+}
+
+/**
+ * 查询呼入通话记录列表
+ * @param {Object} query 查询参数
+ */
+export function listInboundCdr(query) {
+  return request({
+    url: '/company/inboundCallManage/inboundCdrList',
+    method: 'get',
+    params: query
+  })
+}

+ 2 - 2
src/views/company/aiModel/account/info.vue

@@ -226,7 +226,7 @@ export default {
                 id: undefined,
                 name: '',
                 providerClassName: '',
-                concurrentNum: '',
+                concurrentNum: null,
                 interruptFlag: 0,
                 interruptKeywords: '',
                 interruptIgnoreKeywords: '呃 哦 哦哦 嗯 嗯嗯 嗯好的 好的 对 对对 是的 明白 啊 这样啊 是这样啊这样的 您好 你好',
@@ -344,7 +344,7 @@ export default {
             this.form.id = data.id
             this.form.name = data.name || ''
             this.form.providerClassName = data.providerClassName || ''
-            this.form.concurrentNum = data.concurrentNum || ''
+            this.form.concurrentNum = data.concurrentNum
             this.form.interruptFlag = data.interruptFlag || 0
             this.form.interruptKeywords = data.interruptKeywords || ''
             this.form.interruptIgnoreKeywords = data.interruptIgnoreKeywords || ''

+ 672 - 0
src/views/company/aiModel/inboundCallManage/inboundCallRecord.vue

@@ -0,0 +1,672 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索表单 -->
+    <el-form
+      v-show="showSearch"
+      ref="queryForm"
+      :inline="true"
+      :model="queryParams"
+      label-width="100px"
+    >
+      <el-form-item label="通话UUID" prop="uuid">
+        <el-input
+          v-model="queryParams.uuid"
+          clearable
+          placeholder="请输入通话UUID"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="主叫号码" prop="caller">
+        <el-input
+          v-model="queryParams.caller"
+          clearable
+          placeholder="请输入主叫号码"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="接听分机号" prop="extnum">
+        <el-input
+          v-model="queryParams.extnum"
+          clearable
+          placeholder="请输入接听分机号"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="通话时长(秒)">
+        <el-input-number
+          v-model="queryParams.params.timeLenStart"
+          :min="0"
+          size="small"
+          style="width: 120px"
+          placeholder="最小值"
+          controls-position="right"
+        />
+        <span class="range-separator">-</span>
+        <el-input-number
+          v-model="queryParams.params.timeLenEnd"
+          :min="0"
+          size="small"
+          style="width: 120px"
+          placeholder="最大值"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item label="呼入时间">
+        <el-date-picker
+          v-model="inboundTimeRange"
+          type="datetimerange"
+          size="small"
+          style="width: 340px"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="接听时间">
+        <el-date-picker
+          v-model="answeredTimeRange"
+          type="datetimerange"
+          size="small"
+          style="width: 340px"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="挂机时间">
+        <el-date-picker
+          v-model="hangupTimeRange"
+          type="datetimerange"
+          size="small"
+          style="width: 340px"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-table
+      v-loading="loading"
+      :data="tableData"
+      border
+      style="width: 100%"
+    >
+      <el-table-column align="center" label="UUID" prop="uuid" min-width="120" show-overflow-tooltip />
+      <el-table-column align="center" label="主叫号码" prop="caller" min-width="110" show-overflow-tooltip />
+       <el-table-column align="center" label="被叫号码" prop="callee" min-width="110" show-overflow-tooltip />
+      <el-table-column align="center" label="录音文件" min-width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="getMediaType(scope.row.wavFile) === 'audio'" type="primary" size="small">音频</el-tag>
+          <el-tag v-else-if="getMediaType(scope.row.wavFile) === 'video'" type="success" size="small">视频</el-tag>
+          <span v-else>无</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="呼入时间" min-width="150">
+        <template slot-scope="scope">{{ formatTimestamp(scope.row.inboundTime) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="接听时间" min-width="150">
+        <template slot-scope="scope">{{ formatTimestamp(scope.row.answeredTime) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="接听分机" prop="extnum" min-width="90" />
+      <el-table-column align="center" label="接听坐席" prop="opnum" min-width="90" />
+      <el-table-column align="center" label="挂机时间" min-width="150">
+        <template slot-scope="scope">{{ formatTimestamp(scope.row.hangupTime) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="业务组" prop="groupName" min-width="100" show-overflow-tooltip />
+      <el-table-column align="center" label="通话时长" min-width="90">
+        <template slot-scope="scope">{{ formatDuration(scope.row.timeLen) }}</template>
+      </el-table-column>
+      <!-- <el-table-column align="center" label="挂机原因" min-width="120" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <span
+            class="hangup-cause-cell"
+            :title="scope.row.hangupCause"
+            @dblclick="copyText(formatHangupCause(scope.row.hangupCause))"
+          >{{ formatHangupCause(scope.row.hangupCause) }}</span>
+        </template>
+      </el-table-column> -->
+      <el-table-column
+        align="center"
+        class-name="small-padding fixed-width"
+        label="操作"
+        width="160"
+      >
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.wavFileUrl"
+            size="mini"
+            type="text"
+            icon="el-icon-video-play"
+            @click="handlePlay(scope.row)"
+          >播放</el-button>
+          <el-button
+            v-if="scope.row.wavFileUrl"
+            size="mini"
+            type="text"
+            icon="el-icon-download"
+            @click="handleDownload(scope.row)"
+          >下载</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="total > 0"
+      :limit.sync="queryParams.pageSize"
+      :page.sync="queryParams.pageNum"
+      :total="total"
+      @pagination="getList"
+    />
+
+    <!-- 播放弹窗 -->
+    <el-dialog
+      title="录音播放"
+      :visible.sync="playDialogVisible"
+      width="700px"
+      append-to-body
+      @close="handlePlayDialogClose"
+    >
+      <!-- 音频/视频播放器 -->
+      <div v-if="currentMediaType === 'audio'" class="player-wrapper">
+        <audio
+          ref="audioPlayer"
+          :src="currentWavFileUrl"
+          controls
+          style="width: 100%"
+        />
+      </div>
+      <div v-else-if="currentMediaType === 'video'" class="player-wrapper">
+        <video
+          ref="videoPlayer"
+          :src="currentWavFileUrl"
+          controls
+          style="width: 100%"
+        />
+      </div>
+
+      <!-- AI对话内容 -->
+      <div v-if="chatDialogList.length > 0" class="chat-container">
+        <div class="chat-title">AI对话内容</div>
+        <div class="chat-list">
+          <div
+            v-for="(item, index) in chatDialogList"
+            :key="index"
+            class="dialog-item"
+            :class="item.role"
+          >
+            <!-- 左侧角色(assistant / agent / kb) -->
+            <template v-if="item.role !== 'user'">
+              <span class="role-icon">
+                <i :class="getRoleIcon(item.role)" />
+              </span>
+              <span class="role-label">{{ getRoleLabel(item.role) }}</span>
+              <div class="bubble">
+                <span class="content-text">
+                  {{ item.expanded ? item.content : getTruncatedContent(item.content) }}
+                  <span
+                    v-if="item.content.length > 200"
+                    class="more-text"
+                    @click="toggleExpand(index)"
+                  >{{ item.expanded ? '[收起]' : '[更多]' }}</span>
+                </span>
+                <el-tag v-if="item.isKb" size="mini" type="warning" class="kb-tag">知识库来源</el-tag>
+              </div>
+            </template>
+            <!-- 右侧角色(user) -->
+            <template v-else>
+              <div class="bubble">
+                <span class="content-text">
+                  {{ item.expanded ? item.content : getTruncatedContent(item.content) }}
+                  <span
+                    v-if="item.content.length > 200"
+                    class="more-text"
+                    @click="toggleExpand(index)"
+                  >{{ item.expanded ? '[收起]' : '[更多]' }}</span>
+                </span>
+              </div>
+              <span class="role-icon">
+                <i class="el-icon-user" />
+              </span>
+              <span class="role-label">客户</span>
+            </template>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listInboundCdr } from '@/api/company/inboundCallManage'
+
+export default {
+  name: 'InboundCallRecord',
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 表格数据
+      tableData: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        uuid: undefined,
+        caller: undefined,
+        extnum: undefined,
+        params: {
+          timeLenStart: undefined,
+          timeLenEnd: undefined,
+          inboundTimeStart: undefined,
+          inboundTimeEnd: undefined,
+          answeredTimeStart: undefined,
+          answeredTimeEnd: undefined,
+          hangupTimeStart: undefined,
+          hangupTimeEnd: undefined
+        }
+      },
+      // 时间范围
+      inboundTimeRange: [],
+      answeredTimeRange: [],
+      hangupTimeRange: [],
+      // 播放弹窗
+      playDialogVisible: false,
+      currentWavFileUrl: '',
+      currentMediaType: '',
+      // AI对话列表
+      chatDialogList: []
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true
+      // 处理时间范围参数(转为epoch毫秒时间戳,与后端Long类型字段匹配)
+      if (this.inboundTimeRange && this.inboundTimeRange.length === 2) {
+        this.queryParams.params.inboundTimeStart = new Date(this.inboundTimeRange[0]).getTime()
+        this.queryParams.params.inboundTimeEnd = new Date(this.inboundTimeRange[1]).getTime()
+      } else {
+        this.queryParams.params.inboundTimeStart = undefined
+        this.queryParams.params.inboundTimeEnd = undefined
+      }
+      if (this.answeredTimeRange && this.answeredTimeRange.length === 2) {
+        this.queryParams.params.answeredTimeStart = new Date(this.answeredTimeRange[0]).getTime()
+        this.queryParams.params.answeredTimeEnd = new Date(this.answeredTimeRange[1]).getTime()
+      } else {
+        this.queryParams.params.answeredTimeStart = undefined
+        this.queryParams.params.answeredTimeEnd = undefined
+      }
+      if (this.hangupTimeRange && this.hangupTimeRange.length === 2) {
+        this.queryParams.params.hangupTimeStart = new Date(this.hangupTimeRange[0]).getTime()
+        this.queryParams.params.hangupTimeEnd = new Date(this.hangupTimeRange[1]).getTime()
+      } else {
+        this.queryParams.params.hangupTimeStart = undefined
+        this.queryParams.params.hangupTimeEnd = undefined
+      }
+      // 通话时长:前端输入秒,数据库存毫秒,显示用Math.ceil(ms/1000)
+      // Math.ceil(v/1000)==N 等价于 (N-1)*1000 < v <= N*1000,即 v∈[(N-1)*1000+1, N*1000]
+      const params = { ...this.queryParams }
+      params.params = { ...this.queryParams.params }
+      if (params.params.timeLenStart != null && params.params.timeLenStart !== undefined) {
+        const n = params.params.timeLenStart
+        params.params.timeLenStart = n > 0 ? (n - 1) * 1000 + 1 : 0
+      }
+      if (params.params.timeLenEnd != null && params.params.timeLenEnd !== undefined) {
+        params.params.timeLenEnd = params.params.timeLenEnd * 1000
+      }
+      listInboundCdr(params).then(response => {
+        this.tableData = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.inboundTimeRange = []
+      this.answeredTimeRange = []
+      this.hangupTimeRange = []
+      this.resetForm('queryForm')
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        uuid: undefined,
+        caller: undefined,
+        extnum: undefined,
+        params: {
+          timeLenStart: undefined,
+          timeLenEnd: undefined,
+          inboundTimeStart: undefined,
+          inboundTimeEnd: undefined,
+          answeredTimeStart: undefined,
+          answeredTimeEnd: undefined,
+          hangupTimeStart: undefined,
+          hangupTimeEnd: undefined
+        }
+      }
+      this.handleQuery()
+    },
+    /** 格式化时间戳 - 毫秒级时间戳转 yyyy-MM-dd HH:mm:ss */
+    formatTimestamp(value) {
+      if (!value || value <= 0) return '-'
+      const date = new Date(Number(value))
+      const year = date.getFullYear()
+      const month = (date.getMonth() + 1).toString().padStart(2, '0')
+      const day = date.getDate().toString().padStart(2, '0')
+      const hour = date.getHours().toString().padStart(2, '0')
+      const minute = date.getMinutes().toString().padStart(2, '0')
+      const second = date.getSeconds().toString().padStart(2, '0')
+      return `${year}-${month}-${day} ${hour}:${minute}:${second}`
+    },
+    /** 格式化通话时长 - 毫秒转 mm分ss秒 */
+    formatDuration(value) {
+      if (!value || value <= 0) return '0秒'
+      const totalSeconds = Math.ceil(value / 1000)
+      const minutes = Math.floor(totalSeconds / 60)
+      const seconds = totalSeconds % 60
+      if (minutes > 0) {
+        return `${minutes}分${seconds.toString().padStart(2, '0')}秒`
+      }
+      return `${seconds}秒`
+    },
+    /** 判断媒体类型 */
+    getMediaType(wavFile) {
+      if (!wavFile) return 'none'
+      const ext = wavFile.split('.').pop().toLowerCase()
+      if (['wav', 'mp3', 'aac'].includes(ext)) return 'audio'
+      if (['mp4', 'avi', 'mov'].includes(ext)) return 'video'
+      return 'none'
+    },
+    /** 格式化挂机原因 */
+    formatHangupCause(value) {
+      if (!value) return '-'
+      try {
+        const obj = JSON.parse(value)
+        if (obj && obj.code !== undefined) {
+          return `${obj.code}: ${obj.details || ''}`
+        }
+        return value
+      } catch (e) {
+        return value
+      }
+    },
+    /** 双击复制文本 */
+    copyText(text) {
+      if (!text || text === '-') return
+      if (navigator.clipboard && window.isSecureContext) {
+        navigator.clipboard.writeText(text).then(() => {
+          this.$message.success('复制成功')
+        }).catch(() => {
+          this.fallbackCopy(text)
+        })
+      } else {
+        this.fallbackCopy(text)
+      }
+    },
+    /** 降级复制方案 */
+    fallbackCopy(text) {
+      const textArea = document.createElement('textarea')
+      textArea.value = text
+      textArea.style.position = 'fixed'
+      textArea.style.left = '-9999px'
+      document.body.appendChild(textArea)
+      textArea.focus()
+      textArea.select()
+      try {
+        document.execCommand('copy')
+        this.$message.success('复制成功')
+      } catch (err) {
+        this.$message.error('复制失败')
+      }
+      document.body.removeChild(textArea)
+    },
+    /** 播放按钮操作 */
+    handlePlay(row) {
+      this.currentWavFileUrl = row.wavFileUrl
+      this.currentMediaType = this.getMediaType(row.wavFileUrl || row.wavFile)
+      // 解析AI对话内容
+      this.chatDialogList = this.parseChatContent(row.chatContent)
+      this.playDialogVisible = true
+    },
+    /** 解析聊天内容 */
+    parseChatContent(chatContent) {
+      if (!chatContent) return []
+      try {
+        let items = chatContent
+        // 如果是字符串,尝试解析
+        if (typeof items === 'string') {
+          items = JSON.parse(items)
+        }
+        // 可能是双重JSON字符串
+        if (typeof items === 'string') {
+          items = JSON.parse(items)
+        }
+        if (!Array.isArray(items)) return []
+        return items.filter(item => ['user', 'assistant', 'agent'].includes(item.role)).map(item => {
+          let content = item.content || ''
+          let isKb = false
+          // 检查是否包含JSON(知识库返回结果)
+          if (this.containsJson(content)) {
+            isKb = true
+            // 移除JSON部分
+            let cleaned = content
+            while (this.containsJson(cleaned)) {
+              cleaned = cleaned.replace(/\{[^{}]*\}/s, '').trim()
+            }
+            content = cleaned || content
+          }
+          return {
+            role: isKb ? 'kb' : item.role,
+            content: content,
+            isKb: isKb,
+            expanded: false
+          }
+        }).filter(item => item.content)
+      } catch (e) {
+        return []
+      }
+    },
+    /** 检查字符串是否包含JSON */
+    containsJson(input) {
+      if (!input) return false
+      return /\{.*?\}/s.test(input)
+    },
+    /** 获取截断内容 */
+    getTruncatedContent(content) {
+      if (!content || content.length <= 200) return content
+      return content.substring(0, 200) + '...'
+    },
+    /** 切换展开/收起 */
+    toggleExpand(index) {
+      this.$set(this.chatDialogList[index], 'expanded', !this.chatDialogList[index].expanded)
+    },
+    /** 获取角色图标 */
+    getRoleIcon(role) {
+      const iconMap = {
+        assistant: 'el-icon-monitor',
+        agent: 'el-icon-phone-outline',
+        kb: 'el-icon-folder-opened',
+        user: 'el-icon-user'
+      }
+      return iconMap[role] || 'el-icon-user'
+    },
+    /** 获取角色标签 */
+    getRoleLabel(role) {
+      const labelMap = {
+        assistant: 'AI',
+        agent: '坐席',
+        kb: '知识库',
+        user: '客户'
+      }
+      return labelMap[role] || role
+    },
+    /** 下载按钮操作 */
+    handleDownload(row) {
+      if (row.wavFileUrl) {
+        const link = document.createElement('a')
+        link.href = row.wavFileUrl
+        link.target = '_blank'
+        link.download = ''
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+      }
+    },
+    /** 播放弹窗关闭 */
+    handlePlayDialogClose() {
+      // 停止音频/视频播放
+      if (this.$refs.audioPlayer) {
+        this.$refs.audioPlayer.pause()
+        this.$refs.audioPlayer.currentTime = 0
+      }
+      if (this.$refs.videoPlayer) {
+        this.$refs.videoPlayer.pause()
+        this.$refs.videoPlayer.currentTime = 0
+      }
+      this.currentWavFileUrl = ''
+      this.currentMediaType = ''
+      this.chatDialogList = []
+    }
+  }
+}
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+.range-separator {
+  padding: 0 5px;
+}
+.player-wrapper {
+  margin-bottom: 16px;
+}
+/* 对话容器 */
+.chat-container {
+  margin-top: 16px;
+  border-top: 1px solid #ebeef5;
+  padding-top: 12px;
+}
+.chat-title {
+  font-size: 15px;
+  font-weight: bold;
+  margin-bottom: 12px;
+  color: #303133;
+}
+.chat-list {
+  max-height: 400px;
+  overflow-y: auto;
+  padding-right: 6px;
+}
+/* 对话气泡 */
+.dialog-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12px;
+}
+.dialog-item.user {
+  justify-content: flex-end;
+}
+.dialog-item.assistant,
+.dialog-item.agent,
+.dialog-item.kb {
+  justify-content: flex-start;
+}
+.bubble {
+  display: inline-block;
+  max-width: 70%;
+  padding: 10px 14px;
+  border-radius: 12px;
+  word-break: break-word;
+  line-height: 1.6;
+  font-size: 14px;
+  color: #263238;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+}
+.dialog-item.user .bubble {
+  background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
+  text-align: right;
+}
+.dialog-item.assistant .bubble {
+  background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
+}
+.dialog-item.agent .bubble {
+  background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
+}
+.dialog-item.kb .bubble {
+  background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
+}
+.role-icon {
+  display: flex;
+  align-items: center;
+  padding: 0 8px;
+  font-size: 18px;
+  color: #606266;
+}
+.role-label {
+  font-size: 12px;
+  color: #909399;
+  white-space: nowrap;
+  padding: 0 4px;
+  line-height: 32px;
+}
+.content-text {
+  word-break: break-word;
+}
+.more-text {
+  color: #1976d2;
+  cursor: pointer;
+  font-size: 13px;
+  margin-left: 4px;
+  white-space: nowrap;
+}
+.more-text:hover {
+  text-decoration: underline;
+}
+.kb-tag {
+  margin-top: 6px;
+  display: inline-block;
+}
+.hangup-cause-cell {
+  cursor: pointer;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 100%;
+  display: inline-block;
+}
+</style>

+ 709 - 0
src/views/company/aiModel/inboundCallManage/index.vue

@@ -0,0 +1,709 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索表单 -->
+    <el-form
+      v-show="showSearch"
+      ref="queryForm"
+      :inline="true"
+      :model="queryParams"
+      label-width="100px"
+    >
+      <el-form-item label="大模型底座" prop="llmAccountId">
+        <el-select
+          v-model="queryParams.llmAccountId"
+          clearable
+          placeholder="全部"
+          size="small"
+          style="width: 200px"
+        >
+          <el-option label="全部" value="" />
+          <el-option
+            v-for="item in llmAccountList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="被叫号码" prop="callee">
+        <el-input
+          v-model="queryParams.callee"
+          clearable
+          placeholder="请输入被叫号码"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          v-hasPermi="['inboundCallManage:add']"
+          icon="el-icon-plus"
+          plain
+          size="mini"
+          type="success"
+          @click="handleAdd"
+        >新增</el-button>
+      </el-col>
+      <!-- <el-col :span="1.5">
+        <el-button
+          icon="el-icon-edit"
+          plain
+          size="mini"
+          type="primary"
+          :disabled="single"
+          @click="handleEdit()"
+        >修改</el-button>
+      </el-col> -->
+      <!-- <el-col :span="1.5">
+        <el-button
+          icon="el-icon-delete"
+          plain
+          size="mini"
+          type="danger"
+          :disabled="multiple"
+          @click="handleDelete()"
+        >删除</el-button>
+      </el-col> -->
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-table
+      v-loading="loading"
+      :data="inboundList"
+      border
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column align="center" label="名称" prop="inboundAlias" />
+      <el-table-column align="center" label="大模型底座" prop="llmAccountName" />
+      <el-table-column align="center" label="音色" prop="voiceName" />
+      <el-table-column align="center" label="被叫号码" prop="callee" />
+      <el-table-column align="center" label="服务类型" prop="serviceType" width="200">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.serviceType === 'ai'" type="primary">AI</el-tag>
+          <el-tag v-else-if="scope.row.serviceType === 'acd'" type="success">技能组</el-tag>
+          <el-tag v-else-if="scope.row.serviceType === 'ivr'" type="info">IVR</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        class-name="small-padding fixed-width"
+        label="操作"
+        width="180"
+      >
+        <template slot-scope="scope">
+          <el-button
+            v-hasPermi="['inboundCallManage:edit']"
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleEdit(scope.row)"
+          >修改</el-button>
+          <el-button
+          v-hasPermi="['inboundCallManage:delete']"
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="total > 0"
+      :limit.sync="queryParams.pageSize"
+      :page.sync="queryParams.pageNum"
+      :total="total"
+      @pagination="getList"
+    />
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog
+      :title="dialogTitle"
+      :visible.sync="dialogVisible"
+      width="650px"
+      append-to-body
+      @close="handleDialogClose"
+    >
+      <el-form
+        ref="form"
+        :model="form"
+        :rules="rules"
+        label-width="120px"
+      >
+        <!-- 名称 -->
+        <el-form-item label="名称" prop="inboundAlias">
+          <el-input
+            v-model="form.inboundAlias"
+            placeholder="请输入名称"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 被叫号码 -->
+        <el-form-item label="被叫号码" prop="callee">
+          <el-input
+            v-model="form.callee"
+            placeholder="请输入被叫号码"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 服务类型 -->
+        <el-form-item label="服务类型" prop="serviceType">
+          <el-select
+            v-model="form.serviceType"
+            placeholder="请选择服务类型"
+            style="width: 400px;"
+            @change="handleServiceTypeChange"
+          >
+            <el-option label="AI" value="ai" />
+            <el-option label="技能组" value="acd" />
+            <el-option label="IVR" value="ivr" />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 大模型底座 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="大模型底座" prop="llmAccountId">
+          <el-select
+            v-model="form.llmAccountId"
+            placeholder="请选择大模型底座"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in llmAccountList"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- ASR提供商 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="ASR提供商" prop="asrProvider">
+          <el-select
+            v-model="form.asrProvider"
+            placeholder="请选择ASR提供商"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="(value, key) in asrProviderList"
+              :key="key"
+              :label="value"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 音色来源 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="音色来源" prop="voiceSource">
+          <el-select
+            v-model="form.voiceSource"
+            placeholder="请选择音色来源"
+            style="width: 400px;"
+            @change="handleVoiceSourceChange"
+          >
+            <el-option
+              v-for="(value, key) in voiceSourceList"
+              :key="key"
+              :label="value"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 音色 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="音色" prop="voiceCode">
+          <el-select
+            v-model="form.voiceCode"
+            placeholder="请选择音色"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in voiceList"
+              :key="item.voiceCode"
+              :label="item.voiceName"
+              :value="item.voiceCode"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- AI转接类型 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="AI转接类型" prop="aiTransferType">
+          <el-select
+            v-model="form.aiTransferType"
+            placeholder="请选择AI转接类型"
+            style="width: 400px;"
+            @change="handleAiTransferTypeChange"
+          >
+            <el-option label="技能组" value="acd" />
+            <el-option label="分机号" value="extension" />
+            <el-option label="网关" value="gateway" />
+          </el-select>
+        </el-form-item>
+        
+        <!-- IVR下拉 (IVR可见) -->
+        <el-form-item v-if="form.serviceType === 'ivr'" label="IVR" prop="ivrId">
+          <el-select
+            v-model="form.ivrId"
+            placeholder="请选择IVR"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in ivrList"
+              :key="item.id"
+              :label="item.ivrNodeName"
+              :value="String(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 业务组 (ACD/AI+ACD可见) -->
+        <el-form-item v-if="showBizGroup" label="业务组" prop="aiTransferGroupId">
+          <el-select
+            v-model="form.aiTransferGroupId"
+            placeholder="请选择业务组"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in bizGroupList"
+              :key="item.groupId"
+              :label="item.bizGroupName"
+              :value="String(item.groupId)"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 网关 (AI+Gateway可见) -->
+        <el-form-item v-if="showGateway" label="网关" prop="aiTransferGatewayId">
+          <el-select
+            v-model="form.aiTransferGatewayId"
+            placeholder="请选择网关"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in gatewayList"
+              :key="item.id"
+              :label="item.gwDesc"
+              :value="String(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 网关目标号码 (AI+Gateway可见) -->
+        <el-form-item v-if="showGateway" label="网关目标号码" prop="aiTransferGatewayDestNumber">
+          <el-input
+            v-model="form.aiTransferGatewayDestNumber"
+            placeholder="请输入网关目标号码"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 分机号 (AI+Extension可见) -->
+        <el-form-item v-if="showExtension" label="分机号" prop="aiTransferExtNumber">
+          <el-input
+            v-model="form.aiTransferExtNumber"
+            placeholder="请输入分机号"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 服务评价 -->
+        <el-form-item label="服务评价" prop="satisfSurveyIvrId">
+          <el-select
+            v-model="form.satisfSurveyIvrId"
+            placeholder="请选择服务评价IVR"
+            clearable
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in ivrList"
+              :key="item.id"
+              :label="item.ivrNodeName"
+              :value="String(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="呼入场景" prop="fsSceneType">
+          <el-select
+            v-model="form.fsSceneType"
+            placeholder="请选择呼入场景"
+            clearable
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in sceneList"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="parseInt(item.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="线路回调地址" prop="callBackUrl">
+         <el-input
+            v-model="form.callBackUrl"
+            placeholder="请输入线路回调地址"
+            style="width: 400px;"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listInboundLlm,
+  addInboundLlm,
+  updateInboundLlm,
+  delInboundLlm,
+  listLlmAccount,
+  listAsrProvider,
+  listVoiceSource,
+  listVoiceBySource,
+  listBizGroup,
+  listGateway,
+  listIvr
+} from '@/api/company/inboundCallManage'
+
+export default {
+  name: 'InboundCallManage',
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 表格数据
+      inboundList: [],
+      // 选中的数据
+      selectedRows: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        llmAccountId: undefined,
+        callee: undefined
+      },
+      // 大模型账户列表
+      llmAccountList: [],
+      // ASR提供商列表
+      asrProviderList: {},
+      // 音色来源列表
+      voiceSourceList: {},
+      // 音色列表
+      voiceList: [],
+      // 业务组列表
+      bizGroupList: [],
+      // 网关列表
+      gatewayList: [],
+      // IVR列表
+      ivrList: [],
+      // 弹窗控制
+      dialogVisible: false,
+      dialogTitle: '新增呼入大模型配置',
+      // 表单数据
+      form: {
+        id: undefined,
+        inboundAlias: undefined,
+        callee: undefined,
+        serviceType: 'ai',
+        llmAccountId: undefined,
+        asrProvider: undefined,
+        voiceSource: undefined,
+        voiceCode: undefined,
+        aiTransferType: 'acd',
+        aiTransferGroupId: undefined,
+        aiTransferGatewayId: undefined,
+        aiTransferGatewayDestNumber: undefined,
+        aiTransferExtNumber: undefined,
+        ivrId: undefined,
+        satisfSurveyIvrId: undefined,
+        fsSceneType:null,
+        callBackUrl:null
+      },
+      // 表单校验规则
+      rules: {
+        inboundAlias: [
+          { required: true, message: '请输入名称', trigger: 'blur' }
+        ],
+        callee: [
+          { required: true, message: '请输入被叫号码', trigger: 'blur' }
+        ],
+        serviceType: [
+          { required: true, message: '请选择服务类型', trigger: 'change' }
+        ],
+        fsSceneType: [
+          { required: true, message: '请选择呼入场景', trigger: 'change' }
+        ],
+        callBackUrl: [
+          { required: true, message: '请输入线路回调地址', trigger: 'blur' }
+        ],
+      },
+      //场景下拉
+      sceneList:[]
+    }
+  },
+  computed: {
+    // 是否显示业务组选择
+    showBizGroup() {
+      return this.form.serviceType === 'acd' ||
+             (this.form.serviceType === 'ai' && this.form.aiTransferType === 'acd')
+    },
+    // 是否显示网关选择
+    showGateway() {
+      return this.form.serviceType === 'ai' && this.form.aiTransferType === 'gateway'
+    },
+    // 是否显示分机号输入
+    showExtension() {
+      return this.form.serviceType === 'ai' && this.form.aiTransferType === 'extension'
+    }
+  },
+  created() {
+    this.getList()
+    this.loadDropdownData()
+    this.getDicts("task_scene_type").then((response) => {
+        this.sceneList = response.data;
+        console.log(this.sceneList);
+    });
+  },
+  methods: {
+    /** 加载下拉数据 */
+    loadDropdownData() {
+      // 大模型账户
+      listLlmAccount().then(response => {
+        this.llmAccountList = response.data || []
+      })
+      // ASR提供商
+      listAsrProvider().then(response => {
+        this.asrProviderList = response.data || {}
+      })
+      // 音色来源
+      listVoiceSource().then(response => {
+        this.voiceSourceList = response.data || {}
+      })
+      // 业务组
+      listBizGroup().then(response => {
+        this.bizGroupList = response.data || []
+      })
+      // 网关
+      listGateway().then(response => {
+        this.gatewayList = response.data || []
+      })
+      // IVR
+      listIvr().then(response => {
+        this.ivrList = response.data || []
+      })
+    },
+    /** 查询列表 */
+    getList() {
+      this.loading = true
+      listInboundLlm(this.queryParams).then(response => {
+        this.inboundList = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    /** 服务类型改变 */
+    handleServiceTypeChange() {
+      // 清空关联字段
+      this.form.llmAccountId = undefined
+      this.form.asrProvider = undefined
+      this.form.voiceSource = undefined
+      this.form.voiceCode = undefined
+      this.form.aiTransferType = 'acd'
+      this.form.ivrId = undefined
+      this.voiceList = []
+    },
+    /** 音色来源改变 */
+    handleVoiceSourceChange(val) {
+      this.form.voiceCode = undefined
+      if (val) {
+        listVoiceBySource(val).then(response => {
+          this.voiceList = response.data || []
+        })
+      } else {
+        this.voiceList = []
+      }
+    },
+    /** AI转接类型改变 */
+    handleAiTransferTypeChange() {
+      this.form.aiTransferGroupId = undefined
+      this.form.aiTransferGatewayId = undefined
+      this.form.aiTransferGatewayDestNumber = undefined
+      this.form.aiTransferExtNumber = undefined
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    /** 表格多选选中事件 */
+    handleSelectionChange(selection) {
+      this.selectedRows = selection
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset()
+      this.dialogTitle = '新增呼入大模型配置'
+      this.dialogVisible = true
+    },
+    /** 修改按钮操作 */
+    handleEdit(row) {
+      this.reset()
+      let editRow = row
+      if (!editRow) {
+        if (this.selectedRows.length !== 1) {
+          this.$message.warning('请选择一条要修改的数据')
+          return
+        }
+        editRow = this.selectedRows[0]
+      }
+      this.dialogTitle = '修改呼入大模型配置'
+      this.form = { ...editRow }
+      // 如果有音色来源,加载音色列表
+      if (this.form.voiceSource) {
+        listVoiceBySource(this.form.voiceSource).then(response => {
+          this.voiceList = response.data || []
+        })
+      }
+      this.dialogVisible = true
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      let ids = []
+      if (row) {
+        ids = [row.id]
+      } else {
+        if (this.selectedRows.length === 0) {
+          this.$message.warning('请至少选择一条要删除的数据')
+          return
+        }
+        ids = this.selectedRows.map(item => item.id)
+      }
+      this.$confirm(`此操作将永久删除${ids.length > 1 ? '这些' : '该'}记录,是否继续?`, '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        delInboundLlm(ids.join(',')).then(response => {
+          if (response.code === 200) {
+            this.$message.success('删除成功')
+            this.getList()
+          } else {
+            this.$message.error(response.msg || '删除失败')
+          }
+        })
+      }).catch(() => {})
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        inboundAlias: undefined,
+        callee: undefined,
+        serviceType: 'ai',
+        llmAccountId: undefined,
+        asrProvider: undefined,
+        voiceSource: undefined,
+        voiceCode: undefined,
+        aiTransferType: 'acd',
+        aiTransferGroupId: undefined,
+        aiTransferGatewayId: undefined,
+        aiTransferGatewayDestNumber: undefined,
+        aiTransferExtNumber: undefined,
+        ivrId: undefined,
+        satisfSurveyIvrId: undefined
+      }
+      this.voiceList = []
+      this.resetForm('form')
+    },
+    /** 弹窗关闭时清理 */
+    handleDialogClose() {
+      this.reset()
+    },
+    /** 提交表单 */
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          // 处理转接数据
+          const submitData = { ...this.form }
+          if (submitData.serviceType === 'ai') {
+            if (submitData.aiTransferType === 'acd') {
+              submitData.aiTransferData = submitData.aiTransferGroupId
+            } else if (submitData.aiTransferType === 'extension') {
+              submitData.aiTransferData = submitData.aiTransferExtNumber
+            } else if (submitData.aiTransferType === 'gateway') {
+              submitData.aiTransferData = JSON.stringify({
+                gatewayId: submitData.aiTransferGatewayId,
+                destNumber: submitData.aiTransferGatewayDestNumber
+              })
+            }
+          } else if (submitData.serviceType === 'acd') {
+            submitData.aiTransferData = submitData.aiTransferGroupId
+          }
+          
+          if (submitData.id) {
+            updateInboundLlm(submitData).then(response => {
+              if (response.code === 200) {
+                this.$message.success('修改成功')
+                this.dialogVisible = false
+                this.getList()
+              } else {
+                this.$message.error(response.msg || '修改失败')
+              }
+            })
+          } else {
+            addInboundLlm(submitData).then(response => {
+              if (response.code === 200) {
+                this.$message.success('新增成功')
+                this.dialogVisible = false
+                this.getList()
+              } else {
+                this.$message.error(response.msg || '新增失败')
+              }
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+</style>

+ 4 - 1
src/views/company/companyConfig/index.vue

@@ -46,8 +46,11 @@
              <el-form-item label="是否允许重复客户导入" prop="allowRepeatCustomer">
               <el-switch v-model="cidConfig.allowRepeatCustomer"></el-switch>
             </el-form-item>
-            <el-form-item label="回调地址" prop="callbackUrl">
+            <el-form-item label="Ai外呼回调地址" prop="callbackUrl">
                <el-input v-model="cidConfig.callbackUrl" style="width:800px"></el-input>
+            </el-form-item>
+             <el-form-item label="线路呼入回调地址" prop="inboundCallbackUrl">
+               <el-input v-model="cidConfig.inboundCallbackUrl" style="width:800px"></el-input>
             </el-form-item>
             <div class="line"></div>
             <div style="float:right;margin-right:20px">