当前位置: 首页 > java >正文

Onlyoffice集成与AI交互操作指引(Iframe版)

Onlyoffice集成与AI交互操作指引(Iframe版)

本文档系统介绍了软件系统集成OnlyOffice实现在线编辑与AI辅助功能的方案。主要内容包括:后端需提供文档配置信息并实现Callback接口以处理文档保存;前端通过Vue集成编辑器,利用连接器(connector)调用API实现文本操作、菜单定制及事件监听;AI交互采用基于postMessage的消息机制,实现从编辑器发送文本到AI处理并返回结果替换的完整异步流程。该方案实现了文档编辑与AI能力的深度结合。

OnlyOffice集成

后端对接

Onlyoffice只提供的是文档的在线编辑能力,文档的保存、权限、文档信息、配置信息等都需要通过后端服务进行返回。

获取config配置
  1. 描述

    • 配置文件主要包含用户信息,文档信息,编辑器配置,监听事件,及token等配置,用于文件内容的读取和编辑器的渲染。
  2. 文档参考链接

    • https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/config/
  3. 获取配置及文档信息(根据文档链接和操作模式返回配置信息)

{"document": {"fileType": "docx","info": {"favorite": true,"folder": "Example Files","owner": "John Smith","sharingSettings": [{"permissions": "Full Access","user": "John Smith"},{"isLink": true,"permissions": "Read Only","user": "External link"}],"uploaded": "2010-07-07 3:46 PM"},"isForm": true,"key": "Khirz6zTPdfd7","permissions": {"chat": true,"comment": true,"commentGroups": [{"edit": ["Group2",""],"remove": [""],"view": ""}],"copy": true,"deleteCommentAuthorOnly": false,"download": true,"edit": true,"editCommentAuthorOnly": false,"fillForms": true,"modifyContentControl": true,"modifyFilter": true,"print": true,"protect": true,"review": true,"reviewGroups": ["Group1","Group2",""],"userInfoGroups": ["Group1",""]},"referenceData": {"fileKey": "BCFA2CED","instanceId": "https://example.com"},"title": "Example Document Title.docx","url": "https://example.com/url-to-example-document.docx"},"documentType": "word","editorConfig": {"actionLink": "ACTION_DATA","callbackUrl": "https://example.com/url-to-callback.ashx","coEditing": {"change": true,"mode": "fast"},"createUrl": "https://example.com/url-to-create-document/","customization": {"about": true,"anonymous": {"label": "Guest","request": true},"autosave": true,"close": {"text": "Close file","visible": true},"comments": true,"compactHeader": false,"compactToolbar": false,"compatibleFeatures": false,"customer": {"address": "My City, 123a-45","info": "Some additional information","logo": "https://example.com/logo-big.png","logoDark": "https://example.com/dark-logo-big.png","mail": "john@example.com","name": "John Smith and Co.","phone": "123456789","www": "example.com"},"features": {"featuresTips": true,"roles": true,"spellcheck": {"change": true,"mode": true},"tabBackground": {"change": true,"mode": "header"},"tabStyle": {"change": true,"mode": "fill"}},"feedback": {"url": "https://example.com","visible": true},"font": {"name": "Arial","size": "11px"},"forceWesternFontSize": false,"forcesave": false,"goback": {"blank": true,"text": "Open file location","url": "https://example.com"},"help": true,"hideNotes": false,"hideRightMenu": true,"hideRulers": false,"integrationMode": "embed","layout": {"header": {"editMode": true,"save": true,"user": true,"users": true},"leftMenu": {"mode": true,"navigation": true,"spellcheck": true},"rightMenu": {"mode": true},"statusBar": {"actionStatus": true,"docLang": true,"textLang": true},"toolbar": {"collaboration": {"mailmerge": true},"draw": true,"file": {"close": true,"info": true,"save": true,"settings": true},"home": {},"layout": true,"plugins": true,"protect": true,"references": true,"save": true,"view": {"navigation": true}}},"loaderLogo": "https://example.com/loader-logo.png","loaderName": "The document is loading, please wait...","logo": {"image": "https://example.com/logo.png","imageDark": "https://example.com/dark-logo.png","imageLight": "https://example.com/light-logo.png","url": "https://example.com","visible": true},"macros": true,"macrosMode": "warn","mentionShare": true,"mobile": {"forceView": true,"info": false,"standardView": false},"plugins": true,"pointerMode": "select","review": {"hideReviewDisplay": false,"hoverMode": false,"reviewDisplay": "original","showReviewChanges": false,"trackChanges": true},"showHorizontalScroll": true,"showVerticalScroll": true,"slidePlayerBackground": "#000000","submitForm": {"resultMessage": "text","visible": true},"toolbarHideFileName": false,"uiTheme": "theme-dark","unit": "cm","wordHeadingsColor": "#00ff00","zoom": 100},"embedded": {"embedUrl": "https://example.com/embedded?doc=exampledocument1.docx","fullscreenUrl": "https://example.com/embedded?doc=exampledocument1.docx#fullscreen","saveUrl": "https://example.com/download?doc=exampledocument1.docx","shareUrl": "https://example.com/view?doc=exampledocument1.docx","toolbarDocked": "top"},"lang": "en","mode": "edit","plugins": {"autostart": ["asc.{0616AE85-5DBE-4B6B-A0A9-455C4F1503AD}","asc.{FFE1F462-1EA2-4391-990D-4CC84940B754}"],"options": {"all": {"keyAll": "valueAll"},"asc.{38E022EA-AD92-45FC-B22B-49DF39746DB4}": {"keyYoutube": "valueYoutube"}},"pluginsData": ["https://example.com/plugin1/config.json","https://example.com/plugin2/config.json"]},"recent": [{"folder": "Example Files","title": "exampledocument1.docx","url": "https://example.com/exampledocument1.docx"},{"folder": "Example Files","title": "exampledocument2.docx","url": "https://example.com/exampledocument2.docx"}],"region": "en-US","templates": [{"image": "https://example.com/exampletemplate1.png","title": "exampletemplate1.docx","url": "https://example.com/url-to-create-template1"},{"image": "https://example.com/exampletemplate2.png","title": "exampletemplate2.docx","url": "https://example.com/url-to-create-template2"}],"user": {"group": "Group1,Group2","id": "78e1e841","image": "https://example.com/url-to-user-avatar.png","name": "John Smith"}},"events": {},"height": "100%","token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.LwimMJA3puF3ioGeS-tfczR3370GXBZMIL-bdpu4hOU","type": "desktop","width": "100%"
}
实现Callback接口
  1. 描述

    • callback的链接在获取配置时已经返回,onlyoffice会根据配置的callback地址,进行回调,通知服务有关文档编辑的状态,用户需要根据相关状态实现文档的保存。
  2. 文档参考链接

  • https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/callback-handler/
  1. 注意事项
  • 要对回调内容中token进行校验,校验合法后才允许文件的更新保存

