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),中英文都正常。
This commit is contained in:
2026-09-28 17:13:10 +08:00
parent 853e4dd7c6
commit 34473d0120
7 changed files with 184 additions and 44 deletions
+19 -12
View File
@@ -18,6 +18,8 @@
if (form) { form.setAttribute('action', search); form.setAttribute('data-slug', slug); }
if (input) { input.setAttribute('name', param); input.setAttribute('placeholder', ph); }
hideWord();
// 已经输了字就顺手换一份建议(auto 模式下各引擎来源不同)
if (input && input.value.trim() !== '') { askSuggest(); }
}
tabs.forEach(function (el) {
el.addEventListener('click', function () { activateEngine(el); input && input.focus(); });
@@ -26,7 +28,8 @@
/* ---------- 联想下拉 ---------- */
var word = document.getElementById('word');
var timer = null;
var lastQ = '';
var lastQ = null;
var seq = 0; // 请求序号:防止先发的慢响应覆盖后发的快响应
function hideWord() { if (word) { word.innerHTML = ''; word.style.display = 'none'; } }
function showWord(items) {
@@ -40,20 +43,24 @@
word.style.display = 'block';
}
function askSuggest() {
if (!input) return;
var q = input.value.trim();
if (q === '') { lastQ = null; hideWord(); return; }
if (q === lastQ) { return; }
lastQ = q;
var mine = ++seq;
var slug = form ? (form.getAttribute('data-slug') || '') : '';
fetch(cfg.suggest + '?slug=' + encodeURIComponent(slug) + '&q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (data) { if (mine === seq) { showWord(data && data.items); } })
.catch(function () { if (mine === seq) { hideWord(); } });
}
if (input) {
input.addEventListener('input', function () {
var q = input.value.trim();
if (q === '') { hideWord(); return; }
if (q === lastQ) return;
lastQ = q;
clearTimeout(timer);
timer = setTimeout(function () {
var slug = form ? (form.getAttribute('data-slug') || '') : '';
fetch(cfg.suggest + '?slug=' + encodeURIComponent(slug) + '&q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (data) { showWord(data && data.items); })
.catch(function () { hideWord(); });
}, 180);
timer = setTimeout(askSuggest, 180);
});
input.addEventListener('keydown', function (e) {