Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ API_SECRET= # If set, listens on 0.0.0.0 with Bearer auth; if empty, local
# Default timezone for recurring cron tasks (IANA format)
# SCHEDULE_TIMEZONE=Asia/Shanghai

# =============================================================================
# Instance Identity & Cluster Discovery
# =============================================================================

# MetaBot generates ~/.metabot/identity.json on first start and reuses it on reinstall.
# Override these only when joining a managed internal cluster or restoring identity manually.
# METABOT_INSTANCE_ID=
# METABOT_INSTANCE_NAME=
# METABOT_CLUSTER_ID=
# METABOT_CLUSTER_URL= # Bootstrap URL; also added as a peer automatically
# METABOT_CLUSTER_SECRET= # Optional token for METABOT_CLUSTER_URL
# METABOT_DISCOVERY_MODE=auto # auto | static | standalone | off
# METABOT_MEMORY_NAMESPACE= # instance fallback, default: /instances/<instanceId>
# METABOT_BOT_MEMORY_NAMESPACE= # single-bot stable namespace, default: /bots/default
# METABOT_MEMORY_PROJECT= # single-bot project name, derives /projects/<slug>
# METABOT_MEMORY_WRITE_NAMESPACES= # extra comma-separated writable namespaces for instance token

# =============================================================================
# MetaMemory (embedded document server)
# =============================================================================
Expand All @@ -56,6 +73,7 @@ META_MEMORY_URL=http://localhost:8100
# MEMORY_SECRET is the legacy token (gets admin access for backward compat)
# MEMORY_ADMIN_TOKEN= # Admin token (full read/write)
# MEMORY_TOKEN= # Reader token (shared folders only)
# MEMORY_INSTANCE_TOKEN= # Scoped token: write own namespace, read shared content

# =============================================================================
# Feishu Service App (wiki sync & doc reader)
Expand Down Expand Up @@ -92,6 +110,19 @@ META_MEMORY_URL=http://localhost:8100
# How often to poll peers for bot discovery (default: 30000ms = 30s)
# METABOT_PEER_POLL_INTERVAL_MS=30000

# Persistent cache for peer artifacts. Skill Hub uses this to keep peer skills
# discoverable/installable even when the owner MetaBot is temporarily offline.
# METABOT_PEER_CACHE_PATH=./data/peer-cache.json

# Set to 'false' to cache only peer skill summaries instead of SKILL.md content.
# METABOT_PEER_SKILL_CACHE_CONTENTS=true

# Set to 'false' to disable read-only peer MetaMemory mirroring.
# METABOT_PEER_MEMORY_CACHE_ENABLED=true

# Maximum peer memory documents mirrored per peer poll.
# METABOT_PEER_MEMORY_CACHE_LIMIT=200

