Mac自托管通用上下文层:从零搭建AI工作流的统一API 最近在把本地开发工作流往 AI 方向迁移时遇到一个绕不开的问题AI 工具对我当前所处的“上下文”几乎一无所知。我正看着哪个文件、剪贴板复制了哪段内容、最近打开过什么文档、前台运行着哪个应用这些信息在传统工具链里都是分散的每个工具只知道自己那一小块数据。后来我在折腾自托管服务时看到了一个思路在 Mac 上做一个通用的上下文层把系统里散落的上下文统一采集、存储、对外提供 API上层工具按需读取。这样一来不管是 AI 助手、自动化脚本还是效率工具都可以通过同一套接口拿到当前环境的状态而不是各自去适配不同的应用。这篇文章会完整拆解“Self-hosted universal context layer for Mac”这个思路的核心原理并给出一个能在 Mac 本地运行的最小项目。文章会包含环境准备、数据模型设计、完整代码、运行验证、常见排错和工程建议。整篇内容不依赖某个特定开源项目而是从底层思路出发带你亲手搭一层可扩展的上下文服务。如果你是做本地 AI 工作流、自动化工具或者对 macOS 系统能力调用感兴趣这篇应该对你有用。1. 为什么需要“自托管通用上下文层”1.1 从一次 AI 助手答非所问说起很多人用 AI 编程助手时都有过这样的体验助手明明读取了当前文件却不知道你剪贴板里有一段刚复制出来的报错日志它能理解代码仓库结构却不知道你此刻正在看某个文档页面。问题的根源在于AI 工具和系统之间缺少一个统一的上下文供给通道。如果我们可以把“当前前台应用”“剪贴板内容”“最近文件操作”“系统时间”“活跃窗口标题”等状态全部汇总到一个服务里这个服务再把数据暴露成标准 API那么上层工具只要调用一个接口就能拿到一份相对完整的“此刻发生了什么”。1.2 什么是通用上下文层通俗地说上下文层是介于“系统数据源”和“上层应用”之间的一层服务。它做三件事采集从剪贴板、应用状态、文件系统、日历、提醒事项等数据源获取信息。存储把采集到的上下文按统一的模型落库方便回溯和搜索。提供接口通过 HTTP API 或进程内调用把上下文输出给 AI 工具、脚本或其他应用。“通用”体现在数据模型和接口规范是统一的不针对某个具体应用定制。“自托管”则意味着服务运行在你自己控制的机器上数据不需要上传到第三方云端。1.3 为什么要在 Mac 上做这件事Mac 系统有几个特点很适合做上下文层统一的应用层脚本能力通过osascript可以获取前台应用、执行系统自动化操作。剪贴板命令生态完善pbpaste可以直接读取剪贴板文本无需额外权限。本地服务管控方便可以用launchd注册开机自启也可以直接命令行前台运行。开发者工具链成熟Python、Node.js、Swift 都能方便地调用系统能力。对于开发者和效率工具爱好者来说Mac 上自建一个上下文层意味着可以摆脱对第三方云服务的依赖完全掌控自己的数据。2. Mac 环境准备与版本说明下面从零开始搭建一个可运行的示例项目。不同机器的环境可能不同但下面的步骤在绝大多数 Intel 和 Apple Silicon 芯片的 Mac 上都能使用。2.1 系统与硬件要求示例代码主要依赖 macOS 自带的osascript、pbpaste命令以及 Python 3.9 以上的解释器。建议系统为 macOS 12 或更高版本但部分功能在旧版本也能运行。如果你经常在 Mac 上折腾开发环境可能也遇到过 Maven、JDK、Python、MySQL 等工具的环境变量配置问题。本文不展开这些工具链的安装细节只保证示例服务依赖的 Python 环境可用即可。2.2 安装 Xcode Command Line Toolsosascript属于系统自带命令但如果你之前没有安装过开发者工具部分命令行组件可能缺失。建议先执行xcode-select --install系统会弹出安装窗口按提示完成即可。如果已经安装过终端会提示“command line tools are already installed”。2.3 安装 Python 并创建虚拟环境macOS 自带 Python 2 或较低版本的 Python 3但为了避免污染系统环境强烈建议使用 Homebrew 安装独立 Python然后在项目目录创建虚拟环境。# 安装 Homebrew如果还没安装 /bin/bash -c $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh) # 安装 Python brew install python # 检查版本 python3 --version这里版本需要根据你的实际安装情况调整。示例代码基于 Python 3.9 编写推荐使用 3.11 或更高版本。创建项目目录和虚拟环境mkdir ~/context-layer cd ~/context-layer python3 -m venv .venv source .venv/bin/activate2.4 安装 Python 依赖示例代码使用 FastAPI 提供 HTTP 接口使用 uvicorn 作为开发服务器使用 SQLite 做本地存储SQLite 是 Python 内置模块无需额外安装。在requirements.txt中写入fastapi0.100.0 uvicorn[standard]0.23.0然后安装pip install -r requirements.txt如果网络安装缓慢可以配置国内 PyPI 镜像但这不是必须步骤。安装完成后项目环境就准备好了。3. 核心设计拆解上下文层的数据模型与采集流程在写代码之前先理解上下文层内部是怎么工作的。一个好的上下文层不是简单地把“所有数据”塞进数据库而是需要合理建模。3.1 上下文数据的来源在 Mac 上常见上下文来源包括数据源获取方式示例内容剪贴板pbpaste复制的文本、日志、代码片段前台应用AppleScript当前活跃的应用名称窗口标题AppleScript当前窗口标题系统状态system_profiler、date系统运行时间、当前时间文件操作FSEvent、Spotlight最近打开或修改的文件浏览器活动AppleScript/浏览器扩展当前标签页标题和 URL注意浏览器标签页等数据涉及较多隐私自托管方案里要谨慎设计采集范围。示例项目先实现剪贴板和前台应用两个基础采集器。3.2 上下文数据模型设计上下文数据需要统一格式。我建议使用以下模型class ContextItem: id: int # 数据库自增 ID source: str # 数据来源例如 clipboard、frontmost_app content: str # 采集到的核心内容 metadata: dict # 附加信息例如应用名称、窗口标题 created_at: datetime # 创建时间 updated_at: datetime # 更新时间source字段用来区分数据来源metadata可以放结构化辅助数据。这样的模型既简单又容易扩展。3.3 采集策略主动采集还是被动接收上下文层的采集方式有两种主动采集服务定时调用系统命令或者监听系统事件把数据写入存储。被动接收其他小程序通过 API 上报数据服务只负责存储和查询。实际项目中往往两种方式结合。示例项目先用主动采集实现/api/context/current再提供/api/context/ingest作为被动接收入口。3.4 API 接口设计一个最小上下文层需要四类接口接口方法作用/api/context/currentGET返回当前采集到的实时上下文/api/context/recentGET返回最近存储的上下文记录/api/context/searchGET根据关键词搜索历史上下文/api/context/ingestPOST让外部程序上报一条上下文记录3.5 安全与权限边界自托管服务最容易被忽略的是安全边界。本地 API 一旦监听在非回环地址就可能被局域网内其他设备访问。示例项目会明确让服务只监听127.0.0.1。同时采集剪贴板内容属于敏感操作需要在代码中标注清楚并且只把数据保存在本地 SQLite 文件里不做任何外部上传。4. 完整实战搭建一个本地上下文层服务下面开始写代码。先创建项目结构。4.1 创建项目结构在~/context-layer目录下创建如下结构context-layer/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── models.py # 数据模型 │ ├── database.py # SQLite 存储层 │ └── collectors.py # 采集器 ├── requirements.txt └── README.md创建目录mkdir -p app touch app/__init__.py4.2 编写数据模型文件路径app/models.pyfrom datetime import datetime from typing import Optional from pydantic import BaseModel class ContextItem(BaseModel): 上下文数据模型 id: Optional[int] None source: str content: str metadata: dict {} created_at: datetime datetime.now() updated_at: datetime datetime.now()这里使用 Pydantic 的BaseModel做数据校验。FastAPI 会自动根据这个模型解析请求体并生成接口文档。id字段可选因为插入数据库时由 SQLite 自增生成。4.3 编写存储层文件路径app/database.pyimport json import sqlite3 from contextlib import closing from app.models import ContextItem DB_PATH context.db def init_db(): 初始化 SQLite 数据库表结构 with closing(sqlite3.connect(DB_PATH)) as conn: conn.execute( CREATE TABLE IF NOT EXISTS context_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, content TEXT NOT NULL, metadata TEXT DEFAULT {}, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() def insert_item(item: ContextItem) - int: 插入一条上下文记录返回自增 id with closing(sqlite3.connect(DB_PATH)) as conn: cur conn.execute( INSERT INTO context_items (source, content, metadata) VALUES (?, ?, ?), (item.source, item.content, json.dumps(item.metadata, ensure_asciiFalse)) ) conn.commit() return cur.lastrowid def query_recent(limit: int 20) - list[dict]: 查询最近写入的上下文记录 with closing(sqlite3.connect(DB_PATH)) as conn: conn.row_factory sqlite3.Row rows conn.execute( SELECT * FROM context_items ORDER BY created_at DESC LIMIT ?, (limit,) ).fetchall() return [dict(row) for row in rows] def search_content(keyword: str) - list[dict]: 按关键词搜索上下文记录 with closing(sqlite3.connect(DB_PATH)) as conn: conn.row_factory sqlite3.Row rows conn.execute( SELECT * FROM context_items WHERE content LIKE ? ORDER BY created_at DESC, (f%{keyword}%,) ).fetchall() return [dict(row) for row in rows]这里有几个关键点closing负责自动关闭数据库连接避免连接泄漏。row_factory sqlite3.Row让查询结果可以通过字段名访问。LIKE查询在数据量较小时够用大数据量场景需要换用全文索引这个在最佳实践部分再展开。4.4 编写采集器文件路径app/collectors.pyimport subprocess from datetime import datetime def get_frontmost_app() - str: 使用 AppleScript 获取当前前台应用名称 script tell application System Events get name of first application process whose frontmost is true end tell result subprocess.run( [osascript, -e, script], capture_outputTrue, textTrue ) if result.returncode 0: return result.stdout.strip() return unknown def get_clipboard() - str: 读取剪贴板文本内容 result subprocess.run( [pbpaste], capture_outputTrue, textTrue ) if result.returncode 0: return result.stdout.strip() return def collect_current_context() - dict: 汇总当前上下文快照 return { timestamp: datetime.now().isoformat(), frontmost_app: get_frontmost_app(), clipboard_preview: get_clipboard()[:200], }值得说明的是get_frontmost_app依赖osascript调用系统自动化能力。如果是第一次在 Mac 上运行系统可能会弹出自动化授权提示需要允许终端控制“系统事件”。剪贴板读取则通过pbpaste命令实现不需要额外权限。4.5 编写 FastAPI 接口文件路径app/main.pyfrom datetime import datetime from fastapi import FastAPI from app import collectors, database from app.models import ContextItem app FastAPI( titleMac Universal Context Layer, descriptionSelf-hosted universal context layer for Mac, version0.1.0 ) app.on_event(startup) def on_startup(): 应用启动时初始化数据库 database.init_db() app.get(/) def root(): return {service: context-layer, status: running} app.get(/api/context/current) def get_current_context(): 获取当前实时上下文快照 return collectors.collect_current_context() app.get(/api/context/recent) def get_recent(limit: int 20): 获取最近存储的上下文记录 return database.query_recent(limit) app.get(/api/context/search) def search(q: str ): 按关键词搜索历史上下文 return database.search_content(q) app.post(/api/context/ingest) def ingest(item: ContextItem): 接收外部程序上报的上下文记录 row_id database.insert_item(item) return {status: ok, id: row_id}这个接口设计很简洁但已经覆盖了“采集、存储、查询、上报”四个核心能力。4.6 运行与验证在项目根目录执行uvicorn app.main:app --reload --host 127.0.0.1 --port 8010启动成功后会看到类似输出INFO: Uvicorn running on http://127.0.0.1:8010 INFO: Application startup complete.注意这里明确指定--host 127.0.0.1服务只监听本地回环地址不会暴露到局域网。打开另一个终端验证接口curl http://127.0.0.1:8010/api/context/current返回示例{ timestamp: 2025-04-15T10:30:00.123456, frontmost_app: Terminal, clipboard_preview: curl http://127.0.0.1:8010/api/context/current }再测试存储和查询curl -X POST http://127.0.0.1:8010/api/context/ingest \ -H Content-Type: application/json \ -d {source: manual, content: 这是一条测试上下文, metadata: {note: demo}} curl http://127.0.0.1:8010/api/context/recent?limit5 curl http://127.0.0.1:8010/api/context/search?q测试如果这些接口都返回了预期 JSON说明本地上下文层服务已经正常跑起来了。4.7 将当前上下文接入 AI 工作流上下文层最有价值的场景是给 AI 工具提供实时信息。假设你正在使用 Claude Code 或者 Codex 这类 AI 编码工具可以在提示词里要求模型先调用本地接口获取上下文curl -s http://127.0.0.1:8010/api/context/current拿到 JSON 后把它作为 system prompt 的一部分填入 AI 工具就能让 AI 知道你当前正在看哪个应用、剪贴板里有什么内容。如果想要更主动的联动还可以写一个 shell 函数ctx() { curl -s http://127.0.0.1:8010/api/context/current | python3 -m json.tool }把这个函数写到~/.zshrc里随时在终端输入ctx查看当前上下文快照。5. 常见问题与排查思路自托管服务在 Mac 上运行时会遇到一些典型问题。下面按“现象、原因、解决思路”的表格整理方便快速查阅。问题现象常见原因解决思路8000 端口启动失败端口被其他程序占用修改端口例如--port 8010osascript报错not authorized未授予终端自动化权限系统设置-隐私与安全性-自动化勾选终端pbpaste返回空内容剪贴板中没有文本先复制一段文本再测试sqlite3.OperationalError: database is locked多个进程同时写 SQLite简化写入频率或引入连接池/锁机制python3指向系统自带版本Homebrew Python 未加入 PATHbrew install python后确认which python3FastAPI 启动后局域网无法访问服务只监听 127.0.0.1这是正常现象出于安全考虑不建议改监听地址ModuleNotFoundError: No module named appuvicorn 启动目录不对确保在项目根目录执行uvicorn app.main:app5.1 关于自动化权限的完整说明如果你的 Mac 运行的是较新的 macOS 版本第一次执行osascript访问“系统事件”时会弹出一个授权窗口。此时必须点击“允许”否则get_frontmost_app()会一直返回unknown。如果之前误点了“不允许”可以到“系统设置 - 隐私与安全性 - 自动化”里找到终端对应的项打开开关。需要注意的是每次修改授权后需要重启终端或者重启服务才能生效。5.2 关于 SQLite 写入锁SQLite 在单机低并发场景下足够稳定但上下文层如果定时高频写入多个进程同时写同一文件就可能出现database is locked。示例代码里每次请求都会新建连接这在低频调用下没问题。生产环境建议改用单一写入入口或者降低采集频率。5.3 关于 Mac 开发环境配置在 Mac 上跑 Python 项目时很多人会踩环境变量配置的坑。比如安装了 Python 后python3还是指向系统自带的旧版本。可以执行which python3确认路径。如果指向/usr/bin/python3说明用的是系统版本如果指向/opt/homebrew/bin/python3或/usr/local/bin/python3说明 Homebrew 版本生效。项目里始终使用虚拟环境可以避免大部分环境混乱问题。6. 最佳实践与工程建议一个能跑的示例只是起点。如果想把上下文层真正用起来建议在工程化上做更多考虑。6.1 数据安全与隐私边界剪贴板内容可能包含密码、密钥、个人敏感信息。上下文层这类工具天然会触碰敏感数据因此必须默认遵循最小权限原则敏感数据只保留在本地禁止上传到任何外部服务。采集时对内容做脱敏截断示例里只取了剪贴板前 200 个字符这是保护隐私的一种方式。API 只监听127.0.0.1不对外开放。如果确实需要远程访问优先考虑 SSH 隧道或者带认证的反向代理而不是直接暴露端口。定时任务中不要打印完整上下文内容日志中只保留来源和时间。6.2 数据存储与过期策略上下文数据累积到一定程度后会占用磁盘空间也会让搜索变慢。建议引入数据保留策略例如普通上下文记录保留 7 天。标记为“重要”的记录可以长期保存。每天凌晨通过定时任务清理过期记录。SQLite 的表结构可以增加一个expire_at字段清理时执行DELETE FROM context_items WHERE created_at datetime(now, -7 days)即可。6.3 采集器扩展机制示例里的采集器是硬编码函数。实际项目中建议把采集器设计成插件注册模式COLLECTORS { clipboard: get_clipboard, frontmost_app: get_frontmost_app, } def collect_all() - dict: result {} for name, func in COLLECTORS.items(): try: result[name] func() except Exception as e: result[name] ferror: {e} return result这样每次增加新的数据源只需要实现一个采集函数并注册到字典里不需要修改采集逻辑主体。6.4 性能与资源占用上下文层是一个常驻服务资源占用需要控制。几个具体建议使用asyncio或线程池执行subprocess调用避免阻塞事件循环。合理设置 SQLite 的连接超时和busy_timeout减少锁冲突。如果上下文更新频率高可先在内存中做合并再批量写入数据库。用launchd配置开机自启时设置KeepAlive和ThrottleInterval避免崩溃后无限重启。6.5 与 AI 工具链的集成方式上下文层最终要服务上层工具。实际集成时可以考虑几种模式集成方式适用场景说明提前拉取上下文AI 编码工具、脚本在对话开始前调用/api/context/current事件触发拉取自动化工作流某个文件变化时调用接口获取上下文主动上报小程序、浏览器插件通过/api/context/ingest把新的上下文推给服务周期轮询仪表盘、监控面板定时查询/api/context/recent展示最近状态值得强调的是上下文层服务本身不具备 AI 能力它只负责“提供上下文”具体怎么用由上层决定。这种解耦方式让上下文层可以被多种工具复用避免重复开发。7. 总结与下一步学习本文从“Self-hosted universal context layer for Mac”这个思路出发解释了通用上下文层在本地 AI 工作流和自动化工具中的价值并完整实现了一个最小可运行服务。你可以在 Mac 上采集剪贴板和前台应用信息通过 SQLite 做持久化存储再通过 FastAPI 对外提供统一的上下文访问接口。如果你打算继续深入可以从这几个方向扩展增加更多采集器例如窗口标题、浏览器活动标签页、最近文件记录。把存储层从 SQLite 迁移到 PostgreSQL支持更高并发和更灵活的全文检索。为 API 增加 Token 认证让同一台机器上的其他可信应用安全访问。用launchd把服务注册成系统守护进程开机自动运行。把上下文数据接入 Claude Code、Codex 等 AI 编程工具让 AI 真正“看到”你当前的工作状态。自托管上下文层的核心价值是数据自主可控。数据在自己手里、接口在自己手里、运行方式也在自己手里。上手成本不高但能明显提升本地工具链的自动化程度。建议先照着完整跑一遍再按自己的使用习惯增加采集源和集成方式慢慢调整成最适合自己的工作流。