螺壳系统文档
螺壳(Luoke)是螺壳实验室(Luoke Labs)开发的一组本机 AI 工具:所有模型推理和数据都在用户自己的 Mac 上。本文档描述系统的结构、各组件的职责与接口、数据如何流动,以及运行和发布方式。
组件
| 组件 | 仓库 | 职责 |
|---|---|---|
| 螺壳(主控) | luoke-core | 用户装的应用「螺壳」;本机模型推理的唯一入口;安装、升级、卸载;调度空闲任务 |
| 记忆 | luoke-mem | 共用的本机记忆;唯一形成关于用户结论的地方 |
| 邮件助手 | luoke-mail | 分拣、清理、精选、起草;可撤回的处理自动执行并每日复查 |
| 输入法 | luoke-ime | macOS 输入法;沙盒引擎出候选,本地模型重排和联想 |
| agent | luoke-agent | 唯一主动联系用户的组件;合并提问、建议和工具的事项 |
| 螺壳体检 | luoke-audit | 在空闲时查这台 Mac 的空间和安全;只经 agent 说话(未发布) |
| 深度搜索 | luoke-search | 给本机文件和照片图库建索引,按字词和意思找回;没有界面,入口在螺壳(经 agent)(规划中) |
架构
flowchart LR
subgraph Mac["用户的 Mac"]
direction LR
mail["邮件助手"]
ime["输入法"]
agent["agent(螺壳的后台组件)"]
search["深度搜索(未发布)"]
audit["螺壳体检(未发布)"]
subgraph core["主控"]
engine["推理引擎(底座)"]
workers["打分 / 联想 / 嵌入进程"]
mem[("记忆 mem.db")]
ep["高级用户端点"]
end
end
mail -- "生成任务" --> engine
ime -- "打分、续写" --> workers
mem -- "归纳" --> engine
mail -- "记录" --> mem
ime -- "记录" --> mem
mem -- "理解" --> mail
mem -- "理解" --> ime
mem -- "提问、建议" --> agent
agent -.->|"调用能力(接口默认关闭)"| mail
agent -.->|MCP 工具| search
agent -.->|MCP 工具| audit
search -.->|嵌入| workers
ext["外部工具(如 pi)"] -.-> ep --> engine
核心原则
- 本机就是边界:数据不离开用户自己的设备;在本机内部,各组件可以读取完成工作所需的任何数据。
- 模型只经主控:所有推理通过主控的任务接口进行,组件不自行加载模型。
- 只有记忆形成结论:工具记录发生了什么,记忆据此形成带依据、分三档把握程度的理解。
- 先做、事后复查:能撤回的处理在用户确认一次习惯后自动执行并给出提示;不可逆或对外的操作始终由用户点按。
- 凭证不进记忆:密码、验证码、密钥、令牌、完整卡号和证件号在写入时遮去。
决策记录见 决策。
阅读顺序
核心概念
配件
由主控安装和管理的一个签名 .app,例如邮件助手、输入法,以及未发布的深度搜索、体检。agent 现在是开发版,主控不管它,以后随螺壳装好。每个配件在 luoke-accessory.json 中声明自己的身份、需要的任务、空闲任务、数据目录和给 agent 的能力。Python 配件自带固定版本的 Python。记忆没有独立的 .app,它打包在主控里。
任务
配件向主控请求推理的单位,名字形如 luoke-mail.triage。配件只说要哪个任务,不指定模型;主控按配方和机器的硬件档决定用哪个模型、参数是多少。任务分四种:
| kind | 做什么 | 例子 |
|---|---|---|
generate | 生成文字(流式,约束为 JSON) | 邮件分拣、记忆归纳 |
score | 给候选打分 | 输入法重排 |
predict | 续写 | 输入法联想 |
embed | 向量 | 深度搜索 |
提示词、输出格式和业务判断写在调用任务的配件里;主控只提供通用的推理能力。
硬件档
按内存把机器分为 16、32、128 三档,每档有自己的底座模型、内存上限和底座是否常驻。每个任务在哪一档开启,由评测决定:关着的任务连续 3 遍全部通过才开启(当天评一遍,另两遍在发布后补跑,下一版生效),开着的一遍不过即关闭。
请求等级
主控按三个等级调度:打字(输入法打分和联想,最优先)、用户在等(用户正在等结果的请求)、后台(可以等待的工作)。后台工作只在机器空闲、接通电源、未过热、内存充足时运行,用户一回来就让路。
空闲任务
配件登记的"空闲时运行"的命令,例如记忆的归纳、邮件助手判断旧邮件。主控只决定何时启动(安静时刻、满足最短间隔),不关心它做什么;用户回来时让它收尾。
记录
工具写入记忆的"发生了什么":谁(用户、第三方、规则自动执行)、做了什么、什么时候真实发生,可附原文。记录是理解的依据。
理解与把握程度
记忆从记录归纳出的关于用户的认识:事实、关注的话题、不感兴趣、处理习惯、常用说法、精选口味。每条理解引用它的依据,并有三档把握程度:
| 档 | 含义 | 谁能用 |
|---|---|---|
| 猜测 | 推出来但证据不足 | 不使用,只在记忆界面中显示 |
| 比较确定 | 依据足够 | 各工具可以使用 |
| 确定 | 用户说过或确认过 | 各工具可以使用,可据此自动执行 |
一条推断能到哪一档,取决于依据的类型和数量(见记忆 · 定档规则)。会变的理解太久没有新依据会变成"以前的事"。
提问与建议
记忆在空闲时整理理解,产出两类请求:提问(只问用户才能定、答了有用的猜测)和建议(如"以后这类邮件都自动归档?")。它们由 agent 或相关工具的页面展示,用户的回答交回记忆定档。
处理习惯与自动执行
用户对某个发件人某类邮件反复做同样的处理,记忆形成一条处理习惯并建议自动执行;用户同意后,邮件助手在同步时照做。只有能撤回的动作(归档、标已读、加标签)会自动执行,每次都可在"今日处理"中撤销,撤销一次即视为反证。
能力
工具在清单中向 agent 声明的可报事项和可执行动作,经工具协议(MCP 子集)调用。会改动数据的调用只在用户点按卡片时发出。
数据流
一封邮件
sequenceDiagram participant M as 邮箱(IMAP) participant L as 邮件助手 participant C as 主控 participant R as 记忆 participant U as 用户 L->>M: 同步收件箱 L->>C: triage(用户在等或后台) C-->>L: 建议、摘要、截止日期、可疑 L->>M: 标已读;按确定的习惯归档或加标签 L->>R: 记录 mail-read、mail-auto U->>L: 在"今日处理"中撤销或改建议 L->>R: 记录 mail-override、mail-undo-auto R->>C: 空闲时归纳话题和习惯 R-->>L: 建议"以后都这样?"
- 后台循环按间隔同步,新邮件逐封交给主控的
luoke-mail.triage,"关于用户"取自记忆中比较确定以上的事实和习惯。 - 分拣后自动标已读(一条可整批撤销的操作记录),并执行记忆中确定、可自动执行的处理习惯;结果进入"今日处理"。
- 用户的操作(改建议、亲手归档、撤销自动处理)写成记录。
- 记忆的归纳任务从这些记录形成处理习惯和话题,向邮件助手提出建议;用户同意后习惯成为确定,下一次同步开始自动执行。
打的字
sequenceDiagram participant E as 打字引擎(沙盒) participant P as 个人化组件 participant C as 主控 participant R as 记忆 E->>P: score(候选) P->>C: luoke-ime.rerank C-->>P: logprobs P-->>E: 分数(引擎据此重排) P->>R: ime-commit(上屏 + 前文,凭证已遮去) R->>C: 空闲时归纳常用说法和话题
提问和建议
- 记忆的反刍在每次归纳任务的开头和结尾运行,生成提问(每天 ≤ 2)和建议(每天 ≤ 3),写入
asks。 - agent 每 300 秒读取一次,在窗口、通知或简报中展示;与邮件有关的建议也在邮件助手的记忆页中展示。
- 用户的回答经
Memory.reply交回,记忆据此定档。
外部工具
高级用户可开启主控的本机端点,让 pi 等工具通过 OpenAI 兼容接口使用本机模型,包括用户自己登记的模型。这些请求与配件请求一起由主控调度,按"用户在等"处理。
螺壳(主控)
用户装的应用「螺壳」(螺壳.app);"主控"是它在内部分工里的叫法。本机所有模型推理的唯一入口,也是各应用的安装器。它按配方为每个硬件档选择模型,调度各配件的请求和空闲任务,监管推理引擎和专用模型进程,并负责下载、升级和卸载。
仓库 luoke-core · bundle id ai.luoke.core(开发版 ai.luoke.core.dev,名为「螺壳(开发)」,数据在 luoke-core-dev,不认领 luoke://)· 接口细节见「参考 · 主控任务接口」「参考 · 配件清单与配方」
组成
| 进程 | 谁启动 | 说明 |
|---|---|---|
菜单栏本体 螺壳.app(装在 /Applications 或 ~/Applications;改名前的 螺壳主控.app 升级时换成新名字) | 用户、登录项或 luoke:// | Swift:菜单栏海螺图标和菜单、设置、「应用」窗口(逐个应用安装、升级、回到上一版本、卸载)、主控小窗口、首次运行、安装器、检查更新和升级、卸载、打开记忆界面 |
| 主控进程 | 本体 | 自带 Python:python -m luoke_core serve,在 Unix socket run/hub.sock 上提供任务层;父进程退出时随之退出 |
| 推理引擎 | 主控 | oMLX(引擎包内的 omlx-cli serve),只监听 127.0.0.1 随机端口并带随机密钥;密钥写在引擎运行目录里只有本账户能读的 settings.json(auth.api_key),不进命令行和环境变量;以 HF_HUB_OFFLINE=1 运行,不去网上找模型;运行底座模型 |
| 打分 / 联想工作进程 | 主控,首次请求或预热时;空闲 10 分钟停,下次用到再起 | 引擎包 Python 运行 score_worker.py(HF_HUB_OFFLINE=1),经管道通信,不监听端口 |
| 嵌入工作进程 | 主控,首次请求或预热时;空闲 10 分钟停,下次用到再起 | embed_worker.py,同上 |
| 空闲任务 | 主控 | 配件 .app 自带的 Python(或清单 exec 指定的 Contents/MacOS 下的可执行文件)运行清单中 jobs 的命令,环境带 LUOKE_HUB_SOCKET |
| 常驻配件 | 本体 | 以 --resident 启动;10 分钟内最多自动重启 3 次 |
| 记忆界面 | 本体 | 用主控自带的 Python 运行记忆的入口 |
| 高级用户端点 | 主控内的线程 | OpenAI 兼容,只听 127.0.0.1(17860 起,被占往后找);Host 只认 127.0.0.1 / localhost,带 Origin 头的请求答 403;连续请求按能耗模式歇息 |
生命周期
启动
- 本体判断是否以安装器身份运行:位于认得的安装位置(
/Applications或~/Applications下的螺壳.app、螺壳主控.app)时不是;否则文件名不是螺壳.app、位于/Volumes/或被系统隔离时是,进入安装流程。 - 不是装好的那份(按
/Applications/螺壳.app、/Applications/螺壳主控.app、~/Applications/螺壳.app、~/Applications/螺壳主控.app的顺序取第一个 bundle id 相同的)时弹框说明后退出。先装上菜单栏图标;上次换包没成时弹框说明原因(「App 管理」挡住时指明去系统设置里打开),升级标记作废;把luoke命令行链接改指到装好的那份;在后台清理上次经「检查更新」升级时留下的安装包(首次运行没走完也清)。首次运行未完成时显示首次运行向导(升级标记作废,有预选时预选上那个应用,主控进程到下载那一步才启动);否则启动主控进程,有升级标记(run/upgrade.json)时重新登记登录项(用户关掉过的不动)并接着显示升级卡片;没有则直接拉起常驻配件,有预选(run/preselect.json)时打开「应用」窗口在那一行里装。主控进程启动时注销previous.noindex里旧版的 LaunchServices 登记。 - 主控先绑定 socket 并应答(启动期间除
/tasks、/status、/downloads外答busy),再启动引擎:清理上次留下的孤儿进程、等待底座出现、做冒烟检查。 - 启动采样线程,进入运行状态;设置中开启了端点则开启端点。
运行
采样线程每 5 秒检查引擎是否崩溃,刷新电源、温度和内存压力,按需打断后台工作、卸载闲置底座、跑探针、每 60 秒重扫配件清单、调度空闲任务,并写入 state.json。本体每 3 秒轮询一次 /status。空闲超过 10 分钟的专用模型工作进程停掉;内存压力 critical 时全部停掉,30 秒内不起。
退出
本体发 SIGTERM,等待 40 秒。主控依次停止端点、空闲任务(SIGTERM,15 秒后 SIGKILL)、工作进程、引擎,最后删除自己绑定的 socket。
调度
请求等级
| 等级 | 来源 | 待遇 |
|---|---|---|
| 打字 | 打分、联想(输入法) | 立即处理;记为交互;16 GB 档会立即卸载底座让出内存 |
| 用户在等 | 生成请求带 X-Luoke-Priority: user-waiting;端点请求 | 优先;正在运行的后台工作若预计剩余超过 11 秒则被打断(返回 interrupted);端点请求先等完能耗模式的歇息再开始 |
| 后台 | X-Luoke-Priority: background;空闲任务 | 需通过后台接纳 |
交互中指最近 60 秒内有过用户在等的请求或新的预热;安静时刻指距最近一次此类事件满 5 分钟。
后台接纳
按顺序检查,第一条不满足即挡回:
| 条件 | 返回 | 原因 |
|---|---|---|
| 全机后台已暂停(电池、低电量模式、过热、内存压力) | paused | 各原因 |
| 底座放不下 | paused | memory |
| 交互中 | busy | interactive |
| 最近 180 秒打过分 | busy | scored_recently |
| 16 GB 档且不在安静时刻 | busy | not_quiet |
| 别的配件的空闲任务在运行 | busy | idle_job |
| 引擎里已有一项后台工作 | busy | engine_busy |
| 能耗模式的歇息中(空闲任务所属配件的请求不挡回,由主控挂着等歇息过去,最多 4 分钟) | busy | power_cooldown |
能耗模式(全局):全速、均衡、静音。每项后台工作、每条本机端点请求结束后,按占用时长的 0、1、3 倍歇息,端点一次最多歇 600 秒;均衡、静音下端点一次只放一条请求;打字和配件里用户在等的请求不受影响。静音模式下温度 fair 即按过热暂停后台。专用模型通道(嵌入)每个任务有独立通道和间歇,不占用引擎的后台名额;用户在等的嵌入查询插在后台批(每 8 条)之间。
内存
- 模型能否加载要过两道检查:大小不超过引擎上限
(guard_gb − 2) × 0.95;且不超过 Metal 可用上限减去当前 wired 内存(按物理内存的 74%–84% 估算)。端点的用户模型独占引擎时,上限改为 内存 − 8%(3–8 GB)− 常驻专用模型 − 1 GB,换回底座时还原。 - 起专用模型工作进程前先算剩余内存(Metal 上限 − wired + 已加载的底座),装不下它的
wired_gb就不起,答not_ready(retry_after 30)。 - 引擎上限随正在运行的工作进程动态调整。
- 引擎同时只有一个"当前模型";端点登记的小模型放得下时与底座同时加载,放不下则独占(先卸掉其他模型)。
- 底座闲置超过本档
idle_unload_secs后卸载;有后台工作或有效的预热租约时不卸。
空闲任务
同一时间只运行一个。启动条件:主控空闲、处于安静时刻、后台未暂停(清单可声明允许电池)、距上次启动满 min_interval_minutes(上次被收尾时缩短为 10 分钟)。用户回来、别的配件的后台工作占用引擎、超时、后台暂停、卸载配件或主控退出时收尾;任务自己发的请求和它引起的底座加载不算占用。
配方和档位
- 配方(
recipe.json)定义硬件档(内存范围、引擎上限、底座是否常驻)、模型(目录、类型、常驻内存)、每档的模型分配、引擎版本、每个任务的参数、允许接入的配件版本和评测证据。 - 选档:取第一个满足
min_gb ≤ 内存 < max_gb的档;用户可在设置中选择更低的档(如 128 档的"省空间")。 - 任务开关:每个任务在哪一档开启由出版流程的支持台账决定:未开启的档须 3 遍评测全部通过才开启(复测在发布后补跑,补满后下一版开启);开启的档一遍不过即关闭;没评到的档沿用上一版;"上一版"指最近一个真正发布出去的版本;beta 版全部重新评测、重新打包,不沿用别的版本的证据。
- 冒烟检查:引擎启动后测一次实际的预填和生成速度,后台估时使用实测值。
- 探针:打分、联想工作进程就绪后测 p90 延迟,超过配方
latency_ms时把任务标为较慢。
下载、安装、升级、卸载
- 下载:依据发布清单(每项有文件列表、大小、sha256、多个镜像地址)。多地址时先测速排序;每个地址重试 3 次,403、404、410 直接换地址;支持续传;sha256 对上才落位。本机已有同名模型(
~/.omlx/models、Hugging Face 缓存、LM Studio)时逐文件核对后 APFS 克隆,不重复下载。首次运行以后发现还缺模型时自动补下(失败后从 30 秒起加倍间隔,最长半小时);网络按流量计费或开了低数据模式时不自动补下,只在设置「下载」的「还缺」一行说明,点「重试」照下;首次运行、「应用」窗口、换档是用户点的,照下。下了模型或引擎包才重启引擎、重做冒烟检查,只装应用不动引擎;发布版的新引擎冒烟检查通过后,删除engines/里的其他版本。 - 配件安装校验:
codesign --verify --deep --strict、Team ID、bundle id 与清单一致、公证票据有效。 - 安装位置:主控 DMG 是拖放窗口,左边「螺壳」、右边「应用程序」快捷方式,拖过去装在
/Applications/螺壳.app。在 DMG 里直接双击「螺壳」时提示先拖进「应用程序」,另给「帮我装进去」。发布页只放螺壳的 DMG(Luoke.dmg),应用在「应用」窗口里装;应用的安装器(安装<应用>.app)和「帮我装进去」把主控拷进/Applications/螺壳.app(写不进去时拷进~/Applications/螺壳.app),去掉下载标记并登记;目标位置已有 bundle id 不同的同名应用时不覆盖,改放另一处,两处都被占才弹框请用户改名或挪走。已装的主控以实际位置为准:/Applications、~/Applications两处,螺壳.app、螺壳主控.app两个名字都认,只认 bundle id 相同的,两处都有时用/Applications那份:另一份被打开时(例如登录项还指着它)直接交给装好的那份后退出,装好的那份启动时把没在跑的另几份移进previous.noindex、注销登记并重新登记登录项(用户关掉过的不动);已装着主控而安装器不比它新时,把预选交给它后退出。应用(邮件助手等)照旧装在~/Applications。 - 首次运行:步骤条五步:这台 Mac(预检:档位、磁盘、系统版本、照实说明体验;内存不到 16 GB 或系统版本低于引擎要求时停在这一步)→ 选应用 → 确认下载 → 下载安装(逐项进度,主控排第一行;总进度的剩余时间在头 10 秒、下到 2% 以前不估;应用装好即打开,不等模型;含本机试跑)→ 完成;后来加装应用在「应用」窗口那一行里装,不走向导。离开下载页时注册登录项;下载页结束时分几种结局给出下一步:有项没下下来(先继续 / 重试)、试跑没过(重新检查 / 继续)、缺底座、主控没起来(重试会重启主控)。确认页上废纸篓里有旧输入法时提示。完成页:输入法的
--status答not_registered时调一次它的--register,每秒检查是否已添加;添加后输入法没在跑时在后台拉起它,可试打并一键切换(--select),切过去后试打框重新取得焦点,切不过去、或输入法报告还登记着别的副本时给「重新登记输入法」,重新登记后仍不行则提示注销重新登录;装了邮件助手时有一行写「已经在浏览器里打开了,第一次要连接邮箱。」,给「打开」;列出每个应用的通知权限(经启动器的--notify-status):已允许 / 还没问过(按钮发一条测试通知,让系统询问)/ 已关闭(按钮打开该应用的通知设置),未全部允许时每 2 秒刷新。 - 升级:同一张卡片走完五步:下载安装包 → 核对签名和公证 → 换成新版螺壳 → 补齐推理引擎和模型 → 本机试跑,大小、位置相同。有两条路:菜单「检查更新」(前三步在「检查更新」窗口里),或打开比已装的新的安装器(应用的安装器,或在主控 DMG 里直接双击「螺壳」;第三步在安装器的卡片里)。两条路都先把新版拷到环境目录的
staging.noindex并核对签名,写run/upgrade.json,起一个独立的换包脚本,再结束旧主控和记忆进程、自己退出。脚本等它退出(最多 2 分钟),把旧版移入previous.noindex,把新版换到目标位置并登记,再用/usr/bin/open以用户身份打开新版,结果写入run/swap-result.json。目标位置:已装在/Applications的还在那里;装在~/Applications的,/Applications写得进去就搬过去,否则留在原处;名字统一为螺壳.app。目标目录写不进去(标准账户的/Applications)时,脚本经系统的管理员授权框以 root 运行;授权框在旧主控退出之前弹出,取消则什么都不动、原来的主控照常用;只有换包两步用 root,打开新版时切回用户身份。换包没成时脚本把旧版放回并重新打开,旧版启动时弹框说明原因;脚本起不来或授权被取消时卡片写原因,给「先用着」和「重试」。新主控读到标记(读后删除)接着显示卡片:常驻配件在卡片一开始就拉起,补齐未装的引擎包和本档缺的模型,等本机试跑通过或推迟后写「已升级到 X」和这一版的更新说明;已装的应用有新版本时列出名字并给「去升级应用」。应用不随主控升级。补齐或试跑没成时卡片写哪一步、为什么,给「先用着」(标记写回,下次主控启动接着补)和「重试」。经应用的安装器顺带升级的,完成后到「应用」窗口那一行里接着装那个应用。 - 应用:菜单「应用」打开,列出这一版发布清单里的应用(组件配件不列)和清单外已装的应用,每个应用的安装、升级、回到上一版本、卸载都在它那一行里做。没装的标「未安装」,点「安装」由主控按清单下载、核对、放好(读写记忆的应用连同螺壳记忆一起装),装好后经
/usr/bin/open打开,常驻的以--resident在后台拉起,输入法装好后登记一次并提示到系统设置里添加;系统版本不够的写原因、不能装。首次运行没走完时,点「安装」或应用安装器交来预选,都回到首次运行并预选上这个应用。已装且清单版本更新的标「有新版本」、写「旧版 → 新版」,点「升级」只换这一个应用:普通应用先退出、换好后拉起(原来在跑的重新打开);输入法先换包,再结束旧进程并立即重新拉起。结果写在窗口顶部,没成时旧版照常能用。有可升级的应用时顶部提示「升级螺壳不会顺带升级应用」。「…」菜单里的「卸载」把这个窗口换成卸载确认,完成或取消后回到这里。菜单「应用」一行、「应用」窗口、检查更新窗口和升级完成卡片按同一判断提示应用有新版本:已装版本比发布清单旧、这台 Mac 的系统版本够、不是用户回到上一版本时换下的那一版。 - 检查更新:菜单「检查更新」打开窗口,按本机主控的渠道查:内部版查
luokelabs/luoke-internal-builds里的internal-*,beta 和正式版查luokelabs/luoke-releases里的beta*;每页 100 条往后翻,翻到出现这个渠道的版本为止(最多 10 页),取发布时间最新的一版,built_at更新才算;有新版本时列出发布说明的「更新说明」一节和大小,说明这里只升级主控、应用在「应用」里单独升,给「以后再说」和「升级到 …」。升级时在这个窗口里走前三步:下载主控 DMG(显示进度),核大小、sha256、公证票据,只读挂载后核 .app(螺壳.app或螺壳主控.app)的签名和 Team ID,拷到staging.noindex再核一次签名,卸下并删掉 DMG,起换包脚本(要管理员授权时在这里弹),然后主控退出,由新版接着显示卡片;连不上、核对不过、某一步失败时给「重试」(升级中失败还给「先用着」),原来的主控不动。螺壳已是最新、但已装的应用有新版本时,窗口里提示并给「去升级应用」。本机版、开发版不查,窗口里说明。默认自动检查(内部版每 3 小时、beta 每天一次;启动 60 秒后,之后每小时看是否到时间,查成了才算;开关在窗口底栏,升级中不显示),发现新版只在菜单「检查更新」一行显示「有新版本」,点它直接显示那一版,不自动安装。 - 回到上一版本:「应用」窗口里某个应用在
previous.noindex里有经过验证的上一版时,它那一行的「…」菜单有「回到上一版本(版本号)」,就在这一行里做:主控先核签名、Team ID、bundle id、公证,两处对调并注销备份目录里的登记;普通应用对调前先结束,输入法对调后再结束旧进程并立即重新拉起(不动输入源);结果写在窗口顶部。换下的那一版不再算作新版本,再选一次可换回。主控把每个被本版配方认可的配件版本,连同本版当时放行的任务(评测通过的,加上配方里没有评测的,如草稿、每日摘要),记入verified-history.json(每配件 3 个版本)。换回的版本、以及主控升级后还没升级的应用,只能用记录中的任务;记录外的任务答task_unavailable,提示到「应用」里升级。无记录的版本不提供回到上一版本。 - 卸载:入口在「设置 › 关于 › 卸载螺壳」(全部)和「应用」窗口某一行的「…」›「卸载」(该应用),直接到确认页,取消或完成后回到原来的窗口。输入法的输入源还启用时先停用(macOS 26 起不能代停,拦下并请用户在系统设置里移除),删完再核一次,并删除它的临时目录和锁;结束进程;删除 .app 时连同
previous.noindex里的旧版,并注销同 bundle id 的其他副本(含废纸篓里的);可选"一起从记忆里忘掉"和"删除数据"(删数据时先跑配件自己的 uninstall 命令)。只删除配件清单data中声明且符合配件自身标识的路径;不删除mem.db。完成页列出还留着的数据和位置。"卸载全部"删除环境目录、登录项、设置和命令行链接,把主控移到废纸篓后退出。 - LaunchServices 登记:装配件前先注销同 bundle id 的其他副本(含废纸篓里的);
previous.noindex里的旧版在换装、回滚后和主控启动时都注销登记;「检查更新」只挂载安装包、不打开里面的主控,拷到暂存处时注销两边;换包脚本注销移入previous.noindex的旧版、登记换上的新版。主控和安装器拉起别的应用一律经/usr/bin/open,不把自己记成被拉起者的父进程(否则 macOS 27 上已退出的父进程会一直留在程序坞)。 - 重新登记输入法(切不过去时):结束它、注销其他副本,用新复制的一份换回原位并重新登记,不改内容、不重新下载。
- 通知:首次运行、加装、升级都不发通知,结果只在窗口里。
故障和降级
| 情况 | 表现 |
|---|---|
| 引擎崩溃 | 以新端口和密钥重启;600 秒内崩溃 3 次熔断,生成任务保持 paused 直到主控重启 |
| 请求中途引擎出错 | 连接断开且尚无输出时自动重试一次;引擎在流里报错时不把错误当答复转给配件,内存类按内存不够处理,其余以 engine_failed 结束 |
| 模型放不下 | 用户在等的请求返回 needs_user 并附占用内存的进程列表;后台请求返回 paused |
| 引擎因内存拒绝(开始前答 400,或在流里报内存中止,尚无输出) | 卸载并重新加载底座以释放 MLX 缓冲池,重试一次;仍不行答内存不够 |
| 冒烟检查放不下 | 推迟,每 300 秒在空闲时重试 |
| 专用模型进程空闲或内存紧张 | 停掉;按需重起时答 not_ready(retry_after 2),内存不够时 retry_after 30 |
| 专用模型进程失败 ≥ 3 次 | 该任务暂停,不再重启 |
| 下载失败 | 分三类说明:服务器上找不到、连不上、核对不过 |
| 配件版本未通过验证 | 调用返回 unknown_accessory |
菜单和窗口
- 菜单:第一行是状态(启动中 / 工作中 · 底座模型 / 空闲 · 底座模型),后台暂停时下一行写原因(在用电池、低电量模式、过热、等内存);然后是 记忆(随主控带的时候才有)、应用、设置、检查更新,「应用」「检查更新」有新版本时写成「名称:有新版本」、值加粗;最后「退出螺壳」(⌘Q)。主控没连上或引擎出错时,第一行是「出错了」,下一行写原因(进程退出码、连不上的原因或熔断原因),给「重新启动螺壳」和「导出诊断信息」。菜单栏图标是单色海螺,五种状态,都不动:虚线(启动中)、实心(准备好了)、半满(在处理请求或后台任务)、空心(后台暂停)、实心带圆点(出错:连不上主控、引擎没在运行或熔断)。
- 窗口期间:主控的任何窗口(首次运行、升级卡片、应用、卸载、检查更新、设置、主控小窗口)开着时,主控出现在程序坞和 ⌘Tab 里,全关后回到只在菜单栏。同一流程只开一个窗口:卸载接替「应用」或设置窗口,安装器的升级卡片出现后「检查更新」窗口关掉。
- 再次打开主控:有安装器交来的预选时,首次运行没走完就回到首次运行并预选那个应用,否则在「应用」窗口那一行里装;没有预选时有开着的窗口就叫到前面;都没有且首次运行已走完时弹出主控小窗口,显示运行状态(在运行 / 启动中 / 没连上),说明主控在菜单栏的海螺图标里、找不到时可能的原因,给「打开菜单栏设置」,并列出 记忆(随主控带的时候才有)、应用、检查更新、设置 四个入口。⌘, 打开设置窗口。
- 设置:通用(能耗模式 全速 / 均衡 / 静音、使用电池时仍运行后台、登录时启动螺壳)、下载(有下载或缺模型时出现:逐项进度或排队中,缺的可重试)、模型(占多少空间——可选档多于一个时出现,磁盘不够换档时在下面写明;底座模型;换档等待时的说明;已下载的模型及大小;本机放行)、高级(本机端点)、关于(螺壳版本即发布号、导出诊断信息、卸载螺壳)。Finder「显示简介」里的版本也是发布号(如 beta1、internal-1027;
CFBundleShortVersionString),主控版本号在CFBundleVersion。档位、引擎、本机试跑在诊断信息里。 - 只给本体用的接口(下载、设置、端点、冒烟、卸载、回到上一版本、重新登记)按 socket 对端进程核对是不是本体,
/settings、/endpoint也认主控自带的luoke命令行;不是的答forbidden(403)。
文件
环境目录 ~/Library/Application Support/luoke-core/,详见「参考 · 文件位置」。日志只记元数据(任务、优先级、token 数、耗时、结果),请求文字不落盘、不进日志。
已知限制
- 内存压力会暂停和打断后台、停掉专用模型工作进程,但不主动卸载底座。
- 熔断不会自动恢复,需要重启主控。
/status、/jobs/run不核身份(/jobs/run只在内部版、本机版开放)。- 配方不支持撤回已发布的配件版本,也不声明最低主控版本。
- 端点只有
/v1/models和/v1/chat/completions;luoke launch只支持 pi。
记忆
螺壳各组件共用的本机记忆:一个 SQLite 文件。工具写入"发生了什么"(记录);记忆是唯一形成结论的地方,从记录归纳出带依据、分三档把握程度的理解,并在空闲时整理,产出想问用户的问题和建议。
仓库 luoke-mem(包 luoke_mem,仅标准库,Python 3.9+)· 版本 0.23.4 · schema 7 · 接口和表结构见「参考 · 记忆」
职责
- 保存三类数据:记录(records)、理解(notes,每条引用它所依据的记录)、想问的和建议(asks)。
- 归纳关于用户的理解:事实、关注的话题、不感兴趣、处理习惯、常用说法,以及只供精选使用的口味。
- 写入时遮去凭证;提供整份记忆的查看和管理界面。
运行形态
| 形态 | 如何启动 | 说明 |
|---|---|---|
| 库 | 工具进程内 Memory(path, tool) | WAL、secure_delete、10 秒锁等待;工具使用主控内置的那份 luoke_mem |
| 归纳任务 | 主控空闲任务:python -m luoke_mem --path {memory} learn,最短间隔 120 分钟、超时 30 分钟、不在电池上跑 | 文件锁 mem.db-learn.lock 保证同一份记忆只跑一个;收到 SIGTERM 时已完成的批次保留;记忆文件不存在时什么都不做也不创建;主控不可达或任务报错时退出码 1,标准错误只写计数和原因码 |
| 界面 | luoke://memory → 主控执行 -m luoke_mem app | 独立进程,监听 127.0.0.1 随机端口,带令牌;600 秒无请求退出 |
| 命令行 | python -m luoke_mem … | sources、forget-tool、ui、app、learn |
记录与理解
记录
每条记录有写入的工具、说话人(user、third_party、system)、性质(statement、activity、receipt)、话题(如 mail-read、ime-commit)、正文、结构化数据 data(可含原文)和真实发生时间 happened_at。
理解的类别
| 类别 | 含义 | 谁用 |
|---|---|---|
fact | 关于用户的事实 | 邮件分拣、起草 |
interest | 关注的话题 | 输入法、agent |
dislike | 不感兴趣 | 精选 |
preference | 处理习惯(发件人 × 邮件类别 → 动作) | 邮件助手自动执行 |
term | 常用说法 | 输入法候选加分 |
taste | 精选口味,不算对用户的了解,不提问 | 精选 |
三档把握程度
由状态推出,不单独存储:
| 档 | 条件 |
|---|---|
| 猜测 | 状态 proposed 或 observed |
| 比较确定 | 状态 active,用户未表态,来源是推断 |
| 确定 | 状态 active,且用户确认过或来源是用户本人 |
被否定(rejected)、被取代(superseded)、以前的事(past)不属于任何一档。工具读取时默认只得到比较确定以上的理解。
首要原则
记忆的每条规则都从宽开始,再由每个用户自己的反馈收紧(0.27 起,代码在 fit.py)。没有「这类不该提」的固定名单;同一条规则对不同的用户可以收到不同的松紧。不归反馈调的有三类:硬线(凭证遮掉、数据不离开本机、健康法律宗教政治只认用户的话、用户说过的不被推翻);算错了的 bug;用户没表态前,规则不替用户收回已经说过的话。
定档规则
一条推断能否成为"比较确定",取决于它所依据的记录话题和数量。门槛写作(条数, 不同日子数, 首尾相隔天数):
| 理解 | 能撑到"比较确定"的记录 |
|---|---|
| fact | sent-mail (2, 2, 7) |
| interest | sent-mail、own-site、digest-feedback (2, 2, 7);mail-read (3, 3, 60,另需合计打开 ≥ 5 封、发件人 ≥ 2 个);digest-open (3, 2, 28);ime-commit (3, 3, 0) |
| dislike | digest-feedback (2, 2, 7) |
| preference | mail-override (2, 2, 7) |
| term | ime-commit (3, 3, 0) |
| taste | 用户的任何记录 |
- 表中没有的组合(如
own-label、mail-cleared、mail-acted)最多是猜测。规则自动执行的记录(data.by不是 user)和事务邮件的打开不算依据。 - 名字只有品牌或来源名(如"Zillow""X 相关动态""领域:来源名")的话题不会成为兴趣,也不会成为精选口味。
- 第三方来信推出的事实:同一来源 ≥ 6 封、跨 ≥ 3 个月、打开 ≥ 2 封、最近 6 个月内仍有来信时直接生效。
- 健康、法律、宗教政治相关的推断必须由用户确认。
- 门槛按用户调整:每类提议从已有的表里现算(
fit.tally、fit.standing),不存第二份。「否定」分量 ≥ 2 且多于确认,升一档;≥ 4 且至少是确认的两倍,升两档;确认 ≥ 3 次且从未否定,降一档。分量:否定、在记忆页删掉、建议点「不用」各 1,接受后又撤回、建议点「别再提」各 2,问了一直不答到过期 0.5。删掉的只留每类的条数,不留内容;敏感的连条数也不留。 - 一类不牵连别的类:处理习惯按「动作 × 邮件类别」分,事实按话题的前缀(如
服务:、账户:),兴趣和口味按来源(打字或邮件),常用说法一类,退订建议一类。 - 门槛动了什么:处理习惯要的次数(升档后已有的也先不提);常用说法和打字话题生效要的天数、次数;打字话题升档后不提前问;退订建议要的清理次数;事实升档后,待确认的挪到「观察中」(
observed)。默认门槛不变。 - 以前的事:会变的理解(包括猜测和用户确认过的)最新依据超过 730 天,标为
past,不再使用也不再提问,相关的未答问题撤回;姓名、学校、家人等不会变的不受影响。用户说"现在还是"即恢复为确定;同一类被这样唤回 ≥ 2 次后,这一类的期限加倍为 1460 天。 - 以前的事再次出现(工具又提出同一条):只因太久而变成以前的事的,按原样回来——用户确认过的回到确定,否则为猜测、按新依据定档;用户自己说过"以前是,现在不是"的回到猜测,保留表态时间,新依据不能让它自行生效,只能再问用户。
- 邮件推出的兴趣:关于用户的、由邮件记录撑起的兴趣最多 8 条生效;重新定档时按依据条数保留前 8 条,其余回到猜测。
- 取代与合并:同类同话题的新理解生效时,取代用户未表态的旧推断("其他"话题除外);说法相近的话题合并为一条。同一机构的事实只留一条:机构类话题(「机构:X」,以及旧的「账户:」「服务:」「会员:」「学校:」「住房:」「出行:」「其他:」)的名字相同即合并,「同事:」「客户:」「工作:」这类不算机构、不合并;被否定的那条只挡住同样的说法,新说法并进已有那条;未表态的只在说法相同而更完整时换字,用户确认过的保留原话,新说法另记、反刍时问"现在还是这样吗"。已有的重复在反刍时并进先有的(确认过的优先);界面上可把并进来的说法拆出来,拆出的不再合并。
归纳
归纳任务每次依次执行:反刍、打字线、话题线、再反刍一次。反刍不用模型、很快,放在最前面,任务被主控提前收掉时也照样产出提问和建议。两条线都通过主控的后台生成任务调用模型,结果的数量和依据由代码判定,模型只负责提名。
打字线(luoke-mem.typing) | 话题线(luoke-mem.topics) | |
|---|---|---|
| 输入 | 输入法的 ime-commit(排除输入法自动上屏的),每段 ≤ 300 字 | 邮件助手的用户记录(排除规则执行的、事务邮件),最近 365 天,按来源聚合,每来源最新 3 封原文各 ≤ 280 字、合计 ≤ 1200 字 |
| 触发 | 新增 ≥ 80 段,或有新增且距上次 ≥ 1 天 | 上次没读完的来源立即继续;否则新增 ≥ 20 条,或有新增且距上次 ≥ 1 天;提示词版本变化时从头重新归纳 |
| 开跑前 | 先查 GET /tasks:这一档没有该任务、或任务状态不是 available 时不发请求,记下原因 | |
| 产出 | 常用说法(2–12 字)、关注的话题(≤ 20 字) | 关注的话题("领域:具体对象",≤ 24 字,每批 ≤ 5 个)、精选口味、不感兴趣(只来自精选里的"没兴趣",≥ 2 条记录) |
| 定档 | 说法:最近 90 天出现 ≥ 2 次为猜测,3 天 5 次生效;话题:依据跨 3 天生效,不足为猜测。说法只出现 1 次的留作候选,下次再数 | 依据 ≥ 2 条记录的话题成为精选口味(直接生效);同时按定档规则判断能否成为关于用户的兴趣(邮件推出的最多 8 条);只有品牌或来源名的丢弃 |
| 进度 | 每批完成后推进游标,被叫停时从未读的段继续 | 一开始就把要读的来源记入 learning 表 topics.pending,每批完成划掉一批,被叫停时下次从没读完的来源继续 |
| 提示词 | luoke_mem/learn.py PROMPT | luoke_mem/topics.py PROMPT(版本 5) |
主控答"忙""暂停""被打断"等表示稍后再来时,当前这条线停下,已完成的批次保留;主控因其他后台任务或能耗模式的歇息而忙时,打字线最多原地等 300 秒。
反刍
不使用模型,每次归纳任务开头和结尾各执行一次;结果计数写入 learning 表 rumination.last。
- 降档:打字归纳出的兴趣和常用说法,最新依据超过 90 天且用户未表态,回到猜测。
- 整理:用户未表态的推断(猜测和已生效的)每次反刍按现行规则重查:日常词的常用说法、靠长词里的字母才够门槛的常用说法(拉丁字母按整词数,「hub」不算进「github」;已生效的退回猜测,用户表过态的不动)、只有一段打字依据的话题、只有品牌或来源名或以"动态""通知"等结尾的兴趣和口味(口味在生成时就跳过这类名字)、只靠自建标签或整批清理撑起的兴趣和不感兴趣,收起不删;重复的事实并进先有的。
- 处理习惯:同一(邮箱, 发件人, 邮件类别, 动作)出现 ≥ 2 次形成一条习惯;来源是
mail-override的新建议和mail-acted的动作,只认ACTION_NAMES里的动作(archive、read、trash、spam、flag、keep、label、filter)。来自改建议的按定档规则判定,仅来自亲手处理的为猜测。用户撤销一次自动执行即回到猜测。改建议后当场处理只算一次(mail-acted的after_override覆盖整批,或 10 分钟内同一发件人、同一动作)。 - 想问的(都不问敏感的):兴趣和处理习惯里依据无法自行撑到比较确定、180 天内有依据的猜测;打字线再多一天就会生效的话题(这一类门槛升档后不提前问);同一来源 ≥ 6 封但最近 90 天没有来信、用户未表态的机构事实,和用户确认过、邮件里说法变了的机构事实(问"现在还是这样吗")。前两类在前,机构事实在后,各按最新依据和依据条数排序;每天最多 2 个,14 天过期;不再值得问的撤回。
- 常用说法不问用户:只影响候选排序,错了几乎没代价,够门槛自己记下,不要的在记忆页删。
- 建议:每天最多 3 条。"以后都这样?"(有发件人和类别、动作可自动执行的处理习惯,用户点"好"即成为确定并可自动执行);"退订?"(30 天内用户亲手整批清掉同一发件人 ≥ 2 次)。
- 去重:同一件事已有未答的问题时不再生成;"以后再说"或过期未答 7 天后、已答或"不用"60 天后才可能再问;"别再提"永不再问。
读写关系
| 组件 | 写入的记录 | 读取 |
|---|---|---|
| 邮件助手 | mail-read、mail-acted、mail-override、mail-auto、mail-undo-auto、mail-cleared、sent-mail、received-mail、digest-open、digest-feedback、own-label、own-site | 事实和习惯(分拣、起草)、可自动执行的习惯、口味、给它的 asks |
| 输入法 | ime-commit、ime-correction、ime-forget | 常用说法;可读全部记录 |
| agent | 只经 reply 交回回答 | asks 和全文依据 |
任何组件都能读取全部记录和依据;删除其他工具写入的内容只能在记忆自己的界面中进行。
已知限制
- 界面没有"以后再说"和单独的"以前是"按钮(只能通过修改框的文字识别)。
- 归纳中某条输入(打字或话题的一批)连续出错两次就跳过,游标前移;其他错误不打断整次归纳。
- 重新定档、以前的事只在整份记忆被打开时运行(界面、命令行、归纳任务)。
- 没有向工具索取更多原文的通道;自由回答不参与学习。
- 命令行
forget-tool luoke-mem被拒绝(说明原因,退出码 1):记忆自己的内容只能逐条删除或清空整份记忆。 - 敏感词和凭证识别基于正则,存在漏判和误判。
邮件助手
用本机模型整理整个邮箱:逐封分拣近期邮件,清理旧邮件,体检订阅并挑选精选,起草回复放入草稿箱。可撤回的处理会自动执行,并在"今日处理"页集中复查。
仓库 luoke-mail(包 luoke_mail)· 版本 0.18.14 · 支持 Gmail、QQ、网易 163/126、自定义 IMAP,可连接多个邮箱
职责
- 为每封邮件给出建议(回复、加入日程、待跟进、归档、移到垃圾箱),附类别、优先级、摘要、截止日期、日程字段、回复要点和可疑标记。
- 执行用户的操作并保留撤销步骤:归档、移到垃圾箱、移到垃圾邮件、加星标、标已读、加标签、写草稿、加入日历(Gmail 打开 Google 日历的新建日程页面,其他邮箱下载 .ics 文件)、退订。
- 不发信,不永久删除:删除即移到垃圾箱,回复只存入草稿箱。
- 把用户的操作写成记忆记录;关于用户的结论由记忆形成。
运行形态
| 入口 | 行为 |
|---|---|
--app --resident | 主控拉起的常驻实例:本机网页服务 + 后台循环。后台整理开启时经 PUT /accessory/resident 声明为常驻(清单里 resident: false),关闭或清空数据时先 POST /accessory/exiting 再退出 |
--app | 打开正在运行实例的页面,没有则在后台启动一个 |
--job clean|digest|learn | 主控调度的空闲任务(分拣以前的未读邮件、判断旧邮件 / 读订阅和挑精选 / 学习),每次最多 25 分钟,被叫停时做完当前一步退出 |
--forget-secrets | 卸载前删除钥匙串中的本工具条目 |
- 网页服务:只绑定 127.0.0.1,端口默认 8765(被占用时依次试后面 19 个;指定了就只用那一个);令牌保存在
token文件(0600),请求需带X-Luoke-Token;页面从地址里的?t=取得令牌后立即从地址栏去掉,按这个地址存进 localStorage,同一地址的各个标签页、服务重启或升级后都能用,接口返回 401 时清掉,没有令牌时显示「无法验证访问权限。请从「螺壳邮件助手」App 重新打开页面。」;校验 Host;HTML 带 CSP;请求体上限 1 MB;以server.lock保证单实例。 - 日志:常驻实例的标准错误写入
service.log,开头记版本;每条通知同时记入run/notifications.json。 - 后台循环:每 30 秒一次(有空闲任务时 5 秒),启动 60 秒后才开始。后台整理默认关闭,开启需确认说明并已连接邮箱、有模型;间隔默认 30 分钟(可选 15、30、60、120),对每个邮箱执行同步流程;一个邮箱失败不影响其他。
- 主控:经 Unix socket 调用任务(
X-Luoke-Accessory: luoke-mail/<版本>,优先级user-waiting或background);任务状态和参数(max_chars、batch、sender_verdict)从GET /tasks读取并缓存 60 秒;页面可见时预热luoke-mail.triage。打包版只经主控。 - 记忆:优先使用主控内置的
luoke_mem(GET /tasks的luoke_mem.path,要求 ≥ 0.23.0),以tool="luoke-mail"打开;记忆比库新时常驻实例以退出码 3 退出并提示更新。 - agent 接口:outbox 和 MCP 工具(
run/agent.sock)已实现但默认关闭,环境变量LUOKE_MAIL_AGENT=1才开启;清单不声明capabilities(备用声明在packaging/agent-capabilities.json)。开启时 agent 只能执行邮件助手自己做出的方案、只用卡片给出的选项,批量移到垃圾箱或垃圾邮件仍要处理模式;退订和开启处理模式不经 agent。
同步与分拣
- 读取收件箱最近 N 天(默认 30)的邮件,新邮件最多取 100 封,每 10 封读一批、读到就入库;连接断了重开一次接着读,再断就保留已读的,下次同步从断处继续。只取前 1 MiB、不改变已读状态;纯文本部分其实是 HTML 源码的按 HTML 读;长于 60 字符的链接写成
https://域名/…;正文去引用后截到 6000 字。 - 对照收件箱现状:已不在的标记为完成;推断用户在别处做了什么(归档、删除、读过、加标签,写入
mail-acted,where=elsewhere;每次最多查 30 封的去向);邮件助手 3 天内操作记录里动过的邮件不算用户的处理,分析后标已读不移动邮件、不在此列;记为已移走却仍在收件箱的放回待处理。 - 邮箱第一次同步时,把收件箱里当时未读、在分拣范围以前的邮件记成一个队列(新的在前,只记这一次)。
- 补充发件域名验证结论(Gmail 的 DMARC),以及用户最近 7 天自己归档但可能仍需处理的邮件(Gmail,最多 50 封)。
- 逐封分拣(
luoke-mail.triage)。命中的规则作为上下文提供给模型;模型可提出不同建议。寄出日期早于今天的邮件,提示词多一行寄出日期,没写年份的日期和「明天」这类说法按寄出日期换算,代码兜底triage.settle_dates。用户只在抄送、不在收件人里时,提示词多一行「一般 archive,priority 不超过 medium」,代码再把 high 降为 medium(triage.settle_cc)。任务参数sender_verdict为 false 时(16、32 GB 档),提示词里不给发件域名验证结论。 - 依次执行:最近归档的邮件只留需要用户处理的、精选来源排队、自动标已读、按习惯处理、处理螺壳自己的邮箱、更新往来邮件状态(每次最多问 10 串,只问有新邮件的)。
以前的未读邮件
- 第 3 步记下的队列作为空闲任务分拣(空闲任务中排最前,属于主控的
clean任务):每次取 10 封(取时不标已读),入库标items.older,以后台优先级交给luoke-mail.triage,每段最多 4 分钟;主控说暂停或模型出错时停下,下次再分。分好的当场标已读;照确认过的习惯归档、加标签(以及螺壳自己的邮箱的自动处理)在下一次同步时做,和其他待处理邮件一样。 - 同步不等它们,收件箱的"未分析"数也不计它们;"清理 → 收件箱中的旧邮件"不再列出它们。
- 以后用户自己标回未读的旧邮件不再排进来;分拣范围放大后,新范围里的邮件由正常同步分拣。
安全规则
- 邮件内容视为数据。试图指挥 AI、索要密码或验证码、冒充、催促转账的邮件标为可疑;只要模型判为可疑,动作一律改为忽略、类别改为钓鱼。
- 页面渲染不使用 innerHTML。
自动处理
| 动作 | 条件 | 撤销 |
|---|---|---|
| 标已读 | 收件箱中已分析、没出错的邮件(设置 flow.mark_read,默认开);每次同步一条操作记录(动作 seen);邮件助手标的已读另行记录(read_by_us),不当作用户读过 | 今日处理中逐封"标为未读",或在操作记录中整批撤销("分析后标为已读 N 封");撤销过的不再自动标。不写入记忆(不是用户的动作) |
| 按习惯归档、标已读、加标签 | 记忆中确定的处理习惯(发件人 × 邮件类别);跳过可疑、需用户处理、高优先级、登录和安全类通知、撤销过的邮件 | 今日处理中逐封撤销,记一次反证(mail-undo-auto) |
| 螺壳自己的邮箱 | 设置中标为 own 的邮箱:照建议归档或移到垃圾箱 | 今日处理中逐封撤销 |
用户自己的邮箱里,移到垃圾箱、垃圾邮件、退订不会自动执行。处理模式(默认关,开启需确认)只控制不可逆的批量操作:批量移到垃圾箱或垃圾邮件、含删除的清理、清理某发件人的历史邮件。需要处理模式的批量按钮照样能点,旁边写明需要处理模式,点了弹出开启说明,开启后接着执行。
通知
系统通知由后台整理在每个邮箱同步后发出。agent 正在读 outbox 时(10 分钟内读过),新邮件、快到期、精选、清理分析完、今日处理这五类不再发通知,交给 agent。收件箱中的旧邮件全部分析完时通知一次建议归档、移到垃圾箱、待确认各几封,点开进入清理。
- 需要回复、加入日程、待跟进的新邮件(3 天内到达、非可疑):封数和最急的两封。
- 按习惯自动处理后:处理了几封,点开进入"今日处理"。
- 截止日期在明天及以前的:每件只提醒一次。
- 精选:每周一 08:00 之后的第一次后台整理;今日处理:每晚 20:00 之后一次。
- 通知权限:经应用的启动器(
--notify-status)查询 macOS 是否允许本应用通知,缓存 1 分钟。设置的"后台整理"一行:被拒绝或提醒关闭时显示警告和"打开通知设置"(打开系统设置中本应用的通知页,回到页面时重新检查);从未询问过时提示点"发送测试通知"并选"允许"。"发送测试通知"发一条测试通知并说明结果。 - 邮箱连接失败连续 2 次才报,同一问题只报一次;模型不可用时报一次"N 封新邮件尚未分析";钥匙串因锁屏读不到时不报,下一轮再试。
页面
| 页面 | 内容 |
|---|---|
| 今日处理 | 某天每封邮件的结果,需要用户处理的排在前面;可撤销、标未读、改建议;可查看前 7 天。补分的以前未读邮件单列"以前的未读邮件"一节,仍等用户处理的标"已过期"(截止日期已过)、"快到期"(3 天内)或收到日期(一个多月前收到) |
| 收件箱 | 同步、分组显示建议、快到期(3 天内到期或过期不到 7 天)、等对方回复的往来、逐封或批量处理、草稿、日历。卡片时间显示「今天 HH:MM」「昨天 HH:MM」或日期;类别和优先级相同的,新的在前。日程已过的不进快到期,显示「日程已过」;同一预约已从本工具加过日历的显示「已在日程中」 |
| 清理 | 收件箱中的旧邮件和已归档邮件:只读邮件头,模型在后台逐封判断,按类别和来源分组执行;星标和保留类标签的邮件不参与;切换邮箱或标签后迟到的答复不再绘制,分析开启但在等其他任务时显示"已分析 x / y" |
| 精选 | 最近 7 天约 20 条、更早每月约 10 条;收藏和"没兴趣"写入记忆 |
| 订阅体检 | 按发件人统计,建议退订、无法退订、可疑勿退订或常读;可退订、批量归档、清理历史、生成规则 |
| 规则 | 按发件人、域名、邮件列表的规则;导出 Gmail 过滤器前用过去一年邮件预演命中数和误伤;操作记录可撤销 |
| 记忆 | 想问你的、全部类别的理解及依据、用户网站(每 30 天重读一次)、学习的暂停与继续、打开整份记忆 |
| 设置 | 邮箱(含"螺壳自己的邮箱")、本机模型(打包版显示"螺壳 · 模型名"和就绪标记,模型名取 GET /tasks 中分拣任务的 model_name,不解析 combo;换档在主控设置里)、处理模式、后台整理(含通知权限提示和"发送测试通知")、自动标已读、往来邮件、功耗、分析范围 |
页面在浏览器的 localStorage 中记住上次查看的邮箱,刷新或重新打开后仍停在该邮箱(仍连接时);示例邮件只在还没连接邮箱时出现;存不了时显示第一个邮箱。
主控任务
| 任务 | 提示词 | 输入 → 输出 | 批量与失败 |
|---|---|---|---|
triage | triage.py | 一封邮件(含寄出日期、抄送、发件域名验证结论)、规则、关于用户(≤ 600 字)→ 类别、优先级、动作、摘要、截止日期、可疑等 | 一次一封;两次失败后记错误,下次同步重试。以前的未读邮件以后台优先级、每批 10 封 |
draft | triage.py | 回复要点 + 邮件 → 正文 | 一次 |
senders | triage.py | 发件人最近 8 个主题 → 类型 | 每批 ≤ 10;失败则不改判 |
cleanup | triage.py | 邮件头 → 归档、删除、保留类别 | 每批 ≤ 25;失败一律保留 |
digest-read | digest.py | 一封订阅正文(≤ 12000 字)+ 口味 → 条目(≤ 8)、评分、摘要 | 一次一封,每次取 3 封排队;模型内存不足时缩到 4000 字重试;格式不对重问一次 |
digest-pick | editor.py | 候选条目 → 入选及理由 | 每批 ≤ 30;失败按阅读分数排 |
learn-sent | learn.py | 用户发出的邮件(只留本人写的部分)→ 事实 | 每批 ≤ 8 |
learn-orgs | learn.py | 往来机构的封数和主题 → 事实(机构名 ≤ 40 字,事实 ≤ 60 字;写入记忆的话题为「机构:名称」,类别只留在邮件助手) | 每批 ≤ 10,最多 30 个机构 |
thread | threads.py | 往来串最后 ≤ 6 封(每封 ≤ 600 字,末尾注明最后一封是谁写的)→ 轮到谁(user_turn / other_turn / done,代码映射为等你 / 在等对方 / 已了结)、从何时、要做什么 | 每次同步最多 10 串;两次答案不可用判为已了结;主控忙或出错时停下,下次再问 |
写入记忆的记录
所有记录的 data 带 sender、by(user / rule / suggestion)和 account(邮箱的不透明键);正文经凭证遮蔽。示例邮箱不写真实记忆。
| topic | 说话人 / 性质 | 何时写 | 主要字段 |
|---|---|---|---|
mail-read | user / activity | 每个订阅每个整月一条(群发或有 List-Id) | 封数、打开数、类别、主题、最多 3 封原文(事务类不带) |
mail-acted | user / activity | 用户在页面或在别处归档、删除、标垃圾、读过、加标签 | 动作、封数、类别、where(here / elsewhere)、标签、after_override(同一步里先改了建议的封数,有才写) |
mail-override | user / activity | 用户改建议 | 原建议、新建议(用记忆的动作名:trash、keep、archive 等;分拣原始代码在 suggestion)、类别 |
mail-auto | user / activity,by=rule | 习惯自动执行后 | 习惯编号、动作、封数 |
mail-undo-auto | user / activity | 撤销习惯的自动处理 | 习惯编号 |
mail-cleared | user / activity 或 receipt | 清理执行后 | 发件人、类别、封数 |
sent-mail | user / statement | 从发出的邮件中抽出事实(至少两封一致);用户发起的邮件主题 | 本人写的部分(≤ 800 字) |
received-mail | third_party / statement | 往来机构的推测依据 | 封数、首末月份、月数、打开数 |
own-label | user / statement | 用户的邮箱标签 | 封数、首末日期(happened_at 为标签下最新一封的日期) |
own-site | user / statement | 用户网站的文章 | 发布日期 |
digest-feedback、digest-open | user | 精选中收藏、没兴趣、打开 | 标题、来源、摘要 |
从记忆读取
| 内容 | 用途 |
|---|---|
context(["fact","preference"], 600) | 分拣和起草的"关于用户" |
确定且可自动执行的 preference(本邮箱、动作 archive / read / label) | 按习惯自动处理 |
context(["taste","interest","dislike"], 500) + 最近的收藏和没兴趣 | 精选阅读和挑选 |
asks(tool="luoke-mail") | 记忆页"想问你的" |
数据和文件
| 位置 | 内容 |
|---|---|
~/.luoke-mail/config.json | 邮箱、模型、处理模式、后台整理、功耗、学习、往来、自动标已读等设置 |
~/.luoke-mail/luoke.db | 邮件和建议(items,older 标记补分的以前未读邮件)、规则、发件人决定、缓存、清理(old_mail)、操作记录和撤销步骤(log)、精选、学习候选 |
钥匙串 luoke-mail:imap、luoke-mail:engine | 邮箱授权码、模型服务密钥;Mac 锁定时读不到会跳过本轮,不视为未保存 |
service.log、app.log | 常驻实例的标准错误(启动时超过 2 MB 先另存为 service.log.1);从应用后台启动的实例的标准错误(启动时超过 2 MB 另存为 app.log.1);两份日志每行带时间,一分钟内的重复行只计数 |
run/notifications.json | 最近 100 条通知:时间、标题、发送方式、启动器的结果(posted / not allowed / failed / no answer) |
token、server.json、server.lock | 页面令牌(0600);运行中实例的 pid、端口、是否常驻;单实例锁 |
idle.lock、resident | 空闲工作锁(应用和主控的空闲任务不同时做);后台整理已开启的标记 |
account-salt | 生成邮箱不透明键(写入记忆的 account)的随机值;删除数据目录后邮箱得到新键 |
run/agent.sock | agent 接口,仅在 LUOKE_MAIL_AGENT=1 时存在 |
可靠性
- 写入核实:移动后检查是否真的移走,没移走的不标已处理;不支持 MOVE 的服务器先确认副本存在才标删除,只对自己标记的邮件做 UID EXPUNGE。
- 撤销:先用上次见到的 UID 找并核对 Message-ID,再按 Message-ID 搜索,不是 Gmail 的再查文件夹里最新 500 封;改动前先备好撤销步骤,断线时记"结果未确认"并保留撤销。
- 主控不可用:邮件照常入库、暂不分析,主控就绪后自动开始;模型忙时用户等待的请求原地等,后台任务延后。
- 邮箱差异:网易登录后需发 IMAP ID;QQ 先查 CAPABILITY 再用扩展命令;Gmail 归档即移到"所有邮件",标签用
X-GM-LABELS。
已知限制
- 发件域名验证、最近归档的分拣、加标签、过滤器导出只支持 Gmail。
- 单封移到垃圾箱和退订不需要处理模式。
- 往来邮件只看最近 30 天、已发送中最新 60 封,按主题和对方一人归串,等待阈值固定为 3 天。
- 撤销过自动标已读的邮件仍记为"邮件助手标的已读",用户之后自己打开它也不计入订阅的打开数。
输入法
macOS 输入法。壳负责按键、组字、候选窗和上屏;候选全部由沙盒里的引擎子进程给出。个人化组件把上屏写入记忆,并把打分和联想请求转给主控。
仓库 luoke-ime · 版本 0.1.32(build 35)· bundle id ai.luoke.inputmethod.Luoke
结构
| 部分 | 实现 | 职责 |
|---|---|---|
| 壳 | Swift:LuokeShell(平台无关的状态机和引擎宿主)+ LuokeIME(InputMethodKit 外壳);另有共用状态机的命令行版 | 按键、组字、候选窗、上屏、沙盒、转问 |
| 打字引擎 | 自带小鹤双拼 xiaohe、自然码双拼 ziranma、全拼 quanpin(苹果转换引擎和 RIME 出候选)。自然码和小鹤是同一个程序,以 --scheme ziranma 运行,只换韵母键位表,声母、零声母、词库和 RIME 的数据都和小鹤共用(RIME 用雾凇自带的自然码方案);评测时加 --from-xiaohe 把小鹤的按键换成自然码的,600 句语料上和小鹤一样(先给的 78.8%,重排后 89.8%) | 出候选,经个人化组件请求打分后重排。三个打字引擎都有评测用的选项 pause(毫秒,默认 0)和 pause_when(slow / always):键后停顿够久才重排;默认关。 |
| 个人化组件 | engines/personal(Python,仅标准库),运行在主控自带的 Python 上 | 写入记忆;把打分和联想转给主控 |
进程模型
- 壳由系统拉起,不进程序坞;用户自己打开 LuokeIME.app 时也作为输入法服务运行。每个输入框一个
Session;全进程一个EngineManager,主线程每秒轮询配置和引擎状态。带不认识的参数时打印用法退出。只运行一份:父进程不是 launchd 时直接退出;用$TMPDIR/ai.luoke.inputmethod.Luoke.lock文件锁占位(最多等 3 秒),已有一份时请在跑的那份按需弹添加引导,自己退出。 - 换版:每 5 秒读一次磁盘上包里的 build 号,和启动时不同且新包的可执行文件在时,等所有输入框空闲(不在组字、没挂着逗号、不在快捷键会话、联想没打开)且 3 秒没有按键、设置窗口和添加引导都关着,收尾退出,1 秒后用
open -g从原路径拉起新版。 - 引擎以
posix_spawn在独立进程组中启动,命令为/usr/bin/sandbox-exec -p <模板> …,工作目录是引擎文件夹。run[0]为luoke-python时使用螺壳(主控)包内的 Python:依次找/Applications、~/Applications下的螺壳.app、螺壳主控.app,核对 bundle idai.luoke.core,不问 LaunchServices;都没有时用 PATH 里的python3;参数前加-B -E -s -u。自带引擎用固定 PATH;第三方引擎第一次启动时才解析登录 shell 的 PATH(最多 3 秒)。 - 通信:标准输入输出上每行一个 JSON,第一条必定是
hello;写入走每个引擎自己的串行队列。 - 停止:关闭 stdin,1 秒后对整个进程组 SIGKILL。
- 崩溃与卡死:一次询问 1 秒没有答复视为卡死并杀掉;60 秒内崩溃 3 次则停用该引擎。
- 退出:收到 SIGTERM、SIGINT、SIGHUP、正常结束或换版让位时,结束系统日志订阅,并向主控发
POST /accessory/exiting(1 秒超时)。 - 重启条件:设置文件变化、用户引擎目录变化、非自带引擎的文件变化、
options.json变化、主控 Python 被替换(每 10 秒检查)。
引擎协议
版本 1。完整规格见 luoke-ime/docs/引擎协议.md。
| 方向 | 消息 |
|---|---|
| 壳 → 引擎 | hello、query(输入、前文、应用)、committed、deleted、forget、forget_all、unforget、start / stop / cancel(快捷键引擎)、ask、reply |
| 引擎 → 壳 | answer(候选、more、autocommit)、show、commit、end、ask、reply |
引擎说明 engine.json
- 必填
protocol(=1)、name、kind、run;打字引擎需input_keys,快捷键引擎需hotkey。 - 可选
context(0–2000 字前文)、app、feedback、forget_all、asks(⊆ personal、hub)、options、punctuation。 permissions:network(none / local / any)、write(≤ 16 个路径)、local_ports、microphone、camera、screen、gpu、memory(只有个人化组件能声明)。- 声明
asks: personal的引擎只能network: none,或local并用local_ports限定端口;个人化组件不能联网,不能声明麦克风、摄像头、屏幕,asks里只能有hub。
第三方引擎
- 声明联网、GPU 或写权限的第三方引擎不能启用。
- 启用需要用户确认,确认记录绑定引擎说明的指纹(关键字段的 SHA-256),说明变化后需重新确认。
- 不会被自动选中;与自带引擎同名时使用独立的数据目录
<id>.third-party。 - 引擎只能向个人化组件请求
score和ping;predict只有壳能发起。
候选流程
- 按键经
MacKeys.translate翻译,进入会话;空闲时按下输入键会读取前文(安全输入时不读)。 - 壳向打字引擎发
query;只采用最新一次询问的答复,50 ms 内收到的最后一份直接显示。 - 打字引擎先给快答复(改选过的词、苹果候选、RIME 补充),能重排时标
more;再经壳把score转给个人化组件,个人化组件转主控luoke-ime.rerank,最多等 0.7 秒。 - 逐词重排:模型得分须比苹果整句首选高出阈值(自然对数 3)才替换;换掉首选时补发答复,界面让新首选淡入。
- 空格、数字、标点等收尾键在还没有任何答复时先等第一份答复,最多 1 秒(判定卡死);卡死则按原始字母上屏,数字原样交给应用。之后最多再为补发等 150 ms。回车、Esc 不等。上屏后,空格或数字选中的会接着请求联想。
- 拼音显示位置:默认显示在输入框里(显示串),可按全局或应用改成显示在候选窗里。
- 「-」:第一页按「-」(翻页键为 - 和 = 时),已选部分和拼音串照原样字母上屏,再交给应用一个「-」(可打 luoke-ime、gpt-4);不在第一页照旧翻页。
- 数字后的标点:数字后按逗号先挂在光标处,接数字上屏半角「,」,接别的键上屏「,」,退格就不要;数字后的「.」「:」保持半角。
- { }:中文状态下默认打成「」(设置里能关),不看引擎的标点表。
- 忘掉一个词:候选上按 Shift+⌫,告诉打字引擎和个人化组件。
联想
- 在空格或数字选词、标点、快捷键引擎上屏、选中联想后触发;开不开按应用算:应用设置优先,没设时终端(Terminal、iTerm2、Ghostty、Warp 等十个)默认关,其余跟随全局;个人化组件须在运行。上屏后 0.25 秒内没有按键才发出请求,期间按键即取消。
- 个人化组件先从最近 5000 段上屏中找用户写过的接续(最多 3 个),再请主控续写(
/v1/continue,k=8、6 个 token),首 token 是汉字且概率 ≥ 0.2 才采用,合计最多 5 个。 - 发出后 1 秒内无答复则放弃;未激活时淡显前 3 个、不放显示串,按键照常交给应用,4 秒后收起;按 Tab 激活后列出全部,数字、空格选,方向键和 Tab 移动,Esc、回车、退格收起;激活后高亮的那个放在光标处当显示串(拼音显示在候选窗里时也一样),免得 Chromium 把 Tab 交给网页。
- 有的应用(Grok 这类 Electron 应用)在输入法接下 Tab 后自己又插一个制表符:选词时连它一起换掉;收起联想时不去掉。
个人化组件
决策表(PersonalPolicy)
| 情形 | 写入记忆 | 引擎学改选 | 打分和联想 |
|---|---|---|---|
| 安全输入 | 否 | 否 | 否 |
| 用户暂停或该应用禁止记录 · 自带组件 | 否 | 否 | 是 |
| 用户暂停或该应用禁止记录 · 第三方组件 | 否 | 否 | 否 |
| 记录未开启 · 自带组件 | 否 | 是 | 是 |
| 记录未开启 · 第三方组件 | 否 | 是 | 否 |
| 其余 | 是 | 是 | 是 |
"记录开启"对自带组件默认为真(存储用户关闭的列表 memory_off),对第三方组件需用户同意(memory_consent)。
写入记忆的记录
| topic | 内容 | data |
|---|---|---|
ime-commit | 同一应用中连续上屏合成的一段(间隔 > 60 秒、换应用、换了出字的引擎或前文对不上时断段;满 2000 字或停手 20 秒写入;切走时的自动上屏单独一条) | app、source、kind、auto、context(前文末 200 字,标为第三方) |
ime-correction | 选了非首选、删掉刚上屏的字 | 选择或删除的明细 |
ime-forget | 用户在候选中删除某词 | input、text |
- 凭证经
mask_secrets只遮那一串;敏感类别(健康、法律纠纷、宗教、政治)按整段文字计算。 - 保留期(个人化组件选项
keep,只能改options.json)可选永久、365、90、30 天,启动时和每 6 小时清理一次,只清ime-commit、ime-correction,被理解引用的保留。 - 壳每 15 秒 ping 个人化组件取记忆状态,菜单和设置窗口显示「记录中 / 已暂停 / 当前应用禁止 / 已关闭 / 不可用」;第一次用时在候选窗提示一次在哪里关。
- 读取:最近 5000 段
ime-commit,用作联想素材。
主控任务
| 任务 | 端点 | 用途 |
|---|---|---|
luoke-ime.rerank | POST /tasks/<task>/v1/score | 候选打分(按任务参数分批,默认 16 个、前文 2000 字) |
luoke-ime.predict | POST /tasks/<task>/v1/continue | 联想续写 |
请求超时 2 秒。收到 hello 时对两个任务各预热一次。发布版只经主控打分(选项 scorer 写什么都当 hub);主控不可用时没有重排,联想只给用户自己写过的接续。
设置和文件
| 位置 | 内容 |
|---|---|
~/.luoke/ime/settings.json | 当前引擎、全局默认(进入应用时中英文、记住各应用模式、拼音显示位置、联想)、候选窗(排列、每页数、字号)、键位(翻页键、单按 Shift、{ } 打「」)、中英文间自动空格、按应用覆盖、记住的各应用模式、开发模式、记录设置、确认过的第三方引擎、主控 socket;第 1 版读入时迁移并另存 settings.v1.json;本版不认识的键(顶层和 recording 内)原样写回,配件回滚后再升级时不丢 |
~/.luoke/ime/engines/<id>/ | 用户安装的引擎 |
~/.luoke/ime/data/<id>/ | 引擎数据:options.json;小鹤、全拼另有 memory.json(改选、忘掉的词)和 forgotten.json(供设置窗口「记忆」页列出、撤销) |
~/.luoke/ime/records/ | 开发模式记录,留 7 天 |
设置窗口四页:通用(输入方案、中英文、排版、候选窗口、联想、「记录输入内容到记忆」和「暂停所有记录」)、应用(每个应用的默认输入模式、拼音显示位置、联想、禁止记录,可恢复跟随全局)、记忆(记了什么、打开螺壳的记忆界面、当前方案学到的词和忘掉的词;忘掉的可逐个撤销,学到的可确认后全部清掉)、高级(开发模式记录、记录文件夹、引擎文件夹、设置文件)。每行带一句说明;窗口里不放文本框。
输入法菜单只放常用操作:输入方案、当前中英文模式、清掉学到的词、当前应用(此应用设置、在此应用关闭联想(按实际是否联想打勾,终端里取消勾选即打开联想)、在此应用禁止记录)、记忆记录状态(开启 / 暂停 / 恢复、管理记忆)、设置、高级(开发模式、打开引擎文件夹、打开设置文件);引擎停用、沙盒拦截、等确认的第三方引擎也列在这里。
安全
- 安全输入(
IsSecureEventInputEnabled):不读前文、不写记忆、不打分;一段组字中碰到过安全输入,整段都按安全输入处理。 - 沙盒:默认拒绝;禁止读取钥匙、凭据、浏览器、邮件等 40 多个路径和整个
~/.luoke(只放行自己的文件夹);不许创建符号链接;禁止借助open、osascript、剪贴板等外发。 - 拦截上报:每条拒绝带标签,壳读取系统日志,严重的列入菜单。系统日志订阅(
log stream)套在一个 sh 里,输入法退出(含被强制结束)时随之结束。
安装与添加
LuokeIME --register 只注册(macOS 26 起不再请求启用,须用户在系统设置里添加);--status 输出一行 JSON(state:not_registered / not_added / added_not_selectable / ready,另有 registered、added、consent_list、tis_enabled、tis_selectable、current、other_copies(含废纸篓里同一 bundle id 的包)、version),供螺壳的首次运行向导和诊断用;--select 切换后另起进程核对,调用失败退出码 1,调用成功却没切过去退出码 3;另有 --disable、--current。配件清单的 capabilities.listing 给出「添加应用」时显示的一句话介绍(tagline:「小鹤、自然码双拼和全拼,本机模型帮你挑词、往下联想,从你打的字里学」)和类别「输入」(category),不写 permissions。用户自己打开 LuokeIME.app 而还没添加时弹添加引导,每秒检查,加好后提示去任意输入框试打。引导和安装、卸载脚本的提示都用系统设置的原词「键盘 → 文字输入 → 输入法 → 编辑…」;引导卡片右上角能点开「图文教程」:三步,每步一张系统设置的截图(简体中文、浅色,缩到 900 宽)配一句话,可以上一步、下一步、返回,加好了自动退出(--render-guide tutorial1|2|3 能单独画出这几页)。螺壳首次设置的完成页用同一套教程。
自带的 librime 在编译时把源码路径换成相对路径(-ffile-prefix-map),包里的库不带编译机的目录。
系统要求
螺壳只在 macOS 26 起安装输入法(清单 min_macos 26);LuokeIME.app 本身的最低系统是 14.0,供手动安装验证旧系统。
已知限制
- 个人化组件不读记忆中的理解(常用说法等),打分没有个人化。
- 候选窗不能用鼠标点选;引擎选项没有 A2UI 界面。
- 快捷键引擎不检查冲突、停用不进菜单。
- 个人化组件卡死不检测;转问没有限流。
- 没有打错纠正。
- 快捷键引擎上屏后的退格不转给个人化组件。
- 引擎停用、被沙盒拦下只在候选窗提示和菜单里显示,不发系统通知。
- 收起联想时,应用自己插的制表符留着。
agent
螺壳的后台组件,螺壳里唯一主动找用户的部分。以后并进螺壳:不在「应用」列表里单独勾选,随螺壳装好,设置里有「主动提醒」开关;现在的独立开发版「螺壳 agent(开发)」是过渡形态。它把记忆的提问和建议、各工具要报的事合并成卡片和简报,挑时机告诉用户,并代表用户调用工具声明的能力。当前版本不使用模型。
仓库 luoke-agent · 核心 0.0.1 · 自带 luoke_mem 0.23.4 · 按代码 536f134 核对 · 不在 v1 首发中,和体检、螺壳窗口进同一个 v1.x
职责
- 从记忆读取待问的问题和建议(
asks),从各工具读取待报事项(outbox),统一排序后在窗口、通知和简报中呈现。 - 把用户的回答交回来源:交给记忆的经
Memory.reply,交给工具的经工具协议调用对应能力。 - 只在用户点按卡片时发出会改动数据的调用。不自行推断用户信息,也不替工具执行动作。
进程模型
| 部分 | 实现 | 职责 |
|---|---|---|
| 壳 | Swift 应用(macOS 14+),菜单栏常驻,依赖 A2UI 渲染包 | 菜单栏、窗口、系统通知、卡片渲染、上报用户是否在场 |
| 核心 | Python 包 luoke_agent(仅标准库),运行在主控自带的 Python 上 | 读记忆、连接工具、事项库、决定何时说 |
- 启动:设计上主控按配件清单(
resident: true,登录时带--resident,不开窗口)拉起壳;现在配方和发布流水线里都没有 agent,主控不装也不拉起它。壳起核心的命令是<python> -B -E -s -m luoke_agent serve --home H --memory M --parent-pid P,可再带--hub S、--tool 配件=socket(可多个)、--memory-app;环境只给PATH、HOME、PYTHONDONTWRITEBYTECODE、LUOKE_AGENT_PYTHON,标准错误追加到<home>/logs/core.log。 - 借主控的 Python:壳按 bundle id
ai.luoke.core找到主控,用它的Contents/Resources/python/bin/python3;每次起核心都重新找,主控换版本、改名、挪位置都跟得上。 - 存活:核心意外退出时壳会重启它,间隔从 2 秒翻倍到 60 秒;核心每 2 秒检查父进程,壳退出时随之退出。核心同时盯着
LUOKE_AGENT_PYTHON指向的文件:它被换掉(设备号或 inode 变了),或路径消失超过 30 秒,核心以退出码 75 退出,壳立刻重起,不等间隔,并把间隔复位为 2 秒;核心稳定跑过 60 秒后才退出时,间隔也复位。 - 壳与核心之间:
<home>/run/agent.sock上的 HTTP/1.1,见「界面协议」。 - 轮询:核心每 5 秒一个周期;工具 outbox 每 60 秒读一次,记忆和主控每 300 秒各读一次,简报到点前 5 秒起每个周期都读记忆,直到简报发出;窗口打开时立即把全部来源读一遍。一个来源出错不影响其他,暂时不在运行的来源卡片先收起,回来后照旧。
- 使用的主控接口(只在给了
--hub时):GET /accessories发现已安装的工具(只认capabilities.protocol为 1 且有 socket 的,排除自己);主控没答时保持现有工具,答了而清单里没有的工具算已卸载,卡片撤下、事项记为撤回;--tool指定的不受影响。核心每次启动时经GET /tasks取主控内置的luoke_mem路径,取不到或导入失败时用自带的副本。壳正常退出时调POST /accessory/exiting,告诉主控别再拉起。
打包形态
| 形态 | 身份 | 怎么运行 |
|---|---|---|
| 开发版 | ai.luoke.agent.dev,「螺壳 agent(开发)」 | 必须显式给 --home、--memory、--python,缺了就在窗口里报错、不起核心 |
| 试用版 | 开发版加 trial.json,由 app/install_trial.sh 装成 ~/Applications/螺壳 agent(开发).app | 读 ~/.luoke/mem.db,数据在 ~/.luoke/agent,借主控的 Python。不带配件清单、不带 --hub:主控不管它,它也不问主控,所以用自带的 luoke_mem,不接工具 |
--release | ai.luoke.agent,「螺壳 agent」 | 固定位置,默认连主控的 socket。正式签名和公证还没做,没有发布过 |
界面协议(核心 ↔ 壳)
| 方法 | 请求 | 答复 |
|---|---|---|
GET /feed | Server-Sent Events,先推全部卡片,再推变化,每 15 秒心跳 | 事件 a2ui、feed、notify、busy、result |
POST /action | {surfaceId, sourceComponentId, context} | {text, error} |
POST /presence | {idle, locked, asleep},壳每 30 秒和状态变化时上报 | {} |
POST /window | {open, front},front 表示窗口在最前面,不报就按 open | {} |
POST /notifications | {status, alerts},壳每 30 秒上报系统通知的授权 | {};status 缺失或超过 40 字符答 400 |
GET /status | — | 只有元数据:items、surfaces、notices、sources(记忆为 missing / ok / too_new,工具为 running / down)、brief_due、window_open、window_front、notifications、present |
POST /say | — | 501(未实现对话) |
- 事件内容:
feed为{order, open, notices};notify为{id, title, body, urgent, silent, surfaceId},同一id的通知换掉上一条;busy为{surfaceId};result为{surfaceId, text, error, replaced},replaced表示卡片已换成结果,界面不再重复写那句话。 - 窗口里的状态提示(
notices):工具没在运行、螺壳记忆打不开、螺壳记忆比螺壳新请更新、螺壳没在运行暂时找不到工具、这一版不在螺壳验证过的版本里、系统通知被关掉、系统正在问能不能发通知。 - 系统通知被关:通知到不了时,窗口里说一句怎么在系统设置里打开,不替用户改系统设置;系统还在问用户(
not_determined)时,窗口里说明答了以后才到得了;状态变化在core.log记一行。通知到不了时的处理见「何时说」。 - 壳的命令行:
--notify-status打印一行 JSON{authorization, alert},不发通知、不请求授权、不起核心,10 秒没答复退出 5;--notify <标题> <副标题> <正文> [--open <参数>]发一条通知,退出码 0 交给系统了、3 没允许、4 系统拒收、5 十秒没答复、64 用法不对。两者和主控启动器的同名参数一致,给主控首次运行的完成页用。 - 调试子命令:
python -m luoke_agent feed|tap|presence|window --home H,用文字看卡片、点按钮、报在场、开关窗口。
工具协议
工具在配件清单的 capabilities 中声明协议版本(1)、socket 路径、可报事项(reports[]:kind、title、默认 urgency)和动作(actions[]),并在自己的 Unix socket 上提供 MCP 子集。完整规格见 luoke-agent/docs/工具协议.md。目前没有已发布的配件声明 capabilities.protocol:输入法的清单只有 listing,agent 跳过它;邮件助手的实现默认关闭,声明单放在 packaging/agent-capabilities.json,不进发布清单;体检和深度搜索已实现协议并在清单中声明,但都未发布。agent 实际只有记忆一个来源。
传输
- 每个请求一条连接:
POST /mcp,请求体为一条 JSON-RPC 2.0 消息,上限 1 MB;MCP 协议版本2026-07-28,在params._meta中携带。 - 超时:连接 10 秒;读 outbox、列工具 10 秒;只读调用 30 秒;写调用 300 秒。socket 不存在视为工具未运行,不算错误。
方法
| 方法 | 用途 |
|---|---|
server/discover | 协议版本和服务能力;工具要实现,agent 现在不调 |
resources/read | 读 outbox:luoke://<配件>/outbox,内容 {items:[…]} |
tools/list | 能力列表,带 readOnlyHint |
tools/call | 调用能力;写调用必须在 _meta 中带 ai.luoke/user-action({surfaceId, componentId, at}),否则工具返回 needs_user_action |
- outbox 事项字段:
id、kind、urgency、deadline、expires、created、why_now、title、summary、card。缺id或title的丢弃;kind缺省other;urgency不合法时用清单里这一类的默认,再没有就是 normal。outbox 是一份完整状态,上一轮有、这一轮没有的事项视为撤回。完成的编号在记录保留的 30 天内不再显示,工具要重新报同一件事须换新编号;撤回的编号再列出来就恢复,说过的不重说。 - 回执:工具实现了
outbox_ack(看tools/list)时,agent 在一件事第一次说给用户时回state: "shown",每件一次(进了简报、发了急事通知,或用户正看着窗口时进来);用户答完回answered,放下回dismissed。用户不在、简报没发出去、系统通知到不了时不回shown。 - 调用结果的
structuredContent可带card(更新原卡片)、done(事项完成,卡片保留 30 分钟以便撤销)。失败码:needs_user_action、invalid、not_ready、needs_setup、stale、failed。 - 撤销是结果卡上的一个写按钮,走同一条点按路径。
- 打开东西的动作(打开文件、在访达中显示、打开工具自己的页面)一律带
ai.luoke/where: mac,只在用户点了按钮时才调,不管是否只读;readOnlyHint由工具照实写。
卡片(A2UI)
- A2UI v0.9.1(消息的
version字段写"v0.9"),目录ai.luoke/a2ui/agent/1,组件限于 Text、Row、Column、List、Card、Divider、Button、CheckBox、ChoicePicker、TextField、DateTimeInput、Icon。 - 校验:根组件存在、可达组件 ≤ 200、累计 ≤ 1000、无环、单父、绑定路径存在、文字 ≤ 2000 字;未通过校验的卡片改为纯文本卡。
- agent 自己处理的动作:
agent.open_url(仅 https,按钮文字后附域名)、agent.dismiss;agent.remind只用于 agent 自己的「少说」卡片,工具卡片用了点按时答「不认识这个按钮」。 - 渲染器:Swift 包
LuokeA2UI(macOS 14、iOS 17),LuokeA2UICore只依赖 Foundation,LuokeA2UI是 SwiftUI 画法;校验规则和luoke_agent/a2ui.py一条条对应。另有输入法用的目录ai.luoke/a2ui/ime/1。DateTimeInput目前画成文本框。
协议里有、agent 还没做的
- 基础页和权限(§1a,已定):工具可在
capabilities里加service(summary、pages、ui)、permissions、listing;对应资源summary、page/<id>、permissions用相对 id。权限分四种:system、account、delegate、each-time,状态由工具照实报。窗口归螺壳.app 画,agent 供内容;主控和 agent 现在都不读这些字段,体检和深度搜索已实现工具一侧(基础页只有体检有)。 - 问螺壳(§1b,草案):
service.ask的suggest、search两个只读动作,只在用户点开或打字问时才调。search回hits:固定形状的数据,不是 A2UI,由LuokeA2UI里只给窗口用的HitsList画。草案还给目录加Image,现在不在目录里。主控、agent 都没有实现。 - 不从卡片发起的调用(§5,草案):用户在对话里问时,只读工具的结果带一张新卡片,不进 outbox 和简报。agent 没有对话,未实现;深度搜索已按此回。
ai.luoke/where: mac(§5):标明只在 Mac 上有意义的工具,手机上不调。agent 没有手机端,不读它。- 工具自己做了的事(§8a,已定):
auto-done(normal,能撤销给撤销,撤不回给「以后别自动做」)和daily-review(low,每天一份)。agent 不需要专门处理,按普通事项走。体检已报auto-done;邮件助手未实现。
与记忆的交互
- 以
Memory(path, tool="luoke-agent")打开记忆;文件不存在时不创建,遇到版本过新的库时提示更新。 - 读取
asks(status=["open"]),该不该问、每天几条由记忆决定;带about_note的问题和建议附上该理解最新的 3 条依据原文(来源、日期、每条 120 字以内),没有时显示 ask 的evidence。记忆的提问和建议 urgency 默认为 normal,走简报。 - 回答经
reply(ask_id, choice, text)交回:
| 卡片 | 按钮 → choice |
|---|---|
是非类建议(answer.type 为 yes_no,或 action.capability 为 habit) | 好 → useful(处理习惯同时确认这条理解)· 不用 → useless · 别再提 → never |
| 其余建议(包括退订建议) | 知道了 → useful · 别再提 → never |
| 关于某条理解的问题 | 对 → yes · 不对 → no · 以前是,现在不是 → past · 按我改的 → revise · 以后再说 → later(记忆收起这条理解下开着的问题,至少 7 天内不再问;之后由反刍视情况作为新的一条再问) |
| 自由问题(单选、是非、文字) | 交回或 是 / 不是 → answer · 以后再说 → later(只收起这一条) |
带 about_note 的卡片多一个「在螺壳记忆里看」,打开 luoke://memory?note=N,不写入。记忆现在只产生关于理解的问题和建议,自由问题这一行暂时用不到。
何时说
- 排序:urgency(urgent、normal、low)→ deadline → 首次出现时间。
- 每类的方式(
modes):now、brief、window、off;未设置时 urgent 为now,其余为brief。 - 急事:用户在 Mac 前(120 秒内有上报、未锁屏、屏幕未睡、空闲 < 180 秒)时立即通知,不受免打扰时段限制。一轮只有一件时,标题是事项标题、正文是
why_now(没有则用summary);同一轮有多件时合成一条,标题「N 件急事」,正文列前 3 个标题,点开定位第一件。 - 简报:第一件待简报的事项(normal;只有 low 时距上一份 low 简报 ≥ 24 小时)出现后计时 60 分钟,到点后等用户在场且不在免打扰时段才发;每份含全部 normal,最多附 2 件 low;通知正文列前 3 个标题;上一份未打开窗口时新的一份静音。
- 窗口:窗口在最前面、用户也在场时算正看着,现有事项和新来的事项都算看过,不再发通知。窗口开着但最小化、在别的应用后面,或人已离开,都不算。
- 通知到不了:系统通知被拒、横幅关着或用户还没答时不发、也不算说过,急事和简报都排着,打开后补发;壳还没上报过授权时照常发。
- 少说:同一类连续 3 次没有回应,从
brief降为window,并给出一张"以后只放在窗口里"的卡片,用户点了才收起,核心重启后画回来。同一次说的同类事项只算一次。算没回应的:通知过、到下一份简报时仍没看过的;通知后过期的;用户按「放下」。用户答复或点「还是提醒我」把连续计数清零。 - 清理:过期事项撤下;窗口关着时,完成的卡片 30 分钟后撤下,完成或撤回超过 30 天的记录连同
contents一起删除。
数据和文件
| 位置 | 内容 |
|---|---|
~/.luoke/agent/ | 数据目录(700) |
state.db(600) | 表 items(事项状态)、contents(卡片内容,用于重启后恢复和撤销,撤下后留作历史)、settings、meta、reactions(每类展示、回应、忽略的统计);put_off 已不再使用 |
run/agent.sock | 界面 socket(600) |
logs/core.log | 只记元数据:版本、socket、异常类型、错误码 |
设置默认值:brief_minutes 60、away_seconds 180、ignore_limit 3、quiet 无、modes 空。brief_minutes 可由 serve --brief-minutes 写入,modes 只由「少说」改,其余没有修改途径。
安全
- 写调用只在
POST /action(用户点按)时发出,并附 user-action;方案编号、链接、问题编号等写死的值只认核心保存的卡片声明,不采用界面送来的同名值。 - 工具和记忆送来的文字一律按纯文本渲染;每张卡片带来源前缀,按钮只转给出这张卡片的来源。
- socket 只靠文件权限(目录 700、文件 600)限定在当前账户内,不核对调用方身份。
已知限制
- 没有对话,不使用模型;outbox 靠轮询,未实现订阅。
- 写调用只看
tools/list的readOnlyHint,不对照清单里声明的actions。 deadline只影响排序,过期不会标记;off与window效果相同。- 设置没有界面;卡片上用户改过的选项只存在壳里,壳重启、核心重启或重新连上时丢失;
core.log不轮转。 - 简报通知的正文包含事项标题。
- 核心只在启动时取一次主控的
luoke_mem路径,主控换版本后靠退出码 75 重起再取;连不上主控时隔 2 秒重试,共 3 次,都不行就用自带的副本,直到下次重起。 - 连主控时要过版本校验:配方的
verified没有luoke-agent,主控不答它的请求,agent 只能说明这一版用不了工具;主控一侧还没接。 - 下一版修:本页按仓库主线写,试用版换装后才生效的有:窗口要在最前面才算看过、通知到不了时不算说过、少说按次计数且卡片不丢、来源读不到时事项不丢、改写含密码类内容时提示正确、主控不认版本时单独提示、重启间隔复位。这些修复有单元测试,窗口状态的计算在 AppKit 测试程序上跑过,开发版 app 本身还没在真窗口上点过。
深度搜索
给 Mac 上用户选的文件夹和照片图库建索引,按字词和按意思找回来。没有自己的窗口,设计上入口在 agent,螺壳的窗口显示它的概况和权限(这两处都还没做,现在只能用命令行);向量由主控算,自己不带模型。
仓库 luoke-search(Swift 包 LuokeSearchKit)· 版本 0.1.0,按代码 2f06213 核对 · 未发布:不在 v1 首发中,跟随 agent(v1.x)发布 · 只支持 macOS 26.2 及以上
职责
- 第一版搜本机文件(文档、PDF 含扫描件、截图、图片、视频、录音)和照片图库。不搜代码。
- 只读用户的文件,不改、不移动、不上传;照片图库只读。
- 不主动找用户:只经 agent 的 outbox 报三类事。改设置、打开文件、请 macOS 问照片权限,这三件都要用户在卡片上点按。
- 第一版不读也不写记忆,搜索词不写进记忆。
运行形态
| 入口 | 行为 |
|---|---|
螺壳深度搜索.app | bundle id ai.luoke.search,LSUIElement(不出现在程序坞)。主控在登录时带 --resident 拉起,打开即 serve;带 --resident 时向主控 PUT /accessory/resident,收到 SIGTERM 退出前 POST /accessory/exiting |
luoke-search serve | 常驻服务:在 run/agent.sock 上提供 MCP,并跟着文件夹和照片图库更新索引;每 5 分钟补一次还在等的向量,每 2 分钟看一次设置、照片权限和文件夹能不能读;10 分钟没人搜就放掉内存里的向量。--socket 换 socket 路径,--no-watch 只开 socket |
luoke-search index --job | 第一遍建索引(供主控的空闲任务调用):同步设置里的文件夹和照片图库、读等着的、补向量;SIGTERM / SIGINT 时读完手上那个文件就停,退出码 0,下次接着读;标准错误只有计数 |
| 其他子命令 | 同一个可执行文件 Contents/MacOS/luoke-search 也是命令行,见「接口」 |
- 没有自己的窗口:设计上用户在 agent 的对话里提问,agent 调 MCP 工具并画结果卡片(agent 的对话还没做);搜哪些文件夹、是否加入照片图库也由 agent 的卡片询问。配件清单给螺壳的窗口提供概况(
summary)、权限状态(permissions)和一行介绍(listing)。 - 读取和算向量分开:读取(取文字、OCR、抽帧、切声音)由本工具完成,读完即可按字词搜;向量找主控要,主控不在或忙时记为等向量,以后补。
- 跟着文件夹:常驻服务启动时把文件夹走一遍(只看名字和时间,不读内容),移出不再搜的;之后用 FSEvents 盯着,变化停 0.75 秒后一起处理(最长攒 5 秒),只看变了的路径(新的、改过的、移动或改名的只改路径、删掉的、整个搬进来的文件夹)。新变的文件马上读,每次最多 50 个,排在第一遍的积压前面,读完补向量。设置改了就换成盯新的文件夹。
- 第一遍兜底:常驻服务每 2 分钟检查一次,有等着读的且接着电源(或设置允许用电池)、不在低电量模式、不过热、主控说后台在跑时,读一段(最多 100 个);文件一有变化就让路。
- 长文件先读开头:超过 50 页的 PDF 第一遍只读前 30 页,超过 40 段的文档只读前 40 段,条目记为
partial,读到的部分照样能搜;所有文件都过一遍以后,PDF 再每次 30 页往后补,文档剩下的一步读完,每步读完就存,能停能续;文件改了从头读。盘拔了或内容被 iCloud 挪走时留到下次,不当成读不了。 - 单个文件的看门狗(
index、index --job、常驻服务都开着):读一个文件时每秒看一次本进程内存和这个文件读了多久。内存超过上限(物理内存的一半,限在 4–8 GB)且至少一半是读这个文件时涨的,一步读了 15 分钟(只算 Mac 醒着的时间),或各步加起来超过总时限(60 分钟或每页 5 秒,取大的;剩不到 60 秒不开新一步),就把这个文件(连同修改时间和大小)和原因写进run/stopped-file.json,以退出码 75 结束进程;下次起来先把它记为读不了,文件改过才重读。 - 读到一半进程没了:每个条目记读的次数,两次都没读完就不再读(长文件的后续部分同样计数,已读的留着)。
- 照片图库:常驻服务盯着图库的变更通知(变化攒 2 秒合一次),新拍的马上读。常驻服务从不询问:设置打开了图库但还没问过时,outbox 报
needs-access/photos/<编号>,用户在卡片上点「继续」才经search_request_access请 macOS 询问,最多等 240 秒;limited权限算允许。被拒后卡片改成去系统设置,并带「算了」。 - 线程:常驻的活在自己的一条串行队列上,MCP 另有连接线程;读文件(尤其 OCR)不在 Swift 并发的线程池里做,一个进程同时只做一张 OCR。
- 签名:hardened runtime,权限只有 Apple Events(让 QuickTime Player 跳到某一刻)和照片图库。
不读什么
- 不跟符号链接;不进包(含照片图库的包,图库经 PhotoKit 读)、名字像凭证的文件夹、隐藏文件夹、
~/Library、*.noindex、开发用文件夹(node_modules、venv、site-packages、DerivedData、Pods等)和用户跳过的文件夹。 - 代码项目(含
.git、package.json、Package.swift、pyproject.toml、Cargo.toml、.xcodeproj等的文件夹)里只读截图。 - 只在 iCloud、没下载到本机的文件不读(读会触发下载)。
- 像数据集的文件夹:同一个文件夹里同一类媒体(图片、视频、录音)≥ 300 个、其中 ≥ 70% 是机器起的名字(纯数字、十六进制串和 UUID、
frame_0042、clip-000123这类数据集词加数字),这一类不读,文档照读。相机和系统起的名字(IMG_、DSC、PXL_、截屏等)、名字里带日期的、其他词加数字的不算;图片再抽 10 张看 EXIF,一半以上写了相机厂家或型号的不跳。用户直接选的文件夹(含套在另一个选中文件夹里的)不算。status和search_status列出这些文件夹。 - 凭证文件不读:按扩展名(
pem、p12、kdbx、key、ovpn等)、名字开头(id_rsa、.env、.netrc等)、名字里的词(password、credentials、secret、api key、recovery codes、seed phrase 等)、中日韩片段(密码、口令、密钥、私钥、助记词、恢复码等)和密码管理器的导出(logins.csv、bitwarden / lastpass 导出、keepass)。单独的 key、token 不算。开头是 zip 的单文件 Keynote(.key)不算凭证,也不读、不报。 - 发布版的搜索范围:只收家目录内和
/Volumes/<盘>/…;路径任何一段不能隐藏;~/Library下只收Mobile Documents(iCloud 云盘),所以~/Library/CloudStorage里的第三方网盘不收;不收/、/Volumes本身和 Time Machine 的备份。先解开符号链接再判,每次走文件夹都重判。 - 拔掉的盘、macOS 不让读的根文件夹记为读不了,其中已有的条目保留。
读什么
类型(kind) | 怎么读 | 结果指到 |
|---|---|---|
doc | txt、md(自动认编码:UTF-8、UTF-16、GB 18030 含 GBK、Big5、Windows-1252,最多读前 32 MB);csv、tsv 按表格读(认引号、逗号、分号、Tab 和 Excel 的 sep=);rtf、doc(系统文字系统);docx、odt;xlsx(工作表按整行切段,每段带表名和表头;一行比段长时长格子截短、全文另放一段,密码列除外);pptx(每页加演讲者备注,按演示顺序);html(不用 WebKit)。约 1800 字一段、重叠 200 字、在句末断开,每个文件最多 2000 段 | 第几部分、第几个工作表、第几页幻灯片 |
pdf | PDFKit 一页一段(最多 2000 页),每页取前 4000 字算向量。一页字数不到 200(中日文一字按 2.75 计)当扫描页:按 1400 px 画出后 OCR,字进关键词,页面的样子等图片向量。PDF 自带标题像残留文件名时不用它,结果标题用文件名。乱码超过 30% 的页当图读。加密的记读不了 | 第几页 |
screenshot | 按文件名(Screenshot、截屏、屏幕快照、CleanShot 等)或 macOS 的截屏标记认出。OCR 的字只进关键词、不算文字向量;样子等图片向量 | — |
image | 读尺寸和 EXIF 拍摄时间;长边不到 256 px 的跳过;不做 OCR;样子等图片向量 | — |
audio | AVFoundation 解成 16 kHz 单声道,30 秒一窗、每 25 秒起一窗,静音窗丢掉;不转文字,等声音向量 | 那一窗的起止秒 |
video | 每 3 秒一帧(最多 1200 帧),和上一张留下的帧几乎一样的不留;音轨照录音切。AVFoundation 打不开的格式(mkv、webm 等)记读不了 | 那一帧或那一窗的秒数 |
photo | 照片图库(PhotoKit),用户在设置里打开并允许后才读。照片记拍摄时间和尺寸、不做 OCR,样子等图片向量;视频照视频文件取帧和声音。图库的视频 kind 是 video、source 是 photos。不含隐藏项。只在 iCloud 的原件不下载:视频记为 skipped、每 6 小时再试,照片只按名字和时间搜。新的读、改过的重读、删掉的移出,关掉就全部移出 | —(打开「照片」并说明拍摄时间) |
- OCR 用系统的 Vision,指定语言、中文排第一(简体、繁体、英文、日文、韩文,再加本机偏好的语言)。
- 每个条目的时间:有拍摄时间用拍摄时间,否则用修改时间。照片图库的条目路径记为
photos:<本机标识>,标题是导入时的文件名,没有就叫"照片""视频"。 - 每个条目另有一段"名字"(文件名和所在文件夹),只按字词搜。
- 读法升版时(现在文档是 3,其他是 1),旧版读过的条目自动重读。
- 认 59 个扩展名:文档 14 个、PDF、图片 20 个(含 dng、cr2、nef 等相机原片)、视频 13 个、录音 11 个。
ts、mts要看同步字节。列在表里不等于读得出,AVFoundation 打不开的记读不了。
上限
| 项目 | 值 |
|---|---|
| 文字文件 | 读前 32 MiB,单文件最多 400 万字 |
| 分段 | 约 1800 字、重叠 200 字,每个文件最多 2000 段;字母占比不到五分之一的段不算向量 |
| 最多 2000 页;乱码超过 30% 的页当图读;画图宽 1400 px、长边最多 4000 px,长宽比超过 20 的页不画也不送图 | |
| 截图 OCR | 3000 px,短边不低于 1000 px,最长 16000 px |
| 压缩包(docx、xlsx、pptx、odt) | 只支持 stored、deflate,不支持 Zip64;单条 256 MiB,单文件总量 1 GiB,超了停在那里,已读的留着 |
| Office | 幻灯片、工作表各最多 5000 个;单元格 32767 字;标题 2000 字 |
| 视频 | 3 秒一帧、最多 1200 帧,1024 px,8×8 哈希差 4 位以内算同一张 |
| 声音 | 30 秒一窗、每 25 秒起一窗,不足 2 秒的尾巴不成窗,RMS ≤ 0.003 算静音 |
| socket | 请求体 1 MiB、头 16 KiB、32 个连接(超了答 503),读写各 30 秒 |
| JSON-RPC | 嵌套 64 层,回显 100 码位,字符串 id 256 字 |
| 方案 | 每列最多 50 个文件夹,路径 1024 字节,留 20 个未执行的方案,数文件 2 秒,卡片 20 行 |
| 重试 | 读 2 次、重做媒体 2 次、向量输入 3 次;只在 iCloud 的图库视频每 6 小时再试 |
| 补向量 | --wait 最多 1 小时,短歇累计 600 秒,退避最多 300 秒,一次请求 32 MiB,每轮 8 个条目 |
解析恶意文件的防护:XML 不做平方级的追加,解压总量设上限,ODT 的 text:c 不照数造空格(只放一个),docx 和 ODT 到 400 万字停止解析,幻灯片和工作表去重。
怎么搜
- 按字词:SQLite FTS5 两张表,
fts(unicode61 + Porter 词干)和fts_tri(trigram,给中文、日文等不用空格的文字)。查询中的中日文用系统分词(NLTokenizer)切词,去掉停用词和相对时间词("上个月""去年"等,交给 agent 换成since/until);分词器拆成单字的相邻字连起来再试(可选词:找到每个加 0.1、最多 0.3,找不到不扣,不能单独让条目入围);三个字以上走 trigram,两个字用 LIKE;英文找不到原词时按前缀匹配。问题只看前 200 字,必需词最多 12 个、可选词最多 8 个,每个词最多取 5000 条。 - 类型词:查询中的"截图""照片""录音""视频""PDF""文档"等不当关键词,给对应类型加分(不是筛选),这类条目按意思 z ≥ 1 即入围;"照片"同时指文件夹里的图片和图库的照片。只有类型词、按意思也没找到时,列出该类型最新的条目。
- 按意思:查询向量以
user-waiting向主控要(模型在加载时最多等两次,一次请求限时 10 秒)。全部向量以 fp16 存在索引里,搜索时整块读进内存,之后只增量读改过的行,每 1024 行转 fp32 用 vDSP 求余弦。每个条目取最好的一段。 - 混合打分(默认做法 A):余弦先换成 z 分:满 20 行的模态(文字、图片、声音)按自己的均值居中,不满的按全体均值;离散度全体合着算,每行跟其余各行比(留一法),并向典型值 0.03 收缩。条目入围条件:z ≥ 2(类型词对上时 ≥ 1),或含必需的查询词至少一半。分数 = z + 2 ×(命中词比例)1.5 + 0.5 × 名字中的命中比例 + 0.3 × 名次项 + 类型词加 1。查询的词都含数字(编号、单号)时按精确查找:字词权重加倍、z 减半。
- 其他打分做法只在
eval --scoring里用于对比:b(每个模态只和自己比)、c<k>(模态的均值和方差按 k 行的分量往全体拉)、d(各自均值、离散度合着算)。 - 结果:分数不到第一名一半的标
strong: false(卡片上标"可能相关");原文件已不在的不列出。结果指向含查询词最多的一段(名字和内容都对得上时指向内容);按意思入围的指向意思最好的那段。表格命中时片段是含查询词的一两行,每格前写列名。片段和标题送出前遮掉凭证,索引里保留原文。 - 遮凭证:规则从记忆的
find_secrets起步并扩充,分七类:密码、验证码、密钥换成〔密码〕这样;助记词、恢复码同样整串遮;卡号、证件号留后 4 位(〔卡号 ****1111〕)。全角和其他文字的数字先折成半角再认。表格按列名遮;没有可用列名时,一列六成以上像密码就整列遮。search_open的答复、文件夹显示名(方案、卡片、outbox)也遮;path不遮,含 outbox 的 id 和卡片小字里的路径。已知漏:带空格又不是英文词的密码、OCR 认错的卡号、无表头的纯小写字母数字密码。 - 拒答门槛未开:没有结果时
confident为false,有结果时为null("有结果但说不准")。候选门槛(字词全中,或第一条的余弦过它那个模态的门槛:文字 0.70 / 图片 0.66 / 声音 0.70)只在eval --refusal里量,是否开启待定;开启后没过门槛的结果照给、标confident: false。
接口
给 agent 的 MCP 工具
按 agent 工具协议(版本 1,协议版本 2026-07-28,MCP 无状态子集:server/discover、resources/list、resources/read、tools/list、tools/call,另答 initialize、ping),在 ~/.luoke-search/run/agent.sock 上 POST /mcp;luoke-search mcp 在标准输入输出上提供同一组工具(一行一条)。一次只处理一个请求;socket 上的 search_request_access 要等用户答复,不占这个锁。
| 工具 | 只读 | 参数 → 答复 |
|---|---|---|
search_query | 是 | query;可选 kinds(doc pdf screenshot image video audio photo,筛选)、since / until(ISO 8601 日期或时间)、limit(1–50,默认 10)→ hits、partial、partial_reason、confident、took_ms,加一张结果卡片(前 5 条,每条 [打开] [在访达中显示],每次 surfaceId 不同) |
search_open | 否 | id、可选 reveal → 一句说明。没有 ai.luoke/user-action 答 needs_user_action。视频、录音用 QuickTime Player 跳到那一刻;PDF 说明在第几页;照片打开「照片」并说明拍摄时间;文件不在了答 stale。标 ai.luoke/where: mac |
search_status | 是 | — → items(按类型)、waiting(read / rest_of_long_files / vectors / media)、dataset_folders(路径和文件数)、first_pass_done、hub(state:available / downloading / busy / unsupported / not_running / not_connected,note)、folders、photos(on / needs_access / off) |
search_settings | 是 | 不带 change:当前设置。带 change(add_folders、remove_folders、skip_folders、photos、on_battery;文件夹可写路径或 desktop、documents、downloads、pictures、movies、music 和对应的中文名);这一步会往 state.json 写方案→ plan_id、changes 和一张确认卡片:文件夹写访达里的名字,完整路径在小字里;加文件夹时数一下大约多少文件(桌面、文稿、下载、iCloud 云盘、外接盘这类 macOS 保护的位置不数,只说明 macOS 会先询问) |
search_settings_apply | 否 | plan_id、可选 checks(用户在卡片上的勾选)→ 执行自己出过的方案。勾选不合法或全不勾答 invalid。没有 ai.luoke/user-action、或点按的不是这个方案自己的卡片,答 needs_user_action;设置文件读不了答 needs_setup;方案已不在、设置在出方案后变过、选文件夹的方案已选过或放下,都答 stale;macOS 不让读的文件夹答 needs_setup 并换上带 [打开系统设置] 的卡片 |
search_open_system_settings | 是 | pane:files(文件和文件夹)/ full_disk(完全磁盘访问)/ photos → 打开系统设置对应隐私页。标 Mac |
search_request_access | 否 | id(photos)→ 请 macOS 询问,等用户答复。要 ai.luoke/user-action。标 Mac、waits |
outbox_ack | 是 | id、state(shown / answered / dismissed);只收 outbox 现在可能有的 id,别的答 invalid;shown 不记,answered、dismissed 的不再出 |
结果条目:id(条目号加位置,如 12#p3 第 3 页、#s 幻灯片、#w 工作表、#c 第几部分、#t25.0 秒)、kind、title、path(照片图库的条目为空)、where(page / part / sheet / time / end)、date、snippet(约 160 字)、why("第 3 页的文字对得上"等)、score、strong。
资源
luoke://luoke-search/outbox(也可写outbox):见下。summary:{count, text},count只算needs-setup和needs-access。permissions:{photos: granted|denied|ask},不弹框;读不了图库的进程报 denied。
outbox
只报三类事,不推搜索结果:
kind | 紧急度 | 什么时候 |
|---|---|---|
needs-setup | normal | 发布版、从没选过要搜的地方、也没放下时。id 就是 needs-setup;卡片默认勾桌面、文稿、下载(存在且可搜),照片不勾 |
needs-access | low | 设置里的文件夹 macOS 不让读,或打开了照片图库但没得到允许。id 是 needs-access/<路径>/<编号> 或 needs-access/photos/<编号>,编号按时间取,每次又要用户处理时换新。照片那件带 permission: photos;文件夹读不读得了只由常驻服务判定 |
index-ready | low | 第一遍建完,id 是 index-ready/<时间>,3 天后过期 |
命令行
| 子命令 | 做什么 |
|---|---|
index [文件夹...] [--exclude] [--prune] [--wait] [--no-vectors] [--allow-battery] [--json] | 同步、读、补向量;不给文件夹时搜设置里的文件夹并移出不再搜的。--wait 主控忙时按它说的时间等(最多 1 小时) |
index --job | 第一遍建索引,见「运行形态」 |
search <问题> [--kind] [-n] [--since] [--until] [--json] | 同 search_query |
open <编号> [--reveal] | 同 search_open |
status [--json] | 索引位置和大小、各类数量、等什么、主控状态、是否排除出 Time Machine |
settings [--add-folder] [--remove-folder] [--skip-folder] [--photos on|off] [--on-battery on|off] [--json] | 用户自己敲的改动直接执行(经同一套方案检查) |
eval <题库.json>... [--testbed] [-n] [--refusal] [--scoring a|b|d|c<k>] [--timing] [--json] | 跑评测,出第一名、前五、前十、MRR,按题型分开;--refusal 量拒答门槛(expect 为空的是没有答案的题);--scoring 换打分做法对比;--timing 在标准错误打 p50、p90、最长 |
photos-probe [--access] [--sample] [--originals] | 只出汇总数字:本机照片图库能给深度搜索多少(开发版 app 或设 LUOKE_SEARCH_PHOTOS=system 才能用,发布版不接这条命令;不问权限,不从 iCloud 下载) |
probe-audio | 开发用的声音探测,发布版不接 |
mcp、serve | 见上 |
--json的键为 snake_case,search --json与search_query的答复同格式;status --json的形状和search_status不同。- 发布版:bundle id 或代码签名标识等于
ai.luoke.search,或带serve --resident,就按发布版处理,只接serve、index、status、search、open、settings、mcp。--index、--socket、LUOKE_SEARCH_HOME、CFFIXED_USER_HOME拒绝启动,其余开发变量删掉并在标准错误说一句,index不接受另给文件夹。开发版 app 用LUOKE_SEARCH_DEV_APP=1构建,强制 ad-hoc 签名。 - 开发版的每条命令可加
--index <数据目录>(photos-probe除外);环境变量LUOKE_SEARCH_HOME同样换数据目录。不在 app 里运行的命令行必须给出其中之一,不用正式的~/.luoke-search;不认识的选项报错。LUOKE_HUB_SOCKET指定主控。LUOKE_SEARCH_OPENER=dry什么都不真的打开(照片也不),只在标准错误说要打开什么,答复句尾加"(演习)";LUOKE_SEARCH_PHOTOS=system让非 app 进程读图库(开发用),off关掉。
依赖主控
- 任务:客户端请求的任务名为
luoke-search.embed(配件清单同名;开发时LUOKE_SEARCH_EMBED_TASK可换成归 luoke-search 的其他任务)。主控主线配方中该任务仍叫digup.embed、归配件 digup、未分配到任何档,配方的verified也没有 luoke-search。所以对当前主控,带X-Luoke-Accessory: luoke-search/0.1.0的请求都答 403unknown_accessory(含GET /tasks、embed、PUT /accessory/resident、POST /accessory/exiting),深度搜索判为"这版主控还不认识深度搜索",只按字词搜。 - 接口:
GET /tasks读状态和参数(batch、max_chars、max_total_chars、vector_space,以及image_batch、image_budgets、audio_batch、max_image_bytes、max_audio_bytes、max_audio_secs),X-Luoke-Tasks-Version变了就重读;POST /tasks/<任务>/v1/embed送texts或inputs(图片、声音的字节,不送路径)。请求头X-Luoke-Accessory: luoke-search/0.1.0、X-Luoke-Priority,需要时X-Luoke-Allow-Battery。要求 768 维,收到后再做 L2 归一化。 - 提示词:查询
task: search result | query: …;文档title: … | text: …(标题用文件名,PDF 用它自己的标题)。 - 图片和声音:索引里不存像素和声音;主控的参数表明收某一种时,从文件重新做出同样的帧和窗口再送。预算:照片和图片 JPEG ≤ 1024 px、140;截图 PNG ≤ 1536 px、280;扫描页 PNG 1400 px、280;视频帧 JPEG ≤ 1024 px、70;声音 30 秒 WAV。一个一个做、攒够一批就发;这一档不收的预算照旧等;暂时失败的试 3 次,主控说读不了的那条不再要向量。补向量时重做图片和声音这一步也在看门狗下:内存从这个文件开始算,时间只算做图,每 30 张一步,不算送去主控的时间;停下后字照旧能搜,图和声音不再等向量。崩溃两次也放弃。有文件等着读时,一轮补向量最多 60 秒。
- 主控不在或忙:照常读取、按字词搜,答复带
partial_reason(主控没在运行、不认识深度搜索、这台 Mac 不提供、还有多少段等向量等),不退回任何自带模型。补向量时power_cooldown、lane_busy这类短歇原地等完;其他拒绝停下、下次再补(index --wait时按retry_after_ms等,连续没进展才翻倍,最多 300 秒)。vector_space变了只重算向量,不重读文件;空间不一致时这次只按字词搜。 - 开发不碰正在用的主控:只有默认数据目录
~/.luoke-search且是 app 本身,才回退到已安装主控的 socket;带--index或LUOKE_SEARCH_HOME的运行只认LUOKE_HUB_SOCKET,没设就只按字词搜。
数据
| 位置 | 内容 |
|---|---|
~/.luoke-search/ | 数据目录(700),整个排除出 Time Machine |
index.noindex/index.sqlite | 索引(600,schema 5,另有 -wal、-shm):items(路径、来源 file / photos、类型、大小、时间、设备号、inode、拍摄时间、标题、状态 pending / partial / done / failed / skipped、详情和读取信息、读法版本、读完时间、进队时间、没读完的次数)、segments(类型、位置、向量模态、768 维 fp16 向量、等什么、暂时失败的次数、vec_seq)、像数据集的文件夹、fts(读出的文字,另存 NFKC 前的原文给片段和向量用)、fts_tri、meta(vector_space、vector_generation、dataset_folders) |
.lock | settings.json、state.json 的跨进程锁(600) |
settings.json | 要搜的文件夹、跳过的文件夹、照片图库(默认关)、用电池时建索引(默认开)。读不了时不按设置建索引、不改设置 |
state.json | 未执行的方案(最多 20 个,出新方案时清掉 7 天前的,选文件夹那张不计)、outbox 已答或放下的事、第一遍完成时间、选没选过文件夹、照片和文件夹那几件 outbox 的编号 |
run/stopped-file.json | 看门狗停下时正在读的文件、它的修改时间和大小、原因;下次记入索引后删除 |
run/agent.sock | MCP socket(目录 700、socket 600;残留且没人在听的删掉重开,有人在听就拒绝) |
- 日志只写计数("1 new, 0 changed…"),不写路径、搜索词、片段和文件内容。
- 结果、卡片、outbox 中不放凭证,路径除外(路径原样);凭证文件从不读入。
评测
题库在 evals/,语料全是合成的(testbed/zh,英文和多语言语料用上游脚本生成);真实文件的题库不进仓库。以下为默认打分 A 在实验室主控(开了图片和声音的嵌入任务)、测试机上的结果。
| 题库 | 题数 | 第一名 / 前五 / MRR |
|---|---|---|
zh.json 中文 | 51 | 0.90 / 0.96 / 0.93 |
zh-office.json 文档格式 | 11 | 1.00 |
zh-receipts.json 中英混排扫描小票 | 7 | 1.00 |
synthetic.json 英文 | 37 | 0.97 / 1.00 / 0.98 |
multilingual.json 孟加拉文、阿拉伯文 | 48 | 0.92 / 0.98 / 0.94 |
en-answerable.json 英文(与无答案题配对) | 36 | 0.94 / 1.00 / 0.97 |
| 以上合计 | 190 | 178 题第一(0.937)/ 0.984 / 0.959 |
few-*.json 文档多、图少(200 篇文档加 5 / 10 / 19 张图或 3 段录音) | 117 | 115 题第一;问图的 34 题全对,问录音的 3 题对 1 题 |
| 拒答(候选门槛 0.70 / 0.66 / 0.70,未开启) | 结果 |
|---|---|
没有答案的题拦下(zh-noanswer.json 37 + en-noanswer.json 39) | 65 / 76(86%),其中近似题 27 / 34 |
| 有答案的 190 题中第一条对却被拦(错拒) | 21(11%);英文、阿拉伯文对的答案余弦偏低,文字 0.70 对它们偏高 |
| 文档多、图少的无答案题(73) | 拦下 69;这批有答案的题错拒 0 |
- 发布门槛:190 题第一名不少于 171,且不比上一版少 2 题以上(现在是不少于 176,上一版 178)。
- 文档和图讲同一类东西(
same-topic*.json,45 题,不算门槛):图多、图少都是 39 题第一、前五全中,错拒 2 题。稳定的问题是:问"X 的照片"而同文件夹文档写了 X 时,14 题里有 4–6 题文档第一、照片第二。 - 录音:切段和送法没问题,模型的声音向量对中文说话声差:中文 6 题里,问题对录音的余弦 0.609,对同样文字 0.814。只量过合成声音。
- 拒答分开看:
en-noanswer.json拦下 38 / 39,错拒 12 / 121;中文那批拦下 78%(近似题 58%),错拒 7%。 - 打分做法按事先写好的挑法选定:A 在 190 题上第一名最高;
b误拒最少但第一名低得多(164 题);c40与 A 打平但误拒更多;d在文档多、图少的题上与 A 打平,在 190 题上少 4 题。 - 参考向量集 9 条(文字 5、截图、照片、视频帧、声音各 1)经本工具客户端对主控算的向量余弦均 ≥ 0.999。
- 搜索端到端(要查询向量 + 混合排序)p50 15 ms、p90 17 ms。嵌入进程(开图片和声音)峰值约 4 GB。
- 测试 368 个。
| 规模(合成语料) | 1 万文件 | 5 万文件 |
|---|---|---|
| 读文件 | 336 秒 | 25 分钟 |
| 补向量 | 约 55 分钟 | 约 4 小时 |
| 索引大小 | 283 MB | 1.3 GB(45% 文字向量时) |
| 搜索 p50 | 约 0.1 秒 | 0.4–0.5 秒 |
| 常驻空闲内存 | 约 20 MB | 18 MB |
瓶颈是主控算向量:文字每秒 10–12 条,图每秒约 2 张。
故障
- 主控不在、忙或任务不可用:照常读文件、按字词搜,向量以后补;答复带
partial和一句说明。 - 还在建索引或长文件没读完时答复带
partial("还有 N 个文件没读""还有 N 个长文件没读完");图片、视频、录音在等主控时说明暂时只能按名字和里面的字找到。 - 文件读不了(加密 PDF、打不开的视频格式)记为 failed,只在 iCloud 的图库视频记为 skipped,都不影响其他文件;文件消失后结果不再列出,
search_open答stale。 - SIGTERM:手上那个文件读完就停,删掉自己的 socket;剩下的下次接着读。
- 单个文件读得过久或内存失控:看门狗记下该文件、以退出码 75 退出,由主控重新拉起(十分钟内最多 3 次);该文件记为读不了,改过才重读。
已知限制
- 主控配方中的任务名仍是
digup.embed、未分档,verified也没有 luoke-search:在主控改名、分档并收录之前,正式环境只能按字词搜。 - 还没进主控的发布流水线;
build_app.sh不做公证。 - 主控配方里这个任务还没有图片和声音的参数;在那之前图片、视频、录音、照片只能按文件名、拍摄时间和 OCR 的字找。有了图片和声音才发布。
- 拒答门槛未开启,
confident有结果时恒为null,答复不表示"有把握"。 - 默认打分 A 在文档多、图少于 20 张的文件夹里,对的图 z 余量小(测得最低约 2.3)。问"X 的照片"而同文件夹文档写了 X 时,可能文档排第一。录音题成绩不稳定(3 题对 1 题),原因是模型的声音向量对中文说话声差。
- 配件清单没有登记
jobs:第一遍由常驻服务兜底读,index --job没有接上主控的空闲任务。 on_battery开着也挡不住主控的电池暂停:常驻服务不带X-Luoke-Allow-Battery,主控报后台没在跑时,第一遍和补向量都不跑。- 没有干净标题的 PDF 算向量时提示词写
title: none,不用文件名。 - 只有类型词时的兜底列表不查文件还在不在,也含没读成的条目,可能列出已删的文件。
- 只有声音的 mp4、mov 记读不了。走文件夹判断
ts、mts时会读文件开头,只在 iCloud 的文件可能因此被下载。 - 命令行
open打不开照片图库的条目,答stale;settings --json有改动时,JSON 前面多一行中文。 - 录音和视频音轨不转文字;mkv、webm 等 AVFoundation 打不开的视频读不了。
- 照片图库测过假图库和一次真图库只读测试;iCloud"优化 Mac 储存空间"的图库和几万张的图库没测;PDF 无法从外部定位到页,只在答复中说明页码;「照片」无法定位到某一张。
- 邮件、记忆、打字记录的搜索(第二步)未做。判断要不要搜归 agent,手机端先纯文字;全局搜索、统一结果形状、缩略图、找相似待商定。
螺壳体检
在空闲时查一遍这台 Mac:空间去向、会重建的缓存、卸载残留、久未动的项目、重复和久未打开的大文件,以及常驻项、进程、键盘监听、权限、浏览器扩展、MCP 配置、基础安全设置和病毒。是不是垃圾、能不能删由确定的规则决定。结果经 agent 告诉用户;四类按规则挑出的东西不用点就清,其余的删除只执行用户在卡片上点过的方案。除缓存外都移到废纸篓,能放回。
仓库 luoke-audit(Swift 包,三个程序 luoke-audit、luoke-audit-serve、luoke-audit-admin)· 版本 0.1.0,规则包 0.1.0,按代码 3a6c9cb 核对 · 和 agent、螺壳窗口进同一个 v1.x;接进螺壳(luoke-core#27)在 beta1 之后 · 只支持 macOS 26.2 及以上
职责
- 查的时候不改任何东西;定时的一轮查完以后才清(见「清理」)。能用元信息(大小、时间、inode、签名、链接数)判断的不读内容;要内容的(重复文件的哈希、明文凭据、散落的密钥)只在本机读,内容不进报告、日志和卡片。
- 每条发现带结论(放心删
safe/ 要确认confirm/ 仅供参考info)、类别、严重度和依据。只有会重建的缓存能得到"放心删"。 - 检查没跑成记成"没扫描"并写原因,不算没有发现。
- 不主动找用户:没有通知、没有菜单栏图标,要说的事放进 outbox,由 agent 挑时机。
- 不带模型。病毒扫描用随包的 ClamAV 和 YARA(特征扫描,不是模型)。
运行形态
| 程序 | 行为 |
|---|---|
luoke-audit | 「螺壳体检.app」的主程序,用户给「完全磁盘访问」的就是它。扫描、出方案、执行方案、不用点就清都在它里面;从不开 socket。空闲任务 scan --this-mac --autoclean、update --this-mac 起它;agent 的每个请求起一次 luoke-audit mcp(标准输入一条 JSON-RPC,标准输出答复,答完就退出) |
luoke-audit-serve | 只管给 agent 的 socket ~/.luoke-audit/run/agent.sock,不读不动用户的文件,不要完全磁盘访问;只链接 AuditSocket,不含检查和清理的代码。每个 POST /mcp 以"自己负责"(responsibility_spawnattrs_setdisclaim)起同一文件夹里的 luoke-audit mcp,只传 0、1、2 三个描述符,等 290 秒。每 300 秒(--quick-every)起一次 luoke-audit scan --quick 看新下载的 |
luoke-audit-admin | 单独签名(ai.luoke.audit.admin)。平时不运行,只在用户输入 Mac 的密码后以管理员身份做一次「要密码的删除」,见「清理」。不是常驻的 root 辅助程序 |
- 谁起:空闲任务由主控按配件清单的
jobs起(每天一次,接着电源)。luoke-audit-serve要常驻:配件清单写了resident_exec、resident_command,resident还是 false,主控还不认这两个字段。本工具还没进配方,目前主控不会起它,都要手动起。 - 第一轮:进配方以后,主控发现新配件没有运行记录,第一个满足条件的周期就跑(用户安静、后台条件允许、没有别的任务在跑),不用等一天。配件清单里
update排在scan前,第一轮先下病毒库再扫描。第一轮就带--autoclean,persistence.new只记基线,space.growth要攒几天历史。luoke-audit-serve自己不起第一轮;想马上查用audit_scan_now(不清东西)。 - 装好的包只认
--this-mac:在 .app 里运行时,--home、--data、--tmp、--projects、--tool-dir、--fixture、--clock-start和fixture、eval-cleanup子命令都拒绝;findings、status、update要带--this-mac。扫描工具只用包里的,不找 Homebrew 的(那些文件夹用户可写,会借到完全磁盘访问)。 --this-mac查哪里:家目录、本用户的临时目录(DARWIN_USER_TEMP_DIR),/Library和/var/db/receipts(找系统目录里的卸载残留),项目目录取~/Developer、~/Projects、~/Documents/GitHub、~/code、~/src、~/repos、~/workspace里存在的;数据放~/.luoke-audit。- 没有完全磁盘访问时:不碰 macOS 会弹框问的文件夹(桌面、文稿、下载、iCloud 云盘、
~/Library/CloudStorage、Containers、Daemon Containers、Group Containers)。能不能读用打开 TCC 库试探,不弹框,试不出算没有。看不到的部分跳过,写成"没有完全磁盘访问,…没看",已看到的照报;受保护文件夹里的程序签名记成"不知道",不当成不存在;方案不碰这些路径,快速扫描不跑;记过的大小和哈希不忘。outbox 出needs-access卡片。受保护文件夹里的缓存不看,也记成"没看"(发现里不出现,不用点就清也不碰);其余缓存和临时文件夹照常清,安装包和重复文件没有发现就不动。 - 一次一轮:每个数据目录用
round.lock保证空闲任务和 agent 叫的那一轮不重叠;快速扫描另有quick.lock;一个数据目录只能起一个luoke-audit-serve(serve.lock);一份方案同时只在一个进程里执行(plans/<编号>.lock)。 - 跑得轻:
scan、update以 utility 优先级运行(磁盘 I/O 同档,起的 clamscan、yara、git 也是);只在 iCloud 的文件不读(读会触发下载)。外部命令都带超时,超时连进程组一起杀。 - 被打断:收到 SIGTERM 时停在做完的那一步、保存报告、退出码 0;没来得及查的规则沿用上一轮的发现并在
skipped里注明,量过的留给下一轮。扫描被打断的一轮不做不用点就清;清的时候被打断见「不用点就清」。 - 快速扫描(
scan --quick):只在接着电源、不在低电量模式、内存压力不高(kern.memorystatus_vm_pressure_level≤ 1)时跑;FSEvents 日志显示下载或桌面有改动才扫,只跑病毒扫描,用精简库,跳过 120 秒内写过的文件(修改时间在未来的不算还在写);有跳过的文件时不记这次的事件号,下一次不管日志有没有改动都再看,没变的沿用记录不重扫;结果并进上一轮的报告;整轮在跑就不看。还没有完整的一轮或没有完全磁盘访问时不做。
检查什么
一轮按顺序跑 23 条规则,每条记耗时(ruleSeconds)。下表按类别合并;"结论"是规则给的。只有会重建的缓存是"放心删";仅供参考的不删东西,写清怎么处理。
| 类别 | 发现(rule) | 结论 | 要点 |
|---|---|---|---|
| 会重建的缓存 | cache.rebuildable.* | 放心删 | 20 项:pip、SwiftPM、Go、Xcode DerivedData、Cargo(registry 的 cache 和 src)、Bun、uv、npm(只算 ~/.npm/_cacache)、Homebrew、Yarn、CocoaPods、Gradle(caches 和 wrapper)、Poetry(cache 和 artifacts)、node-gyp、Composer、Xcode 文档、模拟器缓存;≥ 50 MB 才报 |
| 临时目录 | leak.temp-prefix | 要确认 | 同一前缀(mkdtemp 式后缀)≥ 20 个、合计 ≥ 100 MB、1 天没动;给出数量、日期范围和每天多少 |
| 久未动的项目 | stale.project、stale.artifacts、stale.folders | 要确认 / 仅供参考 | 项目目录下 6 个月(182 天)没动的;查 git(未推送提交、未提交改动、stash、没有远程、被忽略的非构建产物、仓库旁不在 git 里的东西),全干净才"要确认",否则"仅供参考"并写会丢什么。stale.artifacts 是老项目里 25 种会重建的构建产物,往下 4 层、合计 ≥ 100 MB,查是否被 git 跟踪和内嵌仓库。git 带 --no-optional-locks,关掉仓库配置里会运行程序的项 |
| 空间 | space.growth、space.low、space.partial | 仅供参考 | 家目录下一层(~/Library 的 Caches、Application Support、Containers、Developer 和 .cache、.local/share 再细分一层)和临时目录每天记大小,比 7 天内最早的记录每天多 ≥ 1 GB 报;可用空间 < max(20 GB, 10%) 报;一轮量大小最多 10 分钟;~/Library/CloudStorage 从不量 |
| 系统层空间 | space.tm-snapshots、space.vm-disk、space.ai-model(s)、space.xcode-*、space.simulators | 仅供参考;单个模型和旧设备支持文件要确认 | Time Machine 本地快照留了 2 天以上;容器和虚拟机磁盘 ≥ 5 GB;AI 工具下载的模型单个 ≥ 2 GB 单列、合计 ≥ 5 GB 报一条;Xcode 旧的设备支持文件(≥ 100 MB、30 天没写)、归档 ≥ 1 GB、模拟器 ≥ 5 GB |
| 重复文件 | space.duplicates | 要确认 | 下载、桌面、文稿、影片、音乐、图片里 ≥ 10 MB、内容相同的几份(大小、头尾 64 KB、整份 SHA-256);多占按 APFS 独占大小算,克隆和硬链接不算;一组多占 ≥ 50 MB 才报,最多 20 组 |
| 久未打开的大文件 | stale.large-file | 要确认 | Spotlight 记的上次打开时间在一年前、≥ 500 MB,最大的 20 个 |
| 残留 | leftover.app-data、leftover.installer、leftover.partial-download、leftover.relocated-items、leftover.intel-homebrew(-links) | 要确认 / 仅供参考 | ~/Library 和 /Library 里以 bundle id 命名、没有已装应用对得上、30 天没写、≥ 1 MB 的(名字像钱包、密码库、密钥、VPN、验证码、ssh 的,或同一开发商还有应用装着的,只"仅供参考");下载文件夹里 14 天前来的安装包(≥ 5 MB、没挂着);7 天没写、≥ 1 MB 的没下完的下载;升级挪出的旧配置;Apple 芯片上的旧 Intel 版 Homebrew 和它留下的失效链接 |
| 常驻项 | persistence.* | 要确认 / 仅供参考 | 本用户 LaunchAgent 指向不存在的程序;系统级 LaunchAgents / LaunchDaemons、应用登记的后台程序和登录项(launchctl print gui/<uid>)、crontab、shell 配置和 /etc/paths 里的 PATH:读签名(Security,不运行程序)、本用户能否换掉。记下见过的(第一轮只记),之后新出现、没人担保的报 persistence.new(高) |
| 启动钩子 | startup.* | 仅供参考 | 动态库注入环境变量(dyld-insert)、授权插件、登录钩子、终端启动命令、iTerm 自动运行、periodic 脚本 |
| 新装的应用 | apps.bad-signature、apps.unsigned、apps.not-notarized | 要确认 / 仅供参考 | 最近 30 天放进「应用程序」的:签名对不上、没签名或临时签名、没公证;附隔离标记里下载它的程序。浏览器装成应用的网页不算 |
| 进程 | process.* | 要确认 / 仅供参考 | 程序文件已删除的进程;同一父进程下同一程序 ≥ 20 个;跑了 ≥ 6 小时的孤儿 |
| 键盘监听 | keyboard.unvouched、keyboard.watchers | 仅供参考 | 用 CGGetEventTapList 列出在监听键盘事件的程序;没人担保的算中或高 |
| 对外露出 | exposure.* | 仅供参考 | 本用户程序在回环以外监听 TCP 端口(libproc);正在运行的命令行里带密码、令牌(只记种类);SSH:~/.ssh 和钥匙的权限、没有口令的钥匙、弱算法、authorized_keys、~/.ssh/rc |
| 凭据 | secrets.plaintext、secrets.stray-key | 仅供参考 | 工具明文存的凭据(.git-credentials、.netrc、.npmrc、.pypirc、Docker、gh);下载、桌面、文稿往下两层里的私钥、证书包、云服务密钥。只读小文件的开头,只记种类和个数 |
| 浏览器 | browser.* | 仅供参考 | 7 个 Chromium 系浏览器和 Firefox 的扩展、被策略强装的(高)、侧载的、原生消息程序;不看 Safari |
| MCP 配置 | mcp.servers、mcp.secrets | 仅供参考 | 7 处 AI 工具的 MCP 配置:装了哪些服务器,配置里有没有明文密钥 |
| 权限授予 | permission.stale-grant | 要确认 | TCC 里允许的授权,应用或路径已经不在;读不了 TCC 记成没扫描("需要完全磁盘访问") |
| 基础设置 | posture.* | 仅供参考 | FileVault、SIP、Gatekeeper、自动登录(高);防火墙、隐身模式、锁屏和锁屏延迟、自动检查更新、待装更新且 30 天没重启;远程登录、屏幕共享、文件共享、互联网共享、AirDrop 对所有人、访客账户;用户加的根证书(只算不限主机、不是拒绝的);XProtect 60 天没更新。只报不好的状态 |
| 病毒 | malware.* | 要确认 / 仅供参考 | 见下 |
病毒扫描
- 扫什么:下载文件夹里 30 天内来的文件(按修改时间或加入时间)、Spotlight 记着在家目录别处下载的(
kMDItemWhereFroms)、下载来的应用里的程序(Contents/MacOS全部,另加至多 50 个可执行文件)、本用户 LaunchAgent 运行的程序;一轮最多 2000 个。 - 只扫新的:扫过的记在
malware_scanned(路径、inode、大小、修改时间、当时认出的);没变的 7 天后再扫,病毒库更新后 3 天内来的文件用新库再扫;没有要扫的就不起 clamscan。 - ClamAV:
--max-filesize=100M --max-scansize=400M --alert-exceeds-max=yes,太大的报malware.too-large;读不了的文件不交给扫描器,下一轮再看;文件名带控制字符的单独报malware.odd-name。整轮 clamscan 限 30 分钟、YARA 15 分钟。 - YARA:自带两条规则(反弹 shell、伪造的系统密码对话框),加上 macOS 自带 XProtect 的规则(取版本新的那份;编不过时只用自带的),发现记成
malware.yara.xprotect.<规则>。 - 精简库:快速扫描用的库由
sigtool --unpack拆开后去掉Win.、PUA.Win.开头的特征和 PE 的节、导入表哈希,每个库文件一个子目录、只重做变了的;快速扫描扫过的记成+quick,下一次整轮用全库再扫。 - 病毒库更新(
update):每天最多一次;freshclam 在克隆出的临时目录里下载,sigtool --info逐个校验通过才换库,任何一步失败保留旧库;被上游限流时保留 freshclam 的冷却状态。库超过 14 天没更新报malware.stale-db,照样扫。
增量
- 量大小:每个文件夹和它下面两层 ≥ 1 MB 的子目录记下 FSEvents 事件号、inode、大小;下一轮回放日志,没改动、inode 没变、真正量过不到 7 天的沿用,被碰过的只重量改过的子目录。日志换了或丢了细节就全量。
- 临时目录、老项目、卸载残留:同样按 FSEvents 沿用上次的结论(
temp_entries、project_state、leftover_state),最长 7 天。 - 遍历用
getattrlistbulk,不跟符号链接,不进挂在里面的别的卷;APFS、HFS+ 上空文件夹不打开,不够 20 个的临时目录前缀组不细看。新装应用和常驻项的签名结果按文件记下,最多沿用 7 天。
清理
- 方案:
audit_plan或 outbox 卡片从上一轮的发现出方案(最多 30 项),每项可选移到废纸篓、先不动、以后别提,重复文件另有换成克隆,失效的链接是删除。放心删的默认移到废纸篓,要确认的默认先不动。"仅供参考"的不进方案。要密码的项单独一份方案、单独一个按钮。方案存在plans/,30 天后删掉。 - 能进方案的:缓存、残留(含没下完的下载、旧的 Intel 版 Homebrew)、临时目录的成批前缀、干净的久未动项目和其中的构建产物、久未打开的大文件、重复文件、单个 AI 模型、Xcode 旧设备支持文件、本用户指向不存在程序的 LaunchAgent 和新出现的本用户 LaunchAgent、病毒库或 YARA 认出的文件。目标只能在家目录或临时目录里面(不能是家目录、临时目录、项目目录本身);要密码的那份例外。
- 执行(
audit_apply):没有ai.luoke/user-action就拒绝;只执行本工具出过的方案、只接受方案里列的选项,没送的项当"先不动"。每个目标再核对:还在、不是链接、设备号和 inode 没变、在家目录或临时目录里、父目录本用户可写、里面没挂别的盘、查过以后没改过(会重建的缓存cache.rebuildable.*除外,归在缓存类别里的 AI 模型和 Xcode 设备支持文件要重下,同样要查;未来时间不算;核对总共最多 2 分钟);久未动的项目再问一次 git。核对不上的跳过并写原因。 - 怎么删:移到废纸篓(
trashItem)。LaunchAgent 先launchctl bootout再移配置。重复文件移走前确认要留的那份还在且内容相同;换成克隆前逐字节再比一遍,几份都留着;比的时候读失败算不一样。系统级常驻项、登录项、crontab 不进方案:登录项打开系统设置,crontab 说用crontab -e删哪一行,系统级常驻项说在访达里移走(会要密码)。
不用点就清
四类默认开,只在主控定时起的、带 --autoclean 的一轮里、查完且没被打断时做;agent 叫的一轮、快速扫描都不清。用户说过「以后别提」的不挑。
| 类 | 判定 | 怎么清 |
|---|---|---|
| 会重建的缓存 | 位置正好是缓存清单里的那一处、是真文件夹不是链接、里面没挂盘、用它的程序都没在运行。清单里没写谁在用的不自动清(测试要求每项都写) | Go、uv、npm 用工具自己的清理命令,其余直接删文件夹。不进废纸篓,放不回;还留 ≥ 1 MiB 算没清完 |
| 安装包 | 下载文件夹里的普通文件,名字去掉版本、芯片、系统词后正好是已装的应用(≥ 4 个字母);有隔离标记、有签名、来了 ≥ 14 天;名字带版本时不高于已装版本;没被打开、没挂着 | 移到废纸篓 |
| 重复文件 | 副本在下载文件夹里,来和改都 ≥ 7 天、不在项目里、独占 ≥ 1 MiB;留的那份在下载之外、在本地、不在构建类文件夹里,内容相同;副本没被打开 | 移到废纸篓 |
| 测试工具的临时文件夹 | 只认 6 个前缀(Puppeteer、Playwright 的配置文件夹和产物、Lighthouse);先要有 leak.temp-prefix 发现;每个文件夹 3 天没写、属于本用户、没挂盘、浏览器没开着、没被任何命令行点名;读不到进程列表就都不清 | 移到废纸篓 |
- 移进废纸篓的三类,动手前和点过的方案做同样的核对,每项前重读开关,ctime 不晚于这一轮开始。
- 开关:
audit_autoclean开关全部四类。从某类放回过东西,那一类以后不再自动清(autoOff),关了再开时清空。状态文件读不出就不清,之后从"关"开始。 - 被打断:缓存和移进废纸篓的每项之间都看一次,用户回来就停在两项之间,剩下的等下一轮。主控发 SIGTERM 15 秒后强杀,工具自己的清理命令可能跑两分钟,所以每清一项就写一次记录;这一轮的报告没存下时,由下一轮那一件告诉用户。
- 清了的记成
auto-done:卡片给 [放回](整批,缓存除外)和 [以后别自动清]。
要密码的删除
- 范围:
/Library的 Application Support、Caches、Logs 里 root 拥有的卸载残留,和旧的 Intel 版 Homebrew(Cellar、Caskroom、Homebrew 三个文件夹全动或全不动)。按钮是「移到废纸篓(要输入密码)」,选项绑在/admin_choices。 - 流程:用户进程先再判一遍;点了以后弹 macOS 的授权框(
NSAppleScript),在终端里是sudo -k,每次都问。root 那一侧把luoke-audit-admin拷进只有 root 能写的临时文件夹,核对是小于 100 MB 的普通文件,按发布签名验这份拷贝,用空环境运行。它按和扫描同一个判定重判每一项,再看整棵树,30 天内写过或有别的账户东西的不动,逐层不跟链接地打开,renameatx_np移进废纸篓。结果只经标准输出交回,由用户进程写记录。取消密码框时方案保留。 - 在终端里:
luoke-audit system --this-mac列出,--trash a0,a1移走;root 直接跑会被拒绝。
放回
- 清理记录写在只追加的
cleaned.jsonl,audit_restore按一条(c-…)或一批(p-…/auto-…)放回,能放回的都带ai.luoke.audit.cleaned标记。 - 用
RENAME_EXCL放,原位置有同名的放成「名字(放回)」。整批不带报毒的文件和常驻项;受保护的位置不放回;要密码删的要在访达里拖。缓存放不回。
接口
给 agent 的 MCP 工具
按 agent 工具协议(版本 1,MCP 2026-07-28 的无状态子集:server/discover、resources/list、resources/read、tools/list、tools/call),在 ~/.luoke-audit/run/agent.sock 上 POST /mcp,请求体至多 1 MB。卡片是 A2UI v0.9(目录 ai.luoke/a2ui/agent/1)。不支持订阅。
| 工具 | 只读 | 参数 → 答复 |
|---|---|---|
audit_status | 是 | — → 上一轮的时间和耗时、三种结论各多少、放心删能腾出多少、病毒库版本和日期、没查成的规则、缓存是否自动清、是否正在查;带卡片 |
audit_findings | 是 | 可选 verdict(safe / confirm / info)、category、limit(1–50,默认 20)→ 发现列表和卡片;标成"以后别提"的不列 |
audit_scan_now | 是 | — → 另起 luoke-audit scan --for-agent,马上答复;这一轮不清东西;已经在查就说一声 |
audit_plan | 是 | 可选 scope(all / cache)→ 方案和确认卡片 |
audit_apply | 否(destructive) | plan_id、choices → 移了、克隆了、部分处理、已不在、没动的各几项;什么都没剩时 done: true |
audit_ignore | 否 | finding_id → 以后不再报这一条 |
audit_restore | 否 | id(c-…、p-…、auto-…)→ 放回一条或一批 |
audit_autoclean | 否 | on(旧的 enable 照旧认)→ 开或关不用点就清的四类 |
audit_reveal | 是 | finding_id → 在访达里选中;东西不在了答 stale。标 ai.luoke/where: mac |
audit_open_settings | 是 | grant(隐私与安全性里的某项权限,或「登录项」)→ 打开系统设置对应页。标 Mac |
outbox_ack | 是 | id、state(shown / answered / dismissed);answered、dismissed 的不再出 |
非只读的工具没有完整的 ai.luoke/user-action(surfaceId、componentId、at)答 needs_user_action。其他错误码:not_ready、stale、invalid、failed。发现的编号跨轮稳定:规则名加规范化路径(或规则给的键)的哈希。卡片里家目录下的路径写成 ~/…。
资源
| 资源(相对 id) | 内容 |
|---|---|
outbox(luoke://luoke-audit/outbox) | 见下 |
summary | {count, text},count 算放心删、要确认和风险高的仅供参考,其余仅供参考不算 |
permissions | fda 当场探测(granted / denied / ask)、auto(on / off)、admin(恒为 ask,每次问) |
page/overview | A2UI 基础页:一句摘要、上一轮的计数、现在查一下 |
page/cleaned | A2UI 基础页:最近 20 批清理,能放回的带按钮 |
outbox
| kind | urgency | 什么时候出 |
|---|---|---|
security | urgent | 病毒库或 YARA 认出的文件、新出现的没人担保的常驻项,每件一条、跨轮同一 id;放下(dismissed)后不再出。卡片给移到废纸篓 / 停掉并移到废纸篓(本用户的 LaunchAgent)/ 打开「登录项」设置、在访达里显示、以后别提 |
space | normal | 只在可用空间低(space.low)时,每天一条;有会重建的缓存时同一张卡片带清理方案 |
needs-access | low | 这一轮有检查因为没有完全磁盘访问没查成;带 "permission": "fda" 和打开系统设置的按钮 |
auto-done | normal | 一轮的那一件,有自动清了的时候。上半部是清了的,[放回]、[以后别自动清];下半部是要用户决定的和风险高的提醒。过期时间取清理后 7 天和这一轮开始后 3 天里晚的 |
findings | low | 一轮的那一件,没有自动清了的、但有放心删、要确认或风险高的仅供参考的时候:摘要和清理方案,3 天后过期。用户关了自动清时附「以后自动清」;权限残留附打开系统设置的按钮 |
一轮一件:编号 round/<这一轮开始的时间>,下一轮的那件顶替它;只剩风险不高的仅供参考时不放。agent 还没说过(没回 outbox_ack shown)的自动清带进下一轮那件,最多一周,卡片画最近 2 次;什么都没清掉只是失败的只写一句。space.growth 不进 outbox,只在发现列表里。
卡片上列什么:标题数到的都在卡片上,在方案里、在要收回的授权里,或单独列出。单独列的是进不了方案的要确认项(例如 apps.unsigned、process.deleted-binary)和风险高的仅供参考(例如 posture.filevault、browser.forced-extensions,keyboard.unvouched、mcp.secrets 判为高的),每种至多 10 条,高的在前,每条写怎么办,有的话带打开系统设置和在访达里显示的按钮,这些按钮都不删东西。放不下的写"还有 N 条"。
命令行
| 子命令 | 做什么 |
|---|---|
scan --this-mac [--json] | 查一轮;--autoclean 查完做不用点就清(空闲任务这样起,不和 --for-agent、--quick、--home、--data 一起用);--for-agent agent 叫的一轮;--quick 快速扫描;--if-none 已有报告就不查 |
update --this-mac [--force] | 更新病毒库,每天最多一次 |
report | 终端里看上一轮,按放心删、要确认、仅供参考分组 |
findings、status [--json] | 上一轮的全部发现 / 概况 |
cleaned --this-mac [--json] | 清理记录:移进废纸篓的、直接删的,哪些还能放回 |
restore --this-mac <编号>… | 放回一条(c-…)或一批(p-… / auto-…) |
system --this-mac [--trash a0,a1] | 列出要密码才能删的;--trash 把这几项移到废纸篓,sudo 每次在终端里问密码 |
eval-cleanup [--cases <文件>] | 清理规则评测,挑错一个就退出码 1(只在开发版) |
mcp | 答一个 MCP 请求(luoke-audit-serve 用) |
fixture <目录> | 生成假用户目录(只在开发版);luoke-audit-serve --fixture <目录> 起只查它的服务,时钟从固定日期往前走,移到废纸篓只挪进 <目录>/Trash,不调 launchctl |
version | 程序和规则包版本 |
开发版用 --home、--data、--tmp、--projects、--tool-dir 指向假目录和自己的数据目录。
生成任务
两个任务的提示词在 prompts/(照顶层 ADR 0019 的格式),评测程序读这一份;建议参数 temperature 0、top_p 1、不思考。体检的程序里还没有调用主控的代码,提示词也不打进包:现在报告、卡片和 outbox 的文字都由规则生成,不经模型,有没有模型产出一样。
| 任务 | 提示词 | 输入 → 输出 |
|---|---|---|
luoke-audit.explain | 第 4 版,至多 120 字、300 token | 一条发现(结论、标题、位置、大小、依据)→ 一两句大白话:是什么、为什么报、处理会怎样。不改结论,不编数字,不说已经处理,would_lose 每项都说,不写命令 |
luoke-audit.identify | 第 5 版,200 token | 认不出的目录的元信息(路径、大小、最后改动、文件数、常见扩展名、前几项的名字,不含内容)→ JSON {name, what, kind, sure},kind 为 cache / app-data / user-data / logs / tool / build / unknown;能不能删不由模型说 |
依赖
| 依赖 | 用来做什么 | 现状 |
|---|---|---|
| 主控 luoke-core | 空闲任务 scan(每天一次,memory_mb 1500,60 分钟)、update(每天一次,800,20 分钟),都不用电池;生成任务 luoke-audit.explain、luoke-audit.identify;常驻起 luoke-audit-serve;安装、升级、卸载;GET /tasks 给出写记忆用的 Python | 配件清单 packaging/luoke-accessory.json 写了这些;主控的配方里还没有本工具,也不认 resident_exec |
| agent luoke-agent | 唯一的提醒出口:调 MCP 工具、画卡片、转达 outbox | 体检这边已做好;agent 正式接入和螺壳窗口在同一个 v1.x |
| 记忆 luoke-mem | 写"发生了什么",五种记录:audit-cleaned、audit-ignored、audit-malware、audit-auto-cleaned、audit-autoclean。不写路径,报毒的只写文件名。经 <螺壳的 python> -m luoke_mem record --tool luoke-audit,只认螺壳 Team 签名的 Python,让它自己负责,不沾完全磁盘访问 | 代码已有;进配方之前主控答 403,写不进去。问不到主控就静默跳过。不读记忆 |
| macOS | FSEvents、getattrlistbulk、Security(验签名)、Spotlight(mdfind)、libproc、CGGetEventTapList、launchctl、tmutil、fdesetup、csrutil、spctl、sysadminctl、hdiutil、pkgutil、codesign、security、defaults、crontab、TCC 数据库、XProtect 规则 | 系统自带;不用会弹管理员密码框的 sfltool,不用会卡住的 lsof |
| ClamAV、YARA | 特征扫描;病毒库由 freshclam 从 database.clamav.net 下载 | 随包;往外只有这一项下载,不上传 |
数据
| 位置 | 内容 |
|---|---|
~/.luoke-audit/ | 数据目录(700),排除出 Time Machine |
audit.db | SQLite(WAL):sizes(每天各文件夹的大小,留 90 天)、measured / temp_entries / project_state / leftover_state(增量用的事件号、inode、大小或结论)、malware_scanned、file_hashes(重复文件的 SHA-256,文件不在了就删)、seen(见过的常驻项)、meta(病毒库版本、更新时间、日志游标、签名缓存、常驻项基线、快速扫描的事件号)。都是关于这台 Mac 的事实,删了下一轮重建 |
last-report.json | 上一轮的报告(600,luoke-audit.report/v1):发现、没扫描的项、规则包和病毒库版本、各规则耗时;每轮覆盖 |
agent-state.json | 用户经 agent 说的(600,加锁读改写):以后别提的发现、outbox 的回执(60 天;报毒和权限请求的放下回执一直留,至多 500 条)、自动清的开关和放回过的类别(autoOff)、最近 20 次自动清、办完的方案。读不出时挪到一边,不覆盖 |
cleaned.jsonl | 清理记录,只追加,放回靠它 |
plans/ | 出过的方案(每项的路径、设备号、inode)、执行时的锁和要密码那一步的待办标记,30 天后删 |
clamav/、clamav-quick/ | 病毒库和精简库;更新时用 clamav.staging、clamav.previous;freshclam.conf 每次更新前重写 |
run/agent.sock | MCP socket(目录 700、socket 600),旁边的锁文件在服务退出时删掉 |
round.lock、quick.lock、serve.lock | 见「运行形态」 |
- 报告和卡片里不放文件内容和凭证的值;凭据类发现只有位置、种类和个数。
- 标准错误只写退出码和出错的类型,不写发现的内容。
打包
scripts/package_app.py拼「螺壳体检.app」:bundle idai.luoke.audit,LSUIElement,只出 arm64,最低 26.2。Contents/MacOS里有luoke-audit、luoke-audit-serve(ai.luoke.audit.serve)、luoke-audit-admin(ai.luoke.audit.admin)和clamscan、freshclam、sigtool、yara;它们要的库在Frameworks,路径改成@rpath;ClamAV 的 RAR 支持运行时由LD_LIBRARY_PATH指到Frameworks。- 由内向外签名,hardened runtime,没有 entitlements(临时签名时扫描工具带"可以载入别人签的库",正式签名不许有);可选公证。Developer ID 证书和公证条目由出版机器给,脚本不带凭据。
Resources里有去掉草案标记的配件清单、bundled-tools.json(每个随包程序和库的版本、来源、SHA-256)、许可证(ClamAV 为 GPLv2)和第三方说明。- 打完必核对:签名完整、都是 hardened runtime、没有
get-task-allow、没有包外的库路径、luoke-audit-serve里没有AuditCore/AuditService的代码、第三方库只有允许清单里的、第三方说明覆盖每个文件、RAR 支持真能载入、Info.plist和配件清单一致、清单没有草案标记、exec和resident_exec是包里的程序、包里的luoke-audit不收开发参数,再用包里的 clamscan 和 YARA 真扫一个 EICAR 测试文件。Swift 包不加第三方依赖(测试里也查)。
评测
- 生成任务:评测集合成(
bench/evalsets/第 3 版:explain 32 条、硬性 13;identify 30 条、硬性 8),每条有确定的检查,三遍都过才算过。bench/run_eval.py可经主控的任务层(--hub,给发布脚本用)或本机端点跑;退出码 0 硬性全过、1 没过、3 没评成。
| 底座(各 3 遍,温度 0) | explain 第 4 版 | identify 第 5 版 |
|---|---|---|
qwen3.5-9b-4bit(32 GB 档) | 31 / 32,硬性 13 / 13 | 29 / 30,硬性 8 / 8 |
gemma-4-26b-8bit(128 GB 档) | 29 / 32,硬性 13 / 13 | 29 / 30,硬性 8 / 8 |
- 清理规则:
evals/cleanup/cases.json72 例(安装包 27、缓存 18、重复文件 17、临时文件夹 10),luoke-audit eval-cleanup跑,挑错算硬门槛。 - 性能:
bench/perf.py在 12 万文件的假家目录上跑 release 版,CPU 时间或内存比基线差 25% 以上、且超过下限(CPU 1 秒、内存 20 MB)算不过;墙钟只报不比。一台真实机器上一整轮 348 秒降到 140 秒(空文件夹不打开以后)。基线量于 21 条规则时(64 GB 测试机):不带病毒库,第一轮 CPU 3.6 秒、没变的一轮 0.65 秒、改了 1% 的一轮 1.25 秒,进程树峰值约 30 MB;带病毒库第一轮峰值约 1 GB(clamscan 载入全库),没变的一轮约 37 MB。 - 测试:
AuditCoreTests224 个@Test、AuditServiceTests173 个,全部用生成的假用户目录;scripts/agent_selftest.py用 luoke-agent 的参考实现对luoke-audit-serve --fixture走一遍。
已知限制
- 没接进主控:配方里没有本工具,主控不认
resident_exec。所以定时的一轮、不用点就清、写记忆现在都不会在装好的螺壳上发生。 - 没有模型参与:
explain、identify只有提示词和评测,程序里还没调用;认不出的目录不会被起名。 - 没有窗口:主程序是命令行,双击 .app 只报用法后退出。两页基础页要等螺壳的窗口来画,agent 现在不读;卡片在 agent 接入后显示在它的窗口里。现在能看的是终端的
report、findings、status、cleaned、system。 - 规则写在代码里:规则、门槛、缓存清单都在 Swift 里,没有规则文件;规则包版本只是一个常量。加一条规则要改代码、发新版。
- 要密码的删除还没在真机上弹过密码框。
- 没有按类别手动关自动清的开关:只能全关,或放回一次让那一类停下。
主控任务接口
主控在 Unix socket 上提供的 HTTP/1.1 接口,供配件调用。每次请求一条连接。
连接
- socket:
~/Library/Application Support/luoke-core/run/hub.sock(目录 0700,文件 0600)。配件依次查找:显式路径 → 环境变量LUOKE_HUB_SOCKET→ 默认位置。 - 身份:请求头
X-Luoke-Accessory: <配件>/<版本>。版本须在配方verified中,或本机放行,或是本机verified-history.json中有记录的已安装版本(换回的上一版,只能用记录中的任务);带任务名的请求还要求任务存在且属于该配件。 - 响应头:
X-Luoke-Contract: 1、X-Luoke-Tasks-Version(配方、档、模型分配、引擎版本、任务参数的摘要,变化即应重读/tasks)。 - 请求体上限 32 MB。
错误
错误体为 {"code", "message", "retry_after", …};有 retry_after 时同时给 Retry-After 头。
| HTTP | code | 含义 |
|---|---|---|
| 400 | invalid_request | 请求格式或参数不合法 |
| 403 | unknown_accessory | 配件或版本未通过验证,或任务不属于该配件 |
| 403 | forbidden | 主控本体专用的路由,对端不是本体(或允许的命令行) |
| 404 | task_unavailable、unsupported | 任务不存在;本档不支持该任务;换回的上一版调用其记录外的任务 |
| 413 | input_too_long | 超过任务的 max_chars |
| 503 | busy | 暂时不能接纳,附 reason(见主控 · 后台接纳) |
paused | 全机后台暂停或本档放不下 | |
not_ready | 模型正在加载或工作进程按需重起(retry_after 2);内存不够或内存压力 critical 后暂不启动工作进程(retry_after 30) | |
needs_user | 用户在等但内存放不下,附 occupants(占用内存的进程) | |
engine_failed | 引擎或工作进程出错 |
流式生成过程中的错误以 event: luoke.error 事件给出,另有 interrupted(后台请求被用户请求打断)。
任务
GET /tasks
返回 {contract, hub_version, tier, tasks_version, background: {state, reasons}, tasks: {<名>: {kind, status, reason, progress, slow, combo, model_name, params, vector_space?}}, luoke_mem?: {path, version}}。只列出请求方自己的任务;model_name 是只用来显示的模型名(配方的 label,没有则取目录名,本档没有模型时为 null);luoke_mem 给出主控内置的记忆库路径。
生成 POST /tasks/<task>/v1/chat/completions
- 任务 kind 为
generate。请求为 OpenAI 格式的messages,必须stream: true,带luoke_items(本批条目数,≤batch);response_format可用json_schema或json_object。 - 请求头
X-Luoke-Priority必填(user-waiting或background);X-Luoke-Allow-Battery: 1可选。 - 主控覆盖:
max_tokens = item_tokens × luoke_items + fixed_tokens;采样参数取配方;中和消息中的模型特殊 token;答复中的model改写为组合名。 - SSE 流中另有
event: luoke.progress(queued、loading、running)。
打分 POST /tasks/<task>/v1/score
kind score。请求 {context, candidates}(候选 ≤ batch,默认 16;前文 ≤ max_chars,默认 2000);答复 {logprobs, model, cache_hit, ms}。超时 10 秒。
续写 POST /tasks/<task>/v1/continue
kind predict。请求 {context, k?, max_tokens?, stop?};答复 {branches: [{tokens: [{text, logprob}], finish}], model, cache_hit, ms}。旧接口 /v1/predict 仍保留。
嵌入 POST /tasks/<task>/v1/embed
kind embed。请求 {texts: [...]}(任务参数允许时可用多模态 inputs);答复 {vectors, dims, model, vector_space, ms}。默认每批 32 条、单条 32000 字、合计 320000 字。后台请求走专用模型通道,被挡回时返回 lane_busy 并附 retry_after_ms。
预热 POST /warmup
请求 {task, lease_id?};答复 {accepted, lease_id, expires_in: 600},或 {accepted: false, code}(task_unavailable、unsupported、deferred)。新的预热算作交互,续期不算。打分、联想、嵌入任务的预热会启动工作进程;内存不够时照样答应,真正请求时答 not_ready。
配件管理
| 路由 | 说明 |
|---|---|
GET /accessories | 已安装配件的公开字段:accessory、bundle_id、name、version、type、resident、memory、tasks、capabilities、jobs |
PUT /accessory/resident | {resident},声明是否常驻 |
POST /accessory/exiting | 配件主动退出前通知,主控停止只由它使用的工作进程 |
主控本体专用
以下路由要求身份为 luoke-core,并按内核报告的对端 PID 核对对端是主控进程的父进程(即本体)。/settings 和 /endpoint* 另外接受主控自带的命令行 luoke(与主控同一个 Python 可执行文件、参数为 -m luoke_core.cli)。不符时答 forbidden。
| 路由 | 说明 |
|---|---|
GET /status | 运行状态(不要求身份):档、底座、引擎、交互、后台、冒烟、设置、下载、空闲任务、端点(不含密钥)等 |
GET/POST /downloads | 缺少的下载项和进度;POST 开始下载 |
GET/PUT /settings | power_mode、background_on_battery、tier、endpoint_enabled、endpoint_port、endpoint_key |
GET /endpoint、POST /endpoint/key | 端点状态和密钥;生成新密钥 |
POST /endpoint/models、/endpoint/models/remove | 登记、撤销用户自己的模型 |
POST /smoke | 重新做冒烟检查 |
POST /uninstall | {accessory, data?, forget?, dry_run?};也删除 previous.noindex 里该配件的旧版(答复 previous) |
GET /accessories/previous | 留有旧版的配件:{accessories: [{accessory, name, type, bundle_id, current, previous, verified, release}]};verified 为假的不能换回 |
POST /accessories/rollback | {accessory} → {accessory, from, to, type, bundle_id};无旧版或无验证记录 unsupported,签名不过 invalid_request,对调失败 engine_failed |
POST /jobs/run | 让某个空闲任务在下一个安静时刻立即运行(只在内部版、本机版和实验室版可用) |
高级用户端点
- 默认关;开启后监听 127.0.0.1,默认端口 17860(被占则向后找)。
- 检查:Host 必须是
127.0.0.1或localhost;带Origin头一律拒绝;密钥非空时校验Authorization: Bearer(用户可自定义或留空)。 GET /v1/models:底座名和user/<名字>(用户登记的模型)。POST /v1/chat/completions:按"用户在等"调度。- 错误:
model_not_found(404)、too_large(507,超过独占上限:内存 − 8%(3–8 GB)− 常驻专用模型 − 1 GB)、not_ready、busy、engine_failed(503)。 - 命令行
luoke launch pi [--model M]自动读取端口和密钥,在 pi 的配置中写入luokeprovider 并启动 pi。
配件清单与配方
配件清单 luoke-accessory.json
位于配件 .app 的 Contents/Resources/。主控只加载 contract == 1 且有 accessory、bundle_id 的清单。
| 字段 | 说明 |
|---|---|
contract、accessory、bundle_id | 必填 |
name、version | 显示名和版本;升级只换比清单旧的版本(按数字比较,不降级) |
type | app(装在 ~/Applications)、input-method(装在 ~/Library/Input Methods)、component;只有 app 可常驻 |
entry | 交给自带 Python 的入口参数 |
resident | 是否常驻(配件也可经 PUT /accessory/resident 声明) |
min_macos | 可选;这台 Mac 的系统版本低于它时不能装、说明原因(发布清单照抄) |
memory | 是否读写记忆;决定卸载时能否"一起忘掉"、首次运行是否需要记忆 |
tasks | 需要主控提供的任务(name、kind、main) |
jobs | 空闲任务:name、command(可用占位符 {memory}、{tool})、min_interval_minutes(默认 120)、allow_battery(默认否)、timeout_minutes(默认 30)、exec(可选:不带 Python 的配件用 Contents/MacOS 下的可执行文件名,不能含路径) |
capabilities | 给 agent 的能力声明,主控原样转交、不解释(规格见 agent 的工具协议),可含 service、permissions、listing;出版时把 listing 的 tagline、category 和各权限的说明(id、kind、title、facts、without)抄进发布清单的 listing,不抄 size_mb、min_tier、权限的默认值和动作名 |
data | 卸载时可删除的数据路径,只接受与配件自身标识绑定的形式 |
commands | uninstall(删除数据前运行)、forget-tool(从记忆中忘掉本工具写入的内容) |
icon、ui_element | 打包用 |
打包布局
<名字>.app/Contents/MacOS/<配件标识> 启动器 <名字>.app/Contents/Resources/luoke-accessory.json <名字>.app/Contents/Resources/python/bin/python3 钉住版本的 python-build-standalone <名字>.app/Contents/Resources/python/lib/python3.X/site-packages/<包>
由 luoke-core/tools/package_python_app.py 生成。
校验
- 调用时:版本在配方
verified中,或在内部版、本机版主控上被本机放行(tools/local_allow.py,只放行配方中已有配件的新版本);或是本机verified-history.json中记录过的已安装版本(只限记录中的任务)。 - 安装时:代码签名、Team ID
NJQ5W3T9K5、bundle id 与清单一致、公证票据有效。 - 回滚时:旧版同样核签名、Team ID、bundle id、公证票据,不运行它,版本从其清单读取。
配方 recipe.json
| 字段 | 说明 |
|---|---|
tiers.<档> | min_gb、max_gb、guard_gb(引擎内存上限)、base_resident(底座是否常驻)、background_input_chars、idle_unload_secs |
models.<名> | dir、kind(base / special)、wired_gb、size_gb、速度、vector_space(嵌入)等 |
assignment.<档> | {base, special: {<任务>: <模型>}} |
engine | 引擎版本和位置 |
tasks.<名> | accessory、kind(generate / score / predict / embed)、params("*" 加按档覆盖)、base_params(采样)、latency_ms、tiers(开启的档) |
verified.<配件> | versions(允许接入的版本)和各任务的评测证据 |
evidence | 评测记录:配件、版本、任务、模型、档、结论、评测集和证据的 sha256 |
expectations | 首次运行预检页照实说明的体验:tiers 按档(如 16 GB 档:邮件后台整理平均每封约 50 秒,点「同步新邮件」每封约 22 秒),ime 按芯片类别(m1、base、pro),typing_unsupported 用在不支持打字归纳的档 |
任务参数
| kind | 参数(默认) |
|---|---|
| generate | item_tokens(200)、fixed_tokens、batch、max_chars、think;配件自定义的参数原样转交(如 sender_verdict) |
| score | batch(16)、max_chars(2000)、prefix_snapshots(16)、probe |
| predict | k(8)、n(6)、max_tokens、max_chars、probe |
| embed | batch(32)、max_tokens(2048)、max_chars(32000)、max_total_chars(320000);多模态另有图片、音频的批量和大小限制 |
硬件档
| 档 | 内存 | 引擎上限 | 底座 | 底座常驻 |
|---|---|---|---|---|
| 16 | < 24 GB | 10 GB | qwen3.5-9b-4bit | 否(用完即卸,打字时让路) |
| 32 | 24–64 GB | 20 GB | qwen3.5-9b-4bit | 是 |
| 128 | ≥ 64 GB | 100 GB | gemma-4-26b-8bit(可选省空间用 9B) | 是 |
所有档的打分和联想模型为 qwen3.5-0.8b-base-4bit(独立进程,约 2.7 GB)。
发布清单 release.json
{contract, release, hub_version, built_at, local_only?, items: {<名>: {kind: model | engine | accessory | hub, version, dir, files: [{path, size, sha256}], urls, license?, accessory?, bundle_id?, type?, name?, memory?, installer?, min_macos?, listing?}}, installers?}(installers 只在另出应用安装包时有;发布页只放螺壳的 Luoke.dmg)。模型有 ModelScope 和 Hugging Face 两个镜像,安装包放在 GitHub 上:beta 和正式版在 luokelabs/luoke-releases,内部版在 luokelabs/luoke-internal-builds。主控的"检查更新"从 items.hub 取 DMG。
记忆接口与表结构
Python 包 luoke_mem(仅标准库,3.9+)。工具只需 __init__、memory、sensitive 三个模块;运行在主控里的配件应使用 GET /tasks 返回的 luoke_mem.path。
打开
Memory(path=None, tool="luoke"):工具使用。默认路径$LUOKE_HOME/mem.db,未设置时为~/.luoke/mem.db。文件 0600,目录 0700;记忆文件或其-wal、-shm、-journal是符号链接时拒绝打开。文件不存在时创建(0600);只看不改的入口(命令行sources、forget-tool、learn)不创建。WholeMemory(path=None):整份记忆,供记忆自己的界面、命令行和归纳任务使用;打开时执行重新定档(只降不升)、以前的事、相近话题合并。第一次定档前把整份记忆复制为mem.db.before-0.18,14 天后或有内容被忘掉时删除。- 文件 schema 比库新得多(千位不同)时抛
TooNew。
工具接口
记录
| 方法 | 说明 |
|---|---|
record(speaker, kind, text, *, topic, data, ref, happened_at, sensitive) | 写一条记录;正文和 data 中的字符串先遮去凭证 |
redate(rid, happened_at) | 修正本工具一条记录的发生时间 |
records(*, tool="", topic, kind, speaker, ref, since, limit) | tool="" 为本工具,传工具名读该工具,None 读全部 |
forget_records(*, topics, since, until, match, keep_cited) | 删除本工具的部分记录,只依赖这些记录的理解一并删除 |
forget_tool(tool=None, *, dry_run) | 删除本工具写入的全部内容;用户说过的话和表过态的理解转到记忆名下保留 |
理解
| 方法 | 说明 |
|---|---|
state(kind, text, …) | 用户亲口说的,直接为"确定" |
propose(kind, text, cites, …) | 推测;按定档规则可能直接生效 |
conclude(kind, text, cites, …) | 由用户自己的记录得出;依据须全部来自用户,按定档规则为比较确定或猜测 |
confirm(nid) / reject(nid) / was(nid) / later(nid) / revise(nid, text) | 用户的决定:确认、否定、以前是、以后再说、改写 |
reject_family(nid) | 「这一类都不要」:把这条所在一类里用户还没表态的(待确认和观察中)都标为否定,返回条数 |
notes(kinds, status, tool, confidence) | 默认只返回生效的(比较确定和确定);每条带 confidence、data、auto、fresh |
note(nid) | 一条理解及其全部依据(含原文) |
recent(since, limit) | 未经用户表态而直接生效的推断 |
context(kinds, limit=600) | 按类别拼成供模型使用的一段文字 |
forget(nid) | 删除理解、其历史版本及只被它引用的记录 |
想问的和建议
| 方法 | 说明 |
|---|---|
asks(status=["open"], *, tool) | 未过期的问题和建议;tool 筛出与某工具相关的 |
reply(aid, choice, text=None) | 交回用户的回答:关于某条理解的问题 yes / no / past / revise / later(later 把问题记为 later,7 天后由反刍重新提出);自由问题 answer / later(7 天后再问);建议 useful(关于习惯的建议同时确认该习惯)/ useless / never;其他组合抛 ValueError |
工具函数
mask_secrets(text)(遮去密码、验证码、密钥、通过 Luhn 校验的卡号、证件号)、find_secrets、sensitive_kinds(text)(健康、法律、宗教政治)、confidence(note)、default_path()。
表结构(schema 7)
| 表 | 列 |
|---|---|
records | id、tool、speaker、kind、topic、text、data_json、ref、happened_at、recorded_at、sensitive |
notes | id、tool、kind、text、origin(user / inferred)、status、superseded_by、valid_from、valid_to、formed_at、decided_at、why、topic、sensitive、data_json |
cites | (note_id, record_id) |
asks | id、tool、kind(question / suggestion)、text、why、about_note、about_kind、urgency、answer(JSON)、status、created_at、expires_at、decided_at、answer_record、sensitive、evidence、rank、action(JSON)、key |
candidates | id、kind、text、record_ids、first_seen、last_seen(归纳中积累的候选) |
learning | name、value:fit.deleted(用户删掉的理解,按类只记条数);归纳进度(typing.last_id / last_at / last_run、topics.last_id / last_at / last_run / pending / version)、每次结果(typing.last_outcome、topics.last_outcome、rumination.last)、以前的事唤回次数(past.revived.<kind>)、一次性修复标记 |
grants | 遗留,不再使用 |
升级只加不改:新表、可空的新列、新索引。版本存于 PRAGMA user_version。
取值
| 字段 | 取值 |
|---|---|
| records.speaker | user、third_party、system |
| records.kind | statement、activity、receipt |
| notes.kind | fact、interest、dislike、preference、term、taste |
| notes.status | proposed、active、rejected、superseded、observed(观察中:这一类门槛升档后,待确认的事实挪到这里)、past |
| asks.status | open、answered、dismissed、withdrawn、later、never |
| preference 的 data | {account, sender, action, name, category, label | filter};可自动执行的动作 archive、read、label、filter |
记录话题一览
| 工具 | topic |
|---|---|
| 邮件助手 | mail-read、mail-acted、mail-override、mail-auto、mail-undo-auto、mail-cleared、sent-mail、received-mail、own-label、own-site、digest-open、digest-feedback |
| 输入法 | ime-commit、ime-correction、ime-forget |
| 记忆 | note:<kind>(用户亲口说的)、note-decision、note-later、ask-answer、ask-dismissed |
命令行
| 命令 | 说明 |
|---|---|
python -m luoke_mem [--path P] sources | 各工具写入的条数 |
forget-tool <tool> [--dry-run] | 忘掉某工具写入的内容;luoke-mem 被拒绝(说明原因,退出码 1) |
ui [--open] [--note N] [--view recent] | 打印界面地址 |
app [--note N] [--view recent] | 在浏览器中打开界面(luoke://memory 的入口) |
learn [--force] [--again] [--hub SOCK] | 运行一次归纳;--force 有新记录就跑;--again 从头重新归纳(打字记下的不重复,话题线用户没表态的话题重来);已有一个在跑时直接返回 |
界面
独立进程,监听 127.0.0.1 随机端口;页面令牌在 ?t=,接口令牌在 X-Luoke-Token;校验 Host 和 Origin;600 秒无请求退出。路由:/api/ping、/api/summary、/api/overview、/api/records、/api/notes、/api/note、/api/recent;修改用 POST /api/change(confirm、reject、reject-family、forget、revise;修改文字 ≤ 200 字,拒绝凭证)、/api/forget-tool(拒绝 luoke-mem)、/api/forget-all(需输入「清空」)。/api/grants、/api/grant 为遗留接口,不再影响读取。界面状态(pid、端口、令牌)存于 mem.db-ui.json(0600)。
任务与提示词
所有经主控调用模型的任务:所属配件、种类、各硬件档是否开启、提示词位置。档位以发布时的支持台账为准,下表为最近一次内部版的台账。每个提示词的输入、输出、校验和评测见「提示词」页。
| 任务 | kind | 16 | 32 | 128 | 提示词 / 实现 |
|---|---|---|---|---|---|
luoke-mail.triage | generate | 开 | 开 | 开 | luoke_mail/triage.py |
luoke-mail.draft | generate | 开 | 开 | 开 | luoke_mail/triage.py;无评测,沿用 |
luoke-mail.cleanup | generate | 开 | 开 | 开 | luoke_mail/triage.py |
luoke-mail.senders | generate | 开 | 开 | 开 | luoke_mail/triage.py |
luoke-mail.digest-read | generate | 开 | 开 | 开 | luoke_mail/digest.py;无评测,沿用 |
luoke-mail.digest-pick | generate | 开 | 开 | 开 | luoke_mail/editor.py;无评测,沿用 |
luoke-mail.learn-sent | generate | 开 | 开 | 开 | luoke_mail/learn.py |
luoke-mail.learn-orgs | generate | 开 | 开 | 开 | luoke_mail/learn.py |
luoke-mail.thread | generate | 开 | 开 | 开 | luoke_mail/threads.py |
luoke-mem.typing | generate | 关 | 开 | 开 | luoke_mem/learn.py;16 档过 1 遍,连过 3 遍才开 |
luoke-mem.topics | generate | 开 | 开 | 开 | luoke_mem/topics.py |
luoke-ime.rerank | score | 开 | 开 | 开 | 无提示词(打分) |
luoke-ime.predict | predict | 开 | 开 | 开 | 无提示词(续写) |
digup.embed | embed | 关 | 关 | 关 | 无提示词(嵌入);预留给深度搜索,归配件 digup。深度搜索请求的是 luoke-search.embed,配方尚未改名,verified 也没有收录 |
提示词约定
- 提示词写在调用方配件里,随请求发给主控;主控不保存、不记录请求文字。
- 输出一律用 JSON schema 约束;配件负责解析和校验,失败时有明确的退路(重问一次、判为保留、跳过)。
- 输出上限由主控按
item_tokens× 条目数 +fixed_tokens定,调用方给的max_tokens不用。 - 邮件、网页、打的字等输入一律声明为数据,其中给 AI 的指令不执行。
- 结果的数量、依据和把握程度由代码判定,模型只负责提名;例外是订阅阅读的分数由模型给,≥ 2 才进精选候选。
评测
- 评测集和硬性条目写在主控的
tools/release_evals.json,评测结果写入配方的evidence,并决定各档开关。起草回复、订阅阅读、精选挑选没有评测,各档一直沿用上一版的"开"。 - 分拣的硬性条目:正常邮件零误报,可疑邮件最多漏 1 封。
- 往来邮件:已了结的不能报为"等你","等你"的不能漏,都一次不能错。模型答"轮到谁"(
user_turn/other_turn/done),由代码映射为等你 / 在等对方 / 已了结。 - 话题归纳:只写品牌名或来源名即为失败;另记是否有具体对象。
提示词
每个生成任务的提示词要点:送进去什么、要求输出什么、代码怎么校验、失败时怎么退。只做摘要,原文在各配件仓库。任务名、种类和各档开关见「任务与提示词」。
共同做法
- 提示词、输出 schema 和校验都在调用方配件里;主控只做通用的生成:不思考(
enable_thinking关),把response_format的 JSON schema 交给引擎约束,消息里的特殊 token 先中和。 - 输出上限由主控定:
item_tokens× 条目数 +fixed_tokens;调用方给的max_tokens不用。输入超过任务的max_chars答input_too_long,条目数超过batch拒绝;调用方事先按上限二分截短或减少条目。 - 输入里的邮件、订阅、打的字一律声明为数据,其中给 AI 的话不执行。
- 结果多少、能不能生效、把握几档由代码和记忆的表决定;模型只负责提名和分类。
- 采样参数在配方里按任务定:起草回复温度 0.3、
top_p0.9,其余生成任务温度 0、top_p1;top_k一律写 0。 - 回答不能用时:邮件助手多数任务把原回答(截到放得下,最多 2000 字)和一句纠正接在消息后面再问一次;订阅体检和精选挑选不重问,往来邮件原样再问一次。记忆不在当次重问,见各任务。
- 两个客户端都不看
finish_reason:输出被截断时,邮件助手按"无法解析"处理,记忆按"这批没读完"处理,下次减半重来。
邮件助手
luoke-mail.triage 分拣
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/triage.py:SYSTEM_PROMPT、build_messages、parse_output |
| 输入 | 今天的日期和星期;邮件寄出日期早于今天时,再给寄出日期和星期,并要求没写年份的日期和"明天""下周三"这类说法按寄出日期换算;用户邮箱;用户只在抄送里、不在收件人里时加一句:除非邮件点名要用户做事或回答问题,一般 archive,priority 不超过 medium,回复要点不替用户答应派给别人的事;记忆的"关于用户 / 处理习惯"块(≤ 600 字);用户给这个发件人定的规则(有就给);是否精选来源;发件域名验证结论(配方参数 sender_verdict:16、32 档不给,128 档给);邮件头(发件人、收件人、抄送、时间、主题、附件名 ≤ 10 个)和正文。正文先去掉引用的往来、以 > 开头的行和不可见字符,超过 60 个字符的链接写成 https://域名/…,最多 6000 字;整条消息再按档的字符上限截短(16 / 32 / 128 GB:6000 / 12000 / 32000 字) |
| 输出 | 13 个字段都必填:category(8 选 1)、priority(high / medium / low)、action(reply / schedule / follow_up / archive / ignore)、summary(≤ 40 字)、reason、due_date(只填邮件明写的期限)、5 个 event_*(只在 schedule 时填)、reply_points(≤ 3 条)、suspicious;文字字段用设置的语言 |
| 判断规则 | reply:对方在等用户回信(回答问题、确认时间、发回要的资料都算);对方还在问能不能来、或请用户从几个时间里挑一个,也是 reply;schedule 只给时间已定的会议、预约和活动;follow_up:要用户做、但不用回信的事(付账单、填表、取件、续费);只被抄送、且邮件明确让别人处理的,通常 archive;due_date 只填邮件明写的期限,"下周三"这类按今天换算;回复要点不编邮件里没有的人名、名单和数字。用户的规则一般照办,只有明显要用户亲自处理或看起来可疑时才给不同的 action,并在 reason 里说明;"移到垃圾邮件"的规则让模型给 ignore;精选来源的邮件一般 archive |
| 校验与退路 | 解析不出 JSON 或 action 不在枚举:带一句纠正重问一次,再不行报错;category / priority 不在枚举改成 other / medium;日期须 YYYY-MM-DD、时间须 HH:MM,否则置空;只有 schedule 且日期有效才生成日程;suspicious 为真时强制 ignore + phishing;没写年份、又比寄出日期晚 300 天以上的期限和日程日期,按整年往回拉(邮件里写了那一年或"明年"的不拉);用户只在抄送里的邮件,模型给了 high 也降成 medium |
| 预算 | 输出 320 + 80 token;温度 0 |
| 评测 | 48 封(20 封难题,含钓鱼、注入、发件域名验证、长跟踪链接后的期限);硬性:正常邮件 0 误报,可疑邮件最多漏 1 封。不覆盖规则、记忆块、精选来源、非中文输出,也不评摘要和回复要点 |
luoke-mail.draft 起草回复
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/triage.py:DRAFT_PROMPT、draft_reply |
| 输入 | 记忆块(≤ 600 字);分拣给的回复要点(没有时让模型自己写得体的回复);原邮件(邮件头和正文同分拣,截短;有发件域名验证结论时各档都带);提示词里有今天的日期。不含往来历史 |
| 输出 | {"body"}:用原邮件的语言,不编造事实、日期、数字和承诺,要用户补的地方用【】标出;设了签名就用它署名,没设就不署名、也不编名字;不写用户没给出的电话、住址、账号、证件号;不含主题行 |
| 校验与退路 | 只查正文非空;不行带一句纠正再问一次,再不行报错。草稿只存进草稿箱,由用户自己发 |
| 预算 | 输出 800 + 100 token;温度 0.3、top_p 0.9 |
| 评测 | 无 |
luoke-mail.cleanup 清理判断
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/triage.py:old_prompt、build_old_messages、parse_old_output |
| 输入 | 今天的日期;每封旧邮件一行:发件人、日期、主题(≤ 100 字)、Gmail 分类、用户贴的标签(≤ 4 个);一批 ≤ 25 封,按字符上限再减。星标和"保留"类标签的邮件不送 |
| 输出 | items[{n, kind}]:kind 11 类(归档范围没有 todo);拿不准选保留类,不选 promotion / newsletter / notification |
| 校验与退路 | 编号越界或 kind 不在枚举的丢掉;一条可用的都没有就重问一次;两次都不行,整批记为 other(不建议删) |
| 预算 | 每封 30 + 60 token |
| 评测 | 40 封(归档 30、收件箱 10);硬性:该留的不能判成会建议删的三类 |
luoke-mail.senders 订阅体检
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/triage.py:SENDER_PROMPT、classify_senders |
| 输入 | 很少打开的发件人:名称、地址、最近 ≤ 8 个主题(各 ≤ 60 字);一批 ≤ 10 个 |
| 输出 | items[{n, kind}]:kind 7 类;拿不准选 transactional / security / other |
| 校验与退路 | 越界、不在枚举的丢掉;解析失败返回空(什么都不建议),不重问 |
| 预算 | 每个 25 + 60 token |
| 评测 | 25 个发件人;硬性:账单、登录提醒、个人往来不能判成会建议退订的类别 |
luoke-mail.digest-read 订阅阅读
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/digest.py:PROMPT、build_messages、parse |
| 输入 | 今天的日期;记忆的"不感兴趣 + 精选口味"块(还没有精选口味时用"关注的话题";不感兴趣排在前面;≤ 500 字);收藏和点过"没兴趣"的标题各 ≤ 8 条;订阅正文(HTML 转文字,≤ 12000 字,链接编成〔n〕,≤ 40 个,去掉退订、偏好设置、隐私条款、在浏览器查看、分享和 mailto 链接;正文后附链接表:编号、链接文字(≤ 60 字)、域名),再按档的字符上限截短 |
| 输出 | items ≤ 8 条:title(≤ 30 字)、score(0–3)、lasting、link(编号或 null)、topics(1–3 个)、summary(≤ 80 字,只依据原文)和 why(score ≥ 2 时填);提示词要求 2 分以上要克制,和不感兴趣相关的不超过 1 分 |
| 校验与退路 | 分数夹到 0–3;链接编号不在列表里就不带链接;文字截长;解析失败重问一次,再失败跳过这封。score ≥ 2 才进精选候选 |
| 预算 | 输出 1300 + 100 token |
| 评测 | 无 |
luoke-mail.digest-pick 精选挑选
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/editor.py:PROMPT、choose、edit |
| 输入 | 同阅读的口味块和标题例子;候选每批 ≤ 30 条,最后一轮最多两倍配额(近 7 天 40 条),再按字符上限减:标题、摘要、来源、日期、另有几家报道 |
| 输出 | picks[{n, why}],最多 k 条(不感兴趣的宁可少选,不凑数),why ≤ 30 字 |
| 校验与退路 | 候选不多于 k 条时不问模型,全部入选;编号去重、去越界、why 截到 60 字;一条可用的都没有时,改按用户点过有用、阅读分数、报道家数、日期排序。选几条由代码定(近 7 天 20 条,更早每月 10 条),多轮淘汰 |
| 预算 | 每条 60 + 60 token |
| 评测 | 无 |
luoke-mail.learn-sent 了解你:发出的邮件
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/learn.py:SENT_PROMPT、parse_facts;生效规则在 learner.py |
| 输入 | 用户邮箱;每封:日期、收件人和抄送(各 ≤ 4 个)、主题、用户自己写的部分(去掉引用和转发,≤ 800 字,取头尾);一批 ≤ 8 封 |
| 输出 | facts[{topic, who, text, n}]:topic 15 类之一,关系类填 who,text ≤ 30 字,n 为依据的邮件编号 |
| 校验与退路 | topic 不在列表、没有有效编号的丢掉;文字遮凭证;两封邮件一致才交给记忆,单值类(职位、单位等)以最新依据为准;解析失败重问一次,再失败这批没有事实 |
| 预算 | 每封 100 + 100 token |
| 评测 | 4 组;硬性:第三方的说法、秘密、一次性安排的词不能出现 |
luoke-mail.learn-orgs 了解你:机构来信
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/learn.py:RECEIVED_PROMPT、parse_facts |
| 输入 | 三封以上、跨两个月以上、至少打开过一封的机构(问过的不再送):名称、地址、封数、首末月份、最近 ≤ 6 个主题;一批 ≤ 10 个 |
| 输出 | 同上,topic 8 类之一,who 为机构名,text 写明机构和用户的关系(≤ 30 字,不用人称)。提示词里的例子用虚构的机构名(青禾银行、远帆证券、集贝网),不用「某某…」这种句式:模型会照着例子的句式写,曾把评测题里叫「某某商城」的广告商写成"在某某商城进行购物" |
| 校验与退路 | 同上;只写机构名的丢掉,缺机构名的补上;结果只作猜测,等用户确认 |
| 预算 | 每个 60 + 100 token |
| 评测 | 2 组;硬性:只发广告的不能当客户,不能记某一笔金额 |
luoke-mail.thread 往来邮件
| 项 | 要点 |
|---|---|
| 位置 | luoke_mail/threads.py:PROMPT、parse |
| 输入 | 一串往来的最后 ≤ 6 封(按字符上限再减):日期、用户或对方(名字)、主题、本人写的正文(≤ 600 字);末尾注明最后一封是谁写的 |
| 输出 | state(user_turn / other_turn / done)、since(YYYY-MM-DD)、what(≤ 25 字);拿不准选 done |
| 校验与退路 | state 映射为等你 / 在等对方 / 已了结,解析不了或不在枚举就原样再问一次(不带纠正),再失败按已了结;since 格式不对置空;done 时清空另两项 |
| 预算 | 60 + 100 token |
| 评测 | 16 串(10 串硬性);硬性:已了结的不能报成等你,等你的不能漏,都一次不能错 |
记忆
luoke-mem.typing 常用说法和话题
| 项 | 要点 |
|---|---|
| 位置 | luoke_mem/learn.py:PROMPT、messages、parse、_term、_interest |
| 输入 | 已记下的话题、还在观察的话题、用户否定过的、用户在输入法里删掉的候选;打的字每段 ≤ 300 字,一批 ≤ 40 段、≤ 6000 字 |
| 输出 | terms[{text, evidence}](2–12 字,照用户的写法);interests[{text, evidence}](≤ 20 字,至少两段依据,最多 3 个) |
| 校验与退路 | 编号越界丢掉,兴趣不足两段丢掉;常用说法由代码在近 90 天的打字记录里数:见到 2 次起作猜测问用户,满 3 天、5 次才生效,词长、常用词表、凭证、否定过的拦掉;兴趣依据跨 3 天才生效,之前是猜测。回答不完整(被截断,或不是含两个列表的 JSON)时这批不算读过:下次每批减半重来,同一批两次不行才跳过 |
| 预算 | 每段 8 + 300 token |
| 评测 | 24 例;硬性:一次性安排、别人的事、删掉的候选、秘密不能出现;底线:说法召回 70、话题召回 60、每例普通词误收 ≤ 0.5;32 GB 档有一条已知问题:一例的话题名里带上一次性安排的"答辩" |
luoke-mem.topics 邮件里的话题
| 项 | 要点 |
|---|---|
| 位置 | luoke_mem/topics.py:PROMPT、sources、parse、take |
| 输入 | 已记下的话题(≤ 30)和否定过的(≤ 20);按来源汇总的邮件助手记录(一年内、用户自己的、非事务邮件),每个来源 ≤ 1200 字:名称、往来次数(来信、打开、精选里读过 / 有用 / 没兴趣)、邮件助手说过的"讲的是"、最近 ≤ 3 封原文开头(各 ≤ 280 字);一批 ≤ 40 个来源,16 档 ≤ 20 个、≤ 3000 字 |
| 输出 | interests ≤ 5 个,写成"领域:具体对象"(≤ 30 字);dislikes ≤ 3 个,只认精选里说的"没兴趣" |
| 校验与退路 | 超过 60 字、没有有效来源编号、含凭证的丢掉;只是品牌或来源名的丢掉:整条只有拉丁字母;没有"领域:对象"、又以"动态 / 通知 / 资讯 / 更新 / 消息"结尾;或冒号后只剩来源名、只剩拉丁字母加"动态"之类。写出领域和具体对象的(哪怕以"动态"结尾)保留;跨轮累计 ≥ 2 条记录才算;每个话题立即作为精选口味,作为"关注的话题"要过记忆的表且最多 8 个;不感兴趣要有两条以上精选里点"没兴趣"的记录(收藏、标签、清理、没打开都不算)。回答不完整时这批来源留到下次(每批减半),同一批两次不行才跳过;提示词换版本时全部重读 |
| 预算 | 每个来源 8 + 300 token |
| 评测 | 3 个虚构的人共 12 例;硬性:话题名和不感兴趣里不能有一次性的事、别人的事、健康、凭证,话题只是来源名或纯拉丁字母算失败;另用 5 个该留、6 个该丢的固定话题名检查品牌筛选,错一个也算硬性失败;另记话题召回、是否具体、不感兴趣的召回,和没有"没兴趣"依据的不感兴趣 |
没有提示词的任务
luoke-ime.rerank:只送前文和候选,主控返回每个候选的对数概率。luoke-ime.predict:只送前文,主控续写若干分支,停在标点。luoke-search.embed(配方里仍叫digup.embed,还没分到任何档):只送加好检索前缀的文字,返回向量。- agent 和系统检查目前不调用生成任务。
文件位置
应用
| 位置 | 内容 |
|---|---|
/Applications/螺壳.app | 主控(也可能在 ~/Applications,以实际位置为准;改名前为 螺壳主控.app,升级时换成新名字)(内含自带 Python、记忆代码、luoke 命令行) |
~/Applications/螺壳邮件助手.app | 邮件助手 |
~/Applications/螺壳 agent(开发).app | agent 的试用版(app/install_trial.sh 装,主控不管它);以后并进螺壳,见决策 0014 的 10-24 修订 |
~/Library/Input Methods/LuokeIME.app | 输入法 |
$TMPDIR/ai.luoke.inputmethod.Luoke.lock | 输入法单实例锁 |
~/.local/bin/luoke | 命令行链接(可选安装) |
主控 ~/Library/Application Support/luoke-core/
开发版(bundle id ai.luoke.core.dev)用 luoke-core-dev/,结构相同。
| 路径 | 内容 |
|---|---|
run/hub.sock | 任务接口 socket |
logs/hub.log、logs/app.log、logs/jobs/ | 日志(只记元数据) |
engines/<版本>/、engine/<版本>/ | 引擎包(发布版只留当前版本);引擎运行目录(含引擎密钥 settings.json(0600)、engine.log、engine.pid.json) |
models/ | 模型;用户登记的模型在 user-<名字> |
downloads/ | 下载暂存(应用的包在 downloads/apps/) |
state.json | 运行状态:常驻、冒烟、探针、空闲任务、端点用量 |
settings.json | 能耗模式、电池、档、端点 |
endpoint-key、user-models.json | 端点密钥(0600);登记的模型 |
local-verified.json | 本机放行 |
verified-history.json | 各配件版本由哪一版发布认可、当时放行哪些任务(每配件 3 个版本),供回到上一版本和主控升级后还没升级的应用使用 |
downloads/update/、run/update-mount.json | 检查更新下载的主控安装包,拷到暂存处后立即删除;run/update-mount.json 只为清理旧版留下的挂载而读 |
previous.noindex/ | 换装前的旧版主控(由换包脚本移入,不论原来在 /Applications 还是 ~/Applications)和配件;应用可在「应用」窗口里回到上一版本,卸载配件时一并删除 |
staging.noindex/ | 升级主控时先放新版、核对签名,再由换包脚本移到装好的位置 |
run/upgrade.json、run/preselect.json | 升级标记(新主控读后删除);安装器交来的预选应用 |
run/swap.sh、run/swap-result.json、logs/swap.log | 换包脚本、上次换包的结果(下次启动取走)、换包日志 |
记忆 ~/.luoke/
| 路径 | 内容 |
|---|---|
mem.db(及 -wal、-shm) | 记忆,0600;可用 LUOKE_HOME 换位置 |
mem.db-ui.json、mem.db-learn.lock | 界面进程状态(0600);归纳任务锁 |
mem.db.before-0.18 | 第一次重新定档前的整份副本,14 天后或有内容被忘掉时删除 |
agent/ | agent 的 state.db、socket、日志 |
ime/ | 输入法的设置、用户引擎、引擎数据、开发记录 |
邮件助手 ~/.luoke-mail/
| 路径 | 内容 |
|---|---|
config.json | 设置 |
luoke.db | 邮件、建议、规则、清理、操作记录、精选 |
token、server.json、server.lock | 页面令牌;运行中实例的端口;单实例锁 |
idle.lock、resident、account-salt | 空闲工作锁;后台整理已开启的标记;邮箱不透明键的随机值 |
run/agent.sock | agent 接口(默认关闭,LUOKE_MAIL_AGENT=1 时才有) |
service.log、app.log | 日志 |
深度搜索 ~/.luoke-search/(未发布)
| 路径 | 内容 |
|---|---|
| 目录本身 | 700,整个排除出 Time Machine |
.lock | 设置和状态文件的跨进程锁,0600 |
settings.json、state.json | 设置;未执行的方案和 outbox 的状态,都是 0600 |
index.noindex/index.sqlite(及 -wal、-shm) | 索引,0600 |
run/agent.sock | MCP socket,0600,目录 700 |
run/stopped-file.json | 看门狗停下时正在读的文件,下次记入索引后删除 |
字段见深度搜索页。
钥匙串
luoke-mail:imap(邮箱授权码)、luoke-mail:engine(直连模型服务的密钥)。
环境变量
| 变量 | 作用 |
|---|---|
LUOKE_HUB_SOCKET | 指定主控 socket(指定后不回退默认位置) |
LUOKE_HOME | 记忆所在目录 |
LUOKE_MAIL_HOME | 邮件助手数据目录 |
LUOKE_MAIL_PORT | 邮件助手网页端口(未用 --port 时) |
LUOKE_MAIL_AGENT | 设为 1 时开启邮件助手的 agent 接口 |
LUOKE_SEARCH_HOME | 深度搜索数据目录(发布版拒绝启动) |
LUOKE_SEARCH_EMBED_TASK、LUOKE_SEARCH_OPENER、LUOKE_SEARCH_PHOTOS | 深度搜索开发用的开关(发布版删掉,LUOKE_SEARCH_DEV_APP 只在构建时用),见深度搜索页 |
PI_CODING_AGENT_DIR | luoke launch pi 写入配置的位置 |
运行与发布
版本
| 类型 | 标签 | 说明 |
|---|---|---|
| 内部版 | internal-MMDD | 每天固定时间把各组件主线的提交钉在顶层仓库并打标签;出版流程在三个硬件档上评测,签名公证后发布为预发布版 |
| 本机版 | local-MMDD-HHMM | 只在一台机器的档上评测,签名公证、不上传,用于开发机上的快速验证 |
| beta / 正式 | beta* | 封闭测试和公开发布 |
beta 和正式版的安装包发布在 luokelabs/luoke-releases,团队用的内部版发布在 luokelabs/luoke-internal-builds(10-28 起分开;过渡的那一版两边都有);模型在 ModelScope 和 Hugging Face 各有一份。
出版流程
- 读取钉住的各组件提交,打包配件 .app。
- 按配方在各档评测每个任务,每一格(任务 × 档)当天只评一遍;已有且输入未变的证据复用。
- 评测看门狗:某档某任务 10 分钟没有进展(生成任务看实验室主控日志里有没有成功的请求,其他任务看评测程序的输出是否增长),即停掉评测程序,这一格记为没过(
engine_refused_memory或stalled),接着评其余各格。 - 按支持台账决定各档开关:上一版开着的,失败一遍即关;关着的(上一版关、新任务、评测表 hold 的)当天过了也照"关"发布;这一版没评的档沿用上一版。生成配方和差异报告;仍开着的组合总分比上次低出容差,或主要任务一档都没开时停止。
- 签名、公证、生成 DMG 和发布清单,上传。目标是当天 24:00(JST)前发出,不等复测。
- 发布后复测:关着、当天过了的格子再补跑到 3 遍(hold 的不复测,失败一遍即停),结果只写回这一版的证据和台账,不改已发布版本的开关。下一版中,上一版补满 3 遍都过、这一版也过(或没评)的格子开启。
每一步记录在 steps.json 中,失败后可以从中断处继续;复测中途停下,下次接着补。
换装与回滚
- 用户:主控菜单「检查更新」下载并核对新版,由换包脚本换上、新版主控接着补齐引擎和模型(同一张卡片;标准账户把螺壳装在
/Applications时要一次管理员授权;应用在「应用」窗口里逐个升级),或打开新版的安装器(应用的安装器,或在 DMG 里直接双击「螺壳」);数据和模型保留;旧版主控保存在previous.noindex。单个应用可在「应用」窗口那一行「…」里回到上一版本,主控按那一版当时的验证接受它。 - 开发:本机版用同样的安装器升级;配件的新版本可经"本机放行"在内部版或本机版主控上先行使用。
测试机器
| 机器 | 配置 | 用途 |
|---|---|---|
| 开发机 | Apple M5 Max · 128 GB | 128 档评测、日常试用 |
| 16 GB 测试机 | Mac mini M1 · 16 GB | 16 档评测、干净机首次运行 |
| 32 GB 测试机 | 32 GB | 32 档评测、实验 |
诊断
- 主控设置中可导出诊断:机型、芯片、内存、首次运行各步时刻、运行状态和日志尾部,只含元数据。
- 主控日志
logs/hub.log;空闲任务的标准错误在logs/jobs/,最后 4 KB 也记在state.json。 - 输入法的沙盒拦截在输入法菜单中列出。
决策
影响多个组件的设计决策以 ADR(Architecture Decision Record)记录,原文保存在顶层仓库 docs/adr/。0001–0012 已归档,下列为当前有效的决策。
| 编号 | 决策 | 要点 |
|---|---|---|
| 0013 | 本机就是边界 | 数据不离开用户自己的设备(设备之间端到端加密);本机组件之间不为隐私设限;凭证不进记忆,走钥匙串;高级用户端点的密钥可自定义或留空。修订(2026-10-21):写进用户自己选定并在用的账户(他的邮箱、日历等)不算外传,内容仍不能发到螺壳自己的服务器和用户没选过的第三方;深度搜索的本机索引可以原样存文件里认出的完整证件号、卡号,也原样送主控的嵌入模型,交给 agent 或画成卡片时再遮(只遮那一串、保留后四位),名字像密码、密钥的文件照旧不读,记忆仍不存凭证的值 |
| 0014 | 分工 | 模型在主控,理解在记忆,事情在工具,沟通在 agent;工具之间不互相调用,只有 agent 调用工具的能力;高级用户端点经主控调度。修订(2026-10-21):日历由主控提供(EventKit 薄接口:按时间段读、写一个事件并返回编号,日历权限挂在主控名下,配方登记哪些配件能用,第三方配件要先得到用户同意,beta1 之后做);浏览器桥(native messaging host)也放在主控里,插件经它把页面信息交给主控的任务、调用日历接口、写记忆记录,网页内容只接受固定格式。修订(2026-10-24):用户看到的应用「螺壳」就是主控(螺壳.app,bundle id 仍是 ai.luoke.core);agent 并进它,对话、卡片、提示从螺壳的窗口和通知出去,agent 作为后台组件保留,分工不变 |
| 0015 | 记忆如何形成理解 | 工具只记录、记忆读原文下结论;三档把握程度;合理默认、按反馈调整;看依据的时间;空闲时反刍产出提问和建议;不按上下文隔离 |
| 0016 | 替用户做事 | 能撤回的在确认习惯后自动执行并提示,事后每日复查;不可逆或对外的始终由用户点按;agent 自己的邮箱可按类型放开外发。修订(2026-10-20):系统检查查出的"会重建的缓存"(npm、uv、pip、Xcode DerivedData、Homebrew 旧版本等)用户确认一次后可对该类自动清理,每次提示,对应应用在用时跳过;其余类别每次由用户点按,删除优先移到废纸篓;只往本机下载的签名规则包、特征库不算数据外传 |
| 0017 | v1 首发 | 首发邮件助手和输入法,主控和记忆随装;Apple 芯片、16 GB 起、macOS 26.2+;先封闭测试再公开。修订(2026-10-20):所有面向用户的界面按同一套设计规范,由品牌负责人在 luoke-brand 牵头制定,各项目照着改;beta1 只修明显的不一致,整体改版在 beta1 之后;顶层仓库公开,各组件仓库仍私有。修订(2026-10-22):主控的所有窗口和邮件助手的网页在 beta1 前按设计规范整体改,记忆界面、输入法设置、agent 卡片仍在 beta1 之后;封闭测试顺延到 11-02,只做一批,招满 30 位为止,开测时对应的项目在 GitHub 上公开。修订(2026-10-23):记忆页和输入法设置也在 beta1 前改(agent 卡片仍在之后);主控里加「应用」窗口,各应用单独安装、升级、换回、卸载,升级主控不再顺带替换应用;「螺壳主控」改名「螺壳」,bundle id、数据目录和命令名不变;版本截止 10-28 20:00,beta1 10-29。修订(2026-10-28):内部版换到单独的公开仓库 luokelabs/luoke-internal-builds,luoke-releases 只放 beta 和正式版,螺壳按渠道查对应仓库,过渡的那一版两边都发;检查更新往后翻页,随 beta1.1。螺壳、邮件助手、记忆、输入法四个组件公开:原名是公开仓库,从新的初始提交开始,团队开发和 issue 在 <名>-private,往公开仓库同步每次先问主人。修订(2026-10-29):beta 和正式版的发布包在 GitLab 上放一份镜像,GitHub 仍是主渠道;私有仓库留一个和公开仓库 main 完全一样的 public 分支,同步时把导出的树作为一个新提交加在上面,扫过泄漏再推成公开 main,不改写历史。修订(2026-10-31):公开仓库只在发 beta 或正式版时同步,四个组件一起,是发版流程里的一步;从下一个公开版起,beta 和正式版只钉 public 上的提交,内部版照旧从私有 main 出。公开仓库发现泄漏时立刻修。修订(2026-11-01):要发给大家用的组件默认开源,体检、agent、深度搜索第一次随 beta 或正式版发布时建公开仓库;agent 和体检一起上,和螺壳窗口进同一个 v1.x,用户看得到清了什么、点得到「放回」之前,体检不开放 |
| 0018 | 推理和权重归主控 | 体系内没有脱离主控的模型运行实例;垂类模型也做成主控的任务,对延迟敏感的走优先通道。修订(2026-10-20):嵌入任务由深度搜索(luoke-search)接手,任务名由 digup.embed 改为 luoke-search.embed,接口、评测和多模态安排不变 |
| 0019 | 提示词放成文件 | 每个用到模型的配件在仓库根目录 prompts/ 里按任务放 system、user、retry 的 .txt 和 schema.json,代码只读文件、填占位符;枚举只写在 schema 里;本站「任务与提示词」页改为从各仓库生成;出版证据记 prompt_sha256;没有评测集的任务补齐。beta1 之后推进 |
| 0020 | 引擎换成自己做的薄引擎 | 只服务配方模型,底层用官方 mlx-lm,不换 mlx-serve。先进内部版,beta 线和 11-02 的测试不换;推理和权重仍归主控(ADR 0018)。现在的引擎还是 oMLX。 |
设计原则
- 合理默认,按反馈逼近:不要求用户事先设定规则或逐条审阅;用户的确认、否定和撤销就是调整的依据。
- 测试版不过度保守:能撤回的能力默认开启,配上提示和撤销,通过真实使用检验。
- 本地优势不打折:在本机内部充分使用数据,体验才能弥补本地模型和算力的差距。
- 产出要像用心的助理:理解要说清领域和具体对象,不照搬标题或品牌名。
已知问题
跨组件的未决事项和已发现但未修复的问题。各组件自身的限制见对应章节的"已知限制"。
功能缺口
- 记忆没有向工具索取更多原文的通道;话题归纳只读记录中已有的原文。
- 输入法的个人化组件不读记忆中的理解,打分没有个人化。
- agent 没有对话能力,不使用模型;不在 v1 首发中。
- 配方不支持撤回已发布版本。
- Gmail 转发过滤器的一键生成未完成;规则导出的过滤器未在真实 Gmail 上验证导入。
质量
- 16 GB 档(9B)的邮件分拣会把正常邮件里的催促语(例如正规商城写"请在某日前上传凭证,逾期退款申请将自动关闭")判成可疑;32 GB 档判对。还没有专门盯这种情况的评测题。
- 16 GB 档的打字归纳:beta1.1 没开(评测时过时不过,候选版 beta1.1-rc2 开过);内部版 internal-1031-2 起开了。
- 32 GB 档的打字归纳偶尔把一次性安排(例如"答辩")放进话题名,两个候选分数打平;配方里记为已知问题。
缺陷
- 螺壳:先升螺壳、后升应用时,旧版邮件助手写不了草稿和每日摘要:这三个任务没有评测记录,进不了本机的验证历史。beta1 有这个问题;internal-1026-5 起已修,已经写下的旧记录不重写,升一下邮件助手就好。
- 螺壳:换引擎后旧引擎留在磁盘上(每份约 1.6 GB)。beta1 有这个问题;beta1.1 起新引擎检查通过后删掉别的版本。
- 邮件助手:往返和多段行程只加第一段:分拣只输出一个日程,往返机票、多段行程、系列活动只能挑一个或揉成一个,结束时间可能取成最后一段的(luoke-mail#34)。
- 记忆:同一机构换了名字仍各成一条:按「机构:」后的名字原样合并,模型给的名称不同(如英文名、带域名、中文名)就不合并;别人来信推出的事实仍是一机构一句。
- 记忆:「其他:」话题下的人和机构可能并成一条:旧版邮件助手把机构来信放在「其他:机构名」下,所以「其他:」仍按机构合并;发出邮件里写的「其他:某人」若和机构同名会被并进去。
- 记忆:改建议并当场处理被算成两次时形成的处理习惯不重算,仍按原依据保留。
待讨论
- 社区和第三方配件的接入流程、审核与安全边界。
- agent 何时使用模型、第三方工具规则、手机端。
术语表
| 术语 | 含义 |
|---|---|
| 主控(luoke-core) | 本机推理入口和安装器 |
| 配件 | 由主控安装和管理的应用:邮件助手、输入法,以及未发布的深度搜索、体检 |
| 深度搜索(luoke-search) | 本机文件和照片图库的索引与检索工具,没有自己的窗口,经 agent 的 MCP 工具和命令行使用 |
| 任务 | 配件向主控请求推理的单位,形如 luoke-mail.triage |
| 底座 | 每档用于生成任务的主模型 |
| 专用模型 | 只服务某类任务的小模型,如打分、嵌入 |
| 硬件档 | 按内存划分的 16 / 32 / 128 三档 |
| 配方 | 描述各档模型、任务参数和允许接入版本的 recipe.json |
| 支持台账 | 出版时记录每个任务在每档开或关的依据,发布后的复测结果也写在这里 |
| 冒烟检查 | 引擎启动后的实测,得出本机速度 |
| 空闲任务 | 配件登记、由主控在安静时刻启动的命令 |
| 安静时刻 | 距最近一次用户在等的请求满 5 分钟 |
| 用户在等 | 用户正在等待结果的请求等级 |
| 记录 | 写入记忆的"发生了什么" |
| 理解 | 记忆从记录形成的关于用户的认识 |
| 把握程度 | 理解的三档:猜测、比较确定、确定 |
| 以前的事 | 会变、且长期没有新依据的理解 |
| 反刍 | 记忆在空闲时整理理解、产出提问和建议的过程 |
| 处理习惯 | "某发件人的某类邮件通常这样处理"的理解 |
| 今日处理 | 邮件助手中复查当天全部处理结果的页面 |
| 螺壳自己的邮箱 | 用户为 agent 开设、由邮件助手全自动处理的邮箱 |
| 能力 | 工具声明给 agent 的可报事项和可执行动作 |
| 个人化组件 | 输入法中负责写记忆、转发打分和联想的组件 |
| 引擎(输入法) | 沙盒中给出候选的子进程,如小鹤双拼 |
| 本机放行 | 在内部版或本机版主控上允许尚未进入配方的配件新版本接入 |
| 高级用户端点 | 主控提供给外部工具的 OpenAI 兼容接口 |
文档修订
2026-11-02
- 体检页按代码 3a6c9cb 复核:第一轮先下病毒库;快速扫描不再漏刚写完的文件;风险高的仅供参考进一轮那一件和
summary;不用点就清在两项之间停、边清边记;缓存没写谁在用的不自动清,受保护文件夹里的缓存没访问时不看;模型和设备支持文件执行前也查改动;needs-access改 low;一轮卡片列出标题数到的每一条。已知限制去掉五条;页头写明和 agent、螺壳窗口进同一个 v1.x。
2026-11-01
- 决策页记 ADR 0017 的 11-01 修订:组件默认开源,agent 和体检一起上。agent 页头写明发布安排。
- agent 页跟到 536f134(最初按 49f1083):
/status加window_front,通知授权未答时窗口里说明;窗口在最前面且用户在场才算看过;系统通知到不了时不算说过;少说按次计数、卡片重启后画回来;撤回的编号再列出来就恢复;/window加front;打开东西的动作(§5)。已知限制里这些修复标为下一版修。 - agent 页按代码 f04a2e0 复核:自带
luoke_mem0.23.4;主控换 Python 时核心以 75 退出、壳立刻重起;启动命令和打包形态(现在主控不装它,试用版不连主控);界面协议补POST /notifications、/status字段、事件内容、壳的--notify;outbox_ack已发送,去掉过时的已知限制;drop不是工具字段;A2UI v0.9.1;"以后再说"的准确含义和自由问题的按钮;急事合并、少说按件计、清理只在窗口关着时;新增一节写协议里有、agent 还没做的(§1a、§1b、§5、§8a);已知限制加读代码核到的缺陷。 - 跨页:总览加体检并把 agent 调邮件助手画成虚线(接口默认关闭);概念、词汇表不再把 agent 算作主控管的配件;文件页改成试用版的实际路径;体检页、深度搜索页写明基础页、对话、螺壳窗口都还没做。
- 深度搜索页按代码 2f06213 复核:outbox 三类(加
needs-setup,needs-access改 low);工具 8 个(加search_request_access,search_open要点按);资源、照片权限点「继续」才问、发布版判定和搜索范围、遮凭证七类、schema 5、上限表、同主题和规模成绩;主控不认识深度搜索的原因改成配方verified没收录(403);补向量重做媒体在看门狗下。 - 文件页加深度搜索的数据目录和环境变量;任务页、总览、概念、词汇表把深度搜索的状态改成未发布。已知问题:16 GB 档打字归纳按 internal-1031-2 的配方更新。
2026-10-31
- 体检页按代码 b17d7ed 复核:三个程序(加
luoke-audit-admin);规则 23 条,加启动钩子、键盘监听、浏览器、MCP 配置、SSH 和共享等检查,缓存 20 项;四类不用点就清(默认开)的确切判定;要密码的删除、放回、没有完全磁盘访问时的行为;11 个 MCP 工具和新增的资源;一轮一件的 outbox;写记忆的五种记录;清理规则评测;已知限制按现状重写。 - 决策页补 ADR 0017 的 10-29、10-31 修订。已知问题:32 GB 档银行通知一条去掉(核过 beta1.1 配方),旧引擎一条改成 beta1.1 起已修。
- 记忆页按代码复核到 0.27.0:加「首要原则」;门槛按每类提议、用户的各种反应现算,分量和门槛动了什么写清;常用说法不再问用户,拉丁字母按整词数。参考页加
reject_family、reject-family、fit.deleted。 - 已知问题按 beta1.1 和 internal-1031 安装包里的配方改:话题归纳、机构归纳各档都已开,两条去掉;16 GB 档的打字归纳没开,改正原来写的「已打开」。
- 决策页加 0020(引擎换成自己做的薄引擎)。
2026-10-30
- 输入法页按代码复核到 0.1.32:加自然码双拼(和小鹤共用程序和词库,只换韵母键位,评测成绩相同);添加引导用系统设置的原词并加图文教程,螺壳完成页同用;清单介绍语;自带 librime 不再带编译机路径。「安装与输入源」改名「安装与添加」。
2026-10-29
- 提示词页按邮件助手 0.18.19 复核:分拣在用户只在抄送里时多一句提示,模型给 high 也降成 medium;发件域名验证结论 16、32 档都不给;机构归纳的例子换成虚构机构名,说明为什么不用「某某…」。已知问题页加 128 GB 档机构归纳暂时关着。
- 按 luoke-core main 复核检查更新:内部版查
luoke-internal-builds、beta 和正式版查luoke-releases,每页 100 条往后翻(最多 10 页);运维页、发布清单页同步写明两个发布仓库。决定页补 ADR 0017 的 10-28 修订(内部版换仓库、四个组件开源)。已知问题页去掉指向私有 issue 的链接,32 GB 档银行通知一条写明 beta1.1 起撤掉。
2026-10-28
- 邮件助手页按代码复核到 0.18.14(beta1.1 用的 0.18.11.3 功能相同):页面令牌存进 localStorage、同步分批入库和断点续读、长链接写法、分拣的寄出日期 / 只在抄送 /
sender_verdict、五类通知和清理分析完、收件箱卡片时间和已过日程、处理模式按钮、加入日历、撤销的查找顺序、日志轮转。输入法页复核到 0.1.23:换版等设置窗口和添加引导关着、--status的全部字段和退出码、--disable/--current、清单的listing、菜单里联想的勾选。 - 螺壳页、配件清单页、文件页按 luoke-core eb1ad95 复核:引擎密钥写进 0600 的 settings.json、不联网找模型;计费 / 低数据网络不自动补下;只装应用不动引擎、换引擎后删旧版本;首次设置的进度估算、试打框重新聚焦、邮件助手完成行;验证历史记"本版放行的任务";Finder 版本是发布号;"上一版"只认发布出去的、beta 全部重评;发布页只放 Luoke.dmg;配件清单的
main、min_macos、capabilities里的service/permissions/listing、配方的background_input_chars和expectations、发布清单字段。
2026-10-27
- 新增「螺壳体检」组件页(luoke-audit,按代码 6fa5b14 核对):两个程序、19 条规则、病毒扫描和增量、清理与撤销、给 agent 的 10 个 MCP 工具和 outbox、两个生成任务、数据和打包、评测、已知限制。
- 提示词页按代码复核(luoke-mail 0.18.11、luoke-mem 0.26.0、luoke-core 570eef2):共同做法补采样参数、重问方式和截断后的处理;分拣补寄出日期、正文的预处理(长链接写成
https://域名/…、6000 字)、日期回拉、判断规则,评测改为 48 封;起草回复的输入、署名规则和重问;精选阅读、挑选的口味块、链接表、候选数;learn-orgs 的入选条件;往来邮件的重问;打字和话题的失败处理、话题名上限 60 字、品牌筛选和"没兴趣"的依据、话题评测的硬性条目;luoke-search.embed在配方里仍叫digup.embed。 - 已知问题:32 GB 档银行审批通知误报、打字归纳"答辩"两条记为配方里的已知问题;16 GB 档打字归纳 internal-1027 起打开;补两条缺陷(先升螺壳后升应用时草稿和摘要被拒、换引擎后旧引擎不删),都已在 main 上修;去掉 128 档话题分数偏低。
2026-10-26
- 已知问题:邮件助手往返和多段行程只加第一段(luoke-mail#34)。
- 已知问题(质量):16 GB 档分拣把正常邮件的催促语判成可疑。
- 输入法按代码复核(luoke-ime 0.1.19,307aa9e):换版时空闲自己让位并重新拉起;找螺壳的 Python 先
/Applications后~/Applications、两个名字都认;第一页按「-」打连字符、数字后的逗号、{ } 打「」、Shift+⌫ 忘词;联想按应用算、终端默认不联想、激活后高亮项放在光标处;设置窗口四页和精简后的菜单;新增「安装与输入源」一节;ime-commit 断段和保留期的细节;已知限制补四条。
2026-10-25
- 主控按代码复核(luoke-core 0590c6e):新装放
/Applications,安装盘改成拖进「应用程序」的窗口,在安装盘里双击给「帮我装进去」;已装的以实际位置为准、两处两名都认;升级改为先暂存核签名、再由独立的换包脚本换上并用open打开,管理员升级时从~/Applications搬进/Applications,标准账户写不进去时弹管理员授权框;内部版每 3 小时自动检查;主控和安装器拉起别的应用一律经/usr/bin/open。文件位置页补换包脚本、结果和日志,agent 一行改为「螺壳 agent」;运维页跟着改。 - 已知问题:去掉升级后程序坞留死图标(luoke-core 6bd94be 修好,10-25 升级实测无死图标)。主控 c30aad3:两处各有一份时,打开旧那份会交给装好的那份,装好的那份启动时收走另几份;目标位置被别的应用占着时改放另一处。
- 照顶层 CONTEXT.md「对外怎么叫」统一名称:主控页标题改为「螺壳(主控)」,agent 页去掉「螺壳」这个名字、写明是螺壳的后台组件和以后的安装方式;概览、邮件助手页跟着改。
- 主控 5fef093:空闲任务的请求也照能耗模式歇息,主控挂着等,不再豁免(静音模式下风扇不再猛转)。
2026-10-24
- 主控按代码复核(luoke-core 7d97ce3):应用名改为
螺壳.app(开发版「螺壳(开发)」),旧名字升级时换掉;新增「安装位置」「应用」两条:每个应用的安装、升级、回到上一版本、卸载都在「应用」窗口那一行里做,升级主控不再顺带升级应用;升级改为同一张卡片走完五步,主控自己的回到上一版去掉,换包或补齐没成时给「先用着」和「重试」;检查更新只升级主控、可「去升级应用」;首次运行、加装、升级不再发通知;菜单只剩状态、记忆、应用、设置、检查更新、退出,图标五种状态不动;主控小窗口给四个入口;设置分通用、下载、模型、高级、关于。文件位置和运维页跟着改。 - 决策页补 0014 的 10-24 修订(主控就是用户看到的「螺壳」,agent 并进它)、0017 的 10-23 修订,新增 0019(提示词放成文件)。
- 已知问题新增主控三条:升级后程序坞的死图标、升级卡片运行中常驻应用不在、首次运行没走完时「应用」里的安装不回到向导。
- 主控 db4ff62:升级卡片一开始就拉起常驻应用,「应用」行上的安装也先回首次运行,升级后重新登记登录项;已知问题去掉对应两条。
2026-10-23
- 主控按代码复核(luoke-core e49cf49):能耗模式改为全局并管本机端点,名称为全速 / 均衡 / 静音;新增"菜单和窗口"一节(左对齐的「名称:值」菜单、窗口期间出现在程序坞、主控小窗口、设置、只给本体用的接口核对对端进程);升级写成两步(安装器换主控、新主控升级页确认其余);检查更新窗口的步骤、状态和自动检查时机;首次运行的 16 GB 预检拦截、下载结局、输入法一键切换和重新登记;恢复旧版本应用、卸载、LaunchServices 登记清理、通知;引擎在流里报错的处理;任务开关的复测和"没评到沿用上一版"。已知限制去掉"只核对自报身份"一条。
- 主控任务接口:
/tasks补model_name。任务与提示词:精选挑选改为"最多 k 条",退路改为一条可用的都没有时才按阅读分数排序(luoke-mail 0.17.11)。 - 主控 d185b1e 修好当天发现的缺陷:下载和试跑有结果就发通知;自动检查更新查成了才记时间,核对不过的安装包删除;均衡、静音下端点一次只放一条;升级页没走完就关,常驻应用照常拉起、下次再提示。已知问题中对应四条去掉。
- 主控开发版身份(
ai.luoke.core.dev「螺壳主控(开发)」、数据在luoke-core-dev);升级时新版先放进staging.noindex核签名再一步换上,「App 管理」挡住时照实说明(luoke-core 3c44c63)。 - 记忆、邮件助手按代码复核已知缺陷(luoke-mem 0.25.3、luoke-mail 0.17.16):同一机构的事实合并、确认过的保留原话并问"现在还是这样吗"、反刍时整理已生效的推断、猜测也会变成以前的事、想问的加上打字和机构事实、改建议后当场处理只算一次;邮件助手机构名 40 字、话题「机构:名称」、
after_override、按习惯归档跳过高优先级和安全通知。已知问题去掉机构名截断和整理范围两条,另三条改写为余下的缺口,新增两条(带冒号的人际话题被当成机构、否定一条后新说法不再提出)。 - 记忆 0.25.4:只有机构类话题头才按名字合并(同事、客户、工作等不并,此前误并的打开时拆回);被否定的那条只挡同样的说法;精选口味生成时跳过"动态""通知"等结尾的名字。已知问题去掉对应三条,新增「其他:」话题下人和机构可能并成一条。
2026-10-22
- 新增"提示词"页:逐个生成任务写明输入、输出约定、校验与退路、预算和评测;任务表按最近一次内部版的台账更正档位(往来邮件三档开、话题归纳 16 档开、打字归纳 16 档仍关),注明起草回复、订阅阅读、精选挑选没有评测,往来邮件的硬性条目改为都一次不能错,补主控定输出上限的算法;决策页补 0013、0014 的 2026-10-21 修订(写进用户自己的账户不算外传、深度搜索索引可存完整号码并在输出时遮,日历和浏览器桥由主控提供)和 0017 的 2026-10-22 修订(主控窗口和邮件网页 beta1 前统一改版,封闭测试 11-02 起一批)。
- 在主人的真实数据上核查了一遍记忆的产出(只看计数和理解那一句,记录在团队内部);已知问题新增五条缺陷。
2026-10-21
- 新增深度搜索章节(按登记的设计,尚无代码),总览、核心概念、术语表、任务表补上深度搜索;出版流程改为每格当天评一遍、10 分钟无进展的看门狗、复测挪到发布后并在下一版生效;主控首次运行补五步步骤条和完成页的通知权限,空闲任务补
exec;决策页补 0016–0018 的 2026-10-20 修订(luoke-core cd6a6c7)。 - 深度搜索章节按代码重写(luoke-search 0.1.0,2a26c1b):运行形态(常驻 .app、FSEvents 更新、能停能续的第一遍和常驻兜底、单个文件的看门狗、长文件先读开头、跳过像数据集的文件夹)、各类文件的读法、字词与意思的混合打分(默认做法 A,其他做法只在
eval --scoring里)、拒答门槛未开启(confident无结果为 false、有结果为 null)、MCP 工具和命令行、数据文件、对主控的依赖(客户端用luoke-search.embed,主控配方仍为digup.embed;清单未登记jobs)、各题库与拒答的评测结果。邮件助手更新到 0.16.3:范围以前的未读邮件作为空闲任务补分、标已读并在今日处理单列,清理不再列出;设置里的通知权限提示、"打开通知设置"和"发送测试通知";service.log和run/notifications.json;清理页丢弃迟到的答复;刷新后停在上次查看的邮箱。 - 邮件助手 0.16.4:旧未读邮件照习惯处理在下一次同步时做;日志只按 2 MB 另存,不再清空。
2026-10-20
- 按项目文档的方式重写:分为概览、组件、参考、运维四部分;组件章节按职责、运行形态、接口、数据、故障、限制组织;新增核心概念、数据流、主控任务接口、配件清单与配方、记忆接口与表结构、任务与提示词、文件位置、术语表。
- 各组件内容按代码逐项核对(luoke-core 756d33a、luoke-mem 0.23.1、luoke-mail 0.15.0、luoke-ime 0.1.6、luoke-agent 0.0.1)。
- 正文不再记录讨论过程;决策见"决策"页。
- 主控补检查更新、配件回滚、专用模型进程的空闲停止与内存检查、引擎内存拒绝后的重试、端点独占上限;输入法补单实例、退出通知、收尾键等待、联想停顿、引擎评测选项(luoke-core fad536a、luoke-ime 0.1.9)。
- 已知问题:去掉记忆的三条缺陷(luoke-mem 0.23.2 已修)和输入法的两条(隐私说明已改;最低系统版本不同是有意的,见输入法章节)。
- 主控本体专用路由改为按对端进程核对,
forbidden为 403(luoke-core f5850c1);已知问题中主控的两条去掉。 - 邮件助手 0.15.2:自动标已读进入操作记录、可整批撤销;agent 应用方案只在批量移到垃圾箱或垃圾邮件时要处理模式;已知问题中邮件助手的两条去掉。
- 记忆、邮件助手、agent 按代码逐项复核(luoke-mem 0.23.3、luoke-mail 0.15.4、luoke-agent 7b158a9):记忆补归纳顺序(反刍在前后各一次)、话题线断点续读、开跑前查任务、以前的事再次出现时的规则、邮件推出的兴趣上限、各线的定档和进度;邮件助手补后台整理默认关闭与常驻声明、端口顺延、通知、agent 接口默认关闭、往来邮件"轮到谁"、设置的模型名、数据目录文件,更正订阅阅读和日志说明;agent 补自带 luoke_mem 版本、窗口打开即读、简报计时、"以后再说"由记忆 7 天后重提;任务表更正话题归纳档位。已知问题新增三条缺陷。
- agent 自由问题不再提供"以后再说","以后再说"一律由记忆决定何时再问;已知问题中对应一条去掉(luoke-agent 47b817e)。
- 记忆 0.23.4:一次模型错误不再打断整次归纳,连续出错两次跳过;邮件助手 0.15.5 改用记忆的动作名,旧记录照意思映射;自由问题收"以后再说";已知缺陷清空。
2026-10-19
- 第一版。更正:顶层 ADR 重新编号时误改了 luoke-core 自己的 ADR 编号,已改回。