Browse Source

Merge branch 'saas-ui' of http://1.14.104.71:10880/txl/ylrz_saas_his_scrm_companyUI into saas-ui

lmx 3 days ago
parent
commit
054e0edafa

+ 8 - 0
src/api/company/companyVoiceRoboticCallBlacklist.js

@@ -51,3 +51,11 @@ export function changeVoiceRoboticCallBlacklistStatus(data) {
         data: data
     })
 }
+
+// 查看解密手机号
+export function queryVoiceRoboticCallBlacklistPhone(blacklistId) {
+    return request({
+        url: '/company/companyVoiceRoboticCallBlacklist/queryPhone/' + blacklistId,
+        method: 'get'
+    })
+}

+ 70 - 1
src/api/company/tagBinding.js

@@ -10,6 +10,13 @@ export const tagBindingApi = {
       params
     })
   },
+    getListB: (params) => {
+    return request({
+      url: '/workflow/tagBinding/tag-binding/list',
+      method: 'get',
+      params
+    })
+  },
 
   // 根据ID获取
   getById: (id) => {
@@ -27,6 +34,14 @@ export const tagBindingApi = {
       data
     })
   },
+    // 创建绑定
+  createB: (data) => {
+    return request({
+      url: '/workflow/tagBinding/tag-binding',
+      method: 'post',
+      data
+    })
+  },
 
   // 更新绑定
   update: (id, data) => {
@@ -36,6 +51,14 @@ export const tagBindingApi = {
       data
     })
   },
+    // 更新绑定
+  updateB: (id, data) => {
+    return request({
+      url: `/workflow/tagBinding/tag-binding/${id}`,
+      method: 'put',
+      data
+    })
+  },
 
   // 删除绑定
   delete: (id) => {
@@ -44,6 +67,13 @@ export const tagBindingApi = {
       method: 'delete'
     })
   },
+    // 删除绑定
+  deleteB: (id) => {
+    return request({
+      url: `/workflow/tagBinding/tag-binding/${id}`,
+      method: 'delete'
+    })
+  },
 
   // 批量绑定
   batchBind: (templateId, tagCodes) => {
@@ -70,5 +100,44 @@ export const tagBindingApi = {
       method: 'post',
       data: testTags
     })
-  }
+  },
+    // 测试标签匹配
+  testMatchB: (id, testTags) => {
+    return request({
+      url: `/workflow/tagBinding/tag-binding/${id}/test-match`,
+      method: 'post',
+      data: testTags
+    })
+  },
+    //获取龙虾标签
+   getLobsterTags:(userIds) => {
+  return request({
+    url: '/workflow/tagBinding/lobsterTags',
+    method: 'post',
+    data: userIds
+  })
+},
+    getListByStatus: (params) => {
+    return request({
+      url: '/workflow/tagBinding/tag-binding/listByStatus',
+      method: 'get',
+      params
+    })
+  },
+    // 批量添加龙虾标签给客户
+  batchBindLobsterTag: (data) => {
+    return request({
+      url: '/workflow/tagBinding/tag-binding/batch-bind-lobster-tag',
+      method: 'post',
+      data
+    })
+  },
+    // 批量取消龙虾标签
+  batchUnbindLobsterTag: (data) => {
+    return request({
+      url: '/workflow/tagBinding/tag-binding/batch-unbind-lobster-tag',
+      method: 'post',
+      data
+    })
+  },
 }

+ 14 - 0
src/api/company/workflowLobster.js

@@ -21,6 +21,13 @@ export function listWorkflowTemplate(query) {
     params: query
   })
 }
