把 Jupyter Notebook 搬到鸿蒙 PC:同栈双胞胎的 Notebook-first 移植实战

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_jupyterNotebook

写在前面

这是最有意思的一篇——因为它不是从零开始移植一个应用,而是从已经跑通的姊妹工程里分叉出另一个产品形态。

Jupyter 生态里有个特殊现象:Notebook 7 的前端就是 JupyterLab 组件构建的。上游 jupyter/notebook 从 7.0 开始放弃了老 nbclassic 的 jQuery 代码库,改为复用 Lab 的 React/Phosphor 前端。这意味着:如果你已经把 JupyterLab 移植到了鸿蒙 PC,你就已经顺手完成了 Jupyter Notebook 90% 的移植工作——剩下的是产品身份分叉和 UI 形态收敛。
在这里插入图片描述

任务清单要求两个产品分列交付、独立安装、独立评分,所以这次的核心问题不是「能不能跑」(Lab 已经验证),而是「怎么把一套已验证的技术栈,切成两个能共存、可独立演进的交付物」。

题图:Notebook 真机运行——启动直接进入 Untitled2.ipynb(single-document 模式),Python 3 (Skulpt) 内核就绪,cell 编辑/执行/底部状态栏完整
在这里插入图片描述


一、为什么 Notebook 7 是「最容易的硬骨头」

先看清楚上游关系:

jupyter/notebook (Notebook 7)
        │  前端复用
        ▼
jupyterlab/jupyterlab (Lab 组件库)
        │  OHOS 适配已验证(姊妹工程 ohos_JupyterLab)
        ▼
OpenHarmony Electron (libelectron.so) + node-static + 浏览器内 Python

Notebook 7 = Lab 前端 + single-document 外壳。上游用 pageConfig 控制产品形态:

