Air 系列应用交流/轻学术|AirScholar · 鸿蒙 PC 适配全记录

轻学术|AirScholar · 鸿蒙 PC 适配全记录

【Air 系列 · 鸿蒙 PC 适配全记录】41 / 56

■ 应用:轻学术(AirScholar)
■ 定位:Zotero 移植,学术文献管理与阅读
■ 上游:Zotero 9.0.6 | 路线:Gecko 平台层 | 版本:1.7.1
■ 开源: https://gitcode.com/Andy88/Harmony-zotero
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917613299725711700
■ 官网(免费下载): https://www.shunidc.cn/

————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/

PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:(AtomGit 仓库建立后回填)

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

▍一、为什么要适配轻学术

Zotero 是开源文献管理器的事实标准之一:文库条目管理、元数据抓取、PDF 阅读器、笔记编辑、分类标签与检索构成一条完整的学术工作流。对科研与学习场景,它的价值在于把"找文献、读文献、记笔记、写引用"收进一个可长期积累的个人知识库。这样一条工作流在鸿蒙 PC 上目前没有同量级的原生替代,移植上游是更现实的路径。

轻学术(工程代号 AirScholar)的移植方式比较特殊:Zotero 本身是 XULRunner 应用,它的 UI 与逻辑几乎全部位于 omni.ja(chrome/resource 包)中,Gecko 只需提供平台窗口、渲染与事件。因此只要 Gecko 已经能在鸿蒙上运行,把 Zotero 的 chrome 换进 Firefox 的 omni.ja、把上游删掉的 API 逐个补齐,就能得到由 Gecko 自绘的完整 Zotero 界面。这个工程选择了零源码移植:完全不改动 Gecko 源码,平台层整体复用 AirWeb 项目(Firefox 142.0a1 鸿蒙移植)的 22 个 .so 与 pristine omni.ja。

当前工程包名为 com.airscholar.app,应用名"轻学术",app.json5 版本 1.7.1(versionCode 1700001),module.json5 声明 deviceTypes 为 phone、tablet、2in1,并声明 compressNativeLibs:false(含 .permission 段与 64K 对齐 PT_LOAD 的 libxul.so 需 mmap 直读,压缩会破坏对齐)。上游为 Zotero 9.0.6(zotero-client git HEAD 7132587c),随 HAP 分发 145MB 手术产物 omni.ja;权限面包含 ALLOW_WRITABLE_CODE_MEMORY(SpiderMonkey JIT 写代码段所需)、INTERNET 与 READ_PASTEBOARD,并注册 http/https 与任意文件的打开、分享接收意图。

▍二、先划清边界:不把编译通过当成适配完成

Zotero 桌面版的"应用"其实分两层:XULRunner 自带的 Gecko 运行时,以及 zotero.jar 贡献的全部 chrome。移植的边界因此不在"能否启动",而在三层问题的逐一回答:Gecko 平台层是否在鸿蒙上稳定(由 AirWeb 解决并冻结为只读上游)、chrome 注入后是否保持上游语义(手术脚本逐锚点核验)、删掉的宿主 API 是否都有等价 stub(漏洞会以静默空白或自杀式退出呈现)。轻学术把这三层写成了文档与脚本,而不是口头承诺。

层次 传统 Zotero 桌面版 鸿蒙侧当前方案
应用入口 zotero.exe(XULRunner 启动器) Stage 模型 EntryAbility + ArkTS 壳
平台窗口/渲染 XULRunner 自带 Gecko(Windows 构建) 复用 AirWeb 的 Gecko 142 鸿蒙移植 22 个 .so,零源码改动
UI chrome zotero.jar 注入 XULRunner 的 omni.ja stage_zotero.sh 把 Zotero chrome 注入 Firefox pristine omni.ja
数据层 zotero.sqlite 同上游引擎读写(61 表 / schema 125 实证)
本地化与样式 chrome.manifest 全量注册 Fluent 直注 48 个 locale,补 os=OHOS manifest 行
插件与更新 完整 XPI 生态 + updater chrome 内插件页与 XPI 安装链路;UpdateUtils/AppConstants 手术处置

需要说明的收缩面:Zotero 官方打包链(fetch_xulrunner)以 Windows XULRunner 为目标,本工程以 reference/ 中逆向出的 129 项编号清单逐项对照重实现;Firefox 宿主自带的浏览器 chrome 被替换为 Zotero 语义,品牌层在 UI chrome 接管阶段逐项移除。

▍三、鸿蒙版的整体架构

ArkTS 壳继承 AirWeb 血统,采用双轨路由:native 轨道由 Gecko 自绘全部 chrome,ArkTS 只保留全屏 XComponent 薄壳加 IME 桥与启动遮罩;classic 轨道(ArkTS 自绘工具栏 + 内嵌 GeckoView)作为回退,由 EntryAbility 读取运行时模式选择。菜单动作经白名单命令通道下发,native 侧菜单浮层由 Gecko 的 native_overlay 通道渲染真实菜单项。

text
EntryAbility(ArkTS 壳)
├── NativeShellPage(native 轨道薄壳)
│ ├── XComponent(SURFACE,geckoxcomp)← Gecko WebRender 绘制 + C 原生触摸/鼠标回调
│ ├── ArkTS 浅色菜单栏(文件/编辑/查看/前往/工具/帮助 → ShellCommandChannel 白名单 verb)
│ ├── HiddenImeBridge(隐藏输入软键盘桥)/ NativeMenuOverlay(native_overlay 菜单浮层)
│ └── ShareBridge(碰一碰/系统分享收发,maxFileSupported 8)
├── BrowserPage(classic 轨道回退:ArkTS 自绘 chrome + FirefoxWebView)
└── FirefoxBridge / OHOSMenuBridge
│ surface + NAPI
▼
Gecko 142 平台层(libxul.so 179MB 等 22 个 .so,来自 AirWeb,零源码改动)
└── omni.ja(145MB,随 HAP 分发)
├── Firefox pristine 基座(md5 与 AirWeb 一致)
├── Zotero chrome/locale/resource/translators/styles 注入
└── zotero.sqlite 数据层(61 表 / schema 125)


仓库主要目录如下:

text
AirScholar/
├── AppScope/
├── entry/src/main/
│ ├── ets/
│ │ ├── pages/ # NativeShellPage(3420 行)/BrowserPage/MenuPanelPage
│ │ ├── components/ # Toolbar/TabBar/FindInPageBar/HiddenImeBridge/NativeMenuOverlay/ShareBridge
│ │ ├── native/ # FirefoxBridge / OHOSMenuBridge
│ │ ├── models/ managers/ # 壳层状态与主题、命令模型
│ │ └── entryability/
│ ├── cpp/ # CMakeLists + airprefetch.cpp
│ ├── module.json5 # JIT 权限、分享/打开意图、compressNativeLibs:false
│ └── resources/rawfile/omni.ja # 手术产物(145MB)
├── scripts/
│ ├── stage_zotero.sh # 唯一源真相:2875 行 / 47 章节幂等手术脚本
│ ├── build_phone_hap.sh / build_split_app.sh # 手机变体构建
├── reference/ # Zotero 官方 fetch_xulrunner 逆向:129 项编号清单 + 脚本副本
├── staging/ # pristine omni.ja 与 omni_work 解包树(9329 文件)
├── zotero-client/ # 上游 9.0.6 完整源码树(只读素材)
├── ai_log/ # 过程记录:M0 四轮、阶段总结、UI chrome 接管系列
└── AGENTS.md README.md


▍四、核心适配过程

### 1. omni.ja 手术:幂等脚本与 129 项逆向清单

scripts/stage_zotero.sh 是整个移植的唯一源真相:2875 行、47 个章节的幂等手术脚本,解包 Firefox 142 pristine omni.ja(与 AirWeb 产物 md5 逐字节一致),注入 Zotero 的 chrome、xpcom 子模块与 chrome.manifest 注册,再以 zip STORED 方式重打包并写入版本戳(防止设备缓存旧 chrome)。每个改动点带 MARKER 逐条自检,锚点精确匹配、失配记 SKIP 绝不模糊替换。工程方法来自 reference/modify_omni_checklist.md——对 Zotero 官方 fetch_xulrunner 脚本逆向整理出的 129 项编号清单,逐项确认合并语义与路径。

### 2. 上游 API 断层的 stub 与 polyfill

Firefox 142 相对 Zotero 依赖的时代删掉了一批 API,手术脚本内置补丁族逐个补齐:printUtils 占位、<search-textbox> widget polyfill、nsIUserIdleService 的 omni 层 stub 六处替换、单进程 HiddenBrowser、os=OHOS manifest 行、Fluent 直注 48 个 locale 文件、AppConstants 双保险与 greprefs 处置、浏览器 xhtml shim。这类补丁的共同特征是:缺失不会报移植层错误,而是在 Zotero 初始化链路深处以 TypeError 或空白呈现,必须按调用链逐层回溯。

### 3. JIT 可执行内存的权限双闸

SpiderMonkey 的 JIT 需要写代码段权限,鸿蒙侧由 ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY 控制。工程实证这个权限必须同时满足两处:.p7b Profile 的 acls 与 module.json5 的 requestPermissions 显式声明。module.json5 内的注释完整记载了 A/B 二分实验:同一 .so 同一 pristine omni,满足双闸的进程正常,只靠 Profile ACL 的进程在 XRE_main 早期 JS_NewContext 分配 JIT 可执行内存时失败,mozalloc_handle_oom(0) 触发 MOZ_CRASH。这条结论后来成为 v252 内核换血时权限段补齐的依据。

### 4. ArkTS 薄壳与命令桥

NativeShellPage.ets 吸取 AirWeb 教训:SURFACE 型 XComponent 不设 focusable 以免拦截触摸,事件由 C 原生回调处理,滚轮经 onAxisEvent 转发并动态换算坐标。v159 起壳层增加浅色顶级菜单栏(文件/编辑/查看/前往/工具/帮助),菜单项经 ShellCommandChannel 白名单通道下发,v164 修正为 openViewer verb 寻址(loadURL 只能命中离屏隐藏 browser);v232 起 Gecko 侧弹出菜单经 native_overlay 通道回传,由 NativeMenuOverlay 渲染浮层,OHOSMenuBridge 承接两侧协议。ShareBridge 打通碰一碰与系统分享的文件流接收。

### 5. 多形态推进与内核对齐

ai_log 记录了从 2in1 向手机端的推进链:v258 手机首测暴露 SharedMap 崩溃,v259 以 OHOS_SHM 换血完成三设备验证,v260 锁横屏并根治首启冻结,v262 以 JIT 分包变体通过 Pura X 首验(scripts/build_phone_hap.sh 与 build_split_app.sh 承接)。内核侧保持与 AirWeb 同步换血:每次拉新 .so 必须核对 CMakeLists.txt 的 GECKO_RUNTIME_LIBS 白名单,HAP 清单以 unzip -l 加 MD5 双验——libohinput.so 曾被 hvigor 静默丢弃,这类丢失不报错,只有清单核验能拦住。

▍五、适配中遇到的几个难点

### 难点一:被删 API 以"三栏空白"呈现

printUtils.js 与 <search-textbox> 在 Firefox 142 已被删除,而 zoteroPane 仍会急切加载并实例化它们,connectedCallback 崩溃中断 ZoteroPane.init,最终表现是三栏全空白,且无任何直接指向根因的日志。解法是在 omni 层补占位与 polyfill,让初始化链路走通后再逐项替换为有语义的实现。这类问题的排查成本远高于补丁本身,工程把每一处都沉淀进手术脚本的补丁族,重打包即可复现修复。

### 难点二:本地化与样式注册的时代断层

两处"黑屏与丢字"级现象同源于注册机制:其一,Fluent 本地化不走 chrome.manifest,L10nRegistry 只读 resource://app/localization/{locale}/,zotero.ftl 永远找不到,解法是直注 48 个 locale ftl 与 multilocale.txt;其二,OS_TARGET=OHOS 导致 chrome.manifest 的 os=Linux 行失配,zotero-platform 的 unix 样式表不加载,界面只剩黑底两道白线,解法是补 os=OHOS 行指向 unix/。两处都发生在"文件都在包里"的前提下,属于注册语义而非内容缺失。

### 难点三:idleService 缺失引发的自杀闭环

items.js 的 startEmptyTimer 在 nsIUserIdleService 未注册时抛 TypeError,异常落入 init 的 catch 分支触发 quitZotero()——主面板自我退出,进度条永驻。现象与根因相距两层间接,定位后以 omni 层 stub 六处替换收口。这个案例确立了本项目的排查原则:先确认崩溃路径上的每一步假设,再动手改包。

### 难点四:JIT 权限的声明位缺失

