添加中文文档
This commit is contained in:
782
docs/zh/SPEC.md
Normal file
782
docs/zh/SPEC.md
Normal file
@@ -0,0 +1,782 @@
|
||||
# NanoClaw 规范
|
||||
|
||||
一款个人 Claude 助手,支持多渠道、按会话持久化记忆、定时任务以及容器隔离的 agent(智能体)执行。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构](#架构)
|
||||
2. [架构:渠道系统](#架构渠道系统)
|
||||
3. [目录结构](#目录结构)
|
||||
4. [配置](#配置)
|
||||
5. [记忆系统](#记忆系统)
|
||||
6. [会话管理](#session-管理)
|
||||
7. [消息流转](#消息流转)
|
||||
8. [命令](#命令)
|
||||
9. [定时任务](#定时任务)
|
||||
10. [MCP 服务器](#mcp-服务器)
|
||||
11. [部署](#部署)
|
||||
12. [安全考量](#安全考量)
|
||||
|
||||
---
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ HOST(宿主机,macOS / Linux) │
|
||||
│ (主 Node.js 进程) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌────────────────────┐ │
|
||||
│ │ Channels │─────────────────▶│ SQLite 数据库 │ │
|
||||
│ │ (渠道,启动时 │◀────────────────│ (messages.db) │ │
|
||||
│ │ 自行注册) │ 存储/发送 └─────────┬──────────┘ │
|
||||
│ └──────────────────┘ │ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
|
||||
│ │ Message Loop │ │ Scheduler Loop │ │ IPC Watcher │ │
|
||||
│ │ (消息轮询循环) │ │ (调度器循环) │ │ (IPC 监听器) │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ └───────────────┘ │
|
||||
│ │ │ │
|
||||
│ └───────────┬───────────┘ │
|
||||
│ │ 启动容器 │
|
||||
│ ▼ │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ CONTAINER(容器,Linux VM) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AGENT RUNNER(智能体运行器) │ │
|
||||
│ │ │ │
|
||||
│ │ 工作目录: /workspace/group(从宿主机挂载) │ │
|
||||
│ │ 卷挂载: │ │
|
||||
│ │ • groups/{name}/ → /workspace/group │ │
|
||||
│ │ • groups/global/ → /workspace/global/(仅非主群组) │ │
|
||||
│ │ • data/sessions/{group}/.claude/ → /home/node/.claude/ │ │
|
||||
│ │ • 额外目录 → /workspace/extra/* │ │
|
||||
│ │ │ │
|
||||
│ │ 工具(所有群组): │ │
|
||||
│ │ • Bash(安全——在容器内沙箱化!) │ │
|
||||
│ │ • Read, Write, Edit, Glob, Grep(文件操作) │ │
|
||||
│ │ • WebSearch, WebFetch(互联网访问) │ │
|
||||
│ │ • agent-browser(浏览器自动化) │ │
|
||||
│ │ • mcp__nanoclaw__*(通过 IPC 的调度器工具) │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 组件 | 技术 | 用途 |
|
||||
|-----------|------------|---------|
|
||||
| Channel System | Channel registry (`src/channels/registry.ts`) | 渠道在启动时自行注册 |
|
||||
| Message Storage | SQLite (better-sqlite3) | 存储消息供轮询 |
|
||||
| Container Runtime | Containers(Linux VM) | 用于 agent 执行的隔离环境 |
|
||||
| Agent | @anthropic-ai/claude-agent-sdk (0.2.29) | 带工具和 MCP 服务器的 Claude 运行环境 |
|
||||
| Browser Automation | agent-browser + Chromium | Web 交互与截图 |
|
||||
| Runtime | Node.js 20+ | 用于路由和调度的宿主机进程 |
|
||||
|
||||
---
|
||||
|
||||
## 架构:渠道系统
|
||||
|
||||
核心不含任何内置渠道——每个渠道(WhatsApp、Telegram、Slack、Discord、Gmail)作为 [Claude Code skill](https://code.claude.com/docs/en/skills) 安装,将渠道代码添加到您的 fork 中。渠道在启动时自行注册;已安装但缺少凭证的渠道会发出 WARN 日志并被跳过。
|
||||
|
||||
### 系统图
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Channels["渠道"]
|
||||
WA[WhatsApp]
|
||||
TG[Telegram]
|
||||
SL[Slack]
|
||||
DC[Discord]
|
||||
New["其他渠道 (Signal, Gmail...)"]
|
||||
end
|
||||
|
||||
subgraph Orchestrator["编排器 — index.ts"]
|
||||
ML[Message Loop]
|
||||
GQ[Group Queue]
|
||||
RT[Router]
|
||||
TS[Task Scheduler]
|
||||
DB[(SQLite)]
|
||||
end
|
||||
|
||||
subgraph Execution["容器执行"]
|
||||
CR[Container Runner]
|
||||
LC["Linux Container"]
|
||||
IPC[IPC Watcher]
|
||||
end
|
||||
|
||||
%% Flow
|
||||
WA & TG & SL & DC & New -->|onMessage| ML
|
||||
ML --> GQ
|
||||
GQ -->|并发控制| CR
|
||||
CR --> LC
|
||||
LC -->|文件系统 IPC| IPC
|
||||
IPC -->|任务与消息| RT
|
||||
RT -->|Channel.sendMessage| Channels
|
||||
TS -->|到期任务| CR
|
||||
|
||||
%% DB Connections
|
||||
DB <--> ML
|
||||
DB <--> TS
|
||||
|
||||
%% Styling for the dynamic channel
|
||||
style New stroke-dasharray: 5 5,stroke-width:2px
|
||||
```
|
||||
|
||||
### 渠道注册表
|
||||
|
||||
渠道系统建立在 `src/channels/registry.ts` 中的工厂注册表之上:
|
||||
|
||||
```typescript
|
||||
export type ChannelFactory = (opts: ChannelOpts) => Channel | null;
|
||||
|
||||
const registry = new Map<string, ChannelFactory>();
|
||||
|
||||
export function registerChannel(name: string, factory: ChannelFactory): void {
|
||||
registry.set(name, factory);
|
||||
}
|
||||
|
||||
export function getChannelFactory(name: string): ChannelFactory | undefined {
|
||||
return registry.get(name);
|
||||
}
|
||||
|
||||
export function getRegisteredChannelNames(): string[] {
|
||||
return [...registry.keys()];
|
||||
}
|
||||
```
|
||||
|
||||
每个工厂接收 `ChannelOpts`(包含回调 `onMessage`、`onChatMetadata` 和 `registeredGroups`),并返回一个 `Channel` 实例,如果该渠道的凭证未配置则返回 `null`。
|
||||
|
||||
### 渠道接口
|
||||
|
||||
每个渠道实现此接口(定义在 `src/types.ts` 中):
|
||||
|
||||
```typescript
|
||||
interface Channel {
|
||||
name: string;
|
||||
connect(): Promise<void>;
|
||||
sendMessage(jid: string, text: string): Promise<void>;
|
||||
isConnected(): boolean;
|
||||
ownsJid(jid: string): boolean;
|
||||
disconnect(): Promise<void>;
|
||||
setTyping?(jid: string, isTyping: boolean): Promise<void>;
|
||||
syncGroups?(force: boolean): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 自行注册模式
|
||||
|
||||
渠道使用 barrel 导入模式自行注册:
|
||||
|
||||
1. 每个渠道 skill 向 `src/channels/` 添加一个文件(例如 `whatsapp.ts`、`telegram.ts`),在模块加载时调用 `registerChannel()`:
|
||||
|
||||
```typescript
|
||||
// src/channels/whatsapp.ts
|
||||
import { registerChannel, ChannelOpts } from './registry.js';
|
||||
|
||||
export class WhatsAppChannel implements Channel { /* ... */ }
|
||||
|
||||
registerChannel('whatsapp', (opts: ChannelOpts) => {
|
||||
// 如果凭证缺失则返回 null
|
||||
if (!existsSync(authPath)) return null;
|
||||
return new WhatsAppChannel(opts);
|
||||
});
|
||||
```
|
||||
|
||||
2. barrel 文件 `src/channels/index.ts` 导入所有渠道模块,触发注册:
|
||||
|
||||
```typescript
|
||||
import './whatsapp.js';
|
||||
import './telegram.js';
|
||||
// ... 每个 skill 在此处添加其导入
|
||||
```
|
||||
|
||||
3. 启动时,编排器(`src/index.ts`)循环遍历已注册的渠道,连接返回有效实例的渠道:
|
||||
|
||||
```typescript
|
||||
for (const name of getRegisteredChannelNames()) {
|
||||
const factory = getChannelFactory(name);
|
||||
const channel = factory?.(channelOpts);
|
||||
if (channel) {
|
||||
await channel.connect();
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|---------|
|
||||
| `src/channels/registry.ts` | 渠道工厂注册表 |
|
||||
| `src/channels/index.ts` | 触发渠道自行注册的 barrel 导入 |
|
||||
| `src/types.ts` | `Channel` 接口、`ChannelOpts`、消息类型 |
|
||||
| `src/index.ts` | 编排器——实例化渠道、运行消息循环 |
|
||||
| `src/router.ts` | 查找 JID 所属的渠道,格式化消息 |
|
||||
|
||||
### 添加新渠道
|
||||
|
||||
要添加新渠道,请向 `.claude/skills/add-<name>/` 贡献一个 skill,该 skill 需要:
|
||||
|
||||
1. 添加一个 `src/channels/<name>.ts` 文件,实现 `Channel` 接口
|
||||
2. 在模块加载时调用 `registerChannel(name, factory)`
|
||||
3. 如果凭证缺失,工厂返回 `null`
|
||||
4. 向 `src/channels/index.ts` 添加一条导入行
|
||||
|
||||
请参考已有的 skills(`/add-whatsapp`、`/add-telegram`、`/add-slack`、`/add-discord`、`/add-gmail`)了解模式。
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
nanoclaw/
|
||||
├── CLAUDE.md # Claude Code 的项目上下文
|
||||
├── docs/
|
||||
│ ├── SPEC.md # 本规范文档
|
||||
│ ├── REQUIREMENTS.md # 架构决策
|
||||
│ └── SECURITY.md # 安全模型
|
||||
├── README.md # 用户文档
|
||||
├── package.json # Node.js 依赖
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── .mcp.json # MCP 服务器配置(参考)
|
||||
├── .gitignore
|
||||
│
|
||||
├── src/
|
||||
│ ├── index.ts # 编排器:状态、消息循环、agent 调用
|
||||
│ ├── channels/
|
||||
│ │ ├── registry.ts # 渠道工厂注册表
|
||||
│ │ └── index.ts # 渠道自行注册的 barrel 导入
|
||||
│ ├── ipc.ts # IPC 监听器与任务处理
|
||||
│ ├── router.ts # 消息格式化和出站路由
|
||||
│ ├── config.ts # 配置常量
|
||||
│ ├── types.ts # TypeScript 接口(包含 Channel)
|
||||
│ ├── logger.ts # Pino 日志器配置
|
||||
│ ├── db.ts # SQLite 数据库初始化与查询
|
||||
│ ├── group-queue.ts # 带全局并发限制的按群组队列
|
||||
│ ├── mount-security.ts # 容器挂载白名单验证
|
||||
│ ├── whatsapp-auth.ts # 独立的 WhatsApp 认证
|
||||
│ ├── task-scheduler.ts # 到期时运行定时任务
|
||||
│ └── container-runner.ts # 在容器中启动 agent
|
||||
│
|
||||
├── container/
|
||||
│ ├── Dockerfile # 容器镜像(以 'node' 用户运行,包含 Claude Code CLI)
|
||||
│ ├── build.sh # 容器镜像的构建脚本
|
||||
│ ├── agent-runner/ # 在容器内运行的代码
|
||||
│ │ ├── package.json
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ └── src/
|
||||
│ │ ├── index.ts # 入口点(查询循环、IPC 轮询、会话恢复)
|
||||
│ │ └── ipc-mcp-stdio.ts # 基于 stdio 的 MCP 服务器,用于宿主机通信
|
||||
│ └── skills/
|
||||
│ └── agent-browser.md # 浏览器自动化 skill
|
||||
│
|
||||
├── dist/ # 编译后的 JavaScript(gitignored)
|
||||
│
|
||||
├── .claude/
|
||||
│ └── skills/
|
||||
│ ├── setup/SKILL.md # /setup - 首次安装
|
||||
│ ├── customize/SKILL.md # /customize - 添加能力
|
||||
│ ├── debug/SKILL.md # /debug - 容器调试
|
||||
│ ├── add-telegram/SKILL.md # /add-telegram - Telegram 渠道
|
||||
│ ├── add-gmail/SKILL.md # /add-gmail - Gmail 集成
|
||||
│ ├── add-voice-transcription/ # /add-voice-transcription - Whisper
|
||||
│ ├── x-integration/SKILL.md # /x-integration - X/Twitter
|
||||
│ ├── convert-to-apple-container/ # /convert-to-apple-container - Apple Container 运行时
|
||||
│ └── add-parallel/SKILL.md # /add-parallel - 并行 agent
|
||||
│
|
||||
├── groups/
|
||||
│ ├── CLAUDE.md # 全局记忆(所有群组都读取此文件)
|
||||
│ ├── {channel}_main/ # 主控制渠道(例如 whatsapp_main/)
|
||||
│ │ ├── CLAUDE.md # 主渠道记忆
|
||||
│ │ └── logs/ # 任务执行日志
|
||||
│ └── {channel}_{group-name}/ # 按群组目录(注册时创建)
|
||||
│ ├── CLAUDE.md # 群组专属记忆
|
||||
│ ├── logs/ # 此群组的任务日志
|
||||
│ └── *.md # agent 创建的文件
|
||||
│
|
||||
├── store/ # 本地数据(gitignored)
|
||||
│ ├── auth/ # WhatsApp 认证状态
|
||||
│ └── messages.db # SQLite 数据库(messages、chats、scheduled_tasks、task_run_logs、registered_groups、sessions、router_state)
|
||||
│
|
||||
├── data/ # 应用状态(gitignored)
|
||||
│ ├── sessions/ # 按群组会话数据(.claude/ 目录,含 JSONL 对话记录)
|
||||
│ ├── env/env # .env 的副本,用于挂载到容器
|
||||
│ └── ipc/ # 容器 IPC(messages/、tasks/)
|
||||
│
|
||||
├── logs/ # 运行时日志(gitignored)
|
||||
│ ├── nanoclaw.log # 宿主机 stdout
|
||||
│ └── nanoclaw.error.log # 宿主机 stderr
|
||||
│ # 注意:每个容器的日志在 groups/{folder}/logs/container-*.log
|
||||
│
|
||||
└── launchd/
|
||||
└── com.nanoclaw.plist # macOS 服务配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
配置常量在 `src/config.ts` 中定义:
|
||||
|
||||
```typescript
|
||||
import path from 'path';
|
||||
|
||||
export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy';
|
||||
|
||||
// 路径必须是绝对路径(容器挂载需要)
|
||||
const PROJECT_ROOT = process.cwd();
|
||||
export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
|
||||
export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
|
||||
export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
|
||||
|
||||
// 容器配置
|
||||
export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest';
|
||||
export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '1800000', 10); // 默认30分钟
|
||||
export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30分钟——最后一次结果后保持容器存活
|
||||
export const MAX_CONCURRENT_CONTAINERS = Math.max(1, parseInt(process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5);
|
||||
|
||||
export const TRIGGER_PATTERN = new RegExp(`^@${ASSISTANT_NAME}\\b`, 'i');
|
||||
```
|
||||
|
||||
**注意:** 路径必须是绝对路径,容器卷挂载才能正常工作。
|
||||
|
||||
### 容器配置
|
||||
|
||||
群组可以通过 SQLite `registered_groups` 表中的 `containerConfig`(以 JSON 形式存储在 `container_config` 列中)挂载额外目录。注册示例:
|
||||
|
||||
```typescript
|
||||
setRegisteredGroup("1234567890@g.us", {
|
||||
name: "Dev Team",
|
||||
folder: "whatsapp_dev-team",
|
||||
trigger: "@Andy",
|
||||
added_at: new Date().toISOString(),
|
||||
containerConfig: {
|
||||
additionalMounts: [
|
||||
{
|
||||
hostPath: "~/projects/webapp",
|
||||
containerPath: "webapp",
|
||||
readonly: false,
|
||||
},
|
||||
],
|
||||
timeout: 600000,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
目录命名遵循 `{channel}_{group-name}` 约定(例如 `whatsapp_family-chat`、`telegram_dev-team`)。主群组在注册时会设置 `isMain: true`。
|
||||
|
||||
额外挂载在容器内显示为 `/workspace/extra/{containerPath}`。
|
||||
|
||||
**挂载语法说明:** 读写挂载使用 `-v host:container`,但只读挂载需要使用 `--mount "type=bind,source=...,target=...,readonly"`(`:ro` 后缀可能不是所有运行时都支持)。
|
||||
|
||||
### Claude 认证
|
||||
|
||||
在项目根目录的 `.env` 文件中配置认证。有两种选择:
|
||||
|
||||
**选项1:Claude 订阅(OAuth token)**
|
||||
```bash
|
||||
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
|
||||
```
|
||||
如果您已登录 Claude Code,可以从 `~/.claude/.credentials.json` 中提取 token。
|
||||
|
||||
**选项2:按用量付费的 API Key**
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=sk-ant-api03-...
|
||||
```
|
||||
|
||||
只有认证变量(`CLAUDE_CODE_OAUTH_TOKEN` 和 `ANTHROPIC_API_KEY`)从 `.env` 中提取并写入 `data/env/env`,然后挂载到容器中的 `/workspace/env-dir/env`,由入口点脚本加载。这确保 `.env` 中的其他环境变量不会暴露给 agent。此变通方案是必要的,因为某些容器运行时在使用 `-i`(带管道 stdin 的交互模式)时会丢失 `-e` 环境变量。
|
||||
|
||||
### 更改助手名称
|
||||
|
||||
设置 `ASSISTANT_NAME` 环境变量:
|
||||
|
||||
```bash
|
||||
ASSISTANT_NAME=Bot pnpm start
|
||||
```
|
||||
|
||||
或编辑 `src/config.ts` 中的默认值。这会更改:
|
||||
- 触发模式(消息必须以 `@YourName` 开头)
|
||||
- 响应前缀(自动添加 `YourName:`)
|
||||
|
||||
### launchd 中的占位符值
|
||||
|
||||
包含 `{{PLACEHOLDER}}` 值的文件需要进行配置:
|
||||
- `{{PROJECT_ROOT}}` - nanoclaw 安装的绝对路径
|
||||
- `{{NODE_PATH}}` - node 二进制文件的路径(通过 `which node` 检测)
|
||||
- `{{HOME}}` - 用户的主目录
|
||||
|
||||
---
|
||||
|
||||
## 记忆系统
|
||||
|
||||
NanoClaw 使用基于 CLAUDE.md 文件的层级记忆系统。
|
||||
|
||||
### 记忆层级
|
||||
|
||||
| 层级 | 位置 | 读取者 | 写入者 | 用途 |
|
||||
|-------|----------|---------|------------|---------|
|
||||
| **全局** | `groups/CLAUDE.md` | 所有群组 | 仅主群组 | 在所有对话之间共享的偏好、事实、上下文 |
|
||||
| **群组** | `groups/{name}/CLAUDE.md` | 该群组 | 该群组 | 群组专属上下文、对话记忆 |
|
||||
| **文件** | `groups/{name}/*.md` | 该群组 | 该群组 | 对话过程中创建的笔记、研究、文档 |
|
||||
|
||||
### 记忆如何运作
|
||||
|
||||
1. **Agent 上下文加载**
|
||||
- Agent 运行在 `groups/{group-name}/` 作为 `cwd`
|
||||
- Claude Agent SDK 使用 `settingSources: ['project']` 自动加载:
|
||||
- `../CLAUDE.md`(上级目录 = 全局记忆)
|
||||
- `./CLAUDE.md`(当前目录 = 群组记忆)
|
||||
|
||||
2. **写入记忆**
|
||||
- 当用户说"记住这个",agent 写入 `./CLAUDE.md`
|
||||
- 当用户说"全局记住这个"(仅主渠道),agent 写入 `../CLAUDE.md`
|
||||
- Agent 可以在群组目录中创建 `notes.md`、`research.md` 等文件
|
||||
|
||||
3. **主渠道权限**
|
||||
- 只有"主"群组(自我聊天)可以向全局记忆写入
|
||||
- 主群组可以管理已注册的群组并为任何群组安排定时任务
|
||||
- 主群组可以为任何群组配置额外的目录挂载
|
||||
- 所有群组都有 Bash 访问权限(安全,因为在容器内运行)
|
||||
|
||||
---
|
||||
|
||||
## Session 管理
|
||||
|
||||
Session 实现了对话连续性——Claude 会记住你们之前谈过的内容。
|
||||
|
||||
### Session 如何运作
|
||||
|
||||
1. 每个群组在 SQLite 中有一个 session ID(`sessions` 表,按 `group_folder` 索引)
|
||||
2. Session ID 传递给 Claude Agent SDK 的 `resume` 选项
|
||||
3. Claude 以完整上下文继续对话
|
||||
4. Session 对话记录以 JSONL 文件形式存储在 `data/sessions/{group}/.claude/`
|
||||
|
||||
---
|
||||
|
||||
## 消息流转
|
||||
|
||||
### 入站消息流程
|
||||
|
||||
```
|
||||
1. 用户通过任何已连接的渠道发送消息
|
||||
│
|
||||
▼
|
||||
2. 渠道接收消息(例如 WhatsApp 使用 Baileys,Telegram 使用 Bot API)
|
||||
│
|
||||
▼
|
||||
3. 消息存入 SQLite(store/messages.db)
|
||||
│
|
||||
▼
|
||||
4. 消息循环轮询 SQLite(每2秒)
|
||||
│
|
||||
▼
|
||||
5. 路由检查:
|
||||
├── chat_jid 是否在已注册群组中(SQLite)?→ 否:忽略
|
||||
└── 消息是否匹配触发模式?→ 否:存储但不处理
|
||||
│
|
||||
▼
|
||||
6. 路由追上对话:
|
||||
├── 获取自上次 agent 交互以来的所有消息
|
||||
├── 使用时间戳和发送者名称格式化
|
||||
└── 构建包含完整对话上下文的 prompt
|
||||
│
|
||||
▼
|
||||
7. 路由调用 Claude Agent SDK:
|
||||
├── cwd: groups/{group-name}/
|
||||
├── prompt: 对话历史 + 当前消息
|
||||
├── resume: session_id(用于连续性)
|
||||
└── mcpServers: nanoclaw(调度器)
|
||||
│
|
||||
▼
|
||||
8. Claude 处理消息:
|
||||
├── 读取 CLAUDE.md 文件获取上下文
|
||||
└── 按需使用工具(搜索、邮件等)
|
||||
│
|
||||
▼
|
||||
9. 路由在响应前添加助手名称前缀,并通过所属渠道发送
|
||||
│
|
||||
▼
|
||||
10. 路由更新最后 agent 时间戳并保存 session ID
|
||||
```
|
||||
|
||||
### 触发词匹配
|
||||
|
||||
消息必须以触发模式开头(默认:`@Andy`):
|
||||
- `@Andy what's the weather?` → ✅ 触发 Claude
|
||||
- `@andy help me` → ✅ 触发(不区分大小写)
|
||||
- `Hey @Andy` → ❌ 忽略(触发词不在开头)
|
||||
- `What's up?` → ❌ 忽略(无触发词)
|
||||
|
||||
### 对话追上
|
||||
|
||||
当触发消息到达时,agent 会收到自上次在该聊天中交互以来的所有消息。每条消息都带有时间戳和发送者名称:
|
||||
|
||||
```
|
||||
[Jan 31 2:32 PM] John: hey everyone, should we do pizza tonight?
|
||||
[Jan 31 2:33 PM] Sarah: sounds good to me
|
||||
[Jan 31 2:35 PM] John: @Andy what toppings do you recommend?
|
||||
```
|
||||
|
||||
这使得 agent 能够理解对话上下文,即使它没有在每条消息中被提及。
|
||||
|
||||
---
|
||||
|
||||
## 命令
|
||||
|
||||
### 任何群组中可用的命令
|
||||
|
||||
| 命令 | 示例 | 效果 |
|
||||
|---------|---------|--------|
|
||||
| `@Assistant [message]` | `@Andy what's the weather?` | 与 Claude 对话 |
|
||||
|
||||
### 仅主渠道可用的命令
|
||||
|
||||
| 命令 | 示例 | 效果 |
|
||||
|---------|---------|--------|
|
||||
| `@Assistant add group "Name"` | `@Andy add group "Family Chat"` | 注册新群组 |
|
||||
| `@Assistant remove group "Name"` | `@Andy remove group "Work Team"` | 取消注册群组 |
|
||||
| `@Assistant list groups` | `@Andy list groups` | 显示已注册群组 |
|
||||
| `@Assistant remember [fact]` | `@Andy remember I prefer dark mode` | 添加到全局记忆 |
|
||||
|
||||
---
|
||||
|
||||
## 定时任务
|
||||
|
||||
NanoClaw 内置调度器,将任务作为完整 agent 在其群组上下文中运行。
|
||||
|
||||
### 调度如何运作
|
||||
|
||||
1. **群组上下文**:在群组中创建的任务使用该群组的工作目录和记忆运行
|
||||
2. **完整的 Agent 能力**:定时任务可以访问所有工具(WebSearch、文件操作等)
|
||||
3. **可选的消息发送**:任务可以使用 `send_message` 工具向其群组发送消息,也可以静默完成
|
||||
4. **主渠道权限**:主渠道可以为任何群组安排任务并查看所有任务
|
||||
|
||||
### 调度类型
|
||||
|
||||
| 类型 | 值格式 | 示例 |
|
||||
|------|--------------|---------|
|
||||
| `cron` | Cron 表达式 | `0 9 * * 1`(每周一上午9点) |
|
||||
| `interval` | 毫秒 | `3600000`(每小时) |
|
||||
| `once` | ISO 时间戳 | `2024-12-25T09:00:00Z` |
|
||||
|
||||
### 创建任务
|
||||
|
||||
```
|
||||
User: @Andy remind me every Monday at 9am to review the weekly metrics
|
||||
|
||||
Claude: [调用 mcp__nanoclaw__schedule_task]
|
||||
{
|
||||
"prompt": "Send a reminder to review weekly metrics. Be encouraging!",
|
||||
"schedule_type": "cron",
|
||||
"schedule_value": "0 9 * * 1"
|
||||
}
|
||||
|
||||
Claude: Done! I'll remind you every Monday at 9am.
|
||||
```
|
||||
|
||||
### 一次性任务
|
||||
|
||||
```
|
||||
User: @Andy at 5pm today, send me a summary of today's emails
|
||||
|
||||
Claude: [调用 mcp__nanoclaw__schedule_task]
|
||||
{
|
||||
"prompt": "Search for today's emails, summarize the important ones, and send the summary to the group.",
|
||||
"schedule_type": "once",
|
||||
"schedule_value": "2024-01-31T17:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 管理任务
|
||||
|
||||
在任何群组中:
|
||||
- `@Andy list my scheduled tasks` - 查看此群组的任务
|
||||
- `@Andy pause task [id]` - 暂停任务
|
||||
- `@Andy resume task [id]` - 恢复暂停的任务
|
||||
- `@Andy cancel task [id]` - 删除任务
|
||||
|
||||
在主渠道中:
|
||||
- `@Andy list all tasks` - 查看所有群组的任务
|
||||
- `@Andy schedule task for "Family Chat": [prompt]` - 为其他群组安排任务
|
||||
|
||||
---
|
||||
|
||||
## MCP 服务器
|
||||
|
||||
### NanoClaw MCP(内置)
|
||||
|
||||
`nanoclaw` MCP 服务器在每次 agent 调用时动态创建,带有当前群组的上下文。
|
||||
|
||||
**可用工具:**
|
||||
| 工具 | 用途 |
|
||||
|------|---------|
|
||||
| `schedule_task` | 安排定期或一次性任务 |
|
||||
| `list_tasks` | 显示任务(本群组的任务,主群组则显示全部) |
|
||||
| `get_task` | 获取任务详情和运行历史 |
|
||||
| `update_task` | 修改任务的 prompt 或调度 |
|
||||
| `pause_task` | 暂停任务 |
|
||||
| `resume_task` | 恢复暂停的任务 |
|
||||
| `cancel_task` | 删除任务 |
|
||||
| `send_message` | 通过群组的渠道发送消息 |
|
||||
|
||||
---
|
||||
|
||||
## 部署
|
||||
|
||||
NanoClaw 作为单个 macOS launchd 服务运行。
|
||||
|
||||
### 启动序列
|
||||
|
||||
NanoClaw 启动时会:
|
||||
1. **确保容器运行时正在运行**——如需要自动启动;终止前次运行遗留的 NanoClaw 孤儿容器
|
||||
2. 初始化 SQLite 数据库(如果存在 JSON 文件则从中迁移)
|
||||
3. 从 SQLite 加载状态(已注册群组、sessions、路由状态)
|
||||
4. **连接渠道**——遍历已注册渠道,实例化有凭证的渠道,对每个调用 `connect()`
|
||||
5. 一旦至少有一个渠道连接:
|
||||
- 启动调度器循环
|
||||
- 启动用于容器消息的 IPC 监听器
|
||||
- 设置按群组队列及 `processGroupMessages`
|
||||
- 恢复关闭前未处理的消息
|
||||
- 启动消息轮询循环
|
||||
|
||||
### 服务:com.nanoclaw
|
||||
|
||||
**launchd/com.nanoclaw.plist:**
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.nanoclaw</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{NODE_PATH}}</string>
|
||||
<string>{{PROJECT_ROOT}}/dist/index.js</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{{PROJECT_ROOT}}</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>{{HOME}}/.local/bin:/usr/local/bin:/usr/bin:/bin</string>
|
||||
<key>HOME</key>
|
||||
<string>{{HOME}}</string>
|
||||
<key>ASSISTANT_NAME</key>
|
||||
<string>Andy</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{PROJECT_ROOT}}/logs/nanoclaw.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{PROJECT_ROOT}}/logs/nanoclaw.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### 管理服务
|
||||
|
||||
```bash
|
||||
# 安装服务
|
||||
cp launchd/com.nanoclaw.plist ~/Library/LaunchAgents/
|
||||
|
||||
# 启动服务
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
|
||||
# 停止服务
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
|
||||
# 检查状态
|
||||
launchctl list | grep nanoclaw
|
||||
|
||||
# 查看日志
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全考量
|
||||
|
||||
### 容器隔离
|
||||
|
||||
所有 agent 在容器(轻量级 Linux VM)内运行,提供:
|
||||
- **文件系统隔离**:Agent 只能访问已挂载的目录
|
||||
- **安全的 Bash 访问**:命令在容器内运行,而不是在您的 Mac 上
|
||||
- **网络隔离**:可按容器配置(如需要)
|
||||
- **进程隔离**:容器进程无法影响宿主机
|
||||
- **非 root 用户**:容器以非特权 `node` 用户(uid 1000)运行
|
||||
|
||||
### Prompt 注入风险
|
||||
|
||||
WhatsApp 消息可能包含试图操纵 Claude 行为的恶意指令。
|
||||
|
||||
**缓解措施:**
|
||||
- 容器隔离限制爆炸半径
|
||||
- 仅处理已注册的群组
|
||||
- 需要触发词(减少误处理)
|
||||
- Agent 只能访问其群组挂载的目录
|
||||
- 主群组可以按群组配置额外目录
|
||||
- Claude 内置的安全训练
|
||||
|
||||
**建议:**
|
||||
- 仅注册受信任的群组
|
||||
- 仔细审查额外的目录挂载
|
||||
- 定期审查定时任务
|
||||
- 监控日志中的异常活动
|
||||
|
||||
### 凭证存储
|
||||
|
||||
| 凭证 | 存储位置 | 说明 |
|
||||
|------------|------------------|-------|
|
||||
| Claude CLI Auth | data/sessions/{group}/.claude/ | 按群组隔离,挂载到 /home/node/.claude/ |
|
||||
| WhatsApp Session | store/auth/ | 自动创建,持续约20天 |
|
||||
|
||||
### 文件权限
|
||||
|
||||
groups/ 目录包含个人记忆,应加以保护:
|
||||
```bash
|
||||
chmod 700 groups/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|-------|-------|----------|
|
||||
| 消息无响应 | 服务未运行 | 检查 `launchctl list | grep nanoclaw` |
|
||||
| "Claude Code process exited with code 1" | 容器运行时启动失败 | 检查日志;NanoClaw 会自动启动容器运行时,但可能失败 |
|
||||
| "Claude Code process exited with code 1" | Session 挂载路径错误 | 确保挂载到 `/home/node/.claude/` 而非 `/root/.claude/` |
|
||||
| Session 无法继续 | Session ID 未保存 | 检查 SQLite:`sqlite3 store/messages.db "SELECT * FROM sessions"` |
|
||||
| Session 无法继续 | 挂载路径不匹配 | 容器用户是 `node`,HOME=/home/node;sessions 必须位于 `/home/node/.claude/` |
|
||||
| "QR code expired" | WhatsApp session 过期 | 删除 store/auth/ 并重启 |
|
||||
| "No groups registered" | 尚未添加群组 | 在主渠道中使用 `@Andy add group "Name"` |
|
||||
|
||||
### 日志位置
|
||||
|
||||
- `logs/nanoclaw.log` - stdout
|
||||
- `logs/nanoclaw.error.log` - stderr
|
||||
|
||||
### 调试模式
|
||||
|
||||
手动运行以获取详细输出:
|
||||
```bash
|
||||
pnpm run dev
|
||||
# 或
|
||||
node dist/index.js
|
||||
```
|
||||
Reference in New Issue
Block a user