Files
HomeDash/README.md
HomeDash Bot a6b5513f88 docs+chore: 部署脚本入库、README 补零停机部署与部署物料清单
仓库此前缺少部署脚本,只能靠临时文件操作,新人无法复现。补齐:

- deploy/zd.sh:零停机部署(新容器先在对侧端口验证到底,含 /search 预热
  SearXNG;再同 inode 覆写门户上游 + nginx -t + reload;切后哨兵"不得为 502";
  失败自动回滚,旧容器保留为回滚点)。可用环境变量覆盖主机/路径/镜像。
- deploy/verify-ui.js:部署后浏览器验收(渲染开销、后台窄屏、收边两段式、
  被拒内嵌弹层、分组增删改与「历史访问」保护)。任一运行时异常即判失败。

README 新增:
- 「零停机部署」含**单文件 bind mount 不能用 sed -i** 的坑(换 inode → 容器读到
  旧值 → 全站 502,本项目真实踩过)与正确顺序(同 inode 覆写 → 断言容器看到 →
  nginx -t → reload)
- 「部署需要的全部自定义文件」清单表
- 「管理后台」补分组自由增删、历史分组保护、完整 API 表
- 「工作空间视图」补可内嵌性三态探测(转发 Cookie、WAF 403 误判)、被拒弹层、
  悬浮控件收边与"第一次交互只滑出"
2026-09-11 20:13:44 +08:00

693 lines
27 KiB
Markdown
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.

# 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:从本仓库拉取源码构建(推荐)
```bash
# 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:直接构建镜像运行
```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=<random> \
-e NAVI_ADMIN_TOKEN=<random> \
-v $PWD/data:/app/data \
jianf-navi
```
### 首次配置导航项
容器启动后按需编辑 `data/conf.yml`(sections 列表),或直接用管理后台添加:
```bash
# 先让 conf.yml 从模板生成
docker compose restart
# 或用管理 WebUI 在浏览器添加:http://<ip>: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` 给 `<html>` 打上 `data-navi-ext`。不会在其他站点上运行。
> Chrome / Edge **不允许**从网站直接安装非商店扩展,所以这一步只能下载后手动加载;
> 若你已把扩展发布到商店,把链接写进 `.env` 或 data 卷的 `extension-store.json`,
> 安装页就会出现真正的「从商店一键安装」按钮:
>
> ```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`)只走直连;外网站点先直连,
失败时自动复用 SearXNG `outgoing.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
```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 | 统计信息 |
```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=<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 终止 + 按域名分流 + 统一认证。
```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": "<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)
```toml
# 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
```
```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 <token>`
部署时通过环境变量注入令牌:
```bash
docker run -d ... \
-e NAVI_SYNC_TOKEN=<长随机串> \ # 浏览器扩展同步用
-e NAVI_ADMIN_TOKEN=<长随机串> \ # 管理 API 写操作用
jianf-navi:latest
```
浏览器扩展在设置里填同一个 `NAVI_SYNC_TOKEN`,每次同步请求带 `Authorization: Bearer <token>`。
```bash
# 管理 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 配置,如代理
```
```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