配置JupyterLab 产品Jupyter Notebook 产品
pageConfig.modemultiple-documentsingle-document
启动 URL/lab(Launcher 多文档入口)/lab/tree/Untitled.ipynb(直接打开笔记本)
Launcher 卡片页禁用
左侧文件浏览器禁用
TOC 侧栏禁用
bundleNameorg.jupyter.lab.ohosorg.jupyter.notebook.ohos
环境变量前缀JUPYTERLAB_*NOTEBOOK_*(兼容 JUPYTERLAB_*

所以移植策略非常清晰:fork 姊妹工程 → 改产品身份配置 → 保持技术栈完全复用


二、从 Lab 分叉:bootstrap 脚本做了什么

两个仓库之间用一个 bootstrap 脚本同步 runtime(约 2000 行 Electron 主进程代码不重复维护):

node scripts/bootstrap-from-notebook.mjs        # Notebook → 姊妹方向
node scripts/bootstrap-from-lab.mjs             # 反向(Lab 仓库持有源)

脚本做的字符串/配置级重写:

org.jupyter.lab.ohos        →  org.jupyter.notebook.ohos
productUi: 'lab'            →  productUi: 'notebook'
/lab(Launcher 入口)        →  /lab/tree/Untitled.ipynb
JUPYTERLAB_*(优先)          →  NOTEBOOK_*(优先,JUPYTERLAB_* 兼容)
hnpPackages type: private   →  type: public     ← 关键差异,见 §四

工程判断:这种「同栈双产品」的架构下,bootstrap 脚本就是唯一的真相源。所有产品身份相关的东西都集中在这一个脚本里,避免两个仓库手工 drift。


在这里插入图片描述

三、架构:和 Lab 完全同栈,只有入口不同

在这里插入图片描述

三种服务模式与 Lab 完全一致:

模式环境变量场景
node-static(默认)NOTEBOOK_OHOS_SERVER_MODE=node-static静态前端 + 浏览器内内核,零原生依赖
pythonNOTEBOOK_OHOS_FORCE_PYTHON=1设备上跑 python -m jupyterlab(需 aarch64 发行版)
externalNOTEBOOK_EXTERNAL_SERVER_URL=…连接已有 Jupyter Server

为什么默认 node-static 而不是 python 模式?pyzmq/libsodium 在设备侧仍然脆弱(官方文档原话),交叉编译一个能跑 Jupyter Server 的 Python 环境的工程成本,远超「静态前端 + 浏览器内解释器」的方案。这是整个 Jupyter 双胞胎移植里最重要的一个工程决策。


四、HNP 双胞胎共存机制(本次最大坑)

两个产品都打包了一个 HNP 原生包(jupyterlab_python.hnp,用于可选的原生 Python 能力)。第一次装 Notebook 时直接翻车:

Install Failed: code:9568407
Failed to install the HAP because installing the native package failed.

hilog 关键线索:

ProcessBundleInstallNative … hnp install: electron
[HNP API] native package install! … package name=org.jupyter.notebook.ohos
already exist cfg ignore
… MSG_ERR_NATIVE_INSTALL_FAILED

根因:HNP 包有 public / private 两种作用域:

类型作用域冲突规则
public设备全局唯一同名 public 包只能装一个
private单应用沙箱每个应用各持一份,互不干扰

两个产品声明了同名 jupyterlab_python.hnp,如果都是 public,先装的占坑,后装的报 9568407。

修复方案(不对称设计):

// Notebook(先发布,占 public 坑)
"hnpPackages": [
  { "package": "jupyterlab_python.hnp", "type": "public" }
]

// Lab(后发布,用 private 共存)
"hnpPackages": [
  { "package": "jupyterlab_python.hnp", "type": "private" }
]

改完必须重新 assembleHap——模块元数据是烘焙进 HAP 的,热改无效。

方法论:任何「同栈双胞胎」交付,都要在 HNP 这一层显式设计共存策略,且不对称(一个 public 一个 private)是最稳的——对称 public 会互相踢,对称 private 浪费设备空间且语义不明。


五、真机验收:六个功能点逐一过

设备:HUAWEI MateBook Pro,HarmonyOS 7.0.0。

5.1 启动直达笔记本(single-document 的核心体验)

启动后直接进入 Untitled2.ipynb——没有 Launcher 卡片页、没有左侧文件浏览器,[3] 号 cell 已执行 print("Hello HarmonyOS PC"),底部状态栏 Python 3 (Skulpt) | Idle | Mode: Command | Cell 2/3 | Ln 1, Col 1

这就是 Notebook-first 与 Lab-first 的用户可感知差异:打开就是笔记本,光标在 cell 里,可以直接打代码。Click to add a cell. 提示、Mode: Command/Edit 状态切换、Ln x, Col y 行列号全部工作。

5.2 Cell 执行与返回值显示

cell [3] 执行 print("Hello HarmonyOS PC") 后输出 Hello HarmonyOS PC;下方 [3]: None 是 print 函数的返回值——Jupyter 标准行为

注意 [3]: None 这一行——这是 Jupyter 对表达式返回值的自动回显print() 返回 None)。这个细节存在,说明 Skulpt 内核的执行协议和前端的消息通道是完整实现的,不是简单地把 stdout 打到页面上。

5.3 菜单完整度

File 菜单完整展开:New / Open Recent / Close Tab (Alt+W) / Close and Shut Down (Ctrl+Shift+Q) / Save (Ctrl+S) / Save As / Reload / Revert to Checkpoint / Rename / Download / Workspaces / Save and Export Notebook As / Print;右侧 New 二级菜单含 Notebook / Text File / Markdown File / Python File

20+ 菜单项、快捷键提示、二级菜单全部渲染正确。右下角还能看到系统任务栏(BOSS 直聘、邮件、抖音)——这是真机全屏截图,不是模拟器。

5.4 键盘快捷键帮助

Help → Keyboard Shortcuts 弹窗:完整快捷键列表覆盖在 notebook 上

弹窗、滚动、关闭交互全部正常——Lab 前端的 Modal 组件在 OHOS Electron 上没有降级。

5.5 ASCII 艺术字输出(多行字符串渲染)

连续多个 print() 输出 box-drawing 艺术字 banner(HARMONYOS PC / ◆ AI ◆ CLOUD ◆ PC / >>> SYSTEM ONLINE <<<),多行字符串、Unicode 字符渲染无乱码

值得记录的一个细节:这些 box 字符(╔═╗║╚╝)在 r"""...""" raw 三引号字符串里会触发 Skulpt 词法器 bugSyntaxError: bad input),改用普通 """...""" 三引号或分行 print() 就完全正常。截图里的输出就是修复后的结果——这是 Skulpt 内核的边界,不是平台问题

5.6 无 matplotlib 环境下的数据可视化(本文最有价值的一节)

