WordCraft 的 Zotero 集成:纯 Rust 文字处理器如何实现 Zotero 引文协议

发布时间:2026/11/2 6:14:41
WordCraft 的 Zotero 集成:纯 Rust 文字处理器如何实现 Zotero 引文协议

【免费下载链接】wordcraft

An open-source, clean-room reimplementation of Microsoft Word in pure Rust

项目地址:https://gitcode.com/gh_mirrors/word/wordcraft
点击查看免费下载

WordCraft 是 Microsoft Word 的 clean-room 开源重实现(纯 Rust),它并非"另写一套引文系统",而是直接扮演 Zotero 桌面客户端的被驱动方:由 Zotero 自己的引文对话框、CSL 样式和书目刷新逻辑来完成工作,WordCraft 只负责按 Zotero 的线协议读写文档。本文以 docs/zotero.md 为骨架,结合 crates/zotero 的源码、crates/ui-egui/src/zotero.rs 的 UI 线程桥接、apps/wordcraft-cli/src/main.rs 的 CLI 子命令与相关测试,逐层拆解命令清单、文档存储格式、帧协议、安全边界与已知限制。读完后你可以:在 app、命令行和 MCP/控制通道三种入口上正确发起 Zotero 会话,能读懂.docx里的 Zotero 字段,并在出问题时定位到底层哪一跳出了错。

一、设计定位:Zotero 在窗口里干活,WordCraft 在文档里落地

Zotero 的桌面程序会在本机127.0.0.1:23116上监听一个"文字处理器插件"端口,这就是它公开发布的 LibreOffice 插件线协议。协议的方向很关键:

  • WordCraft → Zotero:只发送一条"集成命令"(添加/编辑引文、书目、刷新……),事务号(txid)为 0,不需要回复;
  • Zotero → WordCraft:Zotero 弹出它自己的对话框,然后通过Document_insertField、Field_setText等调用反过来操作文档,最后以Document_complete收尾,这些调用带非零 txid,WordCraft 必须按 txid 回包。

因此安装成本为零:Zotero 端不需要装任何插件,只要它正在运行。WordCraft 侧的实现分布在三个层次:

层位置职责
协议与文档落地crates/zotero帧编解码、连接与对端校验、把 Zotero 的调用翻译成引擎Session编辑
应用集成crates/ui-egui/src/zotero.rs在后台线程持有 socket,把每个调用交给 UI 线程回答,弹窗询问用户
命令行apps/wordcraft-cli/src/main.rswordcraft-cli zotero子命令,无 UI 地对一个文件跑一次会话

crates/zotero/src/lib.rs 的模块文档明确说明:实现是照 Zotero 公开的协议文档写的,不使用 Zotero 插件代码。

二、界面入口:功能区上的 Zotero 选项卡

桌面应用的功能区有一个Zotero选项卡,按钮分组正好对应"引文 / 书目 / 文档"的操作节奏。渲染代码在 crates/ui-egui/src/ribbon/references.rs,每个按钮只是把ButtonAction指向下面命令表中的一个 id:

  • Citations组:Add/Edit Citation、Add Note、Move Past Citation(注意后者是引擎命令caret.pastCitation,不是ui.zotero.*);
  • Bibliography组:Add/Edit Bibliography;
  • Document组:Refresh、Document Preferences、Unlink Citations。

按钮的可用性由 crates/ui-egui/src/widgets/buttons.rs 统一裁决:ui.zotero.*在已有 Zotero 会话进行中时被禁用,Web 构建(wasm32)下也始终禁用——因为浏览器打不开本地 socket。

三、命令清单与语义

3.1 七个ui.zotero.*命令

命令作用关键实现
ui.zotero.addEditCitation在光标处添加引文,或编辑光标所在的引文Command::AddEditCitation
ui.zotero.addEditBibliography在光标处添加书目,或编辑已有书目Command::AddEditBibliography
ui.zotero.refresh更新全部引文与书目Command::Refresh
ui.zotero.setDocPrefs引文样式、语言、脚注式或文内式Command::SetDocPrefs
ui.zotero.removeCodes断开引文:保留文字,丢掉 Zotero 的域代码Command::RemoveCodes
ui.zotero.addNote插入一条 Zotero 笔记Command::AddNote
ui.zotero.status返回{busy, waitingForAnswer, last}crates/ui-egui/src/zotero.rs
ui.zotero.answer{"button": n}:回答 Zotero 正在提的问题见下文按钮编号