页面集成

VUE集成
  1. 文档参考链接

    • https://api.onlyoffice.com/zh-CN/docs/docs-api/get-started/frontend-frameworks/vue/
  2. 参考实现(测试验证时的实现)

该集成页面实现了编辑器集成,自定义菜单注册,API执行,AI交互,消息发送与监听

<template><div style="height: 100%;"><div>OnlyOffice + AI交互示例</div><!-- 控制按钮 --><button @click="getFullText">获取全文</button><button @click="replaceWithAIResult">替换文本</button><button @click="getFullHtml">获取全文(HTML)</button><button @click="showMessage">消息展示</button><button @click="addComment">添加注释</button><button @click="getSelection">获取选中文本</button><button @click="getSelectionType">获取选择类型</button><button @click="inputText">输入文本</button><button @click="pasteHtml">粘贴HTML</button><button @click="searchAndReplace">搜索替换</button><div id="container"><div id="onlyoffice"></div><!-- <iframe id="aiIframe"src="AIURL"></iframe> --></div></div>
</template><script>
export default {data() {return {docEditor: null,connector: null,aiBox: {width: 226,height: 284,bottom: 5,isDragging: false},aiResult: ""};},mounted() {this.initOnlyOffice();// ---------------- AI iframe 返回结果 ----------------window.addEventListener('message', (event) => {if (!event.data || event.data.type !== 'replaceText') return;const { content } = event.data.data;// 这里执行文档替换操作,比如调用 OnlyOffice connector 方法if (this.connector) {this.connector.executeMethod("PasteText", [content]);console.log("已替换选中文本", content);}});},methods: {// 初始化 OnlyOffice 编辑器async initOnlyOffice() {try {const res = await fetch("http://{yourbacekservice}/onlyoffice/config?id=123");const config = await res.json();config.editorConfig.events = {onAppReady: () => {console.log("OnlyOffice加载完成");},onDocumentReady: this.onDocumentReady};const script = document.createElement("script");script.src = "http://{youronlrofficehost}/web-apps/apps/api/documents/api.js";script.onload = () => {this.docEditor = new DocsAPI.DocEditor("onlyoffice", config.editorConfig);};document.head.appendChild(script);} catch (e) {console.error("OnlyOffice初始化失败", e);}},onDocumentReady() {this.connector = this.docEditor.createConnector();console.log("文档准备就绪", this.connector);this.connector.attachEvent("onContextMenuShow", () => {this.connector.executeMethod("GetSelectedText", [], selectedText => {const hasSelection = selectedText && selectedText.trim().length > 0;const childItems = [{ id: "analyzeText", text: "分析文本内容", onClick: () => this.sendToAI(selectedText, "analyzeText") }];if (hasSelection) {childItems.push({id: "optimizeText",text: "文案优化",onClick: () => this.sendToAI(selectedText, "optimizeText")},{ id: "correctText", text: "文本纠错", onClick: () => this.sendToAI(selectedText, "correctText") },{id: "translateText",text: "文本翻译",onClick: () => this.sendToAI(selectedText, "translateText")});}this.connector.addContextMenuItem([{ id: "hopeSeekAI", text: "HopeSeek(AI)", items: childItems }]);});});const iframe = document.getElementById('aiIframe');if (!iframe) return;iframe.contentWindow.postMessage({type: 'openChange',data: { isShow: true }}, '*');},sendToAI(content, action) {const iframe = document.getElementById("aiIframe");if (!iframe) return;iframe.contentWindow.postMessage({type: "aiRequest",data: { content, action, source: "onlyoffice" }}, "*");},getSelectionType() {this.connector.executeMethod("GetSelectionType", [], selectedType => {console.log("获取选择类型", selectedType);});},inputText() {this.connector.executeMethod("InputText", ["ONLYOFFICE Plugins", ""])},getSelection() {// this.connector.executeMethod("GetSelectedText", [], selectedText => {//     console.log("已获取选中文本", selectedText);// });this.connector.executeMethod("GetSelectedText", [{ "Numbering": false, "Math": false, "TableCellSeparator": '\n', "ParaSeparator": '\n', "TabSymbol": String.fromCharCode(9) }], function (data) {const sText = data;// ExecTypograf (sText);console.log(sText);});},replaceWithAIResult() {this.connector.executeMethod("PasteText", ["要粘贴的内容"]);},pasteHtml() {this.connector.executeMethod("PasteHtml", ["&lt;p&gt;&lt;b&gt;Plugin methods for OLE objects&lt;/b&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;AddOleObject&lt;/li&gt;&lt;li&gt;EditOleObject&lt;/li&gt;&lt;/ul&gt;"]);},searchAndReplace() {this.connector.executeMethod("SearchAndReplace", [{"searchString": "目标","replaceString": "R目标R","matchCase": true}]);},getFullText() {this.connector.callCommand(() => Api.GetDocument().GetText(), data => console.log(data));},getFullHtml() {this.connector.executeMethod("GetFileHTML", null, res => console.log(res));},showMessage() {this.docEditor.showMessage("12324")},// AI 浮窗拖拽操作startDrag(e) {e.preventDefault();this.aiBox.isDragging = true;this.aiBox.startY = e.clientY - this.aiBox.bottom;document.addEventListener('mousemove', this.onDrag);document.addEventListener('mouseup', this.stopDrag);},onDrag(e) {if (!this.aiBox.isDragging) return;let newY = window.innerHeight - e.clientY;if (newY > 5 && (window.innerHeight - newY) > 150) this.aiBox.bottom = newY;document.getElementById('aiBox').style.bottom = this.aiBox.bottom + 'px';},stopDrag() {this.aiBox.isDragging = false;document.removeEventListener('mousemove', this.onDrag);document.removeEventListener('mouseup', this.stopDrag);},addComment() {this.connector.executeMethod("AddComment", [{"UserName": "John Smith","QuoteText": "text","Text": "要填写的注释内容","Time": "1662737941471","Solved": false,}], function (comment) {console.log(comment)});}}};
</script><style scoped>
body,
html {margin: 0;height: 100%;overflow: hidden;
}#container {display: flex;height: 100%;
}#onlyoffice {flex: 2;
}#sidebar {width: 350px;border-left: 1px solid #ddd;
}#aiBox {position: fixed;right: 0;bottom: 5px;width: 600px;height: 100%;z-index: 2000;border: 1px solid #ccc;background: #fff;box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
}#aiIframe {width: 70%;height: 100%;border: none;
}
</style>

