☰
开源替代WorkBuddy:打造本地优先的AI个人工作台
2026/10/27 22:12:45 网站建设 项目流程

前两个月我还在跟朋友吐槽 WorkBuddy 这种 AI 效率工具真香,转头就被它的订阅价格和一堆"用不上但必须付费"的功能劝退了。更要命的是,我的任务、笔记、记忆全锁在它的云端,换个账号就什么都不剩,缓存目录还不能自己改,想清理都没地方下手。折腾了几天之后,我决定不再等官方更新,干脆自己写一个开源版的替代品——魔力工作台。

这个项目想做的事情很朴素:做一个跨平台的 AI 个人工作台,能语音输入、能记任务、能写笔记、能通过 Skill 系统扩展能力、能接任意兼容 OpenAI 接口的模型(包括本地模型),但数据必须本地优先,目录可配置,账号换不换都不影响记忆。这篇文章就把我从选型到实现、从踩坑到开源的完整过程拆开讲清楚,希望给同样被这类工具"绑架"过的人一个可以抄作业的参考。

1. 放着现成的 WorkBuddy 不用,我为什么非要自己做一个

先说清楚,我不是那种"万物皆可自研"的人。工作忙起来连觉都不够睡,哪有闲心造轮子。之所以动手做魔力工作台,纯粹是被原版 WorkBuddy 的几个"卡点"逼的。

1.1 原版体验里的那些"卡点"

WorkBuddy 这类工具的核心卖点是"语音优先 + AI 调度",你按住说话,它帮你建任务、记笔记、查信息。理念很好,但实际用起来有几个绕不开的问题:

价格不透明且不便宜。订阅制按年付,折合下来每个月几十块,功能还得选档位。对我这种轻度使用者来说,大部分时间只用到了"语音记任务 + AI 问答",这俩功能就要我买最贵的档位,性价比很低。

数据完全闭环。任务、笔记、长期记忆都存在官方云端。我一度想换个账号登录,结果一问才知道,记忆是跟账号绑定的,换了账号等于换了个人,之前存的上下文全部归零。论坛上搜了一圈,发现还有不少人在问"workbuddy 换账号如何获得原来账号的记忆",本质上就是官方没做迁移功能。

缓存目录不可控。桌面端用久了,本地缓存越堆越大,但设置里根本没有改缓存路径的选项。你要是搜"workbuddy 缓存目录怎么更改",能看到一堆人同病相怜——这不是技术难题,纯粹是产品没给你这个入口。

模型不可选,AI 味压不住。官方接的是云端模型,回复动不动就"首先……其次……最后",还爱在结尾补一句"希望对你有帮助"。你想换更合适的模型,或者接本地模型离线跑,不行,入口是锁死的。

1.2 需求清单:什么样的替代品才算合格

在动手之前,我给自己列了一份需求清单,后续所有选型和实现都对着这份清单来:

需求说明优先级
本地优先任务、笔记、记忆全部存在本地文件,不依赖账号体系P0
可配置缓存目录缓存和数据目录都能在设置里改,不做死路径P0
可插拔模型支持 OpenAI 兼容接口,支持本地 Ollama 等模型后端P0
语音输入按住说话,离线/在线识别都行,识别准确率要能看P0
Skill 扩展能像插件一样加技能,比如查天气、建日程、跑脚本P1
减少 AI 味回复风格可控,不要官方模板腔P1
跨平台至少覆盖 Windows 和 macOS,最好 Linux 也能跑P2

这份清单里 P0 的几条是底线。"本地优先"这条尤其重要——如果数据还是放云端,那我只是把 WorkBuddy 换成了另一个 WorkBuddy,没有任何意义。

2. 技术选型:把整套方案定下来的全过程

定技术栈的时候,我前后对比了好几轮。毕竟这项目不是写着玩的,是要自己长期用、也要开源出去给别人用的,选型错了后面全是坑。

2.1 客户端框架:Electron 还是 Tauri

第一版我其实先用 Electron 搭了原型,毕竟生态成熟、踩坑资料多。但跑起来之后心里很不舒服:一个记任务的小工具,安装包 200MB 起步,内存占用 500MB 上下,在旧笔记本上开个语音识别风扇就起飞了。

后来对比了 Tauri,核心逻辑让我很心动:

  • 前端依然用 Web 技术栈(React + Tailwind),UI 开发效率不降;
  • 后端用 Rust,包体只有十几 MB,内存占用能压到 100MB 出头;
  • 本地文件系统、SQLite、进程管理等系统能力通过 Rust 侧暴露给前端,安全性也比直接塞 Node.js 好。

最终选了 Tauri 2.0。唯一要妥协的是生态相对年轻,Windows 下的音频设备访问权限要自己配置,这个坑后面单独说。

2.2 语音识别与模型接入