前六条对应 crates/zotero/src/client.rs 里的Command枚举,线名分别是addEditCitation、addEditBibliography、addNote、refresh、removeCodes、setDocPrefs;from_name按不区分大小写匹配(测试里ADDEDITCITATION也能解析),未知名字会报错并列出全部合法值。

这些命令在 app 内可用,也能通过控制通道与 MCP 转发到 app,具体入口见 docs/control-protocol.md(engine.execute接受ui.zotero.*)与 docs/mcp.md。注意 MCP 文档中特别说明:Zotero 命令走的是本地 socket,不读写文件系统,因此自动化读写根目录的限制(--automation-read-root/--automation-write-root)对它没有约束力。

3.2 立即返回 + 状态轮询 + 回答提问

交互模型是"异步开火":命令立刻返回{"started": "<线名>"},随后 Zotero 在自己的窗口里工作,WordCraft 在后台应答。这带来两条必须掌握的规则:

  1. 同一时刻只允许一个会话。再次启动会返回错误Zotero is still working on addEditCitation; finish in Zotero's window first(crates/ui-egui/src/zotero.rs)。
  2. 提问必须由宿主回答。Zotero 会发Document_displayAlert,app 把它变成对话框;Agent 则先用ui.zotero.status读到waitingForAnswer里的文本,再用ui.zotero.answer作答。

按钮编号沿用 Zotero 的常量(crates/zotero/src/bridge.rs):OK组按 1/0,Yes/No按 1/0,三按钮Yes/No/Cancel按 2/1/0。ui.zotero.answer会校验范围:只有三按钮问题允许 0–2,其余只允许 0–1,越界会报错且弹窗保持未答状态,不会吞掉问题。macOS 上按钮的左右顺序会调整(默认按钮在最右),但发给 Zotero 的数值不变。

3.3status的三种last

一次会话结束后,last会是下面两种形态之一:

{"command": "refresh", "completed": true, "calls": 9, "errors": []} {"command": "refresh", "error": "Zotero isn't running (start Zotero and try again): ..."}

completed: false通常意味着用户在 Zotero 对话框里点了取消——Zotero 会直接关闭连接,Outcome::completed保持false。同时 crates/zotero/src/lib.rs 定义了四类错误:NotRunning(端口没人监听)、Io(会话中途连接失败)、Protocol(对端发的不是协议)、Refused(对端身份不被接受)。

3.4 一次命令 = 一个撤销步骤

桥接层在第一次真正改动文档之前打一个检查点,标签固定为"Zotero"(crates/zotero/src/bridge.rs)。因此 Zotero 的一次会话无论在文档里插入了多少字段、走了多少次脚注转换,Ctrl+Z都只回退一步。测试对此有直接断言:撤销后文本回到"Hello world"、自定义属性被清空、can_undo()为 false。

3.5 文档被替换时会话会失败

如果命令进行中用户执行了 New 或 Open,WordCraft 会拒绝该会话后续的调用(错误文本WordCraft replaced the document this command started in),新文档保持原样,且不会留下旧会话的撤销步骤。相关测试见 crates/ui-egui/src/zotero.rs。多窗口场景下行为更细:每个窗口绘制时轮询,只有"会话开始的那个文档所在的窗口"才应答;单窗口(Web 式)布局下则由当前窗口带着那个文档继续完成引文。

四、命令行:wordcraft-cli zotero

对着一个文件跑一次会话:

wordcraft-cli zotero refresh paper.docx --save paper.docx [--trace]

完整语法(来自 CLI 的用法文本与选项表):

