feat: 新增360/搜狗/微信搜索提供商 + PG15→16升级 & 项目文档更新
- search_utils.py: 新增 _call_360/_call_sogou/_call_wechat HTML爬取函数 - initial_data.py: 种子数据新增三个搜索提供商(priority 3/4/5) - models.py: provider_type 注释补充新类型 - admin.html: 搜索提供商类型下拉框新增三个选项 - AGENTS.md/PROGRESS.md/README.md: PostgreSQL 15→16 - README.md: 移除硬编码数据库密码
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
- **Backend**: FastAPI 0.104 + SQLAlchemy 2.0 + PostgreSQL 15 (`yzr_nr`)
|
- **Backend**: FastAPI 0.104 + SQLAlchemy 2.0 + PostgreSQL 16 (`yzr_nr`)
|
||||||
- **Frontend**: Vue 3 (CDN, no build step) + Element Plus — static HTML served by FastAPI
|
- **Frontend**: Vue 3 (CDN, no build step) + Element Plus — static HTML served by FastAPI
|
||||||
- **Auth**: JWT (`python-jose` + bcrypt), default admin `admin/admin123`
|
- **Auth**: JWT (`python-jose` + bcrypt), default admin `admin/admin123`
|
||||||
- **Scheduler**: APScheduler (daily cron: 01:00 searchcache, 01:10 trends, 01:30 collect, 02:00 generate, 03:00 optimize, 05:00 sources, 06:00 metrics)
|
- **Scheduler**: APScheduler (daily cron: 01:00 searchcache, 01:10 trends, 01:30 collect, 02:00 generate, 03:00 optimize, 05:00 sources, 06:00 metrics)
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@
|
|||||||
| 项目 | 内容 |
|
| 项目 | 内容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 品牌 | 宇之然 — 科技沟通万物,愿世间百态回归自然 |
|
| 品牌 | 宇之然 — 科技沟通万物,愿世间百态回归自然 |
|
||||||
| 技术栈 | FastAPI + SQLAlchemy + PostgreSQL 15 + Vue 3 (CDN) + Element Plus |
|
| 技术栈 | FastAPI + SQLAlchemy + PostgreSQL 16 + Vue 3 (CDN) + Element Plus |
|
||||||
| 平台服务 | 运行中 (端口 8001) |
|
| 平台服务 | 运行中 (端口 8001) |
|
||||||
| 策略阶段 | 全球-本土对比研究(2026-04-15 升级) |
|
| 策略阶段 | 全球-本土对比研究(2026-04-15 升级) |
|
||||||
| Git 提交 | 99 commits · 4 tags (v1.0.0~v1.0.4) · main 分支 |
|
| Git 提交 | 99 commits · 4 tags (v1.0.0~v1.0.4) · main 分支 |
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
| 后端 API (auth/topics/articles/publishing/calendar/metrics/assets/tasks/platform_config/admin) | ✅ 完成 | 核心 11 个 API 模块,JWT 认证 |
|
| 后端 API (auth/topics/articles/publishing/calendar/metrics/assets/tasks/platform_config/admin) | ✅ 完成 | 核心 11 个 API 模块,JWT 认证 |
|
||||||
| 扩展 API (cases/audit/llm_configs/system_configs/optimizer_logs/task_logs/task_configs) | ✅ 完成 | 新增案例库、审计、LLM配置、任务配置、任务运行记录模块 |
|
| 扩展 API (cases/audit/llm_configs/system_configs/optimizer_logs/task_logs/task_configs) | ✅ 完成 | 新增案例库、审计、LLM配置、任务配置、任务运行记录模块 |
|
||||||
| 前端页面 (仪表盘/选题/日历/数据/素材/任务/平台/系统管理/用户/日志/文章) | ✅ 完成 | Vue 3 + Element Plus SPA |
|
| 前端页面 (仪表盘/选题/日历/数据/素材/任务/平台/系统管理/用户/日志/文章) | ✅ 完成 | Vue 3 + Element Plus SPA |
|
||||||
| 数据库 (PostgreSQL 15) | ✅ 运行中 | `yzr_nr` 库 |
|
| 数据库 (PostgreSQL 16) | ✅ 运行中 | `yzr_nr` 库 |
|
||||||
| 服务 | ✅ 运行中 | 端口 8001 |
|
| 服务 | ✅ 运行中 | 端口 8001 |
|
||||||
| 数据库迁移 (SQLite→PostgreSQL) | ✅ 完成 | 2026-05-08 |
|
| 数据库迁移 (SQLite→PostgreSQL) | ✅ 完成 | 2026-05-08 |
|
||||||
| PWA 离线支持 | ✅ 完成 | manifest.json + Service Worker + 离线页面 |
|
| PWA 离线支持 | ✅ 完成 | manifest.json + Service Worker + 离线页面 |
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 后端 | FastAPI 0.104+ |
|
| 后端 | FastAPI 0.104+ |
|
||||||
| ORM | SQLAlchemy 2.0+ |
|
| ORM | SQLAlchemy 2.0+ |
|
||||||
| 数据库 | PostgreSQL 15 |
|
| 数据库 | PostgreSQL 16 |
|
||||||
| 前端 | Vue 3 (CDN) + Element Plus |
|
| 前端 | Vue 3 (CDN) + Element Plus |
|
||||||
| 认证 | JWT (python-jose) + bcrypt |
|
| 认证 | JWT (python-jose) + bcrypt |
|
||||||
|
|
||||||
@@ -24,14 +24,7 @@
|
|||||||
|
|
||||||
## 数据库配置
|
## 数据库配置
|
||||||
|
|
||||||
```
|
详见 `platform/backend/.env`
|
||||||
USE_POSTGRES=true
|
|
||||||
PG_HOST=127.0.0.1
|
|
||||||
PG_PORT=5432
|
|
||||||
PG_DATABASE=yzr_nr
|
|
||||||
PG_USER=yzr_nr
|
|
||||||
PG_PASSWORD=aTX3WKKnPfRnM5PC
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ def import_initial_data():
|
|||||||
providers = [
|
providers = [
|
||||||
SearchProvider(name="百度千帆", provider_type="baidu", api_key="", api_url="https://qianfan.baidubce.com/v2/ai_search/web_search", console_url="https://console.bce.baidu.com/qianfan/ais/console/onlineService", priority=1, enabled=True, daily_limit=50),
|
SearchProvider(name="百度千帆", provider_type="baidu", api_key="", api_url="https://qianfan.baidubce.com/v2/ai_search/web_search", console_url="https://console.bce.baidu.com/qianfan/ais/console/onlineService", priority=1, enabled=True, daily_limit=50),
|
||||||
SearchProvider(name="opencode云搜索", provider_type="mcp", api_key="", api_url="", console_url="https://opencode.ai", priority=2, enabled=True, daily_limit=99999),
|
SearchProvider(name="opencode云搜索", provider_type="mcp", api_key="", api_url="", console_url="https://opencode.ai", priority=2, enabled=True, daily_limit=99999),
|
||||||
|
SearchProvider(name="360搜索", provider_type="360", api_key="", api_url="", console_url="https://www.so.com", priority=3, enabled=True, daily_limit=200),
|
||||||
|
SearchProvider(name="搜狗搜索", provider_type="sogou", api_key="", api_url="", console_url="https://sogou.com", priority=4, enabled=True, daily_limit=200),
|
||||||
|
SearchProvider(name="微信搜一搜", provider_type="wechat", api_key="", api_url="", console_url="https://wx.sogou.com/weixin", priority=5, enabled=True, daily_limit=200),
|
||||||
]
|
]
|
||||||
for p in providers:
|
for p in providers:
|
||||||
db.add(p)
|
db.add(p)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class SearchProvider(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
name = Column(String, nullable=False, comment="显示名称")
|
name = Column(String, nullable=False, comment="显示名称")
|
||||||
provider_type = Column(String, nullable=False, comment="baidu / qiniu / tinyfish")
|
provider_type = Column(String, nullable=False, comment="baidu / qiniu / tinyfish / bing / mcp / 360 / sogou / wechat")
|
||||||
api_key = Column(String, nullable=True, comment="API密钥")
|
api_key = Column(String, nullable=True, comment="API密钥")
|
||||||
api_url = Column(String, nullable=True, comment="API地址")
|
api_url = Column(String, nullable=True, comment="API地址")
|
||||||
priority = Column(Integer, default=1, comment="优先级,越小越优先")
|
priority = Column(Integer, default=1, comment="优先级,越小越优先")
|
||||||
|
|||||||
@@ -557,6 +557,9 @@
|
|||||||
<el-select v-model="searchProviderForm.provider_type" style="width:100%">
|
<el-select v-model="searchProviderForm.provider_type" style="width:100%">
|
||||||
<el-option label="百度千帆" value="baidu"></el-option>
|
<el-option label="百度千帆" value="baidu"></el-option>
|
||||||
<el-option label="opencode云搜索" value="mcp"></el-option>
|
<el-option label="opencode云搜索" value="mcp"></el-option>
|
||||||
|
<el-option label="360搜索" value="360"></el-option>
|
||||||
|
<el-option label="搜狗搜索" value="sogou"></el-option>
|
||||||
|
<el-option label="微信搜一搜" value="wechat"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item></el-col>
|
</el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
+106
-1
@@ -205,12 +205,116 @@ def _call_mcp(api_key: str, api_url: str, query: str, max_results: int) -> List[
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _call_360(api_key: str, api_url: str, query: str, max_results: int) -> List[Dict]:
|
||||||
|
"""360搜索(HTML爬取,无需 API Key)"""
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
try:
|
||||||
|
resp = requests.get(
|
||||||
|
api_url or "https://www.so.com/s",
|
||||||
|
params={"q": query},
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
logger.warning("360搜索返回 %s", resp.status_code)
|
||||||
|
return []
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
results = []
|
||||||
|
for item in soup.select("li.res-list, li[class*=result], .rb"):
|
||||||
|
title_el = item.select_one("h3.res-title a, h3[class*=title] a, .res-title a")
|
||||||
|
if not title_el:
|
||||||
|
continue
|
||||||
|
title = title_el.get_text(strip=True)[:120]
|
||||||
|
url = title_el.get("href", "")
|
||||||
|
snippet_el = item.select_one("p.res-desc, p[class*=desc], .res-desc")
|
||||||
|
snippet = snippet_el.get_text(strip=True)[:300] if snippet_el else ""
|
||||||
|
results.append({"title": title, "url": url, "content": snippet, "source": "360"})
|
||||||
|
if len(results) >= max_results:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("360搜索失败: %s", e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _call_sogou(api_key: str, api_url: str, query: str, max_results: int) -> List[Dict]:
|
||||||
|
"""搜狗搜索(HTML爬取,无需 API Key)"""
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
try:
|
||||||
|
resp = requests.get(
|
||||||
|
api_url or "https://sogou.com/web",
|
||||||
|
params={"query": query},
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
logger.warning("搜狗搜索返回 %s", resp.status_code)
|
||||||
|
return []
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
results = []
|
||||||
|
for item in soup.select("div.vrwrap, div[class*=vr], .rb"):
|
||||||
|
title_el = item.select_one("h3.vr-title a, h3[class*=title] a, .vr-title a")
|
||||||
|
if not title_el:
|
||||||
|
continue
|
||||||
|
title = title_el.get_text(strip=True)[:120]
|
||||||
|
url = title_el.get("href", "")
|
||||||
|
snippet_el = item.select_one("p.str-text, div.str-text, p[class*=str]")
|
||||||
|
snippet = snippet_el.get_text(strip=True)[:300] if snippet_el else ""
|
||||||
|
results.append({"title": title, "url": url, "content": snippet, "source": "sogou"})
|
||||||
|
if len(results) >= max_results:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("搜狗搜索失败: %s", e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _call_wechat(api_key: str, api_url: str, query: str, max_results: int) -> List[Dict]:
|
||||||
|
"""微信搜一搜(通过搜狗抓取,无需 API Key)"""
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
try:
|
||||||
|
resp = requests.get(
|
||||||
|
api_url or "https://wx.sogou.com/weixin",
|
||||||
|
params={"type": 2, "query": query},
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
logger.warning("微信搜索返回 %s", resp.status_code)
|
||||||
|
return []
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
results = []
|
||||||
|
for item in soup.select("div.news-box, li.news-list, div[class*=news]"):
|
||||||
|
title_el = item.select_one("h3 a, .txt-box h3 a")
|
||||||
|
if not title_el:
|
||||||
|
continue
|
||||||
|
title = title_el.get_text(strip=True)[:120]
|
||||||
|
url = title_el.get("href", "")
|
||||||
|
if url and not url.startswith("http"):
|
||||||
|
url = "https://wx.sogou.com" + url
|
||||||
|
snippet_el = item.select_one("p.txt-info, div.txt-info, .txt-info")
|
||||||
|
snippet = snippet_el.get_text(strip=True)[:300] if snippet_el else ""
|
||||||
|
results.append({"title": title, "url": url, "content": snippet, "source": "wechat"})
|
||||||
|
if len(results) >= max_results:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("微信搜索失败: %s", e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
_PROVIDER_CALLS = {
|
_PROVIDER_CALLS = {
|
||||||
"baidu": _call_baidu,
|
"baidu": _call_baidu,
|
||||||
"qiniu": _call_qiniu,
|
"qiniu": _call_qiniu,
|
||||||
"tinyfish": _call_tinyfish,
|
"tinyfish": _call_tinyfish,
|
||||||
"bing": _call_bing,
|
"bing": _call_bing,
|
||||||
"mcp": _call_mcp,
|
"mcp": _call_mcp,
|
||||||
|
"360": _call_360,
|
||||||
|
"sogou": _call_sogou,
|
||||||
|
"wechat": _call_wechat,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -221,7 +325,8 @@ def search(query: str, max_results: int = 5) -> List[Dict]:
|
|||||||
if (p.get("usage_today") or 0) >= (p.get("daily_limit") or 99999):
|
if (p.get("usage_today") or 0) >= (p.get("daily_limit") or 99999):
|
||||||
logger.info("提供商 %s 已达日限 %s,跳过", p.get("name"), p.get("daily_limit"))
|
logger.info("提供商 %s 已达日限 %s,跳过", p.get("name"), p.get("daily_limit"))
|
||||||
continue
|
continue
|
||||||
if not p.get("api_key") and p.get("provider_type") != "mcp":
|
no_key_types = {"mcp", "360", "sogou", "wechat"}
|
||||||
|
if not p.get("api_key") and p.get("provider_type") not in no_key_types:
|
||||||
logger.info("提供商 %s 未配置 API Key,跳过", p.get("name"))
|
logger.info("提供商 %s 未配置 API Key,跳过", p.get("name"))
|
||||||
continue
|
continue
|
||||||
call_fn = _PROVIDER_CALLS.get(p.get("provider_type"))
|
call_fn = _PROVIDER_CALLS.get(p.get("provider_type"))
|
||||||
|
|||||||
Reference in New Issue
Block a user