注意:项目真实存在,本文完全 AI。上集:强制使用德语键盘输入中文(更新)

项目地址:https://github.com/fluviusmagnus/de-tastatur

在 macOS 上使用德语键盘,常见的路无非两条:用 Karabiner-Elements 之类的工具改键,或者将就系统自带的德语布局。

前者能解决问题,但代价不小:后台服务、驱动、辅助功能权限,以及一套需要持续维护的规则。后者看起来“系统自带”,实际常常缺东西——E1/E2 的 AltGr 层、完整的死键组合、扩展选择,以及德语布局应有的细节,很难同时齐全。

de-tastatur 选择了第三条路:直接生成 macOS 原生 .bundle 输入源。它不依赖 Karabiner,不需要后台服务,不申请辅助功能权限,也没有第三方运行时。系统把它当成一个普通的德语键盘布局,和其他输入源一样出现在菜单栏里。

项目当前版本是 1.2.0,已在 macOS 27.2、Apple Silicon、ANSI 实体键盘上验证。本文记录它做了什么、怎么做的,以及过程中那些只有真正交给 macOS 加载后才会暴露的问题。

目标:不是“能打出 ä”就够了

这个项目的目标不是做一个“德语字母补丁”,而是尽量完整地复刻 DIN 2137 的 E1/E2 行为。具体包括:

  • QWERTZ:美式键帽的 Y 位置输入 z,Z 位置输入 y。
  • 左右 Option 都是 AltGr:不需要 Control+Option 模拟。
  • 完整死键状态机:重音、多重重音、音标、希腊字母、数学符号、分数、上下标等。
  • Option+F 扩展选择:按下 Option+F,松开,再按目标键;Shift 选择对应大写或第二字符。
  • E2 ANSI 为主布局,E1 ISO 作为备用:两者使用不同的参考表和输入源 ID。

项目交付两个原生布局包:

布局名称适用硬件
German E2German E2 (ANSI)常见 ANSI 键盘
German E1German E1 (ISO)带左 Shift 额外键的 ISO 键盘

两个 bundle 都在 Info.plist 中通过 KLInfo_* 声明 TISIntendedLanguage=de。这一步很关键:只有声明了主语言,系统才会把布局归入“德语”分类,而不是丢进“其他”。

为什么选择原生输入源

原生 .keylayout 的问题很明显:格式老、文档少、调试工具不友好,出错时系统往往只给你一个“布局不生效”。

但它的优点也同样明显:

  • 没有常驻进程,没有事件拦截,不依赖辅助功能权限。
  • 系统级生效,应用看到的是正常的键盘布局,而不是被改写的按键事件。
  • 可以和输入法、Caps Lock、Command 快捷键等系统行为正确协作。
  • 安装方式就是一个 bundle 复制到 ~/Library/Keyboard Layouts/,卸载就是删掉它。

对于“我只是想要一个德语键盘布局”这种需求,原生方案更干净。代价是,你得自己把布局文件生成对,并且用系统真正加载后的结果来验证。

技术路线:从 Windows 表到 Apple keylayout

项目的生成器是纯 Python,不依赖第三方库。输入是固定版本的参考快照:

  • Linux xkeyboard-config 2.48 的 de(e1) / de(e2),用于物理键位参考。
  • Windows KBDGRE1.DLL / KBDGRE2.DLL 的公开布局表,用于字符、死键和扩展选择状态机。

参考文件全部带有 SHA-256 校验和。生成前先核对,避免“参考表悄悄变了,输出也跟着变”。

生成流程大致是:

  1. 解析 Windows 表:读取 VK_TO_WCHAR_TABLE 和 PhysicalKeys,把 PC 扫描码映射到 macOS 虚拟键码。
  2. 建立 10 套 keymap:普通、Shift、Caps、Shift+Caps、Option、Shift+Option,以及 Command/Control 的快捷键变体。
  3. 移植死键状态机:Windows 的 DEADKEY 表记录的是“当前重音状态 + 输入字符 → 结果字符/下一状态”。生成器把它翻译成 keylayout 的 action、state 和 when。
  4. 处理终止符:未完成的死键在遇到普通字符、Escape 或退格时如何收尾,需要和参考实现一致。
  5. 渲染 XML:输出 .keylayout,再打包成 .bundle,写入 Info.plist 和本地化字符串。

这里最容易出错的地方,是把死键当成“一个特殊字符”。它其实是一个状态:按下重音键后,键盘进入某个待定状态;下一个按键可能完成组合、进入另一个死键状态,或者因为不匹配而输出“可见前缀 + 当前字符”。

