Commit 01ec7a6c by lijiabin

feat: 加入一些助手功能

parent 7a5c0576
# 前端:请求本地 AI 代理
VITE_AI_API_URL=
# 后端AI server:OpenAI 兼容接口
MY_OPENAI_API_KEY=
MY_OPENAI_BASE_URL=
# 请求后端API地址
BACKEND_API_URL=
......@@ -19,7 +19,8 @@
"deploy:test": "node deploy/deploytest.js",
"build:prod": "nvm use 20 && vite build --mode production",
"deploy:prod": "node deploy/deployprod.js",
"deploy:prod:update-info": "node deploy/deployprod.js --update-info"
"deploy:prod:update-info": "node deploy/deployprod.js --update-info",
"dev:server": "tsx watch server/index.ts"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
......@@ -30,19 +31,26 @@
"@wecom/jssdk": "^2.3.3",
"archiver": "^7.0.1",
"axios": "^1.13.0",
"cors": "^2.8.6",
"dayjs": "^1.11.19",
"dotenv": "^17.3.1",
"element-plus": "^2.11.5",
"express": "^5.2.1",
"inquirer": "^13.0.2",
"notivue": "^2.4.5",
"openai": "^6.33.0",
"pinia": "^3.0.3",
"ssh2": "^1.17.0",
"vue": "^3.5.22",
"vue-router": "^4.6.3",
"xlsx": "^0.18.5"
"xlsx": "^0.18.5",
"zod": "^4.3.6"
},
"devDependencies": {
"@iconify-json/ep": "^1.2.3",
"@tsconfig/node22": "^22.0.2",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^22.18.11",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
......@@ -57,6 +65,7 @@
"prettier": "3.6.2",
"rollup-plugin-visualizer": "^6.0.5",
"sass-embedded": "^1.93.2",
"tsx": "^4.21.0",
"typescript": "~5.9.0",
"unocss": "^66.5.4",
"unplugin-auto-import": "^20.2.0",
......
import 'dotenv/config'
import cors from 'cors'
import express from 'express'
import OpenAI from 'openai'
import { zodTextFormat } from 'openai/helpers/zod'
import { z } from 'zod'
import dotenv from 'dotenv'
import { setAuthorization } from './utils/headers'
import { addTag } from './utils'
dotenv.config({ path: '.env.local', override: true })
const apiKey = process.env.MY_OPENAI_API_KEY
const baseURL = process.env.MY_OPENAI_BASE_URL
const client = new OpenAI({ apiKey, baseURL })
const app = express()
const PORT = 4000
app.use(cors())
app.use(express.json())
// 写一个中间件 如果请求头里面有 authorization 字段 就把他存在 本地中
app.use((req, res, next) => {
if (req.headers.authorization) {
setAuthorization(req.headers.authorization)
}
next()
})
// 润色标题和内容
app.post('/api/polish-title', async (req, res) => {
const { title, content } = req.body
const InputSchema = z.object({
title: z.string(),
content: z.string(),
})
const response = await client.responses.create({
model: 'gpt-5-mini',
input: [
{
role: 'user',
content: `你是一个企业文化内容编辑专家,请遵循以下规则:
1. 标题要更有传播性,但不能夸张
2. 内容要更通顺、有感染力
3. 保留原意,不要编造
输入:
标题:${title}
内容:${content}`,
},
],
text: {
format: zodTextFormat(InputSchema, 'Input'),
},
})
const data = InputSchema.parse(JSON.parse(response.output_text))
console.log(data)
res.json({
message: 'AI润色标题和内容成功',
data,
})
})
// ai生成标签3条标签
app.post('/api/generate-tags', async (req, res) => {
const Schema = z.object({
tags: z
.array(
z.object({
color: z.string().default('#000000'),
description: z.string(),
title: z.string(),
type: z.literal('culture'),
}),
)
.length(3),
})
const response = await client.responses.create({
model: 'gpt-5-mini',
input: [
{
role: 'user',
content: '请生成5条标签,关于企业文化相关的标签',
},
],
text: {
format: zodTextFormat(Schema, 'Input'),
},
})
const tags = Schema.parse(JSON.parse(response.output_text)).tags
// 调用后端接口
await Promise.all(tags.map((tag) => addTag(tag)))
res.json({ message: 'AI生成标签成功', data: tags })
})
// 根据标题和内容 帮我选取标签
app.post('/api/select-tags-by-ai', async (req, res) => {
const { title, content, tagList } = req.body
const Schema = z.object({
mainTagId: z.string(),
subTagIdList: z.array(z.number()).max(3).min(0),
})
const response = await client.responses.create({
model: 'gpt-5-mini',
input: [
{
role: 'user',
content: `根据用户给出的标题:${title}和内容:${content},以及用户给出的标签列表:${JSON.stringify(tagList)},请根据标题和内容 帮我选取主、副标签,并返回对应的格式`,
},
],
text: {
format: zodTextFormat(Schema, 'Input'),
},
})
console.log(response.output_text)
const data = Schema.parse(JSON.parse(response.output_text))
res.json({ message: 'AI选取标签成功', data })
})
// ai生成评论
app.post('/api/generate-comment', async (req, res) => {
const { content } = req.body
const Schema = z.object({ content: z.string() })
const response = await client.responses.create({
model: 'gpt-5-mini',
input: [
{
role: 'user',
content: `你是一个评论专家
请遵循以下规则:
1. 评论要更通顺、有感染力
2. 保留原意,不要编造
3. 评论要更符合用户给出的内容
输入:
内容:${content}`,
},
],
text: {
format: zodTextFormat(Schema, 'Input'),
},
})
const data = Schema.parse(JSON.parse(response.output_text))
res.json({ message: 'AI生成评论成功', data: data.content })
})
app.use((_req, res) => {
res.status(404).json({ error: 'not found' })
})
app.listen(PORT, () => {
console.log(`[server] http://localhost:${PORT}`)
})
let authorization = ''
export const setAuthorization = (token: string) => {
authorization = token
}
export const getHeaders = () => {
return authorization
}
import { getHeaders } from './headers'
// 添加标签
export const addTag = async (tag: {
color: string
description: string
title: string
type: 'culture' | string
}) => {
return fetch(`${process.env.BACKEND_API_URL}/api/cultureTag/addOrUpdate`, {
method: 'POST',
body: JSON.stringify(tag),
headers: {
'Content-Type': 'application/json',
Authorization: getHeaders(),
},
}).then((res) => res.json())
}
......@@ -81,6 +81,14 @@
</div>
<div v-show="!myCommentLoading">发表</div>
</button>
<button
type="button"
:disabled="loading"
class="comment-publish-btn cursor-pointer disabled:opacity-50 px-6 py-2 text-white rounded-full text-sm hover:shadow-lg transition-all"
@click="handleAiGenerateComment"
>
AI生成评论
</button>
</template>
</CommentBox>
</div>
......@@ -418,7 +426,9 @@ const {
isQuestion = false,
commentId = 0,
type,
content,
} = defineProps<{
content?: string // 文章内容
authorId?: string // 文章作者id
id: number // 文章ID
defaultSize?: number
......@@ -680,6 +690,21 @@ const handleOpenCommentDialog = (item: CommentItemDto) => {
commentListDialogRef.value?.open()
}
const handleAiGenerateComment = async () => {
loading.value = true
const res = await fetch(`${import.meta.env.VITE_AI_API_URL}/api/generate-comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
}),
})
const { data } = await res.json()
myComment.value = data
loading.value = false
}
defineExpose({
scrollToCommentBox: () => handleBackTop(),
search: () => search(),
......
......@@ -116,6 +116,23 @@
<!-- 右侧操作按钮 -->
<div class="flex items-center gap-3">
<el-button
type="primary"
:loading="btnLoading"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
@click="handleTestServer"
>
AI润色标题和内容
</el-button>
<!-- 根据标题和内容 帮我选取标签 -->
<el-button
type="primary"
:loading="btnLoading"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
@click="handleAiGenerateTags"
>
AI选取标签
</el-button>
<el-button
class="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-lg border border-gray-200 text-sm"
@click="handlePublish(ReleaseStatusTypeEnum.DRAFT)"
>
......@@ -300,6 +317,57 @@ const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
loading.value = false
}
}
const btnLoading = ref(false)
const handleTestServer = async () => {
if (!form.value.title || !form.value.content) return push.warning('请输入标题和内容')
btnLoading.value = true
const res = await fetch(`${import.meta.env.VITE_AI_API_URL}/api/polish-title`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: form.value.title,
content: form.value.content,
}),
})
btnLoading.value = false
const { data } = await res.json()
form.value.title = data.title
form.value.content = data.content
push.success('AI润色标题和内容成功')
}
const handleAiGenerateTags = async () => {
if (!form.value.title || !form.value.content) return push.warning('请输入标题和内容')
btnLoading.value = true
const res = await fetch(`${import.meta.env.VITE_AI_API_URL}/api/select-tags-by-ai`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: localStorage.getItem('token') || '',
},
body: JSON.stringify({
title: form.value.title,
content: form.value.content,
// 把全部标签传过去
tagList: tagList.value.map((item) => ({
id: item.id,
title: item.title,
})),
}),
})
const { data } = await res.json()
btnLoading.value = false
form.value.mainTagId = data.mainTagId
form.value.tagList = data.subTagIdList
push.success('AI选取标签成功')
}
</script>
<style scoped>
......
......@@ -7,7 +7,7 @@
:mode="mode"
/>
<Editor
style="height: 500px; overflow-y: hidden"
style="height: 800px; overflow-y: hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
:mode="mode"
......
......@@ -12,6 +12,7 @@
<!-- 评论区 -->
<Comment
:content="articleDetail.content"
class="mt-6"
ref="commentRef"
:id="id"
......
......@@ -23,6 +23,11 @@
<el-icon><IEpPlus /></el-icon>
新增
</el-button>
<!-- ai生成几条标签 -->
<el-button type="primary" @click="handleAiGenerateTags" :loading="aiLoading" class="ml-12px">
AI生成3条标签
</el-button>
</div>
<!-- 表格区域 -->
......@@ -132,7 +137,6 @@ import { useMessageBox } from '@/hooks'
const { confirm } = useMessageBox()
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(getTagList)
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
......@@ -218,6 +222,23 @@ const handleSubmit = async () => {
console.error('表单验证失败:', error)
}
}
const aiLoading = ref(false)
const handleAiGenerateTags = async () => {
aiLoading.value = true
const res = await fetch(`${import.meta.env.VITE_AI_API_URL}/api/generate-tags`, {
method: 'POST',
headers: {
Authorization: localStorage.getItem('token') || '',
},
})
const { data } = await res.json()
push.success(
`生成成功,标签标题如下: ${data.map((item: { title: string }) => item.title).join(', ')}`,
)
refresh()
aiLoading.value = false
}
</script>
<style scoped lang="scss">
......
......@@ -141,6 +141,7 @@
<Transition name="fadeCommentBox" mode="out-in">
<Comment
v-show="item.showComment"
:content="item.content"
:authorId="item.createUserId"
:ref="(e) => (commentRefList[index] = e as InstanceType<typeof Comment>)"
:id="item.id"
......
......@@ -377,6 +377,7 @@
class="mt-4 border border-slate-200 rounded-lg bg-slate-50/50 overflow-hidden"
>
<Comment
:content="questionDetail.content"
:authorId="questionDetail.createUserId"
:ref="(e) => (commentRefList[index] = e as InstanceType<typeof Comment>)"
:id="questionId"
......
......@@ -302,6 +302,7 @@
</div>
<Comment
:content="videoDetail.content"
:authorId="videoDetail.createUserId"
ref="commentRef"
:id="videoId"
......
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"server/**/*.ts",
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment