2026-10-07 05:52:40 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 导航搜索联想代理(v24 第 24 批 用户第 4 条,从 SimpleNav 搬来)
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 浏览器直接调各家联想接口会撞跨域 / JSONP 限制,所以由服务端代理并统一输出 JSON。
|
|
|
|
|
|
* SimpleNav 原文这里有一段**线上实测结论**,照抄保留(它是最贵的部分):
|
|
|
|
|
|
*
|
|
|
|
|
|
* 百度 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 连接超时 ❌ 国内不可达
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所以默认策略是「**联想词统一取百度,搜索仍走各自引擎**」——
|
|
|
|
|
|
* 点哪个引擎就用哪个引擎搜,只是下拉建议来自百度,任何一个引擎都不会出现"下拉是空的"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:`/nav/suggest.php?p=<短语>&slug=bing&q=关键词`
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️⚠️ **这个接口也是免登录的**(展示页本身免登录,它的联想接口必须一起免登录,
|
|
|
|
|
|
* 否则展示页的搜索框就没法联想)。因此它必须有自己的门禁 —— 见下面"门禁"那段:
|
|
|
|
|
|
* **必须带一个有效的短语**才放行,否则任何人都能拿它当免费代理刷外网请求。
|
|
|
|
|
|
*/
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
require __DIR__ . '/app/bootstrap.php';
|
|
|
|
|
|
|
|
|
|
|
|
use App\Nav;
|
|
|
|
|
|
use App\NavFetch;
|
|
|
|
|
|
|
|
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
|
|
|
|
header('X-Robots-Tag: noindex');
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 门禁:必须凭一个有效的短语(见文件头说明)----
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 这**不是**身份校验(短语本来就是公开的),它挡的是"随便谁来都能刷":
|
|
|
|
|
|
// 没有它,这个 URL 就是一个匿名的外网请求放大器(每次 GET 都会去打百度/必应)。
|
|
|
|
|
|
$phrase = trim((string)($_GET['p'] ?? ''));
|
2026-10-08 14:46:16 +08:00
|
|
|
|
/* 🔴 v30 用户第 2 条:容错 —— 短语里**不允许**出现 `?` 或 `&`(短语本身是
|
|
|
|
|
|
`[A-Za-z0-9_-]{4,32}`,见 Nav::PHRASE_RE,永远不含这两个字符)。
|
|
|
|
|
|
|
|
|
|
|
|
为什么要有这一段:前端曾经把 suggest 地址硬拼成 `…?p=abc?slug=bing&q=…`
|
|
|
|
|
|
(suggest 地址本身已带 `?`,旧代码又补了一个 `?`)。PHP 对重复 `?` 的处理是
|
|
|
|
|
|
"第一个 `?` 之后整串都算查询值" → `p` 变成了 `abc?slug=bing` → 403 →
|
|
|
|
|
|
展示页下拉**永远是空的**,而服务端单测是好的(探针自己拼对了)→ 极难查。
|
|
|
|
|
|
|
|
|
|
|
|
修法在前端(`withQuery()`,已改)。这里再做一层"把第一个 `?` 之后的垃圾切掉",
|
|
|
|
|
|
是为了让**任何**老前端 / 缓存住的旧 JS 都不至于整条联想失效 —— 属于廉价兜底:
|
|
|
|
|
|
合法短语不可能含 `?`,切掉不会误伤。 */
|
|
|
|
|
|
if ($phrase !== '') {
|
|
|
|
|
|
$cut = strcspn($phrase, '?&');
|
|
|
|
|
|
if ($cut < strlen($phrase)) {
|
|
|
|
|
|
$phrase = substr($phrase, 0, $cut);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-07 05:52:40 +08:00
|
|
|
|
$owner = $phrase === '' ? 0 : Nav::ownerByPhrase($phrase);
|
|
|
|
|
|
if ($owner <= 0) {
|
|
|
|
|
|
http_response_code(403);
|
|
|
|
|
|
echo json_encode(['q' => '', 'items' => [], 'src' => '', 'error' => 'forbidden'],
|
|
|
|
|
|
JSON_UNESCAPED_UNICODE);
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$slug = isset($_GET['slug']) ? (string)preg_replace('/[^a-z0-9\-_]/i', '', (string)$_GET['slug']) : '';
|
|
|
|
|
|
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
|
|
|
|
|
|
// 联想词是给人看着点的短词,限长免得被拿去当搜索接口刷
|
|
|
|
|
|
$q = mb_substr($q, 0, 60, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
|
|
if ($q === '') {
|
|
|
|
|
|
echo json_encode(['q' => '', 'items' => [], 'src' => ''], JSON_UNESCAPED_UNICODE);
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 取这个人的引擎表(联想来源也按**他的**引擎配置走)----
|
|
|
|
|
|
$engines = Nav::engines($owner);
|
|
|
|
|
|
|
|
|
|
|
|
/** 按 slug 找他的某个引擎 */
|
|
|
|
|
|
$engineBy = static function (string $s) use ($engines): ?array {
|
|
|
|
|
|
if ($s === '') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($engines as $e) {
|
|
|
|
|
|
if ((string)$e['slug'] === $s) {
|
|
|
|
|
|
return $e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 排出「先试谁、再试谁」的候选顺序 —— 照搬 SimpleNav 的 suggest_order()。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 兜底链固定「百度 → 必应 → 360」(实测可用的三家,按快慢排序),
|
|
|
|
|
|
* 只保留库里真实存在、且配了 suggest_url 的引擎。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ SimpleNav 那个 setting('suggest_source') 开关这里**没搬** ——
|
|
|
|
|
|
* 它依赖项目级设置,而本模块是**每人一份**的;为了不给用户多一个看不懂的开关,
|
|
|
|
|
|
* 直接用最有用的那一种(即 auto 的兜底链行为)。这是本批唯一有意"少搬"的一处,
|
|
|
|
|
|
* 在此记明,免得下一个人以为是漏了。
|
|
|
|
|
|
*/
|
|
|
|
|
|
$order = [];
|
|
|
|
|
|
if ($slug !== '') {
|
|
|
|
|
|
$own = $engineBy($slug);
|
|
|
|
|
|
if ($own !== null && (string)$own['suggest_url'] !== '') {
|
|
|
|
|
|
$order[] = $own;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach (['baidu', 'bing', 'so360'] as $s) {
|
|
|
|
|
|
$row = $engineBy($s);
|
|
|
|
|
|
if ($row !== null && (string)$row['suggest_url'] !== '') {
|
|
|
|
|
|
$order[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$items = [];
|
|
|
|
|
|
$used = '';
|
|
|
|
|
|
foreach ($order as $en) {
|
|
|
|
|
|
$items = nav_fetch_suggest($en, $q);
|
|
|
|
|
|
if ($items) {
|
|
|
|
|
|
$used = (string)$en['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;
|
|
|
|
|
|
|
|
|
|
|
|
/* ========================================================================== */
|
|
|
|
|
|
|
|
|
|
|
|
/** 抓一次联想并解析;任何异常都退化成空数组,绝不让下拉框卡住(照搬原函数) */
|
|
|
|
|
|
function nav_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 = NavFetch::get($u, 100000, 3);
|
|
|
|
|
|
if ($resp === null) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
return nav_parse_suggest((string)$engine['suggest_type'], (string)$resp['body'], $q);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 按引擎类型解析联想结果(照搬原函数,逻辑一行未改) */
|
|
|
|
|
|
function nav_parse_suggest(string $type, string $body, string $q): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$body = trim($body);
|
|
|
|
|
|
$out = [];
|
|
|
|
|
|
unset($q);
|
|
|
|
|
|
|
|
|
|
|
|
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,已被 NavFetch 转码)
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j) && !empty($j['items']) && is_array($j['items'])) {
|
|
|
|
|
|
$out = $j['items'];
|
|
|
|
|
|
} elseif (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;
|
|
|
|
|
|
|
2026-10-08 02:20:19 +08:00
|
|
|
|
// ---- v28 用户第 10 条新增的三个联想来源(见 Nav::SUGGEST_LIBRARY)----
|
|
|
|
|
|
case 'sogou':
|
|
|
|
|
|
// {"query":"x","items":["a","b"]}
|
|
|
|
|
|
// ⚠️ 与旧的 `sogou_ajaj` 分开:那条是 GBK 老接口(现已不解析),
|
|
|
|
|
|
// 这条是新的 suggestion.sogou.com,返回 utf-8 JSON。
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j) && !empty($j['items']) && is_array($j['items'])) {
|
|
|
|
|
|
$out = $j['items'];
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'zhihu':
|
|
|
|
|
|
// {"data":[{"query":".."}]} 或 {"suggest":[{"query":".."}]}
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j)) {
|
|
|
|
|
|
foreach (['data', 'suggest'] as $k) {
|
|
|
|
|
|
if (!empty($j[$k]) && is_array($j[$k])) {
|
|
|
|
|
|
foreach ($j[$k] as $it) {
|
|
|
|
|
|
if (is_array($it)) {
|
|
|
|
|
|
foreach (['query', 'title', 'name'] as $f) {
|
|
|
|
|
|
if (isset($it[$f]) && is_string($it[$f])) { $out[] = $it[$f]; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
} elseif (is_string($it)) {
|
|
|
|
|
|
$out[] = $it;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'bili':
|
|
|
|
|
|
// {"code":0,"data":{"tag":[{"value":".."}]}}
|
|
|
|
|
|
// 也兼容 {"result":[{"term":".."}]}
|
|
|
|
|
|
$j = json_decode($body, true);
|
|
|
|
|
|
if (is_array($j)) {
|
|
|
|
|
|
if (!empty($j['data']['tag']) && is_array($j['data']['tag'])) {
|
|
|
|
|
|
foreach ($j['data']['tag'] as $it) {
|
|
|
|
|
|
if (is_array($it) && isset($it['value'])) { $out[] = $it['value']; }
|
|
|
|
|
|
}
|
|
|
|
|
|
} elseif (!empty($j['result']) && is_array($j['result'])) {
|
|
|
|
|
|
foreach ($j['result'] as $it) {
|
|
|
|
|
|
if (is_array($it)) {
|
|
|
|
|
|
foreach (['term', 'name', 'value'] as $f) {
|
|
|
|
|
|
if (isset($it[$f]) && is_string($it[$f])) { $out[] = $it[$f]; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-10-07 05:52:40 +08:00
|
|
|
|
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 : [];
|
|
|
|
|
|
}
|