wordcraft-cli zotero <command> <file> [--cmd 'id={json}' …] [--save OUT] [--trace] [--port P]
  • <command>:addEditCitation、addEditBibliography、addNote、refresh、removeCodes、setDocPrefs,大小写不敏感;
  • <file>:要打开的文档(zotero子命令的第 2 个位置参数,见OPENS_FILE)。它允许是非 UTF-8 文件名的路径,例如 Latin-1 的café.docx;其它位置参数若是非法 Unicode 则报错;
  • --cmd 'id={json}':在连接 Zotero 之前执行引擎命令,常用于把光标摆到正确位置,例如--cmd caret.docEnd;不带=时参数视为{};
  • --save OUT:会话结束后保存;
  • --trace:把每一对请求/响打印到 stderr;
  • --port P:只替换端口,地址仍固定为本机回环(CLI 只对Options.addr调set_port)。

实际运行时输出格式(apps/wordcraft-cli/src/main.rs):

← Document_insertField [1,"ReferenceMark",0] → [1,"",0] ... 3 calls, completed wrote paper.docx

--trace打印的内容会经过escape_peer处理:反斜杠、换行、控制字符、行分隔符与双向文本控制符一律转成\\、\n、\u{1b}之类的转义。这是安全项而非美观项:对端(或冒充对端的程序)因此无法伪造出额外的诊断行来驱动终端。单行最多 400 字符,超出用…截断。

CLI 使用Headless宿主,遇到 Zotero 的提问不会阻塞,而是按"肯定答复"回答(三按钮回 2,其余回 1)并把文本记录到host.alerts,最后打到 stderr。

五、文档里到底存了什么:与 Word 完全一致

这是整个集成的核心承诺:同一份文档可以在 Word 与 WordCraft 之间轮流编辑,Zotero 在两边都能识别。实现方式是照抄 Word 插件的存储布局(crates/zotero/src/fields.rs)。

5.1 引文与书目是带特定代码的域

  • 引文:域代码为ADDIN ZOTERO_ITEM CSL_CITATION {…};
  • 书目:域代码为ADDIN ZOTERO_BIBL … CSL_BIBLIOGRAPHY;
  • 存储前缀常量PREFIX = "ADDIN ZOTERO_",而 Zotero 在协议里看到的是去掉前缀的ITEM CSL_CITATION {…}。zotero_code()与stored_code()负责这对互转,前缀比较不区分大小写,且要求先trim_start。

脚注/尾注里的域会列在正文引用标记处,并带上注释编号(ZField.note_index)。这一点在 crates/zotero/src/fields.rs 的list()里实现:把正文域与"每个注释在其首次引用位置的域"合并后按位置排序。这也解释了一条健壮性设计——被正文多次引用的注释只列一次(测试:5000 个引用、200 个域的注释仍只列出 200 个域,避免恶意文件导致百万级列表)。

列表还有两道预算:MAX_FIELDS = 100_000与MAX_CODE_BYTES = 64 MiB,超出部分会被丢弃并记录一条警告,而不是让 Zotero 卡死。

5.2 文档偏好存在自定义属性里

Document Preferences的内容不是普通正文,而是写入自定义属性ZOTERO_PREF_1…n:

  • 每块最多PREF_CHUNK = 255字符(Word 自定义属性的上限),按字符而非字节切分,所以é这类字符不会把一块撑爆;
  • 读取时从 1 开始连续读到第一个缺失块为止,最多读MAX_PREF_CHUNKS = 10_000块;
  • 写入前会先删掉所有旧的ZOTERO_PREF_*,其它自定义属性不受影响。

最精巧的一处是字段类型的"翻译":Zotero 在 LibreOffice 协议里把该字段类型称为Reference Mark,而文件里必须保留 Word 的Field。于是get_prefs()把name="fieldType" value="Field"换成ReferenceMark再给 Zotero 看,set_prefs()反向换回来再落盘(crates/zotero/src/fields.rs)。测试同时断言了"给 Zotero 的是ReferenceMark"和"文件里是Field"。

5.3 富文本:只保留引文样式需要的那几种字符格式

