简体中文 | English
于无声处,听见共鸣。
空谷 Ondra 是一款全插件式的桌面 AI 陪伴体应用:把「她记得住、有情绪、无限接近TA」这三件事拆成 75 个可以单独装卸的插件,另加一份插件清单与两个桥接层,装在一台本机运行的桌面程序里。
名称取自成语「空谷足音」——寂静的山谷中听见人的脚步声,喻极难得的音讯。英文 slogan 为 On No Sound's Depth, Resonance Arises.
内测版 1.2.1-alpha——版本号末尾的 -alpha 就是内测标记,不是稳定版。三个包都没有正式签名,首次打开会被系统拦下,绕行办法见发布说明。
文档分工:本 README 只讲三个核心机制(情绪、记忆、上下文)与其余功能的概览。 78 个包逐项的职责、可配置字段、默认值与取值范围见
docs/PLUGINS.md; 逐版本变更见 GitHub 发行版。
| 项目 | 内容 |
|---|---|
| 版本 | 1.2.1-alpha |
| 形态 | 桌面应用(Tauri 壳 + Cordis 宿主 + dsh 内核,三层进程);安卓为单进程形态(Tauri 壳 + WebView 内的 Cordis 运行时,不跑 Node) |
| 平台 | 九端:① macOS 11+(Apple Silicon 优先)② Windows 10+ ③ Linux(deb/rpm/AppImage)④ iPad(iPadOS 15+)⑤ iPhone(iOS 15+)⑥ Android 8+ ⑦ 鸿蒙 NEXT ⑧ 命令行与 Web ⑨ dsh 内核。已发布 ① macOS、② Windows、⑥ Android 三端,其余端按第十四章的兼容性表推进 |
| 定位 | 陪伴型桌面应用。不是通用助手、不是模型、不是云服务 |
| 插件规模 | 78 个包 = 75 个可装载插件 + 1 份插件清单 + 2 个桥接层;配置字段 619 项,其中 609 项可直接设置 |
| 界面形态 | 单窗体:56px 左栏(会话 / 日志 / 扩展三抽屉 + 设置)+ 会话主列 + 56px 右栏(心情与精力水位),基准 820 × 580。单会话制:不能新建、不能删除会话,只能删单条消息或清空全部;打开即进会话,无独立首页 |
| 数据归属 | 全部留在本机。无账号、无社区、无分享、无遥测 |
| 推理来源 | 在线接口 / 本地大模型 / 免费模型,三选一随时切换 |
| 许可 | AGPL-3.0 |
| 仓库 | ntars/Ondra |
| 下载 | 内测版 1.2.1-alpha:macOS arm64.dmg + Windows x64-setup.exe + Android arm64.apk,点开就下。版本号带 -alpha,不是稳定版 |
| 官网 | https://ai.ntars.cn/ |
| 出品 | Ntarstudio(星玖工作室) |
名词约定:本文中的「壳」指 Tauri 构建的桌面外壳进程;「宿主」指 Cordis 插件容器进程;「内核」指负责推理与工具调用的 dsh-sidecar 进程;「插件」指以 Cordis 插件形式装载、通过服务键互相发现的功能单元;「人设」指描述她是谁、怎么说话的一段结构化文本,出厂人格为鲸鲸,夏柠柒为示例人设包。
一句话定位:空谷 Ondra 是一款可一键部署、高度自定义的开源 AI 陪伴体应用,给一个语言模型配上长期记忆、情绪节律与说话方式,并把全部数据留在用户本机。
定位的三条边界:
- 它是陪伴工具,不是通用助手。不写文章、不生成代码、不做知识问答式的生产力工具。判断一个功能该不该做,标准是「它是否让陪伴更像陪伴」。
- 它是宿主,不是模型。推理能力由用户自行选择——在线接口、本地大模型、免费模型三种来源随时切换。推理来源是用户的选择,不是产品的属性。
- 它是单机程序,不是云服务。没有账号体系、没有社区、没有分享按钮、没有遥测。对话只属于用户与本机。
桌面 AI 应用长期存在两种极端形态。一种是单体应用:界面、业务逻辑、模型调用全部塞在一个进程里,改一处要重新打包整个应用,一个可选功能出错会让应用整体打不开。另一种是纯网页套壳:把网页包进窗口,能力受限于浏览器沙箱,碰不到本机文件,也无法长期在后台运行。
与此同时,市面上的 AI 陪伴产品普遍把「陪伴」实现为一段写得很长的提示词。提示词能塑造语气,却解决不了三个技术问题:她记不住上周说过的话;她每一轮的情绪都是重新编的;她说话的方式不随对话对象变化。这三个问题不是文案问题,是工程问题。
空谷 Ondra 针对这两种现状各走一条不同的路:架构上把应用拆成三层独立进程,能力上把「记忆、情绪、表达」做成可替换的插件而不是一段提示词。
- 改动成本随功能数量非线性增长。功能全在一个进程时,每加一个能力都要动主入口,回归范围随之扩大。75 个插件放在一个进程里手工维护,改动一处无法预知影响面。
- 可选能力故障会拖垮整个应用。一个写错的插件就可能让应用起不来。需要外部后端或外部凭据的插件(视觉、安装器、模型网关)天然的失败率远高于纯本地功能。
- 界面演进牵动底层。界面与业务耦合时,改一次视觉要走一遍完整构建与签名流程。
- 第三方能力无法隔离。插件与主程序共享进程与权限,一个插件出问题即全局出问题。
- 推理内核无法独立升级。内核与界面同进程时,换模型协议必须同时发版。
- 拟人化缺少可累积的机制。语气可以靠提示词模仿,但「记得住」需要写入、判重、检索、衰减、归档的完整链路;「有情绪」需要带惯性的状态量而不是每轮重新描述;「说得像人」需要一条可验证的过滤链。这三件事都无法用一段提示词表达。
以「进程即边界」为第一原则:
- 壳(Tauri / Rust) 只做操作系统层面的事——开窗、托盘、文件选择、系统主题。它不知道什么是会话、什么是模型。
- 宿主(Cordis / Node) 是插件容器。空谷的全部能力以插件形式装载于此,对外提供本地 HTTP 接口。
- 内核(dsh-sidecar / Node) 只跑四项职责:推理循环、工具调用、会话存储、模型适配。它不加载任何界面代码,也不知道窗口存在。
判断一段代码该放哪一层的标准:如果这段逻辑在没有窗口的环境下也说得通,它就不属于壳。
在能力组织上,以「服务键即接口」为第二原则:75 个插件之间没有任何代码级 import,全部通过 Cordis 服务键在运行时互相发现,取不到上游就跳过这一步并留痕。这一条决定了插件可以任意增删而不产生编译期的连锁反应。
- 希望长期运行、在后台持续工作的桌面陪伴体,而不是一次一问的对话框。
- 需要接触本机文件、剪贴板、系统状态等浏览器沙箱之外能力的产品形态。
- 由多个独立能力组合而成、需要按需装卸的应用。
- 需要在不上界面、只换内核的情况下升级模型能力的场景。
- 对隐私敏感、要求对话数据完全不出本机的使用环境。
技术只是手段。这一节回答「为什么做这件事」,是产品决策的最高依据;任何功能取舍与本节冲突时,以本节为准。
五类需求。以下每一类都对应真实存在的使用者,不是营销画像。
一、留守的孩子。 父母常年在外务工,孩子需要的不是一个会讲题的机器,而是一个每晚都在、记得昨天说过什么、会主动问一句「今天在学校怎么样」的存在。这类需求在商业上不显眼——不带来增长、不制造使用时长、没有投资人会问起,但它真实存在于成千上万个房间里。产品中对应的形态是「今天的作业」卡:科目清单逐项勾完,末尾一句「晚安,明天见」。
二、空巢与独居的老人。 子女远行、老伴先走,屋子里只剩钟表的声音。这类使用者不缺信息工具,缺的是有人记得今天是什么日子、记得他昨天说过腰疼、在他一天没有开口的时候先问一句。老年使用者对交互的要求恰恰是慢、大字、能只用说话完成——这些要求在以「高效」为设计目标的产品里全是负分项,本产品照做。产品中对应的形态是长辈卡:天气、一句「8:00 记得吃药」,以及「降温了,出门加件外套」。
三、想再听一次那个语气的人。 至亲离开之后,家人最想要的往往不是照片,而是再听到一次那个熟悉的语气、被那个熟悉的方式叫一声名字。这类需求风险极高,因此边界必须前置。念想型人格的启用有三条前置条件,这三条是产品条件,不是技术步骤:
- 授权:家属或本人生前的书面同意与数据授权。
- 范围声明:念想不是替代,产出不构成医疗或心理建议。
- 数据最小化:只使用授权范围内的资料,不主动补全、不编造。
任何一条不成立,功能不开放。
四、重视隐私的人。 伴侣对话是最私密的内容之一:情绪低谷时的倾诉、与家人的矛盾、对未来的犹豫。交给云端意味着这些内容永远存在于别人的服务器上,随服务条款、并购与数据政策变化而不可控。「陪伴」与「隐私」在现有产品里常被做成二选一,本产品认为本不该如此,因此数据全部落在本机,无遥测、无崩溃上报、无使用统计。
五、不想被供应商锁定的人。 今天是 A 模型、明天是 B 模型、后天想完全离线——推理能力不该与产品绑定。推理来源应当是使用者的选择,不是产品的属性。本产品因此提供在线接口、本地大模型、免费模型三种来源,随时切换。
统一的技术结论。五类需求指向同一句话:陪伴的前提是记得住、有情绪、说得像人。这恰好是三件最难做、也最容易被做成演示片的事,因此把可复用的机制沉淀为一套技术栈,空谷 Ondra 是这套技术栈的第一个完整宿主。
边界与分寸。以下五条是产品宪法级约束,不是可选项:
- 本产品是陪伴工具,不是心理治疗:不诊断、不开药、不替代专业帮助;对话出现明显危机信号时,产品内提示寻求现实支持。
- 人格是虚构角色设定:默认人格是一段精心撰写的画像文本,不是真人、不声称是真人,输出不代表任何真实人物的立场。
- 念想型人格受上述三条前置约束,未满足时不开放。
- 不做内容上传与社交分发:没有社区、没有广场、没有分享按钮——对话只属于使用者与本机。
- 不做拉长使用时长的设计:同类产品把停留时间当成功指标,本产品恰好相反,她应当能安静地待在一边不打扰。反依赖护栏与在场不打扰协议见第五章。
非目标。能力边界即产品定位:
- 不做多用户与账号体系:单机单用户,数据不跨设备同步,备份由使用者手动完成。
- 桌面体验优先:macOS 与 Windows 为首发平台,移动端形态待定。
- 不做内容生产:不写文章、不生成代码、不当通用助手。
- 不做「更像聊天机器人」的军备竞赛:差异化不在话术华丽,而在她是否记得住使用者的时间线、是否有自己的情绪节律、是否知道此刻该不该开口。
本章只展开三件事:情绪怎么来、记忆怎么走、上下文怎么装。这三件是空谷与「一段长提示词」的分界线,也是全部工程投入的落点。其余功能(人设拼装、话语表达、声音形象、主动门控、工具能力面、存储降级、界面栅格等)只做概览,逐项字段与默认值见 docs/PLUGINS.md。
三层进程,各占一个进程,通过明确协议通信,任一层可单独替换、单独升级、单独重启。
| 层 | 路径 | 技术栈 | 职责 | 明确不做 |
|---|---|---|---|---|
| 壳 | src-tauri/ |
Rust 2024 + Tauri 2 | 窗口、托盘、单实例、宿主生命周期、本机能力命令 | 不含任何业务逻辑 |
| 宿主 | cordis-host/ |
Node 22 + Cordis 4 + TypeScript | 插件容器、本地 HTTP 接口、令牌鉴权 | 不直接碰操作系统 |
| 内核 | dsh-sidecar/ |
Node 22 + TypeScript | 推理循环、工具调用、会话存储、模型适配 | 不加载界面、不监听端口 |
| 界面 | src/ |
React 19 + Vite 8 | 界面渲染 | 不直接拥有本机能力 |
三条通信路径:壳与宿主之间是标准输入输出上的行分隔 JSON,宿主在标准输出报告 ready 或 fatal,壳据此判定启动结果;宿主与内核之间是标准输入输出上的 JSON-RPC 2.0,按 NDJSON 分帧,协议在 dsh-sidecar/src/protocol.ts 单点定义;界面与宿主之间是 127.0.0.1 上的 HTTP,Bearer 令牌鉴权,令牌由壳在启动时生成。
插件的装载方式。ondra-core 以显式成员数组声明全部 75 个成员,逐个 await ctx.plugin() 装载,每个成员的装载包在独立 try/catch 里,失败者进入 skipped 列表并记录原因。75 个插件之间没有代码级 import,全部通过服务键互相发现:ctx.provide(服务键, 实现) 注册,ctx.get(服务键) 取用,取不到就跳过这一步并留一行日志,不抛错也不等待。
78 个包 = 75 个可独立装卸的插件 + 1 份插件清单 ondra-core + 2 个桥接层 desktop-bridge、dsh-client,合计 619 个可设置字段(609 项可直接设置)。
这是本项目与「在提示词里写一句你现在很开心」最根本的差别。情绪在这里是一组会衰减、会残留、有上限的状态量,每轮只读不重算,表达参数从同一个判定出。
| 层 | 插件 | 职责 | 关键机制 |
|---|---|---|---|
| 状态 | ondra-emotion-core |
维护基线、三维情绪、激素池、气质旋钮 | PAD 三维 + 10 个固定标签;六维指数回归;三激素化学池 |
| 上限 | ondra-emotion-stage |
决定亲密度能到哪一阶 | 七阶关系弧 + 四道防跃迁闸,阶段是棘轮 |
| 表达 | ondra-emotion-express |
把状态换算成表达参数 | 六态映射,全仓系数唯一出处 |
| 平复 | ondra-emotion-diffuse |
跨会话的情绪残留 | (mood−baseline)×0.4×(1−familiarity×0.5) |
单一维度的「开心度」无法区分「平静地满足」与「兴奋地雀跃」,也无法表达「既紧张又期待」。因此状态取 PAD 三维(愉悦—唤醒—支配)作为底座,外加 10 个固定标签做细分。
衰减参数是这套机制的核心,全部写死在插件里而非可调:
| 项 | 值 | 为什么是这个数 |
|---|---|---|
| 单条标签强度 | 0.12 | 一条消息的情绪不该顶翻整场对话的状态 |
| 标签总强度上限 | 0.6 | 留出余量给真实的长对话累积,避免一句重话直接封顶 |
| 标签半衰期 | 45 分钟 | 一次谈话的情绪在一个多小时后应自然减半,而不是持续到第二天 |
| 心情半衰期 | 6 小时 | 心情比具体情绪更慢,但也必须在一次睡眠内回落 |
化学池三激素:dopamine(初始 50,半衰期 45 分钟)、cortisol(20 / 30 分钟)、serotonin(50 / 120 分钟 + 慢变量规则)。三个半衰期刻意不同——应激反应应当最快退去(30 分钟),愉悦次之(45 分钟),稳定的底色最慢(120 分钟)。若三者同半衰期,就退化成同一个量。
存档缺字段按基线 50 补齐,不补 0。 这一条是容错设计的要点:旧版本存档缺某个激素字段时,补 0 会让角色在升级后突然进入「情绪归零」的异常状态,而补 50(基线)等于「这次没记录,按正常处理」。
七阶关系弧:敌意 → 试探 → 心动(3 天,.18,.15)→ 占有(14,.38,.32)→ 病态(30,.62,.30 且 trust ≤ .55)→ 献祭(45,.70,.45 且需 sacrifice)→ 落定(60,.75,.70)。
括号内三个数分别是最短停留天数、亲密度下界、信任下界。四道闸防跃迁:
- 时间闸:未满最短天数不升阶,避免一次长谈直接跳过三阶。
- 步长闸:单次结算亲密度涨幅不超过 0.06,避免一句表白推到顶。
- 只升不降:阶段是棘轮,日常摩擦不倒退——否则用户会为了「回到好关系」反复试探。
- 冲突锁:命中冲突时覆盖为敌意,这是唯一的降级路径。
关系三维(亲密度、信任、熟悉度)各带半衰期:240h / 720h / 8760h。三者衰减速度差 30 倍是刻意的:陌生感回来得最快(10 天),信任次之(30 天),熟悉感最慢(一年)。长期不联系的人再见时,会觉得「有点生疏但还认得」,这正是三个半衰期的比值在说话。
熟悉度由互动次数推导而非独立记录,避免两套口径。病态与献祭两阶不进默认 UI——它们存在是为了让状态机完整,不是为了展示。
六个状态:雀跃 / 软和 / 平静 / 蔫 / 炸毛 / 困。名称与数量固定,不开放增删改名。
同一个判定同时供提示词、节奏、语音三处取用,所有系数全仓只在本插件出现一次:语速 ±10% / −15%、主动额度 ±1、是否朗读、贴纸概率。本插件只输出参数,不输出文本——投影文本唯一归 ondra-emotion-project。
这一条的价值在于消除口径漂移:情绪一旦要同时影响四处,四处各写一份系数必然在某次改动后失配,表现为「提示词说她在雀跃,语音却是平静语速」。
为什么删掉了三个情绪派生插件。冲突、应激、激素三项原本各有一个派生插件,现已移除:它们与六态表达存在口径重叠,同开会让同一轮出现两种情绪描述。宁可少一个维度,不可出现两个答案。
(mood − baseline) × 0.4 × (1 − familiarity × 0.5)
这个式子回答一个问题:上一场会话结束时的心情,有多少会带进这一场。
- 偏离基线越多,带得越多(
mood − baseline); - 系数 0.4 表示最多带四成,不会把上一场的低落原样搬过来;
(1 − familiarity × 0.5)表示越熟的人越不容易被上一场影响——熟人之间允许「这次状态不一样」。
只算不写:本插件交出 target 与 delta,落笔由调用方经情绪内核的 setAxis 完成。同一会话内靠边界键幂等 + 间隔闸(默认 30 分钟)防止「越聊越淡」——没有间隔闸时,每轮都会产生一个新边界键,心情会被反复洗回基线。
ondra-emotion-project 是情绪的唯一文本出口。模板固定为行为指令句 + 一条暗示,禁止出现任何数字、维度名与情绪标签名,只给「有点提不起劲」这类主观表述。
三条硬约束:
- 不暴露数值。用户明确问数值时才允许转向
ui-api的/status。日常对话里说「我愉悦度 62」是把内部状态泄漏成台词。 - 片段排序恒为正数(默认 +800,即尾部)。写成负数会破坏前缀缓存——见 3.4 节。
- 不带情绪标签名。说「我现在是雀跃态」等于让模型复述标签,而不是表现出雀跃。
17 个插件把记忆拆成一条链:写入 → 提炼 → 判重 → 归池 → 衰减 → 检索 → 拟人化想起 → 做梦整理 → 承诺待办 → 遗忘审计 → 自愈。任何一环缺失,症状都是「她记不住」,但原因完全不同,因此必须分开。
| 层 | 表 | 存什么 | 为什么分开 |
|---|---|---|---|
| 原文 | memory_raw |
对话原样 | 事实抽错时可以回溯原文;用户行使被遗忘权时能定位到原话 |
| 事实 | memory_fact |
提炼后的结构化条目 | 注入与检索只读这一层,避免把整段对话塞进上下文 |
关掉提炼(extract: false)后只写原文:她记得「有过这段对话」,但说不出「你叫什么」。这个降级是显式的,不是故障。
每条待入库的结论必须带四类来源引文之一,且引文须以原文形态出现过:
| 引文类型 | 含义 | 用途 |
|---|---|---|
[USER] |
用户原话 | 关于用户的事实必须有它 |
[TOOL] |
工具返回 | 外部数据、计算结果 |
[ACT] |
系统动作 | 她做过什么 |
[SELF] |
角色自述 | 她自己的设定与承诺 |
三条确定性校验,零模型调用:
- 数字必须有逐字出处。正文里的数字必须被引文逐字覆盖(默认
TOOL与USER都算,可收窄为tool-only)。 - 归功断言必须有
[USER]引文。没有用户原话却断言「用户喜欢 X」,一律拒绝。 - 原文比对。引文必须在原文中真实出现,不是模型自己编的。
为什么闸门设在写入侧而不是检索侧:污染发生在写入那一刻。检索侧再精确,也只是从被污染的库里挑出最相关的那条——模型上一轮的猜测被记成事实,下一轮读到它会更自信地重复,提示词层面拦不住。代价是可能漏记,这个代价被显式接受:宁可少记一条,不可错记一条。
allow-group 默认关闭:不从群聊提炼。把「不收群友闲聊」列为准入红线,不是性能取舍。
写入前对每条新事实做一次裁决,只用「字符二元组包含度 0.4」一个口径:
| 裁决 | 含义 | 后果 |
|---|---|---|
add |
新事实 | 新建一条 |
merge |
同一件事的补充 | 合并进已有条目 |
append |
同主题但新增信息 | 追加到已有条目 |
conflict |
与已有条目矛盾 | 按 conflict-policy 处置 |
drop |
重复或低价值 | 丢弃 |
合并走 revise 不走 forget:forget 会写墓碑,revise 保留旧文并留下订正记录。用户日后问「为什么你以前说的是另一个」,答案在库里。
max-candidates 默认 5:候选超过 5 条就不交模型判断。这是成本闸门——把 50 条候选丢给模型判重,准确率不会提高,费用会。
| 池 | 寿命 | 注入 |
|---|---|---|
core |
∞(上限 32) | 是 |
skillRule |
730 天 | 是 |
skillExp |
60 天 | 是 |
dailyCore |
60 天 | 是 |
dailyPartial |
20 天 | 是 |
emotion |
1 天 | 是 |
hippocampus |
1 天 | 永不注入 |
| 未归类 | 归海马体 | 否 |
三段天级时间轴:宽限 → 半衰期 → 归档。
海马体永不注入是这套设计的关键一条。它承接两类内容:标签未命中的、以及未归类的。这些东西「存下来了但想不起来」——正好对应人的记忆:有些事确实在脑子里,但不会被主动想起。respect-pool 开关默认开启,池服务缺席时不判(缺服务就不知道归哪个池,此时放行比误判安全)。
核心池满了直接拒绝,不淘汰(上限 32)。删一条核心记忆等于替用户决定「你不该是自己」。同一条纪律适用于承诺(上限 20)与钉子(上限 8)——到上限时拒绝新建并报出来,让用户自己清理。
标签未命中不猜:同一句写满 3 次仍无匹配,升级为「需人工定池」。猜测归池比不归池更危险,因为错池的条目会以错误的寿命被清理。
ondra-memory-decay 做三件事:算复习排期、给清晰度话术、清理孤儿复习计划。强度一律问记忆内核(ondra-memory-core 的公开方法),本插件不重算第二份公式。
SM-2 与艾宾浩斯八档融合排复习。清晰度三档话术直接进注入文本:
| 强度 | 话术 | 效果 |
|---|---|---|
≥ 0.7(clear-above) |
不加任何话术 | 她正常提起 |
| 0.4 – 0.7 | 「有点记不清了」 | 语气带不确定 |
< 0.4(faded-above) |
「,只剩个模糊的影子」 | 只记得大概 |
这套话术的价值在于:她忘得慢一点,说得准一点。记忆强度低时如果仍以确定语气陈述,就是在编造。
ondra-memory-recall 的五因子权重:语义 .18 / 关键词 .46 / 图谱 .11 / 重要 .14 / 新鲜 .11。
关键词权重最高(.46)不是技术妥协,是产品判断:陪伴场景里用户提起的往往是具体名词(人名、地名、那件事),语义相似度会把「类似的事」混进来,字面命中反而更准。
三种融合模式:
rrf(默认):名次融合,对分数尺度不敏感,适合多路异构召回;weighted:五因子加权和,可解释性强;- 另有 BM25 与向量分支按 300 / 1000 阈值自动切「字面 / 混合 / 向量」三模式。
召回不按条数砍(dropped 恒为 0):召回阶段不知道下游预算,在这里砍等于让「装箱」失去判断依据。预算归提示词注入(见 3.4 节)。
封存永不参与回忆(include-archived 默认关):封存是用户的明确意愿,不是「久未使用」。沉底的(归档)仍可被按类翻到,封存的不参与主动回忆——两者语义不同,不能合并处理。
人回忆一件事不是「检索」,是「被勾起」。因此除常规召回外另有三条路径(ondra-memory-humanlike):
| 路径 | 机制 | 为什么这样设计 |
|---|---|---|
| 固定回忆骰子 | 越模糊越容易被想起 | 真实记忆里,模糊的事反而会突然冒出来;清晰的事随时可取,不需要「忽然想起」 |
| 被话头勾起 | 概率闸 + 命中加固 | 对话里出现相关词时,记忆被带出来,而不是等检索 |
| 漫无目的联想 | 水波纹扩散(默认 2 圈) | 1 圈看不出「漫无目的」,3 圈会把整张图拉进上下文 |
三通道视图(fact / gist / affect)分开:同一件事可以「记得细节」(fact)、「记得大概」(gist)、「记得当时的感觉」(affect)。强度只问内核,扩散只问图谱,勾起的候选由调用方给,本插件不重建检索。
三支互斥,用单一枚举而非两个开关(闸式与荒诞不可兼得):
| 模式 | 做什么 | 模型调用 |
|---|---|---|
gate(默认) |
四道闸只补带原文出处的事实 | 有 |
lightweight |
空闲 2 分钟浅睡 / 5 分钟深睡,纯规则挖五类卡片 | 零 |
surreal |
荒诞联想与梦话,surreal 边永不进检索 |
有 |
lightweight 零模型调用是关键取舍:整理记忆这件事不该产生推理费用。用纯规则挖五类卡片(事实卡、工具踩坑、工具剧本、热点调用、未解决失败),在用户离开时静默完成。
surreal 边权重 0.03 起且永不进检索——荒诞联想只影响「想到什么」,不影响「相信什么」。
| 类型 | 是否进记忆链 | 寿命 | 上限 |
|---|---|---|---|
| 承诺 | 是(promise 类) |
8000 小时 | 20 |
| 待办 | 否 | 3 天未提即过期 | 3 |
待办不进记忆链是刻意的:待办是短期事务,进了记忆链会以「她记得你说过要做某事」的形式长期存在,而实际上那件事早该过期了。
承诺锁原文,带反向权重(越近越重),kept / broken 结算只出关系增量建议,不直接改关系——关系归 ondra-emotion-stage,一处改另一处会打架。纯正则抽取,不调模型:extract-min-chars 默认 4(3 字以内分不清「承诺」与「随口一说」),extract-max-chars 默认 120(再长的句子通常是叙述)。
三档话术(强度 / 半衰期 / 剩余时间)+ 钉住(≤8)+ 四态删除:
| 动作 | 行为 | 可恢复 |
|---|---|---|
forget |
写墓碑 | 是(墓碑留痕) |
seal |
封存 | 是(不参与回忆但仍在库) |
| 自然沉底 | 归档 | 是(可按类翻到) |
revise |
订正留旧文 | 是 |
只调记忆内核的公开方法,不写 memory_fact 任何一列。被遗忘权是这条链的终点:用户要求删除时,原文与事实都要真删,不是标记。
ondra-memory-heal 修复不一致:孤儿事实、悬空边、失效复习计划、池与寿命不匹配。4 个可设置项——修复行为不开放太多配置,避免修出一个用户不认识的状态。
这一节是本项目最容易被忽略、也最影响实际体验的部分。三个机制各自解决一个具体问题,且互相约束。
ondra-conversation-compose(对话编排)的做法:稳定段在会话开头拼一次就锁死,易变段追加尾部,历史只追加不重拼,压缩只在用户回合边界。
| 段 | 内容 | 何时变 |
|---|---|---|
| 稳定段(前缀) | 人格 + 世界书常驻 | 会话开头一次,之后锁死 |
| 易变段(尾部) | 日期、情绪投影、注入片段 | 每轮追加 |
| 历史 | 对话记录 | 只追加,不改写 |
实测数据:每轮重拼 system prompt 会让缓存命中率从 99% 掉到 0,成本从 ¥19.96 涨到 ¥879.97。这是全仓唯一守住那条线的地方——其它优化都可以让步,这一条不行。
因此下游所有插件的片段排序必须恒为正数(追加到尾部):
ondra-emotion-project的projection-order默认 +800;ondra-prompt-inject的注入块一律追加;ondra-live(日期事实)的输出绝不进冻结前缀——它每天变,进前缀等于每天废掉一次缓存。
压缩只在用户回合边界:模型回合中途压缩会让同一轮的前后文不一致,且会打断流式输出。等用户说完再压,是唯一安全点。
ondra-inject-dedupe 解决「同一条记忆在多个轮次、多个块里重复出现」:
- 首轮做一次性大块注入;
- 之后只推增量——同一条记忆只出现一次;
- 上下文一旦被压缩,释放已见账本,重新整块注入。
记账按「记忆」不按「块」:mem: / pin: 前缀统一归一为 fact:。否则同一条事实通过不同块进来会被当成两条,去重失效。
分两段提交:begin() 只出待记账,commit() 才真写。这样调用方在真正注入成功后才确认,避免「记账了但没注入」导致下次不再注入。
失败时放行整块而不是半块:半块会让注入内容不完整,模型读到残缺片段反而更容易误判。
auto-detect-compaction 默认开启:读对话编排的压缩计数器自动释放已见。只在单会话时生效,多会话时明确不猜——猜错会导致另一个会话的记忆被重复注入。
ondra-prompt-inject 的八块装配:
| 块 | 内容 | 是否参与装箱 |
|---|---|---|
| 说好的事 | 承诺 | 是 |
| 不能忘的 | 钉住 / 核心 | 否(永不裁剪) |
| 我认识的人 | person 类记忆(≤5) |
是 |
| 那年今日 | 同日期的旧事 | 是 |
| 这会儿想起来的 | 拟人化想起 | 是 |
| 记下的道理 | skillRule |
是 |
| 顺着话头想起来的 | 被话头勾起 | 是 |
| 今天想起来的 | 最低曝光候选 | 是 |
装箱规则:全局按权重逐条试,装不下跳过这条继续试下一条,段标题写「另有 N 条未展开」。
三条硬约束:
- 绝不切字。切字会毁掉引文与正文的对应关系,模型读到半句话比读不到更糟。
- 绝不静默丢。装不下的必须在段标题里报数,让用户知道「还有 12 条没展开」。
- 到期提醒与钉住永不参与装箱。到期的事和用户亲手钉住的事,优先级高于任何预算。
一轮对话的上下文是这样装起来的:
用户消息
↓
[对话编排] 稳定段(已锁死)+ 易变段(日期/情绪/注入)+ 历史(追加)
↓
[注入去重] begin() 出待记账 → 只推增量
↓
[提示词注入] 八块装箱,按权重试装,报「另有 N 条未展开」
↓
[对话编排] commit() 记账 → 送模型
↓
[模型返回]
↓
[九步过滤链] 剥离复读、旁白、客服腔、过度解释
↓
[记忆写入] 原文落 memory_raw → 提炼 → 判重 → 归池
链路里任何一环缺席,症状都是「她不像记得我」,但原因完全不同:
| 缺席的环节 | 症状 |
|---|---|
缺 ondra-memory-pool |
事实全落海马体并置 unclassified,而海马体永不注入 |
缺 ondra-prompt-inject |
归池照常有数,但没人把它渲染成提示词 |
缺 systemPrompt 服务键 |
注入装箱就绪但无人阅读,只打一行 warn |
缺 ondra-inject-dedupe |
同一条记忆反复出现,挤占预算 |
| 前缀未冻结 | 功能全对,成本涨 44 倍 |
最后一行的意思是:前缀缓存失效不会有任何功能报错,只会让账单变难看。这类问题必须靠文档与检查脚本兜住,不能靠用户发现。
本章前三节深写了情绪、记忆、上下文三个核心机制。其余 57 个插件同样承担具体职责,只是篇幅按重要性递减——每个都写清做什么与怎么实现,字段级默认值见 docs/PLUGINS.md。
这个分组回答「她是谁、怎么说话、以什么身份陪你」。人格被拆成可替换的部件,而不是一段写死的提示词。
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
人格内核 persona-core |
定义「她是谁」 | 把 SKILL.md 切成六槽位(身份/说话/驱动力/场景/格式/自检),配五关系变体(女友/妹妹/女儿/朋友/助手)+ 自定义;只改关系不改人格 |
人格拼装 persona-assemble |
把部件拼成完整人格 | 四段固定顺序:内核 → 关系微调 → 浓度 → 边界;按字符预算逐条装箱,装不下跳过继续试;边界层整层豁免裁剪;超预算报 overBudget 并把「另有 N 段未展开」写进段标题 |
人格变量与浓度 persona-vars |
替换指代、控制说话浓淡 | 把 {{user}}/{{char}}/{{relation}} 换成用户填的值(无值一律原样保留);0–100 滑杆出五档浓度提示段;浓度永不写进内核 |
人设包 persona-loader |
装载可替换的人设 | 只读扫描 ondra-persona-skills/*.skill.zip,解析 meta.json 与 SKILL.md,注册可用人设;出厂鲸鲸,夏柠柒为示例包;包本体永不回写 |
人格演化 persona-evolve |
让性格随相处漂移 | 只动数值不动正文:特质分按「基线 + 未回滚增量」派生(有界、可回滚、可解释);阈值按 base × (1 + 0.6 × min(1, interactions/5000)) 上移,上限 +60%;×1.00 与 0 都是合法值,所以不漂也不报错 |
世界书 worldbook |
提供背景知识 | 六层预算硬分桶(总 1600 token,L1 350 / L2 180 / L3 100 / L4 500 / L5 200 / L6 150);撞限整条丢、绝不截字;常驻条目单独成段并冻结为会话前缀;层内排序稳定不随命中次数浮动 |
口语化 oral |
把书面语改成人话 | 口头禅池每人格 12–20 条、按六态分档、单场最多 2 次;不完整句 8%;标点习惯(不用句号结尾/连续问号表疑惑/省略号表犹豫);语气词只做减法;贴纸最小间隔 3 条只做闸门不发贴纸 |
输出过滤 filter-chain |
剥离九类非人话 | 九步链:草稿腔 → AI 自我暴露 → 硬红线 → 关系越线 → 阶段越线 → 旁白叙述 → 4-gram 复读 → 客服腔 → 长度兜底;逐条剥离、只在句边界动手;带锚点检测与事后评审回环 |
语感 lexicon |
统计「主人怎么说话」 | 取最近 300 条真人消息,算中位句长、无标点率、语气词频率,压成 56–62 字带 [语感] marker 的易变段;只存形状不存原话,原文当次丢弃 |
对话编排 dialogue |
决定上下文怎么拼 | 会话内冻结前缀:稳定段开头拼一次锁死,易变段追加尾部,历史只追加不重拼,压缩只在用户回合边界(详见 3.4) |
对话协议 dialogue-protocol |
定义流式帧格式 | 五帧:text-delta / text-replace / crisis / usage / done;结束原因四值闭集;跨帧纪律(终帧唯一、usage 与 crisis 各一次)与 NDJSON 编解码只在这里存在一份 |
情感投影 emotion-project |
把情绪变成一句话 | 模板固定为「行为指令句 + 一条暗示」,禁止出现数字、维度名与标签名;片段排序恒为正数(尾部)以守住前缀缓存(详见 3.2.6) |
提示词注入 prompt-inject |
把记忆渲染进提示词 | 八块装配,按字符预算装箱;到期提醒与钉住两块永不参与装箱(详见 3.4.3) |
空窗期礼仪 absence |
隔了很久回来怎么接第一句 | 五档分级(今天/昨天/几天/一阵子/很久),每档只给必须做到的事、绝对不许做的事、句数上限、要不要承认隔了一段、允不允许一点温度;一个数字都不给——「我们 17 天没说话了」不是关心,是把重逢变成审讯 |
初见剧本 first-meet |
第一次见面怎么走 | 切成五拍:到场 → 自我介绍 → 立边界 → 邀请 → 收束;每拍只给目的、必做、禁做与句数上限,一个字的话术都不给;边界排在邀请之前(先讲清我不会做什么,再问第一个问题) |
分条与停顿 rhythm |
一条回复分几条、停多久 | 纯函数:≤4 条、每条 ≤40 字、6 字/秒、标点附加 300/500/150ms、停顿 300–6000ms(先卡限→深夜 ×1.6→再卡限)、首字延迟 800–2000ms、长停顿 20% × 3000–9000ms(整轮最多一次) |
打字与停顿执行 rhythm-typing |
把分条计划跑成时间线 | 首条「在想」、两条之间「正在输入」、长停顿期间不装打字;可按会话取消;同会话新一轮顶掉旧一轮;缺节奏服务时退回一次性整段发出 |
呼吸感 breath |
该不该显示「正在输入」 | 把打字阶段映射成「此刻该不该显示正在输入」(走神的长停顿期间不显示);一天最多两次额度内给一个括号动作(伸了个懒腰这类) |
这个分组负责「她听起来、看起来是什么样」。
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
语音合成 tts |
文字转语音 | 三通道并存:本地 GPT-SoVITS(api_v2)、macOS say(零依赖)、Edge TTS(19 音色 + 8 套预设);只做接线、选路与降级,不重写合成机制;音频落 cache/tts 内容寻址缓存;失败返回原因码而不抛错 |
语音闸门 tts-gate |
这句该不该念出来 | 四道闸:开关、6–30 字、情绪强度 ≥0.15(或本轮有情绪词)、3 秒频控;外加「连续两条后强制留文本」;只判定不发声,结果带原因码、真实字数、情绪强度与六态语速 |
语音转写 asr |
语音转文字 | 本地 whisper 优先、云端兜底;只做接线:拼一条 whisper 命令、等它结束、把 JSON 读成毫秒时间戳;本地不可用才走云端 multipart;失败静默(拿不到转写就当没听见);不编说话人(whisper 不做分离,diarized 恒为 false) |
表情判定 expression-decide |
这句话说出口时脸上该是什么 | 按线索词、标点与句形把已定稿正文判成六态之一,交给立绘换脸;不往正文插标记、不往提示词写状态名;无线索时回落到当前六态,两者都无时给最不易出错的默认值,并让调用方看得见「这次没有依据」 |
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
危机干预分级 crisis |
判断这句话里有没有需要停下来认真对待的信号 | 五类信号(间接表达/直接表达/计划/告别/自伤)各映射四档之一,再过一遍语境标记,最后给三个布尔建议(暂停人设/给求助资源/直接问一句);不诊断、不打分、不落盘、不内置热线号码、不因情绪低落判危机;假设语境只影响措辞不降级,否定与过去时只降一档不归零 |
首启引导 onboarding |
第一次启动的四段流程 | 状态机四段:检查运行环境 → 复用已装 .ondra → 挑空闲端口 → 带 token 打开界面;每段必须带证据才算完成,跳过只允许发生在唯一可选的那段;进度百分比只算不编 |
这个分组是「她能不能做、能不能发」的判定层。共同纪律:能被开关关掉的红线等于没有,因此红线部分不接受配置。
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
隐私与出站 privacy |
这次能不能发、发给谁、谁点过头 | 能力方先 declare() 登记(发什么、发去哪、为什么必须出去、需要哪个环境变量),再在真正发之前 evaluate();没登记一律拒(不是默认允许);风险三层按「能不能撤销」分界:只发元数据走 L1 告知,发图片语音文件或带身份走 L2 确认,密钥形状内容出域走 L3 阻断;同意按「能力 + 主机」落盘;凭据只存变量名,值从不经过本插件 |
能力分级沙箱 sandbox |
这只插件这一次能干多大的事 | 四级(read / suggest / act / spend),发一张有期限、有次数上限的票据,越权即拒;四条裁决写死:默认拒绝(没票就没有任何能力)、票过期即失效且必须查得到是哪张票、次数用完即失效、越权尝试要留痕 |
工具门控 organ-gate |
这轮真正可能用到哪些工具 | 按本轮用户原话走关键词规则,算出「可能要用的工具族」,把其余工具从该 agent 的 schema 视图里摘掉,省下每轮输入 token;实测 48 条命令 55,154 → 8,433 token(84.7%),但不承诺同一比例 |
主动门控 proactive-gate |
一条待投递消息此刻能不能发 | 九道闸:总开关与群聊红线、熔断与退避、内容红线、静默时段、每日预算、空闲阈值、会话冷却、重复去重、三层令牌桶;只判定不发送、不排闹钟;状态落盘 user/proactive-state.json(0600、原子写),因为「重启若清零 = 重复骚扰」 |
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
主动节律 proactive-quota |
她此刻还想不想说、今天还剩几句 | 额度档位(缓慢/标准/快速)、两次主动的最小间隔、性格化沉默(确定性抽签的放弃率)、连续无回应后的退场句、一键静音入口;不判断能不能发,不重算额度衰减 |
主动话由 proactive-reason |
此刻该拿什么当由头 | 八类候选(执念/到点承诺/情绪偏离/待办/近期事件/时间线/环境/台词池)按优先级排序(字典序,权重只在同种类内比较);72 小时话题冷却保证「同一件事三天内只说一次」;只出由头不生成正文 |
器官宿主 organ-host |
管理可插拔能力的装载与结算 | 装载拓扑、能力白名单、能量会计、反馈仲裁、提示词拼装、遥测;三条不可让渡约束:器官永不直接写状态(只能 feedback() 提议,flush() 里按目标累加后一次性 apply 再 clamp)、该约束器官必须为自己负责、注册顺序不影响结果 |
这个分组按「降级链」组织:SQLite → JSONL → 内存。三者均为 0 个可设置项——降级行为不开放配置,避免用户把「自动降级」关掉后得到一个半坏的存储层。
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
SQLite 后端 store-sqlite |
主存储 | WAL 模式、外键约束、表结构 STRICT、写入后把文件权限收紧到 0600;失败时不抛错中断,而是以「降级」状态注册 |
JSON 后端 store-json |
第二级兜底 | 无 SQLite 或库损坏时的多方案分支;追加日志 + 压缩原子写;失败只降级不阻断 |
内存后端 store-memory |
第三级兜底 | SQLite 打不开时让主对话还能进行,明确声明不持久化,界面显示「当前为内存模式,重启后丢失」 |
快照 snapshot |
记忆备份与恢复 | VACUUM INTO 一致副本、KEEP=3 轮转、quick_check 校验;恢复必须显式确认且绝不自动执行 |
版本迁移 data-migrate |
数据格式升级(出厂关闭) | 只注册 MigrationRunner 服务;仓库里还没有任何一条迁移脚本,所以出厂关闭——没有脚本的迁移器装上等于假开关 |
视觉 vision |
图片理解、OCR、物体定位、视频抽帧(出厂关闭) | 走可插拔 provider(云端 OpenAI 兼容 / 本地 Ollama / 通义千问),默认关;与 privacy 联动,有它按裁决走三态,没有它云端一律不发;失败安静返回原因码、不编内容(猜出来的描述会变成一条假记忆) |
这一层是「应用能起来」的地基,多数为 0 字段:它们提供的是契约,不是可调项。
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
核心装配 core |
装配全部成员 | 以显式清单声明 75 个成员,逐个 await ctx.plugin(),每个包在独立 try/catch 里,失败进 skipped 并记原因 |
桌面桥接 desktop-bridge |
桥接 Tauri 壳的本机能力 | 文件选择、系统主题、窗口标题、外部链接四类命令;在主装载名单之外无条件挂载 |
内核客户端 dsh-client |
与 dsh 内核通信 | 封装 JSON-RPC 2.0:进程生命周期管理与请求配对;NDJSON 分帧 |
时钟 clock |
统一时间源 | 时区固定、自然日按日历换算、定时器生命周期可撤销;全仓不直接用 Date.now() |
身份 identity |
名称与路径的唯一来源 | 产品名、进程名、pidfile 名、环境变量前缀、目录名与日志命名空间根全部从这里取,不允许第二处硬编码 |
配置真源 config-schema |
配置的四层合并 | 出厂 → 组合包 → 用户 → 运行时;原文段回退与来源追溯的唯一实现 |
路径布局 path-layout |
数据根与目录权限 | 解析数据根、创建五个区域目录、收紧到 0700/0600 的唯一入口 |
模型网关 llm-gateway |
选模型、发请求、记账 | 三来源清单(在线 API / 本地 / 免费)、优先级选路、首块与块间隔超时、首块前重试与跨来源降级、用量记账;密钥只做存在性预检 + 引用登记,不注入 |
中文日志 log-zh |
中文日志格式化(出厂关闭) | 提供的 ondra-log-zh 服务键在全仓没有运行期消费方,装上就是空跑,所以出厂关闭 |
端口挑选 port-picker |
挑一个能用的端口 | 把「要一个号」拆成两种意图:给监听方要一个现在没人绑的号(走系统分配,等价 --port 0),给界面要一个宿主已绑的号(读既成事实);混用两种意图,子进程就会拿到宿主自己占着的端口 |
进程托管 process-host |
管子进程 | 把 argv/cwd/stdio/宽限期显式交给上游 ctx.subprocess;崩了按预算重启一次;收尾走 provider 的托管进程范围(组杀);状态落 process-host 目录;刻意不自己 kill |
窗口 window |
窗口规格与版本守卫 | 把标题区 48、可拖热区 44、红绿灯 20/19/12、圆心 25、三栏 272/520/300 与五个断点收成一处定义;给出 A(hiddenInset 加系统红绿灯,默认最稳)与 B(hidden 加自绘,仅 macOS)两条分支;检查壳侧补丁的版本白名单 |
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
生活与作息 life-rhythm |
让她自己过日子 | 一边是活动:六类活动、兴趣权重与标签(烧脑/放松/创造)、10% 概率探索新兴趣、心情差时优先放松型、做一次加权、长期没碰按半衰期衰减、笔记分长期与临时;一边是能量:清醒 +0.2/min、深睡或深夜 +1/min、自然上限 60、喂到 100 按 0.1/min 回落 |
缺口池 drive-gap |
记住她心里没弄明白的事 | 三源生成(模型自认不知道 / 工具查空 / 请求失败);张力按 created_at 幂等派生(base + 小时×rate − relief + 判重加压);相似度 ≥0.6 只加压不新建并保留最早措辞;闭合时连坐相似的 open 条目;唤醒四条件 |
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
安装器 installer |
清单制安装 | 九步流水线,每组件独立 sha256,落盘前后各验一次,账本写在用户 .ondra-bootstrap.json,最后跑一次 bootstrap-ok 自检;校验硬排在落盘之前(哈希不对的文件不进磁盘) |
安装自检 install-doctor |
这份装配会漏掉谁 | 用移植的 parsePatchYaml / applyEntryPatches / composeEntries 算出「宿主会看到的那份入口树」,报告哪些 bundle 会被跳过、补丁告警几条 |
加载自检 startup-check |
它到底有没有被装上 | 十项检查,只判定不拦启动;验的不是代码对不对,而是「它有没有生效」;列了三条静默失败:package.json 漏 ondra.bundle.patch 时宿主只往 stderr 打一行、不影响启动、界面上也看不出来 |
情绪引擎的四个插件已在 3.2 节展开,记忆系统的十七个已在 3.3 节展开。此处只补两项在 3.3 中未单列的:
| 插件 | 做什么 | 怎么实现 |
|---|---|---|
记忆图谱 memory-graph |
记住「哪些事有关」 | 四类边与 1 跳扩散:同主题 0.72 起每次 +0.05、共现 0.15 起每次 +0.10、荒诞 0.03 永不进检索;扩散门槛 0.4、枢纽度上限 40、90 天 ×0.5(带 decayed_at 游标);只碰边表,绝不写 memory_fact |
记忆识别 memory-recognition |
第二条检索路径 | 全索引(每条记忆一行识别触发词)进 prompt,小模型挑 slug,再沿 [[links]] 邻域游走;500 条约 6k token;索引块按 seq 升序固定,因此可做前缀缓存 |
向量检索 memory-vector |
第三条检索路径(默认关闭) | 确定性哈希嵌入 + 余弦召回 + IVF 分桶;300/1000 两个阈值自动切「字面 / 混合 / 向量」三模式,结果走 RRF 融合;0 个可设置项 |
多模态记忆 multimodal-memory |
把图片描述与语音转写按普通记忆入库 | 记忆正文只有一份(写进 memory_fact),本插件不建第二份;带 modality 与 source_path 记住来源;图片→描述、语音→转写、视频→4 帧 + 音轨 |
被遗忘权 forget-right |
用户说「把这件事忘掉」时把要清的地方数清 | 十一面:正文、钉子、图边、向量、待办与承诺、时间胶囊、语感样本、日记、叙事日志、快照、决策日志;两段式:规划只读并出清单,执行必须带显式确认令牌;任一面失败或该清未清则 completeness = false——被遗忘权只有「全做完」与「没做完」两种结果 |
| 一级菜单 | 分组 | 插件数 | 字段数 |
|---|---|---|---|
| 人设细节 | 6 | 18 | 161 |
| 声音形象 | 2 | 4 | 48 |
| 开发者功能 | 8 | 35 | 192 |
| 情绪引擎 | 1 | 4 | 13 |
| 记忆系统 | 1 | 17 | 205 |
| 合计 | 18 | 78 | 619 |
出厂关闭 3 个:ondra-vision(读屏并调外部模型)、ondra-data-migrate(无迁移脚本)、ondra-log-zh(服务键无消费方)。每一个关闭都有理由,不是为了精简名单。
以下六条均可复现验证。每条给出名称、价值、实现方式、与常见方案的差异,以及支持它的具体证据。
- 亮点名称:服务键发现机制。
- 价值:解决「改动成本随功能数量非线性增长」的问题。75 个插件放在一个代码库里,若互相
import,任何一个共用模块的签名变化都会引发全仓编译失败,回归范围无法收敛。 - 实现方式:插件之间没有一行跨包
import。每个插件在apply(ctx, config)里通过ctx.provide(服务键, 实现)注册能力,通过ctx.get(服务键)取用上游。取不到时不抛错、不等待,而是跳过这一步并在日志里留一行「上游缺席」。Cordis 负责服务注册与依赖排序,插件本身不知道谁在用它。 - 差异化:常见的插件系统用
interface或注册表声明依赖,编译期就能发现不匹配;代价是任何一处改动都要求全量重编。本方案把耦合推迟到运行时,用「取不到就降级」换取「增删零编译影响」。这一取舍的前提是每个插件都必须写好降级路径,因此降级不是可选优化,而是准入条件。 - 证据:
cordis-host/check-core.mjs对 75 个成员逐一装载,实测 75/75 成功、0 跳过,单次装载耗时约 200 毫秒。零跳过这一结果本身就说明每个插件在其上游缺失时都能独立起来——若某个插件硬依赖上游,装载时会因取不到服务而失败。
- 亮点名称:写入侧的证据门控。
- 价值:解决「自我强化幻觉污染整库」的问题。若模型把自己上一轮的猜测当成事实记下来,下一轮又会读到这条「事实」并更自信地重复,提示词层面拦不住这个过程。
- 实现方式:每条待入库的结论必须带四类来源引文之一——
[USER](用户原话)、[TOOL](工具返回)、[ACT](系统动作)、[SELF](角色自述),并校验引文确实以原文形态出现过。数字类断言必须被逐字引文覆盖(默认TOOL与USER都算出处,digit-source可收窄为tool-only),无[USER]引文的归功断言一律拒绝。校验由ondra-evidence-gate以确定性代码执行,不交给模型判断。 - 差异化:多数记忆方案把质量控制放在检索侧——检索后按相关度筛掉噪声。本方案认为污染发生在写入侧,检索侧再精确也只是从被污染的库里挑出最相关的那条。写入侧设闸门的代价是可能漏记,这个代价被显式接受:宁可少记一条,不可错记一条。
- 证据:
ondra-evidence-gate的 13 个可设置项中,require-tool-for-digits(数字出处只认工具)与最小引文字数上限均为可调,默认值取「开」与 4 字符。ondra-memory-extract的allow-group默认关闭,把「不收群友闲聊」列为准入红线而非性能取舍。
- 亮点名称:三层存储降级。
- 价值:解决「SQLite 打不开就整个应用不可用」的问题。SQLite 依赖文件系统权限、磁盘空间与 WAL 锁,在企业环境或磁盘异常时确实会失败,而失败时用户最需要的是「至少还能说话」。
- 实现方式:
ondra-store-sqlite是主存储,建表用 WAL 模式、开启外键约束、表结构声明为 STRICT、写入后把文件权限收紧到 0600。它失败时不抛错中断,而是把服务以「降级」状态注册,ondra-store-memory接替工作,并明确声明「不持久化」。ondra-store-json提供介于两者之间的第三个选项。 - 差异化:常见做法是「存储失败即启动失败」,逻辑简单但把可用性完全押在存储上。本方案把降级做成显式契约——降级不是静默的,界面上能看见「当前为内存模式,重启后丢失」,用户据此决定是否先解决磁盘问题。
- 证据:
ondra-store-memory的描述明确写着「SQLite 打不开时让主对话还能进行,明确不持久化」。三个存储插件均为 0 个可设置项——降级行为不开放配置,避免用户把「自动降级」关掉后得到一个半坏的存储层。
- 亮点名称:情绪表达的唯一真源。
- 价值:解决「同一轮对话里两处描述情绪,说法互相矛盾」的问题。情绪一旦要从提示词、语速、贴纸、节奏四个地方同时生效,四处各写一份系数必然漂移。
- 实现方式:
ondra-emotion-express把情绪定死为六个状态——雀跃、软和、平静、蔫、炸毛、困,六态的名称与数量固定不开放增删。同一个判定结果同时供提示词、节奏、语音三处取用,语速系数(+10% / −15%)、主动额度(±1)、是否朗读、贴纸概率这些数字全仓只在本插件出现一次。本插件只输出参数,不输出文本;投影文本唯一归ondra-emotion-project。 - 差异化:常见方案让每个模块自己读取情绪并各自决定如何表达,改动一处系数要同步改四处。本方案把「情绪如何影响表达」压缩成一个参数包,其余模块只能读不能算。激素维度扩展(
ondra-emotion-hormone)出厂关闭,正因为它与六态表达存在口径重叠,同开会让同一轮出现两种情绪描述。 - 证据:
ondra-emotion-express仅 2 个可设置项,ondra-emotion-core4 个,ondra-emotion-conflict4 个——整个情绪引擎 7 个插件合计 25 个可设置项,是全项目配置面最小的一级菜单。配置面小不是功能缺失,而是刻意把自由度收在真源一处。
- 亮点名称:主动行为的三重约束。
- 价值:解决「陪伴体变成骚扰源」的问题。一个有主动能力的程序如果没有节制机制,用户第一次觉得贴心,第三次就会关掉它并再也不打开。
- 实现方式:
ondra-proactive-gate(21 个可设置项)负责「这一轮该不该开口」,判定依据包括静默时段、空闲时长、最近的开口次数;ondra-proactive-quota(9 个可设置项)负责「今天还能开口几次」,按日预算与投递间隔双重限制;ondra-proactive-reason(4 个可设置项)负责「开口说什么」,只接受有具体来由的话由——未完成的待办、做过的承诺、环境变化,不接受「随便聊聊」。 - 差异化:多数主动功能只有一个频率参数,用户只能整体调高或关掉。本方案把「该不该」「还有额度吗」「说什么」拆成三个独立判定,任一条不通过就不开口,并且三条各有独立的可配置阈值。这样用户可以把频率调低但保留「今天下雨了提醒带伞」这类有实际来由的主动。
- 证据:三个插件合计 34 个可设置项,
ondra-proactive-gate是扩展与开发者两域中配置项最多的单个插件之一。ondra-digital-health另有 8 个可设置项,按连续使用段与当日累计时长分为四档(正常、温和提醒、明确劝阻、停止),出厂不锁屏、不弹窗、不打断。
- 亮点名称:九步输出过滤链。
- 价值:解决「模型说话像客服」的问题。语气可以靠提示词模仿,但复读、旁白、客服腔、过度解释这四类特征在长对话里必然出现,提示词只能降低概率,不能清零。
- 实现方式:
ondra-filter-chain(20 个可设置项)在模型输出之后、进入对话之前逐条剥离:复读判定用 n-gram 重合率,旁白判定用括号与星号包裹的动作描述,客服腔判定用「请问还有什么可以帮您」一类的固定句式清单,过长判定用回复长度上限。每一步都是可单独关闭的开关加一个阈值,不是整体风格改写。 - 差异化:常见做法是在系统提示词里写「不要像客服」「不要说教」,属于概率性约束,且随上下文增长而衰减。本方案把约束放在输出之后,与模型无关、与上下文长度无关,可逐条关闭也可逐条加严。
- 证据:
ondra-filter-chain是话语表达组中可设置项最多的插件(20 项)。配套的ondra-lexicon(10 项)只统计最近 300 条真人消息的中位句长、无标点率与语气词频率,把结果作为约 130 字的注入块,只统计不覆盖人格——统计结果给出「这个人说话是什么样」,过滤链负责「她不要说成什么样」,两者分工不重叠。
本章记录非显而易见的设计决定。每条都说明具体做法与背后的取舍。
引文按原文比对而非相似度比对。 证据门控不使用相似度阈值判断引文是否成立,而是要求引文在原文里原样出现。相似度校验会让「他今年二十八」与「他今年三十八」通过,而数字恰恰是最不能错的部分。
超长记忆截断而不拒绝。 单条记忆正文超过上限(默认 200 字)时截断并记一条 issue,不做整条拒绝。理由写在字段说明里:拒绝等于这条记忆彻底消失,而截断至少保住了前半段。
摘要缺失时用正文替代。 模型没有返回摘要时,取正文前若干字作为摘要,不让摘要字段为空——空摘要会让这条记忆在列表视图里显示为一行空白。
上游缺席不抛错。 所有 ctx.get() 调用都按「可能取不到」处理。取不到上游插件时服务照常注册,只是标记为「不可用」。这一条让 75 个插件可以任意顺序装载,也让单独调试某一个插件成为可能。
空数组的语义逐个写明,不统一约定。 全仓 10 个结构化数组字段有两种「空」:ondra-proactive-gate 的 block-words 为空是「不额外屏蔽」,ondra-crisis 的 hotlines 为空却是「一个求助号码都不内置」——号码有地区差异且会变更,内置一份过期清单比空着更危险。同一个形状,两种含义,所以字段说明里逐条写清,不靠使用者猜;统一成「空即不限」会让危机干预那条变成假承诺。
需要外部后端的插件默认关闭。 ondra-vision 是全仓唯一 enable 默认为 false 的插件:它既读屏,又把截图发给外部视觉模型,两件事都在本机之外。默认关闭让「装好就能用」与「未经同意不读屏」同时成立;同一菜单下另外 34 个插件默认开启。
重复写入靠包含度判重,而不是靠时间窗。 同一件事第二次被记下时,ondra-memory-dedupe 按新旧两条正文的包含度决定去向:低于 0.4 视为两件事直接新建;达到 0.95 认为近乎全等,再由新颖度决定是复述还是补充(新增二元组占比 ≤ 0.25 且个数 ≤ 2 才算复述);介于两者之间按槽位覆盖线 0.45 判是不是「同一句式换了个值」。时间窗在跨天、跨设备、补记这三种情形下都不成立,包含度不依赖时钟。候选池只取最近 200 条、单次扫描上限 300 行——两两比对是 O(n²),再大就会卡。
失败归因分八类,arg_error 永不自动重试。 工具调用失败时不笼统记为「失败」,而是分到 tool_missing、arg_error、permission、timeout、network、not_found、conflict、unknown 之一。八类各有处方,但 arg_error 的重试预算在第一行就被写死为 0——参数错误重试一百次结果相同,让不知道密码的人重试一百次还是登不进去。
伤口只在下一次成功时闭合。 失败归因会为每个失败的工具「开一个伤口」,伤口只在该工具下次成功时闭合,不按超时闭合。按超时闭合会让一个一直坏着的工具看起来自愈了。五分钟未闭合的伤口标记为慢性并停止重试。
端口冲突自动让位。 ondra-port-picker(9 个可设置项)在首选端口被占用时按区间向上寻找空闲端口,找到后把实际端口回报给壳。这样同时开两个实例不会有一个静默失败。
人格内核按人设版本号缓存。 ondra-persona-core 的 cache-kernels 默认开启,缓存键包含人设版本号,换人设包即失效。关掉这一项只对「一边改人设包一边调试」有用,日常运行没有理由重算。
零常驻上下文:用指针替换常驻内容。 ondra-zero-residence(7 个可设置项)在上下文里保留的是确定性指针而非完整内容,被替换掉的字节可以从会话日志里原样恢复。这套机制解决的是长期对话里上下文被历史内容挤满的问题——常驻内容占据的 token 是每一轮都要付的。
注入去重:首轮大块 + 只推增量。 ondra-inject-dedupe(7 个可设置项)在第一轮做一次性大块注入,之后只推增量,并维护已见条目 id 的账本。上下文压缩信号出现时释放已见账本重新注入,避免压缩把关键信息一并压掉。
工具 schema 门控。 ondra-organ-gate(9 个可设置项)按意图用确定性规则决定本轮暴露哪些工具,而不是把所有工具定义一次性塞进提示词。工具定义是上下文消耗的第一大项,门控后只暴露当前意图可能用到的子集。
插件串行装载,不做并发。 75 个插件的装载是逐个 await 的,实测约 200 毫秒。并发装载会让依赖方在被依赖方还没 provide 服务时就开始初始化,产生难以复现的竞态。两百余毫秒换掉一整类偶发失败,这个交换是划算的。
器官状态累加后一次性生效。 ondra-organ-host 的 flush() 把本轮的多个状态提议按目标累加,最后一次性写入并夹取到合法区间。逐条写入会让最终结果取决于插件的注册顺序——同一个器官先注册和后注册会得到不同的状态值。
并发上限只有一个旋钮,按最稀缺的资源设。 全仓只有 ondra-memory-intake 的 maxConcurrent 暴露了并发上限,默认 1:记忆提炼每轮只需要一次模型调用,开大只会同时压满模型网关。其余插件不各自开一个并发旋钮——多个旋钮叠加会让「为什么慢」变成一道算术题。
记忆写入在同一事务内完成。 ondra-memory-core 的 appendTurn() 把原文与提炼结果写在同一个事务里,避免出现「原文写进去了但事实没提炼」或「事实提炼了但原文丢失」的中间态。
数据目录权限在创建时收紧。 ondra-path-layout 是数据根解析与目录创建的唯一入口,创建五个区域目录时一次性设为 0700,文件设为 0600。权限在创建时设定而非之后修复,避免中间存在一段任何人可读的窗口。
日志按 4MB 三轮转,同错去重。 ondra-log-zh 采用双层日志:给人看的句子与给开发者看的代码日志分开写。轮转上限 4MB × 3 轮,同一错误在短时间内重复出现时只在首次写入完整内容,后续只累加计数——这防止了一个持续报错的插件把磁盘写满。
状态文件留空即用默认值。 20 个 state-file 类配置项的默认值均为空,留空时使用内建默认路径。这些字段在设置界面里不出现,因为它们的功能是「把状态放到别处」,而绝大多数使用者不需要把状态放到别处。
依赖不重复占用项目空间。 pnpm 的内容寻址存储把所有包的真实文件放在全局 store 里,项目内的 node_modules 只有符号链接。实测项目内 6131 个文件的硬链接数全部为 1,逻辑大小与磁盘占用均为 0 字节。
管道即鉴权。 壳与宿主、宿主与内核之间通过标准输入输出通信,不经过网络。操作系统层面保证了只有父进程能读写子进程的管道,因此这条通道不需要额外的鉴权层。
HTTP 服务只监听回环地址。 宿主的本地 HTTP 服务绑定 127.0.0.1,不监听 0.0.0.0。局域网内的其他设备无法连接。
令牌在启动时生成,不落盘。 32 字节随机令牌由壳在启动宿主时生成并只经内存传递,每个请求校验 Authorization: Bearer,未授权的请求在解析请求体之前即被拒绝。响应头统一带 cache-control: no-store。
浏览器护栏拒绝内网目标。 ondra-browser(10 个可设置项)拦截在页面里执行任意代码的工具,并拒绝 file:、javascript:、data: 等协议与 127.0.0.1、localhost、*.local 等私有目标。这些是服务端请求伪造攻击的入门形态。
工具结果按预算截断并如实报数。 浏览器快照这类工具返回的内容可能达到数百 KB,护栏按 4000 字符与 80 行双重预算砍掉尾部,并在结果开头写出「原始大小是多少、下面没有展开」——不静默截断,使用者知道有内容被省略。
出站闸门做提醒而非代理。 ondra-privacy(6 个可设置项)的定位是合规告知与三层风控提醒,不做加密,也不做代理。做成代理就必须让数据流经该插件,凭据值会进入插件的内存——这会把一个隐私保护功能变成新的隐私风险点。
能力权限默认拒绝。 ondra-organ-host 的能力白名单是 default-deny 的:调用未声明的能力会抛 CapabilityDeniedError,不静默忽略。静默忽略会让插件作者以为自己调用的能力生效了。
权限档位放在输入框左下角。 权限状态是本产品最需要被看见的设置——它决定了本次对话的数据会不会出本机。把它从设置页移到输入框旁边,是为了让使用者每次发消息都能看见当前档位,而不是发完之后才想起去查。
语音输入不支持时给明确提示。 语音走 Web Speech API,在多数桌面 WebView 中不可用。不可用时按钮给出「当前环境不支持语音输入」的提示,不静默失败——静默失败会让使用者反复点按并怀疑麦克风坏了。
窗口尺寸每次启动强制复位。 主窗口默认与最小尺寸统一为 820×580,每次启动回到该尺寸,无论上次被拖成多大。理由是对话界面的阅读宽度有最优区间,宽到 2000 像素时单行过长反而难读。
设置项分两档呈现。 619 个字段中,276 项在界面里直接显示,343 项折叠在「进阶」里。单个插件的字段最多 30 项(ondra-memory-dream),全部平铺会让设置页变成一张无法浏览的表。折叠不删减功能,只是把「很少需要改的」收进第二层。
每一条设置都带说明文字。 619 个字段全部带非空 doc(100%),平均 36 字,最长 115 字。说明缺失时的兜底策略仍在:界面遇到无 doc 的字段直接显示字段键名,不编造说明。说明直接取自编译产物中的字段声明,与代码同源,因此不会出现「文档说默认开、实际默认关」的漂移。
禁用态也显示。 出厂关闭的那一个插件(ondra-vision)在插件列表里以灰点显示而非隐藏。灰点的含义是「装着但没开」,与「没装」是两件事——隐藏会让使用者误以为这些能力不存在。
关系默认 friend。 五种关系(女友、妹妹、女儿、朋友、助手)里,friend 是唯一不预设亲密度的一个。产品不替使用者决定「她跟你是什么关系」,因此把最中性的选项设为默认。
说话浓度默认 50。 浓度滑杆范围 0–100,50 表示人设包原本的说法。默认值取中间不是折中,而是「不改动人设作者的原始表达」——往两端调都是使用者的主动选择。
人格拼装预算默认 12000 字符。 实测一份人设包完整装载需 7,725 字(出厂包「哦鲸鲸」)到 10,057 字(示例包「夏柠柒」),再留约 20% 给片段与关系微调。这个默认值不是「够大就行」的整数:原来的 2600 会让「说话」段(2169 字)整段掉出提示词——她知道自己是谁,却读不到自己怎么说话,而三处装配代码都不报错。出厂模型档声明上下文窗口 1,000,000,2600 并不是被能力逼出来的数字。
重构检查两项关闭的代价被明确写出。 exactOptionalPropertyTypes 与 noUncheckedIndexedAccess 对 75 个迁入插件不生效,这两项在其余部分开启。关闭的代价是这 75 个插件的类型精确度低于本仓其余部分,这是迁入既有资产换来的,不是遗漏。
出厂人设为鲸鲸,夏柠柒为示例包。 鲸鲸是出厂人格,夏柠柒是示例人格包,可随时替换。把两者分开是为了让「换人设」这件事从第一次启动就是一等操作,而不是一个隐藏功能。
插件串行装载而非并发。 见 5.3,约 200 毫秒换掉一整类竞态。
情绪激素维度出厂关闭。 它与六态表达存在口径重叠,同开会让同一轮出现两种情绪描述。默认关闭不是因为它不好,而是因为两套描述同一件事的机制不该同时生效。
旧装配声明保留作对照。 74 份 cordis.patch.yml 保留在插件目录里,但不再被读取。装配语义已转写为 ondra-core 中的显式数组。保留旧文件的目的是对照——出问题时可以比对「旧的补丁声明说它该在哪」与「新的数组说它在哪」。比成员数少一份,因为 ondra-memory-intake 从一开始就按显式数组写,从未生成过补丁文件。
私有 scope 用依赖别名重定向。 空谷自有插件历史上把框架声明为 @ntars/cordis,该 scope 从未发布到公共 npm。本项目在 pnpm-workspace.yaml 里通过 overrides 把它指向官方 cordis@4.0.0-rc.12。这是依赖解析层面的重定向,不修改 Cordis 源码,也不改变其许可证。
时间只认外部喂进来的时刻。 ondra-study 不装定时器、不按文件时间戳推断「过了多久」,只接受外部传入的时刻。两次打点间隔超过 15 分钟即判为中断,那一段一秒都不计入。理由是常驻定时器在没人看着的时候照样走,会导致「关掉窗口八小时、回来读今天学了八小时」。
跨天清零但保留「曾经开始过」。 学伴的当日累计在跨天时归零,但保留「曾经开始过」的标记。清掉它,第二天第一次读取会报「从来没开始过」,对一个昨天学了两小时的人是假平安。
Windows 与 macOS 的界面差异被显式记录。 Windows 的安装向导窗口尺寸(410×290)无法在配置里直接设定,需要自定义 NSIS 模板。当前保持默认 NSIS 以保证 Windows 打包可用,这一差异写在已知问题里而非隐藏。
随应用分发,构成最终安装包的一部分。
| 名称 | 版本 | 许可证 | 在本项目中的用途 |
|---|---|---|---|
| Tauri | 2.12.5 | Apache-2.0 / MIT | 桌面外壳框架:窗口、WebView 注入、IPC 命令通道、打包(dmg / app / nsis) |
| tauri-plugin-opener | 2.7.2 | Apache-2.0 / MIT | 在系统默认浏览器中打开外部链接,统一处理各平台差异与权限校验 |
| tokio | 1.54.1 | MIT | Rust 异步运行时:宿主子进程启动、标准输入输出读取、超时控制、退出等待 |
| serde | 1.0.229 | Apache-2.0 / MIT | 壳与宿主之间消息的序列化与反序列化 |
| serde_json | 1.0.154 | Apache-2.0 / MIT | JSON 数据格式支持 |
| Cordis | 4.0.0-rc.12 | MIT | 宿主侧插件框架:上下文、服务注册与注入、插件生命周期与依赖排序 |
| React | 19.3.1 | MIT | 界面渲染层,不依赖任何 UI 组件库 |
| React DOM | 19.3.1 | MIT | React 的浏览器渲染实现 |
| morphicons | 2.0.2 | MIT | 界面中的形变图标组件 |
| lucide | 1.68.0 | ISC | 界面图标数据。统一 24×24 网格、1.5 描边、圆头端点,由 src/components/icons.ts 按语义包装后经 Icon.tsx 渲染 |
只在开发与构建阶段使用,不进入最终产物。
| 名称 | 版本 | 许可证 | 用途 |
|---|---|---|---|
| TypeScript | 7.0.2 | Apache-2.0 | 全部 TypeScript 代码的类型检查与向 ES 模块的编译 |
| Vite | 8.3.6 | MIT | 前端开发服务器与生产构建 |
| @vitejs/plugin-react | 6.2.1 | MIT | 为 Vite 提供 React 的 JSX 转换与热更新 |
| @tauri-apps/cli | 2.12.4 | Apache-2.0 / MIT | tauri dev 与 tauri build 命令行入口 |
| tauri-build | 2.7.4 | Apache-2.0 / MIT | 构建脚本:生成 Tauri 上下文代码、权限 schema 与打包元数据 |
| @types/node | 26.6.6 | MIT | Node.js 内置模块的类型定义 |
| @types/react | 19.3.0 | MIT | React 的类型定义 |
| @types/react-dom | 19.3.0 | MIT | React DOM 的类型定义 |
| 依赖树 | 包数 | 来源 |
|---|---|---|
| Rust crate(含传递依赖) | 442 | src-tauri/Cargo.lock |
| npm 包(含传递依赖) | 87 | pnpm-lock.yaml |
上述数字由锁文件解析得出,不是估算值。全部 529 个第三方组件的名称、版本、版权方、许可证、官方地址、在本项目中的用途与修改状态,逐项列于 NOTICE。重跑依赖后需同步更新该文件。
许可兼容性。本项目采用 AGPL-3.0,529 个第三方组件没有一个携带与之冲突的许可证。
| 许可证类型 | 组件数 | 说明 |
|---|---|---|
| MIT / Apache-2.0 及两者组合 | 448 | 绝大多数;宽松,与 AGPL-3.0 完全兼容 |
| Zlib(含多选组合) | 22 | 宽松,与 MIT 同级 |
| Unicode-3.0 | 19 | Unicode 数据表;宽松 |
| MPL-2.0 | 17 | 弱著佐权,见下 |
| BSD-3-Clause | 10 | 宽松 |
| Unlicense / 0BSD / CC0-1.0 / MIT-0 | 8 | 公有领域等价,最宽松一档 |
| ISC | 3 | 宽松 |
| LGPL-2.1-or-later,仅作多选项之一 | 2 | r-efi 另提供 MIT 与 Apache-2.0,本项目按宽松选项使用 |
| 合计 | 529 |
17 个 MPL-2.0 组件中,Rust 侧 5 个(cssparser、cssparser-macros、dtoa-short、option-ext、selectors),npm 侧 12 个(lightningcss 及其 11 个平台二进制包)。全部以未修改的发布版本引入,项目内没有改动其中任何一行的源码,因此不产生 MPL-2.0 第 3 条的衍生作品义务,也没有公开源码的义务;若将来需要修改其中任何一个,被修改的文件须以 MPL-2.0 发布。
r-efi 的许可证声明为 MIT OR Apache-2.0 OR LGPL-2.1-or-later,即三者任选其一。本项目按宽松选项使用,不触发 LGPL 条款。
没有任何依赖被修改。全部第三方组件以未修改的发布版本引入,项目内不存在 vendor/ 目录,Cargo.toml 中不存在 [patch] 段。
| 项目 | 要求 | 说明 |
|---|---|---|
| Node.js | 22 及以上 | 宿主与内核均运行于 Node 22;package.json 的 engines 字段已声明 |
| pnpm | 11.28.5 | 由 packageManager 字段固定,使用其他版本可能产生锁文件差异 |
| Rust | 1.85 及以上 | 仅编译壳时需要;只跑开发模式可不装 |
| 系统 | macOS 11+、Windows 10+ | Linux 的系统集成部分尚未适配 |
只在需要构建桌面安装包时执行。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"git clone https://github.com/ntars/Ondra.git
cd Ondra
pnpm installpnpm install 会为 cordis-host/plugins/ 下的 78 个包建立工作区链接。首次执行耗时较长,后续增量安装很快。
# 依次构建前端、插件、宿主、内核
pnpm run build
# 构建桌面安装包(需 Rust 工具链)
pnpm run build:app
# 构建并把安装包搬运到仓库根的 releases/
pnpm run release克隆后必须先构建,才能运行 check:core、smoke 或开发模式。 仓库不保存 TypeScript 编译产物:78 个包与宿主、内核的 lib/ 目录均由 tsc -b 从各自的 src/ 生成(合计 2201 个文件、约 16 MB),已在 .gitignore 中排除。而各包 package.json 的 main 指向 lib/index.js,cordis-host/check-core.mjs 也直接导入 ./plugins/ondra-core/lib/index.js。因此全新克隆的目录里 lib/ 不存在,跳过 pnpm run build 会报「Cannot find module」。
pnpm run build 的执行顺序是 build:web → build:plugins → build:host → build:sidecar,插件先于宿主,正是为了让宿主编译时能解析到各插件的 lib/。
pnpm run release 在完成 build:app(即 tauri build,已包含完整的 build 流程)之后,把 src-tauri/target/release/bundle/ 下的产物(保留 dmg/、macos/、nsis/ 子目录结构)搬运到仓库根的 releases/。releases/ 已在 .gitignore 中,不会入库。
不直接让 Tauri 输出到 releases/ 的原因:Tauri 的 bundle 位置由 Cargo 的 target 目录决定,配置文件里改不了,只能用一步拷贝集中产物。
构建期注意事项。在启用了安全删除垫片(safe-delete shim)的环境中,pnpm 收尾阶段的临时文件清理可能被判定为批量删除而中断构建。此时可临时设置 CODEBUDDY_SAFE_DELETE_ENABLED=0 绕开。该变量只影响构建期清理,不影响产物。
官网由 EdgeOne 自动部署,配置见 edgeone.json:
node tools/build-site.mjs产物输出到 .site-dist/,仅含 index.html、icon.svg、favicon.ico 与 assets/。构建脚本会剔除 .DS_Store、._*、__MACOSX 三类 macOS 元数据文件,并跳过源目录为空的情况,不在产物里留下空目录壳子。
三个进程分别启动,便于单独调试:
# 终端 1:前端(热更新)
pnpm run dev:web
# 终端 2:宿主(自动拉起内核)
pnpm run dev:host
# 终端 3:壳(加载前端并管理宿主)
pnpm run tauri dev验证三层链路是否连通:
pnpm run smoke预期输出:
宿主就绪:http://127.0.0.1:64158
通过 健康检查
通过 无令牌返回 401
通过 创建会话
通过 会话列表包含新会话
通过 模型列表可读
通过 工具列表可读
通过 未知路由返回 404
单独验证插件的装载结果,不依赖 HTTP 服务:
pnpm run check:core预期输出:
=== 空谷核心装配结果 ===
声明成员数:75
启用名单: 75
成功装载: 75
跳过: 0
耗时: 200 ms
有跳过项时逐条打印原因,并以非零退出码结束,可直接用作持续集成的判据。
内核可脱离宿主独立验证:
cd dsh-sidecar
node lib/index.js然后向标准输入逐行输入 JSON-RPC 请求:
{"jsonrpc":"2.0","id":1,"method":"kernel.handshake","params":{"protocolVersion":"1.0","client":"host","clientVersion":"2.1.2"}}
{"jsonrpc":"2.0","id":2,"method":"kernel.ping"}
{"jsonrpc":"2.0","id":3,"method":"session.create","params":{"title":"测试"}}tools/prepare-plugins.mjs 负责为插件生成统一的 package.json 与 tsconfig.json。该脚本是幂等的,反复运行结果相同:
pnpm run prepare:plugins # 处理全部插件
pnpm run prepare:plugins -- ondra-clock # 只处理指定插件手工新增插件的完整步骤:
- 在
cordis-host/plugins/下建目录,放入src/index.ts,导出name与apply。 - 运行
pnpm run prepare:plugins -- <插件名>生成构建配置。 - 在
cordis-host/plugins/ondra-core/src/index.ts里加一行 import 和一个成员条目,并把MIGRATED_PLUGIN_COUNT加一。 - 运行
pnpm install,让 pnpm 建立新包的链接与依赖。 - 运行
pnpm run check:core,确认装载数增加且无跳过。
第 3 步不能省。ondra-core 把全部插件声明为 dependencies,漏掉的话 TypeScript 会直接报「找不到模块」,不会等到运行时才发现。
设置项分三层:环境变量控制进程如何启动,工作区配置控制依赖如何解析,插件的配置声明控制每个功能如何工作。第三层对应 619 个字段,已在第三章逐个列出。
| 变量 | 作用 | 默认值 |
|---|---|---|
ONDRA_HOST_ENTRY |
宿主入口脚本绝对路径 | cordis-host/lib/index.js |
ONDRA_NODE |
Node 可执行文件路径 | 自动探测 |
ONDRA_SIDECAR_ENTRY |
内核入口脚本绝对路径 | dsh-sidecar/lib/index.js |
CODEBUDDY_SAFE_DELETE_ENABLED |
构建期安全删除垫片开关 | 未设置(即启用) |
pnpm-workspace.yaml 中的关键设置:
packages:src、cordis-host、cordis-host/plugins/*(含 78 个包)、dsh-sidecar为工作区成员。src-tauri由 Cargo 管理,不进 pnpm 工作区。overrides:@ntars/cordis别名指向官方cordis@4.0.0-rc.12。旧插件一律peerDepend到私有 scope,该 scope 未发布到公共 npm,而官方包是其唯一上游实现。
pnpm 生成的 node_modules 分两层,理解这一点能解释「为什么依赖看起来在项目里,实际并不占项目空间」:
| 层 | 位置 | 内容 | 是否占项目体积 |
|---|---|---|---|
| 内容寻址存储 | ~/Library/pnpm/store/ |
全部包的真实文件,全局共享 | 否 |
| 虚拟存储 | <项目>/node_modules/.pnpm/ |
指向上述存储的符号链接 | 否 |
真实文件在系统级 store 里,项目内只有链接。当前仓库的 node_modules 目录 du 报 89 MB,主要是 @tauri-apps/cli 的 macOS 原生二进制(单个文件约 14 MB),它是开发期工具,不进最终安装包。
实测数据:node_modules 下 6131 个文件的硬链接数全部为 1,逻辑大小与磁盘占用均为 0 字节,说明文件内容与 store 共享、项目内不重复占用。
不能把 node_modules 完全移出项目的原因:Node 的模块解析从脚本所在目录逐级向上查找 node_modules,这是运行时约定,不是 pnpm 的选择。移动它需要 NODE_PATH 或 loader 钩子,会让 IDE 跳转、调试器断点和 tsc 的模块解析同时失真。
产物目录同样不入库:node_modules/、dist/、target/、.site-dist/、src-tauri/target/、releases/ 全部在 .gitignore 中。
src-tauri/tauri.conf.json 中的关键项:
app.windows:主窗口main的width/height与minWidth/minHeight统一为820/580。除 Windows 安装界面外,所有界面都是这个尺寸,且它就是最小尺寸。- 启动强制尺寸:
src-tauri/src/main.rs的setup里对main窗口再次调用set_size(820, 580)与set_min_size(820, 580),兜底「无论上次被拖成多大,每次启动都回到该尺寸」。 build.frontendDist:../dist,与 Vite 的outDir一致。app.security.csp:default-src 'self',connect-src仅放行127.0.0.1的 HTTP 与 WebSocket。bundle.resources:把cordis-host/lib与dsh-sidecar/lib打进安装包。tauri.windows.conf.json(Windows 平台覆盖):Tauri 会按平台读一份同名配置并与主配置做 JSON Merge Patch 合并。这里只有一件事:app.windows[0].decorations = false。原因见下一段。注意数组是整体替换,所以这份文件必须把label/width/height/minWidth/minHeight/backgroundColor全部重写一遍,漏一个就回退成 Tauri 默认值。
为什么 Windows 必须关掉系统装饰:titleBarStyle: "Overlay" 是 macOS 专属字段,Windows 读它等于没写,系统照旧画一条原生标题栏;而 TitleBar.tsx 在 Windows 上又渲染了一套自绘三键(WindowControls),于是窗口顶部出现两排按钮。关掉 decorations 之后自绘才真正接管:无边框窗口在 tao 里仍保留 WS_OVERLAPPEDWINDOW 的其余样式位(最大化不会盖住任务栏),边缘拖拽缩放由 WM_NCHITTEST 单独处理,整条标题栏的拖拽靠 data-tauri-drag-region。
配套的布局约束:.titlebar__controls 是 position: absolute(不占文档流),所以版本号必须自己让出宽度——layout.css 里 :root[data-platform='win32'] .titlebar__version { margin-right: 146px }(138 三键 + 8 呼吸)。少了这一条,版本号会被 × 直接压住。
- Windows 安装界面尺寸(410×290):macOS 无需安装界面(
.dmg由系统提供)。Windows 的 NSIS 安装向导默认窗口尺寸无法在tauri.conf.json里直接设成任意像素;要精确做到 410×290,需提供自定义 NSIS 模板并通过bundle.windows.nsis.template指向它。做法:先在 Windows 上跑一次tauri build,从生成的target/.../bundle/nsis/*.nsi取出 Tauri 生成的完整脚本,按 MUI 对话框尺寸改造为 410×290,再回填到配置。当前保持默认 NSIS 以保证 Windows 打包可用,该精确尺寸待 Windows 侧构建验证。
src-tauri/capabilities/default.json 只开放主窗口的最小必要权限:核心默认能力、打开外部链接(限 http 与 https)。前端不直接持有任何本机能力,选文件、开链接一律经宿主中转。
所有请求需带 Authorization: Bearer <token>。未授权的请求在解析请求体之前即被拒绝。
宿主共注册 46 条路由(28 个 GET、18 个 POST),下表只列最常用的几条。完整清单在 cordis-host/index.ts 的 routes.set() 调用处——路由表是显式的,没有通配兜底,因此拼错的路由在编译期就能被发现。
| 方法 | 路径 | 说明 | 响应 |
|---|---|---|---|
| GET | /api/health |
健康检查 | { ok, at } |
| GET | /api/sessions |
会话列表 | SessionMeta[] |
| POST | /api/sessions |
创建会话 | SessionMeta,状态 201 |
| GET | /api/sessions/:id |
会话详情 | SessionMeta |
| GET | /api/models |
可用模型 | ModelDescriptor[] |
| GET | /api/tools |
已注册工具 | ToolDescriptor[] |
| POST | /api/proactive/check |
判一次「此刻能不能主动开口」(九道闸归宿主) | ProactiveVerdict |
| POST | /api/proactive/decline |
用户点了「这次不用」,宿主记一次拒绝 | { ok } |
内核的 18 个方法通过 JSON-RPC 2.0 调用,按 NDJSON 分帧。错误码沿用 JSON-RPC 2.0 规范定义的 -32700 至 -32603,本项目另在 -32000 至 -32004 定义内核专属码。
| 域 | 方法 | 参数 | 说明 |
|---|---|---|---|
| 生命周期 | kernel.handshake |
protocolVersion、client、clientVersion |
版本协商,返回能力清单 |
kernel.ping |
无 | 存活探测 | |
kernel.shutdown |
无 | 优雅关闭 | |
| 会话 | session.create |
title |
创建会话 |
session.get |
id |
读取会话元信息 | |
session.list |
includeArchived |
列出会话 | |
session.archive |
id |
归档,幂等 | |
| 推理 | inference.run |
sessionId、input(可省)、nudge(可省) |
发起一轮对话,流式推送增量。input 省略即「她主动开口」,nudge 是不落盘的临时系统提示 |
inference.abort |
sessionId |
中断当前轮次,幂等 | |
| 工具 | tool.list |
无 | 列出已注册工具 |
tool.invoke |
name、arguments |
调用单个工具,执行前校验参数 | |
| 模型 | model.list |
无 | 列出可用模型 |
model.select |
id |
切换当前模型 |
| 路径 | 职责 |
|---|---|
dsh-sidecar/src/protocol.ts |
协议唯一真源:方法名、事件名、错误码、类型守卫 |
dsh-sidecar/src/rpc/transport.ts |
NDJSON 分帧,按行切割消息 |
dsh-sidecar/src/rpc/dispatcher.ts |
请求分发、错误翻译、事件推送 |
dsh-sidecar/src/kernel/types.ts |
四类职责的公共类型 |
dsh-sidecar/src/kernel/sessions.ts |
会话存储 |
dsh-sidecar/src/kernel/models.ts |
模型适配器 |
dsh-sidecar/src/kernel/tools.ts |
工具注册与参数校验 |
dsh-sidecar/src/kernel/index.ts |
装配四类职责到协议方法 |
cordis-host/index.ts |
宿主入口:装配插件、启动服务、报告就绪 |
cordis-host/check-core.mjs |
核心装配核对,逐项验证成员装载 |
cordis-host/src/server.ts |
本地 HTTP 服务与令牌鉴权 |
cordis-host/plugins/ondra-core/src/index.ts |
空谷核心成员清单(75 行 import 与成员数组) |
cordis-host/plugins/ondra-core/src/types.ts |
装配契约:CoreMember、CoreLoadResult |
cordis-host/plugins/dsh-client/ |
内核子进程生命周期与请求配对 |
cordis-host/plugins/desktop-bridge/ |
桌面能力桥 |
cordis-host/plugins/ondra-*/ |
78 个包(75 个可装载插件 + 清单 + 2 个桥接层),各自独立成包 |
tools/prepare-plugins.mjs |
为迁入插件生成统一的 package.json 与 tsconfig.json |
tools/build-site.mjs |
官网静态文件收集与 macOS 元数据剔除 |
tools/version.mjs |
版本号七处同步;--minor 时提交版本文件、打 tag 并推送 |
tools/smoke.mjs |
三层链路冒烟测试 |
tools/check-ui.mjs |
UI 规范断言:尺寸、层级、令牌定义、关闭框与错误分级 |
tools/check-installer.mjs |
Windows 安装脚本语法检查:渲染占位符后用 makensis 真编译一遍 |
tools/build-tray-icons.mjs |
从 icon.svg 渲染托盘剪影三档(16 / 32 / 64),无底板、带 alpha |
src-tauri/src/main.rs |
壳入口:窗口、事件、退出清理 |
src-tauri/src/host.rs |
宿主进程管理:启动、等待、停止 |
src-tauri/src/commands.rs |
本机能力命令 |
src-tauri/src/tray.rs |
托盘图标与菜单:打开、今天、静音(文案随状态在「静音 1 小时」与「取消静音」间切换)、退出 |
src-tauri/src/window.rs |
关闭拦截:隐藏到托盘 / 退出,偏好落盘,无托盘时降级最小化 |
src-tauri/src/browser.rs |
内置只读浏览器:官网、仓库、更新日志、许可在窗内打开,不出应用 |
src-tauri/installer/installer.nsi |
Windows 安装器脚本:三页流程、410 × 290、DPI 自适应 |
src/lib/host.ts |
前端与宿主通信的唯一入口 |
src/lib/shell.ts |
前端与壳通信的薄封装 |
src/lib/api.ts |
业务接口封装 |
每个插件在自己的 src/settings.ts 里声明配置,声明是唯一真源,界面与文档都从这里取值。字段形态为一个对象,键是配置键,值包含以下成员:
| 成员 | 类型 | 作用 |
|---|---|---|
kind |
boolean / number / string / json / enum |
数据类型,决定界面用哪种控件 |
default |
与 kind 对应 |
出厂值,界面「恢复默认」取这个值 |
values 或 options |
字符串数组 | 枚举候选值,两种写法等价,同一个字段只用其中一种 |
min / max |
数值 | 数值型字段的上下限,界面据此限制滑杆范围 |
advancedOnly |
布尔 | 是否折叠进「进阶」层 |
secret |
布尔 | 是否为敏感值,界面内以掩码显示 |
doc |
字符串 | 面向使用者的说明,直接渲染在字段旁 |
doc 与代码同源,因此不会出现文档与实际行为不一致的情况。本项目 619 个字段全部带有 doc,平均 36 字。
Ondra/
├── README.md 中文主文档
├── README.en.md 英文完整版
├── NOTICE 第三方依赖与许可声明
├── docs/
│ ├── PLUGINS.md 78 个包的字段明细(中文)
│ ├── PLUGINS_EN.md 78 个包的字段明细(英文)
│ ├── TEST-REPORT-1.2.0.md 结构化测试报告
│ └── releases/ 各版本变更正文源文件(发布流程读取)
├── LICENSE AGPL-3.0 全文
├── package.json 仓库元信息与脚本
├── pnpm-workspace.yaml 工作区成员与依赖覆盖
├── tsconfig.base.json 共享 TypeScript 配置
├── edgeone.json EdgeOne 部署配置
├── index.html 官网首页
├── icon.svg 应用图标唯一真源
├── favicon.ico 站点图标
├── assets/ 品牌图像资产
├── .github/workflows/ CI:release.yml 双平台构建并发布发行版
├── tools/
│ ├── build-site.mjs 官网静态文件收集
│ ├── prepare-plugins.mjs 为迁入插件生成统一的构建配置
│ ├── collect-runtime.mjs 收集运行时文件
│ ├── bundle-release.mjs 把安装包搬运到 releases/
│ ├── version.mjs 版本号七处同步,--minor 时提交并打 tag
│ └── smoke.mjs 三层链路冒烟测试
├── src/ 前端 UI(React 19 + Vite 8)
│ ├── index.html 单份入口,只有一棵树
│ ├── main.tsx 装好全局前置后挂载 App,无路由
│ ├── App.tsx 主界面根组件:装配界面区块与全局快捷键
│ ├── components/ 界面组件
│ │ ├── TitleBar.tsx 自绘标题栏:人设头像与切换浮层、三档在线状态、窗口按钮
│ │ ├── Avatar.tsx 头像:默认画 icon.svg 同一份几何,自定义图裁成 96 见方
│ │ ├── TopBar.tsx 手机顶栏:汉堡开抽屉、标题、心情状态点、2px 水位线
│ │ ├── Rail.tsx 最左侧图标栏:会话 / 日志 / 扩展三个入口与底部齿轮
│ │ ├── Spine.tsx 右侧脊:一条 6 × 150 水位加三个读数,高度是精力、颜色是心情
│ │ ├── Inspector.tsx 侧写检查器:此刻 → 关系阶段 → 记得的事 → 当前活动
│ │ ├── Drawer.tsx 抽屉容器:会话 / 日志 / 扩展三种内容共用,原地替换
│ │ ├── SessionStats.tsx 会话抽屉:轮数 / 总字数 / 开始于 / 已归档四读数与「清空全部」
│ │ ├── LogDrawer.tsx 日志抽屉:会话日志 / 通道日志 / 系统日志三段
│ │ ├── ExtensionDrawer.tsx 扩展抽屉:三段式包清单(桥接 / 内核 / 能力包)与配置入口
│ │ ├── Timeline.tsx 对话阅读流:消息、引用、复制
│ │ ├── RichText.tsx 正文排版渲染:标题、列表、表格、代码、引用,全程不碰 innerHTML
│ │ ├── Composer.tsx 输入框:附件、拖放、发送方式
│ │ ├── Settings.tsx 设置面板壳:左导航 + 右内容,一个入口一个面板
│ │ ├── SettingsGeneral.tsx 通用页:主题 / 字号 / 语言 / 发送方式 / 系统通知 / 默认权限 / 音效 / 语音输入
│ │ ├── SettingsProviders.tsx 模型与 API 页:官方预设说明与自定义 OpenAI 兼容端点
│ │ ├── SettingsPersona.tsx 人物设定页:朗读 / 口吻 / 节奏 / 规矩 / 知识库
│ │ ├── SettingsData.tsx 备份与还原页:会话导出与导入
│ │ ├── SettingsBackup.tsx 备份与还原页:整库快照与回滚
│ │ ├── SettingsPlugins.tsx 扩展功能页:场面 / 模块两层插件参数(原「原始」层已删)
│ │ ├── SceneLayer.tsx 第一层:按场面填参数,预演写入
│ │ ├── ModuleLayer.tsx 第二层:按模块分组的字段表单
│ │ ├── SettingsAbout.tsx 关于页:版本 / 更新
│ │ ├── SettingsPrivacy.tsx 关于页:数据存放 / 联网范围 / 离线能力
│ │ ├── SettingsPermissions.tsx 关于页:应用权限
│ │ ├── settingsUi.tsx 设置表单原语:单选卡片 / 开关 / 行 / 说明
│ │ ├── Dialog.tsx 确认对话框,焦点收束与还原
│ │ ├── Toast.tsx 提示条:成功 / 警告 / 错误三级
│ │ ├── ScrollArea.tsx 自绘滚动容器,统一 overlay 滚动条
│ │ ├── Icon.tsx 图标唯一出口(morphicons),组件只认名字
│ │ ├── icons.ts 命名图标的重导出,数据可摇树,渲染仍归 Icon.tsx
│ │ ├── SectionFields.tsx 卡片下方原地展开整段字段,不再折叠
│ │ ├── SettingsKnowledge.tsx 人物设定·知识库页:世界书条目管理
│ │ ├── MemoryHealth.tsx 记忆健康:待补、将忘、可修三档读数
│ │ ├── DreamBlock.tsx 梦境整理:昨夜合并与遗忘的摘要块
│ │ ├── SnapshotBlock.tsx 快照块:整库备份的时间线与回滚入口
│ │ ├── QueueDock.tsx 队列坞:待发消息的排队与取消
│ │ ├── FilterCard.tsx 过滤链卡片:九步删改的开关与预览
│ │ ├── OralCard.tsx 口语化卡片:口头禅池与标点习惯
│ │ ├── RhythmCard.tsx 节奏卡片:分条数、字数上限、停顿区间
│ │ ├── VoiceCard.tsx 声音卡片:音色、语速、音量
│ │ ├── ReasonLine.tsx 主动开口的理由行:九道闸哪一道拦下的
│ │ └── Mark.tsx 品牌标记组件,图形取自 brand/mark.ts
│ ├── lib/
│ │ ├── api.ts 业务接口与共享类型
│ │ ├── host.ts 与宿主通信的唯一入口
│ │ ├── shell.ts 与壳通信的薄封装
│ │ ├── useAppState.ts 主界面状态
│ │ ├── persona.ts 人设常量与三档在线状态词
│ │ ├── usePersona.ts 人设单例:清单、当前项、热切换、在线判据
│ │ ├── preferences.ts 偏好真源:读盘、应用、跨窗口广播
│ │ ├── mute.ts 一键静音:截止时刻、到期自愈、托盘文案同步
│ │ ├── sound.ts 提示音:三种音现场合成、音量独立、受静音管
│ │ ├── absence.ts 离开时长:10 分钟阈值、人话换算、归来留一行
│ │ ├── paste.ts 粘贴:图片即附件、缩略图 48 见方、字数上限单一真源
│ │ ├── richText.ts 正文解析:Markdown 转结构体,链接白名单、递归限深
│ │ ├── settings.ts 设置读写
│ │ ├── settingTypes.ts 设置字段与分段的类型
│ │ ├── scenes.ts 场面定义与写入推导
│ │ ├── shortcuts.ts 快捷键表与按键上下文判断
│ │ ├── platform.ts 平台判断唯一真源:写 data-platform / data-input,含 linux 分支
│ │ ├── types.ts 记忆、羁绊、心情等业务类型
│ │ ├── moodColor.ts 水位色 / 心情词 / 精力比例:顶栏与右脊共用的单一真源
│ │ ├── viewport.ts 软键盘遮挡高度写进 --kb-h(读 visualViewport,不用 innerHeight)
│ │ ├── wakelock.ts 生成中保持屏幕常亮,结束即释放,回前台补申请
│ │ ├── useRestoreFocus.ts 焦点还原:关闭浮层后回到触发元素
│ │ └── useResolvedTheme.ts 把主题模式解析成 light / dark
│ ├── styles/ 全站样式
│ │ ├── tokens.css 设计令牌唯一真源(189 个)
│ │ ├── global.css 样式顺序清单,真正的 import 在 main.tsx
│ │ ├── base.css 全局基础样式:重置与不可见规则
│ │ ├── compact.css 移动端与触屏:顶栏、单列骨架、热区 44、字号分档、安全区
│ │ └── *.css 每个组件一份,与组件同名
│ ├── assets/fonts/ 界面字体 MiSans VF(含许可证)
│ ├── brand/mark.ts 品牌标记图形数据,由 icon.svg 自动生成
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── cordis-host/ Cordis 宿主(Node 22 + TypeScript)
│ ├── index.ts 启动入口
│ ├── check-core.mjs 核心装配核对
│ ├── src/
│ │ └── server.ts 本地 HTTP 服务
│ └── plugins/
│ ├── ondra-core/ 空谷核心装配(成员清单在这里)
│ ├── desktop-bridge/ 桥接壳能力
│ ├── dsh-client/ 封装 JSON-RPC 通信
│ └── ondra-*/ 78 个包:75 个成员 + ondra-core 与两个桥接层
│ ├── src/ 源码(含 settings.ts 配置声明)
│ ├── lib/ 编译产物
│ ├── locale/ 界面文案(部分插件有)
│ ├── icon.svg 插件图标
│ └── cordis.patch.yml 旧装配声明(保留作对照,不再被读取)
├── dsh-sidecar/ dsh 内核(Node 22 + TypeScript)
│ ├── src/
│ │ ├── index.ts 进程入口
│ │ ├── protocol.ts 协议唯一真源
│ │ ├── rpc/
│ │ │ ├── transport.ts NDJSON 分帧
│ │ │ └── dispatcher.ts 请求分发
│ │ └── kernel/
│ │ ├── index.ts 装配
│ │ ├── types.ts 公共类型
│ │ ├── sessions.ts 会话存储
│ │ ├── models.ts 模型适配
│ │ └── tools.ts 工具注册
│ ├── package.json
│ └── tsconfig.json
└── src-tauri/ Tauri 壳(Rust 2024)
├── Cargo.toml Rust 依赖
├── Cargo.lock Rust 依赖锁定
├── build.rs 构建脚本
├── tauri.conf.json 应用配置
├── tauri.windows.conf.json Windows 平台覆盖(关系统装饰)
├── capabilities/
│ └── default.json 两个窗口的默认能力集
├── icons/ 应用图标各尺寸
├── installer/
│ └── installer.nsi Windows 安装器脚本
└── src/
├── main.rs 入口:窗口、事件、退出清理
├── host.rs 宿主进程管理:启动、等待、停止
├── commands.rs 本机能力命令
├── browser.rs 内置只读浏览器:官网、许可、更新日志在窗内打开
├── tray.rs 托盘图标与菜单
└── window.rs 关闭拦截:隐藏到托盘 / 退出
以下数据为 Apple Silicon macOS、Node 22.22.2 环境实测。
| 指标 | 数值 | 测量方式 |
|---|---|---|
| 前端产物 JS | 365.86 KB(gzip 118.80 KB) | vite build 输出,两份 chunk 合计 |
| 前端产物 CSS | 70.74 KB(gzip 11.49 KB) | vite build 输出 |
| 前端构建耗时 | 约 2.1 秒 | vite build 输出的 built in |
| 插件装载(75 项) | 75/75,0 跳过,约 200 毫秒 | pnpm run check:core |
| 内核冷启动到握手可用 | 小于 500 毫秒 | 冒烟测试观察 |
| 宿主启动到上报就绪 | 小于 2 秒 | 冒烟测试观察 |
| 冒烟测试全流程 | 小于 5 秒 | 含 7 项接口验证与两次进程退出 |
CSS 从 6 KB 涨到 71 KB 是界面重写的必然代价,不是泄漏。 旧界面是十几个组件共用一份样式;新界面按 §5 拆成 20 个组件样式文件,还带上了完整的设计令牌层(189 个令牌)、移动端与触屏批(compact.css 459 行)、八页偏好设置与打印排版。gzip 后 11.49 KB,比旧版的原始大小还小——这条曲线只说明源文件变多,不说明传输变重。
JS 365 KB 里有 12.65 KB 是懒加载的 region chunk,不进入首屏关键路径。首屏那个 chunk 是 361.98 KB(gzip 117.09 KB)。
装载耗时为何是串行累加而非并行。 75 个插件的装载是逐个 await 的,不做并发。并发装载会让依赖方在被依赖方还没 provide 服务时就开始初始化,出现「服务不存在」的偶发失败。实测约 200 毫秒的代价换掉一整类难以复现的竞态,这个交换是划算的。
上下文消耗的两项实测约束。 工具定义与原始工具输出是上下文消耗的前两大项。ondra-organ-gate 按意图决定本轮暴露哪些工具,ondra-browser 把页面快照按 4000 字符与 80 行双重预算截断。这两处不在本机运行时体现为耗时,而体现为模型调用的 token 成本与响应质量。
- 宿主 HTTP 服务只监听
127.0.0.1,不监听0.0.0.0,本机之外无法访问。 - 内核不监听任何端口,只通过管道与宿主通信,本机其他进程无法连接。
- 界面与宿主之间的
connect-src由 CSP 限制为127.0.0.1。 ondra-browser拒绝127.0.0.1、localhost、*.local等私有目标,阻断服务端请求伪造的入门形态。
- 令牌在宿主启动时以 32 字节随机数生成,经壳传入页面,不落盘。
- 每个请求校验
Authorization: Bearer <token>,未授权请求在解析请求体之前即被拒绝。 - 响应头统一带
cache-control: no-store,令牌与会话数据不进入任何缓存。
- 前端不直接拥有本机能力。选文件、开链接一律经宿主中转。
- 外部链接只允许
http:与https:,file:与自定义协议被拒绝。 - Tauri 能力权限按窗口配置,只开放核心默认能力与受限的外部链接打开。
ondra-organ-host的能力白名单 default-deny:调用未声明的能力抛CapabilityDeniedError,不静默忽略。
- 无遥测、无崩溃上报、无使用统计。项目内不存在任何指向第三方分析服务的代码。
- 数据目录在创建时即收紧权限:目录 0700,文件 0600。
- 没有账号体系,数据不跨设备同步,备份由使用者手动完成。
- 没有社区、没有广场、没有分享按钮,对话只属于使用者与本机。
- 存储层失败时降级到内存并明确声明「不持久化」,不静默丢弃。
- 图形界面操作、电脑只读、手机只读、adb 短信、浏览器护栏、Minecraft、视觉七项能力出厂关闭,需手动开启。
- 出站闸门做提醒与告知,不做代理。让它代理就必须让数据流经该插件,凭据值会进入插件内存,反而制造新的风险点。
| 平台 | 状态 | 说明 |
|---|---|---|
| ① macOS 11+(Apple Silicon) | 已发布 | 完整功能,构建与打包均在此平台验证 |
| ① macOS 11+(Intel) | 支持 | 完整功能 |
| ② Windows 10+ | 已发布 | 安装包可构建;文件选择对话框尚未实现 |
| ③ Linux(deb/rpm/AppImage) | 未验证 | 代码同源,需补系统集成配置 |
| ④ iPad(iPadOS 15+) | 未开始 | 需新增 iOS 构建 |
| ⑤ iPhone(iOS 15+) | 未开始 | 需新增 iOS 构建 |
| ⑥ Android 8+(arm64) | 已发布 | 单进程形态,不跑 Node;75 个插件打包进 WebView,见下 |
| ⑦ 鸿蒙 NEXT | 未开始 | 无现成方案,需先确定 ArkTS 侧的宿主通信路径 |
| ⑧ 命令行与 Web | 部分可用 | 有壳时可远程连接宿主;需补一项能力标记 |
| ⑨ dsh 内核 | 已存在 | 内核进程端,独立可替换,无界面形态 |
安卓端为什么是另一种形态。宿主是 Node 进程(约 108 MB),由壳以子进程拉起,通过 stdio 与本地 HTTP 通信。iOS 沙盒禁止应用创建子进程,也禁止 JIT;Android 虽允许子进程,但把 108 MB 运行时随包分发与「安装即用」的预期冲突。
所以安卓端走单进程形态:宿主逻辑搬进 WebView,用 IndexedDB 替代 SQLite,推理直连用户填的接口。src/lib/local/ 下承担这件事。
界面零改动:request() 与 requestStream() 是全部网络出口,在那里加一层分发即可。26 个端点走本地分发,其余返回空结构而不是 404——后者会让「扩展功能」页显示「读不到插件」,看起来像坏了。
早期版本在 src/lib/local/ 里手工复刻了情绪与记忆的纯函数(emotionPad.ts、memoryStability.ts 等,逐字从插件源码抄来)。那套做法的问题是第二份真源:能抄的只有无状态纯函数,记忆链、人设链、提示词装配那些有状态的插件根本抄不过来。
现在改成把插件本体打包进前端:
cordis是纯 ESM、零 Node 依赖,在 WebView 里能直接跑- 30 个插件
import了node:*,由src/lib/browser/下的替身顶上:fs(内存镜像 + IndexedDB 落盘)、path、os、crypto(自带 SHA-256 / MD5)、zlib(自带 inflate)、url、module、process node:sqlite的替身故意加载失败,让ondra-store-sqlite走它自己的降级链(ondra-store-json→ondra-store-memory),那条路径插件从设计之初就写好了vite.config.ts里用正则别名把node:*与 75 个插件包接到源码位置,只在ONDRA_LOCAL=1时生效
实测 75/76 个插件在安卓 WebView 里装载成功(pnpm check:mobile-plugins 守着)。唯一装不上的需要 node:net 起端口探测,在 WebView 里没有对应物。
包体切分:插件被打成独立的 plugins-*.js(1.47 MB / gzip 472 KB),只在手机端构建时产出。桌面端走真宿主,那个 chunk 永远不会被请求,桌面前端仍是 492 KB。
复刻版仍然保留,但降级为兜底:插件取不到服务时才走它。两份的一致性由 pnpm check:local-parity 逐行比对(130 + 249 行),改一边忘了改另一边就红。
检索走 2-gram 倒排:没有 FTS5 时的既定分支,插件日志里明写「本库不支持 FTS5 时必须走 2-gram 倒排分支,不得启用 BM25 分支」。
代价要说清楚:手机上没有 FTS5 的 BM25 排序,记忆检索精度略低于桌面;没有子进程,ondra-process-host 与 ondra-port-picker 装不上;ondra-tts / ondra-vision 依赖本机可执行文件,在手机上走「不可用」分支。这些是形态决定的取舍,不是待修缺陷。
为什么 iOS 仍然不做。iOS 连子进程都没有,也没有可行的 WebView 内 Node 替代方案;WebView 的 JIT 限制让纯前端推理层性能不可接受。这条路要等一个能在 iOS 上跑的计算层,目前没有。
浏览器开发态(pnpm run dev:web)下没有壳,本机能力调用会明确报错而非静默失败。
Node 版本。宿主与内核依赖 Node 22 的特性,package.json 的 engines 字段声明为 >=22。使用 Node 20 及以下会在启动时失败。
格式兼容。人设包格式为 .skill.zip,内含 meta.json 与 SKILL.md。该格式与本项目 1.x 版本兼容,旧人设包可直接使用。
- 模型适配器仅覆盖 OpenAI 兼容协议。内核的
ChatAdapter只实现/chat/completions这一套接口,供应商差异全部表达在配置里(baseUrl+model+apiKey)。出厂不预置任何供应商,首次启动只有local/echo离线回显模型——它不联网、不产生智能,但会把「会话落盘 → 流式推送 → 界面逐字显示」整条链路走一遍,让「装好了、链路是通的」这件事可被直接看到。真实模型需在「设置 → 模型与 API」里逐个添加。早期版本曾预置一条指向http://127.0.0.1:3425/v1的本地网关,那在开发机上可用、在用户机器上不存在,导致第一句话必然fetch failed;现已移除,并带一次性迁移清掉旧配置里的残留项。 - 工具注册表为空。骨架未注册任何可被模型调用的工具。注意这与 75 个插件的装载是两件事:插件提供的是应用能力(记忆、情绪、表达、装配),
tool.*指的是可被模型调用的工具,后者尚未接入。 - 壳命令通道尚未完全接通。
desktop-bridge的invoke在宿主侧尚未连接到 Tauri 的自定义命令端点,调用会返回明确错误。 - Windows 文件选择未实现。
pick_files在非 macOS 平台返回「尚未实现」。 - 托盘图标与菜单已接入,但未在 Windows / Linux 真机验证。托盘由
src-tauri/src/tray.rs创建,三档图标(16 / 32 / 64)由tools/build-tray-icons.mjs从icon.svg渲染,macOS 上按模板图染色。macOS 侧已实测:菜单四项齐全、单击唤回窗口、关闭拦截与「隐藏到托盘」生效。Windows 与 Linux 只做过代码复核。 - 单窗口形态。主窗口关闭即隐藏到托盘,不退出。设置是主窗口上的一张浮层(不是第二个窗口),关闭浮层即回到会话。
- 无自动更新。更新流程尚未实现。
- 插件配置已接入界面,但只覆盖三层视图。设置面板分「场景 / 模块 / 原始」三层呈现插件的 619 个字段;扩展抽屉把 78 个包按职责收成三段(桌面桥接 / 内核客户端 / 能力包),不提供逐包开关——三段都是核心组件,缺一段应用就跑不起来,开关能力只在宿主侧保留作诊断。未被任何一级菜单归类的边角字段只能在「原始」层按 JSON 直接改。
- 两项严格类型检查对迁入插件不生效。
exactOptionalPropertyTypes与noUncheckedIndexedAccess只在cordis-host、dsh-sidecar、src与新写的三个插件上启用。75 个迁入插件关闭了这两项,理由与代价见 4.1 与 5.8。 - Windows 安装界面精确 410×290 待真机验证。NSIS 默认窗口尺寸无法在配置里直接设成任意像素,自定义模板已在
src-tauri/installer/installer.nsi实现:guiInit里用SetWindowPos设框,GetDpiForWindow按 96 DPI 基准等比换算(旧系统回退 96),check:acceptance有静态断言守着这两个数字。缺的只是在 Windows 上真装一次,见 15.1 未验证清单。 - 语音输入依赖系统能力。语音走 Web Speech API,在多数桌面 WebView 中可能不可用,此时按钮给出「当前环境不支持语音输入」提示。
- 安装包自带运行时,但 Windows 上未做真机启动验证。
build:runtime会把 Node 二进制、宿主与内核的node_modules一并收进src-tauri/runtime/,再由bundle.resources打进安装包;commands.rs按平台找Resources/node/bin/node(Windows 为同级的node/bin/node.exe)。macOS 已实测可双击运行;Windows 的路径查找与启动只在代码层核对过,缺一次真机确认。 - dmg 由
hdiutil手动生成。Tauri 2.12.4 自带的bundle_dmg.sh在较新 macOS 上因grep正则未转义方括号而解析hdiutil输出失败,dmg 创建中断。.app不受影响;release流程改为在 Tauri 打包出.app后用hdiutil create -srcfolder直接封装 dmg,绕开该脚本缺陷。 - 安装包未签名、未公证。macOS 是链接器 ad-hoc 签名(
codesign -dv报flags=0x20002(adhoc,linker-signed),TeamIdentifier未设置),Windows 没有代码签名证书,Android 是 debug 签名(keystore 走 secrets 注入,apksigner校验通过但不构成正式分发签名)。首次打开会被系统拦下, 绕行办法写在 release 正文里。签名与公证属于正式版阶段的工作, 不在内测版的取舍范围内。 - 打进包里的 Node 运行时取自构建机,不是一个钉死的版本。
build:runtime直接复制process.execPath,所以云端构建的包带 Node 24.9.0、本机构建的 带 22.22.2,两者都满足engines: { node: ">=22" }。这意味同一个版本号 在两处构建出的包,运行时未必一致。要彻底消除需要在收集运行时那一步改成 下载固定版本,目前没做。 - 念想型人格未开放。第二章 2.5 节说明的三条前置条件(授权、范围声明、数据最小化)当前没有任何代码实现,因此该人格形态不对外开放。这是产品级约束,不是功能待办。
以上限制均为当前阶段的明确取舍,不代表最终形态。
下面这些项目代码已完成、逻辑可复核,但没有在目标平台上真跑过。单独列出来,是因为「跑过」和「写得对」是两件事,混在「已完成」里会误导后续接手的人。
| 项目 | 已完成的部分 | 缺什么验证 | 风险 |
|---|---|---|---|
| Windows 安装器(NSIS,410×290,3 页) | 模板、页面序列、变量声明、DPI 计算 | 未在 Windows 上真装一次 | 快捷方式落点、卸载残留、高 DPI 居中 |
| Windows 托盘图标 | 三档位(16 / 32 / 64)由脚本渲染并打包进 Resources/tray |
未在 Windows 通知区看过 | 通知区尺寸与 DPI 处理可能与 macOS 菜单栏不同 |
cargo build(release) |
cargo check 通过 |
未跑完整 release 构建 | 链接期或打包期才暴露的问题 |
| Linux 桌面集成 | 代码同源 | 未在任意发行版构建 | deb/rpm 依赖、StatusNotifier 托盘缺失降级 |
| 125% / 150% 缩放下 0.5px 线 | CSS 已声明降级到 1px | 未在真机缩放下核过 | 线可能消失或变虚 |
| 安卓端插件协作机制 | 单进程本地实现已覆盖主线(记忆 / 情绪 / 装配 / 流式) | 未在低于 Android 8 的设备上验过;未做多机型覆盖 | 老设备装不上是预期内的(只出 arm64) |
判定标准很直接:上表任一项在对应平台上手动走一遍、结果符合本章开头那张兼容性表的「状态」列,就可以从这张表移走。
更新日志在发行版页面,本 README 不重复维护。
- 全部版本:https://github.com/ntars/Ondra/releases
- 当前内测版
1.2.1-alpha:https://github.com/ntars/Ondra/releases/tag/v1.2.1-alpha - 各版本变更正文的源文件:
docs/releases/
为什么放在发行版而不是 README:更新日志与某一次构建一一对应,下载入口、安装包大小、签名状态、已知问题都在同一处;README 描述的是项目当前状态,两者混写会让读者分不清「这是现在的样子」还是「那次改了什么」。因此 README 只讲现状,变更归发行版。
发布流程会自己带上变更正文。.github/workflows/release.yml 在生成发行版说明时,先找 docs/releases/<tag>.md;有就整段附上,没有就退回 git log 的提交标题并标注是降级输出。此前正文末尾写的是「完整变更见 README 的更新日志」,而 README 写的是「更新日志在发行版」——两边互相指,读者哪边都找不到实际变更。这条循环引用已经断掉。
版本号口径:内测线从 1.0.1-alpha 起算,末尾的 -alpha 是内测标记,不代表公测,也不代表正式版。当前为 1.2.1-alpha。
按依赖关系排序,前一项是后一项的前提。
- 接通壳命令通道。让
desktop-bridge的invoke真正到达 Tauri。这是文件选择、外部链接可用的前提。 - 流式对话界面。订阅
event.inference.delta,实现增量渲染与中断。 - 工具注册表。内置基础工具集(文件读写、终端、网络),并在执行前校验参数。
- 自动更新。签名校验、增量下载、失败回滚。
- 插件热装载。把扩展抽屉里「重启后生效」的开关变成真正的热插拔:现在拨一个开关只改下次启动的名单,因为运行中撤走一个已
provide服务键的插件会留下悬空引用。 - 恢复两项严格类型检查。逐步修复迁入插件在
exactOptionalPropertyTypes与noUncheckedIndexedAccess下的类型问题,最终全仓统一。 - 独立分发包。把 Node 二进制与
node_modules一并打进安装包,摆脱对目标机 Node 环境的依赖。 - Linux 支持。适配系统集成部分。
- 单位:样式一律用 px 或 rem,禁用 rpx。
- 界面:遵循 Apple Human Interface Guidelines,曲线闭合、线条粗细一致、层级清晰。
- 代码:全面模块化,按需加载;用最小、最直接的代码解决问题,不添加未被要求的功能。
- 注释:说明「为什么这样做」而非「做了什么」;数据库等专业词汇用通俗语言解释。
- 语言:说明、日志、错误信息一律使用简体中文;代码、命令、变量名、路径保留原文。
- 插件:新增插件必须导出
name与apply(ctx, config),必须写好上游缺失时的降级路径,必须为每个配置项提供doc。
pnpm run typecheck # 全部工作区类型检查
pnpm run build # 构建前端、宿主、内核
pnpm run smoke # 三层链路冒烟测试
pnpm run check:core # 插件装配核对
pnpm run check:gap # 缺口链端到端(她说不知道 → 执念池 → 话由)
pnpm run check:gap-wake # 执念唤醒资格(张力 40 的新执念不该压掉承诺)
pnpm run check:system-prompt # 系统提示词装配(人格四段真的产得出)
pnpm run check:prompt-context # 记忆注入(插件登记的上下文真的进了提示词)
pnpm run check:kernel-system # 内核 system 参数(真的送到供应商、且不落盘)
pnpm run check:host-persona # 宿主端到端(HTTP 打进去,供应商收到人格正文)
pnpm run check:e2e # 主动开口端到端(交代一件事 → 入库 → 门控给由头 → 模型收到)
pnpm run check:emotion # 情绪读取链(写一个读数 → 六态 → 话由)
pnpm run check:feel # 情绪写入链(一轮对话 → 信号 → 情绪内核 → 存档)
pnpm run check:diffuse # 跨会话情绪弥散(换一场会话 → 心情按四成回落 → 写回并落盘)
pnpm run check:relation # 关系域(一轮对话 → 互动计数 → 熟悉度 → 弥散真的收到)
pnpm run check:evolve # 人格演化(一轮对话 → 互动计数 → 阈值真的漂到 1.0004)
pnpm run check:drift # 阈值漂移生效(同一个读数在漂移前后判出不同的态)
pnpm run check:project # 情感投影(六态 → 模板 → 系统提示词最后一段 → 过护栏)
pnpm run check:e2e-memory # 记忆提炼端到端(说一句关于自己的话 → 提炼 → 事实入库)
pnpm run check:voice # 声音链端到端(长回复 → 挑句 → 合成一次;用户层配置真的进插件;整轮情绪词结论真的传进 admit)
pnpm run check:plugins # 扩展清单如实上报(宿主级常驻插件不能报成「未装载」)
pnpm run check:intake-switch # 「收对话进记忆」关掉之后,库里真的一行都不收
pnpm run check:no-reason # 没有由头时不编兜底句(正文留空 → 门控判 content-empty,连问两次也不许变 duplicate)
pnpm run check:order # 存储顺序(取存储的插件必须排在提供方之后,备用后端不能永远取不到)
pnpm run check:filter # 删句链保段落(分句再拼回必须逐字节等于原文;删掉一句后段落空行还在)
pnpm run check:logfmt # 日志格式串(不许出现 %.2f 这类精度占位符——logger 不认,实参会掉到行尾)
pnpm run check:defaults # 配置默认值(每个字段的 default 必须能通过它自己的 pattern,否则整段 schema 注册会抛错)
pnpm run check:config-used # 配置字段有没有人读(声明了却零消费 = 假开关:设置页能改、日志照报、行为不变)
pnpm run check:stale # 产物新鲜度(源码比 lib 新就判红:此刻跑的检查读的是旧代码)
pnpm run check:shape # 句形档全覆盖(O-031 的六个 SentenceShape 必须都被 O-038 认得,缺一个就静默回落常句)
pnpm run check:disable # 关掉就不干活(三插件 enable=false 之后五张表一张都不许建出来)
pnpm run check:dedupe-tally # 判重计数五个栏位各对各的动作(灰区未写库 ≠ 复述:它才是「这条新事实没进去」)
pnpm run check:dedupe-digit # 灰区不许吞掉新事实的取值(旧条零数字 + 新条有数字 → 必须新建,不许只加固)
pnpm run check:recall-archived # 召回侧沉底开关四个通道(配置真生效 + 请求单轮覆盖两个方向)
pnpm run check:ui # UI 规范断言与令牌核对
pnpm run check:installer # Windows 安装脚本语法(需要 makensis)四十一项全部通过方可提交。check:stale 是这张清单里唯一一条不看行为、只看时间的检查,也是前面十几轮反复踩坑换来的:巡检、ondra.mjs、乃至「实机验证」读的都是产物,源码改了不重建,症状不是报错而是所有检查照旧全绿——跑的是几小时前的 lib,ondra.mjs config 里新加的配置段一个都不出现,debug.log 还是旧格式。它逐包比 src/**/*.ts 与 lib/**/*.js 的最新时间,另外单独比对打包应用里的宿主入口(落后只警告,因为出包要两分钟,不该让每小时巡检都因为「没出包」而红)。跑任何「实机验证」之前先跑它。check:logfmt 是纯静态的一条:Cordis logger 只认 %s/%d/%i/%o/%j/%%,不认精度,所以 '置信度 %.2f' 会落地成 置信度 %.2f 0.9(占位符留在行里、实参掉到行尾),全仓曾一次犯过 10 处、分布在 9 个插件,没有一处报错,只是日志从此读不出人话。check:core 有跳过项时会以非零退出码结束,可直接用作判据。check:plugins 与 check:intake-switch 是两条「开关是否真的生效」的检查:前者验扩展抽屉里 desktop-bridge / dsh-client 不能报成「未装载」(它们在主装载名单之外无条件挂载,判据只有一处 enabledPlugins 时就会谎报),后者验「收对话进记忆」关掉之后 memory_raw 真的一行不涨——判据都落在库里的行数与接口返回值上,不看服务自报的 available。check:gap 会真装载一次插件链并喂一轮对话,验的是「她答不上来的事真的进了执念池、并且真的能被取成话由」——只查代码里有没有调用,抓不到这类断路。check:gap-wake 补的是前者的背面:执念在话由优先级里排第一且排序是字典序,若不做资格判定,一条刚出生、张力只有 40 的事会压过所有承诺与待办;这个脚本用同一组输入分别走「够格」与「不够格」两条路,看 pick.kind 是否真的换人,并验 markDelivered() 的 picks / lastWakeAt 确实被写。check:system-prompt / check:kernel-system / check:host-persona 是一条链上的三段:第一段读插件自己的视图,验人格四段真的拼得出来(在此之前它拼得出、但全仓没有消费者);第二段起一个假供应商,验 inference.run 的 system 参数真的走到了 messages 最前、且不落盘;第三段从 POST /api/inference 打进去,站在供应商一侧确认人格正文真的到了——这一条是唯一不能造假的口径,因为「装配了」与「送出去了」之间那根线,两侧都不报错。check:e2e 补的是同一条链的另一半:session/event 在整仓里长期只有订阅方、没有发出方,于是 ondra-memory-intake 的轮次草稿永远是空的,memory_raw / working_todo / memory_fact / drive_gap / emotion_state 五张表从建库起一直 0 行,而所有单元级检查都绿(它们各自直接调 intake.ingest(),绕过了「谁触发」这一环)。这个脚本从 POST /api/inference 说一句「明天提醒我把合同打印出来」,然后读库文件确认落盘、读 POST /api/proactive/check 确认判据换成了「你交代过的事」、最后在假供应商一侧确认这句由头真的进了模型上下文,并反证「她自说自话那一轮不新增待办」。check:e2e-memory 补的是记忆链的后半段:修好 session/event 之后 memory_raw 有数了,memory_fact 却仍是 0——因为整条模型调用链上五个环节同时缺席且都不报错(网关没进默认名单、ctx.llm 全仓零注册、网关 sources 默认空、sources[].model 要填供应商 id 而不是模型名、提炼器没进默认名单)。任何一环断掉症状完全相同(下游恒空),所以这个脚本先用 /api/diagnostics/memory 把四层原因码并排打出来,再从 POST /api/inference 说一句「我叫小玖,住在杭州」,最后读库文件确认 memory_fact 里真有那两行,并反证「提炼的提示词与返回 JSON 一个字都没进会话历史」。check:installer 在没装 makensis 的环境退出码为 2(跳过而非失败),装了则真编译一遍安装器——安装脚本一千多行,只有 Windows 才能被 Tauri 打包器吃进去,这个检查是把它的问题提前到本机暴露的唯一手段。check:emotion 与 check:feel 是同一条链的两侧:前者验读取侧(写一个读数 → 六态跟着变 → 变成一条话由),后者验写入侧(一轮对话 → 认出信号 → 投进情绪内核 → 存档落地)。写入侧此前全仓零调用方——feel() / comfort() / setAxis() / save() 五个写接口没有任何人问过,于是 emotion_state 从建库起一直 0 行、六态恒为「平静」,而平静是合法状态,所以没有任何报错。check:feel 的判据刻意不猜语义情绪:stumble 信号只看 ondra-drive-gap 的 detect() 是否命中(判据的唯一真源),apology / gratitude 只认明确的言语行为词表,reunion 只算时间差;位移量交给上游的 0.12 单条上限与 0.08 安慰幅度,调用方一行都不重算。check:diffuse 补的是情绪链跨会话那一半:ondra-emotion-diffuse 的公式、幂等闸、间隔闸、单行标记表、参数总表全都写完了,缺的是调用方——claim() 全仓零调用方、插件也不在默认启用名单里,于是「换一场会话,上一场剩下的心情还剩多少」这件事从来没有发生过,症状是她每次都像刚开完一场没断过的对话。落笔归宿主的 src/diffuse.ts(插件刻意不把 setAxis 放进自己的接口,applied 是字面量 false),因为界面不知道那道 30 分钟闸、也拿不到标记表,重复调就会把残差乘两遍,现象是「她越聊越淡」。这个脚本分三层验:插件算术(0.2 → 0.38 → 残差再乘四成)、宿主真的写了 setAxis(再读一次情绪内核自己的视图,只看返回值会漏掉「没写却把 target 原样回传」)、以及落盘(库里那一行不是上一轮的旧值)。check:relation 补的是关系域的写入侧:ondra-emotion-stage 从七阶表、四闸、闭式衰减到存储都写完了,唯独三个写入口(touch / adjust / observe)全仓零调用方,于是 interactions 恒 0 → familiarity 恒 0 → 弥散公式里 (1 − familiarity × 0.5) 整段白写,七阶永远停在「敌意」,而三维只衰减不成长。这三件的症状都是「什么都没发生」:0 是「陌生人」这个合法取值,敌意是合法阶,所以不会有任何一处报错——本仓第七次踩同一个形态。修法分四处:touch() 成为唯一的互动计数入口(adjust() 从此不计数,两处都计会让一轮 +2,而 5000 这个分母是按一轮一次标定的);宿主 src/relation.ts 的 recordTurn() 在「用户真说了话」的轮次先 touch() 再 observe()(顺序反了第一轮会卡在二阶的互动闸上,症状是「升阶总慢一轮」);src/diffuse.ts 调 claim() 时真的传 familiarity(此前只传边界键,插件一律按 0 处理);ondra-emotion-stage 进默认启用名单。脚本分四层验:插件算术、插件写入(touch() 推得动且落盘)、宿主接线(在真实服务实例上装计数器数调用次数,因为布尔标志证明不了「那一处真的执行了」)、下游消费(用替身记下 claim() 收到的请求,避开「0.0004 的差在保留比例上看不出来」这个量级陷阱)。check:evolve 补的是人格演化的写入侧:ondra-persona-evolve 的漂移公式、三层夹紧、可回滚日志、三张表全都写完了,README 连「以前 60 就算开心,后来要 80」的手感表都列好了,缺的是四样东西,而四样全都不报错——插件不在默认启用名单(服务不注册)、noteInteraction() 全仓零调用方(interactions 恒 0、系数恒 ×1.00)、thresholdOf() 与 personaState() 也零调用方。×1.00 是合法系数、0 是合法互动数,所以症状只有一句「她永远像第一天认识你」——本仓第八次踩同一个形态。修法刻意避开第二份真源:互动次数的唯一真源是关系插件的 touch(),宿主 src/evolve.ts 的 syncEvolveInteractions() 只做差额对齐(真源是多少就把镜像补到多少),不是每轮 +1——后者两处从不同起点各自累加,历史永远补不上且迟早漂开。镜像超前时只报 drifted 不抹平,真源缺席时一律不动(拿镜像当尺子就是自证)。脚本分六层验:算术(5000 饱和点、0.6 斜率、+60% 上限、effectiveThreshold(60, 2778) ≈ 80)、宿主差额对齐(一次补 3 而不是每轮 +1)、幂等(重复对齐不再加)、落盘(库里那一行真是 3)、诊断(appliedTo 只列真的消费了的那个)、镜像超前与真源缺席两条边界。这一轮只接通了「算得出」:六态与七阶的阈值当时都是各自插件的常量表,没有任何一处接受外面传进来的值,所以 appliedTo 刻意留空——GET /api/diagnostics/emotion 的 evolve 一格把这句话按机器可读的形式回答出来,而不是靠文档里一句提醒。第二十八段把这件事做完了,见 check:drift。check:drift 补的正是那半件悬着的事——阈值漂移的判定侧。上一轮接通的是「算得出」,判定照旧读常量表,于是「她越来越难被取悦」只在自己的 README 里成立。这一轮两处都接上了:六态侧新增 thresholds.ts 把 STATE_SPECS 的 base 交给 persona-evolve.thresholdOf() 乘系数,七阶侧同样新增一份把 affection / trust 交给它;base 永远由常量表的主人给(谁的表谁给),乘法只归 O-130,两边都不存对方的数——与它 README「base 每次由调用方传进」逐字对齐。三条纪律写死在代码里:下界一律不漂(蔫 的 mood < 0.35、困 的 energy < 30 是下界,上移等于「她更容易没精神」,那是误读而非「更难取悦」);days 不漂(相处天数是客观经过的时间,乘 1.6 等于说「处得越久,心动需要越久」,循环论证);可达性护栏(mood > 0.7 乘 1.6 等于 1.12,而 mood 量程只到 1——mood > 1.12 永远不成立,「雀跃」这一态在相处 3571 次之后就被判死了;超量程的一律退回出厂值并如实上报,宁可少漂一点也不让六态变成五态)。判据不是「传参了没」,而是同一个读数在漂移前后判出不同的态:mood 0.78 在 0 次互动时判「雀跃」,推到 1667 次(系数 ×1.20)之后同一个 0.78 不再雀跃。GET /api/diagnostics/emotion 的 evolve.appliedTo 从空数组变成真实消费方列表,而它的判据是消费方自己报的 drift.applied 非空——系数 ×1.00 时阈值与出厂逐字相同,此时填上名字就是撒谎,而那正是刚装上的常态。
check:project 补的是「情绪的唯一文本出口」那条边。ondra-emotion-project 的六态模板、四条护栏词表、四道拒绝闸、配置段、启动自检全都写完了,缺的是四样东西,而四样全都不报错:插件不在默认启用名单(服务不注册)、project() 全仓零调用方(模板写好了没人问)、投影文本没有消费者(系统提示词只有人格 + 器官片段,没有一个字关于「今天该怎么开口」)、constraint 也没有消费者(§4.4 的限定句从未进过提示词)。于是她永远用同一种语气说话,而「这一轮没有情绪块」与「她本来就不该有」症状完全相同——本仓第九次踩同一个形态。落笔在宿主的 src/emotion.ts,装配点接在系统提示词的最后一段(手册 §8.5:稳定段在头、易变段追加尾部;人格跨会话不变、器官片段变得慢、投影每轮都变)。三条边界写死在代码里:不带约束就不注入(缺 §4.4 限定句时宁可这一轮没有情绪块,因为一句「你今天话可以多一点」没有「情绪不影响判断」的限定,会被模型当成行为授权);不从对话里猜「用户是不是在问数值」(插件把这件事做成 askedForNumbers 入参,揣测是它刻意避开的,宿主加一份关键词表会更难审计;代价是那个唯一例外出口目前恒不触发,诊断里 askedForNumbersWired: false 如实报出来);拒绝产出不打日志(拒绝是正常产品行为,每分钟刷一行「没有情绪块」会把真正该看的那行淹掉)。脚本分九层验:四个服务在位、六条模板逐条产出且排序 800、例外闸真的拦、情绪段真的进了装配结果且是最后一段、护栏扫成品(模板自检只扫 command + hint,而真正进提示词的是加了约束句的成品——限定句从未被自检扫过)、换状态提示词真的跟着换、幂等、诊断读数,以及六个异常路径(服务没装载 / 配置关掉 / 约束句为空 / 正文为空 / 投影抛错 / 插件拒绝)。真宿主那一段另验两条只有端到端能回答的事:它在不在默认启用名单里,以及 emotionApplied(宿主真的拼进提示词的轮数)与 project.counters.projects(插件产出过的次数)两个数是否同步——只验后者会漏掉「取到了又丢掉」。
check:prompt-context 补的是记忆注入那条边,也是本仓第六次「A 写、B 读、中间缺一个位置」。ondra-prompt-inject 从写完那天起就在做一件对的事:把八块记忆渲染成一段正文,挂进系统提示词。它调 ctx.get('systemPrompt'),拿不到就 ctx.inject(['systemPrompt'], …) 等它出现。而宿主从来没有 provide 过这个键——只是在自己的请求处理函数里直接调 assembleSystemPrompt(ctx)。于是它永远等不到,只打一行「注入装箱已就绪但无人阅读」的 warn;stats() 照常可读、界面照常显示,唯一体现是她从来不提记忆里的任何事。本轮补的是那个位置(src/prompt-context.ts + 装配里的第三段),并顺带把 ondra-memory-pool 一起装进默认名单——它是同一条链的另一侧:缺 pool,事实会全部落海马体并置 unclassified,而海马体按设计永不注入;缺 inject,归池照常有数但没人读。两侧都缺时症状完全相同,所以必须一起进名单,否则只是把空跑挪了个位置。三条纪律写死在代码里:顺序由登记方声明(同 order 按登记先后稳定排序,不稳定排序会让同一输入每轮产出不同提示词,前缀缓存全废);缺省不插值(记忆正文里一个花括号就能让宿主 renderPrompt 抛 unknown prompt variable,整次装配失败);单段抛错只让该段缺席(其余段照常进,理由与装配本身相同)。脚本分八组验:服务真被 provide、插件走的是直路(provide 早于 loadCore)、话头真透传、无话头时缺席而非空串、单段抛错被隔离、上下文排在情绪之前、撤销与幂等、装配不抛错。真宿主那一段另验三格并排:context.registered(注册了几段)、context.names(谁挂的,应含 ondra:injection)、context.applied(几轮真产出了正文)——第三格只报数不判死,因为干净库上「没有旧事可说」正是正确答案,把它也钉成 >0 会让脚本在全新安装上必红,而红会被当噪音。
check:order 补的是装载顺序那条边,也是本仓又一次「声明与事实分家」。三个存储端口按 ctx.get('ondra-store-sqlite') ?? ctx.get('ondra-store-memory') 的优先级取用,?? 右半边是 SQLite 失守时的备用后端。但 loadCore 装的是 orderMembers() 排出的序,而排序只认成员自己声明的 after:ondra-store-memory 的 after 里有 ondra-store-sqlite(它要读 SQLite 状态来判断「我在不在用」),于是它被推到 SQLite 之后,而 17 个只写了 after: ['ondra-store-sqlite'] 的消费方全排在它前面——备用后端在每一个消费方那里都是 undefined,日志还在照常打印「本后端仅作备用」。SQLite 正常时这条断线完全不显形,只在最需要冗余的那一刻出现,而且加一个新插件照抄一行 after 就会再漏一个。判据不是「声明了没」,而是把清单交给真的 orderMembers() 排一次序、再逐个成员的源码核对:凡出现 ctx.get('<存储键>'),它在排序结果里的位置就必须在该键的提供方之后。反向验证做过:把任意一个消费方的 'ondra-store-memory' 删掉,脚本精确报出那一个 id。
check:config-used 补的是另一条边,也是本仓第十次踩同一个形态:配置项声明了,但没有任何人读它。它比前九次更难发现,因为三处都说「生效了」——设置页显示已改、启动日志照实打出新值、类型定义完整无缺,只有「有没有人读这个字段」那一层是空的,而那一层没有界面。判据是把每个插件的 FIELD_SPECS 键逐个拿去包内源码里找消费者(settings.ts 自身不算:那里是声明),零消费的进入豁免清单,且豁免必须写 ≥30 字的理由并把清单本身也钉住(豁免项必须真的还被豁免,否则过期的豁免会把下一轮的人骗过去)。第 18 轮实测 624 个字段里 11 个零消费:2 个是误报(引用在 settings.ts 自己的 resolveEndpoint() 域内函数里)、4 个是待裁决、7 个是真假开关——七个插件都声明了 verbose,doc 一律写「降级时打一行日志」,却零读取。接线时守住一条纪律:warn / error 一律不受 verbose 约束,开关管的是过程噪声,不是故障信号——把故障也压掉,等于把「排查为什么她记不住」的工具收走了。同轮还修了一处 substituteInBoundaries:设置声明完整但拼装侧永远替换,修法是给 substitute() 加 scope 参数而不是新开一个方法——两个方法会让「替换」有两份实现,而这份实现的全部价值就是只有一份。check:defaults 补的是配置字段自己和自己打架那条边。ondra-config-schema 在 register() 时会拿字段的 pattern 校验它自己的 default(layers.ts 的 checkFieldSpec),所以一个「默认值是空串、而正则不收空串」的字段会让整段注册抛错——代价不是崩溃,而是那一段从此没有 schema 层:非法值不再被挡回默认值,每次启动还留一行读起来像故障的 warn。全仓曾命中 2 处(asr.language、vision.credentialRef),两处代码里都写着「留空即自动 / 留空用默认」,也就是空串本来就是合法取值,错的是正则太严。判据是逐包 import lib/settings.js、对每个带 pattern 的字符串字段跑一次 new RegExp(pattern).test(default);反向验证:把其中一处改回严格正则并 build,脚本立刻指名那个包与字段。
check:filter 补的是删句与分段打架那条边,也是本仓第一处「三个下游一致地错、却没有一处报错」的缺陷。ondra-filter-chain 九步里有七步是「删整句」,实现都是 splitSentences() → 逐句判 → joinSentences() 拼回;而 \n 是它认的句末标点之一,分句时逐句 trim() 把换行剪掉、拼回时又用空串相连——于是只要删过一句,模型写好的分段就整段被压平。症状不是报错,是气泡里两段话粘成一句(「…出门只有屏幕。雨我看得见,听不见」),而 done 的文本、界面显示、写进记忆的那条三处一致地都是错的,所以任何单点比对都发现不了。发现路径是同一轮里两个值不一样:say 打的流式原文有三段、「塑形后」只有一段。修法是把「这句后面的空白」单独存成 Sentence.gap(不塞进 text,否则 DRAFT_RULES 那些 ^…$ 锚点会全部失配),拼回时原样带上,删句时顺延给下一句。判据是两条硬的:无删改时 split → join 必须逐字节等于原文;三种删句位置之后段落空行必须还在且不多不少。反向验证:把 gap 改成恒空,脚本红 8 条,红出来的正是线上那个粘句症状。
check:shape 补的是映射表的键与它消费的联合类型不在同一处维护那条边,也是本仓第一个「两边各自合法、运行时被 ?? 吞掉」的缺陷。ondra-emotion-express 的 SentenceShape 联合类型有六个值,sleepy(困)用的是 ellipsis;而 ondra-rhythm 的 SHAPE_ADJUST 只有五个键——缺的正是 ellipsis。缺键的后果有三重静默:查表 ?? PLAIN_SHAPE 回落常句;adjusted 因系数算出来等于默认值判 false;notes 里的 { code: 'shape' } 也不产生。于是她困的时候语速确实慢下来了(tempo: 'slow' 走的是另一张有键的表),分条与单条字数完全按平常档走,而面板「句形」一格照常显示 ellipsis / 短句带省略——看着全对。判据不抄实现(不去断言表里有哪些键,那样表里加错数字也能过),而是拿实际生效的单条上限当派生量:同一段正文、同一个 maxCharsPerMessage,只换句形档,plan().maxCharsPerMessage 必须随档变化。断言写的是每档与 plain 的关系而不是「六档两两不同」——medium 与 plain 的系数都是 1(手册的「中句」与「平常」都不该改单条字数),它们相同是刻意的。反向验证:摘掉 ellipsis 键 → 恰好红 1 条,报的正是 40 < 40。
check:disable 补的是假开关那条边,一次守住三处同构缺陷。ondra-emotion-stage / ondra-emotion-diffuse / ondra-persona-evolve 三个插件都在关闭之后照旧建表(合计五张),因为 settings 已经算好了、if (!settings.enable) 就是没写。对照 ondra-emotion-core 一直是「先判关、再判存储」,说明这是漏写而非取舍。判据必须是去问存储层这张表存不存在(store.tableExists()),不能是 available === false——后者在修复前就绿,等于没测。顺带修掉 diffuse 的两处连带缺陷:原因枚举缺 schema-failed(建表失败复用 no-store,于是「磁盘写满」与「没装存储」报同一个码),以及 reason getter 让 fallbackReason 优先、会把 disabled 盖掉。evolve 另修一处:noteInteraction() 自身无开关守卫,靠宿主替它挡——任何第三个调用方都能在关闭状态下写库。反向验证:三处守卫改 if(false) → 红 5 条,与三处修复一一对应。这里踩过一个坑值得记:表名必须去源码里读,不能按目录名猜。 第一版把 emotion-stage 的表写成 ondra_relation_state(跟着目录名走),真名是 emotion_relation_state,于是那条断言永远绿、反向验证时也一直绿——一个永远不会红的断言比没有断言更糟,它让人以为那一处被守住了。
check:dedupe-tally 补的是日志把两个不同动作印成同一栏那条边,也是本仓第一处「排查工具本身在说谎」的缺陷。ondra-memory-intake 那行「判重裁决:新建 N · 合并 N · 复述 N · 丢弃 N · 冲突 N」的五个 %d 依次收 tally.add+conflict / merge / append / drop / conflict——而 append 的落库语义是只 reinforce()、不写库,它才是真正的「这条新事实没进去」;drop 才是判重术语里的「复述」。两栏标签正好换了位。代价不是措辞难看:实机 say "记一下:备用钥匙在玄关鞋柜最上层,编号 K-9",她回「记下了,我把它改过来了」,库里仍只有「第二层」那条,而日志写着「复述 1」——一句话都没进去,三处读数却完全自洽。全库实测 append 16 条、written 全为 0,其中含「用户让玖玖记住暗号」这种明确要求记住的内容。判据不是「标签含某几个字」(措辞会改),而是一对一:五个栏位、五个实参行,栏位名与它收的 tally 字段必须双向对上,且「灰区未写库」栏收 append、「复述」栏收 drop 互不串位;另有一条锁 append 分支确实不调 this.write(),防止哪天改了行为而日志变成「说过时」。反向验证:标签改回旧版 → 红 5 条,位置精确。check:dedupe-digit 补的是同一处更上游的那一刀,也是本仓第十一次踩「三个读数自洽、事实没进去」这个形态。上一轮修好了日志措辞,于是「灰区未写库 1」终于能被正确读成「这条新事实没进去」——可它为什么被划进灰区,判据里一个字都没有。实测:旧条「用户有一把自行车锁,并问密码是多少」(一个数字都没有)、新条「密码是 4712」,包含度 0.46 落在灰区 [0.40, 0.95),槽位替换不匹配 → append → 只 reinforce() 旧条、不写新条,而对话层照回「记下了」。digitsDiffer() 这个函数当时只在 conflict 分支用,灰区分支完全没用——数字是事实的值,不是修饰;旧条压根没提过任何值、新条第一次给了值,那句话不可能是「又被提起」。修法是在 append 之前加一道窄闸:旧条零数字 + 新条有数字 → 改判 add。判据刻意收窄到这一个象限——旧条已有数字而新条不同属于换值,那是 conflict 的活,放进来会抢判定、把冲突降级成新建。脚本用纯函数直打 judge()(它自己不碰 ctx、不碰库、不调模型,起宿主只会引入无关变量),六条里有一条是校准锚:把同一条的数字抹掉必须回到 append——少了它,上面那条通过可能只是「第 7 步整段被删」,那样每次复述都会多一条重复。反向验证:删掉 7.5 分支 → 恰好红 2 条(主判据 + 理由点名数字),其余 4 条仍绿。
check:recall-archived 补的是假开关那条边的一个新形态:同名键遮蔽。ondra-memory-recall 的 includeArchived 在设置页有一格(默认关),而 src/ 里真正被读的一直是 request.includeArchived——两处同名,所以「字段名在源码里出现过」这类静态扫描永远判它已接线。症状是本仓第 N 次那个形状:设置页能改、日志照实回报、重启也照做,行为一个字不变。判据不能靠读源码,只能造一个真仓库、喂真请求、看真结果:四段锁「配置 true + 请求不传 → 沉底那条进候选」「配置 false + 请求不传 → 不进」「请求显式 false 压过配置 true」「请求显式 true 压过配置 false」两个方向,外加一条锁 explain() 与 search() 用同一套判据(两处判定不一致会让「她为什么没想起这条」的解释说谎)。修复只加了一个 archivedIncluded() 私有方法,优先级是请求参数 > 配置,判据用 typeof === 'boolean' 而不是真值判断——false 是显式值,不能被配置翻回来。反向验证:把两处判定改回 request.includeArchived === true → 红 2 条,位置精确。
请说明:复现步骤、预期结果、实际结果,以及 dsh-sidecar 与 cordis-host 的标准错误输出。涉及具体插件时请附上插件包名与相关配置项的当前值。
| 渠道 | 地址 |
|---|---|
| QQ 群 | 1061684298 |
| 官网 | https://ai.ntars.cn/ |
| 仓库 | https://github.com/ntars/Ondra |
| 发行版与更新日志 | https://github.com/ntars/Ondra/releases |
| 问题反馈 | https://github.com/ntars/Ondra/issues |
| 邮箱 | ntars@qq.com · ntarsx@163.com |
内测期间优先走 QQ 群:群里能直接问到当前版本的实际状态,也方便同步已知问题的临时绕行办法。功能建议与缺陷请走 Issues,公开可追溯。
Copyright © 2024–2026 Ntarstudio(星玖工作室). All Rights Reserved.
本项目以 GNU Affero General Public License v3.0 授权。完整许可证原文见 LICENSE 文件。
AGPL-3.0 的核心义务:若修改本程序并通过网络提供服务,须以相同许可证公开修改后的源代码。这不影响在本机私人使用本程序。
商标。「空谷」「Ondra」「Ntarstudio」「星玖工作室」为本项目使用的名称与标识。AGPL-3.0 授予的是著作权许可,不授予商标许可。基于本项目的衍生作品不得暗示其由 Ntarstudio 出品或获得 Ntarstudio 背书。
无担保。本程序按「现状」提供,不附带任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性与非侵权性的担保。完整免责条款见 LICENSE 第十五、十六条。
本项目使用的全部第三方软件、组件、素材与算法来源,已在 NOTICE 文件中逐项披露,包含名称、版本、版权方、许可证类型、官方地址、在本项目中的用途与修改说明。
披露范围。包含直接依赖与全部传递依赖:Rust crate 442 个(直接 6 个、传递 436 个),npm 包 87 个(直接 12 个、传递 75 个),合计 529 个。数量由 Cargo.lock 与 pnpm-lock.yaml 解析得出,不是估算。同一 crate 的不同版本分别计数,因为版本不同许可证可能不同;按名称去重后是 416 个 Rust crate、74 个 npm 包。
许可证分布。MIT 与 Apache-2.0 及其组合 448 个、Zlib 22 个、Unicode-3.0 19 个、MPL-2.0 17 个、BSD-3-Clause 10 个、Unlicense / 0BSD / CC0-1.0 / MIT-0 合计 8 个、ISC 3 个、LGPL-2.1-or-later 作为多选项之一 2 个,全部与 AGPL-3.0 兼容。逐类表格见 6.3。
弱著佐权义务如何处理。17 个 MPL-2.0 组件全部以未修改的发布版本引入,不产生 MPL-2.0 第 3 条的衍生作品义务,也没有公开源码的义务;若将来需要修改其中任何一个,被修改的文件须以 MPL-2.0 发布。
未使用的内容。本项目未使用任何来源不明、许可证不明或需付费授权的代码、模型权重、数据集、图片或音频素材。界面字体 MiSans VF 随前端产物一同分发(src/assets/fonts/MiSansVF.woff2,8.8 MB,由官方 TTF 子集化生成),依据小米官方免费商用授权嵌入,授权要点、子集化字符范围与注明义务见 src/assets/fonts/LICENSE-MiSans.md,并在 NOTICE 第六节逐项登记;系统字体栈仅作回退。图标的图形数据来自 lucide(ISC),渲染与形变由 morphicons(MIT)承担,两者以未修改的依赖形式引入;应用图标为自有原创设计,随源码一同以 AGPL-3.0 授权。
无模型权重。dsh-sidecar 是纯代码实现,不包含任何模型权重。推理能力由使用者在运行时自行配置的模型服务提供,相关模型的使用条款由使用者与模型提供方之间的协议约定,与本项目许可证无关。
若引用他人内容。已注明来源并确认符合其许可证要求。本项目自身版权声明与第三方版权声明在同一文件中并列保留,不互相覆盖。
Ntarstudio(星玖工作室)· https://www.ntars.cn/