连接器(connector)介绍

简介

对于我们而言,很多情况下都是简单的操作一下文档,做一些和业务系统相关操作的功能,使用到:callCommand 、executeMethod、attachEvent、detachEvent这四个核心块api模块。

初始化
this.connector = this.docEditor.createConnector();
核心块说明
  1. callCommand()
-   基础api调用模块,用于组合并执行复杂api或者自定义代码。-  文档链接:[https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/overview/how-to-call-commands/](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/overview/how-to-call-commands/)
  • 示例
this.connector.callCommand(() => Api.GetDocument().GetText(), data => console.log(data));
  1. executeMethod()。

    • 直接执行某个api,它与callCommand的区别是:callCommand是自己写代码执行也就是执行function(){xxxxx}方法体,executeMethod执行的只是某一个方法。

    • 文档链接:https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/overview/how-to-call-methods/

  • 示例
this.connector.executeMethod("PasteText", ["要粘贴的内容"]);
  1. attachEvent、detachEvent
  • 绑定、解绑事件。
-  文档在这:[https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/overview/how-to-attach-events/](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/overview/how-to-attach-events/)-   示例
/**
* 绑定事件
*/
connector.attachEvent("onAddComment", function(){console.log("event: onAddComment");
});/**
* 解绑事件
*/
connector.detachEvent("onAddComment");