生成器还刻意避免把 Windows 的内部中间状态码直接输出成可见字符。不匹配的组合应该让用户看到重音前缀,而不是一个莫名其妙的符号。

扩展选择也是同一套机制:Option+F 本身是一个死键状态,后续按键从参考表里查结果。Shift 在这一步选择大写或第二字符,而不是要求用户一直按住 Option。

bundle 里的关键声明

生成出来的 Info.plist 大致是这样:

<key>KLInfo_GermanE2ANSI</key>
<dict>
    <key>TISInputSourceID</key>
    <string>local.keyboardlayout.GermanE2.keylayout.GermanE2ANSI</string>
    <key>TISIntendedLanguage</key>
    <string>de</string>
</dict>

TISInputSourceID 必须稳定,系统靠它识别和持久化输入源。TISIntendedLanguage 决定分类。显示名称则通过 Resources/de.lproj/InfoPlist.strings 和 en.lproj 本地化。

安装和注册是两件事。生成器本身不安装、不注册、不切换输入源;verify_macos.swift --register 也只注册,不启用或选择。这个边界很重要——自动化工具不应该在用户不知情时改变当前输入法。

那些只有原生加载才会暴露的坑

1. Apple 的编译器会“吃掉”XML 命名实体

离线测试全部通过后,第一次跑 macOS 原生验证,出现了 12 个失败——两个键盘类型各 6 个。

原因不是布局逻辑,而是 Apple 的 keylayout 编译器在处理 action 输出时,丢弃了 &amp;、&lt;、&gt; 这类命名实体。普通 XML 解析器完全接受这些写法,所以离线测试发现不了。

修复方式是改用数值引用:&#x0026;、&#x003C;、&#x003E;。同时增加离线回归测试,防止以后又改回命名实体。

这件事的教训很直接:能通过 XML 解析,不等于能通过 macOS 加载。 原生验证不是锦上添花,而是这个项目最核心的测试手段之一。

2. 输入源持久化有两个域

排查旧德语布局为什么停不掉时,发现了一个容易误判的事实:

  • 第三方输入源保存在 com.apple.inputsources/AppleEnabledThirdPartyInputSources。
  • 系统内置输入源保存在 com.apple.HIToolbox/AppleEnabledInputSources。

只看 HIToolbox,会误以为 E2 没有被保存;实际上它在第三方域里待得好好的。系统还会保留最后一个内置 ASCII 布局的外部行为,导致直接调用 TISDisableInputSource 返回成功,但重新枚举后它仍然启用。

项目为此写了计划、检查、应用、恢复四个步骤的工具:先备份,再生成计划,--check 只读检查,--apply 只写 AppleEnabledInputSources 这一个键,--restore 则要求当前状态仍与计划应用后一致才允许恢复,避免覆盖用户后来的修改。

即便如此,文档也没有把“写入成功”说成“问题已解决”。注销重登录后的持久效果仍需要用户验收;如果系统又恢复了旧项,那属于平台限制或缺陷,不能靠反复覆盖偏好来假装修复。

3. E1 迁移期间的一次输出缓冲区错误

迁移过程中,E1 的 UCKeyTranslate 曾出现一次输出缓冲区错误。重新注册后复测通过。验证工具后来增加了独立的键盘数据快照,但没有证明缓存错误的根因。

这类问题的处理原则是:记录现象、保留复现条件、不要把一个环境的成功当成所有环境的保证。文档里也明确建议,迁移后注销重登再使用,以刷新旧登记。

测试:把“感觉能打”变成数字

项目的验证分两层。

离线测试共 31 项,包括 23 个布局测试和 8 个定点修改保护测试。覆盖生成 XML 的普通层、Shift、Option、Caps、Command、Control、取消与回退行为,以及参考快照的校验和。

其中死键转换是穷举核对的:

  • E2 参考表共 1,509 条转换,其中 1,507 条可由物理按键路径到达。
  • E1 使用独立参考表,1,508 条转换全部可由 ISO 键码路径到达。
  • 两版各有 92 个可到达死键状态,包括中间状态。

E2 有两条记录无法在已有待定状态中直接从物理按键输入:02DA+00D7(ring 之后输入 ×)和 02DF+200A(扩展选择之后输入 hair space)。生成文件保留了相应字符动作并做字符级测试,但没有为了“凑覆盖率”发明额外快捷键。

原生验证则直接调用 macOS 的 UCKeyTranslate:

布局键盘类型原生检查结果
E2 ANSI40、915,024 项0 失败
E1 ISO38、415,026 项0 失败

两版的主语言都被系统确认为 de。验证工具在运行前后检查当前输入源是否改变,确保它没有偷偷切换输入法。

38/41 是 SDK 定义的便携 USB ISO / 第三方 ISO 类型,属于系统 API 模拟,不是实体 ISO 键盘测试。文档把这一点写得很清楚,没有把模拟验证说成硬件验证。

和 Rime / 鼠须管的配合

德语布局和中文输入法同时使用时,还会遇到一个很具体的问题:鼠须管 1.1.2 用 charactersIgnoringModifiers 转换按键。E2 的 Option+8/9 在系统上输出括号,但传给 Rime 的是 Alt+8/9;裸 ^ 是死键,模拟事件的字符为空,Rime 配置无法凭空捕获。

项目的 RIME-E2.md 记录了一个范围很小的补丁:把 Option+数字、Shift+^ 等事件转换为中文标点入口,同时保留度数符号和 ASCII 脱字符;key_binder 放在 recognizer 之前,避免被反引号反查先截获。

验证是在独立临时目录里用本机 librime 完成的,不碰真实用户词库:修改前复现失败,修改后 62 项检查通过,覆盖全角/半角、空输入/已有候选、英文模式和西文标点模式。

这部分的定位也很克制:它只适配中文标点,不承诺修复英文模式下原有的 Option 行为。

边界:哪些话不能说

这个项目在文档里花了不少篇幅写“尚未验证”:

  • 没有逐键按压实体键盘核对印字位置。
  • 没有覆盖各编辑器、终端、浏览器及其自定义快捷键。
  • 登录窗口、FileVault、密码对话框不保证可用。
  • 没有在 Intel Mac 或较旧 macOS 上验证。
  • ISO/JIS 实体键盘和外接数字键盘未做人工测试。
  • 参考实现和用户布局图不等于 DIN 标准全文,项目不宣称通过标准认证。

这些限制不影响它作为一个可用的原生布局,但决定了文档不能写成“全平台完美支持”。对键盘布局这种底层输入组件来说,诚实列出边界比堆功能列表更有价值。

试用

如果只想使用成品:

mkdir -p "$HOME/Library/Keyboard Layouts"
cp -R dist/GermanE2.bundle "$HOME/Library/Keyboard Layouts/"
cp -R dist/GermanE1.bundle "$HOME/Library/Keyboard Layouts/"   # 需要 ISO 备用版时

然后到 系统设置 → 键盘 → 文本输入 → 编辑 → + → 德语,选择 German E2 (ANSI) 或 German E1 (ISO)。

如果要从源码构建:

python3 tools/build_layout.py
python3 -m unittest discover -s tests -v
swift tools/verify_macos.swift dist/native-cases.json
swift tools/verify_macos.swift dist/native-cases-E1.json

构建只生成 dist/ 下的文件,不会安装、注册或切换输入源。

项目地址:github.com/fluviusmagnus/de-tastatur。代码、构建脚本、测试和生成的布局文件采用 MIT 许可证;references/ 下的第三方参考材料有单独的许可说明。

最后

这个项目最有意思的地方,不是“用 Python 生成了一个 XML”,而是它把一件看起来只能靠手工试错的事情,拆成了可复现的参考表、可穷举的状态机、可自动化的原生验证,以及一份不夸大的限制清单。

原生键盘布局是一个老技术,老到很多细节只能靠读系统头文件、看参考实现和反复加载验证来确认。但也正因为老,一旦做对,它就会安静地待在系统里,不占进程、不要权限,也不打扰任何人。

这大概就是一个输入源最理想的存在方式。

未遂的诗人啊,
不要在红日沉沦后哭泣!
想以此抵御睡眠,
却容易招来他的兄长。
面对未能存在的字句,
渴念变为成瘾的毒药。
因你也是同祂一样,
从太古的虚无中诞生。

升级时间范围: 从 commit fe4db49 到 6be2b85

本次升级为增量优化升级,主要聚焦于记忆检索增强、工具系统完善与架构精简。


主要架构变更

1. Telegram 适配层重构

  • 重构 telegram/ 适配器模块边界,精简 ownership
  • 合并 telegram app runtime 模块
  • 提取 prompt 时间格式化辅助函数
  • 优化 bot 消息格式

2. 上下文事件系统统一

  • 移除: 旧的 system flow
  • 新增: 统一的上下文事件持久化机制

    • 定时任务触发持久化到会话上下文
    • 工具调用结果持久化到 context window

核心功能增强

1. 记忆检索系统重构

新增多阶检索能力

  • 向量辅助摘要召回 (vector-assisted summary recall): 当 FTS5 关键词结果稀疏时,补充向量召回的 summary 候选
  • 混合语言 n-gram 回退 (mixed-language n-gram fallback): 当 query 含中文/日文/混合脚本时,自动切换到更稳健的 n-gram 搜索路径
  • 加权融合 (weighted fusion): 对 summary 与 conversation 结果综合关键词与向量信号进行统一排序
  • 可选 Rerank: 对粗排后的小规模候选做精排(可选配置)

新增搜索控制参数

  • session-aware search targets: 支持指定只搜索当前会话或历史会话
  • search_target="summaries" | "conversations" | "all": 指定搜索目标类型
  • 新增 start_date / end_date 日期范围过滤
  • 支持 role='user' 或 role='assistant' 角色过滤

排序信号增强

当前融合排序考量以下信号:

  • keyword_origin: 关键词命中来源
  • token_coverage: query token 在结果中的覆盖比例
  • exact_match: 完整子串匹配
  • lexical_overlap: 基于混合语言搜索单元的词法重叠度
  • vector_similarity: 向量相似度
  • recency: 时间新鲜度

2. 工具系统增强

新增内置工具

  • Tool Trace: query_tool_history - 查询最近的工具执行历史
  • File Send: 支持将生成的文件直接发送给当前用户

人机协作 (HITL)

  • 新增危险操作人工批准机制 (/approve 命令)
  • 危险 Shell 命令执行前需要用户确认

MCP 改进

  • 启动时预热 MCP tool runtime
  • 限制 MCP 响应长度,避免超长输出

3. Seele 长期记忆增强

  • 新增 LLM-based memory compaction: 使用 LLM 对 seele 记忆进行压缩整理
  • 新增 user location 到 Seele schema
  • 优化 memorable_events 结构
  • 修复 seele migration/repair 逻辑
  • 保留摘要中的消息时间戳

4. 文件处理增强

  • 支持使用文件 caption 生成合成 embedding
  • 文件上传通过系统事件管道处理
  • 优化消息中的代码块渲染

数据库结构变更

新增表/字段

  • 上下文事件表 (context_events): 存储会话中的系统事件、定时任务触发、工具调用结果

启动方式变更

无变更,延续 v0.9.1 启动方式。


代码质量改进

重构

  • 统一 async-first 内存检索流程
  • 精简 Telegram adapter ownership
  • 合并 telegram app runtime 模块
  • 重构架构与模块边界

稳定性修复

  • 修复 Telegram allowed_updates 序列化问题
  • 修复 Telegram 启动循环与超时处理
  • 修复 MCP tool call 丢失问题
  • 修复审批流程阻塞问题
  • 改进网络错误处理

优化

  • 使用 XML 风格优化 prompts
  • 优化 chat prompt
  • 减少不必要消息
  • 更好的代码块渲染

文档更新

新增/更新文档

  • docs/SEARCH_EXAMPLES.md - 扩展搜索示例,包含混合语言搜索与排序规则说明
  • AGENTS.md - 扩展 async-first 重构指南

总结

本次升级是一次增量优化升级,主要变化包括:

  1. 记忆检索大幅增强: 向量辅助召回、混合语言支持、加权融合、可选 rerank
  2. 工具系统完善: 新增 tool trace、file send、HITL 审批机制
  3. Seele 记忆增强: LLM compaction、位置信息、事件结构优化
  4. 架构精简: 统一上下文事件系统、精简 adapter ownership
  5. 稳定性提升: 多个 bug 修复与网络处理改进

这些改进使 Seelenmaschine 在记忆检索的准确性与灵活性方面有显著提升,同时增强了工具系统的可用性与安全性。

概览

项目地址:https://github.com/fluviusmagnus/Seelenmaschine

升级时间范围: 从 commit 8a6d65b 到 fe4db49

本次升级是破坏兼容性的全面升级。请关注以下事项,并按提示进行数据迁移。


主要架构变更

1. 界面框架重构

  • 移除: Flask Web UI (完整的 Web 界面和 CLI 模式)
  • 新增: 纯 Telegram Bot 模式

    • 不再支持 Web 界面
    • 专注于 Telegram 交互体验

2. 向量数据库迁移

  • 移除: LanceDB 向量数据库
  • 新增: SQLite + sqlite-vec(内置向量扩展)

    • 无需外部向量数据库
    • 简化部署和维护

核心功能增强

1. 记忆系统重构

  • 新增完整三层记忆架构:

    • 短期记忆: Context Window 管理 + 自动总结
    • 中期记忆: Embedding + Rerank 智能检索
    • 长期记忆: JSON 结构化人格与用户档案
  • 检索系统增强:

    • 二阶检索(摘要 → 对话)
    • Rerank 模型重排序
    • 时间感知的上下文注入
    • 新增 FTS5 全文搜索(支持布尔运算符)
    • 新增自我查询工具(LLM 主动搜索记忆)

2. MCP 支持增强

  • 支持多种传输方式(stdio、HTTP、SSE)
  • 工具缓存机制
  • 更好的错误处理

3. 新增定时任务系统

  • 支持一次性任务和间隔任务
  • LLM 接口管理任务
  • 数据库存储持久化

4. 新增 Profile 配置系统

  • 多环境配置支持
  • 独立数据目录(data/<profile>/)
  • 启动命令:python src/main_telegram.py <profile>

代码结构重构

新增核心模块

src/
├── core/               # 核心模块
│   ├── database.py     # SQLite + sqlite-vec
│   ├── memory.py       # 记忆系统
│   ├── context.py      # Context Window 管理
│   ├── retriever.py    # 记忆检索
│   └── scheduler.py    # 定时任务
├── llm/
│   ├── client.py       # LLM 客户端
│   ├── embedding.py    # Embedding 客户端
│   └── reranker.py     # Rerank 客户端
├── tools/
│   ├── mcp_client.py
│   ├── memory_search.py
│   └── scheduled_task_tool.py
└── prompts/
    ├── system.py
    ├── summary.py
    └── memory_update.py

移除/合并模块

  • main.py → main_telegram.py
  • chatbot.py (重构整合到各模块)
  • llm.py (拆分为 llm/ 目录)
  • prompts.py (拆分到 prompts/ 目录)
  • flask_webui.py 和 templates/ 目录 (移除)
  • database_maintenance.py (移除)

配置变更

新增配置项

# Context Window
CONTEXT_WINDOW_KEEP_MIN=12
CONTEXT_WINDOW_TRIGGER_SUMMARY=24
RECENT_SUMMARIES_MAX=3

# 记忆检索
RECALL_SUMMARY_PER_QUERY=3
RECALL_CONV_PER_SUMMARY=4
RERANK_TOP_SUMMARIES=3
RERANK_TOP_CONVS=6

# MCP
ENABLE_MCP=false
MCP_CONFIG_PATH=mcp_servers.json

# 时区
TIMEZONE=Asia/Shanghai

移除的配置项

# 旧记忆系统
MAX_CONV_NUM=20
REFRESH_EVERY_CONV_NUM=10
RECALL_SESSION_NUM=2
RECALL_CONV_NUM=4

数据库结构变更

新数据库表结构 (SQLite)

-- 会话表
sessions (id, start_time, end_time, status)

-- 对话表
conversations (id, session_id, role, content, timestamp)

-- 摘要表
summaries (id, session_id, summary_text, timestamp)

-- 向量表 (sqlite-vec)
texts (text_id, text_type, text_content, vec_embedding)

-- 定时任务表
scheduled_tasks (id, task_type, trigger_time, interval_seconds, ...)

移除的存储

  • LanceDB 向量数据库
  • persona_memory.txt / user_profile.txt (合并为 seele.json)

启动方式变更

新的启动方式

# Telegram Bot
python src/main_telegram.py <profile>
./start-telegram.sh <profile>      # Linux/macOS
start-telegram.bat <profile>        # Windows

移除的启动方式

# CLI 模式
python src/main.py <profile>

# Flask Web UI
python src/main.py <profile> --flask
start-flask-webui.sh <profile>

文档更新

新增文档

  • docs/SCHEDULED_TASKS.md - 定时任务指南
  • docs/SEARCH_EXAMPLES.md - 搜索功能示例
  • migration/README.md - 数据迁移指南
  • AGENTS.md - AI 辅助开发指南

移除文档

  • MCP_USAGE.md (内容整合到 README)
  • DATABASE_MAINTENANCE_README.md

测试框架

新增测试

pytest tests/
pytest tests/test_database.py
pytest tests/test_memory.py
pytest tests/test_retriever.py
pytest tests/test_llm.py

总结

这次升级是一次重大架构重构,主要变化包括:

  1. 简化架构: 从 Flask Web UI 切换到纯 Telegram Bot
  2. 一体化存储: 从 LanceDB + SQLite 统一到 SQLite + sqlite-vec
  3. 增强的记忆系统: 完整的三层记忆架构、FTS5 全文搜索、自我查询
  4. 新增功能: 定时任务、Profile 配置系统、改进的 MCP 支持
  5. 代码质量: 模块化设计、完善的测试、详细的文档

这些变化使 Seelenmaschine 更加专注于核心功能——作为具有持久记忆和人格的 AI 助手,同时简化了部署和维护。