+export function listWorkflowTemplateByStatus(query) {
+  return request({
+    url: '/workflow/template/listTemplate',
+    method: 'get',
+    params: query
+  })
+}
 
 export function getWorkflowTemplateDetail(id) {
   return request({
@@ -88,3 +95,10 @@ export function simulateWorkflow(data) {
     data
   })
 }
+
+export function updateWorkflowTemplateStatus(id, status) {
+  return request({
+    url: '/workflow/template/' + id + '/' + status,
+    method: 'put'
+  })
+}

+ 57 - 0
src/api/workflow/lobster-e2e.js

@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+export function runE2e(data) {
+  return request({ url: '/workflow/lobster/e2e/run', method: 'post', data })
+}
+
+export function getE2eReport(runId) {
+  return request({ url: `/workflow/lobster/e2e/report/${runId}`, method: 'get' })
+}
+
+export function listE2eRuns(params) {
+  return request({ url: '/workflow/lobster/e2e/list', method: 'get', params })
+}
+
+export function stepNext(instanceId, data) {
+  return request({ url: `/workflow/lobster-exec/step-next/${instanceId}`, method: 'post', data })
+}
+
+export function multiTurn(data) {
+  return request({ url: '/workflow/lobster/chat/multi-turn', method: 'post', data })
+}
+
+export function listScenarios(params) {
+  return request({ url: '/workflow/lobster/scenario/list', method: 'get', params })
+}
+
+export function getScenario(id) {
+  return request({ url: `/workflow/lobster/scenario/${id}`, method: 'get' })
+}
+
+export function saveScenario(data) {
+  return request({ url: '/workflow/lobster/scenario/save', method: 'post', data })
+}
+
+export function deleteScenario(id) {
+  return request({ url: `/workflow/lobster/scenario/${id}`, method: 'delete' })
+}
+
+export function runScenarioNow(id) {
+  return request({ url: `/workflow/lobster/scenario/${id}/run`, method: 'post' })
+}
+
+export function runAllScenarios() {
+  return request({ url: '/workflow/lobster/scenario/run-all', method: 'post' })
+}
+
+export function listDynamicImpls(status) {
+  return request({ url: '/workflow/lobster/dynamic-impl/list', method: 'get', params: { status } })
+}
+
+export function approveDynamicImpl(id) {
+  return request({ url: `/workflow/lobster/dynamic-impl/${id}/approve`, method: 'post' })
+}
+
+export function rejectDynamicImpl(id, reason) {
+  return request({ url: `/workflow/lobster/dynamic-impl/${id}/reject`, method: 'post', params: { reason } })
+}

+ 1 - 1
src/api/workflow/lobster.js

@@ -237,7 +237,7 @@ export function regenerateAiWorkflow(recordId, data) {
 
 // ======== 模拟对话 ========
 export function simulateChat(data) {
-  return request({ url: '/workflow/lobster-exec/simulate', method: 'post', data })
+  return request({ url: '/workflow/simulate', method: 'post', data })
 }
 
 // ======== 工作流模板获取 ========

+ 6 - 6
src/api/workflow/model-route.js

@@ -5,7 +5,7 @@ import request from '@/utils/request'
 // 获取当前租户的所有模型路由配置
 export function listModelRoutes() {
   return request({
-    url: '/workflow/lobster/model-route/list',
+    url: '/workflow/lobster/model-config/list',
     method: 'get'
   })
 }
@@ -13,7 +13,7 @@ export function listModelRoutes() {
 // 根据类型获取配置
 export function getModelRouteByType(configType) {
   return request({
-    url: '/workflow/lobster/model-route/' + configType,
+    url: '/workflow/lobster/model-config/' + configType,
     method: 'get'
   })
 }
@@ -21,7 +21,7 @@ export function getModelRouteByType(configType) {
 // 获取支持的配置类型
 export function getModelRouteTypes() {
   return request({
-    url: '/workflow/lobster/model-route/types',
+    url: '/workflow/lobster/model-config/types',
     method: 'get'
   })
 }
@@ -29,7 +29,7 @@ export function getModelRouteTypes() {
 // 新增配置
 export function addModelRoute(data) {
   return request({
-    url: '/workflow/lobster/model-route',
+    url: '/workflow/lobster/model-config',
     method: 'post',
     data
   })
@@ -38,7 +38,7 @@ export function addModelRoute(data) {
 // 更新配置
 export function updateModelRoute(id, data) {
   return request({
-    url: '/workflow/lobster/model-route/' + id,
+    url: '/workflow/lobster/model-config/' + id,
     method: 'put',
     data
   })
@@ -47,7 +47,7 @@ export function updateModelRoute(id, data) {
 // 删除配置
 export function deleteModelRoute(id) {
   return request({
-    url: '/workflow/lobster/model-route/' + id,
+    url: '/workflow/lobster/model-config/' + id,
     method: 'delete'
   })
 }

+ 52 - 10
src/router/index.js

@@ -594,19 +594,61 @@ export const constantRoutes = [
         ]
     },
 
-    // ======== 龙虾引擎 敏感词库 ========
+    // ======== 龙虾引擎 E2E / 场景 / 动态节点 ========
     {
-        path: '/lobster/sensitive-words',
+        path: '/lobster/test-scenario',
         component: Layout,
         hidden: true,
-        children: [
-            {
-                path: '',
-                component: () => import('@/views/lobster/sensitive-words/index'),
-                name: 'LobsterSensitiveWords',
-                meta: { title: '敏感词库', activeMenu: '/lobster/sensitive-words' }
-            }
-        ]
+        children: [{
+            path: '',
+            component: () => import('@/views/lobster/test-scenario/index'),
+            name: 'LobsterTestScenario',
+            meta: { title: '测试场景管理', activeMenu: '/lobster/test-scenario' }
+        }]
+    },
+    {
+        path: '/lobster/e2e-history',
+        component: Layout,
+        hidden: true,
+        children: [{
+            path: '',
+            component: () => import('@/views/lobster/e2e-history/index'),
+            name: 'LobsterE2eHistory',
+            meta: { title: 'E2E运行历史', activeMenu: '/lobster/e2e-history' }
+        }]
+    },
+    {
+        path: '/lobster/dynamic-impl',
+        component: Layout,
+        hidden: true,
+        children: [{
+            path: '',
+            component: () => import('@/views/lobster/dynamic-impl/index'),
+            name: 'LobsterDynamicImpl',
+            meta: { title: '动态节点审批', activeMenu: '/lobster/dynamic-impl' }
+        }]
+    },
+    {
+        path: '/lobster/learning-results',
+        component: Layout,
+        hidden: true,
+        children: [{
+            path: '',
+            component: () => import('@/views/lobster/learning-results/index'),
+            name: 'LobsterLearningResults',
+            meta: { title: '学习结果', activeMenu: '/lobster/learning-results' }
+        }]
+    },
+    {
+        path: '/lobster/dashboard',
+        component: Layout,
+        hidden: true,
+        children: [{
+            path: '',
+            component: () => import('@/views/lobster/dashboard/index'),
+            name: 'LobsterDashboard',
+            meta: { title: '引擎看板', activeMenu: '/lobster/dashboard' }
+        }]
     },
 
 ]

+ 156 - 40
src/views/company/companyVoiceRoboticCallBlacklist/companyVoiceRoboticCallBlacklist.vue

@@ -34,16 +34,17 @@
                 />
             </el-form-item>
 
-            <el-form-item label="业务类型" prop="businessType">
-                <el-input
-                    v-model="queryParams.businessType"
-                    placeholder="请输入业务类型"
-                    clearable
-                    size="small"
-                    style="width: 180px"
-                    @keyup.enter.native="handleQuery"
-                />
-            </el-form-item>
+<!--            <el-form-item label="黑名单级别" prop="businessType" label-width="90px">-->
+<!--                <el-select-->
+<!--                    v-model="queryParams.businessType"-->
+<!--                    placeholder="请选择黑名单级别"-->
+<!--                    clearable-->
+<!--                    size="small"-->
+<!--                    style="width: 160px"-->
+<!--                >-->
+<!--                    <el-option label="线路级" value="13" />-->
+<!--                </el-select>-->
+<!--            </el-form-item>-->
 
             <el-form-item label="状态" prop="status">
                 <el-select
@@ -165,18 +166,47 @@
 
 <!--            <el-table-column label="公司ID" align="center" prop="companyId" width="100" />-->
 
-            <el-table-column label="业务类型" align="center" prop="businessType" min-width="120" />
+<!--            <el-table-column label="黑名单级别" align="center" prop="businessType" width="110">-->
+<!--                <template slot-scope="scope">-->
+<!--                    <span v-if="scope.row.businessType === '11'">平台封禁</span>-->
+<!--                    <span v-else-if="scope.row.businessType === '12'">租户级</span>-->
+<!--                    <span v-else-if="scope.row.businessType === '13'">线路级</span>-->
+<!--                    <span v-else>{{ scope.row.businessType || '-' }}</span>-->
+<!--                </template>-->
+<!--            </el-table-column>-->
 
             <el-table-column label="对象类型" align="center" prop="targetType" width="120">
                 <template slot-scope="scope">
-                    <span v-if="scope.row.targetType === 1">手机号</span>
-                    <span v-else-if="scope.row.targetType === 2">客户ID</span>
-                    <span v-else-if="scope.row.targetType === 3">企微客户ID</span>
+                    <span v-if="Number(scope.row.targetType) === 1">手机号</span>
+                    <span v-else-if="Number(scope.row.targetType) === 2">客户ID</span>
+                    <span v-else-if="Number(scope.row.targetType) === 3">企微客户ID</span>
                     <span v-else>-</span>
                 </template>
             </el-table-column>
 
-            <el-table-column label="对象值" align="center" prop="targetValue" min-width="180" show-overflow-tooltip />
+            <el-table-column label="对象值" align="center" min-width="220">
+                <template slot-scope="scope">
+                    <div class="target-value-cell">
+                        <el-tooltip
+                            :content="scope.row.targetValue || '-'"
+                            placement="top"
+                            :disabled="!scope.row.targetValue"
+                        >
+                            <span class="target-value-text">{{ scope.row.targetValue || '-' }}</span>
+                        </el-tooltip>
+                        <el-button
+                            v-if="canQueryPhone(scope.row)"
+                            type="primary"
+                            icon="el-icon-zoom-in"
+                            size="mini"
+                            circle
+                            plain
+                            title="查看真实手机号"
+                            @click="handlePhone(scope.row)"
+                        />
+                    </div>
+                </template>
+            </el-table-column>
 
             <el-table-column label="拉黑原因" align="center" prop="reason" min-width="180" show-overflow-tooltip />
 
@@ -190,8 +220,8 @@
 
             <el-table-column label="状态" align="center" prop="status" width="100">
                 <template slot-scope="scope">
-                    <el-tag v-if="scope.row.status === 1" type="danger" size="mini">生效</el-tag>
-                    <el-tag v-else type="success" size="mini">失效</el-tag>
+                    <el-tag v-if="scope.row.status === 1" type="success" size="mini">生效</el-tag>
+                    <el-tag v-else type="danger" size="mini">失效</el-tag>
                 </template>
             </el-table-column>
 
@@ -259,19 +289,19 @@
         <!-- 新增/编辑弹窗 -->
         <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body>
             <el-form ref="form" :model="form" :rules="rules" label-width="95px">
-                <el-row>
+<!--                <el-row>-->
+<!--&lt;!&ndash;                    <el-col :span="12">&ndash;&gt;-->
+<!--&lt;!&ndash;                        <el-form-item label="公司ID" prop="companyId">&ndash;&gt;-->
+<!--&lt;!&ndash;                            <el-input v-model="form.companyId" placeholder="请输入公司ID" />&ndash;&gt;-->
+<!--&lt;!&ndash;                        </el-form-item>&ndash;&gt;-->
+<!--&lt;!&ndash;                    </el-col>&ndash;&gt;-->
+
 <!--                    <el-col :span="12">-->
-<!--                        <el-form-item label="公司ID" prop="companyId">-->
-<!--                            <el-input v-model="form.companyId" placeholder="请输入公司ID" />-->
+<!--                        <el-form-item label="黑名单级别">-->
+<!--                            <el-input value="线路级(13)" disabled />-->
 <!--                        </el-form-item>-->
 <!--                    </el-col>-->
-
-                    <el-col :span="12">
-                        <el-form-item label="业务类型" prop="businessType">
-                            <el-input v-model="form.businessType" placeholder="请输入业务类型" />
-                        </el-form-item>
-                    </el-col>
-                </el-row>
+<!--                </el-row>-->
 
                 <el-row>
                     <el-col :span="12">
@@ -297,9 +327,10 @@
                 <el-form-item label="对象值" prop="targetValue">
                     <el-input
                         v-model="form.targetValue"
-                        placeholder="请输入手机号 / 客户ID / 企微客户ID"
+                        :placeholder="form.id ? '不填写则不修改对象值' : '请输入手机号 / 客户ID / 企微客户ID'"
                         maxlength="128"
                         show-word-limit
+                        clearable
                     />
                 </el-form-item>
 
@@ -348,7 +379,8 @@ import {
     addVoiceRoboticCallBlacklist,
     updateVoiceRoboticCallBlacklist,
     delVoiceRoboticCallBlacklist,
-    changeVoiceRoboticCallBlacklistStatus
+    changeVoiceRoboticCallBlacklistStatus,
+    queryVoiceRoboticCallBlacklistPhone
 } from "@/api/company/companyVoiceRoboticCallBlacklist";
 
 export default {
@@ -369,7 +401,7 @@ export default {
                 pageNum: 1,
                 pageSize: 10,
                 companyId: null,
-                businessType: null,
+                businessType: "13",
                 targetType: null,
                 targetValue: null,
                 status: null,
@@ -377,14 +409,22 @@ export default {
             },
             form: {},
             rules: {
-                companyId: [
-                    { required: true, message: "公司ID不能为空", trigger: "blur" }
-                ],
                 targetType: [
                     { required: true, message: "对象类型不能为空", trigger: "change" }
                 ],
                 targetValue: [
-                    { required: true, message: "对象值不能为空", trigger: "blur" }
+                    {
+                        validator: (rule, value, callback) => {
+                            const isEdit = this.isEditForm();
+                            const text = value == null ? "" : String(value).trim();
+                            if (!isEdit && !text) {
+                                callback(new Error("对象值不能为空"));
+                            } else {
+                                callback();
+                            }
+                        },
+                        trigger: "blur"
+                    }
                 ]
             }
         };
@@ -393,6 +433,15 @@ export default {
         this.getList();
     },
     methods: {
+        isEditForm() {
+            return this.form.id !== undefined && this.form.id !== null && this.form.id !== "";
+        },
+        normalizeTargetValue(value) {
+            if (value == null) {
+                return "";
+            }
+            return String(value).trim();
+        },
         /** 查询列表 */
         getList() {
             this.loading = true;
@@ -410,7 +459,7 @@ export default {
             this.form = {
                 id: null,
                 companyId: null,
-                businessType: null,
+                businessType: "13",
                 targetType: 1,
                 targetValue: null,
                 reason: null,
@@ -430,6 +479,7 @@ export default {
         /** 重置按钮 */
         resetQuery() {
             this.resetForm("queryForm");
+            this.queryParams.businessType = "13";
             this.handleQuery();
         },
 
@@ -451,9 +501,24 @@ export default {
         /** 修改 */
         handleUpdate(row) {
             this.reset();
-            const id = row.id || this.ids[0];
+            const id = row && row.id != null ? row.id : this.ids[0];
+            if (!id) {
+                this.msgWarning("请选择要修改的数据");
+                return;
+            }
             getVoiceRoboticCallBlacklist(id).then(response => {
-                this.form = response.data;
+                const data = response.data || {};
+                this.form = {
+                    id: data.id,
+                    companyId: data.companyId,
+                    businessType: data.businessType || "13",
+                    targetType: data.targetType != null ? Number(data.targetType) : 1,
+                    targetValue: null,
+                    reason: data.reason,
+                    source: data.source,
+                    status: data.status != null ? data.status : 1,
+                    remark: data.remark
+                };
                 this.open = true;
                 this.title = "修改外呼黑名单";
             });
@@ -466,8 +531,15 @@ export default {
                     return;
                 }
 
-                if (this.form.id != null) {
-                    updateVoiceRoboticCallBlacklist(this.form).then(response => {
+                const payload = { ...this.form };
+                const targetValue = this.normalizeTargetValue(payload.targetValue);
+                if (this.isEditForm()) {
+                    if (targetValue) {
+                        payload.targetValue = targetValue;
+                    } else {
+                        delete payload.targetValue;
+                    }
+                    updateVoiceRoboticCallBlacklist(payload).then(response => {
                         if (response.code === 200) {
                             this.msgSuccess("修改成功");
                             this.open = false;
@@ -477,7 +549,13 @@ export default {
                         }
                     });
                 } else {
-                    addVoiceRoboticCallBlacklist(this.form).then(response => {
+                    if (!targetValue) {
+                        this.msgError("对象值不能为空");
+                        return;
+                    }
+                    payload.businessType = "13";
+                    payload.targetValue = targetValue;
+                    addVoiceRoboticCallBlacklist(payload).then(response => {
                         if (response.code === 200) {
                             this.msgSuccess("新增成功");
                             this.open = false;
@@ -558,6 +636,29 @@ export default {
             this.batchChangeStatus(0);
         },
 
+        /** 是否可查看手机号(非客户ID/企微客户ID 且存在记录 ID) */
+        canQueryPhone(row) {
+            if (!row || row.id == null || row.id === '') {
+                return false;
+            }
+            const type = Number(row.targetType);
+            return type !== 2 && type !== 3;
+        },
+        /** 查看真实手机号 */
+        handlePhone(row) {
+            if (!row || !row.id) {
+                return;
+            }
+            queryVoiceRoboticCallBlacklistPhone(row.id).then(res => {
+                const mobile = (res && res.mobile) || (res && res.data && res.data.mobile);
+                if (mobile) {
+                    this.$alert(mobile, "手机号", { confirmButtonText: "确定" });
+                } else {
+                    this.msgError((res && res.msg) || "获取手机号失败");
+                }
+            });
+        },
+
         /** 批量改状态 */
         batchChangeStatus(status) {
             const text = status === 1 ? "启用" : "失效";
@@ -584,4 +685,19 @@ export default {
 .app-container {
     height: 87.5vh;
 }
+.target-value-cell {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    max-width: 100%;
+    gap: 6px;
+}
+.target-value-text {
+    display: inline-block;
+    max-width: 150px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    vertical-align: middle;
+}
 </style>

+ 29 - 10
src/views/company/tag/binding.vue

@@ -122,6 +122,7 @@
 
 <script>
 import { tagBindingApi } from '@/api/company/tagBinding'
+import { listWorkflowTemplateByStatus } from '@/api/company/workflowLobster'
 
 export default {
   name: 'TagBinding',
@@ -142,7 +143,8 @@ export default {
         tagName: '',
         templateId: null,
         priority: 0,
-        matchCondition: ''
+        matchCondition: '',
+        templateName: ''
       },
       rules: {
         tagCode: [{ required: true, message: '请输入标签编码', trigger: 'blur' }],
@@ -165,7 +167,7 @@ export default {
     async getList() {
       this.loading = true
       try {
-        const res = await tagBindingApi.getList(this.searchForm)
+        const res = await tagBindingApi.getListB(this.searchForm)
         this.tableData = res.data || []
       } catch (error) {
         this.$message.error('获取列表失败')
@@ -174,8 +176,13 @@ export default {
       }
     },
     async loadTemplateOptions() {
-      // TODO: 从工作流模板API加载模板列表
-      this.templateOptions = []
+      try {
+        const res = await listWorkflowTemplateByStatus({ status: 1 })
+        this.templateOptions = res.data || []
+      } catch (error) {
+        this.templateOptions = []
+        this.$message.error('获取模板列表失败')
+      }
     },
     handleSearch() {
       this.getList()
@@ -194,6 +201,7 @@ export default {
         tagCode: '',
         tagName: '',
         templateId: null,
+        templateName: '',
         priority: 0,
         matchCondition: ''
       }
@@ -209,6 +217,7 @@ export default {
         tagCode: row.tagCode,
         tagName: row.tagName,
         templateId: row.templateId,
+        templateName: row.templateName || '',
         priority: row.priority,
         matchCondition: row.matchCondition
       }
@@ -217,11 +226,13 @@ export default {
     async handleSubmit() {
       try {
         await this.$refs.formRef.validate()
+        const selectedTemplate = this.templateOptions.find(item => item.id === this.form.templateId)
+        this.form.templateName = selectedTemplate ? selectedTemplate.templateName : ''
         if (this.form.id) {
-          await tagBindingApi.update(this.form.id, this.form)
+          await tagBindingApi.updateB(this.form.id, this.form)
           this.$message.success('修改成功')
         } else {
-          await tagBindingApi.create(this.form)
+          await tagBindingApi.createB(this.form)
           this.$message.success('新增成功')
         }
         this.dialogVisible = false
@@ -239,7 +250,7 @@ export default {
           cancelButtonText: '取消',
           type: 'warning'
         })
-        await tagBindingApi.delete(row.id)
+        await tagBindingApi.deleteB(row.id)
         this.$message.success('删除成功')
         this.getList()
       } catch (error) {
@@ -249,11 +260,19 @@ export default {
       }
     },
     async handleStatusChange(row) {
+      const statusText = row.status === 1 ? '启用' : '禁用'
       try {
+        await this.$confirm(`确认${statusText}该绑定关系吗?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
         await tagBindingApi.update(row.id, { status: row.status })
-        this.$message.success('状态更新成功')
+        this.$message.success(`修改状态为${statusText}成功`)
       } catch (error) {
-        this.$message.error('状态更新失败')
+        if (error !== 'cancel') {
+          this.$message.error('状态更新失败')
+        }
         this.getList()
       }
     },
@@ -268,7 +287,7 @@ export default {
     async handleTestMatchSubmit() {
       try {
         const tags = this.testForm.tags.split(',').map(t => t.trim()).filter(t => t)
-        const res = await tagBindingApi.testMatch(this.testForm.id, { tags })
+        const res = await tagBindingApi.testMatchB(this.testForm.id, { tags })
         this.testResult = res.data
       } catch (error) {
         this.$message.error('测试失败')

+ 33 - 1
src/views/company/workflowLobster/index.vue

@@ -56,7 +56,13 @@
         <el-table-column prop="templateName" label="模板名称" min-width="180" />
         <el-table-column prop="templateCode" label="模板编码" min-width="140" />
         <el-table-column prop="industryType" label="行业类型" width="110" />
-        <el-table-column prop="status" label="状态" width="80" />
+        <el-table-column prop="status" label="状态" width="0" >
+          <template slot-scope="scope">
+            <el-tag size="mini" :type="scope.row.status === 1 ? 'success' : 'info'">
+              {{ scope.row.status === 1 ? '已发布' : '未发布' }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column prop="createTime" label="创建时间" width="160" />
         <el-table-column label="操作" width="280" fixed="right">
           <template slot-scope="scope">
@@ -64,6 +70,8 @@
             <el-button type="text" @click="handleEditTemplate(scope.row)">编辑</el-button>
             <el-button type="text" @click="handleVisual(scope.row)">流程图</el-button>
             <el-button type="text" @click="handleSimulate(scope.row)">模拟</el-button>
+            <el-button v-if="scope.row.status !== 1" type="text" :loading="statusChangingId === scope.row.id" @click="handlePublish(scope.row)">发布</el-button>
+            <el-button v-else type="text" :loading="statusChangingId === scope.row.id" @click="handleUnpublish(scope.row)">取消发布</el-button>
             <el-button type="text" style="color:#f56c6c" @click="handleDeleteTemplate(scope.row)">删除</el-button>
           </template>
         </el-table-column>
@@ -150,6 +158,7 @@ import {
   listWorkflowTemplate,
   getWorkflowTemplateDetail,
   updateWorkflowTemplate,
+  updateWorkflowTemplateStatus,
   deleteWorkflowTemplate,
   aiGenerateWorkflow,
   getGenerateResultDetail,
@@ -174,6 +183,7 @@ export default {
       templateDialogVisible: false,
       templateDialogMode: 'preview',
       templateSaving: false,
+      statusChangingId: null,
       page: {
         current: 1,
         size: 10,
@@ -413,6 +423,28 @@ export default {
         }
       }
     },
+    async handlePublish(row) {
+      await this.changeTemplateStatus(row, 1)
+    },
+    async handleUnpublish(row) {
+      await this.changeTemplateStatus(row, 0)
+    },
+    async changeTemplateStatus(row, status) {
+      const actionText = status === 1 ? '发布' : '取消发布'
+      try {
+        await this.$confirm(`确认${actionText}该模板吗?`, '提示', { type: 'warning' })
+        this.statusChangingId = row.id
+        await updateWorkflowTemplateStatus(row.id, status)
+        this.$message.success(`${actionText}成功`)
+        await this.loadTemplateList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || `${actionText}失败`)
+        }
+      } finally {
+        this.statusChangingId = null
+      }
+    },
     handleSimulate(row) {
       this.simulateDialogVisible = true
       this.simulateCurrentTemplateId = row.id

+ 9 - 5
src/views/lobster/dedup-config/index.vue

@@ -63,6 +63,7 @@
 </template>
 
 <script>
+import request from '@/utils/request'
 export default {
   name: 'DedupConfig',
   data() {
@@ -76,12 +77,15 @@ export default {
   },
   created() { this.getList() },
   methods: {
-    getList() { this.loading = true; this.loading = false },
-    handleAdd() { this.isAdd = true; this.dialogTitle = '新增去重配置'; this.form = { configName: '', dedupMode: 'hybrid', exactWindowSize: 5, semanticThreshold: 0.85, windowDurationSeconds: 300, ignorePrefixCount: 0, remark: '' }; this.dialogVisible = true },
+    async getList() {
+      this.loading = true
+      try { const res = await request({ url: '/workflow/lobster/dedup-config/list', method: 'get', params: this.queryParams }); this.list = (res.data || []); this.total = this.list.length } catch (e) { this.list = [] } finally { this.loading = false }
+    },
+    handleAdd() { this.isAdd = true; this.dialogTitle = '新增去重配置'; this.form = { configName: '', dedupMode: 'hybrid', exactWindowSize: 5, semanticThreshold: 0.85, windowDurationSeconds: 300, ignorePrefixCount: 0, remark: '', enabled: 1 }; this.dialogVisible = true },
     handleEdit(row) { this.isAdd = false; this.dialogTitle = '修改去重配置'; this.form = { ...row }; this.dialogVisible = true },
-    handleStatusChange(row) { this.$message.success('状态已更新') },
-    submitForm() { this.$refs.formRef.validate(v => { if (!v) return; this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() }) },
-    handleDelete(row) { this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => { this.$message.success('删除成功'); this.getList() }).catch(() => {}) }
+    async handleStatusChange(row) { try { await request({ url: '/workflow/lobster/dedup-config/save', method: 'post', data: row }); this.$message.success('状态已更新') } catch (e) { this.$message.error('更新失败') } },
+    async submitForm() { this.$refs.formRef.validate(async v => { if (!v) return; try { await request({ url: '/workflow/lobster/dedup-config/save', method: 'post', data: this.form }); this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() } catch (e) { this.$message.error('保存失败') } }) },
+    async handleDelete(row) { try { await this.$confirm('确认删除?', '警告', { type: 'warning' }); await request({ url: `/workflow/lobster/dedup-config/${row.id}`, method: 'delete' }); this.$message.success('删除成功'); this.getList() } catch (e) { if (e !== 'cancel') this.$message.error('删除失败') } }
   }
 }
 </script>

+ 98 - 0
src/views/lobster/dynamic-impl/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <el-tabs v-model="tab" @tab-click="load">
+        <el-tab-pane label="待审批" name="PENDING" />
+        <el-tab-pane label="已激活" name="ACTIVE" />
+        <el-tab-pane label="草稿" name="DRAFT" />
+        <el-tab-pane label="已拒绝" name="REJECTED" />
+      </el-tabs>
+
+      <el-table v-loading="loading" :data="list" border size="small">
+        <el-table-column label="ID" prop="id" width="70" />
+        <el-table-column label="节点类型" prop="nodeType" width="90" />
+        <el-table-column label="类型编码" prop="nodeTypeCode" width="140" />
+        <el-table-column label="指纹" prop="fingerprint" width="140" show-overflow-tooltip />
+        <el-table-column label="评分" prop="qualityScore" width="80">
+          <template slot-scope="s">
+            <el-tag :type="s.row.qualityScore>=80?'success':s.row.qualityScore>=60?'warning':'danger'" size="mini">
+              {{ (s.row.qualityScore||0).toFixed(1) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="调用/成功" width="100">
+          <template slot-scope="s">{{ s.row.execCount || 0 }} / {{ s.row.successCount || 0 }}</template>
+        </el-table-column>
+        <el-table-column label="平均耗时" prop="avgDurationMs" width="90" />
+        <el-table-column label="状态" prop="status" width="90">
+          <template slot-scope="s">
+            <el-tag :type="statusTag(s.row.status)" size="mini">{{ s.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template slot-scope="s">
+            <el-button v-if="s.row.status==='PENDING'" type="text" size="mini" style="color:#67C23A" @click="approve(s.row)">通过</el-button>
+            <el-button v-if="s.row.status==='PENDING'" type="text" size="mini" style="color:#F56C6C" @click="reject(s.row)">拒绝</el-button>
+            <el-button type="text" size="mini" @click="showDsl(s.row)">查看DSL</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog title="DSL 详情" :visible.sync="dslDlg" width="700px">
+      <pre style="max-height:500px;overflow:auto;background:#1e1e1e;color:#d4d4d4;padding:12px;border-radius:6px;font-size:12px">{{ dslContent }}</pre>
+    </el-dialog>
+
+    <el-dialog title="拒绝原因" :visible.sync="rejectDlg" width="400px">
+      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
+      <div slot="footer"><el-button @click="rejectDlg=false">取消</el-button><el-button type="danger" @click="doReject">确认拒绝</el-button></div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listDynamicImpls, approveDynamicImpl, rejectDynamicImpl } from '@/api/workflow/lobster-e2e'
+export default {
+  data() {
+    return {
+      tab: 'PENDING', loading: false, list: [],
+      dslDlg: false, dslContent: '',
+      rejectDlg: false, rejectRow: null, rejectReason: ''
+    }
+  },
+  created() { this.load() },
+  methods: {
+    statusTag(s) { return s === 'ACTIVE' ? 'success' : s === 'PENDING' ? 'warning' : s === 'REJECTED' ? 'danger' : 'info' },
+    async load() {
+      this.loading = true
+      try {
+        const res = await listDynamicImpls(this.tab)
+        const d = res.data !== undefined ? res.data : res
+        this.list = Array.isArray(d) ? d : []
+      } finally { this.loading = false }
+    },
+    showDsl(row) {
+      try {
+        this.dslContent = JSON.stringify(JSON.parse(row.subDslJson || row.sub_dsl_json || '{}'), null, 2)
+      } catch (e) {
+        this.dslContent = row.subDslJson || row.sub_dsl_json || '{}'
+      }
+      this.dslDlg = true
+    },
+    approve(row) {
+      this.$confirm('确认激活此节点实现?', '提示', { type: 'info' }).then(async () => {
+        await approveDynamicImpl(row.id)
+        this.$message.success('已激活')
+        this.load()
+      })
+    },
+    reject(row) { this.rejectRow = row; this.rejectReason = ''; this.rejectDlg = true },
+    async doReject() {
+      await rejectDynamicImpl(this.rejectRow.id, this.rejectReason)
+      this.rejectDlg = false
+      this.$message.success('已拒绝')
+      this.load()
+    }
+  }
+}
+</script>

+ 109 - 0
src/views/lobster/e2e-history/index.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <el-row :gutter="8" type="flex" justify="space-between" style="margin-bottom:12px">
+        <el-col :span="14">
+          <el-input v-model="query.runId" placeholder="runId" size="small" style="width:280px" clearable @keyup.enter.native="searchById" />
+          <el-button type="primary" size="small" icon="el-icon-search" @click="load" style="margin-left:8px">刷新</el-button>
+        </el-col>
+        <el-col :span="10" style="text-align:right">
+          <el-tag>成功 {{ stats.success }}</el-tag>
+          <el-tag type="danger" style="margin-left:6px">失败 {{ stats.failed }}</el-tag>
+          <el-tag type="warning" style="margin-left:6px">运行中 {{ stats.running }}</el-tag>
+        </el-col>
+      </el-row>
+
+      <el-table v-loading="loading" :data="list" border size="small" @row-click="showDetail">
+        <el-table-column label="runId" prop="runId" width="240" show-overflow-tooltip />
+        <el-table-column label="模板" prop="templateId" width="80" />
+        <el-table-column label="实例" prop="instanceId" width="80" />
+        <el-table-column label="业务描述" prop="businessDesc" show-overflow-tooltip />
+        <el-table-column label="总分" prop="totalScore" width="80">
+          <template slot-scope="s">
+            <el-tag v-if="s.row.totalScore != null" :type="s.row.totalScore >= 60 ? 'success' : 'danger'" size="mini">{{ Number(s.row.totalScore).toFixed(1) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="通过/总" width="100">
+          <template slot-scope="s">{{ s.row.passedNodeCnt || 0 }} / {{ s.row.totalNodeCnt || 0 }}</template>
+        </el-table-column>
+        <el-table-column label="耗时(ms)" prop="durationMs" width="100" />
+        <el-table-column label="进化建议" prop="evolutionCount" width="100" />
+        <el-table-column label="状态" prop="status" width="100">
+          <template slot-scope="s"><el-tag :type="statusType(s.row.status)" size="mini">{{ s.row.status }}</el-tag></template>
+        </el-table-column>
+        <el-table-column label="时间" prop="createTime" width="160" />
+      </el-table>
+    </el-card>
+
+    <el-drawer title="E2E 运行明细" :visible.sync="drawer" size="55%" direction="rtl">
+      <div v-if="detail" style="padding: 0 16px">
+        <el-descriptions :column="2" border size="small" style="margin-bottom:12px">
+          <el-descriptions-item label="runId">{{ detail.runId }}</el-descriptions-item>
+          <el-descriptions-item label="状态">{{ detail.status }}</el-descriptions-item>
+          <el-descriptions-item label="总分">{{ detail.totalScore }}</el-descriptions-item>
+          <el-descriptions-item label="通过/总">{{ detail.passedNodeCnt }} / {{ detail.totalNodeCnt }}</el-descriptions-item>
+          <el-descriptions-item label="耗时">{{ detail.durationMs }} ms</el-descriptions-item>
+          <el-descriptions-item label="进化建议">{{ detail.evolutionCount }}</el-descriptions-item>
+        </el-descriptions>
+        <el-table :data="detail.nodeTraces" border size="mini">
+          <el-table-column label="#" prop="nodeSeq" width="50" />
+          <el-table-column label="节点" prop="nodeCode" />
+          <el-table-column label="轮" prop="turnNo" width="50" />
+          <el-table-column label="用户" prop="userInput" show-overflow-tooltip />
+          <el-table-column label="AI" prop="aiOutput" show-overflow-tooltip />
+          <el-table-column label="分" prop="score" width="60">
+            <template slot-scope="s">
+              <el-tag v-if="s.row.score" :type="s.row.score >= 60 ? 'success' : 'danger'" size="mini">{{ Number(s.row.score).toFixed(0) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="耗时" prop="durationMs" width="80" />
+        </el-table>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { listE2eRuns, getE2eReport } from '@/api/workflow/lobster-e2e'
+export default {
+  data() {
+    return {
+      loading: false, list: [], query: { runId: '', pageNum: 1, pageSize: 50 },
+      stats: { success: 0, failed: 0, running: 0 },
+      drawer: false, detail: null
+    }
+  },
+  created() { this.load() },
+  methods: {
+    statusType(s) { return s === 'SUCCESS' ? 'success' : s === 'FAILED' ? 'danger' : 'warning' },
+    normalizeList(res) {
+      const d = res.data !== undefined ? res.data : res
+      return Array.isArray(d) ? d : (d && d.list) || []
+    },
+    async load() {
+      this.loading = true
+      try {
+        const res = await listE2eRuns(this.query)
+        this.list = this.normalizeList(res)
+        this.stats = this.list.reduce((acc, x) => {
+          if (x.status === 'SUCCESS') acc.success++
+          else if (x.status === 'FAILED') acc.failed++
+          else acc.running++
+          return acc
+        }, { success: 0, failed: 0, running: 0 })
+      } finally { this.loading = false }
+    },
+    async searchById() {
+      if (!this.query.runId) return this.load()
+      const res = await getE2eReport(this.query.runId)
+      const detail = res.data || res
+      if (detail) { this.detail = detail; this.drawer = true }
+    },
+    async showDetail(row) {
+      const res = await getE2eReport(row.runId)
+      this.detail = res.data || row
+      this.drawer = true
+    }
+  }
+}
+</script>

+ 2 - 3
src/views/lobster/instance/index.vue

@@ -196,9 +196,8 @@ export default {
     getList() {
       this.loading = true
       listInstances(this.queryParams).then(res => {
-        let data = res.data || {}
-        this.list = data.list || res.rows || []
-        // 计算统计
+        const data = res.data
+        this.list = Array.isArray(data) ? data : (data && data.list) || res.rows || []
         this.stats = { running: 0, paused: 0, pending_human: 0, completed: 0, terminated: 0 }
         this.list.forEach(item => {
           if (this.stats[item.status] !== undefined) this.stats[item.status]++

+ 29 - 3
src/views/lobster/learning-results/index.vue

@@ -14,11 +14,37 @@
 import { triggerLearningCycle, getLearningResults, applyLearningResult } from '@/api/workflow/lobster-dashboard'
 export default {
   data() { return { list:[], loading:false } },
+  computed: {
+    companyId() {
+      return this.$store.getters.companyId || (this.$store.state.user && this.$store.state.user.companyId) || 0
+    }
+  },
   created() { this.getList() },
   methods: {
-    async getList() { this.loading=true; try { const res = await getLearningResults(0); this.list = (res.data||res).results || [] } catch(e){} this.loading=false },
-    async trigger() { try { const res = await triggerLearningCycle(0); this.$message.success((res.data||res).summary||'已触发'); this.getList() } catch(e){} },
-    async apply(row) { try { await applyLearningResult(0, row.id); this.$message.success('已应用'); this.getList() } catch(e){} }
+    async getList() {
+      this.loading = true
+      try {
+        const res = await getLearningResults(this.companyId)
+        const data = res.data || res
+        this.list = data.results || []
+      } catch (e) { /* ignore */ }
+      this.loading = false
+    },
+    async trigger() {
+      try {
+        const res = await triggerLearningCycle(this.companyId)
+        const data = res.data || res
+        this.$message.success(data.summary || '已触发')
+        this.getList()
+      } catch (e) { /* ignore */ }
+    },
+    async apply(row) {
+      try {
+        await applyLearningResult(this.companyId, row.id)
+        this.$message.success('已应用')
+        this.getList()
+      } catch (e) { /* ignore */ }
+    }
   }
 }
 </script>

+ 6 - 5
src/views/lobster/profile-config/index.vue

@@ -65,6 +65,7 @@
 </template>
 
 <script>
+import request from '@/utils/request'
 export default {
   name: 'ProfileConfig',
   data() {
@@ -78,12 +79,12 @@ export default {
   },
   created() { this.getList() },
   methods: {
-    getList() { this.loading = true; this.loading = false },
-    handleAdd() { this.isAdd = true; this.dialogTitle = '新增画像配置'; this.form = { configName: '', profileSource: 'crm_tag', refreshStrategy: 'daily', maxFields: 10, fieldMappings: '', remark: '' }; this.dialogVisible = true },
+    async getList() { this.loading = true; try { const res = await request({ url: '/workflow/lobster/profile-config/list', method: 'get', params: this.queryParams }); this.list = (res.data || []); this.total = this.list.length } catch (e) { this.list = [] } finally { this.loading = false } },
+    handleAdd() { this.isAdd = true; this.dialogTitle = '新增画像配置'; this.form = { configName: '', profileSource: 'crm_tag', refreshStrategy: 'daily', maxFields: 10, fieldMappings: '', remark: '', enabled: 1 }; this.dialogVisible = true },
     handleEdit(row) { this.isAdd = false; this.dialogTitle = '修改画像配置'; this.form = { ...row }; this.dialogVisible = true },
-    handleStatusChange(row) { this.$message.success('状态已更新') },
-    submitForm() { this.$refs.formRef.validate(v => { if (!v) return; this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() }) },
-    handleDelete(row) { this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => { this.$message.success('删除成功'); this.getList() }).catch(() => {}) }
+    async handleStatusChange(row) { try { await request({ url: '/workflow/lobster/profile-config/save', method: 'post', data: row }); this.$message.success('状态已更新') } catch (e) { this.$message.error('更新失败') } },
+    async submitForm() { this.$refs.formRef.validate(async v => { if (!v) return; try { await request({ url: '/workflow/lobster/profile-config/save', method: 'post', data: this.form }); this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() } catch (e) { this.$message.error('保存失败') } }) },
+    async handleDelete(row) { try { await this.$confirm('确认删除?', '警告', { type: 'warning' }); await request({ url: `/workflow/lobster/profile-config/${row.id}`, method: 'delete' }); this.$message.success('删除成功'); this.getList() } catch (e) { if (e !== 'cancel') this.$message.error('删除失败') } }
   }
 }
 </script>

+ 6 - 5
src/views/lobster/sensitive-words/index.vue

@@ -72,6 +72,7 @@
 </template>
 
 <script>
+import request from '@/utils/request'
 export default {
   name: 'SensitiveWords',
   data() {
@@ -85,15 +86,15 @@ export default {
   },
   created() { this.getList() },
   methods: {
-    getList() { this.loading = true; this.loading = false },
+    async getList() { this.loading = true; try { const res = await request({ url: '/workflow/lobster/sensitive-words/list', method: 'get', params: this.queryParams }); this.list = (res.data || []); this.total = this.list.length } catch (e) { this.list = [] } finally { this.loading = false } },
     handleQuery() { this.queryParams.pageNum = 1; this.getList() },
     resetQuery() { this.$refs.queryForm.resetFields(); this.handleQuery() },
     handleSelectionChange(selection) { this.selectedIds = selection.map(s => s.id) },
-    handleAdd() { this.isAdd = true; this.dialogTitle = '新增敏感词'; this.form = { word: '', category: '', severity: 1, remark: '' }; this.dialogVisible = true },
+    handleAdd() { this.isAdd = true; this.dialogTitle = '新增敏感词'; this.form = { word: '', category: '', severity: 1, remark: '', enabled: 1 }; this.dialogVisible = true },
     handleEdit(row) { this.isAdd = false; this.dialogTitle = '修改敏感词'; this.form = { ...row }; this.dialogVisible = true },
-    submitForm() { this.$refs.formRef.validate(v => { if (!v) return; this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() }) },
-    handleDelete(row) { this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => { this.$message.success('删除成功'); this.getList() }).catch(() => {}) },
-    handleBatchDelete() { if (!this.selectedIds.length) return; this.$confirm('确认批量删除?', '警告', { type: 'warning' }).then(() => { this.$message.success('批量删除成功'); this.getList() }).catch(() => {}) }
+    async submitForm() { this.$refs.formRef.validate(async v => { if (!v) return; try { await request({ url: '/workflow/lobster/sensitive-words/save', method: 'post', data: this.form }); this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() } catch (e) { this.$message.error('保存失败') } }) },
+    async handleDelete(row) { try { await this.$confirm('确认删除?', '警告', { type: 'warning' }); await request({ url: `/workflow/lobster/sensitive-words/${row.id}`, method: 'delete' }); this.$message.success('删除成功'); this.getList() } catch (e) { if (e !== 'cancel') this.$message.error('删除失败') } },
+    async handleBatchDelete() { if (!this.selectedIds.length) return; try { await this.$confirm(`确认批量删除 ${this.selectedIds.length} 条?`, '警告', { type: 'warning' }); for (const id of this.selectedIds) { await request({ url: `/workflow/lobster/sensitive-words/${id}`, method: 'delete' }) }; this.$message.success('批量删除成功'); this.getList() } catch (e) { if (e !== 'cancel') this.$message.error('批量删除失败') } }
   }
 }
 </script>

+ 6 - 5
src/views/lobster/summary-config/index.vue

@@ -63,6 +63,7 @@
 </template>
 
 <script>
+import request from '@/utils/request'
 export default {
   name: 'SummaryConfig',
   data() {
@@ -76,12 +77,12 @@ export default {
   },
   created() { this.getList() },
   methods: {
-    getList() { this.loading = true; this.loading = false },
-    handleAdd() { this.isAdd = true; this.dialogTitle = '新增摘要配置'; this.form = { configName: '', triggerStrategy: 'message_count', triggerInterval: 20, modelIdentifier: '', maxContextMessages: 50, maxSummaryLength: 500, remark: '' }; this.dialogVisible = true },
+    async getList() { this.loading = true; try { const res = await request({ url: '/workflow/lobster/summary-config/list', method: 'get', params: this.queryParams }); this.list = (res.data || []); this.total = this.list.length } catch (e) { this.list = [] } finally { this.loading = false } },
+    handleAdd() { this.isAdd = true; this.dialogTitle = '新增摘要配置'; this.form = { configName: '', triggerStrategy: 'message_count', triggerInterval: 20, modelIdentifier: '', maxContextMessages: 50, maxSummaryLength: 500, remark: '', enabled: 1 }; this.dialogVisible = true },
     handleEdit(row) { this.isAdd = false; this.dialogTitle = '修改摘要配置'; this.form = { ...row }; this.dialogVisible = true },
-    handleStatusChange(row) { this.$message.success('状态已更新') },
-    submitForm() { this.$refs.formRef.validate(v => { if (!v) return; this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() }) },
-    handleDelete(row) { this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => { this.$message.success('删除成功'); this.getList() }).catch(() => {}) }
+    async handleStatusChange(row) { try { await request({ url: '/workflow/lobster/summary-config/save', method: 'post', data: row }); this.$message.success('状态已更新') } catch (e) { this.$message.error('更新失败') } },
+    async submitForm() { this.$refs.formRef.validate(async v => { if (!v) return; try { await request({ url: '/workflow/lobster/summary-config/save', method: 'post', data: this.form }); this.$message.success(this.isAdd ? '新增成功' : '修改成功'); this.dialogVisible = false; this.getList() } catch (e) { this.$message.error('保存失败') } }) },
+    async handleDelete(row) { try { await this.$confirm('确认删除?', '警告', { type: 'warning' }); await request({ url: `/workflow/lobster/summary-config/${row.id}`, method: 'delete' }); this.$message.success('删除成功'); this.getList() } catch (e) { if (e !== 'cancel') this.$message.error('删除失败') } }
   }
 }
 </script>

+ 129 - 0
src/views/lobster/test-scenario/index.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <el-row :gutter="8" type="flex" justify="space-between" style="margin-bottom:12px">
+        <el-col :span="14">
+          <el-input v-model="query.keyword" placeholder="场景名" size="small" style="width:240px" clearable @keyup.enter.native="load" />
+          <el-select v-model="query.enabled" placeholder="启用状态" size="small" clearable style="width:120px;margin-left:8px">
+            <el-option label="启用" :value="1" /><el-option label="停用" :value="0" />
+          </el-select>
+          <el-button type="primary" size="small" icon="el-icon-search" @click="load" style="margin-left:8px">查询</el-button>
+        </el-col>
+        <el-col :span="10" style="text-align:right">
+          <el-button type="success" size="small" icon="el-icon-video-play" @click="runAll">跑全部启用</el-button>
+          <el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">新增场景</el-button>
+        </el-col>
+      </el-row>
+
+      <el-table v-loading="loading" :data="filteredList" border size="small">
+        <el-table-column label="ID" prop="id" width="70" />
+        <el-table-column label="场景名" prop="scenario_name" />
+        <el-table-column label="模板ID" prop="template_id" width="100" />
+        <el-table-column label="业务描述" prop="business_desc" show-overflow-tooltip />
+        <el-table-column label="最低分" prop="min_score" width="80" />
+        <el-table-column label="启用" prop="enabled" width="80">
+          <template slot-scope="s"><el-tag :type="s.row.enabled === 1 ? 'success' : 'info'">{{ s.row.enabled === 1 ? '是' : '否' }}</el-tag></template>
+        </el-table-column>
+        <el-table-column label="最近运行" prop="last_run_status" width="100">
+          <template slot-scope="s"><el-tag v-if="s.row.last_run_status" :type="s.row.last_run_status === 'SUCCESS' ? 'success' : 'danger'" size="mini">{{ s.row.last_run_status }}</el-tag></template>
+        </el-table-column>
+        <el-table-column label="最近时间" prop="last_run_time" width="160" />
+        <el-table-column label="操作" width="240" fixed="right">
+          <template slot-scope="s">
+            <el-button type="text" size="mini" @click="runOne(s.row)">立即运行</el-button>
+            <el-button type="text" size="mini" @click="openEdit(s.row)">编辑</el-button>
+            <el-button type="text" size="mini" style="color:#F56C6C" @click="del(s.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination background layout="prev, pager, next, total" :total="total" :page-size="query.pageSize" :current-page.sync="query.pageNum" @current-change="load" style="margin-top:12px;text-align:right" />
+    </el-card>
+
+    <el-dialog :title="form.id ? '编辑场景' : '新增场景'" :visible.sync="dlg" width="640px">
+      <el-form :model="form" label-width="100px" size="small">
+        <el-form-item label="场景名" required><el-input v-model="form.scenarioName" /></el-form-item>
+        <el-form-item label="模板ID"><el-input-number v-model="form.templateId" :min="0" style="width:100%" /></el-form-item>
+        <el-form-item label="业务描述"><el-input type="textarea" v-model="form.businessDesc" :rows="2" /></el-form-item>
+        <el-form-item label="用户输入" required>
+          <el-input type="textarea" v-model="form.userInputsText" :rows="6" placeholder="每行一条用户消息" />
+        </el-form-item>
+        <el-form-item label="最低通过分"><el-input-number v-model="form.minScore" :min="0" :max="100" /></el-form-item>
+        <el-form-item label="启用"><el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" /></el-form-item>
+      </el-form>
+      <div slot="footer"><el-button @click="dlg=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listScenarios, saveScenario, deleteScenario, runScenarioNow, runAllScenarios } from '@/api/workflow/lobster-e2e'
+export default {
+  data() {
+    return {
+      loading: false, list: [], total: 0,
+      query: { pageNum: 1, pageSize: 20, keyword: '', enabled: null },
+      dlg: false,
+      form: { id: null, scenarioName: '', templateId: null, businessDesc: '', userInputsText: '', minScore: 60, enabled: 1 }
+    }
+  },
+  computed: {
+    companyId() {
+      return this.$store.getters.companyId || (this.$store.state.user && this.$store.state.user.companyId)
+    },
+    filteredList() {
+      if (!this.query.keyword) return this.list
+      const kw = this.query.keyword.toLowerCase()
+      return this.list.filter(r => (r.scenario_name || '').toLowerCase().includes(kw))
+    }
+  },
+  created() { this.load() },
+  methods: {
+    async load() {
+      this.loading = true
+      try {
+        const res = await listScenarios(this.query)
+        const d = res.data !== undefined ? res.data : res
+        this.list = Array.isArray(d) ? d : (d && d.list) || []
+        this.total = this.list.length
+      } finally { this.loading = false }
+    },
+    openAdd() { this.form = { id: null, scenarioName: '', templateId: null, businessDesc: '', userInputsText: '', minScore: 60, enabled: 1 }; this.dlg = true },
+    openEdit(row) {
+      let inputs = []
+      try { inputs = row.user_inputs_json ? JSON.parse(row.user_inputs_json) : [] } catch (e) {}
+      this.form = {
+        id: row.id, scenarioName: row.scenario_name, templateId: row.template_id,
+        businessDesc: row.business_desc, userInputsText: inputs.join('\n'),
+        minScore: row.min_score, enabled: row.enabled
+      }
+      this.dlg = true
+    },
+    async save() {
+      const userInputs = (this.form.userInputsText || '').split('\n').map(x => x.trim()).filter(x => x)
+      const body = { ...this.form, userInputs, companyId: this.companyId }
+      delete body.userInputsText
+      await saveScenario(body)
+      this.$message.success('已保存')
+      this.dlg = false; this.load()
+    },
+    async runOne(row) {
+      const res = await runScenarioNow(row.id)
+      const data = res.data || res
+      this.$message.success('已触发,runId=' + (data.runId || ''))
+      this.load()
+    },
+    async runAll() {
+      const res = await runAllScenarios()
+      const data = res.data || res
+      this.$message.success('已触发 ' + (data.triggered || 0) + ' 个场景')
+      this.load()
+    },
+    async del(row) {
+      await this.$confirm('确认删除该场景?', '提示', { type: 'warning' })
+      await deleteScenario(row.id)
+      this.$message.success('已删除'); this.load()
+    }
+  }
+}
+</script>

+ 155 - 1
src/views/qw/externalContact/index.vue

@@ -337,6 +337,24 @@
         v-hasPermi="['qw:externalContactInfo:updateTalk']"
 	    >批量更改交流状态</el-button>
 	  </el-col>
+    <el-col :span="1.5">
+      <el-button
+        type="warning"
+        plain
+        size="mini"
+        @click="addLobsterTag"
+        v-hasPermi="['qw:externalContact:addTag']"
+      >批量增加龙虾标签</el-button>
+    </el-col>
+    <el-col :span="1.5">
+      <el-button
+        type="warning"
+        plain
+        size="mini"
+        @click="removeLobsterTag"
+        v-hasPermi="['qw:externalContact:addTag']"
+      >批量取消龙虾标签</el-button>
+    </el-col>
 <!--       <el-col :span="1.5">-->
 <!--        <el-button-->
 <!--          type="primary"-->
@@ -401,6 +419,22 @@
           </div>
         </template>
       </el-table-column>
+      <el-table-column label="龙虾标签" align="center" width="200px">
+        <template slot-scope="scope">
+          <div v-if="scope.row.lobsterTagNames && scope.row.lobsterTagNames.length">
+            <el-tag
+              v-for="name in scope.row.lobsterTagNames"
+              :key="name"
+              type="warning"
+              size="small"
+              style="margin: 2px;"
+            >
+              {{ name }}
+            </el-tag>
+          </div>
+          <span v-else style="color: #c0c4cc;">-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="流失风险" align="center" prop="attritionLevel">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
@@ -744,6 +778,40 @@
         <el-button @click="addTagCancel">取 消</el-button>
       </div>
     </el-dialog>
+    <el-dialog title="批量增加龙虾标签" :visible.sync="lobsterTagOpen" width="700px" append-to-body>
+      <div v-loading="lobsterTagLoading">
+        <el-alert
+          title="龙虾标签来源于已启用的标签-模板绑定关系"
+          type="info"
+          :closable="false"
+          show-icon
+          style="margin-bottom: 16px"
+        />
+        <el-table
+          :data="lobsterTagList"
+          ref="lobsterTagTable"
+          @selection-change="handleLobsterTagSelect"
+          max-height="400"
+          border
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column prop="tagCode" label="标签编码" width="150" />
+          <el-table-column prop="tagName" label="标签名称" min-width="150" />
+          <el-table-column prop="templateName" label="绑定模板" min-width="180" show-overflow-tooltip />
+          <el-table-column prop="priority" label="优先级" width="100" align="center">
+            <template slot-scope="{ row }">
+              <el-tag :type="row.priority >= 50 ? 'danger' : row.priority >= 30 ? 'warning' : 'info'" size="mini">
+                {{ row.priority }}
+              </el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :disabled="lobsterSelectedTags.length === 0" @click="submitLobsterTag">确 定 (已选{{ lobsterSelectedTags.length }})</el-button>
+        <el-button @click="lobsterTagOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
     <el-dialog title="批量添加客户备注" :visible.sync="notesOpen.open" width="800px" append-to-body>
       <el-card>
         <el-row>
@@ -1030,6 +1098,7 @@ import  selectUser  from "@/views/qw/externalContact/selectUser.vue";
 import  collection  from "@/views/qw/externalContact/collection.vue";
 import info from "@/views/qw/externalContact/info.vue";
 import { editTalk } from "@/api/qw/externalContactInfo";
+import { tagBindingApi } from '@/api/company/tagBinding'
 import PaginationMore from "../../../components/PaginationMore/index.vue";
 import userDetails from '@/views/store/components/userDetails.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
@@ -1122,6 +1191,11 @@ export default {
       exportLoading: false,
       tagOpen:false,
       tagDelOpen:false,
+      // 龙虾标签弹窗
+      lobsterTagOpen: false,
+      lobsterTagLoading: false,
+      lobsterTagList: [],
+      lobsterSelectedTags: [],
       // 选中数组
       ids: [],
       isBindActiveName:"all",
@@ -1541,6 +1615,18 @@ export default {
         this.externalContactList = response.rows;
         this.total = response.total;
         this.loading = false;
+                // 加载龙虾标签
+        if (this.externalContactList && this.externalContactList.length) {
+          const userIds = this.externalContactList.map(u => u.id);
+          tagBindingApi.getLobsterTags(userIds).then(res => {
+            const tagMap = res.data || {};
+            this.externalContactList.forEach(row => {
+              const tagData = tagMap[row.id];
+              const tagList = tagData ? (Array.isArray(tagData) ? tagData : [tagData]) : [];
+              this.$set(row, 'lobsterTagNames', tagList.map(tag => tag.tag_name));
+            });
+          }).catch(() => {});
+        }
       });
     },
     bindMiniCustomerId(row){
@@ -2390,6 +2476,74 @@ export default {
 		    this.msgSuccess("成功");
 		  }).catch(() => {});
 	},
+    addLobsterTag() {
+      if (this.ids == null || this.ids.length === 0) {
+        return this.$message('请勾选需要添加龙虾标签的客户');
+      }
+      this.lobsterTagLoading = true;
+      this.lobsterTagOpen = true;
+      this.lobsterSelectedTags = [];
+      tagBindingApi.getListByStatus({ status: 1 }).then(res => {
+        this.lobsterTagList = res.data || [];
+      }).catch(() => {
+        this.$message.error('获取龙虾标签列表失败');
+      }).finally(() => {
+        this.lobsterTagLoading = false;
+        this.$nextTick(() => {
+          if (this.$refs.lobsterTagTable) {
+            this.$refs.lobsterTagTable.clearSelection();
+          }
+        });
+      });
+    },
+    handleLobsterTagSelect(selection) {
+      this.lobsterSelectedTags = selection;
+    },
+    submitLobsterTag() {
+      if (this.lobsterSelectedTags.length === 0) {
+        return this.$message('请选择龙虾标签');
+      }
+      const tagCodes = this.lobsterSelectedTags.map(t => t.tagCode);
+      const tagNames = this.lobsterSelectedTags.map(t => t.tagName);
+      this.$confirm(
+        '将为 ' + this.ids.length + ' 个客户添加以下标签:' + tagNames.join('、') + ',是否确认?',
+        '提示',
+        { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
+      ).then(() => {
+        tagBindingApi.batchBindLobsterTag({
+          userIds: this.ids,
+          tagCodes: tagCodes,
+          qwCorpId: this.queryParams.corpId
+        }).then(res => {
+          this.$message.success('龙虾标签添加任务已提交');
+          this.lobsterTagOpen = false;
+          this.lobsterSelectedTags = [];
+          this.getList();
+        }).catch(() => {
+          this.$message.error('龙虾标签添加失败');
+        });
+      }).catch(() => {});
+    },
+    removeLobsterTag() {
+      if (this.ids == null || this.ids.length === 0) {
+        return this.$message('请勾选需要取消龙虾标签的客户');
+      }
+      this.$confirm(
+        '确认取消 ' + this.ids.length + ' 个客户的龙虾标签吗?',
+        '提示',
+        { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
+      ).then(() => {
+        tagBindingApi.batchUnbindLobsterTag({
+          userIds: this.ids,
+          qwCorpId: this.queryParams.corpId
+        }).then(res => {
+          this.$message.success('龙虾标签取消任务已提交');
+          this.getList();
+        }).catch(() => {
+          this.$message.error('龙虾标签取消失败');
+        });
+      }).catch(() => {});
+    },
     /** 导出按钮操作 */
     handleExport() {
       const { qwUserName, ...queryParams } = this.queryParams;
@@ -2602,4 +2756,4 @@ export default {
   border-radius: 1px;
   margin-bottom: 20px;
 }
-</style>
+</style>