Files
SimpleNav/suggest.php
T
laoyang 34473d0120 fix: 联想建议词改成「统一来源 + 自动兜底」,任何引擎都能出词
问题:除百度/360 外,其它引擎的联想下拉基本是空的。

实测(2026-09-28 在站点服务器上逐个探测):
  百度 sugrec        200 / 0.12s / 1137B  ✅ 最快最稳
  360 sug.so.360.cn  200 / 0.20s /  714B  ✅
  必应 api.bing.com  200 / 0.5~1.6s      ✅ 可用但明显慢
  搜狗 ajajjson      200 / **0 字节**     ❌ 联想域名 w.sugg.sogou.com 已不解析
  谷歌 suggestqueries  连接超时            ❌ 国内不可达
  DuckDuckGo          连接超时            ❌ 国内不可达

按「联想统一取百度、搜索仍走各自引擎」实现:
- 新增设置 suggest_source(设置页可选 百度/必应/360/各引擎自己的接口),默认百度。
- 任何情况下都带自动兜底链:首选来源取不到词(接口挂/超时/返回空)
  就依次回退 百度 → 必应 → 360;单个候选 3 秒超时,绝不让下拉框卡住。
- 候选来源不过滤 is_active —— 作为联想来源的引擎可能在前台被隐藏。
- 响应里带 src 字段标明这次实际用的是谁,便于排查。

顺带修一个更隐蔽的 bug:前台 <form id="search-form"> 原本**没有 data-slug**,
而 JS 只在「点击引擎标签」时才把它写上 → 一进站、没点过引擎时 slug 为空,
suggest.php 查不到引擎,联想**全都不出词**。现在表单自带默认引擎的 slug。

其它:
- 清掉种子里已失效的搜狗联想地址;加一条幂等迁移,只匹配默认种子值,
  用户自己改过的地址不动。谷歌/DDG 也不再预设(前端不可达)。
- app.js:请求加序号,防止先发的慢响应覆盖后发的快响应;切换引擎时
  若输入框已有内容就顺带换一份建议。
- e2e 新增「联想来源」场景(本地桩服务器 + 独立端口,不依赖外网):
  8 条断言覆盖 默认来源 / auto 模式 / 回退 / 空关键词 / 响应头。

验证:本地 e2e 57/57;部署 38 文件哈希逐字节一致、双版本 php -l、37 条
静态断言全过;线上实测 google/sogou/ddg 三个「自家接口不可用」的引擎
均返回 10 条百度建议(src=baidu),中英文都正常。
2026-09-28 17:13:10 +08:00

193 lines
6.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 简航 SimpleNav —— 搜索联想代理
* 浏览器直接调用各搜索引擎的联想接口会有跨域/JSONP 限制,这里由服务端代理并统一输出 JSON。
*
* ⚠️ 各家联想接口在国内服务器上的可用性差别很大。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 设置页)。
*
* 用法:suggest.php?slug=bing&q=关键词
* slug = 当前选中的引擎(auto 模式下用它自己的接口;其它模式下只作为兜底候选)
*/
declare(strict_types=1);
require_once __DIR__ . '/functions.php';
header('Content-Type: application/json; charset=utf-8');
send_no_cache();
$slug = isset($_GET['slug']) ? preg_replace('/[^a-z0-9\-_]/i', '', (string)$_GET['slug']) : '';
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
if ($q === '') {
echo json_encode(['q' => '', 'items' => [], 'src' => ''], JSON_UNESCAPED_UNICODE);
exit;
}
$items = [];
$used = '';
foreach (suggest_order($slug) as $engine) {
$items = fetch_suggest($engine, $q);
if ($items) {
$used = (string)$engine['slug'];
break;
}
}
// 去重、限长、裁剪
$items = array_values(array_unique(array_filter(array_map('trim', $items))));
$items = array_slice($items, 0, 10);
echo json_encode(['q' => $q, 'items' => $items, 'src' => $used], JSON_UNESCAPED_UNICODE);
exit;
/* ========================================================================== */
/** 取一个引擎行(**不过滤 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;
}
/** 按引擎类型解析联想结果 */
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']; }
}
}
return is_array($out) ? $out : [];
}