feat: 集成 SearXNG 元搜索引擎,支持 JSON API 搜索替代四引擎 HTML 解析
- 新增 SearXNG 配置模态框,支持 API 地址、引擎列表、语言、安全搜索等参数
- 顶部添加独立 🔍 搜索按钮,带启用状态小绿点
- 启用后 web_search 走 SearXNG JSON API(70+ 引擎聚合)
- 禁用时回退原有 Bing+百度+搜狗+360 四引擎 HTML 解析方案
- 认证 Key 通过 HTTP Header Authorization: Bearer 传递,非必填
- 配置持久化到 SQLite settings 表
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* SearXNG 配置模态框
|
||||
* 配置 SearXNG 元搜索引擎的 API 地址和参数,支持启用/禁用切换
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import type { ChatDB } from '../db/chat-db.js';
|
||||
import { logInfo, logSuccess } from '../services/log-service.js';
|
||||
|
||||
/** 默认 SearXNG 配置 */
|
||||
const DEFAULTS = {
|
||||
enabled: false,
|
||||
url: 'https://search.metona.cn',
|
||||
engines: 'google,bing,duckduckgo,wikipedia',
|
||||
language: 'zh-CN',
|
||||
safesearch: 1,
|
||||
time_range: '',
|
||||
max_results: 20,
|
||||
auth_key: '',
|
||||
};
|
||||
|
||||
let modalEl: HTMLElement;
|
||||
let initialized = false;
|
||||
|
||||
/** SearXNG 运行时配置缓存(主进程搜索时通过 IPC 读取,渲染进程通过此变量快速访问) */
|
||||
export let searxngConfig = { ...DEFAULTS };
|
||||
|
||||
/** 写入配置到 SQLite + 更新运行时缓存 */
|
||||
async function saveConfig(db: ChatDB): Promise<void> {
|
||||
await db.saveSetting('searxng_enabled', searxngConfig.enabled);
|
||||
await db.saveSetting('searxng_url', searxngConfig.url);
|
||||
await db.saveSetting('searxng_engines', searxngConfig.engines);
|
||||
await db.saveSetting('searxng_language', searxngConfig.language);
|
||||
await db.saveSetting('searxng_safesearch', searxngConfig.safesearch);
|
||||
await db.saveSetting('searxng_time_range', searxngConfig.time_range);
|
||||
await db.saveSetting('searxng_max_results', searxngConfig.max_results);
|
||||
await db.saveSetting('searxng_auth_key', searxngConfig.auth_key);
|
||||
logSuccess('SearXNG 配置已保存');
|
||||
}
|
||||
|
||||
/** 从 SQLite 加载配置到运行时缓存 */
|
||||
export async function loadSearxngConfig(db: ChatDB): Promise<void> {
|
||||
searxngConfig.enabled = await db.getSetting('searxng_enabled', DEFAULTS.enabled);
|
||||
searxngConfig.url = await db.getSetting('searxng_url', DEFAULTS.url);
|
||||
searxngConfig.engines = await db.getSetting('searxng_engines', DEFAULTS.engines);
|
||||
searxngConfig.language = await db.getSetting('searxng_language', DEFAULTS.language);
|
||||
searxngConfig.safesearch = await db.getSetting('searxng_safesearch', DEFAULTS.safesearch);
|
||||
searxngConfig.time_range = await db.getSetting('searxng_time_range', DEFAULTS.time_range);
|
||||
searxngConfig.max_results = await db.getSetting('searxng_max_results', DEFAULTS.max_results);
|
||||
searxngConfig.auth_key = await db.getSetting('searxng_auth_key', DEFAULTS.auth_key);
|
||||
logInfo('SearXNG 配置已加载', searxngConfig.enabled ? `已启用: ${searxngConfig.url}` : '未启用');
|
||||
}
|
||||
|
||||
/** 刷新 UI 控件以匹配当前配置 */
|
||||
function syncFormFromConfig(): void {
|
||||
(document.querySelector('#searxngUrl') as HTMLInputElement).value = searxngConfig.url;
|
||||
(document.querySelector('#searxngEngines') as HTMLInputElement).value = searxngConfig.engines;
|
||||
(document.querySelector('#searxngLanguage') as HTMLSelectElement).value = searxngConfig.language;
|
||||
(document.querySelector('#searxngSafeSearch') as HTMLSelectElement).value = String(searxngConfig.safesearch);
|
||||
(document.querySelector('#searxngTimeRange') as HTMLSelectElement).value = searxngConfig.time_range || '';
|
||||
(document.querySelector('#searxngMaxResults') as HTMLInputElement).value = String(searxngConfig.max_results);
|
||||
(document.querySelector('#searxngAuthKey') as HTMLInputElement).value = searxngConfig.auth_key;
|
||||
const toggle = document.querySelector('#toggleSearxngEnabled') as HTMLInputElement;
|
||||
toggle.checked = searxngConfig.enabled;
|
||||
updateEnabledUI(searxngConfig.enabled);
|
||||
}
|
||||
|
||||
/** 启用/禁用状态切换 UI */
|
||||
function updateEnabledUI(enabled: boolean): void {
|
||||
const badge = document.querySelector('#searxngBadge') as HTMLElement;
|
||||
const toggle = document.querySelector('#toggleSearxngEnabled') as HTMLInputElement;
|
||||
const iconDot = document.querySelector('#searxngIconDot') as HTMLElement;
|
||||
if (badge) {
|
||||
badge.textContent = enabled ? '🟢 已启用' : '⚪ 未启用';
|
||||
badge.className = enabled ? 'searxng-badge enabled' : 'searxng-badge';
|
||||
}
|
||||
if (toggle) toggle.checked = enabled;
|
||||
if (iconDot) {
|
||||
iconDot.className = enabled ? 'searxng-icon-dot enabled' : 'searxng-icon-dot';
|
||||
}
|
||||
}
|
||||
|
||||
export function initSearxngModal(): void {
|
||||
if (initialized) return;
|
||||
modalEl = document.querySelector('#searxngModal') as HTMLElement;
|
||||
|
||||
// ── 打开模态框 ──
|
||||
document.querySelector('#btnSearxng')!.addEventListener('click', async () => {
|
||||
// 重新加载配置(可能在别处被修改)
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await loadSearxngConfig(db);
|
||||
syncFormFromConfig();
|
||||
modalEl.style.display = '';
|
||||
});
|
||||
|
||||
// ── 关闭模态框 ──
|
||||
document.querySelector('#btnCloseSearxng')!.addEventListener('click', () => {
|
||||
modalEl.style.display = 'none';
|
||||
});
|
||||
modalEl.addEventListener('click', (e) => {
|
||||
if (e.target === modalEl) modalEl.style.display = 'none';
|
||||
});
|
||||
|
||||
// ── 启用/禁用开关 ──
|
||||
document.querySelector('#toggleSearxngEnabled')!.addEventListener('change', async (e) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
searxngConfig.enabled = checked;
|
||||
updateEnabledUI(checked);
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('searxng_enabled', checked);
|
||||
logInfo('SearXNG', checked ? '已启用' : '已禁用');
|
||||
});
|
||||
|
||||
// ── 保存按钮 ──
|
||||
document.querySelector('#btnSaveSearxng')!.addEventListener('click', async () => {
|
||||
searxngConfig.url = (document.querySelector('#searxngUrl') as HTMLInputElement).value.trim();
|
||||
searxngConfig.engines = (document.querySelector('#searxngEngines') as HTMLInputElement).value.trim();
|
||||
searxngConfig.language = (document.querySelector('#searxngLanguage') as HTMLSelectElement).value;
|
||||
searxngConfig.safesearch = parseInt((document.querySelector('#searxngSafeSearch') as HTMLSelectElement).value);
|
||||
searxngConfig.time_range = (document.querySelector('#searxngTimeRange') as HTMLSelectElement).value;
|
||||
searxngConfig.max_results = parseInt((document.querySelector('#searxngMaxResults') as HTMLInputElement).value) || 20;
|
||||
searxngConfig.auth_key = (document.querySelector('#searxngAuthKey') as HTMLInputElement).value.trim();
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await saveConfig(db);
|
||||
updateEnabledUI(searxngConfig.enabled);
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/** 外部调用关闭模态框(ESC 键等) */
|
||||
export function closeSearxngModal(): void {
|
||||
if (modalEl) modalEl.style.display = 'none';
|
||||
}
|
||||
@@ -72,6 +72,12 @@
|
||||
<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn searxng-btn" id="btnSearxng" title="SearXNG 搜索配置">
|
||||
<span class="searxng-icon-dot" id="searxngIconDot"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -699,6 +705,100 @@
|
||||
<!-- ═══════════════ Toast 通知容器 ═══════════════ -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- ═══════════════ SearXNG 配置模态框 ═══════════════ -->
|
||||
<div class="modal-overlay" id="searxngModal" style="display:none;">
|
||||
<div class="modal" style="max-width:640px;">
|
||||
<div class="modal-header">
|
||||
<h3>🔍 SearXNG 元搜索引擎</h3>
|
||||
<button class="icon-btn" id="btnCloseSearxng">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">
|
||||
<span class="searxng-badge" id="searxngBadge">⚪ 未启用</span>
|
||||
<label class="toggle-label" style="margin-left:auto;display:inline-flex;">
|
||||
<input type="checkbox" id="toggleSearxngEnabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<p class="text-muted" style="font-size:11px;margin-top:4px;">
|
||||
启用后,联网搜索将使用 SearXNG 元搜索引擎(隐私保护、70+ 引擎聚合),替代内置的四引擎 HTML 解析方案。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">SearXNG API 地址</label>
|
||||
<input class="setting-input" id="searxngUrl" type="url" placeholder="https://search.metona.cn" style="margin-bottom:6px;">
|
||||
<p class="text-muted" style="font-size:11px;">SearXNG 实例的根地址,无需带 /search 路径</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">搜索引擎</label>
|
||||
<input class="setting-input" id="searxngEngines" type="text" placeholder="google,bing,duckduckgo,wikipedia" style="margin-bottom:6px;">
|
||||
<p class="text-muted" style="font-size:11px;">逗号分隔的引擎列表。常用: google, bing, duckduckgo, wikipedia, brave, qwant, startpage</p>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">语言</label>
|
||||
<select class="setting-input" id="searxngLanguage" style="margin-bottom:0;">
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="zh-TW">繁體中文</option>
|
||||
<option value="en">English</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="ko">한국어</option>
|
||||
<option value="">自动检测</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">安全搜索</label>
|
||||
<select class="setting-input" id="searxngSafeSearch" style="margin-bottom:0;">
|
||||
<option value="0">关闭</option>
|
||||
<option value="1">中等</option>
|
||||
<option value="2">严格</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">时间范围</label>
|
||||
<select class="setting-input" id="searxngTimeRange" style="margin-bottom:0;">
|
||||
<option value="">不限</option>
|
||||
<option value="day">最近一天</option>
|
||||
<option value="week">最近一周</option>
|
||||
<option value="month">最近一月</option>
|
||||
<option value="year">最近一年</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">单次结果数</label>
|
||||
<input class="setting-input" id="searxngMaxResults" type="number" min="1" max="50" step="1" value="20" style="margin-bottom:0;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">🔑 认证 Key(非必填)</label>
|
||||
<input class="setting-input" id="searxngAuthKey" type="password" placeholder="留空则不发送认证" style="margin-bottom:6px;">
|
||||
<p class="text-muted" style="font-size:11px;">SearXNG 实例的 API 认证密钥,通过 HTTP Header <code>Authorization: Bearer xxx</code> 传递。需配合 Nginx 反向代理验证</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<div style="flex:1;"></div>
|
||||
<button class="btn btn-primary" id="btnSaveSearxng">💾 保存配置</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;padding:12px;background:var(--bg-layer);border-radius:var(--radius-md);">
|
||||
<p style="font-size:11px;color:var(--text-secondary);margin:0;">
|
||||
💡 <strong>提示:</strong>SearXNG 是开源元搜索引擎,不追踪用户、不记录隐私。
|
||||
需要自行部署或使用公共实例。API 端点格式:<code>{{URL}}/search?q={{query}}&format=json</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════ 应用入口 ════════════════ -->
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { initToolConfirmModal } from './components/tool-confirm-modal.js';
|
||||
import { initWorkspacePanel, clearToolCardsExternal, clearTerminalExternal, switchToTab } from './components/workspace-panel.js';
|
||||
import { initLogPanel, addLog } from './services/log-service.js';
|
||||
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
|
||||
import { initSearxngModal, closeSearxngModal, loadSearxngConfig } from './components/searxng-modal.js';
|
||||
import type { ChatSession } from './types.js';
|
||||
|
||||
// ─── v4.0 数据迁移:IndexedDB → SQLite ───
|
||||
@@ -337,12 +338,14 @@ async function init(): Promise<void> {
|
||||
initTokenDashboard();
|
||||
initWorkspacePanel();
|
||||
setupDesktopIntegration();
|
||||
initSearxngModal();
|
||||
|
||||
bindGlobalEvents();
|
||||
|
||||
await checkConnection();
|
||||
await loadModels();
|
||||
await initMemoryManager();
|
||||
await loadSearxngConfig(db);
|
||||
logInit('所有组件已就绪');
|
||||
|
||||
const savedModel = await db.getSetting('selectedModel', '');
|
||||
@@ -458,6 +461,7 @@ function bindGlobalEvents(): void {
|
||||
closeLightbox();
|
||||
closeToolsModal();
|
||||
closeTokenDashboard();
|
||||
closeSearxngModal();
|
||||
(document.querySelector('#helpModal') as HTMLElement).style.display = 'none';
|
||||
(document.querySelector('#memoryModal') as HTMLElement).style.display = 'none';
|
||||
}
|
||||
|
||||
@@ -1983,6 +1983,41 @@ html, body {
|
||||
.messages { max-width: 900px; }
|
||||
}
|
||||
|
||||
/* ═══ SearXNG 搜索配置 ═══ */
|
||||
.searxng-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.searxng-icon-dot {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-disabled);
|
||||
transition: var(--transition);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.searxng-icon-dot.enabled {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 6px rgba(74, 174, 107, 0.5);
|
||||
}
|
||||
|
||||
.searxng-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.searxng-badge.enabled {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
/* ── 选中文本 ── */
|
||||
::selection {
|
||||
background: rgba(232, 115, 74, 0.2);
|
||||
|
||||
Reference in New Issue
Block a user