语音识别我调研了三条路线:Web Speech API、云端 ASR、本地 Whisper 系模型。

Web Speech API 实现最简单,但浏览器内核的识别效果在中文语境下不太稳定,而且离线不可用。云端 ASR 识别率高,但要另外注册服务、拿 API Key,还要处理上传延迟,而且我不想把语音数据往外送。

最后选了 faster-whisper 作为本地识别引擎。这货是 CTranslate2 重写版的 Whisper,CPU 上跑 int8 量化的small模型,识别速度比原版 Whisper 快很多,中文准确率也能接受。我单独把它封装成了一个本地语音服务,Tauri 后端通过 HTTP 调用,这样以后想换识别引擎也不会动主体代码。

模型接入则相对简单——现在各大模型厂商基本都兼容 OpenAI 的/v1/chat/completions接口,我只需要在设置里填base_url、api_key、model_name三个字段,就能无缝切换云端模型。同时兼容 Ollama 的本地接口,没网的时候也能跑,离线场景直接拉满。

2.3 数据存储与记忆方案

数据层我直接用了 SQLite。理由很现实:单文件、零运维、Tauri 的 Rust 侧用rusqlite操作顺手得很。任务、笔记、记忆、Skill 配置四张表搞定,不需要上重型数据库。

长期记忆这块,我一开始想过用向量数据库做语义检索,但想了想 MVP 阶段太复杂,先用"关键词 + 命名空间"的方式存记忆,每次对话前把相关记忆注入上下文。后面如果记忆量大了,再换成 embedding 检索也不迟,表结构预留了namespace字段,迁移成本很低。

注意:MVP 阶段千万不要一上来就搞向量库。先把手动记忆和关键词召回跑通,你会省掉大量调参时间。

3. 核心模块从 0 到 1 的实现细节

架构定完就开始写代码了,前后大概花了三周把 MVP 跑通。这里挑几个核心模块讲实现细节,代码都简化过,完整版在项目仓库里。

3.1 语音输入链路

语音输入全链路是:按住说话 → 录音 → 送本地语音服务转文字 → 文字进入命令解析器 → 匹配任务创建、笔记或 Skill。

录音这块我用的浏览器MediaRecorder接口,录成 webm 再转 wav。语音服务用 faster-whisper 转写,关键代码如下:

from faster_whisper import WhisperModel # int8 量化,老 CPU 也能跑 model = WhisperModel("small", device="cpu", compute_type="int8") def transcribe(audio_path: str) -> str: segments, info = model.transcribe( audio_path, language="zh", beam_size=1, vad_filter=True, vad_parameters=dict(min_silence_duration_ms=500), ) return "".join(s.text for s in segments).strip()

说几个调参心得:

  • beam_size不要开太大,1就够。实测beam_size=5在 CPU 上延迟翻倍,准确率提升却很有限。
  • vad_filter一定要开。它能去掉录音前后的静音和呼吸声,防止把环境噪声转成"嗯""啊"这种垃圾 token。
  • language="zh"显式指定语言,避免模型在中文和英文之间反复横跳。

转写出来之后,命令解析器用正则做意图识别。比如"明天下午 3 点开会对齐进度"要拆出时间、标题、任务类型。我写了一个时间解析的小工具:

import re from datetime import datetime, timedelta TIME_PATTERNS = [ (r'后天(\d{1,2})点', lambda m: next_day(2, int(m.group(1)), 0)), (r'明天下午(\d{1,2})点', lambda m: next_day(1, int(m.group(1)) + 12, 0)), (r'明天(\d{1,2})点', lambda m: next_day(1, int(m.group(1)), 0)), (r'(\d{1,2})点(\d{2})分', lambda m: today_at(int(m.group(1)), int(m.group(2)))), (r'(\d{1,2})点半', lambda m: today_at(int(m.group(1)), 30)), ] def parse_when(text: str): for pattern, handler in TIME_PATTERNS: m = re.search(pattern, text) if m: return handler(m) return datetime.now().replace(year=2099) # 没解析到就标为待定

解析完时间和任务标题之后,通过 Tauri 的invoke调 Rust 侧写入数据库:

import { invoke } from "@tauri-apps/api/core"; async function addTask(title: string, dueAt?: string) { const id = await invoke<number>("create_task", { title, dueAt }); return id; }
use rusqlite::params; use tauri::State; #[tauri::command] fn create_task(db: State<'_, Db>, title: String, due_at: Option<String>) -> Result<i64, String> { db.conn.execute( "INSERT INTO tasks (title, due_at, status) VALUES (?1, ?2, 'todo')", params![title, due_at], ).map_err(|e| e.to_string())?; Ok(db.conn.last_insert_rowid()) }

3.2 任务、笔记与记忆的存储设计

数据库表结构我迭代了三版才基本稳定。核心就四张表,直接贴建表 SQL:

-- 任务表 CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, due_at TEXT, status TEXT NOT NULL DEFAULT 'todo', -- todo / doing / done / canceled source TEXT NOT NULL DEFAULT 'voice', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- 笔记表 CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL DEFAULT '', content TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- 长期记忆表 CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, namespace TEXT NOT NULL DEFAULT 'general', -- 比如 work / personal key TEXT NOT NULL, value TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(namespace, key) ); -- Skill 注册表 CREATE TABLE IF NOT EXISTS skills ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, manifest_path TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, installed_at TEXT NOT NULL DEFAULT (datetime('now')) );

这里有个容易被忽略的地方:任务的状态流转。很多人做任务管理工具只存done和todo,但实际使用时你会频繁碰到"这任务今天做不完,改到明天"的情况。所以我加了doing状态,同时把语音指令"推迟任务"映射成due_at的更新,而不是把任务删掉重建。

记忆模块的写入时机也非常讲究:用户明确说"记住 XXX"才写入,不能一有对话就自动存。自动记忆虽然显得智能,但会产生大量垃圾记忆,过两天检索出来全是废话,反而干扰回复质量。

3.3 Skill 系统:让工作台学会新技能

Skill 系统是魔力工作台最核心的扩展机制,也是 WorkBuddy 官方做得很封闭、我想重点突破的部分。设计思路是:每个 Skill 是一个独立目录,里面一个manifest.json描述元信息,一个主脚本实现逻辑。

{ "name": "weather", "version": "0.1.0", "description": "查询指定城市的实时天气", "triggers": ["天气", "weather", "气温", "下雨"], "entry": "./skill.py", "language": "python" }

Skill 主脚本的入口约定很简单:接收一个 JSON 字符串作为参数,输出一个 JSON 字符串作为结果。Python 实现长这样:

import sys import json from urllib.request import urlopen def main(params: dict) -> dict: city = params.get("city", "北京") url = f"https://wttr.in/{city}?format=j1" with urlopen(url, timeout=10) as resp: data = json.loads(resp.read()) current = data["current_condition"][0] return { "temperature": current["temp_C"], "description": current["weatherDesc"][0]["value"], } if __name__ == "__main__": result = main(json.loads(sys.argv[1])) print(json.dumps(result, ensure_ascii=False))

运行 Skill 时,工作台把用户指令里的参数提取出来传给脚本,拿到输出结果后交给 AI 模型,让模型用自然语言把结果组织成回复。这样 AI 负责"说话",Skill 负责"做事",各司其职。

触发匹配我用了"关键词优先、语义兜底"的策略:

  • 先检查用户指令是否命中triggers里的关键词;
  • 如果没命中,把指令发给模型做一次意图分类,判断是否应该调用某个 Skill。

关键词优先是为了速度和确定性,语义兜底是为了处理"今天出门要带伞吗"这种不提"天气"两字的问法。

3.4 怎么把"AI 味"压下去

这个点我要多说几句,因为"减少 AI 味"是我搜热词时看到的高频需求,也是自己实际用的时候最烦的问题。官方版 WorkBuddy 的回复一看就是大模型模板腔,我做了两层处理。

第一层是系统提示词,直接把约束写死:

你是我的效率助手,名叫"魔力工作台"。 回复要求: 1. 能一句话说完的不要写三句。 2. 禁止使用"首先/其次/然后/总之"这类连接词。 3. 禁止在回复末尾添加"希望对你有帮助""如有任何问题"等客套话。 4. 只有用户明确说"记住"时才记录记忆,其他时候不要提"我记住了"。 5. 回复要有信息增量,不知道就直说不知道。 6. 不要重复用户指令里已经说清楚的内容。

第二层是后处理规则。模型偶尔还是会"惯性客套",这时候我用简单的字符串匹配把常见客套话直接过滤掉。比如检测到结尾出现"希望这能帮助你""祝您生活愉快"这类句子,就在返回前端前裁剪掉。

两层处理下来,AI 味基本压住了。当然这跟模型本身的风格也有关系,所以模型设置里我留了一个"系统提示词模板"编辑框,用户可以自己调。

4. 开发过程中踩过的最深的几个坑

任何项目做出来光鲜,背后都是坑。有些坑属于"网上搜不到、文档里没有、只能自己踩"的类型,我把印象最深的几个写出来,给后来者省点时间。

4.1 语音识别的同音字和标点问题

第一个坑来自 faster-whisper 的转写结果。中文场景下,模型经常把同音字转错,比如"明天对齐进度"被转成"明天堆齐进度","开会对齐"变成"开会兑齐"。字错了不影响人看,但命令解析器拿到错字之后,正则匹配直接失效。

我试过微调模型,成本太高,果断放弃。最终用了一个轻量方案:维护一个"纠错词典"做后处理替换。

