From 640935ec21a5f42feeca9018415a71b70d03f311 Mon Sep 17 00:00:00 2001 From: electroluxcode <3451613934@qq.com> Date: Tue, 14 Jul 2026 22:15:44 +0800 Subject: [PATCH 1/3] feat: support rename in local resource --- .../core/editor-manager.ts | 111 +++++++++++++++++- .../internal/editor/server.ts | 50 +++++++- .../internal/editor/types.ts | 2 + 3 files changed, 160 insertions(+), 3 deletions(-) diff --git a/src/components/onlyoffice-web-comp/core/editor-manager.ts b/src/components/onlyoffice-web-comp/core/editor-manager.ts index 6b3d1d57..dcb668e6 100644 --- a/src/components/onlyoffice-web-comp/core/editor-manager.ts +++ b/src/components/onlyoffice-web-comp/core/editor-manager.ts @@ -65,6 +65,7 @@ import { prepareRevisionReviewDisplay, } from "../feature/revisions"; import { getOnlyOfficeLang, type OnlyOfficeLang } from "../store/lang"; +import { getDocumentObj, setDocumentObj } from "../store/document"; import { initializeOnlyOffice } from "../util/initialize"; import { removeOfficeXmlSizeLimitOverlay, @@ -139,6 +140,8 @@ type OnlyOfficeSdkApi = { pluginMethod_InputText?: (text: string) => void; pluginMethod_PasteText?: (text: string) => void; asc_AddText?: (text: string) => void; + /** OnlyOffice 内部 WOPI 重命名通道;通过 socket rpc 请求宿主重命名。 */ + asc_wopi_renameFile?: (fileName: string) => void; }; /** iframe 内运行时;混淆字段见 type/sdk-internal.ts,asc_* 为公开 API */ @@ -194,6 +197,13 @@ export class EditorManager { private officeXmlSizeLimitOverlayTeardown: (() => void) | null = null; private officeXmlSizeLimitPayload: OfficeXmlSizeLimitExceededPayload | null = null; + private pendingRename: + | { + resolve: (fileName: string) => void; + reject: (error: Error) => void; + timer: number; + } + | null = null; constructor(containerId = ONLYOFFICE_ID) { this.containerId = containerId; @@ -209,6 +219,9 @@ export class EditorManager { onLoadError: (error) => { this.handleServerLoadError(error); }, + onDocumentRename: (fileName) => { + this.syncRenamedDocument(fileName); + }, }); } @@ -1460,6 +1473,21 @@ export class EditorManager { this.dirty = true; } }, + // DocsAPI 仅在注册此回调时暴露「文件 → 重命名」入口。 + // 回调由 OnlyOffice 的内部 Gateway 发出;标题显示已由 iframe 更新, + // 使用 iframe 内 asc_wopi_renameFile 发起 RPC,由内存服务按 WOPI 协议回包。 + onRequestRename: (event: { data?: unknown }) => { + const fileName = + typeof event.data === "string" ? event.data : ""; + void this.renameDocument(fileName).catch((error) => { + this.logger.error("operation", "OnlyOffice document rename failed", { + fileName, + instanceId: this.instanceId, + containerId: this.containerId, + error, + }); + }); + }, // 不注册 onSave/onSaveDocument:内部保存已禁用,导出统一走 export() → downloadAs。 onDownloadAs: () => { // Required so DocsAPI.downloadAs can request the current editor binary. @@ -1623,7 +1651,7 @@ export class EditorManager { this.teardownWordContentSync(); if (options.isNew) { - this.server.openNew(fileType); + this.server.openNew(fileType, options.fileName); } else if (options.file) { await this.server.open(options.file, { fileName: options.fileName, @@ -1810,6 +1838,86 @@ export class EditorManager { return this.fileName; } + /** + * @description 通过 iframe 内 asc_wopi_renameFile 重命名当前实例。 + * SDK 经 socket rpc 收到内存 WOPI 回包后才更新本地标题,因此返回 Promise。 + */ + renameDocument(fileName: string): Promise { + const requestedName = + typeof fileName === "string" ? fileName.trim() : ""; + if (!requestedName) { + return Promise.reject( + new Error("OnlyOffice document name cannot be empty"), + ); + } + + if (this.pendingRename) { + return Promise.reject(new Error("OnlyOffice document rename is pending")); + } + + return new Promise((resolve, reject) => { + const timer = window.setTimeout(() => { + if (this.pendingRename?.timer !== timer) { + return; + } + this.pendingRename = null; + reject(new Error("OnlyOffice document rename timed out")); + }, 5000); + + this.pendingRename = { resolve, reject, timer }; + + try { + + const api = this.requireSdkApi(); + if (!api.asc_wopi_renameFile) { + throw new Error("OnlyOffice WOPI rename API is not available"); + } + api.asc_wopi_renameFile(requestedName); + } catch (error) { + this.rejectPendingRename(error); + } + }); + } + + private syncRenamedDocument(renamedFileName: string) { + this.fileName = renamedFileName; + + const document = getDocumentObj(this.containerId); + setDocumentObj( + { + ...document, + fileName: renamedFileName, + }, + this.containerId, + ); + + const pendingRename = this.pendingRename; + if (pendingRename) { + window.clearTimeout(pendingRename.timer); + this.pendingRename = null; + pendingRename.resolve(renamedFileName); + } + + this.logger.operation("OnlyOffice document renamed through WOPI RPC", { + fileName: renamedFileName, + instanceId: this.instanceId, + containerId: this.containerId, + }); + } + + private rejectPendingRename(error: unknown) { + const pendingRename = this.pendingRename; + if (!pendingRename) { + return; + } + + window.clearTimeout(pendingRename.timer); + this.pendingRename = null; + pendingRename.reject( + error instanceof Error ? error : new Error(String(error)), + ); + } + getContainerParentSelector() { return `${ONLYOFFICE_CONTAINER_CONFIG.PARENT_SELECTOR}[data-onlyoffice-container-id="${this.containerId}"], ${ONLYOFFICE_CONTAINER_CONFIG.PARENT_SELECTOR}`; } @@ -2469,6 +2577,7 @@ export class EditorManager { } destroy() { + this.rejectPendingRename(new Error("OnlyOffice editor was destroyed")); if (this.userSaveTimer !== null) { window.clearTimeout(this.userSaveTimer); this.userSaveTimer = null; diff --git a/src/components/onlyoffice-web-comp/internal/editor/server.ts b/src/components/onlyoffice-web-comp/internal/editor/server.ts index 289a0e06..f5a2bc44 100644 --- a/src/components/onlyoffice-web-comp/internal/editor/server.ts +++ b/src/components/onlyoffice-web-comp/internal/editor/server.ts @@ -1082,11 +1082,14 @@ export class EditorServer { }; } - openNew(fileType?: string) { + openNew(fileType?: string, fileName?: string) { this.fileType = fileType || "docx"; this.id = randomId(); this.loadPromise = null; - this.title = ensureTitleWithExtension("New Document", this.fileType); + this.title = ensureTitleWithExtension( + fileName || "New Document", + this.fileType, + ); const documentType = getDocumentType(this.fileType); let binData: Uint8Array | null = null; @@ -1160,6 +1163,20 @@ export class EditorServer { }; } + /** + * @description 更新当前内存文档标题。文件内容与 document key 保持不变。 + */ + rename(fileName: string) { + const requestedName = fileName.trim(); + if (!requestedName) { + throw new Error("OnlyOffice document name cannot be empty"); + } + + this.title = ensureTitleWithExtension(requestedName, this.fileType); + this.options.onDocumentRename?.(this.title); + return this.title; + } + /** * @description 另存为产物 blob URL,cache GET 失败时供 getFile 回退下载。 */ @@ -2076,6 +2093,35 @@ export class EditorServer { } break; } + case "rpc": { + const rpc = msg.data as Record | undefined; + if (rpc?.type !== "wopi_RenameFile") { + break; + } + + const responseKey = msg.responseKey; + if (typeof responseKey !== "number") { + break; + } + + try { + const title = this.rename(String(rpc.name ?? "")); + const extension = getFileExt(title); + const name = extension + ? title.slice(0, -(extension.length + 1)) + : title; + + // asc_wopi_renameFile 期望 { Name },SDK 会自行补回当前扩展名并广播 asc_onMeta。 + send({ type: "rpc", responseKey, data: { Name: name } }); + } catch (error) { + this.logRaw("warn", "socket", "wopi rename failed", [ + "[EditorServer] wopi rename failed:", + error, + ]); + send({ type: "rpc", responseKey, data: null }); + } + break; + } } } diff --git a/src/components/onlyoffice-web-comp/internal/editor/types.ts b/src/components/onlyoffice-web-comp/internal/editor/types.ts index 97c8401c..4fcd5b17 100644 --- a/src/components/onlyoffice-web-comp/internal/editor/types.ts +++ b/src/components/onlyoffice-web-comp/internal/editor/types.ts @@ -345,6 +345,8 @@ export function isOfficeXmlSizeLimitExceededError( export interface ServerOptions { getState?: () => { readOnly?: boolean }; logger?: EditorLogger; + /** WOPI 重命名 RPC 成功后的回调,携带 SDK 最终采用的完整文件名。 */ + onDocumentRename?: (fileName: string) => void; /** * @description 用户触发保存(非 export/downloadAs 导出)时回调,携带最新文档快照。 */ From 8dddc7e90bec6dd774f696fc51292fae5f577feb Mon Sep 17 00:00:00 2001 From: electroluxcode <3451613934@qq.com> Date: Tue, 14 Jul 2026 23:18:48 +0800 Subject: [PATCH 2/3] feat: support rename in cdn resource --- .../vendor/onlyoffice-cross-origin-bridge.js | 10 ++++++++++ .../onlyoffice-web-comp/const/index.ts | 2 +- .../core/editor-manager.ts | 19 ++++++++++++++++++- .../docs/\346\240\270\345\277\203API.md" | 4 ++-- .../internal/editor/runtime-bridge.ts | 1 + src/features/demo/resource-switcher.tsx | 2 +- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/public/packages/onlyoffice/9.4.0-develop/web-apps/vendor/onlyoffice-cross-origin-bridge.js b/public/packages/onlyoffice/9.4.0-develop/web-apps/vendor/onlyoffice-cross-origin-bridge.js index bb18407a..8b44acc2 100644 --- a/public/packages/onlyoffice/9.4.0-develop/web-apps/vendor/onlyoffice-cross-origin-bridge.js +++ b/public/packages/onlyoffice/9.4.0-develop/web-apps/vendor/onlyoffice-cross-origin-bridge.js @@ -10,6 +10,7 @@ }; var EDITOR_COMMAND = { EDITOR_SUBSCRIBE: "editor:subscribe", + DOCUMENT_RENAME: "document:rename", COMMENT_ADD: "comment:add", COMMENT_UPDATE: "comment:update", COMMENT_REMOVE: "comment:remove", @@ -1598,6 +1599,15 @@ switch (command) { case EDITOR_COMMAND.EDITOR_SUBSCRIBE: return registerEditorCallback(api, payload && payload.event); + case EDITOR_COMMAND.DOCUMENT_RENAME: + if (!payload || !payload.fileName) { + throw new Error("Document file name is required"); + } + if (typeof api.asc_wopi_renameFile !== "function") { + throw new Error("OnlyOffice WOPI rename API is not available"); + } + api.asc_wopi_renameFile(String(payload.fileName)); + return true; case EDITOR_COMMAND.COMMENT_ADD: { var data = payload && payload.data ? payload.data : {}; return ( diff --git a/src/components/onlyoffice-web-comp/const/index.ts b/src/components/onlyoffice-web-comp/const/index.ts index edbd9591..aaeeb361 100644 --- a/src/components/onlyoffice-web-comp/const/index.ts +++ b/src/components/onlyoffice-web-comp/const/index.ts @@ -115,7 +115,7 @@ export type StaticResource = { }; export type OnlyOfficeStaticResourceOptions = { - /** CDN packages 根地址,例如 https://a6006408.onlyoffice-packages.pages.dev。 */ + /** CDN packages 根地址,例如 https://8057ac0e.onlyoffice-packages.pages.dev。 */ cdnOrigin?: string | null; /** CDN 上的 OnlyOffice 目录版本;未传时使用当前 SDK 版本。 */ onlyofficeVersion?: string | null; diff --git a/src/components/onlyoffice-web-comp/core/editor-manager.ts b/src/components/onlyoffice-web-comp/core/editor-manager.ts index dcb668e6..b7081c81 100644 --- a/src/components/onlyoffice-web-comp/core/editor-manager.ts +++ b/src/components/onlyoffice-web-comp/core/editor-manager.ts @@ -1484,7 +1484,7 @@ export class EditorManager { fileName, instanceId: this.instanceId, containerId: this.containerId, - error, + error: error instanceof Error ? error.message : String(error), }); }); }, @@ -1867,6 +1867,23 @@ export class EditorManager { this.pendingRename = { resolve, reject, timer }; try { + if (isOnlyOfficeCdnMode()) { + void callCrossOriginEditor( + this.containerId, + CROSS_ORIGIN_EDITOR_COMMAND.DOCUMENT_RENAME, + { fileName: requestedName }, + ) + .then(() => { + // 跨域 bridge 只能确认 iframe 已调用 SDK API;原生「文件 → 重命名」 + // 路径不一定把 WOPI RPC 回包转回父页。由宿主提交标题,确保导出 + // 快照与当前文件名同步;若 RPC 已先到达,pendingRename 已被清空。 + if (this.pendingRename) { + this.server.rename(requestedName); + } + }) + .catch((error) => this.rejectPendingRename(error)); + return; + } const api = this.requireSdkApi(); if (!api.asc_wopi_renameFile) { diff --git "a/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" "b/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" index f5d2b486..da1ad757 100644 --- "a/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" +++ "b/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" @@ -57,7 +57,7 @@ import { // packages 根地址,目录下应包含 onlyoffice/{version}/... OnlyOfficeManager.registerStaticResource({ - cdnOrigin: "https://a6006408.onlyoffice-packages.pages.dev", + cdnOrigin: "https://8057ac0e.onlyoffice-packages.pages.dev", }); const resource = getStaticResource(); @@ -78,7 +78,7 @@ OnlyOfficeManager.resetStaticResource(); ```typescript type OnlyOfficeStaticResourceOptions = { - /** CDN packages 根地址,例如 https://a6006408.onlyoffice-packages.pages.dev */ + /** CDN packages 根地址,例如 https://8057ac0e.onlyoffice-packages.pages.dev */ cdnOrigin?: string | null; /** CDN 的 SDK 版本;默认当前 9.4 SDK */ onlyofficeVersion?: string | null; diff --git a/src/components/onlyoffice-web-comp/internal/editor/runtime-bridge.ts b/src/components/onlyoffice-web-comp/internal/editor/runtime-bridge.ts index f5a70644..5e562d43 100644 --- a/src/components/onlyoffice-web-comp/internal/editor/runtime-bridge.ts +++ b/src/components/onlyoffice-web-comp/internal/editor/runtime-bridge.ts @@ -603,6 +603,7 @@ export const CROSS_ORIGIN_BRIDGE_MESSAGE = { export const CROSS_ORIGIN_EDITOR_COMMAND = { EDITOR_SUBSCRIBE: "editor:subscribe", + DOCUMENT_RENAME: "document:rename", COMMENT_ADD: "comment:add", COMMENT_UPDATE: "comment:update", COMMENT_REMOVE: "comment:remove", diff --git a/src/features/demo/resource-switcher.tsx b/src/features/demo/resource-switcher.tsx index a0df5c2e..11882a90 100644 --- a/src/features/demo/resource-switcher.tsx +++ b/src/features/demo/resource-switcher.tsx @@ -2,7 +2,7 @@ import { DemoButton, DemoField } from "./demo-toolbar"; import { OnlyOfficeManager } from "@/components/onlyoffice-web-comp"; export const DEFAULT_DEMO_CDN_ORIGIN = - "https://a6006408.onlyoffice-packages.pages.dev"; + "https://8057ac0e.onlyoffice-packages.pages.dev"; export type DemoResourceMode = "local" | "cdn"; From 3b4a6f43bbe5235ecd32956c45043e38410ded7f Mon Sep 17 00:00:00 2001 From: electroluxcode <3451613934@qq.com> Date: Tue, 14 Jul 2026 23:23:02 +0800 Subject: [PATCH 3/3] chore: hidden onRequestRename --- .../core/editor-manager.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/onlyoffice-web-comp/core/editor-manager.ts b/src/components/onlyoffice-web-comp/core/editor-manager.ts index b7081c81..00f97da5 100644 --- a/src/components/onlyoffice-web-comp/core/editor-manager.ts +++ b/src/components/onlyoffice-web-comp/core/editor-manager.ts @@ -1476,18 +1476,18 @@ export class EditorManager { // DocsAPI 仅在注册此回调时暴露「文件 → 重命名」入口。 // 回调由 OnlyOffice 的内部 Gateway 发出;标题显示已由 iframe 更新, // 使用 iframe 内 asc_wopi_renameFile 发起 RPC,由内存服务按 WOPI 协议回包。 - onRequestRename: (event: { data?: unknown }) => { - const fileName = - typeof event.data === "string" ? event.data : ""; - void this.renameDocument(fileName).catch((error) => { - this.logger.error("operation", "OnlyOffice document rename failed", { - fileName, - instanceId: this.instanceId, - containerId: this.containerId, - error: error instanceof Error ? error.message : String(error), - }); - }); - }, + // onRequestRename: (event: { data?: unknown }) => { + // const fileName = + // typeof event.data === "string" ? event.data : ""; + // void this.renameDocument(fileName).catch((error) => { + // this.logger.error("operation", "OnlyOffice document rename failed", { + // fileName, + // instanceId: this.instanceId, + // containerId: this.containerId, + // error: error instanceof Error ? error.message : String(error), + // }); + // }); + // }, // 不注册 onSave/onSaveDocument:内部保存已禁用,导出统一走 export() → downloadAs。 onDownloadAs: () => { // Required so DocsAPI.downloadAs can request the current editor binary.