Skip to content

Commit 5f623bb

Browse files
authored
Merge pull request #5 from dream-num/codex/workspace-exchange-ribbon-sync
feat(workspace): add server-side Office exchange
2 parents a2d9d93 + 7c3f4b0 commit 5f623bb

29 files changed

Lines changed: 2298 additions & 145 deletions

apps/workspace/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ Product data is stored in `.data/univer-workspace.sqlite`. Univer unit data is
3636
stored separately in `.data/univer-collaboration.sqlite`, and uploaded Blob
3737
bytes default to `.data/univer-workspace-blobs`.
3838

39+
The Univer editors import and export XLSX/CSV/TSV, DOCX, and PPTX through the
40+
server-side `@univerjs-pro/exchange-node` runtime. These endpoints follow the
41+
Universer Exchange shape under `/universer-api/exchange/**`; they are not part
42+
of the product OpenAPI. The Workspace file action automatically imports
43+
XLS/XLSX/CSV/TSV, DOC/DOCX, and PPT/PPTX as normal Univer Resources in the
44+
selected Space and folder; other file types remain downloadable Blob
45+
Resources. Editor Ribbon imports default to the root of the signed-in user's
46+
Personal Space. Uploaded source files, converted JSON snapshots, and export
47+
files are temporary BlobStore objects with process-local task metadata and a
48+
two-hour lifetime. Exchange actions are not shown for Worktree or merge-preview
49+
editors because those scopes do not yet have scope-aware Office conversion.
50+
3951
### Configuration
4052

4153
Copy `.env.example` to `.env`. Development, database, and production start

apps/workspace/contracts/http/openapi.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ paths:
9494
$ref: ./paths/resources.yaml#/~1api~1resources
9595
/api/resources/{resourceId}:
9696
$ref: ./paths/resources.yaml#/~1api~1resources~1{resourceId}
97+
/api/unit-resources/{unitId}:
98+
$ref: ./paths/resources.yaml#/~1api~1unit-resources~1{unitId}
9799
/api/resources/{resourceId}/open:
98100
$ref: ./paths/resources.yaml#/~1api~1resources~1{resourceId}~1open
99101
/api/blob-upload-sessions:

apps/workspace/contracts/http/paths/resources.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,29 @@
6969
"404":
7070
$ref: ../schemas/common.yaml#/NotFound
7171

72+
/api/unit-resources/{unitId}:
73+
parameters:
74+
- name: unitId
75+
in: path
76+
required: true
77+
schema:
78+
type: string
79+
get:
80+
tags: [Resources]
81+
operationId: getResourceByUnit
82+
summary: Resolve Resource and owning Node metadata from a Univer Unit.
83+
responses:
84+
"200":
85+
description: Current Resource and Node accessible to the signed-in User.
86+
content:
87+
application/json:
88+
schema:
89+
$ref: ../schemas/resources.yaml#/ResourceResponse
90+
"401":
91+
$ref: ../schemas/common.yaml#/Unauthorized
92+
"404":
93+
$ref: ../schemas/common.yaml#/NotFound
94+
7295
/api/resources/{resourceId}/open:
7396
parameters:
7497
- name: resourceId

apps/workspace/docs/application-design.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ React
1414
│ ├── Nodes
1515
│ ├── Resources
1616
│ ├── Blobs / BlobStore
17+
│ ├── Office Exchange
1718
│ ├── Univer Assets / File Gateway
1819
│ ├── Permissions / Trash / Views
1920
│ ├── Worktrees
@@ -130,6 +131,25 @@ Univer Asset Module 适配原生 File API。它将 Slide、Board、Base 等 Unit
130131
`/content` 网关,网关在每次请求时重新解析 Unit/Worktree 权限并禁止缓存。Worktree-local
131132
Asset 在对应 Unit 合入后发布到 Trunk。
132133

134+
Office Exchange Module 适配 Univer Exchange Client 使用的 Universer 协议:
135+
136+
- `source=HttpImport``/universer-api/stream/file/upload` 由 Exchange 接收;Unit Embedded
137+
Asset 的上传继续由 Univer Asset Module 处理;
138+
- import task 使用 `@univerjs-pro/exchange-node` 转换 XLS/XLSX/CSV/TSV、DOC/DOCX 或
139+
PPT/PPTX;Workspace 文件入口按扩展名自动识别这些格式,并在当前 Space 与目录创建正式
140+
Resource,Editor Ribbon 导入则默认创建在当前 User 的 Personal Space 根目录。两种入口都
141+
必须通过 Resource Module,不能直接写入一个没有产品归属的 Collaboration Unit;
142+
- export task 只接受服务端 `AccessResolver` 能解析且允许打开的 Trunk Unit,服务端自行
143+
确认 Unit Type,并物化最新 snapshot 与 Sheet blocks;
144+
- 转换用 source、JSON 和 output 字节保存在 `BlobStore`,任务与临时文件身份只在当前进程
145+
保存并在两小时后失效,不写入产品数据库;
146+
- Worktree 和 Merge Preview 暂不注册 Exchange 插件,避免把 Trunk 内容误当作当前 Scope
147+
导出。Board 没有受支持的 Office 格式,也不注册 Exchange。
148+
149+
这些 `/universer-api` Route 属于 Univer/Universer 兼容入口,不是产品 HTTP contract,因此
150+
不添加到 `contracts/http`。身份仍来自 Workspace Login Session;客户端提交的 User、Role、
151+
Unit Type 或 Unit 可见性都不作为授权依据。
152+
133153
## Collaboration Access Resolver
134154

