Files
SimpleNav/db.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

246 lines
11 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 —— 数据库(PDO SQLite)
* 首次访问自动建表并写入默认数据(含从 n.yangyq.net 迁移的常用/工具/合集链接)。
*/
declare(strict_types=1);
require_once __DIR__ . '/config.php';
function db(): PDO
{
static $pdo = null;
if ($pdo === null) {
if (!is_dir(DATA_PATH)) {
@mkdir(DATA_PATH, 0755, true);
}
$pdo = new PDO('sqlite:' . DB_PATH);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA foreign_keys = ON');
db_init($pdo);
}
return $pdo;
}
function db_init(PDO $pdo): void
{
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT DEFAULT '',
display TEXT NOT NULL DEFAULT 'grid', -- grid=普通网格板块 tabs=合集标签页
sort_order INTEGER NOT NULL DEFAULT 0,
is_visible INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS sites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL,
url TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '', -- 自动抓取的网站名称
short_name TEXT NOT NULL DEFAULT '', -- 用户自定义简称(页面展示用)
icon TEXT NOT NULL DEFAULT '', -- 本地图标文件名
icon_custom INTEGER NOT NULL DEFAULT 0, -- 1=用户自定义图标("全部更新"不再覆盖)
sort_order INTEGER NOT NULL DEFAULT 0,
is_visible INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT '',
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS engines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL,
search_url TEXT NOT NULL,
param TEXT NOT NULL DEFAULT 'q',
suggest_url TEXT NOT NULL DEFAULT '',
suggest_type TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '#2F6BF7',
is_active INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS settings (
k TEXT PRIMARY KEY,
v TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT ''
);
SQL);
// 老库升级:为已存在的 sites 表补 icon_custom 列
$cols = [];
foreach ($pdo->query('PRAGMA table_info(sites)') as $c) {
$cols[(string)$c['name']] = true;
}
if (!isset($cols['icon_custom'])) {
$pdo->exec('ALTER TABLE sites ADD COLUMN icon_custom INTEGER NOT NULL DEFAULT 0');
}
// 老库升级:清掉「搜狗联想」这个已经彻底失效的默认值
// (2026-09-28 实测:suggnew/ajajjson 返回 200 但 0 字节,联想域名 w.sugg.sogou.com 已不解析)
// 只匹配默认种子里的那个 URL —— 用户自己改过的地址绝不动。
$pdo->prepare('UPDATE engines SET suggest_url=?, suggest_type=? WHERE slug=? AND suggest_url=?')
->execute(['', '', 'sogou', 'https://www.sogou.com/suggnew/ajajjson?type=web&key={q}']);
// 是否为空库(用于判断是否写入默认数据)
$isEmpty = (int)$pdo->query('SELECT COUNT(*) FROM groups')->fetchColumn() === 0;
seed_settings($pdo);
seed_engines($pdo);
if ($isEmpty) {
seed_groups_and_sites($pdo);
}
}
function seed_settings(PDO $pdo): void
{
$defaults = [
'site_name' => '简航',
'subtitle' => '个人网站导航',
'default_engine' => 'bing',
'primary_color' => '#2F6BF7',
'open_blank' => '1',
];
$stmt = $pdo->prepare('INSERT OR IGNORE INTO settings (k, v) VALUES (?, ?)');
foreach ($defaults as $k => $v) {
$stmt->execute([$k, $v]);
}
}
function seed_engines(PDO $pdo): void
{
if ((int)$pdo->query('SELECT COUNT(*) FROM engines')->fetchColumn() > 0) {
return;
}
// name, slug, search_url, param, suggest_url, suggest_type, color, active, sort
// ⚠️ suggest_url 留空的含义:该引擎自己没有可用的联想接口。
// 反正默认「联想词来源 = 百度」,留空只会让 auto 模式下少一次无谓的超时等待。
// 搜狗(suggnew/ajajjson 已失效)、谷歌 / DuckDuckGo(国内不可达)都不填。
$rows = [
['必应', 'bing', 'https://cn.bing.com/search', 'q', 'https://api.bing.com/osjson.aspx?query={q}', 'bing_osjson', '#60A504', 1, 1],
['百度', 'baidu', 'https://www.baidu.com/s', 'wd', 'https://www.baidu.com/sugrec?prod=pc&wd={q}', 'baidu_sugrec', '#2100E0', 1, 2],
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0, 3],
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0, 4],
['360', 'so360', 'https://www.so.com/s', 'q', 'https://sug.so.360.cn/suggest?encodein=utf-8&encodeout=utf-8&format=json&word={q}', 'so360', '#1BB04A', 0, 5],
['DuckGo','ddg', 'https://duckduckgo.com/', 'q', '', '', '#DE5833', 0, 6],
];
$stmt = $pdo->prepare('INSERT INTO engines (name,slug,search_url,param,suggest_url,suggest_type,color,is_active,sort_order) VALUES (?,?,?,?,?,?,?,?,?)');
foreach ($rows as $r) {
$stmt->execute($r);
}
}
function seed_groups_and_sites(PDO $pdo): void
{
// 板块定义: [名称, 类型, 排序, [ [名称, 网址], ... ] ]
$data = [
['常用', 'grid', 1, [
['海大', 'http://www.ouc.edu.cn/'],
['阿里云', 'https://www.aliyun.com/'],
['知乎', 'https://www.zhihu.com/'],
['天翼云盘', 'https://cloud.189.cn/'],
['快科技', 'http://www.mydrivers.com/'],
['轻舟', 'https://yangjh.net/'],
['老杨', 'https://yangyq.net/'],
['红围脖', 'https://hongbowei.com/'],
]],
['工具', 'grid', 2, [
['宝塔', 'https://bt.yangyq.net:8868/44d5b346'],
['海大VPN', 'https://otrust.ouc.edu.cn'],
['BB系统', 'https://wlkc.ouc.edu.cn/'],
['教务系统', 'http://jwgl.ouc.edu.cn/'],
['信息门户', 'http://my.ouc.edu.cn/'],
['信息学部', 'http://it.ouc.edu.cn/'],
['淘宝', 'https://www.taobao.com/'],
['京东', 'https://www.jd.com/'],
['日历', 'https://nas.yangyq.net/calendar'],
['办公Nas', 'https://nas.yangyq.net/'],
['家庭Nas', 'https://home.yangyq.net/'],
['Git', 'https://git.yangyq.net/'],
['网盘', 'https://nas.yangyq.net/drive/'],
['电影', 'https://movie.yangyq.net/'],
['相册', 'https://home.yangyq.net/photo'],
['文件', 'https://nas.yangyq.net/file'],
['bilibili', 'https://www.bilibili.com/'],
['优酷', 'https://www.youku.com/'],
['爱奇艺', 'https://www.iqiyi.com/'],
['腾讯视频', 'https://v.qq.com/'],
['芒果TV', 'https://www.mgtv.com/'],
['Draw.IO', 'https://draw.yangyq.net'],
['稿定PS', 'https://ps.gaoding.com/'],
['密码生成', 'https://n.yangyq.net/password/'],
['PDF工具', 'https://pdf.wps.cn/'],
['微信读书', 'https://weread.qq.com/'],
['高德地图', 'https://gaode.com/'],
['百度地图', 'https://map.baidu.com/'],
['建设银行', 'http://ccb.com/cn/home/index.html'],
['海大财务', 'http://hdcw.ouc.edu.cn'],
['MOOC', 'https://www.icourse163.org/'],
]],
['常用软件', 'tabs', 10, [
['WPS', 'https://www.wps.cn/'],
['Chrome', 'https://www.google.cn/chrome/index.html'],
['VS Code', 'https://code.visualstudio.com/'],
['Draw.IO', 'https://github.com/jgraph/drawio-desktop/releases'],
['火绒', 'https://www.huorong.cn/'],
['7-Zip', 'https://sparanoid.com/lab/7z/'],
['Tabby', 'https://github.com/Eugeny/tabby/'],
['GitAhead', 'https://gitahead.github.io/gitahead.com/'],
['Everything', 'https://www.voidtools.com/zh-cn/'],
['Wox', 'https://github.com/Wox-launcher/Wox'],
['BCUninstaller', 'https://github.com/Klocman/Bulk-Crap-Uninstaller'],
['DBeaver', 'https://github.com/dbeaver/dbeaver'],
['Thunderbird', 'https://www.thunderbird.net/zh-CN/'],
['FDM', 'https://www.freedownloadmanager.org/zh/'],
['GIMP', 'https://www.gimp.org/'],
['VLC', 'https://www.videolan.org/vlc/'],
['Zotero', 'https://www.zotero.org/'],
['QQ音乐', 'https://y.qq.com/'],
['微信', 'https://weixin.qq.com/'],
['TIM', 'https://tim.qq.com/'],
['Shotcut', 'https://github.com/mltframework/shotcut'],
]],
['图片资源', 'tabs', 11, [
['图标库', 'https://www.iconfont.cn/'],
['插画库', 'https://undraw.co/illustrations'],
['Pixabay', 'https://pixabay.com/zh/'],
['Pexels', 'https://www.pexels.com/'],
]],
['科研论文', 'tabs', 12, [
['DeepL翻译', 'https://www.deepl.com/'],
['Linggle', 'https://linggle.com/'],
['Netspeak', 'https://netspeak.org/'],
['Semantic Scholar', 'https://www.semanticscholar.org/'],
['LaTex Table', 'https://www.tablesgenerator.com/'],
]],
];
$g = $pdo->prepare('INSERT INTO groups (name, slug, display, sort_order, is_visible) VALUES (?,?,?,?,1)');
$s = $pdo->prepare('INSERT INTO sites (group_id, url, name, short_name, icon, sort_order, is_visible, created_at, updated_at) VALUES (?,?,?,?,\'\',?,1,?,?)');
$now = date('Y-m-d H:i:s');
foreach ($data as $row) {
[$name, $display, $order, $sites] = $row;
$g->execute([$name, slugify($name), $display, $order]);
$gid = (int)$pdo->lastInsertId();
$i = 0;
foreach ($sites as $site) {
$s->execute([$gid, $site[1], $site[0], $site[0], $i++, $now, $now]);
}
}
}
/** 生成 slug(中文退化为拼音不可用时用 md5 短串) */
function slugify(string $s): string
{
$s = trim($s);
if (preg_match('/^[a-z0-9\-]+$/i', $s)) {
return strtolower($s);
}
return 'g' . substr(md5($s), 0, 6);
}