常用API

添加上下文菜单-addContextMenuItem
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/automation-api/
  • 代码示例
connector.attachEvent("onContextMenuShow", (options) => {connector.addContextMenuItem([{text: "mainItem",onClick: () => {console.log("[CONTEXTMENUCLICK] menuSubItem1");},}]);
});
添加工具栏菜单-addToolbarMenuItem
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/automation-api/#addtoolbarmenuitem
  2. 代码示例

connector.addToolbarMenuItem({tabs: [{text: "Connector",items: [{id: "toolConnector1",type: "button",text: "Meaning",hint: "Meaning",lockInViewMode: true,icons: "./icon.svg",items: [{id: "toolC1",text: "Text",data: "Hello",onClick: (data) => {console.log(`[TOOLBARMENUCLICK]: ${data}`);},},],},],},],
});
事件监听-attachEvent
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/automation-api/#attachevent
  2. 代码示例

connector.attachEvent("onChangeContentControl", (obj) => {console.log(`[EVENT] onChangeContentControl: ${JSON.stringify(obj)}`)
})
添加注释-AddComment
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/AddComment/
  2. 代码示例

addComment() {this.connector.executeMethod("AddComment", [{"UserName": "John Smith","QuoteText": "text","Text": "要填写的注释内容","Time": "1662737941471","Solved": false,}], function (comment) {console.log(comment)});}
获取选中内容-GetSelectedContent
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/GetSelectedContent/
  2. 代码示例