Zotero 写回的引文/书目可能是 RTF(以{\rtf开头)或 HTML(含<)。crates/zotero/src/rich.rs 的fragment()会分别走wordcraft_formats::rtf::import或wordcraft_formats::html::import,然后只叠加属于引文样式的属性:粗体、斜体、下划线、删除线、小型大写、全大写、上下标、链接;其余一律以字段所在位置的格式为基底。RTF 解析失败会降级为纯文本插入并记警告,而不是让会话失败。

规模保护同样存在:最多MAX_PARAS = 100_000段、MAX_RUNS = 1_000_000个格式 run;超过后文本沿用其前面的格式,保证处理时间与文档规模线性相关而非平方级。

六、线协议细节:一帧、一次会话

crates/zotero/src/wire.rs 是整条链路的"物理层",全部内容不到百行却定义了所有边界:

  • 帧格式:4 字节大端 txid + 4 字节大端长度 + 载荷。载荷是 UTF-8 JSON;失败时载荷以ERR:开头,原文为错误消息。
  • 常量:PORT = 23116、MAX_FRAME = 32 MiB(长书目的富文本最多也就几 MB)、TEMPLATE_VERSION = 1。
  • 命令载荷:{"command": "refresh", "templateVersion": 1},txid 固定 0,没有回包。
  • 调用解析:Call接受["method", [args…]]也接受扁平的["method", arg, …];不是 JSON 数组、首元素不是字符串都会报错。
  • 错误处理:帧头读到一半断开是协议错误;长度超过上限直接拒绝,不会预分配 4GB 内存。

客户端的时序(crates/zotero/src/client.rs)值得逐条记下:

  1. check_addr先判断地址是否回环——在连接之前就拒绝其它地址;
  2. 用connect_timeout(默认 3 秒)连上,失败即NotRunning;
  3. 连上后立刻peer::check(stream)做对端身份校验(详见第八节);
  4. 设置TCP_NODELAY,并把读超时设为idle_timeout(默认1 小时)——因为 Zotero 的对话框可能等用户很久,这个超时只用于结束"Zotero 没关连接却再也不说话"的会话;
  5. 发送命令帧,然后循环读取调用,逐条回答,直到Document_complete或对端关闭连接。

serve()的回包规则是严格的:txid 为 0 的帧不回;每个非零 txid 的调用都回一帧同样的 txid;回答出错时错误会同时计入Outcome.errors并已ERR:形式返回给 Zotero(Zotero 会显示并取消该次操作)。

七、Bridge:把 Zotero 的调用翻译成文档编辑

crates/zotero/src/bridge.rs 是整个集成中体量最大、也最值得读的一层。它实现了Hosttrait(alert/activate)以便 UI 或无头环境介入,核心是dispatch()里那串方法分派。

7.1 协议版本与文档 id

会话以Application_getActiveDocument开始。Zotero 传入它使用的协议版本,WordCraft 把双方版本夹到[1, PROTOCOL_VERSION=3]后返回[version, doc_id];同时重置本次事务的全部状态(checkpointed、caret_after、start_sel、completed),并记下事务开始时的选区。

7.2 字段 id 与文档顺序的平行列表

Zotero 用整数 id 指代字段,WordCraft 里并没有这种 id。做法是维护一个与"Zotero 字段文档顺序列表"平行的Vec<u64>:

  • fresh_id()递增发号;
  • Document_insertField后调用adopt(),用"新字段起始位置"在列表里定位并插入 id;
  • Field_delete/Field_removeCode成功后forget(k)移除对应槽位;
  • 如果发现 id 列表与当前字段数量不一致(说明文档在这个 bridge 之外被改过),就整体重新发号。

这套机制让Field_setText之类的后续调用在插入、删除交错发生时仍能命中正确字段。

7.3 已实现的方法一览

调用行为要点
Document_displayAlert交给Host::alert,返回按钮数值
Document_activate让文档窗口获得焦点
Document_canInsertField只有正文、脚注、尾注可以插引文(页眉里返回 false)
Document_getDocumentData/setDocumentData读写ZOTERO_PREF_*,内容未变则不产生编辑/撤销点
Document_cursorInField取"包含光标的最内层字段"(最后一个起点早于光标的字段),返回[id, code, noteIndex]
Document_insertField插入域标记并在中间放占位文本{Citation},返回 id
Document_insertText以富文本方式插入,先删除选区
Document_getFields返回[ids, codes, notes]三个平行数组
Document_convertPlaceholdersToFields把带占位链接的文字提升为真正的 Zotero 域
Document_setBibliographyStyle以 twip 为单位设置书目段落样式
Document_convert/Field_convert引文在正文、脚注、尾注之间迁移
Field_setCode/Field_getText/Field_setText域代码与域结果读写
Field_select/Field_delete/Field_removeCode选中、删除域、剥离代码保留文字
Document_complete收尾:恢复/移动光标、标记完成
Document_importDocument/exportDocument明确拒绝,返回"不支持文档传输"

未知方法返回WordCraft doesn't support <method>;Bridge::handle外面还套了catch_unwind,即使分派出 panic 也只变成一句internal error回包,并且会clamp_selection()保证文档状态可继续使用。

7.4 占位文本{Citation}的用意

新字段先写{Citation},而不是空内容。原因写在常量注释里:Zotero 会先回读新字段再填充,而"空书目"会被它当成"用户删掉了书目"从而触发 unlink。占位符同时也通过PLACEHOLDER_URL = "https://www.zotero.org/?"关联链接被convertPlaceholdersToFields找回——find_link会在正文与所有部件里搜索该链接的 run 区间,允许&…后缀。

7.5 书目样式:twip 到点、240 到单倍行距

Document_setBibliographyStyle(doc, firstLineIndent, bodyIndent, lineSpacing, entrySpacing, tabStops, count)全部以 twip 传入。转换规则:

  • twips(v) = clamp(v, -31680, 31680) / 20.0得到点值,避免恶意大数炸掉布局;
  • 行距 240 表示单倍:Multiple(line/240)夹到0.5..5.0,line <= 0时按 1.0;
  • 制表位最多取 64 个,位置取非负;
  • 样式名固定为Bibliography(与 Word 的 Zotero 插件同名),不存在则基于Normal创建,next指向自身。

Field_setText若发现域代码以BIBL开头,会把结果覆盖到的所有段落样式都设为Bibliography——这就是书目外观能跟样式定义联动的原因。测试验证了多段书目、斜体书名、以及indent_left/first/space_after = 36/-36/12的具体数值。

7.6 脚注式与文内式之间的迁移

Document Preferences里切换文内样式与注释样式时,引文要搬家。convert_field()的算法是:

  1. 计算当前所在部件类型与目标类型,相同则直接返回(幂等);
  2. 复制整个域的片段,从原位置删除;
  3. 目标位置:原本在正文就用原位;原本在注释里,若该注释只剩这个引文则连同引用标记一起删掉注释,否则定位到注释引用标记之后(OBJ之后);
  4. 目标是注释时,先执行references.footnote或references.endnote创建注释,再把片段插进新注释;
  5. 失败则把片段插回原处并恢复选区,避免文档处于半迁移状态。

remove_empty_note()判定"空注释"的条件很严格:除了文本空白与标记对象外,任何图片、图形、公式都算内容,注释会保留。

Document_convert批量版本还有两处优化:同一 id 出现多次时以最后一次的 note 类型为准;先用一次索引校验所有 id 存在,之后只有"确实需要移动"的字段才重建列表,避免每次移动都线性重扫。

八、安全边界:谁能驱动你的文档

这是文档里最需要注意的一节,因为 Zotero 的协议没有任何可以证明对端就是 Zotero 的握手,而那条连接的另一端可以读你的引文、改你的文档。WordCraft 用两道检查来收窄这个面(crates/zotero/src/peer.rs):

8.1 所有系统:只连本机

check_addr()要求对端 IP 是回环地址(IPv4-mapped IPv6 会先to_canonical()再判断)。192.0.2.1、0.0.0.0、[::]、公网 IPv6 全部在连接之前被拒绝,并有单测覆盖。

8.2 仅 Linux:监听者必须与我是同一个用户

在 Linux(含 Android)上,WordCraft 会读取内核 socket 表/proc/net/tcp与/proc/net/tcp6,找出这条连接两行(本地一行、对端一行)的 uid,然后判定:

  • 两行 uid 相同 → 放行;
  • uid 不同 →Refused,错误是"the program on port 23116 runs as another user (uid …), so it isn't your Zotero";
  • 自己的 uid 优先取"本进程 socket 那一行",取不到时退回/proc/self的属主 uid;
  • 监听方不在表里(WSL2 镜像网络下 Zotero 跑在 Windows 宿主、或本地代理)且表是完整读过的 → 归属未知,只按回环规则放行,并记一条 debug 日志。

有几处实现细节体现了"宁可拒绝"的取向:

  • 表打不开且错误不是NotFound(例如 AppArmor/Snap 沙箱拒绝)→拒绝;
  • 读到一半 I/O 出错 →拒绝,绝不会把"没读完"当成"监听者不在表里";
  • 单行长度上限 4 KiB(防超长行粘住下一行导致漏判);
  • 扫描是流式的,不把整表读进内存,因此恶意用户灌入 70 MiB 假行也照样能找到真正的两行(测试里特意构造了超过旧 64 MiB 上限的表);
  • 被信号中断的读会重试,不算"表不可读"。

8.3 Windows、macOS 与 BSD:只有回环规则

这些系统上没有"不做 OS 调用就能读"的 socket 表(本 crate 不使用unsafe代码),所以没有同用户校验,只剩回环规则。残留风险写得很清楚:在与他人共享的机器上,别的账号在 Zotero 未运行时占住 23116 端口,就会被认为是 Zotero。针对这些系统的属主校验记录在 issue #3831。无论在哪个系统,"以你的身份运行的程序"都是被信任的——它本来就能改你的文档。

8.4 CLI 输出转义

第八节最后一条常被忽略:wordcraft-cli zotero打印 Zotero 的消息与--trace行时,会把控制字符、换行、反斜杠、行分隔符与 bidi 控制符全部转义显示,防止对端伪造输出或操纵终端显示。见 apps/wordcraft-cli/src/main.rs。

九、不依赖 Zotero 的两个引擎命令

有两条能力直接读文档里的域,Zotero 不运行也能用:

9.1caret.pastCitation(Zotero › Move Past Citation)

把光标移到"它所在或紧前的引文"之后;相邻引文算作一个整体;若段落里没有,则跳到文档中下一个引文。实现见 crates/engine/src/cmd/speech.rs:

  • 收集field_ranges中的引文域,以及段落内InlineObject::Field形态的引文;
  • 找"结束位置在光标之后"的第一个 span;
  • 然后向后吞并仅由空白、;、,分隔的相邻引文,所以(A)(B)与(A) (B)都一次跳过;
  • 没有任何引文时报错there's no citation after the caret。

9.2 Read Aloud(review.readAloud,Review › Speech,Mod+Alt+Space)

朗读默认跳过引文与书目。识别函数is_citation_code()(crates/engine/src/speech.rs)覆盖:

  • Word 自有的CITATION与BIBLIOGRAPHY;
  • ADDIN开头的ZOTERO_ITEM、ZOTERO_BIBL、CSL_CITATION、CSL_BIBLIOGRAPHY、MENDELEY、EN.CITE、EN.REFLIST。

也就是说 Zotero、Mendeley、EndNote 与 Word 内建引文一视同仁。因为引文文本根本不进朗读队列,所以(Smith et al., 2020)、[3]、上标数字在任何引文样式下都被同样跳过。

播放器命令与参数:

命令参数说明
review.readAloud{"skipCitations"?: bool}从光标或选区开始朗读;已在读则暂停/继续;读到文末会从头再读
readAloud.playPause/.next/.previous/.stop—播放控制
readAloud.speed{"value": number}0.5–3 倍,越界夹紧,非有限值回落到 1.0
readAloud.skipCitations{"value"?: bool}不带值即取反;切换后从当前句重排队列,暂停状态会保持
readAloud.status—state、open、index、count、rate、skipCitations、current、error

朗读本身由系统语音程序完成(macOSsay、Linux/BSDespeak/spd-say、Windows PowerShell 合成器),并会按 run 的语言标签或文字脚本选择语音。设置环境变量WORDCRAFT_SPEECH=off可让它彻底静默(测试场景也用它)。单句上限 600 字符,超长句在空格处切分,无空格时最多切到 1200 字符;队列上限 20 000 句。浏览器构建下 Read Aloud 不可用,会返回明确的错误信息。

十、已知限制

限制说明
Bookmarks 字段类型不支持,请保持 Zotero 的默认设置
Zotero 的文档传输(Export/Import Document)不需要也不支持——引文已按 Word 的方式存好;桥接层对这两个方法直接返回错误
Web 版浏览器打不开本地 socket,无法连接 Zotero;ui.zotero.*在wasm32下禁用
页眉/页脚不能插入引文(Document_canInsertField返回 false)
同用户校验仅 Linux 有;Windows/macOS/BSD 只靠回环规则

十一、排错速查与源码地图

现象大概率原因去哪里看
Zotero isn't running (start Zotero and try again): …Zotero 没开,或集成被关;端口没人监听client.rs的NotRunning
Zotero is still working on …上一个会话没结束(Zotero 窗口还开着对话框)ui.zotero.status的busy
waitingForAnswer长期有值Zotero 在等一个弹窗答复用ui.zotero.answer回答
WordCraft won't let this program edit the document: …地址非回环,或 Linux 上监听者是别的 uidpeer.rs的Refused
couldn't read the system's socket table …沙箱拒绝/proc/net/tcppeer.rs的unreadable
a citation can't go here光标在页眉等不允许的位置parse.rs之外的can_insert_at
no field with id N文档在会话中被外部改动,id 列表重排Bridge::field
书目被当成"已删除"并 unlink新字段写成了空内容(应保留{Citation}占位)bridge.rs的PLACEHOLDER

按图索骥的阅读顺序建议:

  1. crates/zotero/src/wire.rs —— 先弄清帧与调用形状;
  2. crates/zotero/src/client.rs —— 会话时序、超时、Outcome;
  3. crates/zotero/src/peer.rs —— 安全判定与其单测(含 70 MiB 大表、沙箱拒绝、双栈映射地址等用例);
  4. crates/zotero/src/fields.rs —— 字段与偏好的存储格式;
  5. crates/zotero/src/bridge.rs —— 方法分派与各种文档操作;
  6. crates/zotero/src/rich.rs —— RTF/HTML 的受限转换;
  7. crates/zotero/src/tests.rs 与 crates/zotero/src/tests/limits.rs —— 端到端会话、恶意调用不 panic、性能上界;
  8. crates/ui-egui/src/zotero.rs —— 后台线程与 UI 线程的交接、弹窗、文档替换保护。

其中hostile_calls_never_panic这个测试值得一提:它把 10 种垃圾参数(null、-1、1e300、[]、{}、u64::MAX…)与 21 个方法、0–7 个参数的所有组合都打一遍,最后还要求文档能被wordcraft_docx::write正常序列化。这解释了为什么arg_i64会夹紧浮点、arg_str接受任意 JSON 并转字符串、arg_bool对未知类型返回false——所有入口都按"对端可能不友好"来设计。

结语

WordCraft 的 Zotero 集成之所以能做到"Zotero 端零安装、文档与 Word 互通",靠的是三件明确的事:用 Word 的字段与自定义属性作为唯一的落盘格式、用 Zotero 公开的 23116 线协议作为唯一的会话入口、用回环 + 同类用户检查把"谁能驱动文档"压到最小。当你想扩展它时,先判断需求属于哪一类:新的文档操作要落在Bridge::dispatch的调用表里;新的语义(比如跳过更多引文来源)要落在is_citation_code;而任何"放宽对端校验"的改动,都应该先读一遍 crates/zotero/src/peer.rs 顶部的模块注释,再对照 docs/zotero.md 里那段风险说明。

【免费下载链接】wordcraft

An open-source, clean-room reimplementation of Microsoft Word in pure Rust

项目地址:https://gitcode.com/gh_mirrors/word/wordcraft
点击查看免费下载
上一篇:Dio大文件上传进度:精确到字节的监控
下一篇:SeedVR2终极指南:3步实现AI视频高清修复,告别模糊画质

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考