From f788149ca74b1734c3c840ab493f13aafc032d82 Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Mon, 11 May 2026 17:54:39 +0800 Subject: [PATCH] docs(collabora): organize deployment guides and fix proxy chain --- deploy/collabora-proxy/conf.d/collabora.conf | 56 +- docs/Collabora/README.md | 44 + .../参考资料/Collabora Docker镜像.md | 10 + .../Collabora canvas 滚动与增量更新机制.md | 632 ++++++++++++++ .../参考资料/Collabora canvas 绘制.md | 244 ++++++ docs/Collabora/参考资料/Collabora编辑栏.md | 23 + docs/Collabora/参考资料/Collabora请求流程.md | 52 ++ docs/Collabora/参考资料/LibreOffice UNO.md | 3 + .../参考资料/PostMessage API 接口.md | 236 ++++++ docs/Collabora/参考资料/建议提示框.md | 4 + docs/Collabora/参考资料/新版本提示框.md | 4 + docs/Collabora/参考资料/部署.md | 549 ++++++++++++ docs/Collabora/参考资料/镜像备份.md | 4 + docs/Collabora/参考资料/高亮.md | 66 ++ docs/Collabora/部署排障/IP链路部署与排障.md | 247 ++++++ docs/Collabora/部署排障/正确配置全流程.md | 800 ++++++++++++++++++ 16 files changed, 2959 insertions(+), 15 deletions(-) create mode 100644 docs/Collabora/README.md create mode 100644 docs/Collabora/参考资料/Collabora Docker镜像.md create mode 100644 docs/Collabora/参考资料/Collabora canvas 滚动与增量更新机制.md create mode 100644 docs/Collabora/参考资料/Collabora canvas 绘制.md create mode 100644 docs/Collabora/参考资料/Collabora编辑栏.md create mode 100644 docs/Collabora/参考资料/Collabora请求流程.md create mode 100644 docs/Collabora/参考资料/LibreOffice UNO.md create mode 100644 docs/Collabora/参考资料/PostMessage API 接口.md create mode 100644 docs/Collabora/参考资料/建议提示框.md create mode 100644 docs/Collabora/参考资料/新版本提示框.md create mode 100644 docs/Collabora/参考资料/部署.md create mode 100644 docs/Collabora/参考资料/镜像备份.md create mode 100644 docs/Collabora/参考资料/高亮.md create mode 100644 docs/Collabora/部署排障/IP链路部署与排障.md create mode 100644 docs/Collabora/部署排障/正确配置全流程.md diff --git a/deploy/collabora-proxy/conf.d/collabora.conf b/deploy/collabora-proxy/conf.d/collabora.conf index a87b8cf..6fc15e2 100644 --- a/deploy/collabora-proxy/conf.d/collabora.conf +++ b/deploy/collabora-proxy/conf.d/collabora.conf @@ -10,9 +10,13 @@ server { # Local WOPI routes must stay on the frontend app. location /wopi/ { proxy_pass http://127.0.0.1:5193/wopi/; + proxy_redirect http://127.0.0.1:5193/ http://172.16.0.59:5173/; + proxy_redirect http://localhost:5193/ http://172.16.0.59:5173/; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; 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; @@ -26,14 +30,14 @@ server { # Collabora shell endpoint. location /collabora/ { - proxy_pass http://172.16.0.58:9980/; + proxy_pass http://127.0.0.1:9980/; proxy_http_version 1.1; - proxy_set_header Host nas.7bm.co:5173; + proxy_set_header Host 172.16.0.59:5173; 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; - proxy_set_header X-Forwarded-Host nas.7bm.co:5173; + proxy_set_header X-Forwarded-Host 172.16.0.59:5173; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -46,14 +50,14 @@ server { location /browser/ { # Keep the original escaped URI; Collabora websocket/document paths # break if nginx normalizes `%2F` inside the encoded WOPI URL. - proxy_pass http://172.16.0.58:9980; + proxy_pass http://127.0.0.1:9980; proxy_http_version 1.1; - proxy_set_header Host nas.7bm.co:5173; + proxy_set_header Host 172.16.0.59:5173; 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; - proxy_set_header X-Forwarded-Host nas.7bm.co:5173; + proxy_set_header X-Forwarded-Host 172.16.0.59:5173; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -65,14 +69,32 @@ server { location /cool/ { # Websocket path contains an encoded WOPI URL in the URI path. # Do not append a URI here, otherwise nginx may decode `%2F`. - proxy_pass http://172.16.0.58:9980; + proxy_pass http://127.0.0.1:9980; proxy_http_version 1.1; - proxy_set_header Host nas.7bm.co:5173; + proxy_set_header Host 172.16.0.59:5173; 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; - proxy_set_header X-Forwarded-Host nas.7bm.co:5173; + proxy_set_header X-Forwarded-Host 172.16.0.59:5173; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 60s; + } + + location /coolws/ { + # Child websocket bootstrap path used by Collabora Online. + proxy_pass http://127.0.0.1:9980; + + proxy_http_version 1.1; + proxy_set_header Host 172.16.0.59:5173; + 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; + proxy_set_header X-Forwarded-Host 172.16.0.59:5173; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -82,14 +104,14 @@ server { } location /hosting/ { - proxy_pass http://172.16.0.58:9980; + proxy_pass http://127.0.0.1:9980; proxy_http_version 1.1; - proxy_set_header Host nas.7bm.co:5173; + proxy_set_header Host 172.16.0.59:5173; 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; - proxy_set_header X-Forwarded-Host nas.7bm.co:5173; + proxy_set_header X-Forwarded-Host 172.16.0.59:5173; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -99,14 +121,14 @@ server { } location /loleaflet/ { - proxy_pass http://172.16.0.58:9980; + proxy_pass http://127.0.0.1:9980; proxy_http_version 1.1; - proxy_set_header Host nas.7bm.co:5173; + proxy_set_header Host 172.16.0.59:5173; 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; - proxy_set_header X-Forwarded-Host nas.7bm.co:5173; + proxy_set_header X-Forwarded-Host 172.16.0.59:5173; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -118,9 +140,13 @@ server { # Everything else remains on Next dev server. location / { proxy_pass http://127.0.0.1:5193; + proxy_redirect http://127.0.0.1:5193/ http://172.16.0.59:5173/; + proxy_redirect http://localhost:5193/ http://172.16.0.59:5173/; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; 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; diff --git a/docs/Collabora/README.md b/docs/Collabora/README.md new file mode 100644 index 0000000..cc994e9 --- /dev/null +++ b/docs/Collabora/README.md @@ -0,0 +1,44 @@ +# Collabora 文档索引 + +## 适用场景 + +本目录分成两类内容: + +- `部署排障/` + - 面向实施、运维、上线迁移 + - 用于部署、重建、验收、排错 +- `参考资料/` + - 面向开发联调与功能扩展 + - 用于理解 Collabora 行为、插件、UNO、PostMessage 与渲染机制 + +## 目录结构 + +### 部署排障 + +- `部署排障/正确配置全流程.md` + - 主部署文档 + - 适合新服务器迁移、上线实施、参数替换、全流程验收 +- `部署排障/IP链路部署与排障.md` + - 已落地问题的修复记录 + - 适合快速排查“为什么当前环境不通” + +### 参考资料 + +- `参考资料/Collabora Docker镜像.md` +- `参考资料/镜像备份.md` +- `参考资料/部署.md` +- `参考资料/Collabora请求流程.md` +- `参考资料/PostMessage API 接口.md` +- `参考资料/LibreOffice UNO.md` +- `参考资料/Collabora编辑栏.md` +- `参考资料/高亮.md` +- `参考资料/建议提示框.md` +- `参考资料/新版本提示框.md` +- `参考资料/Collabora canvas 绘制.md` +- `参考资料/Collabora canvas 滚动与增量更新机制.md` + +## 推荐使用顺序 + +1. 先看 `部署排障/正确配置全流程.md` +2. 如果当前环境已经出问题,再看 `部署排障/IP链路部署与排障.md` +3. 需要做功能开发或定制时,再查 `参考资料/` diff --git a/docs/Collabora/参考资料/Collabora Docker镜像.md b/docs/Collabora/参考资料/Collabora Docker镜像.md new file mode 100644 index 0000000..5508896 --- /dev/null +++ b/docs/Collabora/参考资料/Collabora Docker镜像.md @@ -0,0 +1,10 @@ +1. +![](https://cdn.nlark.com/yuque/0/2025/png/53523408/1764224484289-6e522f3d-3785-40ec-8773-f1f2c997e9ba.png) +2. 官方镜像缺少依赖,重新打包了 + +```plain +docker build --no-cache -t docker-collabora:v2 . +``` + +2. + diff --git a/docs/Collabora/参考资料/Collabora canvas 滚动与增量更新机制.md b/docs/Collabora/参考资料/Collabora canvas 滚动与增量更新机制.md new file mode 100644 index 0000000..72e636e --- /dev/null +++ b/docs/Collabora/参考资料/Collabora canvas 滚动与增量更新机制.md @@ -0,0 +1,632 @@ +## 一、Canvas 滚动实现 (上拉下拉操作) +### 1. 核心滚动类: `ScrollSection` +```javascript +// 初始化滚动属性 +ScrollSection.prototype.initialize = function() { + this.sectionProperties = { + // 滚动条样式 + scrollBarThickness: 6 * app.roundedDpiScale, + scrollBarRailwayThickness: 6 * app.roundedDpiScale, + scrollBarRailwayColor: "#EFEFEF", + + // 显示控制 + drawVerticalScrollBar: window.mode.isDesktop() ? true : false, + drawHorizontalScrollBar: window.mode.isDesktop() ? true : false, + + // 交互状态 + mouseIsOnVerticalScrollBar: false, + mouseIsOnHorizontalScrollBar: false, + clickScrollVertical: false, + clickScrollHorizontal: false, + + // 滚动同步 + pointerSyncWithVerticalScrollBar: true, + pointerSyncWithHorizontalScrollBar: true, + + // 最小滚动条大小 + minimumScrollSize: 80 * app.roundedDpiScale, + + // 滚动轮增量 + scrollWheelDelta: [0, 0] + }; +}; +``` + +### 2. 滚动触发方式 +#### **方式 1: 鼠标滚轮** +```javascript +// 监听滚轮事件 +ScrollSection.prototype.onMouseWheel = function(e) { + var delta = e.deltaY || e.deltaX; + + // 垂直滚动 + if (e.deltaY !== 0) { + this.sectionProperties.scrollWheelDelta[1] += e.deltaY; + this.scrollVerticalWithOffset(delta); + } + + // 水平滚动 + if (e.deltaX !== 0) { + this.sectionProperties.scrollWheelDelta[0] += e.deltaX; + this.scrollHorizontalWithOffset(delta); + } +}; +``` + +#### **方式 2: 拖动滚动条** +```javascript +ScrollSection.prototype.onMouseMove = function(point, dragDistance, e) { + if (this.sectionProperties.clickScrollVertical) { + this.showVerticalScrollBar(); + + var diffY = dragDistance[1] - this.sectionProperties.previousDragDistance[1]; + + if (this.isMousePointerSyncedWithVerticalScrollBar(scrollProps, point)) { + // 根据滚动条比例计算实际滚动距离 + this.scrollVerticalWithOffset(diffY * scrollProps.verticalScrollRatio); + } + + this.sectionProperties.previousDragDistance[1] = dragDistance[1]; + } + + // 水平滚动类似 + if (this.sectionProperties.clickScrollHorizontal) { + // ... + } +}; +``` + +#### **方式 3: 点击滚动条轨道** +```javascript +ScrollSection.prototype.quickScrollVertical = function(point) { + var localY = this.getLocalYOnVerticalScrollBar(point); + var scrollProps = app.activeDocument.activeView.scrollProperties; + + // 计算目标滚动位置 + var targetY = (localY / scrollProps.scrollSize) * scrollProps.verticalScrollLength; + + // 滚动到目标位置 + app.activeDocument.activeView.scrollVertical(targetY); +}; +``` + +#### **方式 4: 触摸滑动 (移动端)** +```javascript +ScrollSection.prototype.onScrollVelocity = function(e) { + if (e.vx === 0 && e.vy === 0) { + clearInterval(this.autoScrollTimer); + return; + } + + // 使用速度持续滚动 + this.autoScrollTimer = setInterval(function() { + this.onScrollBy({ + x: e.vx, + y: e.vy + }); + }.bind(this), 100); +}; +``` + +### 3. 滚动核心方法 +#### **垂直滚动** +```javascript +ScrollSection.prototype.scrollVerticalWithOffset = function(offset) { + if (!app.activeDocument.activeView.canScrollVertical(documentAnchor)) { + return; + } + + // 更新视图滚动属性 + app.activeDocument.activeView.scrollVertical(offset); + + // 请求重绘 + app.sectionContainer.requestReDraw(); +}; + +// ViewLayoutBase 中的实际滚动逻辑 +ViewLayoutBase.prototype.scrollVertical = function(pY) { + var scrollProps = this.scrollProperties; + + // 计算新的滚动位置 + var newY = scrollProps.yOffset + pY; + + // 限制在有效范围内 + newY = Math.max(0, Math.min(newY, scrollProps.verticalScrollLength)); + + // 更新滚动偏移 + scrollProps.yOffset = newY; + + // **关键**: 发送新的可见区域到服务器 + this.sendClientVisibleArea(); +}; +``` + +#### **水平滚动** +```javascript +ViewLayoutBase.prototype.scrollHorizontal = function(pX, ignoreScrollbarLength) { + var scrollProps = this.scrollProperties; + + var newX = scrollProps.xOffset + pX; + newX = Math.max(0, Math.min(newX, scrollProps.horizontalScrollLength)); + + scrollProps.xOffset = newX; + + // 发送可见区域更新 + this.sendClientVisibleArea(); +}; +``` + +## 二、滚动条渲染逻辑 +### 1. 垂直滚动条绘制 +```javascript +ScrollSection.prototype.drawVerticalScrollBar = function() { + var scrollProps = app.activeDocument.activeView.scrollProperties; + + // 计算滚动条位置和尺寸 + var startX = this.size[0] - scrollProps.usableThickness; // 右侧位置 + var startY = scrollProps.startY; // 顶部位置 + var scrollBarHeight = scrollProps.scrollSize; // 滚动条高度 + + // 绘制滚动条轨道 (背景) + if (this.sectionProperties.drawScrollBarRailway) { + this.context.globalAlpha = this.sectionProperties.scrollBarRailwayAlpha; + this.context.fillStyle = this.sectionProperties.scrollBarRailwayColor; + this.context.fillRect( + startX, + 0, + this.sectionProperties.scrollBarRailwayThickness, + this.size[1] // 整个高度 + ); + } + + // 绘制滚动条本体 + this.context.globalAlpha = 1.0; + this.context.fillStyle = "#AAAAAA"; // 滚动条颜色 + this.context.fillRect( + startX, + startY, + this.sectionProperties.scrollBarThickness, + scrollBarHeight + ); + + // 测试模式: 创建 DOM 元素用于自动化测试 + if (this.containerObject.testing) { + var element = document.getElementById("test-div-vertical-scrollbar"); + if (!element) { + element = document.createElement("div"); + element.id = "test-div-vertical-scrollbar"; + document.body.appendChild(element); + } + element.style.position = "absolute"; + element.style.left = startX + "px"; + element.style.top = startY + "px"; + element.style.width = this.sectionProperties.scrollBarThickness + "px"; + element.style.height = scrollBarHeight + "px"; + } +}; +``` + +### 2. 滚动条尺寸计算 +```javascript +// 在 ViewLayoutBase 中计算滚动属性 +ViewLayoutBase.prototype.updateScrollProperties = function() { + var scrollProps = this.scrollProperties; + + // 文档总长度 + var documentHeight = this.getDocumentHeight(); // Twips 单位 + + // 可见区域高度 + var viewportHeight = this.getViewportHeight(); // Twips 单位 + + // 滚动条可用空间 + var availableSpace = this.size[1] - 20; // Canvas 高度减去边距 + + // 滚动条高度 = (视口高度 / 文档高度) * 可用空间 + scrollProps.scrollSize = Math.max( + this.sectionProperties.minimumScrollSize, + (viewportHeight / documentHeight) * availableSpace + ); + + // 滚动比例: 滚动条移动1像素,文档滚动多少 Twips + scrollProps.verticalScrollRatio = + (documentHeight - viewportHeight) / (availableSpace - scrollProps.scrollSize); + + // 滚动长度 (文档可滚动的总距离) + scrollProps.verticalScrollLength = documentHeight - viewportHeight; + + // 滚动条当前位置 + scrollProps.startY = + (scrollProps.yOffset / scrollProps.verticalScrollLength) * + (availableSpace - scrollProps.scrollSize); +}; +``` + +### 3. 滚动条交互增强 +```javascript +// 鼠标悬停增粗滚动条 +ScrollSection.prototype.increaseScrollBarThickness = function() { + this.sectionProperties.scrollBarThickness = 8 * app.roundedDpiScale; + this.sectionProperties.scrollBarRailwayThickness = 8 * app.roundedDpiScale; +}; + +ScrollSection.prototype.decreaseScrollBarThickness = function() { + this.sectionProperties.scrollBarThickness = 6 * app.roundedDpiScale; + this.sectionProperties.scrollBarRailwayThickness = 6 * app.roundedDpiScale; +}; + +// 检测鼠标是否在滚动条上 +ScrollSection.prototype.isMouseOnScrollBar = function(point) { + var scrollProps = app.activeDocument.activeView.scrollProperties; + + // 检测垂直滚动条 + var isOnVertical = point.pX >= this.size[0] - scrollProps.usableThickness; + this.sectionProperties.mouseIsOnVerticalScrollBar = isOnVertical; + + if (isOnVertical) { + this.showVerticalScrollBar(); + this.increaseScrollBarThickness(); // 悬停增粗 + } else { + this.hideVerticalScrollBar(); + this.decreaseScrollBarThickness(); + } +}; +``` + +## 三、增量更新 (Delta) 与瓦片请求机制 +### 1. 可见区域更新触发瓦片请求 +```javascript +// 滚动时发送可见区域到服务器 +ViewLayoutBase.prototype.sendClientVisibleArea = function() { + // 获取当前可见区域 (Canvas 坐标) + var visibleArea = app.map.getPixelBounds(); + + // 转换为 Twips 坐标 (文档坐标系统) + visibleArea = new cool.Bounds( + app.map._docLayer._pixelsToTwips(visibleArea.min), + app.map._docLayer._pixelsToTwips(visibleArea.max) + ); + + var size = visibleArea.getSize(); + var visibleTopLeft = visibleArea.min; + + // 构建 clientvisiblearea 命令 + var command = + "clientvisiblearea " + + "x=" + Math.round(visibleTopLeft.x) + " " + + "y=" + Math.round(visibleTopLeft.y) + " " + + "width=" + Math.round(size.x) + " " + + "height=" + Math.round(size.y); + + // **发送到服务器** + app.socket.sendMessage(command); +}; + +// 多页视图的可见区域计算 +MultiPageViewLayout.prototype.sendClientVisibleArea = function() { + var visibleArea = this.getVisibleAreaRectangle(); + + var command = + "clientvisiblearea " + + "x=" + visibleArea.x1 + " " + + "y=" + visibleArea.y1 + " " + + "width=" + visibleArea.width + " " + + "height=" + visibleArea.height; + + app.socket.sendMessage(command); +}; +``` + +### 2. 服务器响应: invalidatetiles 消息 +```javascript +// 服务器检测到可见区域变化后,发送 invalidatetiles 消息 +// 格式: "invalidatetiles: part=0 x=0 y=5120 width=21000 height=7680" + +CanvasTileLayer.prototype.handleInvalidateTilesMsg = function(textMsg) { + var payload = textMsg.substring("invalidatetiles:".length + 1); + + if (!payload.startsWith("EMPTY")) { + // 解析失效区域 + var tokens = payload.split(" "); + var invalidRect = { + part: 0, + x: 0, + y: 0, + width: 0, + height: 0 + }; + + for (var i = 0; i < tokens.length; i++) { + if (tokens[i].startsWith("part=")) { + invalidRect.part = parseInt(tokens[i].substring(5)); + } else if (tokens[i].startsWith("x=")) { + invalidRect.x = parseInt(tokens[i].substring(2)); + } else if (tokens[i].startsWith("y=")) { + invalidRect.y = parseInt(tokens[i].substring(2)); + } else if (tokens[i].startsWith("width=")) { + invalidRect.width = parseInt(tokens[i].substring(6)); + } else if (tokens[i].startsWith("height=")) { + invalidRect.height = parseInt(tokens[i].substring(7)); + } + } + + // 标记受影响的瓦片为失效 + TileManager.overlapInvalidatedRectangleWithView( + invalidRect.part, + 0, // mode + null, // wireId + invalidRect, + textMsg + ); + } else { + // EMPTY 表示所有瓦片都失效,需要全部重新请求 + TileManager.invalidateAllTiles(); + } +}; +``` + +### 3. 瓦片失效处理 +```javascript +TileManager.overlapInvalidatedRectangleWithView = function( + part, mode, wireId, invalidatedRectangle, textMsg +) { + var needsNewTiles = false; + + // 遍历所有已加载的瓦片 + for (var [key, tile] of this.tiles) { + var coords = tile.coords; + + if (coords.part === part && coords.mode === mode) { + // 计算瓦片的矩形区域 + var tileRectangle = this.getTileRectangle(coords); + + // 检查瓦片是否与失效区域重叠 + if (invalidatedRectangle.intersectsRectangle(tileRectangle)) { + // 如果瓦片在当前视口内,标记需要新瓦片 + if (tile.distanceFromView === 0) { + needsNewTiles = true; + } + + // 标记瓦片为失效 + this.invalidateTile(key, wireId); + } + } + } + + // 如果有失效的可见瓦片,触发更新 + if (needsNewTiles) { + TileManager.update(); // 请求新的瓦片 + } +}; + +TileManager.invalidateTile = function(key, wireId) { + var tile = this.tiles.get(key); + if (!tile) return; + + tile.invalidateCount++; + + // 强制请求关键帧 (完整瓦片) + tile.forceKeyframe(wireId ? wireId : tile.wireId); +}; +``` + +### 4. 增量更新 (Delta) 实现 +#### **Delta 消息格式** +```javascript +// 服务器发送的 delta 消息 +// 格式: "delta: nviewid=0 part=0 width=256 height=256 tileposx=0 tileposy=0 tilewidth=3840 tileheight=3840 ver=123 wireId=456" +// 后面跟着二进制数据: [keyframe_size (4 bytes)] [keyframe_image] [delta1] [delta2] ... + +CanvasTileLayer.prototype._onMessage = function(textMsg, img) { + if (textMsg.startsWith("tile:") || textMsg.startsWith("delta:")) { + TileManager.onTileMsg(textMsg, img); + } +}; +``` + +#### **应用 Delta** +```javascript +TileManager.applyDelta = function(tile, rawDeltas, deltas, keyframeDeltaSize, keyframeImage) { + var tileSize = 256; // 瓦片大小 + + // 1. 如果有关键帧,先加载关键帧 + if (keyframeDeltaSize > 0 && keyframeImage) { + tile.image = keyframeImage; // PNG 图像 + tile.loadCount++; + tile.deltaCount = 0; + tile.updateCount = 0; + } else { + tile.deltaCount++; + } + + // 2. 获取瓦片的当前像素数据 + var canvas = document.createElement("canvas"); + canvas.width = tileSize; + canvas.height = tileSize; + var ctx = canvas.getContext("2d"); + + // 绘制当前瓦片图像 + if (tile.image) { + ctx.drawImage(tile.image, 0, 0); + } + + var imageData = ctx.getImageData(0, 0, tileSize, tileSize); + var oldData = new Uint8ClampedArray(imageData.data); // 保存旧数据 + + // 3. 应用所有 delta + var offset = 0; + for (var i = 0; i < deltas.length; i++) { + var delta = deltas[i]; + + // 应用单个 delta 块 + var len = CanvasTileUtils.applyDeltaChunk( + imageData, + delta, + oldData, + tileSize, + tileSize, + false // debug + ); + + offset += len; + } + + // 4. 将更新后的像素数据写回 Canvas + ctx.putImageData(imageData, 0, 0); + + // 5. 将 Canvas 转换为图像 + tile.image = canvas; + + // 6. 标记需要重绘 + tile.needsRedraw = true; +}; +``` + +#### **Delta 块解码算法** +```javascript +CanvasTileUtils.applyDeltaChunk = function(imgData, delta, oldData, width, height, debug) { + var pixSize = 4; // RGBA 4 字节 + var offset = 0; + var pixels = imgData.data; + + while (offset < delta.length) { + // 读取操作码 + var opcode = delta[offset++]; + + if (opcode === 99) { // 'c' - Copy (复制旧数据) + // 格式: 'c' [count] [source_offset] + var count = delta[offset++] | (delta[offset++] << 8); + var srcOffset = delta[offset++] | (delta[offset++] << 8) | (delta[offset++] << 16); + + // 从旧数据复制像素 + for (var i = 0; i < count; i++) { + var srcIdx = (srcOffset + i) * pixSize; + var dstIdx = (currentPixel + i) * pixSize; + + pixels[dstIdx + 0] = oldData[srcIdx + 0]; // R + pixels[dstIdx + 1] = oldData[srcIdx + 1]; // G + pixels[dstIdx + 2] = oldData[srcIdx + 2]; // B + pixels[dstIdx + 3] = oldData[srcIdx + 3]; // A + } + + currentPixel += count; + + } else if (opcode === 100) { // 'd' - Diff (差异数据) + // 格式: 'd' [count] [pixel1_rgba] [pixel2_rgba] ... + var count = delta[offset++] | (delta[offset++] << 8); + + // 直接写入新的像素数据 + for (var i = 0; i < count; i++) { + var idx = currentPixel * pixSize; + pixels[idx + 0] = delta[offset++]; // R + pixels[idx + 1] = delta[offset++]; // G + pixels[idx + 2] = delta[offset++]; // B + pixels[idx + 3] = delta[offset++]; // A + currentPixel++; + } + + } else { + console.error("Unknown delta opcode:", opcode); + break; + } + } + + return offset; // 返回处理的字节数 +}; +``` + +### 5. 完整的滚动 → 瓦片请求 → Delta 更新流程 +```javascript +┌─────────────────────────────────────────────────────────────┐ +│ 1. 用户滚动 (鼠标滚轮/拖动滚动条/触摸滑动) │ +│ → ScrollSection.onMouseWheel() │ +│ → ScrollSection.scrollVerticalWithOffset(offset) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. 更新视图偏移 │ +│ → ViewLayoutBase.scrollVertical(offset) │ +│ → scrollProps.yOffset += offset │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. 发送可见区域到服务器 │ +│ → ViewLayoutBase.sendClientVisibleArea() │ +│ → WebSocket: "clientvisiblearea x=0 y=5120 width=21000 │ +│ height=7680" │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 4. 服务器检测新瓦片需求 │ +│ - 计算哪些瓦片在新的可见区域内 │ +│ - 检查客户端是否已缓存这些瓦片 │ +│ - 如果已缓存且未修改,无需发送 │ +│ - 如果未缓存或已修改,发送 delta 或完整瓦片 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 5. 服务器发送 invalidatetiles 消息 │ +│ WebSocket ← "invalidatetiles: part=0 x=0 y=5120 │ +│ width=21000 height=7680" │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 6. 客户端处理失效消息 │ +│ → CanvasTileLayer.handleInvalidateTilesMsg() │ +│ → TileManager.overlapInvalidatedRectangleWithView() │ +│ → TileManager.invalidateTile(key, wireId) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 7. 请求新瓦片 (如果需要) │ +│ → TileManager.update() │ +│ → WebSocket: "tilecombine part=0 tileposx=0,256,512 │ +│ tileposy=5120,5120,5120 ..." │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 8. 服务器发送 delta 或完整瓦片 │ +│ WebSocket ← "delta: part=0 width=256 height=256 │ +│ wireId=123 ..." │ +│ + 二进制数据: [keyframe_size][keyframe_png][delta_data] │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 9. 客户端应用 delta │ +│ → TileManager.onTileMsg(textMsg, img) │ +│ → TileManager.applyDelta(tile, deltas, keyframe) │ +│ → CanvasTileUtils.applyDeltaChunk() - 逐块解码 │ +│ → 更新 tile.image │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 10. 重绘 Canvas │ +│ → app.sectionContainer.requestReDraw() │ +│ → ScrollSection.onDraw() │ +│ → ctx.drawImage(tile.image, dx, dy) │ +│ → 用户看到更新后的文档内容 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 总结 +### 1. **Canvas 的上拉下拉操作** ++ ✅ **可以实现**: 通过 `ScrollSection` 类处理所有滚动事件 ++ 支持多种输入方式: 鼠标滚轮、拖动滚动条、点击轨道、触摸滑动 ++ 核心方法: `scrollVerticalWithOffset()` / `scrollHorizontalWithOffset()` + +### 2. **滚动条渲染** ++ 使用 Canvas 2D API 直接绘制滚动条 (不是 HTML 元素) ++ 位置: 垂直滚动条在右侧, 水平滚动条在底部 ++ 动态计算: 滚动条大小根据文档长度与视口大小的比例动态计算 ++ 交互增强: 鼠标悬停时滚动条增粗 (6px → 8px) + +### 3. **增量更新 (Delta) 机制** ++ **触发**: 滚动时发送 `clientvisiblearea` 命令到服务器 ++ **响应**: 服务器发送 `invalidatetiles` 标记失效区域 ++ **请求**: 客户端发送 `tilecombine` 请求新瓦片 ++ **优化**: 服务器发送 `delta` 消息而非完整瓦片 + - Delta 格式: 复制指令 ('c') + 差异数据 ('d') + - 大幅减少网络传输 (通常节省 70-90% 带宽) ++ **应用**: 客户端解码 delta 并更新现有瓦片的像素数据 + +这种机制实现了**高效的文档滚动和实时协作编辑**,类似于视频编码中的 I 帧(关键帧)和 P 帧(增量帧)技术。 + diff --git a/docs/Collabora/参考资料/Collabora canvas 绘制.md b/docs/Collabora/参考资料/Collabora canvas 绘制.md new file mode 100644 index 0000000..465b767 --- /dev/null +++ b/docs/Collabora/参考资料/Collabora canvas 绘制.md @@ -0,0 +1,244 @@ +## canvas 绘制(dist\bundle.js) +```plain +┌─────────────────────────────────────────────────────────────┐ +│ 1. 用户打开 cool.html │ +│ 参数: WOPISrc=..., access_token=... │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. global.js 初始化 │ +│ - 检测浏览器类型 (BrowserProperties) │ +│ - 创建 WebSocket 连接 │ +│ - 发送 "load url=..." 命令 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. bundle.js 加载并处理 "status:" 消息 │ +│ status: { type: "text", parts: 5, width: 21000, ... } │ +│ → 创建 L.WriterTileLayer │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 4. TileManager 请求瓦片 │ +│ → "tilecombine part=0 tileposx=0,256,512 ..." │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 5. 服务器返回瓦片数据 │ +│ textMsg: "tile: nviewid=0 part=0 width=256 height=256" │ +│ img: ImageBitmap (二进制图像数据) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 6. TileManager.onTileMsg() 处理 │ +│ - 解析 tileMsgObj │ +│ - 存储到 _tiles Map │ +│ - 标记需要重绘 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 7. CanvasSectionContainer 绘制 │ +│ ctx.drawImage(tile.image, sx, sy, sw, sh, dx, dy, dw, dh)│ +│ → 用户看到完整的文档页面 │ +└─────────────────────────────────────────────────────────────┘ + +``` + ++ **文档类型检测**: `_docType` 属性标识文档类型 ("text", "spreadsheet", "presentation", "drawing") ++ **消息处理**: `_onMessage(textMsg, img)` 是核心消息处理函数 ++ **WriterTileLayer**: 专门用于处理 Word 文档 (docx) 的图层类 ++ **瓦片消息**: 通过 WebSocket 接收 "tile:" 和 "tilecombine:" 消息 ++ **Canvas 渲染**: 使用 `canvas.drawImage()` 将瓦片图像绘制到 Canvas 上 + +### 核心架构 +### 1. 文档类型层次结构 +```javascript +L.Layer (基类) + ↓ +L.CanvasTileLayer (瓦片图层基类) + ↓ + ├── L.WriterTileLayer (Word 文档 - docx) + ├── L.CalcTileLayer (电子表格 - xlsx) + └── L.ImpressTileLayer (演示文稿 - pptx/odp) +``` + +### 2. DOCX 文档数据对象结构 +#### **TileMsgObj** (瓦片消息对象) +```javascript +{ + id: number, // 瓦片唯一标识 + width: number, // 瓦片宽度 (像素) + height: number, // 瓦片高度 (像素) + part: number, // 文档部分 (页码) + mode: number, // 渲染模式 (默认 0) + nviewid: number, // 视图ID + wireId: number, // 传输ID (用于增量更新) + zoom: number // 缩放级别 +} +``` + +#### **文档层对象** (L.WriterTileLayer) +```javascript +{ + _docType: "text", // 文档类型标识 + _selectedPart: number, // 当前选中的页码 + _selectedMode: number, // 当前模式 + _viewId: number, // 视图ID + _debug: { // 调试信息 + tileInvalidationsOn: boolean, + tileOverlaysOn: boolean + }, + _onMessage: Function // 核心消息处理函数 +} +``` + +### 渲染流程 +### 第一阶段: 初始化与连接 +```javascript +1. cool.html 加载 + ↓ +2. global.js 初始化浏览器属性和配置 + ↓ +3. 创建 WebSocket 连接 + websocketURI = "ws://collabora-server/cool/{docURL}" + ↓ +4. 发送 load 命令 + "load url={encodedDocURL} lang=zh-CN accessibilityState=..." +``` + +### 第二阶段: 文档加载 +```javascript +1. 服务器返回 "status:" 消息 + 包含文档类型、尺寸、页数等元数据 + ↓ +2. 根据文档类型创建对应的 TileLayer + if (command.type === "text") + docLayer = new L.WriterTileLayer(options) + ↓ +3. 设置文档属性 + docLayer._docType = "text" + docLayer.options = { + tileWidthTwips: 3840, + tileHeightTwips: 3840, + tileSize: 256 // 默认瓦片大小 256x256 像素 + } +``` + +### 第三阶段: 瓦片渲染 +#### **瓦片请求** +```javascript +// 客户端发送瓦片组合请求 +"tilecombine nviewid=0 part=0 width=256 height=256 + tileposx=0,256,512 tileposy=0,0,0 tilewidth=3840 tileheight=3840" +``` + +#### **瓦片接收与处理** +```javascript +TileManager.onTileMsg(textMsg, img) { + // 1. 解析瓦片消息 + var tileMsgObj = parseServerCmd(textMsg); + // textMsg 格式: "tile: nviewid=0 part=0 width=256 height=256 ..." + + // 2. 转换为坐标 + var coords = tileMsgToCoords(tileMsgObj); + // coords = { x, y, z (zoom), part, mode } + + // 3. 获取或创建瓦片对象 + var tile = this.get(coords); + tile.viewId = tileMsgObj.nviewid; + tile.wireId = tileMsgObj.wireId; + tile.image = img; // ImageBitmap 或 Image 对象 + + // 4. 标记需要重绘 + this.rehydrateTile(tile, true); +} +``` + +### 第四阶段: Canvas 绘制 +#### **CanvasSectionContainer** (Canvas 容器管理) +```javascript +{ + canvas: HTMLCanvasElement, // 主 Canvas 元素 + context: CanvasRenderingContext2D, // 2D 渲染上下文 + width: number, // Canvas 宽度 + height: number, // Canvas 高度 + clearColor: string // 背景色 +} +``` + +#### **瓦片绘制到 Canvas** +```javascript +// 核心绘制逻辑 +function paintTile(tile, canvas) { + // 1. 获取 2D 渲染上下文 + var ctx = canvas.getContext("2d"); + + // 2. 计算源矩形 (瓦片图像中的区域) + var sx = 0, sy = 0; + var sWidth = tile.image.width; + var sHeight = tile.image.height; + + // 3. 计算目标矩形 (Canvas 中的位置) + var dx = tile.coords.x * 256; // 目标 X 坐标 + var dy = tile.coords.y * 256; // 目标 Y 坐标 + var dWidth = 256; // 目标宽度 + var dHeight = 256; // 目标高度 + + // 4. 绘制瓦片 + if (tile.image) { + canvas.drawImage( + tile.image, // 源图像 + sx, sy, // 源起点 + sWidth, sHeight, // 源尺寸 + dx, dy, // 目标起点 + dWidth, dHeight // 目标尺寸 + ); + } +} +``` + +### 第五阶段: 增量更新 (Delta) +```javascript +// 服务器发送增量更新而非完整瓦片 +if (textMsg.startsWith("delta:")) { + var deltaObj = parseServerCmd(textMsg); + // 应用增量到现有瓦片 + applyDelta(tile, deltaObj); +} +``` + +### 关键特性 +### 1. **瓦片缓存机制** +```javascript +TileManager._tiles = new Map(); // 缓存所有已加载的瓦片 +// Key: "z:x:y:part:mode" +// Value: { image, wireId, viewId, coords } +``` + +### 2. **视口可见性优化** +```javascript +// 仅请求当前视口内的瓦片 +clientvisiblearea = "0;0;{width};{height}" +// 优先级: 视口中心 > 视口边缘 > 视口外 +``` + +### 3. **消息队列** +```javascript +global.queueMsg = []; // 在文档层准备好之前缓存消息 +socket.onmessage = function(event) { + if (typeof socket._onMessage === "function") { + socket._emptyQueue(); + socket._onMessage(event); + } else { + queueMsg.push(event.data); // 延迟处理 + } +} +``` + +### 4. **多用户协作** +```javascript +// 每个用户视图有独立的 viewId +tileMsgObj.nviewid = 0; // 当前用户 +// 其他用户的光标、选择通过单独的消息同步 +``` + diff --git a/docs/Collabora/参考资料/Collabora编辑栏.md b/docs/Collabora/参考资料/Collabora编辑栏.md new file mode 100644 index 0000000..754edea --- /dev/null +++ b/docs/Collabora/参考资料/Collabora编辑栏.md @@ -0,0 +1,23 @@ +编辑栏的 HTML + +```html + +``` + diff --git a/docs/Collabora/参考资料/Collabora请求流程.md b/docs/Collabora/参考资料/Collabora请求流程.md new file mode 100644 index 0000000..0766044 --- /dev/null +++ b/docs/Collabora/参考资料/Collabora请求流程.md @@ -0,0 +1,52 @@ +`1️⃣ 前端请求流程 + + 用户点击预览 + ↓ + CollaboraViewer.tsx (组件) + ↓ + fetch('/api/collabora/view?fileId=xxx&mode=view&userId=xxx&userName=xxx') + ↓ + 后端 API: app/api/collabora/view/route.ts + ↓ + 返回配置 { iframeUrl, accessToken, wopiSrc, ... } + ↓ + 前端渲染 + + 2️⃣ Collabora 加载文档流程 + + 浏览器加载 iframe + ↓ + Collabora 服务器 ([http://172.16.0.81:9980](http://172.16.0.81:9980)) + ↓ + Collabora 解析 URL 中的 WOPISrc 和 access_token + ↓ + Collabora 调用: GET {WOPISrc}?access_token=xxx + 实际: GET [http://172.16.0.78:3000/api/collabora/wopi/files/contracts/xxx.docx?access_token=xxx](http://172.16.0.78:3000/api/collabora/wopi/files/contracts/xxx.docx?access_token=xxx) + ↓ + 后端 WOPI 端点: app/api/collabora/wopi/files/[...fileId]/route.ts + - 验证 JWT token + - 返回 CheckFileInfo (文件元数据) + ↓ + Collabora 再次调用: GET {WOPISrc}/contents?access_token=xxx + ↓ + 后端 WOPI 端点检测到 /contents 后缀 + - 从 MinIO 读取文件内容 + - 返回文件二进制数据 + ↓ + Collabora 在 iframe 中渲染文档 + + 3️⃣ 保存文档流程(编辑模式) + + 用户编辑文档 → 点击保存/关闭 + ↓ + Collabora 调用: POST {WOPISrc}/contents?access_token=xxx + 请求体: 编辑后的文档二进制数据 + ↓ + 后端 WOPI 端点 POST 处理器 + - 验证 token 和权限 + - 将文档保存到 MinIO(覆盖原文件) + ↓ + 返回 200 OK + ↓ + Collabora 显示保存成功 + diff --git a/docs/Collabora/参考资料/LibreOffice UNO.md b/docs/Collabora/参考资料/LibreOffice UNO.md new file mode 100644 index 0000000..ea5c3b1 --- /dev/null +++ b/docs/Collabora/参考资料/LibreOffice UNO.md @@ -0,0 +1,3 @@ +1. 回第一页:`.uno:GoToStartOfDoc` `{}` +2. + diff --git a/docs/Collabora/参考资料/PostMessage API 接口.md b/docs/Collabora/参考资料/PostMessage API 接口.md new file mode 100644 index 0000000..401f260 --- /dev/null +++ b/docs/Collabora/参考资料/PostMessage API 接口.md @@ -0,0 +1,236 @@ +PostMessage API 用于在 Collabora Online 的浏览器部分被包含在一个父框架中时,与其进行交互。这对于希望将 Collabora Online 集成到自身应用中的宿主(Host)非常有用。 + +此 API 主要基于 **WOPI 规范**,并带有少量扩展/修改。所有发送的消息都采用以下形式: + +```plain +{ + "MessageId": "", + "SendTime": "", + "Values": { + "": "" + } +} +``` + +`SendTime` 是浏览器 `Date.now()` 返回的时间戳。从 WOPI 宿主发送的 Post 消息也应采用相同的格式。 + +需要注意的是,正如 WOPI 规范中提到的,如果尚未收到 `Host_PostmessageReady` 消息,Collabora Online 框架将忽略所有来自宿主框架的 Post 消息。此外,由于将 Collabora Online 嵌入为 iframe 必须实现 WOPI,因此要求 WOPI 宿主的 `CheckFileInfo` 响应中必须包含 `PostMessageOrigin` 属性。否则,将不会发出任何 Post 消息。 + +--- + +## 初始化(Initialization) +### 编辑器(Editor)发送给 WOPI 宿主(WOPI Host) +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **App_LoadingStatus** | `Status:`(状态) `DocumentLoadedTime:`(文档加载时间) `Features:`(功能) | **状态 (**`Status`**):** - `Frame_Ready`: Collabora Online 框架已加载,可显示 UI。 - `Document_Loaded`: 文档已完全加载,宿主可以开始使用 PostMessage API。 - `Failed`: 文档加载失败,但宿主可显示 Collabora Online 框架以呈现错误。 - `Initialized`: 编辑器中的 PostMessage 监听器已初始化。WOPI 宿主可以开始发送 Post 消息(甚至在 `Frame_Ready` 之前)。 **附加键值:** - `Features`: 此客户端的功能。支持的值包括:`VersionStates`(通知宿主客户端支持不同的版本状态)。 - `DocumentLoadedTime`: 当 Status 为 `Document_Loaded` 时可用。 | + + +### WOPI 宿主(WOPI Host)发送给编辑器(Editor) +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Host_PostmessageReady** | _无_ | 详情请参见 WOPI 文档。 | + + +--- + +## 查询(Query) +您可以使用 Post 消息 API 从编辑器查询数据。所有响应的 `MessageId` 都以查询消息的 `MessageId` 加上 `_Resp` 后缀返回。 + +### 查询消息(WOPI 宿主发送给 Editor) +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Get_Views** | _无_ | 查询编辑器中当前活跃的文档视图。响应以 `Get_Views_Resp` 形式返回。 | +| **Get_Export_Formats** | _无_ | 查询编辑器中当前打开文档支持的所有导出格式。响应以 `Get_Export_Formats_Resp` 形式返回。 | +| **Get_User_State** | _无_ | 查询编辑器中当前用户活动状态(是空闲或活跃)。响应以 `Get_User_State_Resp` 形式返回。 | + + +### 查询响应(Editor 发送给 WOPI Host) +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Get_Views_Resp** | `ViewId:`(视图 ID) `UserId:`(用户 ID) `UserName:`(用户名) `Color:`(颜色) `ReadOnly:`(只读) `IsCurrentView:`(是否为当前视图) | 提供使用 `Get_Views` 查询时所有当前视图的详细信息。 | +| **Get_Export_Formats_Resp** | `Label:`(标签) `Format:`(格式) | 对 `Get_Export_Formats` 查询的响应。 `Label` 包含解释格式的本地化字符串。 `Format` 是请求文档导出时所需的文件扩展名。 | +| **Get_User_State_Resp** | `State:`(状态) `Elapsed:`(已逝时间) | 对 `Get_User_State` 查询的响应。 `State` 包含用户活动状态的非本地化字符串(“active”或“idle”)。 `Elapsed` 是用户上次活动以来的秒数。 | + + +--- + +## 会话管理(Session Management) +### WOPI 宿主发送给 Editor +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Action_RemoveView** | `ViewId:`(视图 ID) | 移除会话。 | +| **Reset_Access_Token** | `token:`(令牌) | 重置访问令牌。从现在起将使用新的令牌。 | + + +### Editor 发送给 WOPI Host +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **View_Added** | `ViewId:` `UserId:` `UserName:` `Color:` `ReadOnly:` | **已弃用 (Deprecated: true)**; 新成员已添加。 _注:此消息已弃用,请转而实现对 _`Views_List`_ 的处理,其有效载荷与 _`Get_Views_Resp`_ 相同。_ | +| **View_Removed** | `ViewId:`(视图 ID) | **已弃用 (Deprecated: true)**; `ViewId` 对应的视图已关闭文档。 _注:此消息已弃用,请转而实现对 _`Get_Views_Resp`_ 或 _`Views_List`_ 的处理。_ | +| **Session_Closed** | `Reason:`(原因) | 会话已关闭。`Reason` 字段详细说明了来源:`OwnerTermination` (所有者终止), `DocumentIdle` (文档空闲), `OOM` (内存不足), `ShuttingDown` (服务器维护关机), `DocumentDisconnected` (文档问题)。 | +| **Views_List** | _参见 Get_Views_Resp_ | 关于当前连接视图的完整信息。 | +| **User_Idle** | _无_ | 指示用户进入空闲状态。发生在配置文件 `/etc/coolwsd/coolwsd.xml` 中定义的较长非活动时间之后。 | +| **User_Active** | _无_ | 指示用户再次变为活跃状态。发生在用户点击空闲屏幕之后。 | + + +--- + +## 动作(Actions) +### WOPI 宿主发送给 Editor +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Action_Save** | `DontTerminateEdit:`(不终止编辑) `DontSaveIfUnmodified:`(未修改不保存) `Notify:`(通知) `ExtendedData:`(扩展数据) | 保存文档。 - `DontTerminateEdit`: 与电子表格相关,设置 true 不会终止编辑模式。 - `DontSaveIfUnmodified`: 如果文档未修改,则阻止保存到存储。 - `Notify`: 如果为 true,则在保存文档时通知宿主(参见 `Action_Save_Resp`)。 - `ExtendedData`: 可选数据,通过 `X-COOL-WOPI-ExtendedData` 头部传递给 WOPI 宿主。 | +| **Action_SaveAs** | `Filename:`(文件名) `Notify:`(通知) | 以给定 `Filename` 创建文档副本。 | +| **Action_FollowUser** | `Follow:`(跟随) `ViewId:`(视图 ID) | 开启或关闭跟随用户功能。 - `Follow`: `true` 开启跟随,`false` 关闭。 - `ViewId`: 指定要跟随的用户。未定义时,跟随当前编辑器。 | +| **Action_Close** | _无_ | 关闭文档。 | +| **Close_Session** | _无_ | 允许在文档完全加载之前关闭会话。 | +| **Action_Fullscreen** | _无_ | 切换到全屏模式。 | +| **Action_FullscreenPresentation** | `StartSlideNumber:`(起始幻灯片编号) `CurrentSlide:`(当前幻灯片) | 在 Impress 中开始演示。 - `StartSlideNumber`: 可选,指定起始幻灯片(从 0 开始)。 - `CurrentSlide`: 如果为 true,则从当前幻灯片开始演示。 | +| **Action_Print** | _无_ | 打印文档。 | +| **Action_Export** | `Format:`(格式) `Notify:`(通知) | 以下载指定 `Format` 的文档(`Format` 必须来自 `Get_Export_Formats` 列表)。 | +| **Action_InsertGraphic** | `url:` | 从 URL 下载图像并将其插入到文档中。通常是对 `UI_InsertGraphic` 的响应。 | +| **Action_InsertLink** | `url:` | 在文档中插入链接。通常是对 `UI_PickLink` 的响应。 | +| **Action_InsertMultimedia** | `url:` | _24.04.10 版本新增。_ 从 URL 下载多媒体并插入。可能是对 `UI_InsertFile` 的响应。 | +| **Action_ShowBusy** | `Label:`(标签) | 显示一个进行中的覆盖层,带有给定的 `Label`。 | +| **Action_HideBusy** | _无_ | 隐藏任何进行中的覆盖层。 | +| **Action_ChangeUIMode** | `Mode:`(模式) | 更改用户界面:`'classic'` (经典工具栏) 或 `'notebookbar'` (笔记本栏)。 | +| **Action_Paste** | `Mimetype:`(MIME 类型) `Data:`(数据) | 将数据直接粘贴到文档中,绕过内部粘贴机制。 示例:`{Mimetype: "text/plain;charset=utf-8", Data: "foo"}`。 | + + +### Editor 发送给 WOPI 宿主(响应) +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Action_Load_Resp** | `success:`(成功) `result:`(结果) `errorMsg:`(错误消息) `errorType:`(错误类型) | 加载完成时的确认。 - `success`: COOL 是否成功加载文档。 - `result`: 文档未加载的原因。 - `errorMsg`: 详细的错误消息。 - `errorType`: 可选的错误标识符。 | +| **Action_Save_Resp** | `success:`(成功) `result:`(结果) `errorMsg:`(错误消息) `fileName:`(文件名) | 保存完成时的确认。仅当 `Action_Save` 包含 `Notify` 参数时发出。 - `result`: 文档未保存的原因(例如,'unmodified')。 - `fileName`: 如果成功为 true,则包含保存的文件名。 | +| **FollowUser_Changed** | `FollowedViewId:`(被跟随视图 ID) `IsFollowUser:`(是否跟随用户) `IsFollowEditor:`(是否跟随编辑器) | 当前跟随状态的通知。 | +| **Action_ChangeUIMode_Resp** | `Mode:`(模式) | 关于 UI 模式切换(标签式/紧凑式)的通知。 | + + +--- + +## 版本恢复(Version Restore) +### WOPI 宿主发送给 Editor +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Host_VersionRestore** | `Status:`(状态) | 唯一可能的值是 `Pre_Restore`。在实际恢复文档之前发送,以便 Online 可以在恢复前保存任何未保存的更改。 | + + +### Editor 发送给 WOPI Host +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **App_VersionRestore** | `Status:`(状态) | 对 `Host_VersionRestore` 消息的回复。可能的值(目前)是:`Pre_Restore_Ack`。意味着宿主可以继续将文档恢复到较早的版本。 | + + +> **注意:** 仅当 `App_LoadingStatus` 中的 `Features` 包含 `VersionStates` 时,才会发出这些消息。否则,宿主可以立即将版本恢复到较早的修订。 +> + +--- + +## 杂项(Miscellaneous) +### WOPI 宿主发送给 Editor +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Insert_Button** | `id:`(ID) `imgurl:`(图像 URL) `hint:`(提示) `accessKey:`(访问键) `mobile:`(移动端) `tablet:`(平板端) `label:`(标签) `insertBefore:`(插入到之前) `unoCommand:`(UNO 命令) | 在顶部工具栏插入一个按钮。如果未设置 `unoCommand`,则在点击时响应 `Clicked_Button` Post 消息事件。 | +| **Insert_ContextualButton** | `id:` `imgurl:` `hint:` `unoCommand:` | _25.04.5.3 版本新增。_ 在上下文工具栏(选中时出现)中插入按钮。如果未设置 `unoCommand`,则在点击时响应 `Clicked_ContextualButton`。 | +| **Hide_Button** | `id:`(ID) | 从工具栏隐藏按钮。 | +| **Show_Button** | `id:`(ID) | 在工具栏显示按钮。 | +| **Remove_Button** | `id:`(ID) | 从工具栏移除按钮。 | +| **Hide_Command** | `id:`(ID) | 隐藏某个命令的 UI(包括菜单项、上下文菜单项和工具栏按钮)。 | +| **Show_Command** | `id:`(ID) | 显示某个命令的 UI。 | +| **Hide_NotebookTab** | `id:`(ID) | _24.04.11.1 版本新增。_ 永久隐藏特定的笔记本栏选项卡。 | +| **Show_NotebookTab** | `id:`(ID) | _24.04.11.1 版本新增。_ 显示特定的笔记本栏选项卡。 | +| **Collapse_Notebookbar** | _无_ | 隐藏笔记本栏按钮。 | +| **Extend_Notebookbar** | _无_ | 显示笔记本栏按钮。 | +| **Hide_StatusBar** | _无_ | 隐藏状态栏。 | +| **Show_StatusBar** | _无_ | 显示状态栏。 | +| **Remove_Statusbar_Element** | `id:`(ID) | 从状态栏移除元素。 | +| **Hide_Menubar** | _无_ | 隐藏菜单栏。 | +| **Show_Menubar** | _无_ | 显示菜单栏。 | +| **Grab_Focus** | _无_ | 将焦点恢复到应用程序。 | +| **Hide_Ruler** | _无_ | 隐藏水平文档标尺(仅限 Writer)。 | +| **Show_Ruler** | _无_ | 显示水平文档标尺(仅限 Writer)。 | +| **Hide_Menu_Item** | `id:`(ID) | 隐藏菜单项。`id` 在 `browser/src/control/Control.Menubar.js` 中定义。 | +| **Show_Menu_Item** | `id:`(ID) | 显示菜单项。 | +| **Show_Sidebar** | `id:`(ID) | _24.04.10 版本新增。_ 显示侧边栏。可选 `id`:`Navigator`, `ModifyPage`, `SlideChangeWindow`, `CustomAnimation`, `MasterSlidesPanel`。 | +| **Hide_Sidebar** | _无_ | _24.04.10 版本新增。_ 隐藏侧边栏。 | +| **Disable_Default_UIAction** | `action:`(动作) `disable:`(禁用) | 禁用 UI 命令的默认处理程序和动作(例如 `UI_Save`、`UI_Close`)。当 `disable` 为 true 时,仅发出 Post 消息。 | +| **Send_UNO_Command** | `Command:`(命令) `Args:`(参数) | 向编辑器发送 UNO 命令。 | +| **Error_Messages** | `list:`(列表) | 用于覆盖编辑器中错误消息的对象列表(`{type: , msg: }`)。 | +| **Hint_OnscreenKeyboard** | _无_ | 指示设备使用屏幕键盘(例如平板)。 | +| **Hint_NoOnscreenKeyboard** | _无_ | 指示设备不是平板。 | + + +#### 查找工具栏按钮 ID +工具栏按钮 ID 在以下文件中的 `getToolItems`/`create` 函数中定义: + ++ `Control.TopToolbar.js` (桌面/平板顶部工具栏) ++ `Control.MobileTopBar.ts` (智能手机顶部工具栏) ++ `Control.MobileBottomBar.js` (智能手机底部工具栏) ++ `Control.StatusBar.js` (桌面状态栏) + +#### 查找状态栏元素 ID +状态栏按钮 ID 在 `Control.StatusBar.js` 中的 `onDocLayerInit` 函数中定义。 + +### Editor 发送给 WOPI Host +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Clicked_Button** | `id:`(ID) | 点击通过 `Insert_Button` 添加的自定义按钮时发出。 | +| **Clicked_ContextualButton** | `id:`(ID) | 点击通过 `Insert_ContextualButton` 添加的自定义按钮时发出。 | +| **Download_As** | `Type:`(类型) `URL:` | 当用户选择 'Print'、'Show slideshow' 或 'Download As...' 且 CheckFileInfo 中的 `DownloadAsPostMessage` 为 true 时发出。`Type` 值: `'print'`, `'slideshow'`, `'export'`。 | +| **UI_OpenDocument** | _无_ | 请求 WOPI 宿主打开一个弹出窗口,用户可在其中选择要查看和编辑的另一个文档。 | +| **UI_CreateFile** | `DocumentType:`(文档类型) | 请求 WOPI 宿主打开一个新浏览器标签并创建新文档。`DocumentType` 可以是 `'text'`, `'spreadsheet'`, `'presentation'` 或 `'drawing'`。 | +| **UI_SaveAs** | `Args: {format: ''}`(参数) | 请求 WOPI 宿主创建相应的 UI,供用户选择路径和文件名以创建当前文件的副本。对该查询的响应通过 `Action_SaveAs` 消息发送。 | +| **UI_InsertGraphic** | _无_ | 请求 WOPI 宿主打开弹出窗口供用户选择图像插入。成功后,WOPI 宿主将发送 `Action_InsertGraphic` Post 消息。 | +| **UI_PickLink** | _无_ | 请求 WOPI 宿主打开弹出窗口供用户选择链接插入。成功后,WOPI 宿主将发送 `Action_InsertLink` Post 消息。 | +| **UI_InsertFile** | `callback:`(回调) `mimeTypeFilter:`(MIME 类型过滤器) | _24.04.10 版本新增。_ 请求 WOPI 宿主打开弹出窗口供用户选择文件插入。成功后,WOPI 宿主将发送 `callback` 参数中定义的 Post 消息。 | +| **UI_InsertAIContent** | _无_ | _24.04.12 版本新增。_ 请求 WOPI 宿主打开弹出窗口供用户选择插入 AI 生成的内容。 | +| **UI_Cancel_Password** | _无_ | 通知 WOPI 宿主用户在打开受密码保护的文件时点击了“取消”。 | +| **UI_Hyperlink** | _无_ | 通知 WOPI 宿主用户点击了超链接并确认要离开文档以跟随该超链接。 | +| **UI_Paste** | _无_ | 在移动端请求粘贴时发送。允许嵌入的移动应用程序处理剪贴板内容。 | +| **Doc_ModifiedStatus** | `Values.Modified`(已修改值) | 通知以更新文档的修改状态。`Values.Modified` 为 true 表示自上次保存以来已修改,否则为 false。 | + + +--- + +## 调用 Python 脚本(Calling Python scripts) +### WOPI 宿主发送给 Editor +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **CallPythonScript** | `ScriptFile:`(脚本文件) `Function:`(函数) `Values:`(值) | 调用 Python 脚本。`Values` 包含传递给脚本的命名参数对象。 | + + +### Editor 发送给 WOPI Host +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **CallPythonScript-Result** | `commandName:`(命令名称) `Values:`(值) | 返回结果。`commandName` 中是调用脚本的 URL。 | + + +--- + +## 提及功能(Mentions) +_注意:目前提及功能仅在 Writer 中可用。_ + +### Editor 发送给 WOPI Host +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **UI_Mention** | `type: autocomplete`(类型: 自动完成) `text:`(文本) | 当用户输入“@”时,Collabora Online 会发送此 Post 消息,附带部分文本,以获取集成商提供的用户名列表。 | +| **UI_Mention** | `type: selected`(类型: 已选择) `username:`(用户名) | 当用户从集成商提供的列表中选择一个用户名时触发此消息。 | + + +### WOPI 宿主发送给 Editor +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Action_Mention** | `list:`(列表) | 集成商应发送此消息作为对 `UI_Mention` (自动完成类型) 消息的响应。消息必须包含用户对象列表: `{ username: "...", profile: "...", label: "..." }` | + + +--- + +## 嵌入 Iframe(Embedding Iframe) +### Editor 发送给 WOPI Host +Collabora Online 发送 `Iframe_Height` Post 消息给 WOPI 宿主,以动态调整嵌入式 iframe 的高度并防止出现滚动条。 + +| MessageId | Values(值) | Description(描述) | +| :--- | :--- | :--- | +| **Iframe_Height** | `ContentHeight:`(内容高度) | 使用 `ContentHeight` 值在 UI 中动态设置嵌入式 iframe 的高度。 | + + diff --git a/docs/Collabora/参考资料/建议提示框.md b/docs/Collabora/参考资料/建议提示框.md new file mode 100644 index 0000000..8750d1f --- /dev/null +++ b/docs/Collabora/参考资料/建议提示框.md @@ -0,0 +1,4 @@ +```html +
+``` + diff --git a/docs/Collabora/参考资料/新版本提示框.md b/docs/Collabora/参考资料/新版本提示框.md new file mode 100644 index 0000000..32a10db --- /dev/null +++ b/docs/Collabora/参考资料/新版本提示框.md @@ -0,0 +1,4 @@ +```html +
+``` + diff --git a/docs/Collabora/参考资料/部署.md b/docs/Collabora/参考资料/部署.md new file mode 100644 index 0000000..a7ebcdd --- /dev/null +++ b/docs/Collabora/参考资料/部署.md @@ -0,0 +1,549 @@ +```xml + + + + + + + + + + + false + + + + de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru zh_CN + + + + false + + + + true + + + + + + false + + + + + + + + true + + + + + false + true + + + + 4 + 10 + true + + + + 4 + 5 + 5 + 120 + false + 96 + + + 3600 + 36000 + 0 + + + true + true + false + + + 0 + 8000 + 0 + 0 + 100 + 5 + 100 + 500 + 5000 + + + + 10000 + 60 + 300 + 3072 + 85 + 120 + + + + + + 300 + 900 + + 6 + + + + + + + + true + + warning + trace + Socket,WebSocket,Admin,Pixel + notice + fatal + false + + -INFO-WARN + + + + + /var/log/coolwsd.log + never + timestamp + true + 10 days + 10 + true + false + + + + + false + 82589933 + + + false + false + false + + + + true + + + + true + true + + /var/log/coolwsd-ui-cmd.log + 10 + true + false + + + + + + /var/log/coolwsd.trace.json + + + + false + + + + + + + + + false + + + + + + + all + any + + + + + + 192\.168\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3} + 127\.0\.0\.1 + ::ffff:127\.0\.0\.1 + ::1 + 172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + 10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + + + + + 192\.168\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3} + 127\.0\.0\.1 + ::ffff:127\.0\.0\.1 + ::1 + 172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + 10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + localhost + + + + + 30 + + + false + + + + + + true + + false + /etc/coolwsd/cert.pem + /etc/coolwsd/key.pem + /etc/coolwsd/ca-chain.cert.pem + false + + + + + 1000 + + + + + + + + + false + 31536000 + + + + + + true + + + true + + 1800 + + true + 0 + false + false + false + + + + + + + + + + 0.2 + + + + + + + compact + + + true + + + false + + + + + + + + + + + + + + + 0 + + + + 900 + + + + + + + http://172\.16\.0\.[0-9]{1,3} + http://nas\.7bm\.co.* + + + + + + false + + + + + true + + + + + + + + + + + true + false + + + + + + true + true + true + true + + + + + + + + + + + 250 + 5 + + 3000 + + + + + + 1000 + + + + + false + false + false + false + false + false + + + + + 3600 + + + + + + + + + false + + + + + + + + + log + + + + + + 180 + + + + false + + + + + + + + + + + + false + + + + + true + + + + + + + + false + + + + + false + false + + + + + true + + + + +``` + + + + + +369 行,要改成前端具体的 ip 地址 + diff --git a/docs/Collabora/参考资料/镜像备份.md b/docs/Collabora/参考资料/镜像备份.md new file mode 100644 index 0000000..19826ca --- /dev/null +++ b/docs/Collabora/参考资料/镜像备份.md @@ -0,0 +1,4 @@ +备份在Nas 的:`smb://%E8%A2%81%E4%BD%B3%E8%A3%95@172.16.0.208/内部共享/002 项目资料/260415 智慧法务 Collabora 备份`目录 + +直接把文件夹内所有的文件打包到要部署的服务器上,导入镜像,直接` docker compose up -d` 就可以了 + diff --git a/docs/Collabora/参考资料/高亮.md b/docs/Collabora/参考资料/高亮.md new file mode 100644 index 0000000..8ff3f43 --- /dev/null +++ b/docs/Collabora/参考资料/高亮.md @@ -0,0 +1,66 @@ +# 模板填充与预览完整流程(基于 UNO Command 高亮) +## 阶段 1:打开模板(高亮占位符) +| 角色 | 动作 | API/操作详情 | +| :--- | :--- | :--- | +| **前端** | 用户点击"打开模板" | 发起请求。 | +| **后端** | `POST /api/docxtemplater/create-temp-template` | **请求体示例:** `{ templateId: 'templates/合同模板.docx' }` | +| **后端** | 复制和处理文件 | 1. 从 MinIO 复制文件:`templates/合同模板.docx` → `temp/合同模板-temp-{timestamp}.docx`。 2. 提取占位符(例如:`{姓名}`,`{年龄}`)。 | +| **后端** | 返回响应 | **响应体示例:** `{ tempFileId: 'temp/合同模板-temp-1763460000.docx', placeholders: [{ name: '姓名', label: '姓名', ... }, { name: '年龄', label: '年龄', ... }] }` | +| **前端** | 渲染 CollaboraViewer | **组件配置:** `` | +| **前端** | `useCollaboraHighlight` Hook | 1. 监听 `Document_Loaded` 事件。2. 遍历 `highlightTexts`,依次发送 UNO Command 进行高亮。3. 高亮完成后发送 `.uno:Save` 命令保存修改。 | + + +### 🔍 前端高亮 UNO Command 示例 (针对 `{姓名}`): +```plain +// 1. 搜索文本 +{ + MessageId: "Send_UNO_Command", + Values: { + Command: ".uno:ExecuteSearch", + Args: { SearchItem: { value: "{姓名}", type: "string" } } + } +} +// 2. 设置背景色(黄色:16776960) +{ + MessageId: "Send_UNO_Command", + Values: { + Command: ".uno:CharBackColor", + Args: { BackColor: { value: 16776960, type: "long" } } + } +} +// 3. 保存修改(重要:将高亮持久化到 temp 文件) +{ + MessageId: "Send_UNO_Command", + Values: { Command: ".uno:Save" } +} +``` + +--- + +## 阶段 2:填充表单(高亮填充值) +| 角色 | 动作 | API/操作详情 | +| :--- | :--- | :--- | +| **前端** | 用户填写并提交表单 | 发起请求。 | +| **后端** | `POST /api/docxtemplater/fill` | **请求体示例:** `{ templateId: 'templates/合同模板.docx', data: { 姓名: '张三', 年龄: '25' }, outputName: 'filled_合同_张三.docx' }` | +| **后端** | 填充和复制文件 | 1. `docxtemplater` 填充源文件。2. 保存**正式文件**到 MinIO:`contracts/filled_合同_张三_1763460000.docx` (✅)。3. 复制正式文件到 temp:`temp/filled_合同_张三_1763460000-temp.docx`。 | +| **后端** | 返回响应 | **响应体示例:** `{ fileId: 'contracts/filled_合同_张三_1763460000.docx', tempFileId: 'temp/filled_合同_张三_1763460000-temp.docx', filledValues: ['张三', '25'] }` | +| **前端** | 渲染 CollaboraViewer | **组件配置:** `` | +| **前端** | `useCollaboraHighlight` Hook | 同样使用该 Hook 监听 `Document_Loaded`,遍历 `filledValues` 进行高亮,并发送 `.uno:Save` 命令保存高亮状态到新的临时文件。 | + + +--- + +## 阶段 3:清理(删除临时文件) +| 角色 | 动作 | API/操作详情 | +| :--- | :--- | :--- | +| **前端** | 用户关闭页面 | 发起请求。 | +| **后端** | `DELETE /api/docxtemplater/cleanup-temp` | **请求体示例:** `{ tempFileIds: ['temp/合同模板-temp-1763460000.docx', 'temp/filled_合同_张三_1763460000-temp.docx'] }` | +| **后端** | 删除文件 | 遍历并从 MinIO 删除请求中列出的所有临时文件。 | + + +--- + +## 总结:核心优势和变动 ++ **核心优势:** 利用前端 UNO Command 进行高亮,避免了复杂的后端 DOCX XML 操作,同时通过 Collabora 的 `.uno:Save` 命令确保高亮状态持久化到临时文件中,不污染源文件和最终正式文件。 ++ **模式变动:** CollaboraViewer 必须设置为 `mode="edit"` 才能执行 UNO Command 和保存操作,这使得高亮能够生效。虽然用户在此模式下可以看到编辑界面,但由于文件位于临时路径,且最终用户下载的是干净的正式文件,该风险可控。 + diff --git a/docs/Collabora/部署排障/IP链路部署与排障.md b/docs/Collabora/部署排障/IP链路部署与排障.md new file mode 100644 index 0000000..e69a51b --- /dev/null +++ b/docs/Collabora/部署排障/IP链路部署与排障.md @@ -0,0 +1,247 @@ +# Collabora IP链路部署与排障 + +## 本次目标 + +将文档预览/编辑链路统一收口到 `http://172.16.0.59:5173`,避免 `nas.7bm.co` 与内网 IP 混用导致: + +- Collabora iframe 打不开 +- `chrome-error://chromewebdata/` +- `refused to connect` / `拒绝连接` +- WOPI 已经 200,但浏览器侧仍然失败 + +## 最终生效配置 + +### 1. 前端环境变量 + +文件:`legal-platform-frontend/.env` + +关键项: + +```env +API_BACKEND_TARGET=http://172.16.0.59:8096 +APP_URL=http://172.16.0.59:5173 +DOCUMENT_URL=http://172.16.0.59:8096/docauditai/ +COLLABORA_URL=http://172.16.0.59:5173 +``` + +说明: + +- `APP_URL` 决定 WOPI 回调地址 `WOPISrc` +- `COLLABORA_URL` 决定 iframe 中加载的 `cool.html` +- 如果这里仍然写域名,最终会重新生成域名链路 + +### 2. nginx 代理 + +文件:`deploy/collabora-proxy/conf.d/collabora.conf` + +必须代理这些路径: + +- `/wopi/` +- `/browser/` +- `/cool/` +- `/coolws/` +- `/hosting/` +- `/loleaflet/` +- `/` + +关键点: + +- Collabora 相关 `Host` / `X-Forwarded-Host` 统一写成 `172.16.0.59:5173` +- `proxy_redirect` 统一改写到 `http://172.16.0.59:5173/` +- `/coolws/` 不能漏,否则 websocket 子通道会 404 + +### 3. Collabora 白名单与 CSP + +文件:`/home/wren-dev/Porject/collabora-backup/coolwsd-config/coolwsd.xml` + +关键项: + +```xml +frame-ancestors http://172.16.0.59:5173; +``` + +```xml + + + http://172\.16\.0\.59:5173 + + +``` + +说明: + +- 不再保留 `nas.7bm.co`,避免域名/IP 混用 +- 不再使用带端口正则的 alias,之前会触发 `Bad URI syntax` / `Invalid regular expression` + +### 4. Compose 参数 + +文件:`/home/wren-dev/Porject/collabora-backup/docker-compose.yml` + +当前使用: + +```yaml +extra_params=--o:ssl.enable=false --o:ssl.termination=false --o:logging.level=warning --o:security.seccomp=false +``` + +说明: + +- 已移除旧的 `--o:net.frame_ancestors=http://*` +- 该参数会污染最终 CSP,导致浏览器报 `frame-ancestors` 违规 + +## 本次修复过的问题 + +### 问题 1:登录跳转到了 `localhost:5193` + +现象: + +- 访问 `5173` 后,响应头返回 `Location: http://localhost:5193/login?...` + +原因: + +- 代理层转发头不完整 +- Next 在开发环境下生成了错误绝对跳转 + +处理: + +- 补 `X-Forwarded-Host` / `X-Forwarded-Port` +- 补 `proxy_redirect` + +### 问题 2:`/coolws/` 没代理 + +现象: + +- `WOPI` 已通 +- Collabora 页面仍起不来 + +原因: + +- `coolws/newchild` 被打到 Next,返回 404 + +处理: + +- 为 `/coolws/` 增加代理到 `127.0.0.1:9980` + +### 问题 3:allowed host 正则写坏 + +现象: + +- Collabora 日志出现: + - `Invalid regular expression for allowed host` + - `Bad URI syntax` + +原因: + +- 使用了复杂 alias 正则: + - `http://172\.16\.0\.[0-9]{1,3}(:[0-9]{1,5})?` + +处理: + +- 改为显式 host,不再用该正则 + +### 问题 4:浏览器控制台报 CSP / frame-ancestors 错误 + +现象: + +- `Framing 'http://172.16.0.59:5173/' violates Content Security Policy directive...` +- `chrome-error://chromewebdata/` + +原因: + +- Compose 里的旧 `net.frame_ancestors=http://*` +- `coolwsd.xml` 里的 CSP 与实际 host 混用 +- 旧标签页中还混有 `nas.7bm.co` + +处理: + +- 删除旧 `net.frame_ancestors` +- `content_security_policy` 只允许 `http://172.16.0.59:5173` +- 重建容器而不是简单 restart + +## 重启方式 + +### 前后端 + +```bash +./leaudit.sh restart +``` + +### Collabora + +```bash +cd /home/wren-dev/Porject/collabora-backup +docker-compose up -d --force-recreate collabora +``` + +### 代理 + +```bash +docker restart leaudit-collabora-proxy +``` + +## 验证命令 + +### 前端入口 + +```bash +curl -I http://172.16.0.59:5173/ +``` + +期望: + +- 跳转到 `http://172.16.0.59:5173/login?...` + +### Collabora 发现接口 + +```bash +curl -I http://172.16.0.59:5173/hosting/discovery +``` + +期望: + +- `200 OK` + +### Collabora 页面入口 + +```bash +curl -I http://172.16.0.59:5173/browser/dist/cool.html +``` + +期望: + +- `200 OK` +- `Content-Security-Policy` 里只出现 `172.16.0.59:5173` + +### WOPI 文件接口 + +由页面实际触发,日志里应看到: + +- `GET /wopi/files/... 200` + +## 浏览器测试要求 + +为避免旧的 `nas.7bm.co` 页面上下文污染: + +1. 关闭旧标签页 +2. 打开无痕窗口 +3. 直接访问 `http://172.16.0.59:5173/login` +4. 登录后进入文档页 +5. 不要从 `nas.7bm.co` 页面里跳转到 IP 页面 + +## 已做的前端小收口 + +文件:`legal-platform-frontend/lib/services/collabora.config.server.ts` + +处理: + +- 移除了 `ui_defaults` 中的 `SavedUIState=false` + +原因: + +- Collabora 会打印:`unknown UI default's component SavedUIState` +- 该项不是必须,去掉后能减少无关告警 + +## 后续建议 + +- 如果后面要回切域名方案,不要与 IP 方案混用 +- 需要整套一起回切:前端 `.env`、nginx、Collabora host/CSP 同步改回 +- 如果当前 IP 方案稳定,可单独保留一份 docker 与 nginx 配置快照 diff --git a/docs/Collabora/部署排障/正确配置全流程.md b/docs/Collabora/部署排障/正确配置全流程.md new file mode 100644 index 0000000..87c7ab0 --- /dev/null +++ b/docs/Collabora/部署排障/正确配置全流程.md @@ -0,0 +1,800 @@ +# Collabora 正确配置全流程 + +## 1. 文档目标 + +本文档用于沉淀 LeAudit 当前 Collabora 集成的**正确配置全流程**,便于: + +- 本机继续维护 +- 迁移到新服务器 +- 上线前后按清单验收 +- 遇到故障时快速排查 + +本文档同时包含两类信息: + +- 当前环境已经验证可用的实际配置 +- 可迁移到其他服务器的模板化配置说明 + +当前已验证可用的主链路为: + +```text +http://172.16.0.59:5173 +``` + +注意: + +**浏览器入口、前端生成的 WOPI 地址、Collabora iframe 地址、nginx 转发头、Collabora 白名单、CSP 的 host 必须完全一致。** + +如果后续要改成域名方案,必须整套一起切换,不要域名/IP 混用。 + +--- + +## 2. 参数模板 + +迁移到新服务器时,建议先把下面这些参数定义清楚,再开始修改配置: + +```text + LeAudit 项目根目录 + Collabora 部署目录 + 浏览器实际访问的主机名或 IP + 前端代理入口端口,默认 5173 + Next 开发服务端口,默认 5193 + 后端服务端口,默认 8096 + Collabora 容器端口,默认 9980 + 文档读写后端基础地址 +``` + +当前环境对应值: + +```text + = /home/wren-dev/Porject/leaudit-platform + = /home/wren-dev/Porject/collabora-backup + = 172.16.0.59 + = 5173 + = 5193 + = 8096 + = 9980 + = http://172.16.0.59:8096/docauditai/ +``` + +--- + +## 3. 当前架构 + +当前链路如下: + +1. 浏览器访问前端入口:`http://172.16.0.59:5173` +2. `5173` 上运行 nginx 代理 +3. nginx 将普通前端请求转发到 Next:`127.0.0.1:5193` +4. nginx 将 Collabora 请求转发到容器:`127.0.0.1:9980` +5. 前端通过 `/api/collabora/config` 生成: + - `iframeUrl` + - `WOPISrc` +6. Collabora 再通过 `/wopi/files/...` 回调前端获取文件信息与文件内容 + +简化后的链路: + +```text +Browser + -> 172.16.0.59:5173 + -> nginx proxy + -> 127.0.0.1:5193 (Next 前端) + -> 127.0.0.1:9980 (Collabora) +``` + +--- + +## 4. 关键目录与文件 + +### 4.1 项目目录 + +```text + +``` + +当前实际值: + +```text +/home/wren-dev/Porject/leaudit-platform +``` + +### 4.2 Collabora 部署目录 + +```text + +``` + +当前实际值: + +```text +/home/wren-dev/Porject/collabora-backup +``` + +### 4.3 本次关键文件 + +- 前端环境变量 + - `legal-platform-frontend/.env` +- Collabora iframe 配置生成 + - `legal-platform-frontend/lib/services/collabora.config.server.ts` +- nginx 代理配置 + - `deploy/collabora-proxy/conf.d/collabora.conf` +- Collabora compose 配置 + - `/home/wren-dev/Porject/collabora-backup/docker-compose.yml` +- Collabora 主配置 + - `/home/wren-dev/Porject/collabora-backup/coolwsd-config/coolwsd.xml` + +--- + +## 5. 前置要求 + +开始前请确认: + +- Docker 可用 +- Docker Compose 可用 +- 前端、后端可正常启动 +- 目标服务器开放需要的端口 +- Collabora 镜像已准备好 + +如需重新导入镜像: + +```bash +docker load -i /collabora-image-v5.tar +``` + +当前环境示例: + +```bash +docker load -i /home/wren-dev/Porject/collabora-backup/collabora-image-v5.tar +``` + +需要重点确认的端口: + +- `` +- `` +- `` + +--- + +## 6. 前端正确配置 + +文件:`legal-platform-frontend/.env` + +### 6.1 当前已验证可用配置 + +```env +API_BACKEND_TARGET=http://172.16.0.59:8096 +APP_URL=http://172.16.0.59:5173 +DOCUMENT_URL=http://172.16.0.59:8096/docauditai/ +COLLABORA_URL=http://172.16.0.59:5173 +``` + +### 6.2 模板写法 + +迁移到新服务器时,替换为: + +```env +API_BACKEND_TARGET=http://: +APP_URL=http://: +DOCUMENT_URL=http://:/docauditai/ +COLLABORA_URL=http://: +``` + +### 6.3 每个变量的作用 + +- `APP_URL` + - 决定 `WOPISrc` + - 必须与浏览器实际访问入口一致 +- `COLLABORA_URL` + - 决定 iframe 中 `cool.html` 的主机与端口 +- `API_BACKEND_TARGET` + - 前端调用后端 API 的入口 +- `DOCUMENT_URL` + - WOPI 获取文件、写回文件时使用的后端文档基础地址 + +### 6.4 为什么必须统一 + +如果浏览器入口是: + +- `http://172.16.0.59:5173` + +但前端却生成: + +- `http://nas.7bm.co:5173/wopi/files/...` +- `http://nas.7bm.co:5173/browser/dist/cool.html` + +就会出现: + +- 父页面是 IP +- iframe 是域名 +- WOPI 又是另一个 host +- 浏览器按跨源处理 +- 最终触发 iframe 拒绝加载、CSP 冲突、`chrome-error://chromewebdata/` + +所以前端配置必须与最终入口完全一致。 + +--- + +## 7. Collabora iframe 参数生成 + +文件:`legal-platform-frontend/lib/services/collabora.config.server.ts` + +### 7.1 当前逻辑 + +- `WOPISrc = ${APP_URL}/wopi/files/${encodeURIComponent(fileId)}` +- `cool.html = ${COLLABORA_URL}/browser/dist/cool.html?...` + +### 7.2 当前建议保留的 UI 默认项 + +```ts +const uiDefaults = [ + "UIMode=compact", + "TextRuler=false", + "TextStatusbar=false", + "TextSidebar=false", +].join(";") +``` + +### 7.3 已移除的项 + +- `SavedUIState=false` + +原因: + +- Collabora 会输出: + - `unknown UI default's component SavedUIState` +- 该项不是必须项,移除后可减少无关噪音日志 + +--- + +## 8. nginx 代理正确配置 + +文件:`deploy/collabora-proxy/conf.d/collabora.conf` + +### 8.1 必须代理的路径 + +以下路径必须明确代理: + +- `/wopi/` +- `/browser/` +- `/cool/` +- `/coolws/` +- `/hosting/` +- `/loleaflet/` +- `/` + +### 8.2 路径作用 + +- `/wopi/` + - 前端本地 WOPI 接口 +- `/browser/` + - Collabora 页面与静态资源 +- `/cool/` + - 文档会话相关路径 +- `/coolws/` + - websocket 子通道 +- `/hosting/` + - discovery 接口 +- `/loleaflet/` + - Collabora 相关资源 +- `/` + - 普通前端页面与 API + +### 8.3 Collabora 相关转发头 + +对 Collabora 相关 location,必须统一: + +```nginx +proxy_set_header Host :; +proxy_set_header X-Forwarded-Host :; +proxy_set_header X-Forwarded-Proto $scheme; +``` + +当前环境实际值: + +```nginx +proxy_set_header Host 172.16.0.59:5173; +proxy_set_header X-Forwarded-Host 172.16.0.59:5173; +proxy_set_header X-Forwarded-Proto $scheme; +``` + +### 8.4 前端主路由需要补的头 + +```nginx +proxy_set_header X-Forwarded-Port $server_port; +``` + +### 8.5 为什么 `/coolws/` 不能漏 + +如果遗漏 `/coolws/`,就会出现: + +- `WOPI` 文件接口已经返回 `200` +- `cool.html` 也能打开 +- 但 websocket 子通道建立失败 +- 文档仍无法进入编辑状态 + +本次修复中,`/coolws/` 是一个关键缺失点。 + +### 8.6 错误跳转修复 + +针对 Next 开发环境错误返回的绝对地址,需要增加: + +```nginx +proxy_redirect http://127.0.0.1:/ http://:/; +proxy_redirect http://localhost:/ http://:/; +``` + +当前环境实际值: + +```nginx +proxy_redirect http://127.0.0.1:5193/ http://172.16.0.59:5173/; +proxy_redirect http://localhost:5193/ http://172.16.0.59:5173/; +``` + +否则可能出现: + +- 浏览器跳到 `localhost:5193/login?...` +- 远端用户直接无法访问 + +### 8.7 迁移到新服务器时要替换的项 + +- `listen 5173;` +- `proxy_pass http://127.0.0.1:5193` +- `proxy_pass http://127.0.0.1:9980` +- 所有 `Host :` +- 所有 `X-Forwarded-Host :` +- 所有 `proxy_redirect` + +--- + +## 9. Collabora Docker Compose 正确配置 + +文件:`/docker-compose.yml` + +### 9.1 当前关键配置 + +```yaml +services: + collabora: + image: docker-collabora:v5 + container_name: collabora_code + privileged: true + ports: + - "9980:9980" + environment: + - "domain=.*" + - "extra_params=--o:ssl.enable=false --o:ssl.termination=false --o:logging.level=warning --o:security.seccomp=false" +``` + +### 9.2 不要再保留旧参数 + +不要再使用: + +```text +--o:net.frame_ancestors=http://* +``` + +原因: + +- 这是过时配置 +- 会污染最终 `Content-Security-Policy` +- 会让 `frame-ancestors` 混乱 +- 浏览器会直接拦 iframe + +### 9.3 当前正确做法 + +- 删除 `net.frame_ancestors` +- 在 `coolwsd.xml` 中显式设置 `content_security_policy` + +### 9.4 新服务器迁移要点 + +迁移时需要确认: + +- 镜像已导入 +- `docker-compose.yml` 中挂载目录存在 +- 插件目录存在: + - `mycustom_collabora_plugins` +- 脚本目录存在: + - `custom_python_scripts` +- `` 未被占用 + +--- + +## 10. Collabora 主配置正确写法 + +文件:`/coolwsd-config/coolwsd.xml` + +### 10.1 WOPI 白名单 + +模板写法: + +```xml + + + http://: + + +``` + +当前环境实际值: + +```xml + + + http://172\.16\.0\.59:5173 + + +``` + +### 10.2 CSP 配置 + +模板写法: + +```xml +frame-ancestors http://:; +``` + +当前环境实际值: + +```xml +frame-ancestors http://172.16.0.59:5173; +``` + +### 10.3 不推荐写法 + +不要再使用复杂 alias 正则,例如: + +```xml +http://172\.16\.0\.[0-9]{1,3}(:[0-9]{1,5})? +``` + +这在当前环境中已经实测触发过: + +- `Invalid regular expression for allowed host` +- `Bad URI syntax` + +因此当前方案统一改为显式 host。 + +--- + +## 11. 正确重启顺序 + +### 11.1 前后端 + +```bash +cd +./leaudit.sh restart +``` + +### 11.2 代理 + +```bash +docker restart leaudit-collabora-proxy +``` + +### 11.3 Collabora + +推荐直接强制重建,而不是简单 restart: + +```bash +cd +docker-compose up -d --force-recreate collabora +``` + +### 11.4 为什么要 force-recreate + +如果只执行: + +```bash +docker restart collabora_code +``` + +旧容器里的环境变量可能仍保留,尤其是: + +- `extra_params` + +本次就遇到过: + +- compose 文件已经删掉了 `net.frame_ancestors` +- 但旧容器环境里仍残留该参数 +- 只有 `--force-recreate` 之后才真正清理干净 + +### 11.5 推荐标准重建流程 + +```bash +cd +./leaudit.sh restart + +docker restart leaudit-collabora-proxy + +cd +docker-compose up -d --force-recreate collabora +``` + +--- + +## 12. 正确验证步骤 + +### 12.1 验证前端入口 + +```bash +curl -I http://:/ +``` + +期望: + +- 返回 `307` +- 跳转到 `http://:/login?...` + +当前环境示例: + +```bash +curl -I http://172.16.0.59:5173/ +``` + +### 12.2 验证 discovery + +```bash +curl -I http://:/hosting/discovery +``` + +期望: + +- `200 OK` + +### 12.3 验证 Collabora 页面 + +```bash +curl -I http://:/browser/dist/cool.html +``` + +期望: + +- `200 OK` +- 响应头包含 `Content-Security-Policy` +- `frame-ancestors` 围绕当前 `:` 收口 + +### 12.4 验证 WOPI 文件接口 + +通过页面实际访问时,日志里应看到: + +```text +GET /wopi/files/... 200 +``` + +### 12.5 验证 websocket 子通道 + +Collabora 日志中应能看到类似: + +- `/coolws/forkit` +- `/coolws/newchild` +- `Upgrading to WebSocket` + +这说明文档会话已建立。 + +### 12.6 上线验收清单 + +上线到新服务器后,至少完成以下验收: + +- 能打开登录页 +- 能成功登录 +- 能打开 2~3 个不同文档 +- 能进入编辑态 +- 能保存并再次打开 +- 浏览器控制台不再出现: + - `refused to connect` + - `chrome-error://chromewebdata/` + - `Framing ... violates Content Security Policy` +- Collabora 日志不再出现: + - `Invalid regular expression for allowed host` + - `Bad URI syntax` + - `No authorized hosts found` + +--- + +## 13. 浏览器侧正确测试方式 + +如果浏览器之前已经打开过: + +- `nas.7bm.co` +- 或旧的失败 iframe 页面 + +即使后端已经修好,也可能继续残留: + +- `chrome-error://chromewebdata/` +- 错误 iframe 上下文 +- 跨源错误 + +### 正确做法 + +1. 关闭旧标签页 +2. 打开无痕窗口 +3. 只访问: + - `http://:/login` +4. 登录后直接进入文档页面 +5. 不要从旧域名页面跳转到新 IP 页面 + +当前环境示例: + +- `http://172.16.0.59:5173/login` + +--- + +## 14. 本次实际修复过的问题 + +### 问题 1:错误跳转到 `localhost:5193` + +原因: + +- 代理头不完整 +- Next 在开发环境生成了错误绝对地址 + +修复: + +- 增加 `X-Forwarded-Host` / `X-Forwarded-Port` +- 增加 `proxy_redirect` + +### 问题 2:`/coolws/` 未代理 + +原因: + +- 只代理了 `/cool/`,漏了 websocket 子通道 + +修复: + +- 增加 `/coolws/` 到 Collabora 容器的代理 + +### 问题 3:Collabora 白名单正则错误 + +原因: + +- 复杂 alias 正则不兼容当前 coolwsd 解析 + +修复: + +- 改为显式 host + +### 问题 4:CSP / frame-ancestors 冲突 + +原因: + +- 旧的 `net.frame_ancestors=http://*` +- 域名与 IP 混用 + +修复: + +- 删除旧参数 +- 使用 `content_security_policy` 显式配置 +- 强制重建容器 + +### 问题 5:`SavedUIState` 无关告警 + +原因: + +- `ui_defaults` 里传了 Collabora 当前不识别的项 + +修复: + +- 从 `legal-platform-frontend/lib/services/collabora.config.server.ts` 中移除该项 + +--- + +## 15. 回切域名方案时的注意事项 + +如果后续要恢复域名方案,不能只改其中一项,必须一起改: + +- 前端 `.env` +- nginx 代理头与 `proxy_redirect` +- Collabora 白名单 +- Collabora CSP `frame-ancestors` + +### 15.1 推荐原则 + +上线时只选一种主链路: + +- 全域名 +或 +- 全 IP + +不要混合: + +- 父页面域名 +- iframe IP +- WOPI 又是另一个 host + +否则最终还是会回到浏览器跨源与 iframe 拦截问题。 + +--- + +## 16. 推荐日常运维命令 + +### 查看前后端状态 + +```bash +cd +./leaudit.sh doctor +``` + +### 重启前后端 + +```bash +./leaudit.sh restart +``` + +### 重建 Collabora + +```bash +cd +docker-compose up -d --force-recreate collabora +``` + +### 查看 Collabora 日志 + +```bash +docker logs --tail 200 collabora_code +``` + +### 查看代理日志 + +```bash +tail -n 200 /deploy/collabora-proxy/logs/error.log +``` + +--- + +## 17. 新服务器迁移 Checklist + +### 17.1 准备阶段 + +- [ ] 准备好项目代码目录 `` +- [ ] 准备好 Collabora 部署目录 `` +- [ ] 准备好 Collabora 镜像 tar 或镜像仓库访问方式 +- [ ] 确认 `` +- [ ] 确认 ``、``、`` 不冲突 + +### 17.2 配置阶段 + +- [ ] 修改 `legal-platform-frontend/.env` +- [ ] 修改 `deploy/collabora-proxy/conf.d/collabora.conf` +- [ ] 修改 `docker-compose.yml` +- [ ] 修改 `coolwsd.xml` + +### 17.3 启动阶段 + +- [ ] 导入 Collabora 镜像 +- [ ] 启动前后端 +- [ ] 重启代理 +- [ ] 强制重建 Collabora 容器 + +### 17.4 验证阶段 + +- [ ] `curl -I http://:/` +- [ ] `curl -I http://:/hosting/discovery` +- [ ] `curl -I http://:/browser/dist/cool.html` +- [ ] 浏览器无痕访问登录页 +- [ ] 打开并保存至少一个文档 + +### 17.5 故障定位阶段 + +若失败,优先检查: + +- [ ] 前端 `.env` 与实际入口是否一致 +- [ ] nginx 是否代理了 `/coolws/` +- [ ] `proxy_redirect` 是否仍指向旧地址 +- [ ] `coolwsd.xml` 白名单是否与实际 host 一致 +- [ ] compose 是否仍残留旧 `extra_params` +- [ ] Collabora 是否已 `force-recreate` + +--- + +## 18. 当前结论 + +当前这套正确配置的核心原则只有一句话: + +**浏览器入口、前端生成的 WOPI 地址、Collabora iframe 地址、代理转发头、Collabora 白名单、CSP 来源,必须全部使用同一个 host。** + +本次已经验证可用的 host 为: + +```text +172.16.0.59:5173 +``` + +如果迁移到新服务器,请先替换 `` 等参数,再按本文档的重启与验证步骤执行。