平台层 .permission ELF 段与 manifest 声明缺一不可,只满足其一的进程会在 JS 上下文创建时崩溃。工程用同库同包的 A/B 对照把唯一差异收敛到声明缺失,并把两处配置与实验结论一起写进 module.json5 注释与 ai_log,成为后续所有内核更换的固定检查项。v252 内核换血时该权限段补齐被再次实测确认。

### 难点五:mmap 硬约束与打包静默丢失

Gecko 要求 omni.ja 以 STORED(不压缩)方式打包,这是 mmap 直读的硬要求;libxul.so 的 .permission 段与 64K 对齐 PT_LOAD 同理,compressNativeLibs:false 因此成为 module.json5 中的固化配置并附注释。另一面是 hvigor 的静默丢库:.so 不进 HAP 不报任何错误,工程以"unzip -l 加 MD5 双验"作为每次构建的收尾动作,stage_zotero.sh 的版本戳则保证设备不会因缓存跑旧 chrome。

▍六、编译、安装与启动

构建链分两段:先运行手术脚本生成 omni.ja,再构建 HAP。AGENTS.md 红线明确:手术产物就位后执行 devecocli build,必须看到 BUILD SUCCESSFUL——ArkTS 编译失败时旧 HAP 会被误装,构建结论以日志为准。

bash
bash scripts/stage_zotero.sh # 幂等手术:重复运行安全,失配项记 SKIP
devecocli build # 必须见 BUILD SUCCESSFUL


标准 hvigor 命令行流程:

bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

hvigorw --mode module -p module=entry assembleHap --no-daemon


产物位于 entry/build/default/outputs/default/entry-default-signed.hap,安装启动:

bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airscholar.app


手机变体构建对应 scripts/build_phone_hap.sh 与 build_split_app.sh。平台层更换内核时,须同步核对 GECKO_RUNTIME_LIBS 白名单并对 HAP 清单做 unzip -l 加 MD5 双验。

▍七、当前功能边界

当前版本已覆盖的能力(均有文档或代码依据):

- M0 主窗渲染:Zotero 三栏 UI 由 Gecko 自绘,在 2in1 与平板真机稳定运行;
- M1 数据层:zotero.sqlite 61 表、schema 125 的读写链路达成;
- UI chrome 接管推进至 v166 之后:菜单对齐、工具栏、插件页、阅读器返回导航、品牌移除;
- 插件与阅读器链路:XPI 插件安装修复(v233)、阅读器笔记编辑产物补齐(v236)、导入与引导遮罩(v237-v238);
- 交互收口:同步解锁与布局切换(v255)、标题返回按钮与切换残留(v256)、删除确认双向通道与 modal 边界化(v257);
- 多形态:手机端 OHOS_SHM 换血后三设备验证(v259)、锁横屏与首启冻结根治(v260)、冷启动恢复(v261)、JIT 分包手机变体过 Pura X(v262)、滚动条浅色化与笔记工具栏图标闪烁根治(v265-v266)。

尚未覆盖或仍挂账的能力:

- 免 JIT 运行路线仍处调研,被 SharedMap 崩溃阻断(v258 记载);
- v257 遗留盘点中的复核项(如 LaTeX 图标虚警)尚未全部销号;
- 平台层完全依赖 AirWeb 的只读上游,内核更换需双方联动(工程红线明确单向拷贝)。

当前版本更准确的定位是"Zotero 核心链路的零源码移植版":文库、条目、笔记与阅读器这条学术主流程已在鸿蒙真机上由 Gecko 自绘界面真实承载,宿主补齐与缺陷收口仍在按 ai_log 的版本序列推进。

▍八、总结

轻学术的适配把"零源码移植"走成了一套方法论:不动 Gecko 一行代码,全部适配以 omni.ja 手术脚本与 ArkTS 薄壳承载;上游官方打包链被逆向成 129 项清单,再固化为幂等脚本与 MARKER 自检。它证明当平台层(AirWeb 的 Gecko 142)已经就位时,另一个 XULRunner 世代的应用可以以可控成本跟进移植,且上游升级只需重跑手术。

后续工作的重心按文档脉络十分清晰:继续销号 ai_log 中的缺陷账与遗留盘点,推进免 JIT 路线的调研验证,保持与 AirWeb 内核换血的同步节奏。对于鸿蒙 PC 生态,这个项目还提供了一个中间形态的样本——在"整体交叉编译"与"ArkUI 重写"两个极端之间,chrome 手术是第三条值得纳入选型的路线。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。