boss пре 5 дана
родитељ
комит
8a4be3c000

+ 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 } })
+}

+ 52 - 10
src/router/index.js

@@ -585,19 +585,61 @@ export const constantRoutes = [
         ]
         ]
     },
     },
 
 
-    // ======== 龙虾引擎 敏感词库 ========
+    // ======== 龙虾引擎 E2E / 场景 / 动态节点 ========
     {
     {
-        path: '/lobster/sensitive-words',
+        path: '/lobster/test-scenario',
         component: Layout,
         component: Layout,
         hidden: true,
         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' }
+        }]
     },
     },
 
 
 ]
 ]

+ 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() {
     getList() {
       this.loading = true
       this.loading = true
       listInstances(this.queryParams).then(res => {
       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.stats = { running: 0, paused: 0, pending_human: 0, completed: 0, terminated: 0 }
         this.list.forEach(item => {
         this.list.forEach(item => {
           if (this.stats[item.status] !== undefined) this.stats[item.status]++
           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'
 import { triggerLearningCycle, getLearningResults, applyLearningResult } from '@/api/workflow/lobster-dashboard'
 export default {
 export default {
   data() { return { list:[], loading:false } },
   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() },
   created() { this.getList() },
   methods: {
   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>
 </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>