# =============================================================================
# Voice API (/api/voice — STT + Agent + optional TTS)
# =============================================================================
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Slim summary only — see [docs/internal/architecture.md](docs/internal/architec
- **Single-bot mode** (default): `.env` with `FEISHU_APP_ID` + `FEISHU_APP_SECRET` (see `.env.example`).
- **Multi-bot mode**: `BOTS_CONFIG=./bots.json` runs multiple bots in one process (see `bots.example.json`). When set, the `FEISHU_APP_*` env vars are ignored.
- **PersistentClaudeExecutor** (opt-in): `METABOT_PERSISTENT_EXECUTOR=true` keeps one long-lived `query()` per `chatId` so subagents / Agent Teams / `/background` / `/goal` survive across turns. Per-bot override via `persistentExecutor` in `bots.json`. Observability at `GET /api/executors`.
- **MetaMemory**: external FastAPI+SQLite server at `META_MEMORY_URL` (default `http://localhost:8100`). Claude reads/writes via the `metamemory` skill; `/memory list|search|status` query directly.
- **MetaMemory**: embedded SQLite document server at `META_MEMORY_URL` (default `http://localhost:8100`). Claude reads/writes via the `metamemory` skill; `/memory list|search|status` query directly. In federated/LAN mode, each instance has a stable fallback `METABOT_MEMORY_NAMESPACE=/instances/<instanceId>`, while bots can use stable project namespaces via `memoryProject` (`/projects/<slug>`) or explicit `memoryNamespace`; `MEMORY_INSTANCE_TOKEN` can write the instance fallback plus configured bot/project namespaces while reading shared folders.

## Branching Strategy

Expand Down
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,14 @@ MetaBot 支持 4 种方式与你的 Agent 团队交互:
| **常驻会话与目标循环** | 每个会话一个常驻 Claude 进程 — `/goal` 让 Agent 在多轮之间持续自驱直到目标达成;团队成员和后台任务跨轮存活 |
| **Agent 团队(运行时)** | 主导 Agent 并行派遣专家队友,互相路由任务、汇总结果 —— 全部在一个飞书会话中完成 |
| **CC 原生调度** | 直接用 Claude Code 内置的 `CronCreate` / `/loop` —— 即开即用,会话内最简单 |
| **MetaMemory** | 内嵌 SQLite 知识库,全文搜索,Web UI,变更自动同步到飞书知识库 |
| **MetaMemory** | 内嵌 SQLite 知识库,全文搜索,Web UI,实例 namespace ACL,变更自动同步到飞书知识库 |
| **IM Bridge** | 飞书、Telegram、微信(含手机端)对话任意 Agent,流式卡片 + 工具调用追踪 |
| **Agent 总线** | Agent 通过 `mb talk` 互相对话,运行时创建/删除 Bot |
| **MetaSchedule(可选)** | 跨重启的服务端定时调度器,Cron + 一次性延迟,HTTP API + `mb schedule` CLI。默认不装,按需 `cp src/skills/metaschedule/SKILL.md` 启用 |
| **MetaSkill(可选)** | Agent 工厂。`/metaskill` 一键生成可迁移的 Agent 团队。默认不装,按需 `cp src/skills/metaskill/` 启用 |
| **飞书 Lark CLI** | 200+ 命令覆盖文档、消息、日历、任务等 11 大业务域,19 个 AI Agent Skills |
| **Skill Hub** | 跨实例技能共享注册中心。`mb skills` 发布、发现、安装技能,FTS5 全文搜索 |
| **Peers 联邦** | 跨实例 Bot 发现和任务路由,`mb talk alice/backend-bot` 自动路由 |
| **Skill Hub** | 跨实例技能共享注册中心。Skill 带 owner/hash/visibility 元数据,`mb skills` 发布、发现、安装,FTS5 全文搜索 |
| **Peers 联邦** | 跨实例 Bot/Skill 发现和任务路由,支持 `METABOT_CLUSTER_URL` 内网 bootstrap,`mb talk alice/backend-bot` 自动路由 |
| **语音助手** | Jarvis 模式 — AirPods 说 "Hey Siri, Jarvis" 语音控制 Agent |

## 快速开始
Expand Down Expand Up @@ -236,6 +236,27 @@ MetaBot 支持 4 种方式与你的 Agent 团队交互:
搜索一下 MetaMemory 里有没有关于 API 设计规范的文档。
```

### 内网联邦 — MetaMemory / Skill Hub

每个 MetaBot 实例首次启动会生成稳定身份,作为兜底 memory namespace;每个 bot 还可以绑定稳定项目 namespace:

```text
~/.metabot/identity.json
/instances/<instanceId>/...
/projects/<project>/...
/bots/<botName>/...
```

内网里如果有一个稳定 MetaBot/cluster 地址,只需要配置:

```env
METABOT_CLUSTER_URL=http://metabot.internal:9100
METABOT_CLUSTER_SECRET=optional-token
MEMORY_INSTANCE_TOKEN=instance-scoped-token
```

`METABOT_CLUSTER_URL` 会自动作为 peer 加入,当前实例就能发现对方的 Bot 和 Skill。`MEMORY_INSTANCE_TOKEN` 可写当前实例兜底 namespace 以及已配置的 bot/project namespace,读共享内容;管理员 token 仍保留完整访问权限。

### 定时任务(Claude Code 原生)

直接用 CC 内置的 `CronCreate` 和 `/loop`,会话内即开即用:
Expand Down Expand Up @@ -378,6 +399,17 @@ MetaBot 支持 4 种方式与你的 Agent 团队交互:
| `MEMORY_PORT` | 8100 | MetaMemory 端口 |
| `MEMORY_ADMIN_TOKEN` | — | 管理员 Token(完整访问) |
| `MEMORY_TOKEN` | — | 读者 Token(仅共享文件夹) |
| `MEMORY_INSTANCE_TOKEN` | — | 实例级 Token(可写实例兜底 namespace 和配置的 bot/project namespace,可读共享内容) |
| `METABOT_INSTANCE_ID` | 自动生成 | 当前 MetaBot 实例 ID,用于联邦发现和 memory namespace |
| `METABOT_CLUSTER_URL` | — | 可选的内网 cluster/registry 引导地址,当前会自动作为 peer 加入 |
| `METABOT_CLUSTER_SECRET` | — | `METABOT_CLUSTER_URL` 的可选 Token |
| `METABOT_MEMORY_NAMESPACE` | `/instances/<instanceId>` | 当前实例兜底 memory namespace |
| `METABOT_BOT_MEMORY_NAMESPACE` | `/bots/default` | 单 Bot 稳定写入 namespace 覆盖 |
| `METABOT_MEMORY_PROJECT` | — | 单 Bot 项目名,推导 `/projects/<slug>` |
| `METABOT_PEER_CACHE_PATH` | `./data/peer-cache.json` | Peer artifact 持久缓存;Skill Hub 可在 owner 离线时继续发现/安装缓存 skill |
| `METABOT_PEER_SKILL_CACHE_CONTENTS` | true | 是否缓存 peer skill 的完整 `SKILL.md` 内容 |
| `METABOT_PEER_MEMORY_CACHE_ENABLED` | true | 是否将 peer MetaMemory 文档镜像成本地只读 cache |
| `METABOT_PEER_MEMORY_CACHE_LIMIT` | 200 | 每次 peer 拉取最多镜像的 memory 文档数 |
| `WIKI_SYNC_ENABLED` | true | 启用 MetaMemory→飞书知识库同步 |
| `WIKI_SPACE_NAME` | MetaMemory | 飞书知识库空间名称 |
| `WIKI_AUTO_SYNC` | true | MetaMemory 变更时自动同步 |
Expand Down Expand Up @@ -422,7 +454,8 @@ MetaBot 以 `bypassPermissions` 模式运行 Claude Code — 无交互式确认
- 通过飞书/Telegram/微信平台设置控制访问
- 用 `maxBudgetUsd` 限制单次花费
- `API_SECRET` 保护 API 服务器和 MetaMemory
- MetaMemory 支持文件夹级 ACL(Admin/Reader 双角色)
- MetaMemory 支持 Admin/Reader/Instance scoped token;实例 token 默认只能写自己的 namespace
- Skill Hub 记录 owner instance、visibility 和 content hash,便于后续来源校验和更新检测

</details>

Expand Down Expand Up @@ -450,6 +483,7 @@ MetaBot 以 `bypassPermissions` 模式运行 Claude Code — 无交互式确认
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/api/health` | 健康检查 |
| `GET` | `/api/manifest` | 当前实例身份、能力和联邦发现 manifest |
| `GET` | `/api/bots` | 列出 Bot(本地 + Peer) |
| `POST` | `/api/bots` | 运行时创建 Bot |
| `DELETE` | `/api/bots/:name` | 删除 Bot |
Expand Down Expand Up @@ -479,7 +513,7 @@ MetaBot 以 `bypassPermissions` 模式运行 Claude Code — 无交互式确认

```bash
# MetaBot 管理
metabot update # 拉取最新代码,重新构建,重启
metabot update # 拉取最新代码,重新构建,更新 skills,重启
metabot start / stop / restart # PM2 管理
metabot logs # 查看实时日志

Expand Down Expand Up @@ -513,6 +547,8 @@ mb skills install <skill> <bot> # 安装技能到 Bot
mb voice "你好世界" --play
```

`metabot update` 会自动更新已安装的 `lark-cli` 和飞书/Lark skills,并同步到 bot 工作目录;新机器首次安装时仍由安装器引导是否启用飞书 skills。

CLI 支持连接远程 MetaBot/MetaMemory 服务器,在 `~/.metabot/.env` 配置 `METABOT_URL` 和 `META_MEMORY_URL` 即可。

</details>
Expand Down
84 changes: 79 additions & 5 deletions bin/metabot
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ fi
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
BOLD='\033[1m'
NC='\033[0m'

info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }

