mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-06 03:07:50 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fcd589482 | |||
| 7a34fc0079 | |||
| 6af713b67a | |||
| 6992b58208 | |||
| 1604decd3c | |||
| 98971588ae | |||
| 531844f33c | |||
| 76f00db86d | |||
| 761fad17bc | |||
| 0cab5c1cda | |||
| 9574b02d8a | |||
| c48c2b9143 | |||
| cdad76cd3b | |||
| d3e59bc7f3 |
+3
-4
@@ -37,9 +37,6 @@ MANIFEST
|
|||||||
*.manifest
|
*.manifest
|
||||||
*.spec
|
*.spec
|
||||||
|
|
||||||
# uv
|
|
||||||
*.lock
|
|
||||||
|
|
||||||
# Installer logs
|
# Installer logs
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
@@ -192,4 +189,6 @@ cython_debug/
|
|||||||
|
|
||||||
**/data/*
|
**/data/*
|
||||||
**/docker/*
|
**/docker/*
|
||||||
backend/app/static/*
|
backend/app/static/*
|
||||||
|
|
||||||
|
test*.*
|
||||||
@@ -1,2 +1,70 @@
|
|||||||
# InsightRadar
|
# 聚势智见 — 基于语义聚类与大模型的热点资讯聚合平台
|
||||||
An AI-powered trend monitoring and news intelligence platform
|
|
||||||
|
一个智能热点监测与个性化分发平台,通过语义聚类与大模型技术,将分散在微博、知乎、抖音、百度等平台的热点资讯自动归并为统一事件,生成AI摘要与标签,并支持个性化订阅与定时推送。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
- **跨平台热点聚合**:基于Embedding语义相似度计算,自动识别不同平台的同一事件
|
||||||
|
- **AI智能摘要**:调用大模型生成统一标题、综合摘要与标准化标签
|
||||||
|
- **个性化推荐**:支持关键词订阅、语义匹配与多因子排序
|
||||||
|
- **舆情分析工具**:提供热度趋势追踪、标题修改监控、时间线分析
|
||||||
|
- **定时简报推送**:自定义推送时间与接收邮箱,生成个性化AI简报
|
||||||
|
|
||||||
|
## 快速部署
|
||||||
|
|
||||||
|
### 方式一:Docker部署(推荐)
|
||||||
|
|
||||||
|
**环境要求**
|
||||||
|
- Linux系统(推荐Ubuntu 22.04 LTS / Debian 12)
|
||||||
|
- Docker ≥ 20.10.0,Docker Compose v2
|
||||||
|
- 内存 ≥ 512MB(建议1GB以上)
|
||||||
|
|
||||||
|
**部署步骤**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 构建镜像
|
||||||
|
docker build -t insightradar:latest .
|
||||||
|
|
||||||
|
# 2. 配置目录(参考docker/ereadm.txt)
|
||||||
|
mkdir -p ./data ./logs
|
||||||
|
|
||||||
|
# 3. 启动服务
|
||||||
|
cd docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:源码部署
|
||||||
|
|
||||||
|
**环境要求**
|
||||||
|
|
||||||
|
- Python ≥ 3.11,uv包管理器
|
||||||
|
- Node.js ≥ 22
|
||||||
|
- 内存 ≥ 512MB
|
||||||
|
|
||||||
|
**后端部署**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# 复制
|
||||||
|
cd backend
|
||||||
|
uv sync
|
||||||
|
uv run
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端部署**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
# 将dist/目录内容复制到 backend/app/static/
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置说明**
|
||||||
|
|
||||||
|
- 复制 .env.example 为 .env 并填写配置
|
||||||
|
- 将Embedding模型(Qwen3-Embedding-4B)放入 backend/data/ 目录
|
||||||
|
|
||||||
|
### 访问应用
|
||||||
|
|
||||||
|
部署完成后,通过 http://<服务器IP>:<配置端口> 访问Web界面。
|
||||||
@@ -24,6 +24,10 @@ def get_multi(db: Session, skip: int = 0, limit: int = 100) -> List[InfoSource]:
|
|||||||
def create(db: Session, obj_in: InfoSourceCreate) -> InfoSource:
|
def create(db: Session, obj_in: InfoSourceCreate) -> InfoSource:
|
||||||
"""创建新的信息源"""
|
"""创建新的信息源"""
|
||||||
db_obj = InfoSource(**obj_in.model_dump())
|
db_obj = InfoSource(**obj_in.model_dump())
|
||||||
|
exits =db.query(InfoSource).filter(InfoSource.source_name == db_obj.source_name).first()
|
||||||
|
if exits:
|
||||||
|
db.close()
|
||||||
|
return db_obj
|
||||||
try:
|
try:
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
+17
-5
@@ -1,10 +1,11 @@
|
|||||||
# app/main.py
|
# app/main.py
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from fastapi.responses import FileResponse
|
from pathlib import Path
|
||||||
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||||
import httpx
|
import httpx
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI, staticfiles
|
from fastapi import FastAPI, HTTPException, Request, staticfiles
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -112,14 +113,25 @@ app.add_middleware(
|
|||||||
# 版本控制
|
# 版本控制
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
app.include_router(api_router, prefix="/api/v1")
|
||||||
|
|
||||||
# 把目录改成static对应我们放dist内容的路径就可以
|
|
||||||
app.mount("/", staticfiles.StaticFiles(directory="app/static", html=True), name="static")
|
|
||||||
|
|
||||||
# 只需要保留API的优先匹配,catch_all可以简化成这样
|
# 只需要保留API的优先匹配,catch_all可以简化成这样
|
||||||
@app.get("/api/{full_path:path}")
|
@app.get("/api/{full_path:path}")
|
||||||
async def api_not_found(full_path: str):
|
async def api_not_found(full_path: str):
|
||||||
return {"detail": "API Not Found"}
|
return {"detail": "API Not Found"}
|
||||||
|
|
||||||
|
staticPath = staticfiles.StaticFiles(directory="app/static", html=True)
|
||||||
|
|
||||||
|
# 把目录改成static对应我们放dist内容的路径就可以
|
||||||
|
app.mount("/", staticPath, name="static")
|
||||||
|
|
||||||
|
INDEX_HTML = Path("app/static/index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
@app.exception_handler(404)
|
||||||
|
async def not_found_handler(request: Request, exc: HTTPException):
|
||||||
|
# 如果是API路径才返回404,前端路径走catch-all不会进这里
|
||||||
|
if request.url.path.startswith("/api/"):
|
||||||
|
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
||||||
|
return HTMLResponse(INDEX_HTML)
|
||||||
|
|
||||||
# 健康检查
|
# 健康检查
|
||||||
@app.get("/", tags=["健康检查"])
|
@app.get("/", tags=["健康检查"])
|
||||||
async def root():
|
async def root():
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ SIMILARITY_THRESHOLD = float(os.getenv("SIMILARITY_THRESHOLD", 0.72))
|
|||||||
API_BASE_URL = os.getenv("API_BASE_URL", "https://newsnow.busiyi.world/api/s")
|
API_BASE_URL = os.getenv("API_BASE_URL", "https://newsnow.busiyi.world/api/s")
|
||||||
EMBEDDING_MODEL_PATH = os.getenv("EMBEDDING_MODEL_PATH", "")
|
EMBEDDING_MODEL_PATH = os.getenv("EMBEDDING_MODEL_PATH", "")
|
||||||
|
|
||||||
print("正在加载 BAAI/bge-m3 向量模型...")
|
print("正在加载模型...")
|
||||||
# 全局单例
|
# 全局单例
|
||||||
embedder_model = SentenceTransformer(EMBEDDING_MODEL_PATH, local_files_only=True, device="cuda")
|
embedder_model = SentenceTransformer(EMBEDDING_MODEL_PATH, local_files_only=True)
|
||||||
print("模型加载完成。")
|
print("模型加载完成。")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -49,7 +49,6 @@ dependencies = [
|
|||||||
"safetensors==0.7.0",
|
"safetensors==0.7.0",
|
||||||
"scikit-learn==1.8.0",
|
"scikit-learn==1.8.0",
|
||||||
"scipy==1.17.1",
|
"scipy==1.17.1",
|
||||||
"sentence-transformers==5.2.3",
|
|
||||||
"shellingham==1.5.4",
|
"shellingham==1.5.4",
|
||||||
"sniffio==1.3.1",
|
"sniffio==1.3.1",
|
||||||
"sqlalchemy==2.0.48",
|
"sqlalchemy==2.0.48",
|
||||||
@@ -57,8 +56,6 @@ dependencies = [
|
|||||||
"sympy==1.14.0",
|
"sympy==1.14.0",
|
||||||
"threadpoolctl==3.6.0",
|
"threadpoolctl==3.6.0",
|
||||||
"tokenizers==0.22.2",
|
"tokenizers==0.22.2",
|
||||||
"torch==2.10.0",
|
|
||||||
"torchvision==0.25.0",
|
|
||||||
"tqdm==4.67.3",
|
"tqdm==4.67.3",
|
||||||
"transformers==5.3.0",
|
"transformers==5.3.0",
|
||||||
"typer==0.24.1",
|
"typer==0.24.1",
|
||||||
@@ -68,4 +65,16 @@ dependencies = [
|
|||||||
"tzlocal==5.3.1",
|
"tzlocal==5.3.1",
|
||||||
"urllib3==2.6.3",
|
"urllib3==2.6.3",
|
||||||
"uvicorn==0.41.0",
|
"uvicorn==0.41.0",
|
||||||
|
"torch==2.11.0+cpu",
|
||||||
|
"torchvision==0.26.0+cpu",
|
||||||
|
"torchaudio==2.11.0+cpu",
|
||||||
|
"sentence-transformers>=5.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "pytorch-cpu"
|
||||||
|
url = "https://download.pytorch.org/whl/cpu"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
index-strategy = "unsafe-best-match"
|
||||||
|
|||||||
Generated
+1720
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -20,7 +20,7 @@ WORKDIR /backend
|
|||||||
COPY backend/pyproject.toml backend/uv.lock ./
|
COPY backend/pyproject.toml backend/uv.lock ./
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
pip install --no-cache-dir uv && \
|
pip install --no-cache-dir uv && \
|
||||||
uv sync --frozen --no-dev
|
uv sync --frozen --no-dev --index https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||||
|
|
||||||
# 复制后端代码
|
# 复制后端代码
|
||||||
COPY backend/app ./app
|
COPY backend/app ./app
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const hoursOptions = [
|
|||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ label: '时间排序', value: 'created_at' },
|
{ label: '时间排序', value: 'created_at' },
|
||||||
{ label: '热度排序', value: 'hot_score' },
|
{ label: '热度排序', value: 'hot_score' },
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const recSortOptions = [
|
const recSortOptions = [
|
||||||
@@ -846,10 +846,10 @@ watch(() => route.query.event, (newId) => {
|
|||||||
<i class="fa-regular fa-clock"></i>
|
<i class="fa-regular fa-clock"></i>
|
||||||
最后同步: {{ lastSyncText }}
|
最后同步: {{ lastSyncText }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="stats.error_tasks_today > 0" class="error-count">
|
<!-- <span v-if="stats.error_tasks_today > 0" class="error-count">
|
||||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
{{ stats.error_tasks_today }} 个异常
|
{{ stats.error_tasks_today }} 个异常
|
||||||
</span>
|
</span> -->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -235,17 +235,17 @@ async function handleSearch() {
|
|||||||
<div class="tips-box glass-panel">
|
<div class="tips-box glass-panel">
|
||||||
<h2 class="panel-title"><i class="fa-regular fa-lightbulb"></i> 搜索建议</h2>
|
<h2 class="panel-title"><i class="fa-regular fa-lightbulb"></i> 搜索建议</h2>
|
||||||
<div class="tips-content">
|
<div class="tips-content">
|
||||||
<button class="tip-tag" @click="keyword='新能源汽车'; hours=168; handleSearch()">
|
<button class="tip-tag" @click="keyword='火箭发射'; hours=168; handleSearch()">
|
||||||
<i class="fa-solid fa-rocket"></i> 新能源汽车
|
<i class="fa-solid fa-rocket"></i> 火箭发射
|
||||||
</button>
|
</button>
|
||||||
<button class="tip-tag" @click="keyword='苹果公司'; hours=168; handleSearch()">
|
<button class="tip-tag" @click="keyword='苹果公司'; hours=168; handleSearch()">
|
||||||
<i class="fa-brands fa-apple"></i> 苹果产业链
|
<i class="fa-brands fa-apple"></i> 苹果公司
|
||||||
</button>
|
</button>
|
||||||
<button class="tip-tag regex-tag" @click="keyword='AI|LLM'; hours=168; handleSearch()">
|
<button class="tip-tag regex-tag" @click="keyword='AI|LLM'; hours=168; handleSearch()">
|
||||||
<i class="fa-solid fa-code-branch"></i> AI / 大模型
|
<i class="fa-solid fa-code-branch"></i> AI / 大模型
|
||||||
</button>
|
</button>
|
||||||
<button class="tip-tag regex-tag" @click="keyword='美国关税'; hours=168; handleSearch()">
|
<button class="tip-tag regex-tag" @click="keyword='美国'; hours=168; handleSearch()">
|
||||||
<i class="fa-solid fa-flag-usa"></i> 美国关税
|
<i class="fa-solid fa-flag-usa"></i> 美国
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,9 +261,15 @@ async function handleSearch() {
|
|||||||
<div v-else-if="searchResult" class="results-container">
|
<div v-else-if="searchResult" class="results-container">
|
||||||
<section class="chart-section glass-panel">
|
<section class="chart-section glass-panel">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2 class="section-title">
|
<div class="section-title-group">
|
||||||
<i class="fa-solid fa-wave-square"></i> 时间热度脉络
|
<h2 class="section-title">
|
||||||
</h2>
|
<i class="fa-solid fa-wave-square"></i> 时间热度脉络
|
||||||
|
</h2>
|
||||||
|
<span class="chart-tip">
|
||||||
|
<i class="fa-solid fa-hand-pointer"></i>
|
||||||
|
点击时间点查看具体事件列表
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<span class="meta-info">共 {{ searchResult.timeline.length }} 个时间节点 · 覆盖 {{ searchResult.events.length }} 个聚合事件</span>
|
<span class="meta-info">共 {{ searchResult.timeline.length }} 个时间节点 · 覆盖 {{ searchResult.events.length }} 个聚合事件</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -553,6 +559,30 @@ async function handleSearch() {
|
|||||||
color: var(--brand-primary);
|
color: var(--brand-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-title-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-tip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--brand-primary-alpha);
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
color: var(--brand-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-tip i {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.time-filter-badge {
|
.time-filter-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -599,6 +629,10 @@ async function handleSearch() {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-container :deep(.apexcharts-marker) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.events-section {
|
.events-section {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user