getSelection() {方法一:this.connector.executeMethod("GetSelectedText", [], selectedText => {console.log("已获取选中文本", selectedText);});方法二:this.connector.executeMethod("GetSelectedText", [{"Numbering": false, "Math": false, "TableCellSeparator": '\n', "ParaSeparator": '\n', "TabSymbol": String.fromCharCode(9)}], function (data) {const sText = data;// ExecTypograf (sText);console.log(sText);});
},
获取选择类型-GetSelectionType
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/GetSelectionType/
  2. 代码示例

getSelectionType() {this.connector.executeMethod("GetSelectionType", [], selectedType => {console.log("获取选择类型", selectedType);});
},
输入文本-InputText
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/InputText/
  2. 代码示例

  inputText() {this.connector.executeMethod("InputText", ["ONLYOFFICE Plugins", ""])},
粘贴文本-PasteText(如果当期有选中内容的话,实际是删除并粘贴)
  1. 文档链接

    • https://api.onlyoffice.com/zh-CN/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/PasteText/
  2. 代码示例

this.connector.executeMethod("PasteText", ["要粘贴的内容"]);
查找并替换文本-SearchAndReplace
  1. 文档链接
  • https://api.onlyoffice.com/zh-CN/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/SearchAndReplace/
  1. 代码示例
searchAndReplace() {this.connector.executeMethod("SearchAndReplace", [{"searchString": "目标","replaceString": "R目标R","matchCase": true}]);},

AI交互

整体说明

ONLYOFFICE与AI助手的交互本质上是一个基于 “消息驱动” 的异步通信模型,其核心是 postMessage API。整个过程可以清晰地划分为两个主要阶段:请求阶段响应阶段

整个交互流程可以概括为以下两个核心阶段:

第一阶段:从编辑器到AI助手(发送请求)

此阶段完成从用户操作到AI接收处理任务的闭环。

  1. 用户操作触发

    • 用户在ONLYOFFICE在线编辑器中选择一段文本内容。

    • 用户点击编辑器菜单中集成的AI功能按钮(例如:“内容优化”、“续写”、“翻译”、“添加注释”等)。

  2. 集成页面捕获事件并组装消息

    • 集成页面(即嵌入编辑器的父页面)监听到来自编辑器的这个特定菜单点击事件。

    • 集成页面通过编辑器提供的API(如getSelectedText)获取用户当前选中的内容。

    • 集成页面将操作事件类型(如:"analyzeText")和选中的内容组装成一个结构化的消息对象。例如:

{"type": "ai-request", // 固定消息类型,表明这是一条AI请求"source":"onlyoffice", "data": {"action": "analyzeText",     // 具体的事件类型"selectedText": "这里是用户选中的文本内容...","otherParams": {}     // 其他可能需要的参数}
}
  1. 发送消息至AI助手

    • 集成页面通过 postMessage 方法,将该消息对象发送到AI助手(通常是一个独立的、隐藏的或浮层的<iframe>窗口)。

    • 发送时指定AI助手窗口的源(origin),以确保安全。

 const iframe = document.getElementById("aiIframe");if (!iframe) return;iframe.contentWindow.postMessage({"type": "ai-request", // 固定消息类型,表明这是一条AI请求"source":"onlyoffice", "data": {"action": "analyzeText",     // 具体的事件类型"selectedText": "这里是用户选中的文本内容...","otherParams": {}     // 其他可能需要的参数}}, "");

第二阶段:从AI助手回编辑器(执行操作)

此阶段完成从AI生成结果到编辑器内容更新的闭环。

  1. AI助手监听并处理消息

    • AI助手窗口通过window.addEventListener('message', ...)持续监听消息。

    • 接收到消息后,首先验证消息来源的合法性,确保其来自集成的父页面。

    • 解析消息内容,根据action字段判断需要执行的具体AI任务(例如:调用“内容优化”的API)。

    • AI助手调用相应的后端AI服务接口,获取生成的结果。

  2. 用户确认与指令发送

    • AI助手将生成的结果展示给用户(在它的UI界面中)。

    • 用户查看结果后,点击“替换”、“插入”或“取消”等按钮。

    • 当用户点击“替换”时,AI助手会组装一条响应消息。例如:


{"type": "ai-response", // 固定消息类型,表明这是一条AI请求"source":"ai-plugin", "data": {"action": "analyzeText",     // 具体的事件类型"content": "content","otherParams": {"sourceMessage":{源消息内容}}     // 其他可能需要的参数}
}
  1. 集成页面接收并执行操作

    • 集成页面监听来自AI助手窗口的message事件。

    • 接收到响应消息后,同样进行来源验证和解析。

    • 根据解析出的

      • 替换(Replace):用 响应的content 替换当前选中的文本。

      • 插入(Insert):在光标处或选定位置插入响应的 content

      • 添加注释(Comment):为选定文本或指定位置添加以响应的content为内容的注释。

 window.addEventListener('message', (event) => {//进行事件,消息源,源数据的校验
});

整体交互流程

整个交互过程可以概括为下图所示的闭环流程:

UserEditor(ONLYOFFICE)Parent Page(集成页面)AI Helper(AI助手 iframe)AI ServiceAI Helper1. 选择文本并点击AI菜单2. 触发事件3. 获取选中文本,组装消息4. postMessage(请求)5. 调用API生成内容6. 返回结果7. 展示生成结果8. 点击“替换”9. 组装响应消息10. postMessage(响应)11. 调用编辑器API执行操作12. 更新文档内容UserEditor(ONLYOFFICE)Parent Page(集成页面)AI Helper(AI助手 iframe)AI ServiceAI Helper

核心特点:

  • 解耦设计:编辑器与AI功能模块分离,通过标准API通信,易于开发和维护。

  • 安全通信:使用postMessage并严格验证 origin,保障跨域通信安全。

  • 异步交互:所有操作均为非阻塞,保证用户体验流畅。

  • 可扩展性:只需定义新的type和对应的处理逻辑,即可轻松添加更多AI功能。

http://www.xdnf.cn/news/20181.html

相关文章:

  • 美团发布 | LongCat-Flash最全解读,硬刚GPT-4.1、Kimi!
  • 标签系统的架构设计与实现
  • Oracle软件在主机平台的应用(课程下载)
  • 请求超过Spring线程池的最大线程(处理逻辑)
  • 企业级项目管理方法设计指南
  • Scikit-learn Python机器学习 - 特征预处理 - 标准化 (Standardization):StandardScaler
  • 音视频面试题集锦第 38 期
  • 电影级文字生视频核心代码手册
  • CASToR 生成的文件进行转换
  • 1.数据库介绍
  • java面试:有了解过数据库事务吗,能详细讲一讲么
  • 四川地区燃气从业人员考试题库及答案
  • Redis中的hash数据类型
  • 在LangChain中无缝接入MCP服务器扩展AI智能体能力
  • 从零开始的云计算生活——第五十九天,基于Jenkins自动打包并部署Tomcat环境
  • 浅析多模态标注对大模型应用落地的重要性与标注实例
  • 图像的几种成像方式简介
  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(二十六)windows平台运行时隐藏控制台
  • 手把手教你用Go打造带可视化的网络爬虫
  • Day36 IO多路复用技术
  • Docker Desktop 安装 wsl问题
  • android 四大组件—Activity源码详解
  • 沪深300股指期权包含上证50期权吗?
  • Chatwith:定制你的AI 聊天机器人
  • 如何从chrome中获取会话id
  • 三坐标测量机在汽车制造行业中的应用
  • 用得更顺手的 Protobuf 文件后缀、流式多消息、大数据集与“自描述消息”实战
  • 禁毒教育展厅互动设备-禁毒教育基地-禁毒体验馆方案-VR禁毒教育软件
  • 设计模式从入门到精通之(六)策略模式
  • 资源管理-dd命令