飞牛 fnOS 集成 OnlyOffice + Photopea 完整指南
本指南记录在飞牛 fnOS 上部署 OnlyOffice 和 Photopea 的成功路径,涵盖局域网和外网访问场景。
目录
- 目标概述
- 是否需要公网
- 前置准备
- OnlyOffice 部署
- Photopea 部署
- 外网访问配置
- 需要避免的错误
- 常见问题排查
1. 目标概述
OnlyOffice 实现目标
- 局域网和外网均可通过桌面图标或右键菜单新建/打开/编辑 Office 文档(Word、Excel、PPT)
- 右键文档文件 → "OnlyOffice 编辑" 直接在线编辑
- 桌面图标一键新建文档
- 编辑后自动保存回 NAS 原路径
Photopea 实现目标
- 局域网和外网均可通过右键菜单打开 PSD、AI、TIFF、RAW 等专业图片格式
- 支持选择导出格式(PSD/PNG/JPG/GIF/BMP/TIFF/WebP/SVG)
- 编辑后保存到 NAS 原路径
- JPG/PNG/GIF/HEIC/HEIF/PDF/MOV 双击用系统默认程序打开
2. 是否需要公网
| 场景 |
是否需要公网 |
说明 |
| 仅局域网使用 |
❌ 不需要 |
所有功能在内网即可完成 |
| 外网访问 |
✅ 需要 |
需要公网 IP + 域名 + TLS 证书 + 路由器端口转发 |
外网访问的必要条件:
- 公网 IP(联系运营商获取,或使用 fnOS 自带的 FN Connect 中继)
- 域名(用于 TLS 证书和 WebSocket 连接)
- TLS 证书(Let's Encrypt 免费证书,通过域名验证获取)
- 路由器端口转发(外部端口 → NAS 内部端口)
3. 前置准备
需要在 fnOS 上安装的应用
| 应用 |
用途 |
安装方式 |
| Docker |
容器化部署所有服务 |
fnOS 应用中心 |
| WatchCow(fnOS 内置) |
文件右键菜单 + 桌面图标集成 |
fnOS 系统自带 |
Docker 镜像拉取加速
fnOS 默认 Docker Hub 可能访问缓慢,需配置镜像加速:
# 在 fnOS 终端执行,配置 Docker 镜像加速
# 编辑 /etc/docker/daemon.json,添加镜像源
或使用代理镜像拉取,例如在 docker pull 时使用 docker.1ms.run 前缀。
4. OnlyOffice 部署
4.1 架构概览
用户浏览器
↓
Caddy(反向代理 + HTTPS + WebSocket)
**─ /editor, /new, /open, /callback 等 → editor-server(Flask 应用)
**─ 其余路径 → OnlyOffice Document Server(文档处理核心)
4.2 Docker 容器清单
| 容器名 |
镜像 |
端口 |
作用 |
onlyoffice-documentserver |
onlyoffice/documentserver |
8060(仅内部) |
文档处理核心 |
onlyoffice-editor |
自建 Flask 应用 |
5000(仅内部) |
编辑器入口 + WatchCow 集成 |
onlyoffice-caddy |
sliamb/caddy:latest |
9080(HTTP)、9443(HTTPS) |
反向代理 |
4.3 关键配置文件
docker-compose.yaml 核心结构
services:
caddy:
image: sliamb/caddy:latest
container_name: onlyoffice-caddy
ports:
- "9080:80" # 局域网 HTTP
- "9443:9443" # 外网 HTTPS
volumes:
- ./Caddyfile:/data/caddyfile.txt:ro
- ./cert.pem:/certs/cert.pem:ro # TLS 证书
- ./key.pem:/certs/key.pem:ro
labels:
watchcow.enable: "true"
watchcow.appname: "onlyoffice-editor"
watchcow.service_port: "9080"
watchcow.path: "/editor?path="
watchcow.file_types: "docx,doc,xlsx,xls,pptx,ppt,odt,ods,odp,csv,txt,rtf"
editor:
build: ./editor-server
container_name: onlyoffice-editor
environment:
- SHARE_ROOT=/
- DOCUMENT_SERVER_URL=http://host.docker.internal:8060
- JWT_SECRET=<你的JWT密钥>
- HOST_ADDRESS=http://<NAS局域网IP>:9080
- EXTERNAL_BASE_URL=https://<域名>:<外部端口>
volumes:
- /vol1:/vol1:rw
- /vol2:/vol2:rw
documentserver:
image: onlyoffice/documentserver
container_name: onlyoffice_documentserver
ports:
- "127.0.0.1:8060:80"
environment:
- JWT_SECRET=<你的JWT密钥>
Caddyfile 核心结构
:80 {
# editor 路径优先
handle /editor* { reverse_proxy editor:5000 }
handle /new* { reverse_proxy editor:5000 }
handle /open* { reverse_proxy editor:5000 }
handle /callback* { reverse_proxy editor:5000 }
# 其余 → Document Server
handle { reverse_proxy host.docker.internal:8060 }
}
:9443 {
tls /certs/cert.pem /certs/key.pem
# 同上路由规则
handle /editor* { reverse_proxy editor:5000 }
# ...
handle { reverse_proxy host.docker.internal:8060 }
}
4.4 editor-server 核心逻辑
editor-server/app.py 是一个 Flask 应用,核心功能:
/editor?path=<文件路径> — 返回编辑器 HTML 页面,加载 OnlyOffice JS SDK
/new?filetype=<类型> — 创建新文档,返回编辑器页面
/callback — 接收 OnlyOffice Document Server 的保存回调,将文件写回 NAS
/open?path=<文件路径> — 提供文件下载(供 Document Server 拉取文件)
4.5 JWT 密钥一致性
所有容器的 JWT 密钥必须一致。需要在以下位置配置相同的密钥:
- Document Server 环境变量
JWT_SECRET
- editor-server 环境变量
JWT_SECRET
- Document Server 内部配置文件(
/etc/onlyoffice/documentserver/local.json)
5. Photopea 部署
5.1 架构概览
用户浏览器
↓
Caddy(反向代理 + HTTPS)
**─ /editor, /open, /save, /launch → file-proxy(Flask 应用)
**─ 其余路径 → Photopea(静态文件服务)
5.2 Docker 容器清单
| 容器名 |
镜像 |
端口 |
作用 |
photopea-app |
ramuses/photopea:latest |
8887(仅内部) |
Photopea 静态文件服务(238MB,轻量) |
photopea-file-proxy |
自建 Flask 应用 |
5001(仅内部) |
文件桥接 + 编辑器页面 |
photopea-caddy |
sliamb/caddy:latest |
9090(HTTP)、9091(HTTPS) |
反向代理 |
5.3 关键配置文件
docker-compose.yaml 核心结构
services:
caddy:
image: sliamb/caddy:latest
container_name: photopea-caddy
ports:
- "9090:80" # 局域网 HTTP
- "9091:9443" # 外网 HTTPS
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./cert.pem:/certs/cert.pem:ro
- ./key.pem:/certs/key.pem:ro
labels:
watchcow.enable: "true"
watchcow.appname: "photopea-editor"
watchcow.service_port: "9090"
watchcow.path: "/editor?path="
# 所有图片格式都注册右键菜单,但双击行为由系统默认程序控制
watchcow.file_types: "psd,ai,xd,fig,sketch,tiff,bmp,webp,svg,ico,tga,dds,raw,cr2,nef,arw,dng,xcf,png,jpg,jpeg,gif,heic,heif,pdf,mov"
photopea:
image: ramuses/photopea:latest
container_name: photopea-app
networks:
- photopea-net
expose:
- "8887"
file-proxy:
build: ./file-proxy
container_name: photopea-file-proxy
environment:
- SHARE_ROOT=/
- PHOTOPEA_URL=/
- EXTERNAL_HOST=https://<域名>:<外部端口>
volumes:
- /vol1:/vol1:rw
- /vol2:/vol2:rw
file-proxy 核心路由
| 路由 |
功能 |
/editor?path=<文件路径> |
返回编辑器页面(iframe + Photopea + 保存工具栏) |
/open?path=<文件路径> |
提供文件下载(供 Photopea 打开) |
/save?path=<文件路径> |
接收文件数据,写回 NAS |
/launch |
桌面图标入口(重定向到首页) |
/health |
健康检查 |
5.4 编辑器页面设计
编辑器采用 iframe + hash 配置 模式:
<!-- iframe src 直接用 hash 传递配置,Photopea 自动打开文件 -->
<iframe src="/#{"files":["http://IP:PORT/open?path=/vol/xxx/file.psd"]}"></iframe>
<!-- 底部工具栏:格式选择 + 保存按钮 -->
<div id="toolbar">
<span>📄 文件名</span>
<select id="format-select">
<option>PSD</option>
<option>PNG</option>
<option>JPEG</option>
<!-- ... -->
</select>
<button *="saveToNas()">💾 保存到 NAS</button>
</div>
5.5 保存流程
- 用户点击"保存到 NAS"
- JavaScript 调用
ppWindow.postMessage("app.activeDocument.saveToOE('格式')")
- Photopea 导出文件为 ArrayBuffer
- 通过
fetch POST /save?path=<路径> 发送到 file-proxy
- file-proxy 将数据写入 NAS 对应路径
6. 外网访问配置
6.1 证书准备
使用 Let's Encrypt 免费 TLS 证书(有效期 90 天,可自动续期):
# 方式一:使用 certbot 申请
certbot certonly --standalone -d your-domain.com
# 方式二:使用 DNS 验证(适合没有 80 端口的情况)
certbot certonly --manual --preferred-challenges dns -d *.your-domain.com
# 证书文件位置
# /etc/letsencrypt/live/your-domain.com/fullchain.pem → 合并为 cert.pem
# /etc/letsencrypt/live/your-domain.com/privkey.pem → 复制为 key.pem
注意:Caddy 需要将证书链合并到 cert.pem 中:
cat fullchain.pem privkey.pem > cert.pem
6.2 路由器端口转发
在路由器管理界面配置端口转发规则:
| 服务 |
外部端口 |
内部 IP |
内部端口 |
协议 |
| OnlyOffice |
自定义(如 52000) |
NAS IP |
9443 |
TCP |
| Photopea |
自定义(如 52001) |
NAS IP |
9091 |
TCP |
6.3 DNS 配置
在域名管理面板添加 A 记录:
onlyoffice.your-domain.com → 公网 IP
photopea.your-domain.com → 公网 IP
7. 需要避免的错误
7.1 WebSocket 连接失败导致编辑器报错
现象:外网访问时编辑器提示"下载失败"或白屏
原因:反向代理阻断了 WebSocket 连接。OnlyOffice Document Server 的协同编辑通道走 WebSocket。
解决方案:
- 使用 Caddy 而非其他反向代理(Caddy 原生支持 WebSocket 透传)
- 确保 WebSocket 头部正确转发(
Upgrade、Connection 头)
- 避免使用某些 NAS 自带的反代工具(可能不支持 WebSocket)
7.2 Python 模板中 \n 导致白屏
现象:编辑器页面白屏,浏览器控制台报 Uncaught SyntaxError: Invalid or unexpected token
原因:Python 的 .format() 或 f-string 中的 \n 被解释为真实换行符,输出到 HTML/JavaScript 后破坏了语法结构。
解决方案:在 Python 模板字符串中,JavaScript 内的 \n 必须写成 \\n:
# ❌ 错误
template = "var s = 'line1\\nline2';"
# ✅ 正确
template = "var s = 'line1\\\\nline2';"
7.3 SSL 证书验证失败
现象:文件保存回调失败,日志显示 SSL: CERTIFICATE_VERIFY_FAILED
原因:Document Server 回调时使用外网 HTTPS 地址,但自签名证书不被信任。
解决方案:
- 使用 Let's Encrypt 正式证书(推荐)
- 或在回调函数中将外网地址替换为内网直连地址,使用
verify=False:
# 回调中替换下载地址
def callback():
download_url = data.get("download_url", "")
# 外网地址 → 内网直连地址
download_url = download_url.replace("https://domain:port", "http://host.docker.internal:8060")
resp = requests.get(download_url, verify=False)
7.4 iframe src 中文件路径编码问题
现象:通过右键菜单打开文件时,路径中的 @ 符号被错误编码为 40
原因:URL 编码/解码过程中的符号处理问题
解决方案:使用 encodeURIComponent 编码路径参数,并在服务端使用 unquote_plus 解码:
from urllib.parse import unquote_plus, quote
# 编码
encoded_path = quote(file_path, safe="")
# 解码
raw_path = unquote_plus(encoded_path)
7.5 Photopea 离线版不支持 postMessage API
现象:iframe 加载 Photopea 后,文件没有自动打开,显示首页
原因:离线版 Photopea 不支持通过 postMessage 传递配置来打开文件。
解决方案:使用 URL hash 配置 代替 postMessage:
# 通过 iframe src 的 hash 传递配置
config = json.dumps({"files": ["http://IP:PORT/open?path=/vol/xxx/file.psd"]})
encoded_config = quote(config, safe="")
iframe_src = "/" + "#" + encoded_config
7.6 Photopea 保存按钮一直处于禁用状态
现象:底部工具栏的"保存到 NAS"按钮始终灰色不可点击,状态显示"加载中..."
原因:等待 Photopea 发送 app.opened 事件来启用按钮,但离线版不会发送此事件。
解决方案:iframe onload 后延迟 2 秒直接启用按钮,不依赖 Photopea 事件:
ppFrame.onload = function() {
ppWindow = ppFrame.contentWindow;
setTimeout(function() {
document.getElementById('btn-save').disabled = false;
document.getElementById('save-status').textContent = '就绪';
}, 2000);
};
7.7 Caddy 配置路径错误
现象:Caddy 启动失败或配置不生效
原因:Caddy 配置文件挂载路径不正确。使用 caddyfile adapter 时,配置文件路径必须与 command 中指定的一致。
解决方案:
# docker-compose.yaml
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro # 正确路径
command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
7.8 Docker 命令权限问题
现象:docker 命令执行失败,提示权限不足
原因:fnOS 的 Docker 需要通过特定方式调用。
解决方案:使用 sg 命令切换到 docker 组执行:
/usr/bin/sg docker -c "docker compose up -d"
7.9 删除文件被安全机制拦截
现象:rm -f 命令被拦截,无法删除文件
原因:fnOS 安全机制拦截了 rm 命令。
解决方案:使用 truncate 命令清空文件内容代替删除:
truncate -s 0 filename
8. 常见问题排查
8.1 查看容器状态
# 查看所有容器
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
# 查看特定容器日志
docker logs <container_name> --tail 50
# 查看容器内进程
docker exec <container_name> ps aux
8.2 测试服务连通性
# 测试 HTTP 入口
curl -s http://127.0.0.1:<端口>/health
# 测试 HTTPS 入口
curl -sk https://127.0.0.1:<HTTPS端口>/health
# 测试文件下载接口
curl -s http://127.0.0.1:<端口>/open?path=/vol/xxx/test.txt -o /dev/null -w "%{http_code}"
8.3 重建容器
修改配置后需要重建容器:
cd /path/to/project
/usr/bin/sg docker -c "docker compose up -d --force-recreate <服务名>"
8.4 编辑器代码修改同步
如果修改了 editor-server 或 file-proxy 的 Python 代码,有两种方式同步:
# 方式一:重新构建镜像(推荐)
/usr/bin/sg docker -c "docker compose up -d --build <服务名>"
# 方式二:直接复制文件并重启(快速调试用)
docker cp ./app.py <容器名>:/app/app.py
docker restart <容器名>
附录:端口规划参考
| 服务 |
HTTP 端口 |
HTTPS 端口 |
外部端口(路由器转发) |
| OnlyOffice |
9080 |
9443 |
52000 → 9443 |
| Photopea |
9090 |
9091 |
52001 → 9091 |
端口号可根据实际情况调整,避免与 NAS 上其他服务冲突。