feat: 添加网络搜索功能(Tavily Search)
- 后端:新增 search_config 表存储搜索接口配置 - 后端:实现 /api/search 和 /api/search/detail API - 前端:首页添加搜索配置入口(模态框) - 前端:主题详情页添加搜索面板(搜索框+结果列表+详情查看) - 搜索结果可一键添加为素材 - 支持搜索深度和时间范围筛选 - 更新 README 文档
This commit is contained in:
136
app.py
136
app.py
@@ -117,6 +117,12 @@ def init_db():
|
||||
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS search_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
provider TEXT DEFAULT 'tavily',
|
||||
api_key TEXT DEFAULT '',
|
||||
updated_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
''')
|
||||
# 初始化默认Prompt模板
|
||||
default_templates = [
|
||||
@@ -147,6 +153,9 @@ def init_db():
|
||||
conn.execute('ALTER TABLE materials ADD COLUMN selected INTEGER DEFAULT 1')
|
||||
except Exception:
|
||||
pass
|
||||
# 初始化默认搜索配置
|
||||
conn.execute('INSERT OR IGNORE INTO search_config (id, provider, api_key) VALUES (1, ?, ?)',
|
||||
('tavily', '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'))
|
||||
conn.close()
|
||||
|
||||
|
||||
@@ -724,6 +733,133 @@ def analyze_material(material_id):
|
||||
return jsonify({'analysis': analysis_text})
|
||||
|
||||
|
||||
# ==================== 搜索配置与搜索API ====================
|
||||
|
||||
def get_search_config():
|
||||
"""从数据库读取搜索配置"""
|
||||
conn = get_db()
|
||||
row = conn.execute('SELECT * FROM search_config WHERE id=1').fetchone()
|
||||
conn.close()
|
||||
if row and row['api_key']:
|
||||
return {'provider': row['provider'], 'api_key': row['api_key']}
|
||||
return {'provider': 'tavily', 'api_key': ''}
|
||||
|
||||
|
||||
@app.route('/api/search-config', methods=['GET'])
|
||||
def get_search_config_api():
|
||||
config = get_search_config()
|
||||
key = config['api_key']
|
||||
masked = key[:8] + '***' + key[-4:] if len(key) > 12 else ('***' if key else '')
|
||||
return jsonify({
|
||||
'provider': config['provider'],
|
||||
'api_key_masked': masked,
|
||||
'api_key_set': bool(key)
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/search-config', methods=['PUT'])
|
||||
def update_search_config():
|
||||
data = request.get_json(force=True)
|
||||
conn = get_db()
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
current = conn.execute('SELECT * FROM search_config WHERE id=1').fetchone()
|
||||
provider = data.get('provider', current['provider'] if current else 'tavily')
|
||||
api_key = data.get('api_key', '') or (current['api_key'] if current else '')
|
||||
conn.execute('''
|
||||
INSERT INTO search_config (id, provider, api_key, updated_at) VALUES (1, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET provider=excluded.provider, api_key=excluded.api_key, updated_at=excluded.updated_at
|
||||
''', (provider, api_key, now))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/search', methods=['POST'])
|
||||
def search_web():
|
||||
"""调用 Tavily Search API 进行搜索"""
|
||||
data = request.get_json(force=True)
|
||||
query = data.get('query', '').strip()
|
||||
if not query:
|
||||
return jsonify({'error': '搜索关键词不能为空'}), 400
|
||||
|
||||
search_config = get_search_config()
|
||||
if not search_config['api_key']:
|
||||
return jsonify({'error': '请先配置搜索API Key(首页右上角设置)'}), 400
|
||||
|
||||
max_results = data.get('max_results', 8)
|
||||
search_depth = data.get('search_depth', 'basic')
|
||||
time_range = data.get('time_range', None)
|
||||
|
||||
try:
|
||||
req_body = {
|
||||
'query': query,
|
||||
'max_results': max_results,
|
||||
'search_depth': search_depth,
|
||||
'include_raw_content': False,
|
||||
}
|
||||
if time_range:
|
||||
req_body['time_range'] = time_range
|
||||
|
||||
resp = requests.post(
|
||||
'https://api.tavily.com/search',
|
||||
headers={
|
||||
'Authorization': f"Bearer {search_config['api_key']}",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json=req_body,
|
||||
timeout=30
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
return jsonify(result)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
return jsonify({'error': f'搜索请求失败: HTTP {e.response.status_code}'}), 502
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'搜索失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.route('/api/search/detail', methods=['POST'])
|
||||
def search_detail():
|
||||
"""调用 Tavily Search API 获取详细内容(include_raw_content)"""
|
||||
data = request.get_json(force=True)
|
||||
query = data.get('query', '').strip()
|
||||
url = data.get('url', '').strip()
|
||||
if not query:
|
||||
return jsonify({'error': '搜索关键词不能为空'}), 400
|
||||
|
||||
search_config = get_search_config()
|
||||
if not search_config['api_key']:
|
||||
return jsonify({'error': '请先配置搜索API Key'}), 400
|
||||
|
||||
try:
|
||||
req_body = {
|
||||
'query': query,
|
||||
'max_results': 3,
|
||||
'search_depth': 'advanced',
|
||||
'include_raw_content': True,
|
||||
}
|
||||
if url:
|
||||
req_body['include_domains'] = [url.split('/')[2]] if '/' in url else [url]
|
||||
|
||||
resp = requests.post(
|
||||
'https://api.tavily.com/search',
|
||||
headers={
|
||||
'Authorization': f"Bearer {search_config['api_key']}",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json=req_body,
|
||||
timeout=45
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
# 如果指定了URL,过滤只返回该URL的结果
|
||||
if url:
|
||||
result['results'] = [r for r in result.get('results', []) if r.get('url') == url]
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'获取详情失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
# ==================== LLM配置API ====================
|
||||
|
||||
@app.route('/api/llm-config', methods=['GET'])
|
||||
|
||||
Reference in New Issue
Block a user