'', '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 : []; }