# HomeDash — 自托管内网导航门户 将 **Dashy + SearXNG + 浏览器历史同步 + 管理后台**整合在单个 Docker 容器中,并附带**完整的内网服务发布方案**(带统一认证的反向代理 + webui4frpc 内网穿透),让外网通过域名安全访问内网服务。 ## 架构总览(生产全链路) ``` 公网用户 │ ▼ 阿里云 VPS (frps 服务端, :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:从本仓库拉取源码构建(推荐) ```bash # 1. 拉取仓库(把 换成你自己的 Git 托管地址) git clone 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://:3003" ``` ### 方式 B:直接构建镜像运行 ```bash 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= \ -e NAVI_ADMIN_TOKEN= \ -v $PWD/data:/app/data \ jianf-navi ``` ### 首次配置导航项 容器启动后按需编辑 `data/conf.yml`(sections 列表),或直接用管理后台添加: ```bash # 先让 conf.yml 从模板生成 docker compose restart # 或用管理 WebUI 在浏览器添加:http://:3003/admin/ ``` > 首次没有 data/ 目录时,容器会用内置 `conf.example.yml` 生成一份最小配置。 ## 配置说明 ### conf.yml 核心字段 ```yaml 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 引用文件名: ```yaml 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` 给 `` 打上 `data-navi-ext`。不会在其他站点上运行。 > Chrome / Edge **不允许**从网站直接安装非商店扩展,所以这一步只能下载后手动加载; > 若你已把扩展发布到商店,把链接写进 `.env` 或 data 卷的 `extension-store.json`, > 安装页就会出现真正的「从商店一键安装」按钮: > > ```json > { "chrome": "https://chrome.google.com/webstore/detail/", > "firefox": "https://addons.mozilla.org/firefox/addon//" } > ``` > > 放到 `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-.png`,缓存命中后 conf.yml 里写的是本地文件名,与手工图标走同一套解析。 - 抓取顺序:`/favicon.ico` → 首页 `` - `.ico` 会先尝试抽出内嵌 PNG;若是传统 BMP/DIB 编码,则服务端解码并转成 PNG (Dashy 的 `isImage()` 只认 `svg/png/jpg`,直接写 `.ico` 是渲染不出来的) - 内网地址(含端口,如 `192.168.1.10:3000`)只走直连;外网站点先直连, 失败时自动复用 SearXNG `outgoing.proxies` 的代理(也可用 `NAVI_FAVICON_PROXY` 单独指定) - 抓不到就保持 `fas fa-globe`,6 小时后再试;缓存文件持久化在 data 卷 ## 管理后台 访问 `http://: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 ```bash # 查看 curl -H "Authorization: Bearer " http://:3003/admin/api/sections # 添加 curl -H "Authorization: Bearer " -X POST -H 'Content-Type: application/json' \ -d '{"sectionIndex":0,"item":{"title":"New","url":"https://example.com"}}' \ http://:3003/admin/api/item # 删除 curl -H "Authorization: Bearer " -X DELETE -H 'Content-Type: application/json' \ -d '{"sectionIndex":0,"itemIndex":2}' \ http://:3003/admin/api/item/delete ``` ## 历史同步 API | 端点 | 方法 | 说明 | |------|------|------| | `/api/history/sync` | POST | 批量同步浏览记录 | | `/api/history/` | GET | 获取历史(按频次排序) | | `/api/history/` | DELETE | 清空全部历史 | | `/api/history/stats` | GET | 统计信息 | ```json // 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(本项目实际发生过)。 用仓库里的脚本,它会先在对侧端口把新容器验证到底、再瞬时切换上游: ```bash deploy/zd.sh v29 # 版本号只用于镜像 tag ``` 流程(任一步失败自动回滚): 1. 本地构建镜像 → `docker save` → scp 2. 远端在**另一侧端口**(3003/3004 自动对调)起 `navi-next` 3. 在该端口逐个验证 `/conf.yml`、`/admin`、`/navi.js`、**`/search?q=linux`** (后者既确认 SearXNG 可用,也把它预热,避免用户撞上冷启动 502) 4. 改门户 nginx 上游那一行 → 容器侧确认看到新值 → `nginx -t` → `nginx -s reload` 5. **切后哨兵**:经门户访问不得是 502/503/504(未登录正常是 302) 6. 停旧容器并保留为回滚点 `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` 已按这个顺序实现。 ### 部署后验收 ```bash # 起一个 headless chromium 指向 CDP 端口,然后: PORT= CK= 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 终止 + 按域名分流 + 统一认证。 ```nginx # 按域名映射到内网后端 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),只做两件事: 1. `GET/POST /login` — 校验用户名密码,签发 `portal_session` cookie 2. `/_auth` 被 nginx `auth_request` 调用 — 校验 cookie 是否有效(200=通过,401=拒绝) ```python # 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 ``` 部署: ```bash 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 块加: ```nginx location /search { auth_request off; proxy_pass $backend; } ``` > ⚠️ 管理后台 `/admin/` 与历史 API `/api/` **不应**免认证—— > 它们能修改导航与数据,建议保留登录墙,或改用 **Bearer Token**(见下文鉴权章节)。 ## 内网穿透:webui4frpc + frp > [webui4frpc](https://gitcode.com/JianFeeeee/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 服务端二进制并写配置: ```toml # 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](https://gitcode.com/JianFeeeee/webui4frpc) 下载二进制,启动: ```bash /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`: ```json { "serverAddr": "", "serverPort": 7000, "auth": { "method": "token", "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) ```toml # frpc.toml serverAddr = "" serverPort = 7000 auth.method = "token" auth.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 ``` ```bash ./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/` 能读写数据,**不应裸奔**。推荐两层: 1. **外层**:portal-nginx 保留登录墙(`/admin/` 页面需登录) 2. **内层**:Go 服务校验 `Authorization: Bearer ` 部署时通过环境变量注入令牌: ```bash docker run -d ... \ -e NAVI_SYNC_TOKEN=<长随机串> \ # 浏览器扩展同步用 -e NAVI_ADMIN_TOKEN=<长随机串> \ # 管理 API 写操作用 jianf-navi:latest ``` 浏览器扩展在设置里填同一个 `NAVI_SYNC_TOKEN`,每次同步请求带 `Authorization: Bearer `。 ```bash # 管理 API 示例(写操作必须带 admin token) curl -H "Authorization: Bearer " \ -X POST -H 'Content-Type: application/json' \ -d '{"sectionIndex":0,"item":{"title":"New","url":"https://example.com"}}' \ http://: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 配置,如代理 ``` ```bash # 自动备份 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