135155
Collaboration Endpoint 不信任客户端提交的 Resource ID、Unit Type、Role 或

apps/workspace/docs/architecture.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ apps/workspace/
6767
│ │ ├── nodes/
6868
│ │ ├── resources/
6969
│ │ ├── blobs/
70+
│ │ ├── exchange/
7071
│ │ ├── univer-assets/
7172
│ │ ├── operations/
7273
│ │ ├── trash/
@@ -180,6 +181,14 @@ snapshot、changeset 或 revision。Tree Blob 和内嵌 Asset 共用注入的 `B
180181
本地目录,未来可替换为 `S3BlobStore` 或迁移包装器。Univer Collaboration Database Adapter
181182
独立管理协作数据。
182183

184+
Office Exchange Module 使用已发布的 `@univerjs-pro/exchange-node` 将 Office 字节与 Univer
185+
数据互转。`/universer-api/exchange/**`、Exchange File Upload 和签名下载遵循 Universer
186+
协议形状,不进入产品 OpenAPI。导入为 Unit 时通过 Resource Module 创建 Personal Space
187+
根目录下的正式 Node/Resource/Unit;导出时通过 Collaboration SDK 的公开 Unit Runtime
188+
物化最新 Trunk snapshot 和 Sheet blocks,再交给 Exchange Node。转换源文件、JSON snapshot
189+
和导出文件是 `BlobStore` 中的临时对象,任务元数据只存在当前进程并在两小时后过期;它们
190+
不写入产品数据库,也不改变 Collaboration Database Adapter 的所有权边界。
191+
183192
## OpenAPI
184193

185194
```text

apps/workspace/generated/http/openapi.bundled.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,29 @@ paths:
772772
$ref: '#/components/responses/Unauthorized'
773773
'404':
774774
$ref: '#/components/responses/NotFound'
775+
/api/unit-resources/{unitId}:
776+
parameters:
777+
- name: unitId
778+
in: path
779+
required: true
780+
schema:
781+
type: string
782+
get:
783+
tags:
784+
- Resources
785+
operationId: getResourceByUnit
786+
summary: Resolve Resource and owning Node metadata from a Univer Unit.
787+
responses:
788+
'200':
789+
description: Current Resource and Node accessible to the signed-in User.
790+
content:
791+
application/json:
792+
schema:
793+
$ref: '#/components/schemas/ResourceResponse'
794+
'401':
795+
$ref: '#/components/responses/Unauthorized'
796+
'404':
797+
$ref: '#/components/responses/NotFound'
775798
/api/resources/{resourceId}/open:
776799
parameters:
777800
- name: resourceId

apps/workspace/generated/http/schema.d.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,25 @@ export interface paths {
483483
patch?: never;
484484
trace?: never;
485485
};
486+
"/api/unit-resources/{unitId}": {
487+
parameters: {
488+
query?: never;
489+
header?: never;
490+
path: {
491+
unitId: string;
492+
};
493+
cookie?: never;
494+
};
495+
/** Resolve Resource and owning Node metadata from a Univer Unit. */
496+
get: operations["getResourceByUnit"];
497+
put?: never;
498+
post?: never;
499+
delete?: never;
500+
options?: never;
501+
head?: never;
502+
patch?: never;
503+
trace?: never;
504+
};
486505
"/api/resources/{resourceId}/open": {
487506
parameters: {
488507
query?: never;
@@ -2533,6 +2552,30 @@ export interface operations {
25332552
404: components["responses"]["NotFound"];
25342553
};
25352554
};
2555+
getResourceByUnit: {
2556+
parameters: {
2557+
query?: never;
2558+
header?: never;
2559+
path: {
2560+
unitId: string;
2561+
};
2562+
cookie?: never;
2563+
};
2564+
requestBody?: never;
2565+
responses: {
2566+
/** @description Current Resource and Node accessible to the signed-in User. */
2567+
200: {
2568+
headers: {
2569+
[name: string]: unknown;
2570+
};
2571+
content: {
2572+
"application/json": components["schemas"]["ResourceResponse"];
2573+
};
2574+
};
2575+
401: components["responses"]["Unauthorized"];
2576+
404: components["responses"]["NotFound"];
2577+
};
2578+
};
25362579
openResource: {
25372580
parameters: {
25382581
query?: never;

apps/workspace/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"@univerjs-pro/embed-ui": "1.0.0-insiders.20260819-8595af2",
7373
"@univerjs-pro/engine-chart": "1.0.0-insiders.20260819-8595af2",
7474
"@univerjs-pro/engine-formula": "1.0.0-insiders.20260819-8595af2",
75+
"@univerjs-pro/exchange-client": "1.0.0-insiders.20260819-8595af2",
76+
"@univerjs-pro/exchange-node": "1.0.0-insiders.20260819-8595af2",
7577
"@univerjs-pro/ink": "1.0.0-insiders.20260819-8595af2",
7678
"@univerjs-pro/ink-ui": "1.0.0-insiders.20260819-8595af2",
7779
"@univerjs-pro/license": "1.0.0-insiders.20260819-8595af2",

apps/workspace/server/src/app.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { openWorkspaceDatabase } from "./db/initialize.js";
99
import {
1010
createCollaborationRuntime,
1111
type CollaborationRuntime,
12+
type UnitSnapshotStore,
1213
type UnitStore,
1314
} from "./integrations/univer/unit-store.js";
1415
import { createWorktreeBackend } from "./integrations/univer/worktree-store.js";
@@ -97,6 +98,11 @@ import {
9798
UniverAssetsRepository,
9899
type UniverAssetsModule,
99100
} from "./modules/univer-assets/index.js";
101+
import {
102+
createExchangeModule,
103+
createExchangeRouter,
104+
type ExchangeModule,
105+
} from "./modules/exchange/index.js";
100106

101107
export interface WorkspaceApplication {
102108
readonly app: express.Express;
@@ -113,6 +119,7 @@ export interface WorkspaceApplication {
113119
readonly operations: OperationsModule;
114120
readonly blobs: BlobsModule;
115121
readonly univerAssets: UniverAssetsModule;
122+
readonly exchange: ExchangeModule;
116123
attachWebSocket(server: Server): void;
117124
closeRealtime(): Promise<void>;
118125
close(): Promise<void>;
@@ -122,6 +129,7 @@ export function createWorkspaceApplication(
122129
config: WorkspaceConfig,
123130
dependencies: {
124131
readonly unitStore?: UnitStore;
132+
readonly unitSnapshotStore?: UnitSnapshotStore;
125133
readonly worktreeBackend?: WorktreeBackend;
126134
readonly blobStore?: BlobStore;
127135
readonly githubOAuthProvider?: GitHubOAuthProvider;
@@ -136,6 +144,10 @@ export function createWorkspaceApplication(
136144
(collaboration = createCollaborationRuntime(
137145
config.collaborationDatabaseFilename
138146
)).unitStore;
147+
const unitSnapshotStore =
148+
dependencies.unitSnapshotStore ??
149+
collaboration?.unitSnapshotStore ??
150+
unavailableUnitSnapshotStore();
139151
const oauthStateSecret =
140152
dependencies.oauthStateSecret ??
141153
config.githubOAuth?.clientSecret ??
@@ -219,6 +231,13 @@ export function createWorkspaceApplication(
219231
resources,
220232
worktrees,
221233
});
234+
const exchange = createExchangeModule({
235+
access,
236+
resources,
237+
spaces,
238+
snapshots: unitSnapshotStore,
239+
store: blobStore,
240+
});
222241
const collaborationGateway: CollaborationGateway | null = collaboration
223242
? createCollaborationGateway({
224243
service: collaboration.service,
@@ -283,6 +302,10 @@ export function createWorkspaceApplication(
283302
"/api",
284303
createWorktreesRouter({ identity, worktrees })
285304
);
305+
app.use(
306+
"/universer-api",
307+
createExchangeRouter({ identity, exchange })
308+
);
286309
app.use(
287310
"/universer-api",
288311
createUniverAssetsRouter({ identity, assets: univerAssets })
@@ -318,6 +341,7 @@ export function createWorkspaceApplication(
318341
operations,
319342
blobs,
320343
univerAssets,
344+
exchange,
321345
attachWebSocket(server) {
322346
collaborationGateway?.attachWebSocket(server);
323347
},
@@ -329,15 +353,29 @@ export function createWorkspaceApplication(
329353
await collaborationGateway?.dispose();
330354
} finally {
331355
try {
332-
await collaboration?.dispose();
356+
await exchange.dispose();
333357
} finally {
334-
database.close();
358+
try {
359+
await collaboration?.dispose();
360+
} finally {
361+
database.close();
362+
}
335363
}
336364
}
337365
},
338366
};
339367
}
340368

369+
function unavailableUnitSnapshotStore(): UnitSnapshotStore {
370+
return {
371+
async materialize() {
372+
throw new Error(
373+
"A Collaboration snapshot store is required for Unit export."
374+
);
375+
},
376+
};
377+
}
378+
341379
function unavailableWorktreeBackend(): WorktreeBackend {
342380
const unavailable = async (): Promise<never> => {
343381
throw new Error(

0 commit comments

Comments
 (0)