cmd_update() {
Expand Down Expand Up @@ -77,9 +79,31 @@ cmd_update() {
# Update skills in ~/.claude/skills and ~/.codex/skills
SKILLS_DIR="$HOME/.claude/skills"
CODEX_SKILLS_DIR="$HOME/.codex/skills"
AGENTS_SKILLS_DIR="$HOME/.agents/skills"
mkdir -p "$SKILLS_DIR" "$CODEX_SKILLS_DIR"
info "Updating skills..."
local src=""
local -a lark_skill_names=(
lark-base
lark-calendar
lark-contact
lark-doc
lark-drive
lark-event
lark-im
lark-mail
lark-minutes
lark-openapi-explorer
lark-shared
lark-sheets
lark-skill-maker
lark-task
lark-vc
lark-whiteboard
lark-wiki
lark-workflow-meeting-summary
lark-workflow-standup-report
)
# metaskill / metaschedule are opt-in (not in the default list); CC native
# CronCreate / /loop already cover ad-hoc scheduling. Sources still ship in
# src/skills/* — users who want them can copy manually.
Expand Down Expand Up @@ -115,11 +139,53 @@ cmd_update() {
rm -rf "$CODEX_SKILLS_DIR/metaskill"
fi

# Update lark-cli skills only if previously installed (opt-in via install.sh)
# Update lark-cli and its AI Agent skills only if previously installed.
# New installs still opt in via install.sh; updates keep existing setups fresh.
local has_lark_skills=false
if [[ -d "$SKILLS_DIR/lark-doc" ]]; then
local skill_root
for skill_root in "$AGENTS_SKILLS_DIR" "$SKILLS_DIR" "$CODEX_SKILLS_DIR"; do
if [[ -d "$skill_root/lark-doc" ]]; then
has_lark_skills=true
break
fi
done
if command -v lark-cli &>/dev/null; then
info "Updating lark-cli..."
if npm install -g @larksuite/cli@latest 2>/dev/null || npm install -g --prefix "$HOME/.local" @larksuite/cli@latest 2>/dev/null; then
success "lark-cli updated ($(lark-cli --version 2>/dev/null || echo 'version unknown'))"
else
warn "lark-cli update failed; MetaBot update will continue"
fi
has_lark_skills=true
fi
if [[ "$has_lark_skills" == "true" ]]; then
info "Updating lark-cli AI Agent skills..."
if npx skills add larksuite/cli --all -y -g 2>/dev/null; then
success "lark-cli AI Agent skills updated"
else
warn "lark-cli skills update failed; keeping existing installed copies"
fi
# Mirror lark-cli skills into Claude/Codex skill roots. Recent lark-cli
# installs use ~/.agents/skills, while older installs may use ~/.claude.
local lark_skill
for lark_skill in "${lark_skill_names[@]}"; do
local lark_src=""
for skill_root in "$AGENTS_SKILLS_DIR" "$SKILLS_DIR" "$CODEX_SKILLS_DIR"; do
if [[ -d "$skill_root/$lark_skill" ]]; then
lark_src="$skill_root/$lark_skill"
break
fi
done
if [[ -n "$lark_src" ]]; then
for dst_root in "$SKILLS_DIR" "$CODEX_SKILLS_DIR"; do
mkdir -p "$dst_root/$lark_skill"
if [[ "$lark_src" != "$dst_root/$lark_skill" ]]; then
cp -r "$lark_src/." "$dst_root/$lark_skill/"
fi
done
fi
done
fi
success "Skills updated"

# Update workspace skills if bots.json exists
Expand Down Expand Up @@ -155,11 +221,19 @@ cmd_update() {
done
# Copy lark-cli skills if previously installed
if [[ "$has_lark_skills" == "true" ]]; then
for lark_skill in lark-base lark-calendar lark-contact lark-doc lark-drive lark-event lark-im lark-mail lark-minutes lark-openapi-explorer lark-shared lark-sheets lark-skill-maker lark-task lark-vc lark-whiteboard lark-wiki lark-workflow-meeting-summary lark-workflow-standup-report; do
if [[ -d "$SKILLS_DIR/$lark_skill" ]]; then
local lark_skill
for lark_skill in "${lark_skill_names[@]}"; do
local lark_src=""
for skill_root in "$AGENTS_SKILLS_DIR" "$SKILLS_DIR" "$CODEX_SKILLS_DIR"; do
if [[ -d "$skill_root/$lark_skill" ]]; then
lark_src="$skill_root/$lark_skill"
break
fi
done
if [[ -n "$lark_src" ]]; then
for dst_root in "$ws_skills_dir" "$ws_codex_skills_dir"; do
mkdir -p "$dst_root/$lark_skill"
cp -r "$SKILLS_DIR/$lark_skill/." "$dst_root/$lark_skill/"
cp -r "$lark_src/." "$dst_root/$lark_skill/"
done
fi
done
Expand Down
32 changes: 31 additions & 1 deletion bin/mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,24 @@ _find_env() {
METABOT_ENV="$(_find_env)"
if [[ -n "$METABOT_ENV" && -f "$METABOT_ENV" ]]; then
_admin_token=$(sed -n 's/^MEMORY_ADMIN_TOKEN=//p' "$METABOT_ENV" 2>/dev/null || true)
_instance_token=$(sed -n 's/^MEMORY_INSTANCE_TOKEN=//p' "$METABOT_ENV" 2>/dev/null || true)
_reader_token=$(sed -n 's/^MEMORY_TOKEN=//p' "$METABOT_ENV" 2>/dev/null || true)
_memory_secret=$(sed -n 's/^MEMORY_SECRET=//p' "$METABOT_ENV" 2>/dev/null || true)
_secret=$(sed -n 's/^API_SECRET=//p' "$METABOT_ENV" 2>/dev/null || true)
_memory_url=$(sed -n 's/^META_MEMORY_URL=//p' "$METABOT_ENV" 2>/dev/null || true)
_metabot_url=$(sed -n 's/^METABOT_URL=//p' "$METABOT_ENV" 2>/dev/null || true)
[[ -z "$_memory_url" ]] && _memory_url=$(sed -n 's/^MEMORY_SERVER_URL=//p' "$METABOT_ENV" 2>/dev/null || true)
fi
META_MEMORY_URL="${META_MEMORY_URL:-${_memory_url:-http://localhost:8100}}"
METABOT_URL="${METABOT_URL:-${_metabot_url:-http://localhost:9100}}"

# Token priority: MEMORY_ADMIN_TOKEN > MEMORY_TOKEN > MEMORY_SECRET > API_SECRET
# Token priority: MEMORY_ADMIN_TOKEN > MEMORY_INSTANCE_TOKEN > MEMORY_TOKEN > MEMORY_SECRET > API_SECRET
# Always resolve from .env; ignore pre-set MEMORY_AUTH (may be stale/wrong token)
{
_token="${MEMORY_ADMIN_TOKEN:-${_admin_token:-}}"
if [[ -z "$_token" ]]; then
_token="${MEMORY_INSTANCE_TOKEN:-${_instance_token:-}}"
fi
if [[ -z "$_token" ]]; then
_token="${MEMORY_TOKEN:-${_reader_token:-}}"
fi
Expand Down Expand Up @@ -57,6 +63,15 @@ _curl() {
fi
}

_metabot_curl() {
local token="${API_SECRET:-${_secret:-}}"
if [[ -n "$token" ]]; then
curl -s -H "Authorization: Bearer $token" "$@"
else
curl -s "$@"
fi
}

# Build JSON safely using python3 to handle escaping
_jsonify() {
python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))" 2>/dev/null
Expand All @@ -70,9 +85,22 @@ case "$cmd" in
query=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(' '.join(sys.argv[1:])))" "$@" 2>/dev/null || echo "$*")
_curl "$META_MEMORY_URL/api/search?q=$query" | _json
;;
peer-search|ps)
query=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(' '.join(sys.argv[1:])))" "$@" 2>/dev/null || echo "$*")
_metabot_curl "$METABOT_URL/api/peer-memory/search?q=$query" | _json
;;
get|g)
_curl "$META_MEMORY_URL/api/documents/$1" | _json
;;
peer-get|pg)
if [[ $# -lt 2 ]]; then
echo "Usage: mm peer-get <peer_name> <doc_id>"
exit 1
fi
peer=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$1" 2>/dev/null || echo "$1")
doc=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$2" 2>/dev/null || echo "$2")
_metabot_curl "$METABOT_URL/api/peer-memory/documents/$peer/$doc" | _json
;;
path|p)
# Get document by path, e.g.: mm path /metabot/weekly-updates
path=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(' '.join(sys.argv[1:])))" "$@" 2>/dev/null || echo "$*")
Expand Down Expand Up @@ -214,7 +242,9 @@ print(json.dumps({'name': sys.argv[1], 'parent_id': sys.argv[2]}))
echo ""
echo " Read:"
echo " mm search <query> - Search documents"
echo " mm peer-search <query> - Search cached peer memory"
echo " mm get <doc_id> - Get document by ID"
echo " mm peer-get <peer> <doc_id> - Get cached peer memory document"
echo " mm path </folder/doc-slug> - Get document by path"
echo " mm list [folder_id] - List documents (default: root)"
echo " mm folders - List folder tree"
Expand Down
2 changes: 2 additions & 0 deletions bots.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "Frontend development agent for Project Alpha — React, TypeScript, CSS",
"specialties": ["frontend", "react", "typescript", "css"],
"icon": "🎨",
"memoryProject": "project-alpha",
"maxConcurrentTasks": 3,
"budgetLimitDaily": 5.0,
"ttsVoice": "zh_male_rap_mars_bigtts",
Expand All @@ -16,6 +17,7 @@
"name": "project-beta",
"description": "Backend API development agent for Project Beta",
"specialties": ["backend", "api", "database"],
"memoryNamespace": "/projects/project-beta",
"feishuAppId": "cli_yyy",
"feishuAppSecret": "secret2",
"defaultWorkingDirectory": "/home/user/project-beta",
Expand Down
Loading