实测(打开被拒绝内嵌的应用时弹出提示): .navi-ws-modal 遮罩已是深色,但 .navi-ws-modal-card 仍为 rgba(255,255,255,.96), 深色下是刺眼白块且与浅色文字冲突;按钮同样是亮色。 补齐卡片/标题/正文/按钮的深色样式。内嵌区上一条已改为透明(去黑方块)。
HomeDash — 自托管内网导航门户
将 Dashy + SearXNG + 浏览器历史同步 + 管理后台整合在单个 Docker 容器中,并附带完整的内网服务发布方案(带统一认证的反向代理 + webui4frpc 内网穿透),让外网通过域名安全访问内网服务。
架构总览(生产全链路)
公网用户
│
▼
阿里云 VPS (frps 服务端, <vps-public-ip>:7000/8080/443)
│ frps: vhost 端口 + tcp 隧道
▼
frpc (由 webui4frpc 在内网主机上管理, WebUI :7500)
│ configs/*.json → 定义各隧道
▼
portal-nginx (统一认证反向代理, 容器)
│ auth_request → portal-auth(:4001) 统一登录
│ map $host → 按子域名分流
▼
navi 单容器 (Dashy+SearXNG+历史+管理, :3003)
│ 内层 nginx :8080 按路径分流
▼
Dashy / SearXNG(→Clash 代理出网) / 历史 API / 管理 UI
navi 容器内部
┌─────────────────────────────────────────────────────┐
│ nginx (:8080) — 统一入口,同域名同端口 │
│ ├─ / → Dashy 静态仪表盘 │
│ ├─ /search → SearXNG 搜索 (:8888) │
│ ├─ /static/ → SearXNG 静态资源 (:8888) │
│ ├─ /api/history/ → 历史同步 API (:3004) │
│ ├─ /admin/ → 管理后台 WebUI (:3005) │
│ ├─ /conf.yml → 运行时导航配置(数据卷) │
│ └─ /item-icons/ → 自定义图标 (数据卷) │
└─────────────────────────────────────────────────────┘
SearXNG 配置优先级:容器启动时如果发现数据卷里有
data/searxng-settings.yml,就用它;否则用镜像内置的
/etc/searxng/settings.yml。所以代理这类部署相关配置应放在数据卷:
改完只需重启容器,不需要重建镜像,重建容器也不会丢。
技术栈:Go(历史 API + 管理 API)· SearXNG(Python) · Dashy/Vue · Nginx · Supervisord · frp/webui4frpc
快速开始
前置条件
- Docker 20+ 与 Docker Compose v2
- 可选:公网域名 + 反向代理,用于外网访问(见下文「生产部署」)
方式 A:从本仓库拉取源码构建(推荐)
# 1. 拉取仓库(把 <your-gitea> 换成你自己的 Git 托管地址)
git clone <repo-url> HomeDash
cd HomeDash
# 2. 配置环境变量
cp .env.example .env
# 编辑 .env:
# - NAVI_SYNC_TOKEN / NAVI_ADMIN_TOKEN:鉴权令牌,openssl rand -hex 24 生成
# - SEARXNG_BASE_URL:对外访问地址(有域名填 https://nav.example.com)
# - NAVI_PORT:对外端口(默认 3003)
# 3. 首次启动(会拉取 golang/dashy/searxng 基础镜像并编译)
docker compose up -d --build
# 4. 访问
echo "打开 http://<ip>:3003"
方式 B:直接构建镜像运行
docker build -t jianf-navi .
docker run -d --name jianf-navi -p 3003:8080 \
-e SEARXNG_BASE_URL=https://nav.example.com \
-e NAVI_SYNC_TOKEN=<random> \
-e NAVI_ADMIN_TOKEN=<random> \
-v $PWD/data:/app/data \
jianf-navi
首次配置导航项
容器启动后按需编辑 data/conf.yml(sections 列表),或直接用管理后台添加:
# 先让 conf.yml 从模板生成
docker compose restart
# 或用管理 WebUI 在浏览器添加:http://<ip>:3003/admin/
首次没有 data/ 目录时,容器会用内置
conf.example.yml生成一份最小配置。
配置说明
conf.yml 核心字段
appConfig:
theme: default
webSearch:
searchEngine: custom
customSearchEngine: /search?q= # 同域名反代到 SearXNG
openingMethod: newtab
sections:
- name: 常用服务 # 按访问频次自动排序
icon: fas fa-star
collapsed: false
items:
- title: Gitea
icon: fas fa-code-branch
url: http://192.168.1.100:3000
statusCheck: true
图标
放入 data/item-icons/,conf.yml 引用文件名:
icon: my-icon.png # 本地图标
icon: fas fa-star # Font Awesome
环境变量
| 变量 | 说明 | 默认 |
|---|---|---|
NAVI_SYNC_TOKEN |
浏览器扩展同步历史令牌 | 空(不鉴权) |
NAVI_ADMIN_TOKEN |
管理后台/API 写操作令牌 | 空(不鉴权) |
SEARXNG_BASE_URL |
对外访问地址,影响搜索结果链接 | http://localhost:3003 |
NAVI_PORT |
compose 对外端口 | 3003 |
NAVI_FAVICON_PROXY |
抓历史图标用的代理;留空自动复用 searxng-settings.yml 的 outgoing.proxies |
空 |
浏览器扩展
安装(两种方式)
方式 1:导航页内一键引导(推荐)
导航页会自动检测插件是否已安装:
- 未安装时,「历史访问」第一条固定显示「安装浏览历史插件」入口;
- 点击后打开
/extension/install.html,自动识别浏览器并给出对应安装步骤与安装包; 页面同时展示服务地址与同步令牌(各带「复制」按钮),不用再去翻.env。 令牌由admin-server的/admin/api/sync-info提供 ——/admin/在门户登录墙之内, 而插件上报用的/api/history/sync是被门户放行的,所以令牌绝不能挂在/api/下。 - 已安装时,
/navi.js读到插件写入的data-navi-ext标记,把该入口自动隐藏。
检测原理:插件设置里填了服务地址后,会向该站点的域名动态注册一个 content script
(Chrome 用 chrome.scripting.registerContentScripts,Firefox 用 browser.contentScripts.register),
在 document_start 给 <html> 打上 data-navi-ext。不会在其他站点上运行。
Chrome / Edge 不允许从网站直接安装非商店扩展,所以这一步只能下载后手动加载; 若你已把扩展发布到商店,把链接写进
.env或 data 卷的extension-store.json, 安装页就会出现真正的「从商店一键安装」按钮:{ "chrome": "https://chrome.google.com/webstore/detail/<id>", "firefox": "https://addons.mozilla.org/firefox/addon/<slug>/" }放到
data/extension-store.json即可,无需重建镜像。
方式 2:手动加载源码目录
Chrome / Edge:chrome://extensions/ → 开发者模式 → 加载 extension/chrome/
Firefox:about:debugging#/runtime/this-firefox → 临时加载 extension/firefox/manifest.json
扩展的「同步令牌」需填服务端
.env里NAVI_SYNC_TOKEN的同一个值,否则同步被拒(401)。
配置
| 字段 | 示例 |
|---|---|
| 服务地址 | https://nav.example.com |
| 同步令牌 | .env 中的 NAVI_SYNC_TOKEN |
| 设备名称 | desktop-chrome |
行为
- 每 30 秒检查本地队列,仅在有新数据时 POST
- 搜索引擎 URL 在本地过滤,不上传
- 「常用服务」和「历史访问」按总访问频次自动排序
- 历史条目的图标由服务端自动抓取并缓存(见下)
历史条目的站点图标
history 里的站点不可能像手工导航项那样预先配好图标,所以服务端会后台抓取每个
host 的 favicon 并缓存到 data/item-icons/favicon-<host>.png,缓存命中后
conf.yml 里写的是本地文件名,与手工图标走同一套解析。
- 抓取顺序:
<host>/favicon.ico→ 首页<link rel="icon"> .ico会先尝试抽出内嵌 PNG;若是传统 BMP/DIB 编码,则服务端解码并转成 PNG (Dashy 的isImage()只认svg/png/jpg,直接写.ico是渲染不出来的)- 内网地址(含端口,如
192.168.1.10:3000)只走直连;外网站点先直连, 失败时自动复用 SearXNGoutgoing.proxies的代理(也可用NAVI_FAVICON_PROXY单独指定) - 抓不到就保持
fas fa-globe,6 小时后再试;缓存文件持久化在 data 卷
管理后台
访问 http://<your-ip>:3003/admin/ 可视化管理导航项。
页面分卡片:站点信息(标题/副标题/顶部链接/备案号)、导航分组、添加服务、 历史访问、外观(壁纸)、接口令牌。窄屏(手机)已适配:字段竖排、分组行可换行、 输入框整行占满、字号 ≥16px 防 iOS 聚焦缩放。
导航分组可以自由增删
分组就是导航页上的栏目,初始为空(conf.example.yml 的 sections: []),
按需创建即可。要点:
- 删除非空分组需要二次确认(接口侧要求
?force=1),避免误点连带删掉一组服务 - 「历史访问」分组由 history-server 依据浏览记录自动维护:改名/删除/手工新建 都会被拒绝(409)并在界面上锁住——否则下次同步就把它覆写回来
- 若配置里完全没有该分组,history-server 会补建一个(但完全空配置时不补, 保持"初始无分组")
API(需 NAVI_ADMIN_TOKEN)
| 端点 | 方法 | 说明 |
|---|---|---|
/admin/api/sections |
GET | 列出分组与服务 |
/admin/api/section |
POST | 新建分组 {name,icon};重名/空名/占用「历史访问」→ 4xx |
/admin/api/section |
DELETE | 删除分组 {index};非空需 ?force=1 |
/admin/api/section/update |
POST | 改名/换图标 {index,name,icon} |
/admin/api/item |
POST | 向分组添加服务 |
/admin/api/item/delete |
DELETE | 删除服务 {sectionIndex,itemIndex} |
/admin/api/pageinfo |
GET/POST | 站点信息与备案号 |
/admin/api/icon |
POST | 上传图标(.ico 前端转 PNG) |
/admin/api/wallpaper |
GET/POST | 壁纸与遮罩强度 |
/admin/api/history |
GET/DELETE | 浏览历史列表 / 清空全部 |
/admin/api/history/item |
DELETE | 删除单条历史 ?url= |
/admin/api/frameable |
GET | 目标地址能否被 iframe 内嵌(三态,见下) |
/admin/api/sync-info |
GET | 插件安装页用的同步令牌 |
```bash
# 查看
curl -H "Authorization: Bearer <token>" http://<ip>:3003/admin/api/sections
# 添加
curl -H "Authorization: Bearer <token>" -X POST -H 'Content-Type: application/json' \
-d '{"sectionIndex":0,"item":{"title":"New","url":"https://example.com"}}' \
http://<ip>:3003/admin/api/item
# 删除
curl -H "Authorization: Bearer <token>" -X DELETE -H 'Content-Type: application/json' \
-d '{"sectionIndex":0,"itemIndex":2}' \
http://<ip>:3003/admin/api/item/delete
历史同步 API
| 端点 | 方法 | 说明 |
|---|---|---|
/api/history/sync |
POST | 批量同步浏览记录 |
/api/history/ |
GET | 获取历史(按频次排序) |
/api/history/ |
DELETE | 清空全部历史 |
/api/history/stats |
GET | 统计信息 |
// POST /api/history/sync
{
"items": [{
"url": "https://example.com/page",
"title": "Page Title",
"device": "desktop-chrome",
"timestamp": 1725800000,
"visit_count": 3
}]
}
零停机部署
不要直接 docker stop/rename/run:那样有十几秒完全不可用,而且新容器里
nginx 先就绪、SearXNG 还在启动,这期间 /search 会是 502(本项目实际发生过)。
用仓库里的脚本,它会先在对侧端口把新容器验证到底、再瞬时切换上游:
deploy/zd.sh v29 # 版本号只用于镜像 tag
流程(任一步失败自动回滚):
- 本地构建镜像 →
docker save→ scp - 远端在另一侧端口(3003/3004 自动对调)起
navi-next - 在该端口逐个验证
/conf.yml、/admin、/navi.js、/search?q=linux(后者既确认 SearXNG 可用,也把它预热,避免用户撞上冷启动 502) - 改门户 nginx 上游那一行 → 容器侧确认看到新值 →
nginx -t→nginx -s reload - 切后哨兵:经门户访问不得是 502/503/504(未登录正常是 302)
- 停旧容器并保留为回滚点
navi-prev-<时间戳>,新容器改名为navi
可用环境变量:NAVI_HOST、NAVI_SSH_PASS、NAVI_UPSTREAM_HOST、NAVI_PORTAL_CONF、
NAVI_DATA_DIR、NAVI_BASE_IMAGE。
一个必须知道的坑:单文件 bind mount 不能用 sed -i
门户 nginx 配置文件是以单文件 bind mount(且只读)挂进 portal-nginx 容器的。
sed -i / mv 会写临时文件再 rename,换了 inode,而容器在启动时就绑定了当时那个
inode —— 于是宿主机文件改好了,容器里看到的还是旧的,nginx -s reload 读的也是旧的。
一旦上游被切走而新端口无人应答,就是全站 502。
所以:同 inode 覆写(cat new > file),改完先用
docker exec portal-nginx grep -q "<新端口>" /etc/nginx/nginx.conf 断言容器真的看到了,
看不到就 docker restart portal-nginx(唯一出路),然后再 nginx -t + reload。
deploy/zd.sh 已按这个顺序实现。
部署后验收
# 起一个 headless chromium 指向 CDP 端口,然后:
PORT=<cdp端口> CK=<portal_session> VPS=<对外IP> node deploy/verify-ui.js
覆盖:渲染开销(backdrop-filter 元素数)、后台窄屏是否横向溢出、工作空间收边与
"第一次交互只滑出"、不支持内嵌应用的弹层、分组增删改与「历史访问」保护。
任何运行时异常都会判失败(promise 里抛出的错误很容易被忽略,本项目因此漏掉过
一个 ReferenceError 五个版本)。
部署需要的全部自定义文件
仓库即完整部署物料,没有额外依赖仓库外的文件:
| 文件 | 作用 |
|---|---|
Dockerfile |
镜像定义(构建期用 web/patch-index.py 往 Dashy 的 index.html 注入资源) |
nginx.conf |
容器内 nginx:Dashy 静态 + SearXNG + history/admin API 的统一切入口 |
supervisord.conf |
容器内三服务(nginx / SearXNG / 两个 Go 二进制)的进程管理 |
searxng-settings.yml |
SearXNG 引擎与出网代理由此注入(可被数据卷同名文件覆盖) |
conf.example.yml |
首次启动生成 conf.yml 的模板(sections 为空,分组由后台创建) |
admin/index.html |
管理后台单页(站点信息 / 分组 / 服务 / 图标 / 壁纸 / 历史) |
admin-server/ |
Go:站点信息、分组与服务增删改、图标与壁纸上传、历史转发、可内嵌性探测 |
history-server/ |
Go:浏览历史接收与排序、favicon 抓取、按访问频次重建 Dashy 分组 |
web/navi.js / web/navi.css |
门户表现层:搜索球、视图切换、管理入口、收边、工作空间内嵌、页脚 |
web/patch-index.py |
幂等地把 navi.css / navi.js 注入 Dashy 的 index.html |
web/extension/install.html |
插件安装引导页(含同步令牌展示) |
extension/chrome/ extension/firefox/ |
浏览器扩展(MV3 / MV2) |
deploy/zd.sh |
零停机部署脚本 |
deploy/verify-ui.js |
部署后浏览器验收脚本 |
docker-compose.yml / .env.example |
单机 compose 启动与变量样例 |
生产部署:外层统一认证反向代理
内网门户通常不直接暴露 3003 端口,而是放在一个带统一登录认证的反向代理后面, 所有子域名共用一个登录入口。下面是本项目参考的 portal-nginx 方案。
portal-nginx(统一入口容器)
结构:一个 nginx 容器负责所有 *.example.com 的 TLS 终止 + 按域名分流 + 统一认证。
# 按域名映射到内网后端
map $host $backend {
hostnames;
default "";
navi.example.com http://127.0.0.1:3003; # 本导航容器
gitea.example.com http://127.0.0.1:3000;
blog.example.com http://127.0.0.1:8080;
}
# 需登录的域名统一走 auth_request
server {
listen 443 ssl;
http2 on;
server_name navi.example.com gitea.example.com blog.example.com;
auth_request /_auth; # 先问认证服务
error_page 401 = @login; # 未登录 → 跳登录页
location /_auth {
internal;
proxy_pass http://127.0.0.1:4001; # portal-auth 认证服务
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
}
location @login {
return 302 https://login.example.com/login?redirect=$scheme://$host$request_uri;
}
location / {
if ($backend = "") { return 404; }
proxy_pass $backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
portal-auth(统一认证服务)
一个极简 HTTP 认证服务(:4001),只做两件事:
GET/POST /login— 校验用户名密码,签发portal_sessioncookie/_auth被 nginxauth_request调用 — 校验 cookie 是否有效(200=通过,401=拒绝)
# app.py(示意)
USERNAME = os.environ['PORTAL_USERNAME'] # admin
PASSWORD = os.environ['PORTAL_PASSWORD'] # 强密码
sessions = {} # token -> 过期时间
# /login 成功 → uuid token 写入 sessions + Set-Cookie
# /_auth 读 Cookie → 校验存在且未过期 → 200/401
部署:
docker run -d --name portal-auth \
-p 4001:4001 \
-e PORTAL_USERNAME=admin \
-e PORTAL_PASSWORD=change-me \
-v /path/auth/app.py:/app/app.py:ro \
python:3.11-slim
让搜索/管理子路径免登录(可选)
Dashy 域名整体在认证后,登录用户能正常搜索。若希望搜索页免登录公开访问,在对应 server 块加:
location /search { auth_request off; proxy_pass $backend; }
⚠️ 管理后台
/admin/与历史 API/api/不应免认证—— 它们能修改导航与数据,建议保留登录墙,或改用 Bearer Token(见下文鉴权章节)。
内网穿透:webui4frpc + frp
webui4frpc — 基于 Go 与 Web 的可视化 frpc 配置管理工具, 提供 WebUI 创建/启停 frpc 隧道,无需手写 frpc.toml。 也可不用它,直接用原生 frpc(见文末「原生 frpc 方式」)。
为什么要穿透
本导航容器跑在内网(如 192.168.x.x:3003)。要让外网通过域名访问,需要:
外网用户
→ 公网 VPS(有公网IP) 上的 frps [frp 服务端,监听控制端口 + 域名端口]
→ frpc(内网运行,webui4frpc 管理或原生)
→ 你内网的 portal-nginx / navi [把域名流量转发进来]
第 1 步:公网 VPS 部署 frps
下载 frp 服务端二进制并写配置:
# frps.toml
bindPort = 7000 # frpc 连接 frps 的控制端口
vhostHTTPPort = 8080 # HTTP 类型代理的入口端口(雷池/WAF 也走它)
auth.method = "token"
auth.token = "<长随机token>" # frpc 必须带相同 token 才能连
tls.enable = true # frpc↔frps 加密
# 用 systemd 启动,或直接:
# ./frps -c frps.toml
VPS 安全组/防火墙需放行:7000(frpc连接)、8080(http域名)、443/80(若 https 由 frps 终结)。
第 2 步:内网主机部署 webui4frpc(可视化)
从 webui4frpc 下载二进制,启动:
/opt/webui4frpc/webui4frpc \
-addr 0.0.0.0:7500 \
-user admin -password '<强密码>' \
-workdir /opt/webui4frpc/data
# 打开 http://<内网IP>:7500 进入 WebUI
第 3 步:在 WebUI 建「portal」隧道
在 WebUI 里新建配置,指向公网 frps,并添加代理。等价配置文件存于 configs/*.json:
{
"serverAddr": "<vps-public-ip>",
"serverPort": 7000,
"auth": { "method": "token", "token": "<frp-token>" },
"proxies": [
{
"name": "portal_https",
"type": "https",
"localIP": "<内网portal-nginx主机IP>",
"localPort": 3080,
"customDomains": ["*.nav.example.com", "nav.example.com"]
},
{
"name": "portal_http",
"type": "tcp",
"localIP": "<内网portal-nginx主机IP>",
"localPort": 3081,
"remotePort": 8080
},
{
"name": "navi_ssh",
"type": "tcp",
"localIP": "<内网目标主机IP>",
"localPort": 22,
"remotePort": 220
}
]
}
webui4frpc 会自动下载并启动 frpc(存 data/bin/),配置变更即热加载。
第 4 步:DNS 解析
把 nav.example.com、*.nav.example.com 的 DNS 指向公网 VPS IP。
type: https代理:frps 把域名 443 流量转发内网 portal-nginx:3080,TLS 证书由你内网管理type: tcp(portal_http):把外网 8080 → 内网 3081(HTTP→HTTPS 跳转或备用入口)
原生 frpc 方式(不用 webui4frpc)
# frpc.toml
serverAddr = "<vps-public-ip>"
serverPort = 7000
auth.method = "token"
auth.token = "<frp-token>"
[[proxies]]
name = "portal_https"
type = "https"
localIP = "192.168.1.100"
localPort = 3080
customDomains = ["nav.example.com", "*.nav.example.com"]
[[proxies]]
name = "portal_http"
type = "tcp"
localIP = "192.168.1.100"
localPort = 3081
remotePort = 8080
./frpc -c frpc.toml
隧道规划对照
| 代理名 | type | 本地 | 远端 | 用途 |
|---|---|---|---|---|
| portal_https | https | 内网:3080 | 443域名 | 统一反代 HTTPS 入口 |
| portal_http | tcp | 内网:3081 | 8080 | HTTP 跳 HTTPS / 备用 |
| gitea_ssh | tcp | 内网:22 | 220 | Git SSH |
鉴权(历史同步 + 管理 API)
导航门户的 /api/ 和 /admin/api/ 能读写数据,不应裸奔。推荐两层:
- 外层:portal-nginx 保留登录墙(
/admin/页面需登录) - 内层:Go 服务校验
Authorization: Bearer <token>
部署时通过环境变量注入令牌:
docker run -d ... \
-e NAVI_SYNC_TOKEN=<长随机串> \ # 浏览器扩展同步用
-e NAVI_ADMIN_TOKEN=<长随机串> \ # 管理 API 写操作用
jianf-navi:latest
浏览器扩展在设置里填同一个 NAVI_SYNC_TOKEN,每次同步请求带 Authorization: Bearer <token>。
# 管理 API 示例(写操作必须带 admin token)
curl -H "Authorization: Bearer <NAVI_ADMIN_TOKEN>" \
-X POST -H 'Content-Type: application/json' \
-d '{"sectionIndex":0,"item":{"title":"New","url":"https://example.com"}}' \
http://<ip>:3003/admin/api/item
实现参考:
history-server的/sync校验NAVI_SYNC_TOKEN,admin-server的写操作校验NAVI_ADMIN_TOKEN。
数据备份
data/
├── conf.yml # 导航配置
├── history.json # 浏览历史
├── item-icons/ # 自定义图标 + 历史站点 favicon 缓存
└── searxng-settings.yml # (可选)覆盖内置 SearXNG 配置,如代理
# 自动备份
0 2 * * * docker exec navi tar czf /tmp/bak.tar.gz -C /app/data . && \
docker cp navi:/tmp/bak.tar.gz /backup/navi-$(date +\%Y\%m\%d).tar.gz
项目结构
├── history-server/ # Go 历史同步 API (:3004)
├── admin-server/ # Go 管理后台 API (:3005)
├── admin/ # 管理后台前端
├── web/ # Dashy 注入脚本 + 扩展安装引导页
├── extension/ # 浏览器扩展 (Chrome/Firefox)
├── nginx.conf # 容器内统一反代
├── supervisord.conf # 进程管理
├── conf.example.yml # 脱敏示例配置
├── searxng-settings.yml
├── docker-compose.yml
├── Dockerfile
└── .env.example
工作空间视图
点左侧分组 → 右侧在 iframe 里打开该应用。不是所有应用都允许被嵌:
- 实测本仓库作者环境 24 个应用里 19 个可以,Gitea(含内网 IP)、docker.com 为
X-Frame-Options: SAMEORIGIN,Dify 为DENY - 是否可嵌由
GET /admin/api/frameable服务端探测决定(浏览器侧无法区分"被拒绝" 与"加载中",onload两种情况都会触发)。探测是三态:确定可嵌 / 确定被拒 / 无法确认(被 WAF 拦成 403、或跳到了别的主机)。第三态照常尝试内嵌,但在工具条里 说明原因并保留"在新标签页打开"的退路 - 探测会转发浏览器自己的 Cookie:门户后面的应用对未登录请求会 302 到登录页, 而登录页没有 X-Frame-Options,不转发会话就会把"跳登录页"误判成"可内嵌"
- 被拒时:右侧面板给出说明,并弹出提示卡片(应用不支持在工作区显示 + 原因 +
在新标签页打开),同时清空 iframe 的
src,不留一个拒绝渲染的空白框
悬浮控件会"收边",第一次交互只滑出
工作空间中间是别人的应用,所以搜索球/视图切换/管理入口/备案号默认收在屏幕边缘 (各露 14px,备案号 13px)。第一次点击/触摸只把它滑出来,第二次才真正生效—— 否则贴边的按钮会被直接点中(想看搜索框,结果把管理后台打开了)。判据是"元素是否真的 露在视口内",所以鼠标悬停已展开时一次点击即生效。6 秒无操作或 Esc 自动收回。
常见问题
搜索无结果:确认容器能访问搜索引擎。
- 国内服务器通常需要代理才能出网:SearXNG 不读
http_proxy环境变量, 必须在outgoing.proxies里配置(见文件内注释)。 推荐把配置放到数据卷:cp searxng-settings.yml data/searxng-settings.yml改完后编辑data/searxng-settings.yml并重启容器即可,无需重建镜像。 - Bing 结果异常少:
www.bing.com会 302 跳到cn.bing.com,而 SearXNG 引擎请求不跟随重定向,只会解析跳转页得到 0 条且不报错。已在默认配置里把 bing 的base_url指向https://cn.bing.com绕开该重定向。 - 引擎大面积超时:
outgoing.max_request_timeout会覆盖每个引擎自己的timeout。默认配置已放宽到request_timeout: 8.0/max_request_timeout: 15.0, 经代理访问时不要再调回 3/4 秒。 - 百度与搜狗会返回 302 验证页(并触发引擎 1 小时封禁),默认已关闭。
管理后台 401:设置了 NAVI_ADMIN_TOKEN 时需携带 Bearer token。
历史不显示:检查扩展配置中的服务地址和同步令牌。
「安装浏览历史插件」入口不消失:该入口由插件写入的 data-navi-ext 标记驱动隐藏。
- 确认扩展已启用,且设置里的服务地址主机名与当前访问地址一致
(例如填
https://nav.example.com、却用http://192.168.1.100:3003访问,就检测不到)。 - 改完设置请刷新页面。Chrome 若因新增
scripting权限把扩展置为待确认, 需在chrome://extensions重新启用。 - 检测失败只会多显示一个入口,不影响同步功能。
内存占用:正常约 100MB。
许可
MIT