CORRECTIONS = { "堆齐": "对齐", "兑齐": "对齐", "再开": "召开", }

词典放在配置目录里,用户可以自己扩展。效果立竿见影,解析器的命中率从 80% 提到了 94% 左右。但说实话,这终究是补丁方案,如果有条件,针对你的常用词汇微调一个识别模型才是正解。

4.2 Tauri 权限配置与 Windows 录音兼容

Tauri 2.0 在 Windows 上调用麦克风,不是前端getUserMedia就能直接跑的。我第一版在开发环境一切正常,打包成 exe 之后录音按钮点了没反应,控制台提示权限错误。

排查了半天,发现是 Tauri 的 capability 配置问题。需要在src/capabilities/default.json里显式声明麦克风权限:

{ "identifier": "default", "windows": ["main"], "permissions": [ "core:default", "core:window:allow-start-dragging", "core:event:default", "dialog:default" ] }

Windows 平台还需要在tauri.conf.json的 bundle 配置里声明 microphone 的权限声明,否则打包后的应用拿不到麦克风访问权。这个在 Tauri 的官方文档里写得比较隐蔽,我是翻 GitHub issue 才找到的。

4.3 SQLite 并发与缓存目录管理

第三坑:Tauri 前端多个异步操作同时调invoke写 SQLite,偶尔会出现database is locked。原因是默认 SQLite 在并发写的时候锁冲突。解决方案就一行:

let conn = Connection::open_with_flags( db_path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_NO_MUTEX, ).expect("failed to open db"); conn.busy_timeout(Duration::from_millis(5000)).unwrap();

再加一句PRAGMA journal_mode=WAL;,并发问题基本消失。

缓存目录这个点则是受了原版 WorkBuddy 的刺激。我在设置里专门加了两个可配置项:数据目录和缓存目录,用户可以在界面上改,改动后重启生效。配置本身存在 SQLite 的settings表里,代码通过app_config_dir()动态读取。这样"缓存目录不能换"这口气算是出来了。

5. 开源之后的项目现状与下一步

项目做到一个阶段后,我把它放上了 GitHub,仓库名字就叫 MagicWorkbench。最初只想自己用,但想想被订阅制绑住的人不止我一个,干脆开源出来,给同样有需求的人一个可自托管的选项。

5.1 项目结构、许可证与当前进度

当前仓库的结构大致如下:

magic-workbench/ ├── src/ # Tauri 前端(React + TypeScript + Tailwind) │ ├── components/ │ ├── pages/ │ └── lib/ ├── src-tauri/ # Rust 后端 │ ├── commands/ # Tauri command 实现 │ ├── db/ # SQLite 连接与迁移 │ └── skills/ # Skill 加载与执行器 ├── speech-service/ # 本地语音识别服务(faster-whisper) ├── skills/ # 内置 Skill 示例 │ ├── weather/ │ └── timer/ ├── docs/ └── LICENSE

许可证我选了 MIT,原因很简单:这种工具类项目,限制越少,大家用得越放心,参与贡献的意愿也越高。如果你要在自己的项目里封装它做商业产品,MIT 也不会给你造成阻碍。

当前进度:MVP 稳定版 v0.1.0,支持语音建任务、写笔记、长期记忆、天气/计时器两个内置 Skill,Windows 和 macOS 均测试通过。Linux 还有个别音频兼容问题没修完,后续会跟进。

5.2 我自己最想继续做的三件事

第一件是把长期记忆升级成向量检索。现在关键词召回在记忆量小的时候够用,但记忆一旦过千条,检索精度会明显下降。计划接入本地 embedding 模型,配合 SQLite 的sqlite-vec扩展做语义检索,不需要单独跑一个向量数据库服务。

第二件是扩充 Skill 生态。目前内置技能还是太少,我打算把 Skill 的安装做成命令行友好的一键安装,mw-cli install skill-name就能从仓库拉取并注册。顺便给 Skill 加上权限声明——这是复刻 WorkBuddy 时必须认真对待的问题,你不能让任意技能随意读写文件,权限边界要可配置。

第三件是移动端。自己用了一段时间后,发现"掏出手机录音"还是比"打开电脑"频率高得多。Tauri 2.0 支持移动端,但音频后台采集这块限制挺多,打算先用 Web 版本顶一顶,后续再评估打包成 App 的可行性。


这次做魔力工作台,最大的体会是:像 WorkBuddy 这种工具,卡住你的往往不是 AI 能力,而是数据归属权和可扩展性。你愿意为 AI 功能付费,但不愿意为"我不拥有的数据"和"我改不了的缓存路径"付费。开源替代的价值不只是省钱,而是把所有决定权拿回到自己手里。如果你也在找 WorkBuddy 的替代品,建议先跑通本项目再决定要不要继续自研——二三十块的订阅费事小,数据被绑死才是真正的大麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询