鸿蒙 PC 的 Jupyter 双胞胎默认内核是 Skulpt,没有 matplotlib / numpy。但数据可视化需求是真实的——教学场景画个函数曲线、演示趋势,不能没有。解法是纯标准库 ASCII 折线图

纯标准库 ASCII 折线图在 notebook 里完美渲染:sin 波 + 缓慢上升趋势,y 轴 │、x 轴 ─、数据点 ●、插值连线 ·、峰值 ▼、谷值 ▲,y_max=4.97 / y_min=-1.79 / x: 0.0 → 9.2

完整代码(约 40 行,纯 math + 列表推导,Skulpt 100% 兼容):

import math

# 1. 数据
xs = [i * 0.4 for i in range(24)]
ys = [math.sin(x) * 3 + x * 0.25 for x in xs]

# 2. 画布
W, H = 64, 18
min_y, max_y = min(ys), max(ys)
min_x, max_x = min(xs), max(xs)

def to_col(x):
    return int((x - min_x) / (max_x - min_x) * (W - 2)) + 1

def to_row(y):
    return int((y - min_y) / (max_y - min_y) * (H - 2))

# 3. 画布初始化 + 坐标轴
canvas = [[' ' for _ in range(W)] for _ in range(H)]
for r in range(H):
    canvas[r][0] = '│'
for c in range(W):
    canvas[H - 1][c] = '─'
canvas[H - 1][0] = '└'

# 4. 描点 + 插值连线
for i in range(len(xs)):
    col = to_col(xs[i])
    row = H - 1 - to_row(ys[i])
    canvas[row][col] = '●'
    if i > 0:
        prev_col = to_col(xs[i - 1])
        span = col - prev_col
        if span > 1:
            for c in range(prev_col + 1, col):
                t = (c - prev_col) * 1.0 / span
                y = ys[i - 1] + (ys[i] - ys[i - 1]) * t
                canvas[H - 1 - to_row(y)][c] = '·'

# 5. 峰谷标注
peak_i = ys.index(max(ys))
low_i = ys.index(min(ys))
canvas[H - 1 - to_row(ys[peak_i])][to_col(xs[peak_i])] = '▲'
canvas[H - 1 - to_row(ys[low_i])][to_col(xs[low_i])] = '▼'

# 6. 输出
print("y_max = {:.2f}".format(max_y))
for row in canvas:
    print(''.join(row))
print("y_min = {:.2f}   x: {:.1f} -> {:.1f}".format(min_y, min_x, max_x))

