【免费下载链接】wordcraft
An open-source, clean-room reimplementation of Microsoft Word in pure Rust
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.rs | wordcraft-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 在后台应答。这带来两条必须掌握的规则:
- 同一时刻只允许一个会话。再次启动会返回错误
Zotero is still working on addEditCitation; finish in Zotero's window first(crates/ui-egui/src/zotero.rs)。 - 提问必须由宿主回答。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)值得逐条记下:
check_addr先判断地址是否回环——在连接之前就拒绝其它地址;- 用
connect_timeout(默认 3 秒)连上,失败即NotRunning; - 连上后立刻
peer::check(stream)做对端身份校验(详见第八节); - 设置
TCP_NODELAY,并把读超时设为idle_timeout(默认1 小时)——因为 Zotero 的对话框可能等用户很久,这个超时只用于结束"Zotero 没关连接却再也不说话"的会话; - 发送命令帧,然后循环读取调用,逐条回答,直到
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()的算法是:
- 计算当前所在部件类型与目标类型,相同则直接返回(幂等);
- 复制整个域的片段,从原位置删除;
- 目标位置:原本在正文就用原位;原本在注释里,若该注释只剩这个引文则连同引用标记一起删掉注释,否则定位到注释引用标记之后(
OBJ之后); - 目标是注释时,先执行
references.footnote或references.endnote创建注释,再把片段插进新注释; - 失败则把片段插回原处并恢复选区,避免文档处于半迁移状态。
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 上监听者是别的 uid | peer.rs的Refused |
couldn't read the system's socket table … | 沙箱拒绝/proc/net/tcp | peer.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 |
按图索骥的阅读顺序建议:
- crates/zotero/src/wire.rs —— 先弄清帧与调用形状;
- crates/zotero/src/client.rs —— 会话时序、超时、
Outcome; - crates/zotero/src/peer.rs —— 安全判定与其单测(含 70 MiB 大表、沙箱拒绝、双栈映射地址等用例);
- crates/zotero/src/fields.rs —— 字段与偏好的存储格式;
- crates/zotero/src/bridge.rs —— 方法分派与各种文档操作;
- crates/zotero/src/rich.rs —— RTF/HTML 的受限转换;
- crates/zotero/src/tests.rs 与 crates/zotero/src/tests/limits.rs —— 端到端会话、恶意调用不 panic、性能上界;
- 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
相关推荐
如何使用Zotero-GPT集成Gemini API实现智能文献处理:完整指南
如何使用Zotero GPT集成Gemini API实现智能文献处理:完整指南 Zotero GPT是一款强大的开源工具,它将Zotero文献管理软件与GPT等
人工智能AI 应用大模型科研如何用Jasminum实现Zotero中文文献的智能管理
还在为手动整理中文文献而烦恼吗?Zotero中文文献管理一直是个痛点,特别是面对大量中文期刊和PDF文档时。Jasminum插件通过自动化文献整理技术,让这一切
桌面应用科研PinchImageView性能优化指南:避免OOM的7个关键技巧
PinchImageView性能优化指南:避免OOM的7个关键技巧 PinchImageView是一款体验极佳的Android图片手势控件,在处理图片展示时,性
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考