feat: 热点改进
This commit is contained in:
75
frontend/app/web-gold/src/api/forecast.js
Normal file
75
frontend/app/web-gold/src/api/forecast.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Forecast 文案改写 API
|
||||
*/
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
import { API_BASE } from '@gold/config/api'
|
||||
|
||||
const BASE_URL = `${API_BASE.APP_TIK}`
|
||||
|
||||
/**
|
||||
* 流式文案改写(SSE)
|
||||
* @param {Object} options - 请求配置
|
||||
* @param {number} options.agentId - 智能体ID
|
||||
* @param {string} options.userText - 用户输入文案
|
||||
* @param {number} [options.level] - 改写级别/强度
|
||||
* @param {string} [options.modelType] - 模型类型:forecast_standard/forecast_meiju
|
||||
* @param {AbortController} [options.ctrl] - 取消控制器
|
||||
* @param {Function} options.onMessage - 消息回调
|
||||
* @param {Function} [options.onError] - 错误回调
|
||||
* @param {Function} [options.onClose] - 关闭回调
|
||||
*/
|
||||
export async function rewriteStream(options) {
|
||||
const {
|
||||
agentId,
|
||||
userText,
|
||||
level = 50,
|
||||
modelType = 'forecast_standard',
|
||||
ctrl,
|
||||
onMessage,
|
||||
onError,
|
||||
onClose
|
||||
} = options || {}
|
||||
|
||||
const token = tokenManager.getAccessToken()
|
||||
|
||||
return fetchEventSource(`${BASE_URL}/dify/forecast/rewrite`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'tenant-id': import.meta.env?.VITE_TENANT_ID
|
||||
},
|
||||
openWhenHidden: true,
|
||||
body: JSON.stringify({
|
||||
agentId,
|
||||
userText,
|
||||
level,
|
||||
modelType
|
||||
}),
|
||||
onmessage: (event) => {
|
||||
if (typeof onMessage === 'function') {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
// 解析 CommonResult 包装
|
||||
const result = data.code === 0 ? data.data : data
|
||||
onMessage(result)
|
||||
} catch (e) {
|
||||
console.error('解析 SSE 数据失败:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
onerror: (err) => {
|
||||
if (typeof onError === 'function') {
|
||||
onError(err)
|
||||
}
|
||||
throw err // 不重试
|
||||
},
|
||||
onclose: () => {
|
||||
if (typeof onClose === 'function') {
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
signal: ctrl ? ctrl.signal : undefined
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { onMounted, reactive, ref, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
import { CommonService } from '@/api/common'
|
||||
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
|
||||
import { UserPromptApi } from '@/api/userPrompt'
|
||||
import PromptSelector from '@/components/PromptSelector.vue'
|
||||
import { rewriteStream } from '@/api/forecast'
|
||||
import { getAgentList } from '@/api/agent'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
|
||||
|
||||
@@ -25,25 +24,42 @@ const generatedContent = ref('')
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
keyword: '',
|
||||
offset: 0,
|
||||
sort_type: '1',
|
||||
publish_time: '7',
|
||||
cursor: 0,
|
||||
sort_type: '0',
|
||||
publish_time: '0',
|
||||
filter_duration: '0',
|
||||
content_type: '0'
|
||||
content_type: '0',
|
||||
search_id: '',
|
||||
backtrace: ''
|
||||
})
|
||||
|
||||
// 创作详情
|
||||
const topicDetails = reactive({
|
||||
title: '',
|
||||
copywriting: '',
|
||||
stylePrompt: '',
|
||||
stylePromptId: null
|
||||
selectedAgentId: null,
|
||||
modelType: 'forecast_standard',
|
||||
level: 50
|
||||
})
|
||||
|
||||
// 提示词相关
|
||||
const allPrompts = ref([])
|
||||
const loadingPrompts = ref(false)
|
||||
const promptSearchKeyword = ref('')
|
||||
// 智能体列表
|
||||
const agentList = ref([])
|
||||
const loadingAgents = ref(false)
|
||||
const agentSearchKeyword = ref('')
|
||||
|
||||
// 当前选中的智能体
|
||||
const selectedAgent = ref(null)
|
||||
|
||||
// 过滤后的智能体列表
|
||||
const filteredAgentList = computed(() => {
|
||||
if (!agentSearchKeyword.value.trim()) {
|
||||
return agentList.value
|
||||
}
|
||||
const keyword = agentSearchKeyword.value.trim().toLowerCase()
|
||||
return agentList.value.filter(agent =>
|
||||
agent.name.toLowerCase().includes(keyword) ||
|
||||
(agent.description && agent.description.toLowerCase().includes(keyword))
|
||||
)
|
||||
})
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
@@ -80,39 +96,41 @@ async function copyContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// 提示词管理
|
||||
async function loadUserPrompts() {
|
||||
if (!userStore.userId) {
|
||||
console.warn('用户未登录,无法加载提示词')
|
||||
return
|
||||
}
|
||||
|
||||
loadingPrompts.value = true
|
||||
// 加载智能体列表
|
||||
async function loadAgentList() {
|
||||
loadingAgents.value = true
|
||||
try {
|
||||
const response = await UserPromptApi.getUserPromptPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
status: 1
|
||||
})
|
||||
|
||||
if (response?.data?.list) {
|
||||
allPrompts.value = response.data.list
|
||||
const res = await getAgentList()
|
||||
if (res.code === 0 && res.data) {
|
||||
agentList.value = res.data.map(item => ({
|
||||
id: item.id,
|
||||
agentId: item.agentId,
|
||||
name: item.agentName,
|
||||
description: item.description,
|
||||
systemPrompt: item.systemPrompt,
|
||||
avatar: item.icon,
|
||||
categoryName: item.categoryName || '其他'
|
||||
}))
|
||||
// 默认选中第一个
|
||||
if (agentList.value.length > 0 && !topicDetails.selectedAgentId) {
|
||||
topicDetails.selectedAgentId = agentList.value[0].id
|
||||
selectedAgent.value = agentList.value[0]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提示词失败:', error)
|
||||
console.error('加载智能体列表失败:', error)
|
||||
} finally {
|
||||
loadingPrompts.value = false
|
||||
loadingAgents.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理提示词选择
|
||||
function handlePromptChange(prompt) {
|
||||
if (!prompt?.content) {
|
||||
message.warning('提示词内容为空')
|
||||
return
|
||||
// 处理智能体选择
|
||||
function handleAgentChange(agentId) {
|
||||
const agent = agentList.value.find(a => a.id === agentId)
|
||||
if (agent) {
|
||||
selectedAgent.value = agent
|
||||
topicDetails.selectedAgentId = agentId
|
||||
}
|
||||
topicDetails.stylePromptId = prompt.id
|
||||
topicDetails.stylePrompt = prompt.content
|
||||
}
|
||||
|
||||
// 语音分析
|
||||
@@ -145,16 +163,14 @@ async function analyzeVoice(audioUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
// 选择话题
|
||||
function handleSelectTopic(topic) {
|
||||
selectedTopic.value = topic.id
|
||||
}
|
||||
|
||||
// 创作处理
|
||||
async function handleCreate(topic) {
|
||||
selectedTopic.value = topic.id
|
||||
topicDetails.title = topic.title
|
||||
|
||||
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
|
||||
const firstPrompt = allPrompts.value[0]
|
||||
topicDetails.stylePromptId = firstPrompt.id
|
||||
topicDetails.stylePrompt = firstPrompt.content || ''
|
||||
}
|
||||
|
||||
if (topic.audioUrl?.trim()) {
|
||||
await analyzeVoice(topic.audioUrl)
|
||||
@@ -170,7 +186,7 @@ async function handleGenerate() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!topicDetails.stylePrompt?.trim()) {
|
||||
if (!selectedAgent.value?.id) {
|
||||
message.warning('请先选择文案风格')
|
||||
return
|
||||
}
|
||||
@@ -179,12 +195,6 @@ async function handleGenerate() {
|
||||
generatedContent.value = ''
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
audio_prompt: topicDetails.stylePrompt,
|
||||
user_text: topicDetails.copywriting.trim(),
|
||||
amplitude: 50
|
||||
}
|
||||
|
||||
const ctrl = new AbortController()
|
||||
let fullText = ''
|
||||
let completed = false
|
||||
@@ -198,40 +208,27 @@ async function handleGenerate() {
|
||||
}
|
||||
}, 180000)
|
||||
|
||||
CommonService.callWorkflowStream({
|
||||
data: requestData,
|
||||
rewriteStream({
|
||||
agentId: selectedAgent.value.id,
|
||||
userText: topicDetails.copywriting.trim(),
|
||||
level: topicDetails.level,
|
||||
modelType: topicDetails.modelType,
|
||||
ctrl,
|
||||
onMessage: (event) => {
|
||||
if (completed || !event?.data) return
|
||||
onMessage: (data) => {
|
||||
if (completed) return
|
||||
|
||||
const dataStr = event.data.trim()
|
||||
|
||||
if (dataStr === '[DONE]') {
|
||||
// 处理 Dify 响应格式
|
||||
if (data.event === 'message' && data.content) {
|
||||
fullText += data.content
|
||||
generatedContent.value = fullText
|
||||
} else if (data.event === 'done') {
|
||||
completed = true
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if (dataStr.startsWith('[TIMEOUT]')) {
|
||||
} else if (data.event === 'error') {
|
||||
completed = true
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(dataStr.replace('[TIMEOUT]', '').trim() || '请求超时'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(event.data)
|
||||
const piece = obj?.text || obj?.content || obj?.data || ''
|
||||
if (piece) {
|
||||
fullText += piece
|
||||
generatedContent.value = fullText
|
||||
}
|
||||
} catch {
|
||||
if (event.data && !event.data.startsWith('[')) {
|
||||
fullText += event.data
|
||||
generatedContent.value = fullText
|
||||
}
|
||||
reject(new Error(data.errorMessage || '生成失败'))
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -284,8 +281,13 @@ function extractCover(video) {
|
||||
|
||||
function processSearchResults(response, startId = 1) {
|
||||
try {
|
||||
currentCursor.value = response?.data?.cursor || null
|
||||
const dataList = response?.data?.data || []
|
||||
// 保存分页信息用于下次请求
|
||||
const responseData = response?.data || {}
|
||||
currentCursor.value = responseData.cursor || null
|
||||
searchParams.search_id = responseData.search_id || ''
|
||||
searchParams.backtrace = responseData.backtrace || ''
|
||||
|
||||
const dataList = responseData.data || []
|
||||
|
||||
return dataList
|
||||
.map(el => el.aweme_info)
|
||||
@@ -321,8 +323,11 @@ async function handleSearch() {
|
||||
return
|
||||
}
|
||||
|
||||
searchParams.offset = 0
|
||||
// 重置分页参数
|
||||
searchParams.keyword = keyword
|
||||
searchParams.cursor = 0
|
||||
searchParams.search_id = ''
|
||||
searchParams.backtrace = ''
|
||||
currentCursor.value = null
|
||||
hotTopics.value = []
|
||||
isLoading.value = true
|
||||
@@ -330,11 +335,13 @@ async function handleSearch() {
|
||||
try {
|
||||
const urlParams = {
|
||||
keyword: searchParams.keyword,
|
||||
offset: String(searchParams.offset),
|
||||
sort_type: String(searchParams.sort_type),
|
||||
publish_time: String(searchParams.publish_time),
|
||||
cursor: searchParams.cursor,
|
||||
sort_type: searchParams.sort_type,
|
||||
publish_time: searchParams.publish_time,
|
||||
filter_duration: searchParams.filter_duration,
|
||||
content_type: String(searchParams.content_type),
|
||||
content_type: searchParams.content_type,
|
||||
search_id: searchParams.search_id,
|
||||
backtrace: searchParams.backtrace
|
||||
}
|
||||
|
||||
const response = await TikhubService.postTikHup({
|
||||
@@ -364,13 +371,7 @@ async function handleSearch() {
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (userStore.userId) {
|
||||
loadUserPrompts()
|
||||
} else if (userStore.isLoggedIn) {
|
||||
setTimeout(() => {
|
||||
if (userStore.userId) loadUserPrompts()
|
||||
}, 500)
|
||||
}
|
||||
loadAgentList()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -413,26 +414,26 @@ onMounted(() => {
|
||||
<label>排序</label>
|
||||
<a-select v-model:value="searchParams.sort_type" size="small">
|
||||
<a-select-option value="0">综合</a-select-option>
|
||||
<a-select-option value="1">最热</a-select-option>
|
||||
<a-select-option value="2">最新</a-select-option>
|
||||
<a-select-option value="1">最多点赞</a-select-option>
|
||||
<a-select-option value="2">最新发布</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>时间</label>
|
||||
<a-select v-model:value="searchParams.publish_time" size="small">
|
||||
<a-select-option value="0">不限</a-select-option>
|
||||
<a-select-option value="1">今天</a-select-option>
|
||||
<a-select-option value="7">本周</a-select-option>
|
||||
<a-select-option value="180">半年</a-select-option>
|
||||
<a-select-option value="1">最近一天</a-select-option>
|
||||
<a-select-option value="7">最近一周</a-select-option>
|
||||
<a-select-option value="180">最近半年</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>时长</label>
|
||||
<a-select v-model:value="searchParams.filter_duration" size="small">
|
||||
<a-select-option value="0">不限</a-select-option>
|
||||
<a-select-option value="0-1"><1分钟</a-select-option>
|
||||
<a-select-option value="0-1">1分钟以内</a-select-option>
|
||||
<a-select-option value="1-5">1-5分钟</a-select-option>
|
||||
<a-select-option value="5-10000">>5分钟</a-select-option>
|
||||
<a-select-option value="5-10000">5分钟以上</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -464,7 +465,7 @@ onMounted(() => {
|
||||
<article
|
||||
v-for="topic in hotTopics"
|
||||
:key="topic.id"
|
||||
@click="handleCreate(topic)"
|
||||
@click="handleSelectTopic(topic)"
|
||||
class="topic-card"
|
||||
:class="{ active: selectedTopic === topic.id }"
|
||||
>
|
||||
@@ -535,16 +536,6 @@ onMounted(() => {
|
||||
</header>
|
||||
|
||||
<div class="editor-section">
|
||||
<!-- 标题 -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">标题</label>
|
||||
<a-input
|
||||
v-model:value="topicDetails.title"
|
||||
placeholder="选择热点或输入标题"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文案 -->
|
||||
<div class="form-group">
|
||||
<div class="form-label-row">
|
||||
@@ -566,21 +557,75 @@ onMounted(() => {
|
||||
<!-- 风格选择 -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">风格</label>
|
||||
<PromptSelector
|
||||
v-model="topicDetails.stylePromptId"
|
||||
:prompts="allPrompts"
|
||||
:loading="loadingPrompts"
|
||||
:search-keyword="promptSearchKeyword"
|
||||
@change="handlePromptChange"
|
||||
@update:searchKeyword="promptSearchKeyword = $event"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="topicDetails.selectedAgentId"
|
||||
placeholder="选择文案风格"
|
||||
style="width: 100%"
|
||||
:loading="loadingAgents"
|
||||
show-search
|
||||
:filter-option="false"
|
||||
@change="handleAgentChange"
|
||||
@search="agentSearchKeyword = $event"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="agent in filteredAgentList"
|
||||
:key="agent.id"
|
||||
:value="agent.id"
|
||||
>
|
||||
<div class="agent-option">
|
||||
<span class="agent-option-name">{{ agent.name }}</span>
|
||||
<span class="agent-option-category">{{ agent.categoryName }}</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 模型类型切换 -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">模式</label>
|
||||
<div class="model-type-switch">
|
||||
<button
|
||||
:class="['type-btn', { active: topicDetails.modelType === 'forecast_standard' }]"
|
||||
@click="topicDetails.modelType = 'forecast_standard'"
|
||||
>
|
||||
标准版
|
||||
</button>
|
||||
<button
|
||||
:class="['type-btn', { active: topicDetails.modelType === 'forecast_meiju' }]"
|
||||
@click="topicDetails.modelType = 'forecast_meiju'"
|
||||
>
|
||||
美剧版
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 改写幅度 -->
|
||||
<div class="form-group">
|
||||
<div class="form-label-row">
|
||||
<label class="form-label">改写幅度</label>
|
||||
<span class="level-value">{{ topicDetails.level }}%</span>
|
||||
</div>
|
||||
<div class="level-slider">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="topicDetails.level"
|
||||
min="10"
|
||||
max="100"
|
||||
step="10"
|
||||
class="slider"
|
||||
/>
|
||||
<div class="level-labels">
|
||||
<span>保守</span>
|
||||
<span>激进</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<button
|
||||
class="generate-btn"
|
||||
:class="{ generating: isGenerating }"
|
||||
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating"
|
||||
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.selectedAgentId || isGenerating"
|
||||
@click="handleGenerate"
|
||||
>
|
||||
<span v-if="isGenerating" class="btn-loading">
|
||||
@@ -1098,6 +1143,90 @@ onMounted(() => {
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
// 模型类型切换
|
||||
.model-type-switch {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: @text-secondary;
|
||||
background: @bg;
|
||||
border: 1px solid @border;
|
||||
border-radius: @radius-sm;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: @text;
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: @primary;
|
||||
border-color: @primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 改写幅度滑块
|
||||
.level-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: @primary;
|
||||
}
|
||||
|
||||
.level-slider {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: @bg;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: @primary;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 0 4px fade(@primary-color, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: @primary;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.level-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
// 生成按钮
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user