docs(collabora): organize deployment guides and fix proxy chain
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 内容为: -->
|
||||

|
||||
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 并更新现有瓦片的像素数据
|
||||
|
||||
这种机制实现了**高效的文档滚动和实时协作编辑**,类似于视频编码中的 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: "..." }` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 嵌入 Iframe(Embedding 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>
|
||||
```
|
||||
|
||||
@@ -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="允许/拒绝 POST(REST)的客户端 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` 就可以了
|
||||
|
||||
@@ -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`
|
||||
|
||||
### 问题 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 配置快照
|
||||
@@ -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 容器的代理
|
||||
|
||||
### 问题 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 <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>` 等参数,再按本文档的重启与验证步骤执行。
|
||||
Reference in New Issue
Block a user