feat: 添加 AI 推荐功能
新增 AI 推荐模块,包括前端界面和后端服务 添加 OpenAI API 密钥配置 实现工程数据分析和推荐功能
This commit is contained in:
5
.env
5
.env
@@ -11,4 +11,7 @@ PROXY_HOST=127.0.0.1
|
|||||||
PROXY_PORT=3211
|
PROXY_PORT=3211
|
||||||
|
|
||||||
# 日志级别(可选):error, warn, info, debug, verbose
|
# 日志级别(可选):error, warn, info, debug, verbose
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# OpenAI API Key (用于 AI 推荐)
|
||||||
|
ARK_API_KEY=a63d58b6-cf56-434b-8a42-5c781ba0822a
|
||||||
@@ -13,4 +13,7 @@ PROXY_PORT=6000
|
|||||||
# PROXY_PASSWORD=
|
# PROXY_PASSWORD=
|
||||||
|
|
||||||
# 日志级别(可选):error, warn, info, debug, verbose
|
# 日志级别(可选):error, warn, info, debug, verbose
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# OpenAI API Key (用于 AI 推荐)
|
||||||
|
ARK_API_KEY=your_openai_api_key_here
|
||||||
2
frontend/.env
Normal file
2
frontend/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# ARK API Key (用于 AI 推荐)
|
||||||
|
VITE_ARK_API_KEY=a63d58b6-cf56-434b-8a42-5c781ba0822a
|
||||||
2
frontend/.env.example
Normal file
2
frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# OpenAI API Key (用于 AI 推荐)
|
||||||
|
VITE_OPENAI_API_KEY=your_openai_api_key_here
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"element-plus": "^2.13.1",
|
"element-plus": "^2.13.1",
|
||||||
|
"openai": "^6.16.0",
|
||||||
"vue": "^3.5.24"
|
"vue": "^3.5.24"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -15,10 +15,14 @@
|
|||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="2">
|
<el-menu-item index="2">
|
||||||
|
<el-icon><MagicStick /></el-icon>
|
||||||
|
<span>Dashboard AI</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="3">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<span>Bids</span>
|
<span>Bids</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="3">
|
<el-menu-item index="4">
|
||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
<span>Keywords</span>
|
<span>Keywords</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@@ -41,9 +45,14 @@
|
|||||||
@refresh="fetchData"
|
@refresh="fetchData"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Bids
|
<DashboardAI
|
||||||
v-if="activeIndex === '2'"
|
v-if="activeIndex === '2'"
|
||||||
:bids="bids"
|
:bids="bids"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bids
|
||||||
|
v-if="activeIndex === '3'"
|
||||||
|
:bids="bids"
|
||||||
:source-options="sourceOptions"
|
:source-options="sourceOptions"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
@@ -51,7 +60,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Keywords
|
<Keywords
|
||||||
v-if="activeIndex === '3'"
|
v-if="activeIndex === '4'"
|
||||||
:keywords="keywords"
|
:keywords="keywords"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@refresh="fetchData"
|
@refresh="fetchData"
|
||||||
@@ -64,8 +73,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { DataBoard, Document, Setting } from '@element-plus/icons-vue'
|
import { DataBoard, Document, Setting, MagicStick } from '@element-plus/icons-vue'
|
||||||
import Dashboard from './components/Dashboard.vue'
|
import Dashboard from './components/Dashboard.vue'
|
||||||
|
import DashboardAI from './components/Dashboard-AI.vue'
|
||||||
import Bids from './components/Bids.vue'
|
import Bids from './components/Bids.vue'
|
||||||
import Keywords from './components/Keywords.vue'
|
import Keywords from './components/Keywords.vue'
|
||||||
|
|
||||||
|
|||||||
294
frontend/src/components/Dashboard-AI.vue
Normal file
294
frontend/src/components/Dashboard-AI.vue
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
|
<h2 style="margin: 0;">Dashboard AI</h2>
|
||||||
|
<el-button type="primary" :loading="loading" @click="fetchAIRecommendations">
|
||||||
|
<el-icon style="margin-right: 5px"><MagicStick /></el-icon>
|
||||||
|
获取 AI 推荐
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
|
<h3 style="margin: 0;">选择日期范围</h3>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="To"
|
||||||
|
start-placeholder="Start Date"
|
||||||
|
end-placeholder="End Date"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
clearable
|
||||||
|
style="width: 240px;"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="setLast3Days">3天</el-button>
|
||||||
|
<el-button type="primary" @click="setLast7Days">7天</el-button>
|
||||||
|
<el-button type="success" :loading="bidsLoading" @click="fetchBidsByDateRange">
|
||||||
|
<el-icon style="margin-right: 5px"><List /></el-icon>
|
||||||
|
列出时间范围内所有工程
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card class="box-card" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>AI 推荐项目</span>
|
||||||
|
<el-tag type="success">{{ aiRecommendations.length }} 个推荐</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="loading" style="text-align: center; padding: 40px;">
|
||||||
|
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||||
|
<p style="margin-top: 10px; color: #909399;">AI 正在分析中...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="aiRecommendations.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||||||
|
<el-icon :size="40"><InfoFilled /></el-icon>
|
||||||
|
<p style="margin-top: 10px;">暂无 AI 推荐项目</p>
|
||||||
|
<p style="font-size: 12px; margin-top: 5px;">点击上方按钮获取 AI 推荐</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-table :data="aiRecommendations" style="width: 100%" size="small">
|
||||||
|
<el-table-column prop="title" label="项目名称">
|
||||||
|
<template #default="scope">
|
||||||
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source" label="来源" width="200" />
|
||||||
|
<el-table-column prop="confidence" label="推荐度" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getConfidenceType(scope.row.confidence)">
|
||||||
|
{{ scope.row.confidence }}%
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="showAllBids" :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card class="box-card" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>时间范围内所有工程</span>
|
||||||
|
<el-tag type="info">{{ bidsByDateRange.length }} 个工程</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="bidsLoading" style="text-align: center; padding: 40px;">
|
||||||
|
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||||
|
<p style="margin-top: 10px; color: #909399;">加载中...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="bidsByDateRange.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||||||
|
<el-icon :size="40"><InfoFilled /></el-icon>
|
||||||
|
<p style="margin-top: 10px;">该时间范围内暂无工程</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-table :data="bidsByDateRange" style="width: 100%" size="small">
|
||||||
|
<el-table-column prop="title" label="项目名称">
|
||||||
|
<template #default="scope">
|
||||||
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source" label="来源" width="200" />
|
||||||
|
<el-table-column prop="publishDate" label="发布日期" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.publishDate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { MagicStick, Loading, InfoFilled, List } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
|
||||||
|
interface AIRecommendation {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
source: string
|
||||||
|
confidence: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bids: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const aiRecommendations = ref<AIRecommendation[]>([])
|
||||||
|
const dateRange = ref<[string, string] | null>(null)
|
||||||
|
const showAllBids = ref(false)
|
||||||
|
const bidsLoading = ref(false)
|
||||||
|
const bidsByDateRange = ref<any[]>([])
|
||||||
|
|
||||||
|
// 根据日期范围过滤 bids
|
||||||
|
const filteredBids = computed(() => {
|
||||||
|
let result = props.bids
|
||||||
|
|
||||||
|
// 按日期范围筛选(只限制开始时间,不限制结束时间)
|
||||||
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
|
const [startDate] = dateRange.value
|
||||||
|
result = result.filter(bid => {
|
||||||
|
if (!bid.publishDate) return false
|
||||||
|
const bidDate = new Date(bid.publishDate)
|
||||||
|
const start = new Date(startDate)
|
||||||
|
// 设置时间为当天的开始
|
||||||
|
start.setHours(0, 0, 0, 0)
|
||||||
|
return bidDate >= start
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听日期范围变化并显示提示
|
||||||
|
watch(dateRange, () => {
|
||||||
|
const totalBids = props.bids.length
|
||||||
|
const filteredCount = filteredBids.value.length
|
||||||
|
|
||||||
|
if (totalBids > 0 && filteredCount < totalBids) {
|
||||||
|
ElMessage.info(`筛选结果:共 ${filteredCount} 条数据(总共 ${totalBids} 条)`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置日期范围为最近3天
|
||||||
|
const setLast3Days = () => {
|
||||||
|
const endDate = new Date()
|
||||||
|
const startDate = new Date()
|
||||||
|
startDate.setDate(startDate.getDate() - 2) // 最近3天(包括今天)
|
||||||
|
|
||||||
|
const formatDateForPicker = (date: Date) => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
dateRange.value = [formatDateForPicker(startDate), formatDateForPicker(endDate)]
|
||||||
|
fetchBidsByDateRange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置日期范围为最近7天
|
||||||
|
const setLast7Days = () => {
|
||||||
|
const endDate = new Date()
|
||||||
|
const startDate = new Date()
|
||||||
|
startDate.setDate(startDate.getDate() - 6) // 最近7天(包括今天)
|
||||||
|
|
||||||
|
const formatDateForPicker = (date: Date) => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
dateRange.value = [formatDateForPicker(startDate), formatDateForPicker(endDate)]
|
||||||
|
fetchBidsByDateRange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 AI 推荐项目
|
||||||
|
const fetchAIRecommendations = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 准备发送给后端的数据(使用过滤后的 bids)
|
||||||
|
const bidsData = filteredBids.value.map(bid => ({
|
||||||
|
title: bid.title,
|
||||||
|
url: bid.url,
|
||||||
|
source: bid.source,
|
||||||
|
publishDate: bid.publishDate
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 调用后端 API
|
||||||
|
const response = await axios.post('/api/ai/recommendations', {
|
||||||
|
bids: bidsData
|
||||||
|
})
|
||||||
|
|
||||||
|
aiRecommendations.value = response.data
|
||||||
|
|
||||||
|
ElMessage.success('AI 推荐获取成功')
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('获取 AI 推荐失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取时间范围内的所有工程
|
||||||
|
const fetchBidsByDateRange = async () => {
|
||||||
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||||
|
ElMessage.warning('请先选择日期范围')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showAllBids.value = true
|
||||||
|
bidsLoading.value = true
|
||||||
|
try {
|
||||||
|
const [startDate, endDate] = dateRange.value
|
||||||
|
|
||||||
|
// 检查 endDate 是否是今天
|
||||||
|
const today = new Date()
|
||||||
|
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
||||||
|
|
||||||
|
// 如果 endDate 是今天,则不传递 endDate 参数(不限制截止时间)
|
||||||
|
const params: any = { startDate }
|
||||||
|
if (endDate !== todayStr) {
|
||||||
|
params.endDate = endDate
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get('/api/bids/by-date-range', { params })
|
||||||
|
bidsByDateRange.value = response.data
|
||||||
|
ElMessage.success(`获取成功,共 ${response.data.length} 个工程`)
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('获取工程列表失败')
|
||||||
|
} finally {
|
||||||
|
bidsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期,只显示年月日
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据推荐度返回标签类型
|
||||||
|
const getConfidenceType = (confidence: number) => {
|
||||||
|
if (confidence >= 90) return 'success'
|
||||||
|
if (confidence >= 70) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时设置默认日期范围为最近3天
|
||||||
|
setLast3Days()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,5 +12,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../src/ai/Prompt.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"crawl": "ts-node -r tsconfig-paths/register src/scripts/crawl.ts",
|
"crawl": "ts-node -r tsconfig-paths/register src/scripts/crawl.ts",
|
||||||
"update-source": "ts-node -r tsconfig-paths/register src/scripts/update-source.ts",
|
"update-source": "ts-node -r tsconfig-paths/register src/scripts/update-source.ts",
|
||||||
"web":"npm --prefix frontend run build"
|
"web": "npm --prefix frontend run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"openai": "^6.16.0",
|
||||||
"puppeteer": "^24.34.0",
|
"puppeteer": "^24.34.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
|
|||||||
23
src/ai/ai.controller.ts
Normal file
23
src/ai/ai.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
import { AiService } from './ai.service';
|
||||||
|
|
||||||
|
export class BidDataDto {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
source: string;
|
||||||
|
publishDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BidsRequestDto {
|
||||||
|
bids: BidDataDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller('api/ai')
|
||||||
|
export class AiController {
|
||||||
|
constructor(private readonly aiService: AiService) {}
|
||||||
|
|
||||||
|
@Post('recommendations')
|
||||||
|
async getRecommendations(@Body() request: BidsRequestDto) {
|
||||||
|
return this.aiService.getRecommendations(request.bids);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/ai/ai.module.ts
Normal file
12
src/ai/ai.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { AiController } from './ai.controller';
|
||||||
|
import { AiService } from './ai.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
controllers: [AiController],
|
||||||
|
providers: [AiService],
|
||||||
|
exports: [AiService],
|
||||||
|
})
|
||||||
|
export class AiModule {}
|
||||||
@@ -9,6 +9,7 @@ import { KeywordsModule } from './keywords/keywords.module';
|
|||||||
import { CrawlerModule } from './crawler/crawler.module';
|
import { CrawlerModule } from './crawler/crawler.module';
|
||||||
import { TasksModule } from './schedule/schedule.module';
|
import { TasksModule } from './schedule/schedule.module';
|
||||||
import { LoggerModule } from './common/logger/logger.module';
|
import { LoggerModule } from './common/logger/logger.module';
|
||||||
|
import { AiModule } from './ai/ai.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,7 +17,7 @@ import { LoggerModule } from './common/logger/logger.module';
|
|||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: join(__dirname, '..', 'frontend', 'dist'),
|
rootPath: join(__dirname, '..', 'frontend', 'dist'),
|
||||||
exclude: ['/api*'],
|
exclude: ['/api/(.*)'],
|
||||||
}),
|
}),
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
@@ -24,6 +25,7 @@ import { LoggerModule } from './common/logger/logger.module';
|
|||||||
KeywordsModule,
|
KeywordsModule,
|
||||||
CrawlerModule,
|
CrawlerModule,
|
||||||
TasksModule,
|
TasksModule,
|
||||||
|
AiModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
Reference in New Issue
Block a user