基于 @earendil-works/pi-agent-core 的最小个人聊天 Agent。
当前版本只包含:
- DeepSeek Anthropic-compatible API 调用
deepseek-v4-flash和deepseek-v4-pro模型- CLI 交互式聊天
- 流式输出
- 进程内多个独立会话的
AgentManager(CLI 保留单会话兼容入口,Web 支持动态多会话) - 基于
sessions/messages的结构化 SQLite 会话持久化 - 按轮次批量追加消息,保留完整
AgentMessageJSON - FTS5 全文搜索、中文 trigram 搜索与 LIKE 兜底
- 应用层
SessionStore抽象 - 基于 Token 估算和安全切点的自动 Context 压缩
- 使用
session_compactions保存摘要并在重启后恢复 - 摘要失败时保留原上下文,不删除原始历史
SKILL.md格式的 Skills Registry 和 YAML frontmatter 校验- Skills 元数据渐进式披露、显式/描述匹配和按需加载
- Tool Registry:工具注册、筛选、TypeBox 参数校验、超时和取消
- 只读
load_skill、search_session、current_time工具(不会执行 Skill 目录中的脚本) - SQLite
tool_calls工具调用审计 - 轻量本地 Web Gateway(HTTP JSON API + SSE 流式回复)
- Vite + React + shadcn/ui Web 聊天界面(Gateway 同源静态托管)
- 共享 SQLite/审批资源、按 session 隔离的 Agent/ChatService 与串行队列
- 可扩展的
ChatService业务边界
DeepSeek 请求地址:
https://api.deepseek.com/anthropic
- Node.js >= 22.19.0
- DeepSeek API Key
npm install
npm --prefix web install第二条安装独立 web/ 前端子项目的依赖。
PowerShell:
$env:DEEPSEEK_API_KEY = "你的 API Key"
npm run devmacOS / Linux / WSL:
export DEEPSEEK_API_KEY="你的 API Key"
npm run dev可选模型配置:
# 可选:deepseek-v4-flash(默认)或 deepseek-v4-pro
# PowerShell
$env:EVANSCLAW_MODEL = "deepseek-v4-pro"
# macOS / Linux / WSL
export EVANSCLAW_MODEL="deepseek-v4-pro"/help:显示帮助/reset:清空当前会话/exit:退出
启动完整 Web 应用:
npm run webpreweb 会先自动构建 web/,随后由同一个 Gateway 进程托管前端静态文件和 API。浏览器直接访问 http://127.0.0.1:8787,不需要另外启动 Vite 或配置跨域。
Gateway 提供以下接口:
GET /api/health
GET /api/sessions # 当前 Web 身份可见的会话
POST /api/sessions # 创建会话,ID 由服务端生成
GET /api/sessions/:id/messages
POST /api/sessions/:id/messages # {"text":"..."},SSE 流式响应
GET /api/sessions/:id/approvals # 当前会话的脱敏 pending 审批
POST /api/sessions/:id/approvals/:approvalId # {"decision":"approve"|"deny"}
POST /api/sessions/:id/reset
# /api/approvals 和 /api/approvals/:approvalId 仍作为默认会话兼容别名
web/ 是独立的 Vite + React + TypeScript + shadcn/ui 子项目。需要热更新时运行两个进程:
npm run dev:web # 终端 1:Gateway(需 DEEPSEEK_API_KEY)
npm run dev:ui # 终端 2:Vite http://localhost:5173,/api 代理到 8787无 API Key 时可用 mock Gateway 联调:cd web && npm run mock,并用 npm run smoke 跑无头浏览器冒烟测试。审批中的写入/外部操作会通过 SSE 推送审批卡片,浏览器只提交批准或拒绝决策。
默认 Web 会话为 web:local:personal,与 CLI 的 personal 会话分开;它会作为首次打开 Web 时的历史会话,之后可以在页面中创建和切换多个独立会话。可通过 EVANSCLAW_WEB_HOST、EVANSCLAW_WEB_PORT、EVANSCLAW_WEB_CORS_ORIGIN、EVANSCLAW_WEB_SESSION_ID 和 EVANSCLAW_WEB_USER_ID 配置。EVANSCLAW_WEB_STATIC_DIR 可覆盖默认的 web/dist 静态目录;显式目录无效时启动会失败并给出错误。Web 的 write_file 默认工作区为 data/workspace,可通过 EVANSCLAW_WORKSPACE_DIR 配置。
Gateway 默认只监听本机;回环地址可以继续使用本机个人入口。若设置 EVANSCLAW_WEB_TOKEN,所有会话、消息和审批 API 都要求 Authorization: Bearer <token>,浏览器页面会在收到 401 后提示输入 Token,并只保存到当前标签页的 sessionStorage。若监听非回环地址,必须配置 EVANSCLAW_WEB_TOKEN,否则启动失败。当前一个 Token 映射一个 EVANSCLAW_WEB_USER_ID,健康检查和静态前端资源保持公开。
把 Skill 放到以下目录之一:
skills/<skill-name>/SKILL.md
.agents/skills/<skill-name>/SKILL.md
启动时只读取 Skill 的元数据;模型或用户请求需要时才加载完整正文。用户可以在请求中使用 /skill-name 或 $skill-name 显式激活 Skill。格式和示例见 skills/README.md。
当前不会执行 Skill 目录中的脚本,也不会因为 Skill 的 allowed-tools 声明自动授予工具权限。
生产装配可使用 createAgentManagerRuntime() 创建一个进程级 Manager:
const runtime = await createAgentManagerRuntime({
databasePath: "data/evansclaw.sqlite",
});
const route = createChannelSessionRoute({
adapter: {
adapterId: "telegram-primary",
channel: "telegram",
accountId: "primary",
},
conversationKind: "direct",
externalConversationId: "123456",
canonicalUserId: "telegram:primary:user:123456",
});
const session = await runtime.manager.getOrCreate({
sessionId: route.sessionId,
channel: route.channel,
conversationId: route.conversationId,
userId: route.userId,
identity: { authenticated: true },
profile: "read-only",
});
await session.send("你好", (delta) => process.stdout.write(delta));一个 Manager 共享一个 SqliteSessionStore 和一个 ApprovalBroker;每个 session 拥有独立 Agent、ChatService 和 ToolRegistry。同一 session 串行执行,不同 session 可以并行。sessionId 应由可信的 Gateway/Adapter 生成,不能由用户文本决定。
统一的 ChannelAdapter 协议、可信 Session 路由、Telegram Long Polling Adapter 和运行入口已完成:
- Adapter 只负责平台协议解析、连接和发送,不直接调用 Agent;
- 入站消息必须携带
externalMessageId,由 SQLite Inbox 按 adapter/account 去重; - Allowlist 在服务端映射 canonical
userId,默认只允许私聊; - Session ID 使用版本化元组和 SHA-256 派生,不能由用户文本或客户端字段决定;
- SQLite Outbox 保存最终回复和投递重试,发送失败不会重新执行 Agent turn;
- 外部渠道 V1 固定使用
read-onlyprofile,不开放write_file或审批; src/channel/telegram/telegram-adapter.ts使用 Long Polling,仅标准化私聊文本,update_id作为入站幂等 ID,并支持最终回复分片、回复引用、429 等待和 AbortSignal 停止;src/channel/telegram/telegram-api.ts使用原生fetch调用 Bot API,不引入 Telegram SDK,网络错误不会把 Bot Token 写入错误消息。
阶段 1–5 已完成:Telegram 运行入口、Allowlist 配置及离线全链路集成测试已接入。测试使用真实 Adapter、Gateway、AgentManager、ChatService、Agent 和 SQLite,仅替换模型流及 Telegram HTTP,不需要真实凭据。尚未进行真实 Bot 联调。Web 继续使用自己的 REST/SSE Gateway,CLI 继续使用本地交互循环。
需要 Node.js 22.19+、可访问 Telegram Bot API 的网络、BotFather 创建的 Bot Token,以及获准用户的 Telegram 数字 user ID(不是 username 或群 ID)。仅在本机设置凭据,不要提交到仓库或粘贴进聊天。
npm install
$env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>"
$env:EVANSCLAW_TELEGRAM_BOT_TOKEN = "<BotFather 提供的 Token>"
$env:EVANSCLAW_TELEGRAM_ALLOWED_USER_IDS = "123456789,987654321"
$env:EVANSCLAW_TELEGRAM_ACCOUNT_ID = "primary" # 可省略,默认 primary
npm run telegram- Token、非空 Allowlist 必填;用户 ID 必须是安全范围内的正十进制整数。任一错误条目会拒绝整份配置,不会退化为允许所有用户。
- account ID 只能包含 1–64 位字母、数字、下划线或短横线。每个 Bot 使用稳定且独立的 account ID;更改它会进入新的会话命名空间,不要用同一个 account ID 换成其他 Bot。
- 启动成功后,用 Allowlist 中的账号私聊 Bot;未知用户不创建 Session,也不触发模型。只注册
load_skill、search_session、current_time;普通“同意/approve”是聊天文本,不是审批。 Ctrl+C/SIGTERM停止轮询及平台请求,排空已接受的 Agent turn 后关闭 SQLite;关闭过程中生成但尚未发送的回复留在 Outbox,供下次启动投递。关闭可能需要等待模型完成。
运行限制与恢复语义:
- 每个 Bot 只能运行一个 Long Polling 实例;不要同时运行其他 Bot 程序。若 Bot 已配置 Webhook,先由管理员使用 Telegram
deleteWebhook取消(不要选择丢弃待处理消息);本程序不会自动修改 Webhook。冲突通常表现为 409 错误。 - 当前入口独占默认
data/evansclaw.sqlite的运行时恢复所有权。不要与另一个 Telegram/Web/CLI 进程同时打开同一数据库;多入口同进程共享装配尚未提供。SQLite 中保存会话及 Inbox/Outbox 正文,应保护本地数据文件。 - 只支持私聊文本、最终纯文本回复;不支持群聊、媒体、Webhook、流式编辑或按钮审批。超过 4096 字符的回复分片发送,最多 100 片。
- 出站为 at-least-once:网络结果不确定、关闭或部分分片已发出时,重试可能产生重复消息/重复前缀;发送失败不会重新运行模型。重试耗尽进入
dead,需要人工检查,不提供自动无限重试或管理界面。 - 重启恢复
received入站及待发/失败/不确定 Outbox;崩溃遗留的running入站变为uncertain,不会自动重新执行。Telegram 未确认 update 可重投,由 Inbox 去重;不要删除数据库来“重置 offset”。 - 轮询 401 会关闭运行时并以失败状态退出;网络故障及 429 会退避重试,单次 HTTP 请求最长 60 秒。终端不输出 Token 或原始异常正文。
离线验证:node --import tsx --test test/telegram-runtime.test.ts test/telegram-adapter.test.ts。真实 Bot 的接收、发送及网络可用性仍需按上述配置手工验证。
Tool Policy、审批 Broker、审批持久化和 Web 审批交互已完成;CLI 仍只注册三个只读工具,Web 另外注册受审批保护的 write_file。
会话数据库默认保存到:
data/evansclaw.sqlite
CLI 入口只注册 load_skill、search_session 和 current_time 三个只读工具。Web 入口另外注册受审批保护的 write_file,默认只能写入 data/workspace(可由 EVANSCLAW_WORKSPACE_DIR 覆盖),不会执行 Shell、网络发送或其他外部通信。工具调用审计保存在会话数据库的 tool_calls 表中;写文件的审批和审计会按当前配置保留完整参数。