docs(collabora): organize deployment guides and fix proxy chain

This commit is contained in:
wren
2026-05-11 17:54:39 +08:00
parent dcc0f3c30d
commit f788149ca7
16 changed files with 2959 additions and 15 deletions
+41 -15
View File
@@ -10,9 +10,13 @@ server {
# Local WOPI routes must stay on the frontend app. # Local WOPI routes must stay on the frontend app.
location /wopi/ { location /wopi/ {
proxy_pass http://127.0.0.1:5193/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_http_version 1.1;
proxy_set_header Host $http_host; 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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
@@ -26,14 +30,14 @@ server {
# Collabora shell endpoint. # Collabora shell endpoint.
location /collabora/ { location /collabora/ {
proxy_pass http://172.16.0.58:9980/; proxy_pass http://127.0.0.1:9980/;
proxy_http_version 1.1; 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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
@@ -46,14 +50,14 @@ server {
location /browser/ { location /browser/ {
# Keep the original escaped URI; Collabora websocket/document paths # Keep the original escaped URI; Collabora websocket/document paths
# break if nginx normalizes `%2F` inside the encoded WOPI URL. # 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_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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
@@ -65,14 +69,32 @@ server {
location /cool/ { location /cool/ {
# Websocket path contains an encoded WOPI URL in the URI path. # Websocket path contains an encoded WOPI URL in the URI path.
# Do not append a URI here, otherwise nginx may decode `%2F`. # 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_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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
@@ -82,14 +104,14 @@ server {
} }
location /hosting/ { location /hosting/ {
proxy_pass http://172.16.0.58:9980; proxy_pass http://127.0.0.1:9980;
proxy_http_version 1.1; 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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
@@ -99,14 +121,14 @@ server {
} }
location /loleaflet/ { location /loleaflet/ {
proxy_pass http://172.16.0.58:9980; proxy_pass http://127.0.0.1:9980;
proxy_http_version 1.1; 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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
@@ -118,9 +140,13 @@ server {
# Everything else remains on Next dev server. # Everything else remains on Next dev server.
location / { location / {
proxy_pass http://127.0.0.1:5193; 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_http_version 1.1;
proxy_set_header Host $http_host; 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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
+44
View File
@@ -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. 需要做功能开发或定制时,再查 `参考资料/`
@@ -0,0 +1,10 @@
1. <!-- 这是一张图片,ocr 内容为: -->
![](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.
@@ -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 并更新现有瓦片的像素数据
这种机制实现&#x4E86;**&#x9AD8;效的文档滚动和实时协作编辑**,类似于视频编码中的 I 帧(关键帧)和 P 帧(增量帧)技术。
@@ -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; // 当前用户
// 其他用户的光标、选择通过单独的消息同步
```
File diff suppressed because one or more lines are too long
@@ -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 显示保存成功
@@ -0,0 +1,3 @@
1. 回第一页:`.uno:GoToStartOfDoc` `{}`
2.
@@ -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: "..." }` |
---
## 嵌入 IframeEmbedding Iframe
### Editor 发送给 WOPI Host
Collabora Online 发送 `Iframe_Height` Post 消息给 WOPI 宿主,以动态调整嵌入式 iframe 的高度并防止出现滚动条。
| MessageId | Values(值) | Description(描述) |
| :--- | :--- | :--- |
| **Iframe_Height** | `ContentHeight:`(内容高度) | 使用 `ContentHeight` 值在 UI 中动态设置嵌入式 iframe 的高度。 |
@@ -0,0 +1,4 @@
```html
<div class="jsdialog lokdialog ui-dialog-content ui-widget-content"><div class="root-container jsdialog"><div class="vertical jsdialog"><div id="snackbar-container" class="jsdialog ui-grid-cell" style="grid-auto-flow: column; display: grid;"><label class="jsdialog ui-text" id="label">请向我们提供您的反馈意见</label><div class="d-flex justify-content-center ui-pushbutton-wrapper jsdialog" id="button" aria-labelledby="label"><button class="ui-pushbutton jsdialog" id="button-button" tabindex="0" aria-labelledby="label">确定</button></div></div></div></div></div>
```
@@ -0,0 +1,4 @@
```html
<div class="iframe-welcome-wrap undefined" tabindex="-1" style=""><div class="iframe-welcome-content"><form class="" action="http://172.16.0.81:9980/browser/e808afa229/welcome/welcome.html" target="iframe-welcome-form" method="get"><input class="" type="hidden" name="ui_theme" value="light"></form><iframe class="iframe-welcome-modal" name="iframe-welcome-form" title="欢迎对话框"></iframe></div></div>
```
+549
View File
@@ -0,0 +1,549 @@
```xml
<!-- -*- nxml-child-indent: 4; tab-width: 4; indent-tabs-mode: nil -*- -->
<config>
<!-- 更详细的配置文档请参阅:https://sdk.collaboraonline.com/docs/installation/Configuration.html -->
<!-- 注意:'default' 属性用于记录设置的默认值,并在部署时缺少该条目时用作备用值 -->
<!-- 注意:添加新条目时,必须在 WSD 中设置默认值,以防部署时缺少该条目 -->
<!-- ========================================
辅助功能设置
======================================== -->
<accessibility desc="辅助功能设置">
<enable type="bool" desc="控制是否启用辅助功能支持" default="false">false</enable>
</accessibility>
<!-- ========================================
允许的写作辅助语言
======================================== -->
<allowed_languages desc="此实例支持的写作辅助语言列表(拼写检查、语法检查、同义词词典、连字符)。允许过多会对启动性能产生负面影响。" default="de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru">de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru zh_CN</allowed_languages>
<!-- ========================================
外部拼写和语法检查服务配置
======================================== -->
<languagetool desc="远程拼写和语法检查 API 设置">
<enabled desc="启用远程拼写和语法检查器" type="bool" default="false">false</enabled>
<base_url desc="API 服务器的 HTTP 端点,末尾不带 /check 或 /languages 后缀" type="string" default=""></base_url>
<user_name desc="高级使用的 LanguageTool 或 Duden 账户用户名" type="string" default=""></user_name>
<api_key desc="高级使用的 API 密钥" type="string" default=""></api_key>
<ssl_verification desc="启用或禁用 SSL 验证。在使用自签名证书的测试环境中可能需要禁用" type="string" default="true">true</ssl_verification>
<rest_protocol desc="REST API 协议。LanguageTool 留空,Duden Korrekturserver 使用字符串 'duden'" type="string" default=""></rest_protocol>
</languagetool>
<!-- ========================================
DeepL 翻译服务配置
======================================== -->
<deepl desc="DeepL 翻译服务 API 设置">
<enabled desc="如果为 true,在紧凑视图中显示翻译选项为菜单条目,在选项卡视图中显示为图标" type="bool" default="false">false</enabled>
<api_url desc="API 的 URL" type="string" default=""></api_url>
<auth_key desc="由您的账户生成的身份验证密钥" type="string" default=""></auth_key>
</deepl>
<!-- ========================================
系统路径配置
======================================== -->
<sys_template_path desc="包含共享库等的模板树路径,用作子进程 chroot jail 的源" type="path" relative="true" default="systemplate"></sys_template_path>
<child_root_path desc="将在其下创建子进程 chroot jail 的目录路径。应与 systemplate 和 lotemplate 位于同一文件系统上。必须是空目录。" type="path" relative="true" default="jails"></child_root_path>
<mount_jail_tree desc="控制是否挂载 systemplate 和 lotemplate 内容,这比默认的链接/复制每个文件快得多" type="bool" default="true">true</mount_jail_tree>
<!-- ========================================
服务器配置
======================================== -->
<server_name desc="运行 coolwsd 的服务器的外部主机名:端口。如果为空,则从请求中派生(如果这不起作用,请设置它)。可以在反向代理后面或主机名无法直接访问时指定。" type="string" default=""></server_name>
<file_server_root_path desc="应被视为文件服务器根目录的目录路径。这应该是包含 cool 的目录。" type="path" relative="true" default="browser/../"></file_server_root_path>
<hexify_embedded_urls desc="启用以保护编码的 URL 不被中间跳点解码。在 Azure 部署上特别有用" type="bool" default="false">false</hexify_embedded_urls>
<experimental_features desc="启用/禁用实验性功能" type="bool" default="true">true</experimental_features>
<!-- ========================================
性能和资源配置
======================================== -->
<memproportion desc="所有 Collabora Online 进程消耗的可用内存的最大百分比,超过此百分比后,我们开始清理空闲文档。如果设置了 cgroup 内存限制,则这是该限制的最大百分比。" type="double" default="80.0"></memproportion>
<num_prespawn_children desc="提前启动并等待新客户端的子进程数量" type="uint" default="4">4</num_prespawn_children>
<fetch_update_check desc="每隔多少小时获取最新版本数据。默认为 10 小时。" type="uint" default="10">10</fetch_update_check>
<allow_update_popup desc="允许在编辑器中显示关于更新的通知" type="bool" default="true">true</allow_update_popup>
<!-- ========================================
文档特定设置
======================================== -->
<per_document desc="文档特定设置,包括 LO Core 设置">
<max_concurrency desc="处理文档时使用的最大线程数" type="uint" default="4">4</max_concurrency>
<batch_priority desc="批处理(如 convert-to)进程使用的(较低)优先级,以避免使交互式进程饿死" type="uint" default="5">5</batch_priority>
<bgsave_priority desc="后台保存进程使用的(较低)优先级,以释放时间给交互式进程" type="uint" default="5">5</bgsave_priority>
<bgsave_timeout_secs desc="等待后台保存进程完成的默认最大秒数,超时后放弃并恢复为同步保存" type="uint" default="120">120</bgsave_timeout_secs>
<redlining_as_comments desc="如果为 true,则将修订显示为注释" type="bool" default="false">false</redlining_as_comments>
<pdf_resolution_dpi desc="用于将 PDF 文档渲染为图像的分辨率(DPI)。内存消耗成比例增长。必须是小于 385 的正值。默认为 96。" type="uint" default="96">96</pdf_resolution_dpi>
<!-- 超时设置 -->
<idle_timeout_secs desc="卸载空闲文档之前的最大秒数。默认为 1 小时。" type="uint" default="3600">3600</idle_timeout_secs>
<idlesave_duration_secs desc="文档空闲后,如果已修改,应保存的秒数。设为 0 时禁用。默认为 30 秒。" type="uint" default="30">36000</idlesave_duration_secs>
<autosave_duration_secs desc="文档修改后,应自动保存的秒数。设为 0 时禁用。默认为 5 分钟。" type="uint" default="300">0</autosave_duration_secs>
<!-- 保存设置 -->
<background_autosave desc="允许在可能的情况下在 forked 后台进程中进行自动保存" type="bool" default="true">true</background_autosave>
<background_manualsave desc="允许在可能的情况下在 forked 后台进程中进行手动保存" type="bool" default="true">true</background_manualsave>
<always_save_on_exit desc="在退出最后一个编辑器时,如果文档已修改,始终执行保存和上传。这允许存储来存储文档,如果它之前作为优化而跳过了这样做。" type="bool" default="false">false</always_save_on_exit>
<!-- 资源限制 -->
<limit_virt_mem_mb desc="每个文档进程允许的最大虚拟内存。0 表示无限制。" type="uint">0</limit_virt_mem_mb>
<limit_stack_mem_kb desc="每个文档进程允许的最大堆栈大小。0 表示无限制。" type="uint">8000</limit_stack_mem_kb>
<limit_file_size_mb desc="每个文档进程允许写入的最大文件大小。0 表示无限制。" type="uint">0</limit_file_size_mb>
<limit_num_open_files desc="每个文档进程允许打开的最大文件数。0 表示无限制。" type="uint">0</limit_num_open_files>
<limit_load_secs desc="等待文档加载成功的最大秒数。0 表示无限制。" type="uint" default="100">100</limit_load_secs>
<limit_store_failures desc="卸载文档时连续保存并上传到存储失败的最大次数。0 表示无限制(不推荐)。" type="uint" default="5">5</limit_store_failures>
<limit_convert_secs desc="等待文档转换成功的最大秒数。0 表示无限制。" type="uint" default="100">100</limit_convert_secs>
<min_time_between_saves_ms desc="在磁盘上保存文档之间的最小毫秒数" type="uint" default="500">500</min_time_between_saves_ms>
<min_time_between_uploads_ms desc="将文档上传到存储之间的最小毫秒数" type="uint" default="5000">5000</min_time_between_uploads_ms>
<!-- 清理设置 -->
<cleanup desc="检查资源消耗(不良)文档并终止相关的 kit 进程。如果文档在 idle_time_secs 时间内处于空闲状态且内存使用超过 limit_dirty_mem_mb 或 CPU 使用超过 limit_cpu_per,则认为该文档是资源消耗型(不良)文档" enable="true">
<cleanup_interval_ms desc="两次检查之间的间隔" type="uint" default="10000">10000</cleanup_interval_ms>
<bad_behavior_period_secs desc="在终止相关 kit 进程之前,文档处于不良状态的最小时间段。如果在此期间不良文档的条件未满足一次,则此期间将重置" type="uint" default="60">60</bad_behavior_period_secs>
<idle_time_secs desc="文档成为不良状态候选者的最小空闲时间" type="uint" default="300">300</idle_time_secs>
<limit_dirty_mem_mb desc="文档成为不良状态候选者的最小内存使用量" type="uint" default="3072">3072</limit_dirty_mem_mb>
<limit_cpu_per desc="文档成为不良状态候选者的最小 CPU 使用率" type="uint" default="85">85</limit_cpu_per>
<lost_kit_grace_period_secs desc="丢失的 kit 进程(未被 coolwsd 引用)在终止之前解决其丢失状态的最小宽限期。要禁用丢失的 kit 清理,请使用值 0" default="120">120</lost_kit_grace_period_secs>
</cleanup>
</per_document>
<!-- ========================================
视图特定设置
======================================== -->
<per_view desc="视图特定设置">
<out_of_focus_timeout_secs desc="当浏览器选项卡不再处于焦点时,在变暗并停止更新之前的最大秒数。默认为 300 秒。" type="uint" default="300">300</out_of_focus_timeout_secs>
<idle_timeout_secs desc="当用户不再活动时(即使浏览器处于焦点),在变暗并停止更新之前的最大秒数。默认为 15 分钟。" type="uint" default="900">900</idle_timeout_secs>
<custom_os_info desc="在'关于'对话框中显示的自定义 OS 版本字符串,如果为空则从系统获取" type="string" default=""></custom_os_info>
<min_saved_message_timeout_secs type="uint" desc="显示上次修改消息之前的最小秒数" default="6">6</min_saved_message_timeout_secs>
</per_view>
<!-- ========================================
版本后缀
======================================== -->
<ver_suffix desc="附加到 etag 以允许在开发期间轻松刷新已更改的文件" type="string" default=""></ver_suffix>
<!-- ========================================
日志配置
======================================== -->
<logging>
<color type="bool">true</color>
<!-- 注意:使用 "make run" 时,logging.level 将在 coolwsd 命令行上设置,
因此如果要更改它进行测试,请在 Makefile.am 中进行,而不是在这里 -->
<level type="string" desc="可以是 0-8(数字越小越不详细),或 none(关闭日志记录),fatal、critical、error、warning、notice、information、debug、trace" default="warning">warning</level>
<level_startup type="string" desc="与 level 相同 - 但用于最有问题的初始启动阶段,启动完成后日志记录恢复为上面配置的 level" default="trace">trace</level_startup>
<disabled_areas type="string" desc="高详细日志(即 info 到 trace)可禁用,逗号分隔:Generic、Pixel、Socket、WebSocket、Http、WebServer、Storage、WOPI、Admin、Javascript" default="Socket,WebSocket,Admin,Pixel">Socket,WebSocket,Admin,Pixel</disabled_areas>
<most_verbose_level_settable_from_client type="string" desc="来自客户端的 loggingleveloverride 消息不能设置比这更详细的日志级别" default="notice">notice</most_verbose_level_settable_from_client>
<least_verbose_level_settable_from_client type="string" desc="来自客户端的 loggingleveloverride 消息不能设置比这更不详细的日志级别" default="fatal">fatal</least_verbose_level_settable_from_client>
<protocol type="bool" desc="从开始启用最小的客户端 JS 协议日志记录">false</protocol>
<!-- lokit_sal_log 示例:记录与 WebDAV 相关的消息,这对调试插入 - 图像操作很有趣:"+TIMESTAMP+INFO.ucb.ucp.webdav+WARN.ucb.ucp.webdav"
另请参阅:https://docs.libreoffice.org/sal/html/sal_log.html -->
<lokit_sal_log type="string" desc="微调来自 LOKit 的日志消息。默认是抑制来自 LOKit 的日志消息。" default="-INFO-WARN">-INFO-WARN</lokit_sal_log>
<!-- 文件日志配置 -->
<file enable="false">
<!-- 如果使用 /var/log 以外的路径,并且从 systemd 运行 coolwsd,请确保在 coolwsd.service 中启用该路径(ReadWritePaths)。
此外,日志文件路径必须对 'cool' 用户可写。 -->
<property name="path" desc="日志文件路径">/var/log/coolwsd.log</property>
<property name="rotation" desc="日志文件轮换策略。参见 Poco FileChannel。">never</property>
<property name="archive" desc="将时间戳或数字附加到存档的日志文件名">timestamp</property>
<property name="compress" desc="启用/禁用日志文件压缩">true</property>
<property name="purgeAge" desc="要保留的日志文件的最大年龄。参见 Poco FileChannel。">10 days</property>
<property name="purgeCount" desc="要保留的日志存档的最大数量。使用 'none' 禁用清除。参见 Poco FileChannel。">10</property>
<property name="rotateOnOpen" desc="启用/禁用打开时的日志文件轮换">true</property>
<property name="flush" desc="启用/禁用记录每行后刷新。可能会损害性能。请注意,如果不在每行后刷新,来自不同进程的日志行将不会按时间顺序出现。">false</property>
</file>
<!-- 匿名化配置 -->
<anonymize>
<anonymize_user_data type="bool" desc="启用以在日志中匿名化/混淆用户数据。如果默认值为 true,则在编译时强制执行,无法禁用。" default="false">false</anonymize_user_data>
<anonymization_salt type="uint" desc="用于在日志中匿名化/混淆用户数据的盐。使用秘密的 64 位随机数。" default="82589933">82589933</anonymization_salt>
</anonymize>
<docstats type="bool" desc="启用以在日志中查看文档处理信息" default="false">false</docstats>
<userstats desc="启用用户统计。即:记录文件和用户的详细信息" type="bool" default="false">false</userstats>
<disable_server_audit type="bool" desc="禁用服务器审计对话框和通知。管理员将不再在应用程序用户界面中看到警告。这不会影响日志文件。" default="false">false</disable_server_audit>
</logging>
<!-- ========================================
Canvas 幻灯片放映
======================================== -->
<canvas_slideshow_enabled type="bool" desc="如果为 true,则在客户端渲染 WebGL 演示文稿,否则使用交互式 SVG" default="true">true</canvas_slideshow_enabled>
<!-- ========================================
UI 命令日志
======================================== -->
<logging_ui_cmd>
<merge type="bool" desc="如果为 true,则将彼此之后的重复命令合并到 1 行。如果为 false,则每个命令都是 1 个新行。" default="true">true</merge>
<merge_display_end_time type="bool" desc="如果为 true,则还会记录合并命令的持续时间" default="false">true</merge_display_end_time>
<file enable="false">
<property name="path" desc="日志文件路径">/var/log/coolwsd-ui-cmd.log</property>
<property name="purgeCount" desc="要保留的日志存档的最大数量。使用 'none' 禁用清除。参见 Poco FileChannel。">10</property>
<property name="rotateOnOpen" desc="启用/禁用打开时的日志文件轮换">true</property>
<property name="flush" desc="启用/禁用记录每行后刷新">false</property>
</file>
</logging_ui_cmd>
<!-- ========================================
跟踪事件
======================================== -->
<trace_event desc="打开生成 Chrome 跟踪事件文件的可能性" enable="false">
<path desc="跟踪事件文件的输出路径,如果在运行时打开,将写入该路径" type="string" default="/var/log/coolwsd.trace.json">/var/log/coolwsd.trace.json</path>
</trace_event>
<!-- ========================================
浏览器控制台日志
======================================== -->
<browser_logging desc="在浏览器控制台中记录" default="false">false</browser_logging>
<!-- ========================================
跟踪配置
======================================== -->
<trace desc="转储命令和通知以供重放。当 'snapshot' 为 true 时,源文件首先被复制到路径。" enable="false">
<path desc="保存跟踪文件和文档的输出路径。使用 '%' 作为时间戳以避免覆盖。例如:/some/path/to/cooltrace-%.gz" compress="true" snapshot="false"></path>
<filter>
<message desc="要排除的消息的正则表达式模式"></message>
</filter>
<outgoing>
<record desc="是否记录传出消息" default="false">false</record>
</outgoing>
</trace>
<!-- ========================================
网络设置
======================================== -->
<net desc="网络设置">
<!-- 在 localhost 解析为 IPv6 [::1] 地址优先的系统上,当 net.proto 为 all 且 net.listen 为 loopback 时,
coolwsd 意外地仅在 [::1] 上监听。如果要使用 127.0.0.1,则需要将 net.proto 更改为 IPv4。 -->
<proto type="string" default="all" desc="要使用的协议:IPv4、IPv6 或 all(两者)">all</proto>
<listen type="string" default="any" desc="coolwsd 绑定到的监听地址。可以是 'any' 或 'loopback'">any</listen>
<!-- 这允许您将所有 URL 移到子路径中,从 https://my.com/browser/a123... 到 https://my.com/my/sub/path/browser/a123... -->
<service_root type="path" default="" desc="为所有页面、websocket 等的基本 URL 添加此路径前缀。这包括发现 URL。"></service_root>
<!-- POST 请求允许/拒绝配置 -->
<post_allow desc="允许/拒绝 POSTREST)的客户端 IP 地址" allow="true">
<host desc="IPv4 私有 192.168 块作为纯 IPv4 点分十进制地址">192\.168\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 环回(localhost)地址">127\.0\.0\.1</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:127\.0\.0\.1</host>
<host desc="IPv6 环回(localhost)地址">::1</host>
<host desc="IPv4 私有 172.16.0.0/12 子网第 1 部分">172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 私有 172.16.0.0/12 子网第 2 部分">172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 私有 172.16.0.0/12 子网第 3 部分">172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 私有 10.0.0.0/8 子网(Podman">10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}</host>
</post_allow>
<!-- LOK 允许配置 -->
<lok_allow desc="允许作为已编辑文件内的外部数据源的主机。所有允许的 post_allow.host 和 storage.wopi 条目也被视为允许的数据源。例如用于:PostMessage Action_InsertGraphic、=WEBSERVICE() 函数、单元格中的外部引用">
<host desc="IPv4 私有 192.168 块作为纯 IPv4 点分十进制地址">192\.168\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 环回(localhost)地址">127\.0\.0\.1</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:127\.0\.0\.1</host>
<host desc="IPv6 环回(localhost)地址">::1</host>
<host desc="IPv4 私有 172.16.0.0/12 子网第 1 部分">172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 私有 172.16.0.0/12 子网第 2 部分">172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 私有 172.16.0.0/12 子网第 3 部分">172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="IPv4 私有 10.0.0.0/8 子网(Podman">10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="同上,但作为 IPv4 映射的 IPv6 地址">::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}</host>
<host desc="通过名称访问 localhost">localhost</host>
</lok_allow>
<content_security_policy desc="通过指定一个或多个策略指令(用分号分隔)来自定义 CSP 标头。参见 w3.org/TR/CSP2"></content_security_policy>
<frame_ancestors desc="已过时:使用 content_security_policy。指定允许嵌入 Collabora Online iframe 的人(coolwsd 和 WOPI 主机始终允许)。用空格分隔多个主机。"></frame_ancestors>
<connection_timeout_secs desc="指定 coolwsd 发起的连接(如 WOPI 连接)的连接、发送、接收超时(以秒为单位)" type="int" default="30">30</connection_timeout_secs>
<!-- 此设置从根本上改变了 online 的工作方式,不应在生产环境中使用 -->
<proxy_prefix type="bool" default="false" desc="启用通过传入的 ProxyPrefix 重定向请求">false</proxy_prefix>
</net>
<!-- ========================================
SSL 配置
======================================== -->
<ssl desc="SSL 设置">
<!-- 从 https:// + wss:// 切换到 http:// + ws:// -->
<enable type="bool" desc="控制是否启用 coolwsd 与网络之间的 SSL 加密(生产部署不要禁用)。如果默认值为 false,则必须先使用 SSL 支持编译才能启用。" default="true">true</enable>
<!-- SSL 卸载可以在代理中完成,如果是这样,则禁用 SSL,并在生产中启用下面的终止 -->
<termination desc="通过代理连接,其中 coolwsd 作为通过 https 工作,但实际使用 http" type="bool" default="false">false</termination>
<cert_file_path desc="证书文件路径" type="path" relative="false">/etc/coolwsd/cert.pem</cert_file_path>
<key_file_path desc="密钥文件路径" type="path" relative="false">/etc/coolwsd/key.pem</key_file_path>
<ca_file_path desc="CA 文件路径" type="path" relative="false">/etc/coolwsd/ca-chain.cert.pem</ca_file_path>
<ssl_verification desc="启用或禁用 coolwsd 远程主机的 SSL 验证。如果为 true,则 SSL 验证将是严格的,否则不会验证主机的证书。在使用自签名证书的测试环境中可能需要禁用它。" type="string" default="false">false</ssl_verification>
<cipher_list desc="要接受的 OpenSSL 密码列表" type="string" default="ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"></cipher_list>
<!-- HTTP 公钥固定 -->
<hpkp desc="启用 HTTP 公钥固定" enable="false" report_only="false">
<max_age desc="HPKP 的 max-age 指令 - 浏览器应记住 pin 的时间(秒)" enable="true" type="uint" default="1000">1000</max_age>
<report_uri desc="HPKP 的 report-uri 指令 - pin 验证失败在此 URL 报告" enable="false" type="string"></report_uri>
<pins desc="要固定的密钥的 Base64 编码 SPKI 指纹">
<pin></pin>
</pins>
</hpkp>
<!-- 严格传输安全 -->
<sts desc="严格传输安全设置,根据 rfc6797。始终包括子域">
<enabled desc="是否启用严格传输安全。仅在准备生产时启用。在不重置浏览器的情况下无法禁用。" type="bool" default="false">false</enabled>
<max_age desc="严格传输安全 max-age 指令,以秒为单位。允许 0;有关详细信息,请参见 rfc6797。默认为 1 年。" type="int" default="31536000">31536000</max_age>
</sts>
</ssl>
<!-- ========================================
安全设置
======================================== -->
<security desc="更改这些默认值可能会使您面临重大风险">
<seccomp desc="启用 seccomp 系统调用过滤失败是否应是致命错误" type="bool" default="true">true</seccomp>
<!-- 已弃用:如果 capabilities 为 'false'coolwsd 将假定 mount_namespaces 为 'true' 以实现此目标,
仅在 linux 命名空间不可用时避免 chroot 进行进程隔离 -->
<capabilities desc="我们是否需要 capabilities 来将进程隔离到 chroot jail 中" type="bool" default="true">true</capabilities>
<jwt_expiry_secs desc="管理控制台的 JWT 令牌过期前的时间(秒)" type="int" default="1800">1800</jwt_expiry_secs>
<!-- 启用宏执行以支持 Python 脚本 (CallPythonScript PostMessage) -->
<enable_macros_execution desc="指定是否一般启用宏执行。这将启用 Basic 和 Python 脚本从已安装的和文档中执行。如果设置为 false,则忽略 macro_security_level。如果设置为 true,则提到的条目指定宏安全级别。" type="bool" default="false">true</enable_macros_execution>
<macro_security_level desc="宏安全级别。1(中)在执行来自不受信任来源的宏之前需要确认。0(低,不推荐)所有宏将在没有确认的情况下执行。" type="int" default="1">0</macro_security_level>
<enable_websocket_urp desc="我们是否应启用通过 websocket 的 URP(UNO 远程协议)通信。这允许任何有权访问 websocket 的人完全控制 Kit 子服务器,包括在没有确认的情况下执行宏或在 jail 中运行任意 shell 命令。" type="bool" default="false">false</enable_websocket_urp>
<enable_metrics_unauthenticated desc="启用后,/cool/getMetrics 端点将不需要身份验证" type="bool" default="false">false</enable_metrics_unauthenticated>
<server_signature desc="是否在 HTTP 响应标头中发送服务器签名" type="bool" default="false">false</server_signature>
</security>
<!-- ========================================
证书数据库
======================================== -->
<certificates>
<database_path type="string" desc="所有用户都可以使用的 NSS 证书的路径" default=""></database_path>
</certificates>
<!-- ========================================
水印配置
======================================== -->
<watermark>
<opacity desc="屏幕水印的不透明度,从 0.0 到 1.0" type="double" default="0.2">0.2</opacity>
<text desc="如果输入,要在文档上显示的水印文本" type="string"></text>
</watermark>
<!-- ========================================
🎨 用户界面配置(UI 定制核心)
======================================== -->
<user_interface>
<!-- UI 模式:default(默认)/ compact(紧凑)/ tabbed(选项卡) -->
<mode type="string" desc="控制用户界面样式。'default' 表示:从 ui_defaults 获取值,或决定使用 compact 或 tabbed 之一" default="default">compact</mode>
<!-- 集成主题 -->
<use_integration_theme desc="使用来自集成商的主题" type="bool" default="true">true</use_integration_theme>
<!-- 状态栏保存指示器 -->
<statusbar_save_indicator desc="在状态栏中显示保存状态指示器" type="bool" default="true">false</statusbar_save_indicator>
<!-- 🆕 以下是增强的 UI 定制选项(基于官方文档和社区实践) -->
<!-- 显示/隐藏菜单栏 -->
<!-- 注意:这些选项可能需要通过 URL 参数或 PostMessage API 配合使用 -->
<!-- 通过 URL 参数 ui_defaults 可以实现更细粒度的控制,例如:
ui_defaults=UIMode=compact;TextRuler=false;TextStatusbar=false;TextSidebar=false
-->
</user_interface>
<!-- ========================================
后端存储配置
======================================== -->
<storage desc="后端存储">
<filesystem allow="false" />
<!-- WOPI 配置 -->
<wopi desc="允许/拒绝 wopi 存储" allow="true">
<max_file_size desc="要加载的最大文档大小(字节)。0 表示无限制。" type="uint">0</max_file_size>
<!-- 锁定设置 -->
<locking desc="锁定设置">
<refresh desc="我们应该多久与存储服务器重新获取一次锁,以秒为单位(默认 15 分钟)或 0 表示不刷新" type="int" default="900">900</refresh>
</locking>
<!-- 别名组配置 -->
<alias_groups desc="默认模式是 'first',当未定义组时,它只允许第一个主机。将模式设置为 'groups' 并定义组以允许多个主机及其别名" mode="groups">
<!-- 如果需要使用多个 wopi 主机,请将模式更改为 "groups" 并在下面添加主机。
如果一个主机可以通过多个 IP 地址或名称访问,请将它们添加为别名。 -->
<group>
<host desc="Allow some IP" allow="true">http://172\.16\.0\.[0-9]{1,3}</host>
<alias desc="Remix Domain">http://nas\.7bm\.co.*</alias>
<!-- <host desc="Remix Base" allow="true">http://172.16.0.78:51703</host>
<alias desc="Remix Dynamic Ports">http://172\.16\.0\.78:517\d{2}</alias> -->
</group>
<!-- 这里可以有更多 "group" -->
</alias_groups>
<is_legacy_server desc="对于需要已弃用标头的旧服务器,设置为 true" type="bool" default="false">false</is_legacy_server>
</wopi>
<!-- 存储 SSL 配置 -->
<ssl desc="SSL 设置">
<as_scheme type="bool" default="true" desc="设置后,我们专门使用 WOPI URI 的方案来启用存储的 SSL">true</as_scheme>
<enable type="bool" desc="如果 as_scheme 为 false 或未设置,则可以设置此项以强制存储和 coolwsd 之间的 SSL 加密。当为空时,默认遵循 ssl.enable 设置"></enable>
<cert_file_path desc="证书文件路径。当为空时,默认遵循 ssl.cert_file_path 设置" type="path" relative="false"></cert_file_path>
<key_file_path desc="密钥文件路径。当为空时,默认遵循 ssl.key_file_path 设置" type="path" relative="false"></key_file_path>
<ca_file_path desc="CA 文件路径。当为空时,默认遵循 ssl.ca_file_path 设置" type="path" relative="false"></ca_file_path>
<cipher_list desc="要接受的 OpenSSL 密码列表。如果为空,则使用默认值。只有在绝对必要时才能覆盖这些。"></cipher_list>
</ssl>
</storage>
<!-- ========================================
管理控制台配置
======================================== -->
<admin_console desc="Web 管理控制台设置">
<enable desc="启用管理控制台功能" type="bool" default="true">true</enable>
<enable_pam desc="使用 PAM 启用管理用户身份验证" type="bool" default="false">false</enable_pam>
<username desc="管理控制台的用户名。如果启用了 PAM,则忽略。"></username>
<password desc="管理控制台的密码。在大多数平台上已弃用。相反,使用 PAM 或 coolconfig 设置安全密码。"></password>
<!-- 日志记录 -->
<logging desc="记录管理活动,不考虑 logging.level">
<admin_login desc="当管理员登录到控制台时记录" type="bool" default="true">true</admin_login>
<metrics_fetch desc="当访问 metrics 端点并启用 metrics 端点身份验证时记录" type="bool" default="true">true</metrics_fetch>
<monitor_connect desc="当外部监视器连接时记录" type="bool" default="true">true</monitor_connect>
<admin_action desc="当管理员执行某些操作(例如终止进程)时记录" type="bool" default="true">true</admin_action>
</logging>
</admin_console>
<!-- ========================================
监视器配置
======================================== -->
<monitors desc="我们在启动时连接以进行监视的服务器地址">
<!-- <monitor desc="监视器的地址以及断开连接后应尝试重新连接的间隔" retryInterval="20">wss://foobar:234/ws</monitor> -->
</monitors>
<!-- ========================================
隔离文件配置
======================================== -->
<quarantine_files desc="在崩溃或类似情况下,文件存储在此处以供以后检查" default="false" enable="false">
<limit_dir_size_mb desc="最大目录大小,以 MB 为单位。超过指定限制时,将删除较旧的文件。" default="250" type="uint">250</limit_dir_size_mb>
<max_versions_to_maintain desc="要保留同一文件的多少个版本" default="5" type="uint">5</max_versions_to_maintain>
<path desc="将存储隔离文件的目录的绝对路径。不要使用相对路径。" type="path" relative="false"></path>
<expiry_min desc="隔离文件将被删除的时间(分钟)" type="int" default="3000">3000</expiry_min>
</quarantine_files>
<!-- ========================================
缓存文件配置
======================================== -->
<cache_files desc="文件缓存在此处以加快配置支持">
<path desc="将存储缓存文件的目录的绝对路径。不要使用相对路径。" type="path" relative="false"></path>
<expiry_min desc="不使用后缓存文件将被删除的时间(分钟)" type="int" default="3000">1000</expiry_min>
</cache_files>
<!-- ========================================
额外导出格式
======================================== -->
<extra_export_formats desc="启用各种额外的导出格式以实现额外的兼容性。请注意,禁用此处的选项*仅*在视觉上禁用它们:这些导出都是'安全'的,只是可能不希望显示它们,因此您不能在服务器端禁用导出这些">
<impress_swf desc="启用从演示文稿导出 Adobe flash .swf 文件" type="bool" default="false">false</impress_swf>
<impress_bmp desc="启用从演示文稿幻灯片导出 .bmp 位图文件" type="bool" default="false">false</impress_bmp>
<impress_gif desc="启用从演示文稿幻灯片导出 .gif 图像文件" type="bool" default="false">false</impress_gif>
<impress_png desc="启用从演示文稿幻灯片导出 .png 图像文件" type="bool" default="false">false</impress_png>
<impress_svg desc="启用从演示文稿导出交互式 .svg 图像文件" type="bool" default="false">false</impress_svg>
<impress_tiff desc="启用从演示文稿幻灯片导出 .tiff 图像文件" type="bool" default="false">false</impress_tiff>
</extra_export_formats>
<!-- ========================================
服务器端配置
======================================== -->
<serverside_config>
<idle_timeout_secs desc="卸载空闲子 forkit 之前的最大秒数。默认为 1 小时。" type="uint" default="3600">3600</idle_timeout_secs>
</serverside_config>
<!-- ========================================
远程配置
======================================== -->
<remote_config>
<remote_url desc="您将向其发送请求以在响应中获取远程配置的远程服务器" type="string" default=""></remote_url>
</remote_config>
<!-- ========================================
配置更改时停止
======================================== -->
<stop_on_config_change desc="每当配置文件更改时停止 coolwsd" type="bool" default="false">false</stop_on_config_change>
<!-- ========================================
远程字体配置
======================================== -->
<remote_font_config>
<url desc="列出要包含在 Online 中的字体的可选 JSON 文件的 URL" type="string" default=""></url>
</remote_font_config>
<!-- ========================================
缺少字体处理
======================================== -->
<fonts_missing>
<handling desc="如何处理文档中缺少的字体:'report'、'log'、'both' 或 'ignore'" type="string" default="log">log</handling>
</fonts_missing>
<!-- ========================================
间接端点配置
======================================== -->
<indirection_endpoint>
<url desc="以 json 格式提供 routeToken 的服务器的 URL 端点" type="string" default=""></url>
<migration_timeout_secs desc="在卸载文档之前等待来自间接服务器的关闭迁移消息的最大秒数。默认为 180 秒。" type="uint" default="180">180</migration_timeout_secs>
<!-- 地理位置设置 -->
<geolocation_setup>
<enable desc="使用具有地理位置配置的间接服务器时启用 geolocation_setup" type="bool" default="false">false</enable>
<timezone desc="服务器的 IANA 时区。例如:Europe/Berlin" type="string"></timezone>
<allowed_websocket_origins desc="在 websocket 升级期间接受的 Origin 标头">
<!-- <origin></origin> -->
</allowed_websocket_origins>
</geolocation_setup>
<server_name desc="要在集群概述管理面板中显示的服务器名称" type="string" default=""></server_name>
</indirection_endpoint>
<!-- ========================================
家庭模式
======================================== -->
<home_mode>
<enable desc="家庭用户可以启用此设置,这将禁用欢迎屏幕和用户反馈弹出窗口,但也将并发打开连接限制为 20,并发打开文档限制为 10。默认值表示并发打开连接和并发打开文档的数量是无限的,但无法关闭欢迎屏幕和用户反馈。" type="bool" default="false">false</enable>
</home_mode>
<!-- ========================================
Zotero 插件
======================================== -->
<zotero desc="Zotero 插件配置。有关 Zotero 的更多详细信息,请访问 https://www.zotero.org/">
<enable desc="启用 Zotero 插件" type="bool" default="true">true</enable>
</zotero>
<!-- ========================================
帮助 URL
======================================== -->
<help_url desc="帮助根 URL,或留空表示没有帮助(隐藏帮助按钮)" type="string" default="https://help.collaboraoffice.com/help.html?"></help_url>
<!-- ========================================
覆写模式
======================================== -->
<overwrite_mode>
<enable desc="启用覆写模式(用户可以使用插入键)" type="bool" default="false">false</enable>
</overwrite_mode>
<!-- ========================================
WASM 支持
======================================== -->
<wasm desc="WASM 特定设置">
<enable desc="启用 WASM 支持" type="bool" default="false">false</enable>
<force desc="启用后,所有请求都重定向到 WASM" type="bool" default="false">false</force>
</wasm>
<!-- ========================================
文档签名
======================================== -->
<document_signing desc="文档签名设置">
<enable desc="启用文档签名" type="bool" default="true">true</enable>
</document_signing>
</config>
```
369 行,要改成前端具体的 ip 地址
@@ -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` 就可以了
+66
View File
@@ -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 | **组件配置:** `<CollaboraViewer fileId="temp/合同模板-temp-1763460000.docx" mode="edit" highlightTexts={['{姓名}', '{年龄}']} />` |
| **前端** | `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 | **组件配置:** `<CollaboraViewer fileId="temp/filled_合同_张三_1763460000-temp.docx" mode="edit" highlightTexts={['张三', '25']} />` |
| **前端** | `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 和保存操作,这使得高亮能够生效。虽然用户在此模式下可以看到编辑界面,但由于文件位于临时路径,且最终用户下载的是干净的正式文件,该风险可控。
@@ -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
<content_security_policy>frame-ancestors http://172.16.0.59:5173;</content_security_policy>
```
```xml
<alias_groups mode="groups">
<group>
<host desc="LAN Frontend" allow="true">http://172\.16\.0\.59:5173</host>
</group>
</alias_groups>
```
说明:
- 不再保留 `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`
### 问题 3allowed 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 配置快照
@@ -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
<PROJECT_ROOT> LeAudit 项目根目录
<COLLABORA_DEPLOY_ROOT> Collabora 部署目录
<APP_HOST> 浏览器实际访问的主机名或 IP
<APP_PORT> 前端代理入口端口,默认 5173
<FRONTEND_DEV_PORT> Next 开发服务端口,默认 5193
<BACKEND_PORT> 后端服务端口,默认 8096
<COLLABORA_PORT> Collabora 容器端口,默认 9980
<DOCUMENT_BASE_URL> 文档读写后端基础地址
```
当前环境对应值:
```text
<PROJECT_ROOT> = /home/wren-dev/Porject/leaudit-platform
<COLLABORA_DEPLOY_ROOT> = /home/wren-dev/Porject/collabora-backup
<APP_HOST> = 172.16.0.59
<APP_PORT> = 5173
<FRONTEND_DEV_PORT> = 5193
<BACKEND_PORT> = 8096
<COLLABORA_PORT> = 9980
<DOCUMENT_BASE_URL> = 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
<PROJECT_ROOT>
```
当前实际值:
```text
/home/wren-dev/Porject/leaudit-platform
```
### 4.2 Collabora 部署目录
```text
<COLLABORA_DEPLOY_ROOT>
```
当前实际值:
```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_DEPLOY_ROOT>/collabora-image-v5.tar
```
当前环境示例:
```bash
docker load -i /home/wren-dev/Porject/collabora-backup/collabora-image-v5.tar
```
需要重点确认的端口:
- `<APP_PORT>`
- `<BACKEND_PORT>`
- `<COLLABORA_PORT>`
---
## 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_HOST>:<BACKEND_PORT>
APP_URL=http://<APP_HOST>:<APP_PORT>
DOCUMENT_URL=http://<APP_HOST>:<BACKEND_PORT>/docauditai/
COLLABORA_URL=http://<APP_HOST>:<APP_PORT>
```
### 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 <APP_HOST>:<APP_PORT>;
proxy_set_header X-Forwarded-Host <APP_HOST>:<APP_PORT>;
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:<FRONTEND_DEV_PORT>/ http://<APP_HOST>:<APP_PORT>/;
proxy_redirect http://localhost:<FRONTEND_DEV_PORT>/ http://<APP_HOST>:<APP_PORT>/;
```
当前环境实际值:
```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 <APP_HOST>:<APP_PORT>`
- 所有 `X-Forwarded-Host <APP_HOST>:<APP_PORT>`
- 所有 `proxy_redirect`
---
## 9. Collabora Docker Compose 正确配置
文件:`<COLLABORA_DEPLOY_ROOT>/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`
- `<COLLABORA_PORT>` 未被占用
---
## 10. Collabora 主配置正确写法
文件:`<COLLABORA_DEPLOY_ROOT>/coolwsd-config/coolwsd.xml`
### 10.1 WOPI 白名单
模板写法:
```xml
<alias_groups mode="groups">
<group>
<host desc="Frontend Host" allow="true">http://<APP_HOST_REGEX>:<APP_PORT></host>
</group>
</alias_groups>
```
当前环境实际值:
```xml
<alias_groups mode="groups">
<group>
<host desc="LAN Frontend" allow="true">http://172\.16\.0\.59:5173</host>
</group>
</alias_groups>
```
### 10.2 CSP 配置
模板写法:
```xml
<content_security_policy>frame-ancestors http://<APP_HOST>:<APP_PORT>;</content_security_policy>
```
当前环境实际值:
```xml
<content_security_policy>frame-ancestors http://172.16.0.59:5173;</content_security_policy>
```
### 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 <PROJECT_ROOT>
./leaudit.sh restart
```
### 11.2 代理
```bash
docker restart leaudit-collabora-proxy
```
### 11.3 Collabora
推荐直接强制重建,而不是简单 restart:
```bash
cd <COLLABORA_DEPLOY_ROOT>
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 <PROJECT_ROOT>
./leaudit.sh restart
docker restart leaudit-collabora-proxy
cd <COLLABORA_DEPLOY_ROOT>
docker-compose up -d --force-recreate collabora
```
---
## 12. 正确验证步骤
### 12.1 验证前端入口
```bash
curl -I http://<APP_HOST>:<APP_PORT>/
```
期望:
- 返回 `307`
- 跳转到 `http://<APP_HOST>:<APP_PORT>/login?...`
当前环境示例:
```bash
curl -I http://172.16.0.59:5173/
```
### 12.2 验证 discovery
```bash
curl -I http://<APP_HOST>:<APP_PORT>/hosting/discovery
```
期望:
- `200 OK`
### 12.3 验证 Collabora 页面
```bash
curl -I http://<APP_HOST>:<APP_PORT>/browser/dist/cool.html
```
期望:
- `200 OK`
- 响应头包含 `Content-Security-Policy`
- `frame-ancestors` 围绕当前 `<APP_HOST>:<APP_PORT>` 收口
### 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://<APP_HOST>:<APP_PORT>/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 容器的代理
### 问题 3Collabora 白名单正则错误
原因:
- 复杂 alias 正则不兼容当前 coolwsd 解析
修复:
- 改为显式 host
### 问题 4CSP / 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 <PROJECT_ROOT>
./leaudit.sh doctor
```
### 重启前后端
```bash
./leaudit.sh restart
```
### 重建 Collabora
```bash
cd <COLLABORA_DEPLOY_ROOT>
docker-compose up -d --force-recreate collabora
```
### 查看 Collabora 日志
```bash
docker logs --tail 200 collabora_code
```
### 查看代理日志
```bash
tail -n 200 <PROJECT_ROOT>/deploy/collabora-proxy/logs/error.log
```
---
## 17. 新服务器迁移 Checklist
### 17.1 准备阶段
- [ ] 准备好项目代码目录 `<PROJECT_ROOT>`
- [ ] 准备好 Collabora 部署目录 `<COLLABORA_DEPLOY_ROOT>`
- [ ] 准备好 Collabora 镜像 tar 或镜像仓库访问方式
- [ ] 确认 `<APP_HOST>`
- [ ] 确认 `<APP_PORT>``<BACKEND_PORT>``<COLLABORA_PORT>` 不冲突
### 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://<APP_HOST>:<APP_PORT>/`
- [ ] `curl -I http://<APP_HOST>:<APP_PORT>/hosting/discovery`
- [ ] `curl -I http://<APP_HOST>:<APP_PORT>/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
```
如果迁移到新服务器,请先替换 `<APP_HOST>` 等参数,再按本文档的重启与验证步骤执行。