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.
+
+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
+```
+
+如果迁移到新服务器,请先替换 `` 等参数,再按本文档的重启与验证步骤执行。