refactor(Agent/Chat): 使用 cozeInfo 替代 botId 获取聊天记录

- 移除了 HistoryChat 组件中的 botId属性
- 使用 cozeInfo.bot_id 替代 botId 获取历史聊天数据
This commit is contained in:
林志军
2025-07-24 19:07:46 +08:00
parent 3c9be781a6
commit 9fa28c76cc
20 changed files with 3470 additions and 652 deletions

View File

@ -133,7 +133,7 @@ const str: string = join(['a', 'b', 'c'], '~');
> 命名导出: `Comp/Index.js`
```js
// Comp/Index.js
// Comp/index.js
export { CompA, CompB, CompC };
```

View File

@ -17,10 +17,12 @@
"ali-oss": "^6.17.1",
"axios": "^1.3.0",
"dayjs": "^1.11.7",
"dompurify": "^3.2.6",
"echarts": "^5.6.0",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"lodash-es": "^4.17.21",
"marked": "^16.1.1",
"mitt": "^3.0.0",
"normalize.css": "^8.0.1",
"pinia": "^2.0.29",

View File

@ -0,0 +1,137 @@
<template>
<a-upload
:custom-request="customRequest"
action="/"
:limit="limit"
:fileList="fileList"
@change="onChange"
@success="handleSuccess"
@error="handleError"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { fetchImageUploadFile, fetchUploadFile } from '@/api/all';
import axios from 'axios';
const fileList = ref([]);
const props = defineProps({
modelValue: {
type: [Array, String],
default: '',
},
limit: {
type: Number,
default: 0, // 0 表示不限制
},
});
const emit = defineEmits(['update:modelValue']);
const handleSuccess = (fileItem) => {
let response = fileItem.response;
response = JSON.parse(response);
if (response && response.data.file_url) {
if (props.limit === 1) {
emit('update:modelValue', response.data.file_url);
} else {
emit('update:modelValue', [...props.modelValue, response.data.file_url]);
}
}
};
watch(
() => props.modelValue,
async (value) => {
console.log(value, 'value');
if (value) {
fileList.value =
props.limit == 1
? [
{
name: '',
url: props.modelValue as string,
},
]
: (props.modelValue as string[]).map((item) => {
return {
name: '',
url: item,
};
});
} else {
fileList.value = [];
}
},
{ once: true },
);
let previousFileListLength = 0;
//删除图片
const onChange = (fileList) => {
if (fileList.length < previousFileListLength) {
if (props.limit === 1) {
if (fileList.length === 0) {
emit('update:modelValue', '');
}
} else {
if (fileList.length === 0) {
emit('update:modelValue', []);
} else {
let image_data = fileList.map((item) => item.url);
emit('update:modelValue', image_data);
}
}
}
previousFileListLength = fileList.length;
};
const beforeUpload = (file, files) => {
if (props.limit > 0 && files.length >= props.limit) {
Message.warning(`最多只能上传 ${props.limit} 张图片`);
return false; // 阻止上传
}
return true;
};
const handleError = (error) => {
Message.error('上传失败');
console.error(error);
};
const customRequest = async (option) => {
const { onProgress, onError, onSuccess, fileItem, name } = option;
try {
// 1. 获取预签名上传URL
const response = await fetchUploadFile({ suffix: getFileExtension(fileItem.file.name) });
const preSignedUrl = response?.data?.upload_url;
if (!preSignedUrl) {
throw new Error('未能获取有效的预签名上传地址');
}
console.log('preSignedUrl', preSignedUrl);
// 2. 使用预签名URL上传文件
const blob = new Blob([fileItem.file], { type: fileItem.file.type });
await axios.put(preSignedUrl, blob, {
headers: { 'Content-Type': fileItem.file.type },
});
onSuccess(JSON.stringify(response));
} catch (error) {
onError(error);
}
};
function getFileExtension(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
</script>
<style scoped>
/* 添加一些样式 */
</style>

View File

@ -38,7 +38,18 @@ export const router = createRouter({
{
path: '/agent/chat',
name: 'Chat',
component: () => import('@/views/Agent/chat'),
component: () => import('@/views/agent/chat'),
meta: {
hideSidebar: true,
requiresAuth: false,
requireLogin: true,
id: MENU_GROUP_IDS.WORK_BENCH_ID,
},
},
{
path: '/agent/workFlow',
name: 'WorkFlow',
component: () => import('@/views/agent/work-flow'),
meta: {
hideSidebar: true,
requiresAuth: false,

View File

@ -4,42 +4,31 @@ import { MENU_GROUP_IDS } from '@/router/constants';
import IconRepository from '@/assets/svg/icon-repository.svg';
const COMPONENTS: AppRouteRecordRaw[] = [
{
path: '/agent',
name: 'Agent',
redirect: 'agent/index',
meta: {
locale: '扣子智能体',
icon: IconRepository,
requiresAuth: true,
requireLogin: true,
roles: ['*'],
id: MENU_GROUP_IDS.PROPERTY_ID,
},
children: [
{
path: 'index',
name: 'AgentIndex',
component: () => import('@/views/agent/index'),
meta: {
requiresAuth: false,
requireLogin: true,
id: MENU_GROUP_IDS.WORK_BENCH_ID,
},
},
{
path: 'chat',
name: 'Chat',
component: () => import('@/views/agent/chat'),
meta: {
hideSidebar: true,
requiresAuth: false,
requireLogin: true,
id: MENU_GROUP_IDS.WORK_BENCH_ID,
},
},
],
},
// {
// path: '/agent',
// name: 'Agent',
// redirect: 'agent/index',
// meta: {
// locale: '扣子智能体',
// icon: IconRepository,
// requiresAuth: true,
// requireLogin: true,
// roles: ['*'],
// id: MENU_GROUP_IDS.AGENT,
// },
// children: [
// {
// path: 'index',
// name: 'AgentIndex',
// component: () => import('@/views/agent/index'),
// meta: {
// requiresAuth: false,
// requireLogin: true,
// },
// }
//
// ],
// },
];
export default COMPONENTS;

View File

@ -1,36 +0,0 @@
<template>
<a-layout-content style="padding: 24px; background: #fff; min-height: 280px;">
<div style="text-align: center; margin-bottom: 20px">
<img src="@/assets/chatbot-icon.png" alt="Chatbot Icon" style="width: 60px" />
<h3>舆情脉络整理</h3>
</div>
<a-card :bordered="false">
<p>
您好我是舆情脉络整理助手可以帮您梳理事件发展脉络提取核心观点分析情绪倾向快速生成舆情摘要与应对建议
</p>
</a-card>
<div style="margin-top: 20px">
<a-input-search
v-model:value="message"
placeholder="发送消息..."
enter-button
@search="onSearch"
>
<template #enterButton>
<a-button type="primary">
<!-- <template #icon><SendOutlined /></template>-->
</a-button>
</template>
</a-input-search>
</div>
<small style="display: block; text-align: center; margin-top: 10px">
内容由AI生成无法确保真实准确仅供参考
</small>
</a-layout-content>
</template>
<script lang="ts" setup>
import { defineComponent, ref } from 'vue';
// import { SendOutlined } from '@ant-design/icons-vue';
</script>

View File

@ -1,217 +0,0 @@
<template>
<div>
<!-- 聊天容器 -->
<div ref="chatContainer" class="coze-chat-container"></div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<p>正在加载聊天服务...</p>
</div>
<!-- 错误提示 -->
<div v-if="error" class="error-state">
<p> 聊天服务加载失败</p>
<button @click="retry">重新加载</button>
</div>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';
export default {
props: {
botId: {
type: String,
default: '7522056630889381923',
},
title: {
type: String,
default: 'Coze助手',
},
token: {
type: String,
required: true,
},
},
setup(props) {
const chatContainer = ref(null);
const loading = ref(true);
const error = ref(false);
let chatClient = null;
let scriptLoaded = false;
// 加载SDK脚本
const loadSDK = () => {
return new Promise((resolve, reject) => {
// 检查是否已加载
if (window.CozeWebSDK) {
resolve();
return;
}
// 检查是否正在加载
if (document.querySelector('script[src*="coze.cn"]')) {
const checkInterval = setInterval(() => {
if (window.CozeWebSDK) {
clearInterval(checkInterval);
resolve();
}
}, 100);
return;
}
// 创建新脚本
const script = document.createElement('script');
script.src = 'https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.10/libs/cn/index.js';
script.onload = () => {
scriptLoaded = true;
resolve();
};
script.onerror = (err) => {
console.error('SDK加载失败:', err);
reject(new Error('无法加载聊天SDK'));
};
document.head.appendChild(script);
});
};
// 初始化聊天
const initChat = () => {
try {
if (!window.CozeWebSDK) {
throw new Error('SDK未加载');
}
chatClient = new window.CozeWebSDK.WebChatClient({
container: chatContainer.value,
config: {
bot_id: props.botId,
},
componentProps: {
title: props.title,
// 可选配置
// theme: 'light',
// welcome_message: '您好!需要什么帮助?',
// input_placeholder: '输入您的问题...'
},
auth: {
type: 'token',
token: props.token,
onRefreshToken: () => {
// 实际项目中应从API获取新token
console.log('Token刷新');
return props.token;
},
},
});
loading.value = false;
} catch (err) {
console.error('聊天初始化失败:', err);
error.value = true;
loading.value = false;
}
};
const retry = async () => {
error.value = false;
loading.value = true;
try {
await loadSDK();
initChat();
} catch (err) {
console.error('重试失败:', err);
error.value = true;
loading.value = false;
}
};
onMounted(async () => {
try {
await loadSDK();
initChat();
} catch (err) {
console.error('初始化失败:', err);
error.value = true;
loading.value = false;
}
});
onBeforeUnmount(() => {
if (chatClient && typeof chatClient.destroy === 'function') {
chatClient.destroy();
}
if (scriptLoaded) {
const scripts = document.querySelectorAll('script[src*="coze.cn"]');
scripts.forEach((script) => script.remove());
window.CozeWebSDK = undefined;
}
});
return {
chatContainer,
loading,
error,
retry,
};
},
};
</script>
<style scoped>
.coze-chat-container {
height: 600px;
width: 100%;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
background: #f5f7fa;
}
.loading-state,
.error-state {
height: 600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #f9f9f9;
border-radius: 12px;
border: 1px dashed #ddd;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-state button {
margin-top: 16px;
padding: 8px 16px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.error-state button:hover {
background: #2980b9;
}
</style>

View File

@ -1,217 +0,0 @@
<template>
<div>
<!-- 聊天容器 -->
<div ref="chatContainer" class="coze-chat-container"></div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<p>正在加载聊天服务...</p>
</div>
<!-- 错误提示 -->
<div v-if="error" class="error-state">
<p> 聊天服务加载失败</p>
<button @click="retry">重新加载</button>
</div>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';
export default {
props: {
botId: {
type: String,
default: '7522056630889381923',
},
title: {
type: String,
default: 'Coze助手',
},
token: {
type: String,
required: true,
},
},
setup(props) {
const chatContainer = ref(null);
const loading = ref(true);
const error = ref(false);
let chatClient = null;
let scriptLoaded = false;
// 加载SDK脚本
const loadSDK = () => {
return new Promise((resolve, reject) => {
// 检查是否已加载
if (window.CozeWebSDK) {
resolve();
return;
}
// 检查是否正在加载
if (document.querySelector('script[src*="coze.cn"]')) {
const checkInterval = setInterval(() => {
if (window.CozeWebSDK) {
clearInterval(checkInterval);
resolve();
}
}, 100);
return;
}
// 创建新脚本
const script = document.createElement('script');
script.src = 'https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.10/libs/cn/index.js';
script.onload = () => {
scriptLoaded = true;
resolve();
};
script.onerror = (err) => {
console.error('SDK加载失败:', err);
reject(new Error('无法加载聊天SDK'));
};
document.head.appendChild(script);
});
};
// 初始化聊天
const initChat = () => {
try {
if (!window.CozeWebSDK) {
throw new Error('SDK未加载');
}
chatClient = new window.CozeWebSDK.WebChatClient({
container: chatContainer.value,
config: {
bot_id: props.botId,
},
componentProps: {
title: props.title,
// 可选配置
// theme: 'light',
// welcome_message: '您好!需要什么帮助?',
// input_placeholder: '输入您的问题...'
},
auth: {
type: 'token',
token: props.token,
onRefreshToken: () => {
// 实际项目中应从API获取新token
console.log('Token刷新');
return props.token;
},
},
});
loading.value = false;
} catch (err) {
console.error('聊天初始化失败:', err);
error.value = true;
loading.value = false;
}
};
const retry = async () => {
error.value = false;
loading.value = true;
try {
await loadSDK();
initChat();
} catch (err) {
console.error('重试失败:', err);
error.value = true;
loading.value = false;
}
};
onMounted(async () => {
try {
await loadSDK();
initChat();
} catch (err) {
console.error('初始化失败:', err);
error.value = true;
loading.value = false;
}
});
onBeforeUnmount(() => {
if (chatClient && typeof chatClient.destroy === 'function') {
chatClient.destroy();
}
if (scriptLoaded) {
const scripts = document.querySelectorAll('script[src*="coze.cn"]');
scripts.forEach((script) => script.remove());
window.CozeWebSDK = undefined;
}
});
return {
chatContainer,
loading,
error,
retry,
};
},
};
</script>
<style scoped>
.coze-chat-container {
height: 600px;
width: 100%;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
background: #f5f7fa;
}
.loading-state,
.error-state {
height: 600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #f9f9f9;
border-radius: 12px;
border: 1px dashed #ddd;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-state button {
margin-top: 16px;
padding: 8px 16px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.error-state button:hover {
background: #2980b9;
}
</style>

View File

@ -27,7 +27,7 @@ import { useRouter } from 'vue-router';
const router = useRouter();
//
const authToken = ref('pat_tuIM7jubM1hLXaIWzbWg1U15lBe66AlYwu9BkXMQXInh8VdPszRFTwlTPmdziHwg');
const authToken = ref('');
// APItoken
const fetchToken = async () => {
@ -89,9 +89,8 @@ const initChat = async () => {
};
const cozeWebSdkConfig = (botId, name, auth) => {
console.log(name, 'title');
auth.onRefreshToken = function () {
return 'pat_tuIM7jubM1hLXaIWzbWg1U15lBe66AlYwu9BkXMQXInh8VdPszRFTwlTPmdziHwg';
return '';
};
let config = {
config: {
@ -105,6 +104,9 @@ const cozeWebSdkConfig = (botId, name, auth) => {
title: name,
isNeedFunctionCallMessage: true,
},
footer:{
expressionText:"内容由AI生成无法确保真实准确仅供参考。",
},
},
auth: auth,
base: {

View File

@ -2,7 +2,7 @@
<div class="agent-wrap">
<a-input
style="float: right; width: 300px"
v-model="query.title"
v-model="query.name"
@blur="getData()"
placeholder="搜索智能体"
size="medium"
@ -57,7 +57,7 @@ const getData = async () => {
};
const query = reactive({
title: '',
name: '',
});
const goDetail = (type: number, id: number) => {
if (type === 1) {

View File

@ -4,16 +4,30 @@
<a-form-item
v-for="(field, index) in formFields"
:key="index"
:label="field.label"
:field="field.key"
:rules="field.rules"
:label="field.props.label"
:field="field.props.name"
:rules="field.props.rules"
>
<a-input v-if="field.type === 'input'" v-model="formData[field.key]" :placeholder="field.placeholder" />
<a-textarea v-if="field.type === 'textarea'" v-model="formData[field.key]" :placeholder="field.placeholder" />
<ImageUpload v-if="field.type == 'upload'" v-model="formData[field.key]" :limit="1"></ImageUpload>
<a-select v-else-if="field.type === 'select'" v-model="formData[field.key]" :placeholder="field.placeholder">
<a-option v-for="(option, optIndex) in field.options" :key="optIndex" :value="option.value">
<a-input
allowClear
v-if="field.type === 'input'"
v-model="formData[field.props.name]"
:placeholder="field?.props?.placeholder"
/>
<a-textarea
v-if="field.type === 'textarea'"
style="width: 500px; height: 200px;"
v-model="formData[field.props.name]"
:placeholder="field?.props?.placeholder"
/>
<ImageUpload v-if="field.type == 'upload_image'" v-model="formData[field.props.name]" :limit="field.props.limit"></ImageUpload>
<FileUpload v-if="field.type == 'upload_file'" v-model="formData[field.props.name]" :limit="field.props.limit"></FileUpload>
<a-select
v-else-if="field.type === 'select'"
v-model="formData[field.props.name]"
:placeholder="field.placeholder"
>
<a-option v-for="(option, optIndex) in field.props.options" :key="optIndex" :value="option.value">
{{ option.label }}
</a-option>
</a-select>
@ -25,6 +39,7 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import ImageUpload from '@/components/upload/ImageUpload.vue';
import FileUpload from '@/components/upload/FileUpload.vue';
const props = defineProps({
formFields: {
@ -45,7 +60,7 @@ const formRef = ref(null);
const handleSubmit = async () => {
const errors = await formRef.value.validate();
if (errors) return;
console.log(props.formFields, 'props.formFields');
emit('submit', props.formData);
};
</script>

View File

@ -5,11 +5,10 @@
</div>
<a-menu mode="inline" theme="light">
<a-menu-item key="1">
<span>{{ cozeInfo.title }}</span>
<span>{{ cozeInfo.name }}</span>
<span style="color: #8492ff; font-size: 12px">{{ cozeInfo.type == 1 ? '智能体' : '对话式' }}</span>
<span style="float: right">{{ cozeInfo.views }}次使用 </span>
</a-menu-item>
<a-menu-item key="2">历史对话</a-menu-item>
</a-menu>
</a-layout-sider>
@ -28,15 +27,8 @@ const props = defineProps({
default: '',
},
});
console.log(props.cozeInfo, 'cozeInfo');
const getHistoryChat = async () => {
const { code, data } = await getHistoryChat({ botId: props.botId });
console.log(data, 'data');
//
};
onMounted(() => {
getHistoryChat();
});
</script>

View File

@ -13,9 +13,8 @@
</a-layout-sider>
<a-layout-content ref="contentRef" class="content-container">
<a-spin v-if="loading" class="spin-center" tip="生成中。。。" />
<div class="work-res">
<span > {{ workFlowRes?.output }}</span>
</div>
<div v-if="workFlowRes?.output != ''" class="work-res" v-html="renderedMarkdown"></div>
<NoData v-else />
</a-layout-content>
</a-layout>
</a-layout>
@ -26,9 +25,11 @@
<script setup>
import { ref, reactive } from 'vue';
import HistoryChat from './components/historyChat.vue';
import { executeWorkFlow, getWorkFlowInfo } from '@/api/all/agent';
import DynamicForm from './components/DynamicForm.vue';
import { executeWorkFlow, getWorkFlowInfo } from '@/api/all/agent';
import { useRoute, useRouter } from 'vue-router';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
const formFields = ref([]);
@ -49,7 +50,7 @@ const goChatIndex = async () => {
const loading = ref(false);
const cozeInfo = reactive({
title: '',
name: '',
description: '',
icon_url: '',
workflow_id: '',
@ -59,11 +60,22 @@ const getData = async () => {
Object.assign(cozeInfo, data.info);
formFields.value = data.form_config;
};
const workFlowRes = reactive({});
const workFlowRes = reactive({
output: '',
});
// Markdown
const renderedMarkdown = computed(() => {
if (workFlowRes?.output) {
const rawHtml = marked.parse(workFlowRes.output || '');
return DOMPurify.sanitize(rawHtml); // XSS
}
return '';
});
//
const handleSubmit = async (formData) => {
console.log(formData, 'formData');
try {
const param = { form_data: formData, workflow_id: cozeInfo.workflow_id };
loading.value = true;

View File

@ -40,7 +40,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// 目标地址
target: 'https://lingjiapi.lvfunai.com/api',
target: 'http://www.lingji.com/api',
},
},
},

3350
yarn.lock

File diff suppressed because it is too large Load Diff