2026-09-28 15:48:35 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 简航 SimpleNav —— 搜索联想代理
|
|
|
|
|
|
* 浏览器直接调用各搜索引擎的联想接口会有跨域/JSONP 限制,这里由服务端代理并统一输出 JSON。
|
2026-09-28 17:13:10 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 各家联想接口在国内服务器上的可用性差别很大。2026-09-28 在本站服务器实测:
|
|
|
|
|
|
*
|
|
|
|
|
|
* 百度 sugrec?prod=pc&wd= 200 / 0.12s / 1137B ✅ 最快最稳 → 默认来源
|
|
|
|
|
|
* 360 sug.so.360.cn/suggest 200 / 0.20s / 714B ✅
|
|
|
|
|
|
* 必应 api.bing.com/osjson 200 / 0.5~1.6s / 408B ✅ 可用但明显慢
|
|
|
|
|
|
* 搜狗 suggnew/ajajjson 200 / **0 字节** ❌ 联想域名已不解析
|
|
|
|
|
|
* 谷歌 suggestqueries.google 连接超时 ❌ 国内不可达
|
|
|
|
|
|
* DuckDuckGo 连接超时 ❌ 国内不可达
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所以默认策略是「**联想词统一取百度,搜索仍走各自引擎**」:
|
|
|
|
|
|
* 点哪个引擎就用哪个引擎搜索,只是输入框下面的建议来自百度。这样任何一个引擎
|
|
|
|
|
|
* 都不会出现「下拉是空的」。想恢复「各引擎用自己的接口」,把设置里的
|
|
|
|
|
|
* 「联想词来源」改成 auto 即可(见 admin 设置页)。
|
|
|
|
|
|
*
|
2026-09-28 15:48:35 +08:00
|
|
|
|
* 用法:suggest.php?slug=bing&q=关键词
|
2026-09-28 17:13:10 +08:00
|
|
|
|
* slug = 当前选中的引擎(auto 模式下用它自己的接口;其它模式下只作为兜底候选)
|
2026-09-28 15:48:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
require_once __DIR__ . '/functions.php';
|
|
|
|
|
|
|
|
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
2026-09-28 16:43:10 +08:00
|
|
|
|
send_no_cache();
|
2026-09-28 15:48:35 +08:00
|
|
|
|
|
|
|
|
|
|
$slug = isset($_GET['slug']) ? preg_replace('/[^a-z0-9\-_]/i', '', (string)$_GET['slug']) : '';
|
2026-09-28 17:13:10 +08:00
|
|
|
|
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
|
2026-09-28 15:48:35 +08:00
|
|
|
|
|
|
|
|
|
|
if ($q === '') {
|
2026-09-28 17:13:10 +08:00
|
|
|
|
echo json_encode(['q' => '', 'items' => [], 'src' => ''], JSON_UNESCAPED_UNICODE);
|
2026-09-28 15:48:35 +08:00
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$items = [];
|
2026-09-28 17:13:10 +08:00
|
|
|
|
$used = '';
|
|
|
|
|
|
foreach (suggest_order($slug) as $engine) {
|
|
|
|
|
|
$items = fetch_suggest($engine, $q);
|
|
|
|
|
|
if ($items) {
|
|
|
|
|
|
$used = (string)$engine['slug'];
|
|
|
|
|
|
break;
|
2026-09-28 15:48:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 去重、限长、裁剪
|
|
|
|
|
|
$items = array_values(array_unique(array_filter(array_map('trim', $items))));
|
|
|
|
|
|
$items = array_slice($items, 0, 10);
|
|
|
|
|
|
|
2026-09-28 17:13:10 +08:00
|
|
|
|
echo json_encode(['q' => $q, 'items' => $items, 'src' => $used], JSON_UNESCAPED_UNICODE);
|
2026-09-28 15:48:35 +08:00
|
|
|
|
exit;
|
|
|
|
|
|
|
2026-09-28 17:13:10 +08:00
|
|
|
|
/* ========================================================================== */
|
|
|
|
|
|
|
|
|
|
|
|
/** 取一个引擎行(**不过滤 is_active** —— 作为联想来源的引擎可能在前台被隐藏了) */
|
|
|
|
|
|
function engine_row(string $slug): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
static $cache = [];
|
|
|
|
|
|
if ($slug === '') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!array_key_exists($slug, $cache)) {
|
|
|
|
|
|
$st = db()->prepare('SELECT * FROM engines WHERE slug=? LIMIT 1');
|
|
|
|
|
|
$st->execute([$slug]);
|
|
|
|
|
|
$r = $st->fetch();
|
|
|
|
|
|
$cache[$slug] = $r ?: null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $cache[$slug];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 抓一次联想并解析;任何异常都退化成空数组,绝不让下拉框卡住 */
|
|
|
|
|
|
function fetch_suggest(array $engine, string $q): array
|
|
|
|
|
|
{
|
|
|
|
|
|
if ((string)($engine['suggest_url'] ?? '') === '') {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
$u = str_replace('{q}', rawurlencode($q), (string)$engine['suggest_url']);
|
|
|
|
|
|
// 3 秒。既然有兜底链,宁快速失败也不要让输入框等太久。
|
|
|
|
|
|
$resp = http_get($u, 100000, 3);
|
|
|
|
|
|
if ($resp === null) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
return parse_suggest((string)$engine['suggest_type'], ensure_utf8((string)$resp['body']), $q);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 排出「先试谁、再试谁」的候选顺序。
|
|
|
|
|
|
*
|
|
|
|
|
|
* setting('suggest_source') = 'auto' → 先用当前引擎自己的接口,再走兜底链
|
|
|
|
|
|
* 其它值(默认 'baidu') → 统一用该来源,失败才走兜底链
|
|
|
|
|
|
*
|
|
|
|
|
|
* 兜底链固定为「百度 → 必应 → 360」(实测可用的三家,按快慢排序),
|
|
|
|
|
|
* 且只保留库里真实存在、且配了 suggest_url 的引擎。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function suggest_order(string $slug): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$src = trim(setting('suggest_source', 'baidu'));
|
|
|
|
|
|
$slugs = [];
|
|
|
|
|
|
|
|
|
|
|
|
if ($src === 'auto') {
|
|
|
|
|
|
if ($slug !== '') { $slugs[] = $slug; }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$slugs[] = $src;
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach (['baidu', 'bing', 'so360'] as $s) {
|
|
|
|
|
|
$slugs[] = $s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out = [];
|
|
|
|
|
|
foreach (array_unique($slugs) as $s) {
|
|
|
|
|
|
$row = engine_row($s);
|
|
|
|
|
|
if ($row !== null && (string)$row['suggest_url'] !== '') {
|
|
|
|
|
|
$out[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-28 15:48:35 +08:00
|
|
|
|
/** 按引擎类型解析联想结果 */
|
|
|
|
|
|
function parse_suggest(string $type, string $body, string $q): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$body = trim($body);
|
|
|
|
|
|
$out = [];
|
|
|
|
|
|
|
|
|
|
|
|
switch ($type) {
|
|
|
|
|
|
case 'bing_osjson':
|
|
|
|
|
|
case 'google_firefox':
|
|
|
|
|
|
// ["query", ["s1","s2",...]]
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j) && isset($j[1]) && is_array($j[1])) {
|
|
|
|
|
|
$out = $j[1];
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'baidu_sugrec':
|
|
|
|
|
|
// {"q":"x","g":[{"type":"..","q":".."}], ...}
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j)) {
|
|
|
|
|
|
if (!empty($j['g']) && is_array($j['g'])) {
|
|
|
|
|
|
foreach ($j['g'] as $it) { if (isset($it['q'])) $out[] = $it['q']; }
|
|
|
|
|
|
} elseif (!empty($j['s']) && is_array($j['s'])) {
|
|
|
|
|
|
$out = $j['s'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'so360':
|
|
|
|
|
|
// {"s":[".."]} 或 {"result":[{"word":".."}]}
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j)) {
|
|
|
|
|
|
if (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; }
|
|
|
|
|
|
elseif (!empty($j['result']) && is_array($j['result'])) {
|
|
|
|
|
|
foreach ($j['result'] as $it) { if (isset($it['word'])) $out[] = $it['word']; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'sogou_ajaj':
|
|
|
|
|
|
// {"query":"x","items":["a","b"]}(可能是 GBK,已在上面转码)
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j) && !empty($j['items']) && is_array($j['items'])) {
|
|
|
|
|
|
$out = $j['items'];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 兜底:正则抓引号字符串
|
|
|
|
|
|
if (preg_match_all('#"([^"]{1,40})"#u', $body, $m)) {
|
|
|
|
|
|
$out = array_slice($m[1], 1); // 跳过 query 自身
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'ddg':
|
|
|
|
|
|
// [{"phrase":".."},...]
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j)) {
|
|
|
|
|
|
foreach ($j as $it) {
|
|
|
|
|
|
if (is_array($it) && isset($it['phrase'])) { $out[] = $it['phrase']; }
|
|
|
|
|
|
elseif (is_string($it)) { $out[] = $it; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 通用兜底:尝试 JSON,再退化正则
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j)) {
|
|
|
|
|
|
if (isset($j[1]) && is_array($j[1])) { $out = $j[1]; }
|
|
|
|
|
|
elseif (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-28 17:13:10 +08:00
|
|
|
|
return is_array($out) ? $out : [];
|
2026-09-28 15:48:35 +08:00
|
|
|
|
}
|