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:
@@ -9,6 +9,7 @@ import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
|
||||
import { mainWindow } from './main.js';
|
||||
import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js';
|
||||
import { getWorkspaceDir } from './workspace.js';
|
||||
import { getSetting } from './db/sqlite.js';
|
||||
|
||||
/** 当前工具命令进程(用于用户手动终止) */
|
||||
let _toolProc: ReturnType<typeof spawn> | null = null;
|
||||
@@ -435,9 +436,114 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
||||
return { success: false, error: lastError || '所有重试均失败' };
|
||||
}
|
||||
|
||||
/**
|
||||
* SearXNG 元搜索引擎 — JSON API 搜索
|
||||
* 支持 70+ 引擎聚合、隐私保护、无广告
|
||||
*/
|
||||
async function handleWebSearchSearxng(
|
||||
query: string,
|
||||
maxResults: number,
|
||||
timeRange: string,
|
||||
_enhanceSnippets: boolean
|
||||
): Promise<ToolResult> {
|
||||
const url = getSetting<string>('searxng_url', 'https://search.metona.cn');
|
||||
const engines = getSetting<string>('searxng_engines', 'google,bing,duckduckgo,wikipedia');
|
||||
const language = getSetting<string>('searxng_language', 'zh-CN');
|
||||
const safesearch = getSetting<number>('searxng_safesearch', 1);
|
||||
const cfgTimeRange = getSetting<string>('searxng_time_range', '');
|
||||
const effectiveTimeRange = timeRange || cfgTimeRange;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
format: 'json',
|
||||
});
|
||||
if (engines) params.set('engines', engines);
|
||||
if (language) params.set('language', language);
|
||||
if (safesearch > 0) params.set('safesearch', String(safesearch));
|
||||
if (effectiveTimeRange) params.set('time_range', effectiveTimeRange);
|
||||
|
||||
const authKey = getSetting<string>('searxng_auth_key', '');
|
||||
|
||||
const apiUrl = `${url.replace(/\/+$/, '')}/search?${params.toString()}`;
|
||||
sendLog('info', `🔍 SearXNG 搜索`, `"${query}" → ${url}`);
|
||||
|
||||
try {
|
||||
const headers = buildFetchHeaders(apiUrl, false);
|
||||
if (authKey) headers['Authorization'] = `Bearer ${authKey}`;
|
||||
const resp = await fetchWithTimeout(apiUrl, HTTP_TIMEOUT, headers);
|
||||
if (!resp || !resp.ok) {
|
||||
const fallbackUrl = encodeURIComponent(query);
|
||||
return {
|
||||
success: false,
|
||||
error: `SearXNG 搜索失败 (HTTP ${resp?.status || 'timeout'})。请检查 SearXNG 实例是否运行正常:${url}。你也可以在设置中禁用 SearXNG 回退到内置四引擎搜索。`,
|
||||
_searxng_fallback: `${url}/search?q=${fallbackUrl}`
|
||||
};
|
||||
}
|
||||
|
||||
const data = await resp.json() as {
|
||||
query: string;
|
||||
number_of_results?: number;
|
||||
results?: Array<{
|
||||
title: string;
|
||||
url: string;
|
||||
content: string;
|
||||
engine: string;
|
||||
score?: number;
|
||||
category?: string;
|
||||
}>;
|
||||
unresponsive_engines?: Array<[string, string]>;
|
||||
};
|
||||
|
||||
const rawResults = (data.results || []).slice(0, maxResults);
|
||||
if (rawResults.length === 0) {
|
||||
return { success: false, error: 'SearXNG 未返回任何结果,请尝试其他关键词或调整搜索引擎配置' };
|
||||
}
|
||||
|
||||
// 转换为兼容格式
|
||||
const results = rawResults.map(r => ({
|
||||
title: (r.title || '').replace(/<[^>]+>/g, '').trim(),
|
||||
url: r.url,
|
||||
snippet: (r.content || '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(),
|
||||
engine: r.engine || 'searxng',
|
||||
weight: 80,
|
||||
reachable: true,
|
||||
}));
|
||||
|
||||
const engineNames = [...new Set(rawResults.map(r => r.engine))].filter(Boolean);
|
||||
const unresponsive = data.unresponsive_engines || [];
|
||||
const engineStats: Record<string, string> = {};
|
||||
for (const engine of engineNames) {
|
||||
engineStats[engine] = `${rawResults.filter(r => r.engine === engine).length} 条`;
|
||||
}
|
||||
for (const [eng, reason] of unresponsive) {
|
||||
engineStats[eng] = `⚠️ ${reason}`;
|
||||
}
|
||||
|
||||
sendLog('success', `🔍 SearXNG 完成`, `${results.length} 条结果, 引擎: ${engineNames.join(', ')}`);
|
||||
|
||||
return buildSearchResponse(
|
||||
results.map(r => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
snippet: r.snippet,
|
||||
engine: `searxng:${r.engine}`,
|
||||
weight: 80,
|
||||
reachable: true,
|
||||
})),
|
||||
maxResults,
|
||||
false,
|
||||
engineStats
|
||||
);
|
||||
} catch (err) {
|
||||
sendLog('error', `🔍 SearXNG 异常`, (err as Error).message);
|
||||
return { success: false, error: `SearXNG 搜索异常: ${(err as Error).message}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Search — 联网搜索(并行多引擎 + LRU 缓存 + 智能排序 + 时间过滤 + 摘要增强)
|
||||
* 同时请求 Bing / 百度 / 搜狗 / 360搜索,智能排序后返回
|
||||
* 如果启用了 SearXNG,则使用 SearXNG JSON API 替代
|
||||
*/
|
||||
export async function handleWebSearch(params: { query: string; max_results?: number; time_range?: string; enhance_snippets?: boolean }): Promise<ToolResult> {
|
||||
try {
|
||||
@@ -449,6 +555,13 @@ export async function handleWebSearch(params: { query: string; max_results?: num
|
||||
const maxResults = Math.min(params.max_results || 15, 20);
|
||||
const timeRange = params.time_range || '';
|
||||
const enhanceSnippets = params.enhance_snippets !== false; // 默认开启
|
||||
|
||||
// ── SearXNG 模式:如果启用了 SearXNG,走 JSON API ──
|
||||
const searxngEnabled = getSetting<boolean>('searxng_enabled', false);
|
||||
if (searxngEnabled) {
|
||||
return handleWebSearchSearxng(query, maxResults, timeRange, enhanceSnippets);
|
||||
}
|
||||
|
||||
const cacheKey = `${query}|${maxResults}|${timeRange}`;
|
||||
|
||||
// ── 时间范围 → URL 参数映射 ──
|
||||
|
||||
@@ -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