&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'] ?? '')); $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; 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 : []; }