Skip to content

空谷 Ondra v1.3.0-alpha(内测版)

Latest

Choose a tag to compare

@github-actions github-actions released this 14 Nov 13:19
· 1 commit to main since this release

下载 1.3.0-alpha

平台 安装包 大小
macOS(Apple Silicon) Ondra-1.3.0-alpha-macos-arm64.dmg 56.0 MB
Windows(x64) Ondra-1.3.0-alpha-windows-x64-setup.exe 33.2 MB
Android(arm64) Ondra-1.3.0-alpha-android-arm64.apk 14.8 MB

内测版:版本号末尾的 -alpha 就是标记。不是公测版,也不是正式版。

首次打开会被系统拦下

三个包都没有正式签名(macOS 是链接器 ad-hoc 签名,Windows 没有代码签名证书,Android 是 debug 签名)。

  • macOS:右键点应用 →「打开」;或执行
    xattr -dr com.apple.quarantine "/Applications/空谷 Ondra.app"
  • Windows:SmartScreen 提示时点「更多信息」→「仍要运行」
  • Android:系统会提示「未知来源」。允许浏览器/文件管理器安装即可;
    这个包只支持 arm64(近几年的手机都是),老设备装不上是正常的。

装完之后

桌面端首次启动会拉起本机的插件宿主与内核,自带 Node 运行时,不需要另装。
安卓端不跑 Node:记忆与情绪在 WebView 里用 IndexedDB 直接算,
推理直连你在设置里填的接口,所以 APK 只有几十 MB,也不需要额外依赖。
数据全部留在本机,无账号、无遥测。推理来源在设置里三选一:在线接口 / 本地大模型 / 免费模型。


1.3.0-alpha · 安卓端接入插件系统(2026-11-14)

这一版把 75 个真插件装进了安卓 WebView。此前手机端是「手工复刻」路线,这次彻底换掉了。

复刻路线为什么必须放弃

src/lib/local/ 里原来手工重写了情绪与记忆的纯函数(emotionPad.ts、memoryStability.ts,逐字从插件源码抄来),扩展抽屉里硬编码返回 totals: { all: 0 }——用户看到的是「0 个包」。

根本缺陷是第二份真源:能抄的只有无状态纯函数,记忆链、人设链、提示词装配那些有状态的插件抄不过来;而抄过来的部分一旦桌面端改了算法、手机端不跟,同一句话在两个平台会算出不同的情绪值。

把插件本体打包进前端

验证过 cordis 是纯 ESM、零 Node 依赖,在 WebView 里能直接创建 Context 并 apply 插件。剩下唯一的问题是 30 个插件 import 了 node:*。

src/lib/browser/ 下新增 9 个替身:

模块 实现
fs 内存镜像 + IndexedDB 异步落盘,同步 API 全部可用(插件的「写临时文件 → rename」原子写模式原样成立)
path / os / url / module POSIX 语义,homedir() 返回虚拟根 /
crypto 自带 SHA-256 与 MD5(约 250 行),randomUUID 走 WebCrypto
zlib 自带 raw-deflate 解码器(约 250 行),人设包解压依赖它
process 在 main.tsx 第一个 import 注入——ESM 求值顺序决定了写在模块体里已经太晚
sqlite 故意加载失败,让 ondra-store-sqlite 走它自己的降级链

vite.config.ts 用正则别名(不是字符串——字符串前缀匹配会把 node:fs/promises 吃成 node:fs)把 node:* 与 75 个插件包接到源码位置,只在 ONDRA_LOCAL=1 时生效。

三个关键设计决定

sqlite 不实现,让它失败。 在 IndexedDB 上模拟 SQL 必然有语义缺口,而缺口会以「某类查询静默返回错数据」的形式出现。插件从设计之初就写了 loadSqliteModule() 失败时 return 且不注册服务的路径,下游用 ctx.get('ondra-store-sqlite') ?? ctx.get('ondra-store-memory') 取端口,自然落到 json / memory 版。一行插件代码都不用改。

插件模块改成动态 import。 静态 import 会让桌面端前端从 490 KB 涨到 1955 KB——全是它永远不会执行的插件代码。改成 import() 后切成独立 chunk(plugins-*.js 1471 KB),桌面端永远不会请求它。

复刻版保留但降级为兜底。 插件取不到服务时才走它,一致性由 check-local-parity 逐行比对(130 + 249 行)守着。

实测结果

  • 75/76 个插件在安卓 WebView 里装载成功(pnpm check:mobile-plugins)
  • 扩展抽屉显示「75 个包 / 75 个在跑」
  • 情绪由 ondra-emotion-core 计算:mood 0.5、energy 60、tension 0(基线正确)
  • 6 个配置文件落进 IndexedDB,刷新后仍在
  • 唯一装不上的需要 node:net 起端口探测,WebView 里没有对应物

顺带修的单位错位

companionView() 原读插件的 axes.energy(0–100),而界面按 0–1 画滑杆,60 被钳到满格显示成「精力 1.00」。改用 normalized(插件已做过除法),与桌面端 GET /api/companion/state 同源。

CI 三端全挂的修复

打 tag 后三端全部失败,报同一处:

lib/local/plugins.ts(44,35): error TS2307: Cannot find module
  '@ondra/cordis-plugin-ondra-core' or its corresponding type declarations.

本地能过、CI 全红,是两个独立的顺序问题叠在一起。

一、前端 tsc 不该依赖插件的编译产物。 tsconfig.paths 原来指向真包,tsc 要读它的 lib/index.d.ts——而 CI 的 build:web 跑在 build:plugins 之前,那时 lib/ 不存在。本地能过是因为之前构建残留了那些文件。

→ 新增 src/lib/local/ondra-core.d.ts(手写的、与插件接口对齐的存根),tsconfig.paths 指向它。实现仍由 Vite 的 pluginAliases 指向真包,两边互不依赖。存根只声明真实用到的三个成员,不是 any——插件接口改名时这里要跟着改,那是故意的摩擦。

二、桌面端不该把插件打进产物。 host.ts 里 type PluginRuntimeModule = typeof import('./plugins.ts') 看着是类型位置,但对打包器来说是一次真实的模块解析请求。改成手写接口后,桌面端构建又报 MISSING_EXPORT: loadCore:Vite 在非 localMode 时没有插件别名,退回 tsconfig.paths 的存根,而存根只有类型没有实现。

→ loadPluginModule() 加构建期守卫 if (!__ONDRA_LOCAL_MODE__)。那是 define 注入的字面量,桌面端构建时是 false,整个分支(含 import())被摇掉。

三、安卓 job 缺一步「编译插件」。 build:mobile 单独跑,那时插件的 lib/ 同样不存在。→ release.yml 的 android job 加「编译插件」步骤,放在 build:mobile 之前。

验证

模拟 CI 干净态(删掉全部 154 个插件产物)后依次跑:

  1. src 的 tsc --noEmit → exit 0(存根生效)
  2. pnpm run build:plugins → 75 个包全部 Done
  3. vite build → 492 KB,无插件
  4. ONDRA_LOCAL=1 vite build → 493 KB + plugins 1471 KB
  5. check:mobile-plugins → 15/15

check-mobile-plugins 15/15、check-mobile-drawer 126/126、check-vfs 10/10、check-ui 432/432、check-acceptance 97/97、check-contrast 67/67、check-local-parity 全过、两端 tsc 全过。

反向验证:注释掉 installProcess() → 插件全部装载失败(0 个包、0 个文件),精确报红 4 条;还原后 15/15 转绿。