几个工程要点:

  • 刻意不用 f-string(部分 Skulpt 版本支持不稳),全部 .format()
  • 刻意不用 raw stringr""" 是词法坑),box 字符只出现在普通短字符串里
  • 插值连线拆成 span/t/y 三个中间变量,避免长链表达式触发解析器边界
  • [i]: None 返回值回显、错误行号定位(bad input on line N)都正常工作

这套「降级可视化」思路适用于任何没有图形栈的嵌入式 Python 环境——Skulpt、MicroPython、受限容器都是同一个问题域。


六、双胞胎工程的坑位对照

这次 Notebook 分叉把 Lab 踩过的坑原样再踩一遍,但每个坑的解法都能直接复用,成本大幅下降:

Lab 首次解决成本Notebook 复用成本
CompileArkTS 10705000(import lazy排查半天,定位到 compatibleSdkVersionStage: beta10(bootstrap 直接钉住 6.0.1(21)
SignHap 00303074(profile 绑 bundle)摸清 debug profile 的 bundle 绑定机制5 分钟(DevEco Fix 一次)
Install 9568320(signingConfig: ""半小时(发现 DevEco 不回填 products 引用)5 分钟(知道看产物文件名)
Install 9568407(HNP 冲突)理解 public/private 作用域0(bootstrap 写好 public
resfile 放错模块白屏半天0(脚本生成正确路径)

这就是「同栈双胞胎」模式的最大红利:第一个产品的踩坑成本是沉没成本,第二个产品的边际成本趋近于零。反过来,如果两个产品各自独立移植,这些坑要踩两遍。

七、复现命令

cd ohos_JupyterNotebook

# 1. 从 Lab 工程同步 runtime(首次必做)
node scripts/bootstrap-from-lab.mjs   # 或按仓库实际脚本名

# 2. 打包 runtime 到 web_engine resfile
node pkg/ohos/build-package.mjs

# 3. 构建 HAP
node pkg/ohos/build-hap.mjs
# 跳过 hvigor 仅打包资源:
# NOTEBOOK_OHOS_SKIP_HVIGOR=1 node pkg/ohos/build-hap.mjs

# 4. DevEco 自动签名 → 检查 products[].signingConfig = "default"

# 5. 安装 + 启动
hdc install -r electron/build/default/outputs/default/electron-default-signed.hap
hdc shell aa start -a EntryAbility -b org.jupyter.notebook.ohos

环境变量速查:NOTEBOOK_PORT=8888NOTEBOOK_TOKEN(默认随机)、NOTEBOOK_ROOT_DIR=<userData>/notebooksNOTEBOOK_DISABLE_GPU=1


八、给「同栈多产品」交付的方法论

这套模式适用于所有「一个技术栈要交付多个产品形态」的场景(IDE 的社区版/专业版、浏览器的稳定版/测试版、办公套件的多个组件):

  1. 先跑通一个,再分叉第二个——第一个产品的踩坑日记就是第二个产品的施工图
  2. bootstrap 脚本是唯一真相源——产品身份的所有差异集中在一个可 review 的 diff 里,禁止手工双仓库漂移
  3. HNP/权限/签名按 bundle 隔离——debug profile 绑 bundleName,HNP 设计不对称共存(public/private),一个都别复用
  4. UI 形态差异交给上游配置——pageConfig.mode 这种官方开关比魔改前端代码稳定一个数量级
  5. 降级路径先于完整路径——node-static + 浏览器内内核先落地,原生 Server 后补;能跑的 60 分比跑不起来的 100 分有价值
  6. 受限环境的可视化用 ASCII——没有图形栈时,40 行纯标准库代码就能覆盖教学场景 80% 的画图需求

回头看这个项目最大的价值不是「又移植了一个应用」,而是验证了同栈双胞胎的工程模式:Notebook 7 的分叉只花了 Lab 首次移植约 20% 的时间,而这 80% 的节省全部来自 Lab 留下的踩坑记录和 bootstrap 脚本。如果你手上也有类似「一个底座多个产品」的移植任务,强烈建议按这个模式组织仓库——第一个产品慢一点没关系,它是在为后面的所有产品铺路。


常见问题 FAQ

Q1:和 JupyterLab 版有什么区别?能同时装吗?

能共存。两者技术栈完全相同,差异只在产品形态:Notebook 打开就是单个笔记本(single-document,无 Launcher / 文件浏览器),Lab 是多文档 + Launcher。HNP 层做了不对称设计(Notebook 用 public、Lab 用 private),互不冲突。

Q2:为什么打开后不是经典的 /tree 老界面?

上游 Notebook 7 已经放弃老 nbclassic 的 jQuery 前端,改为复用 Lab 组件。本工程走的是官方路线——Lab 静态资源 + pageConfig.mode = single-document,视觉上是「单文档笔记本」,但底层不是老 /tree

Q3:能装 numpy / matplotlib 吗?

默认不能。内核是 Skulpt(浏览器内 Python 子集),没有 C 扩展生态。两个出路:切 NOTEBOOK_EXTERNAL_SERVER_URL 连远程 Jupyter Server 用真内核;或者用纯标准库 ASCII 图表顶住教学场景(见 §5.6 的 40 行折线图代码)。

Q4:为什么 r"""...""" 三引号字符串报 SyntaxError: bad input

Skulpt 词法器对 raw 三引号 + box-drawing 多字节字符有边界 bug。去掉 r 前缀用普通 """...""",或把内容拆成多行 print() 就正常。真 Python 解释器跑同样代码没问题——这是内核限制,不是平台 bug。

Q5:签名成功了还是报 9568320 no signature file

看产物文件名。unsigned 说明 build-profile.json5products[].signingConfig 还是空串——DevEco 自动签名只写材料不回填这个引用。手动改成 "default" 重新构建。

Q6:装了 Lab 再装 Notebook,报 9568407 原生包失败?

HNP 同名冲突。两个产品的 hnpPackages 里声明了同名 jupyterlab_python.hnppublic 类型设备全局唯一,后装的会被拒。用新版 HAP(Notebook public / Lab private 的不对称组合)即可共存;不想重编就先卸载另一个再装。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