Files
nanoclaw/docs/zh/api-details.md
2026-05-12 13:14:17 +00:00

366 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NanoClaw API 详解
架构的实现级细节。高层设计请参见 [architecture.md](architecture.md)。
## Channel Adapter 接口
### NanoClaw Channel 接口
```typescript
interface ChannelSetup {
// 来自 central DB 的对话配置 — 在 setup 时传入,不由 adapter 查询
conversations: ConversationConfig[];
// 主机回调
onInbound(platformId: string, threadId: string | null, message: InboundMessage): void;
onMetadata(platformId: string, name?: string, isGroup?: boolean): void;
}
interface ConversationConfig {
platformId: string;
agentGroupId: string;
triggerPattern?: string; // 正则表达式字符串(用于原生 channel
requiresTrigger: boolean;
sessionMode: 'shared' | 'per-thread';
}
interface ChannelAdapter {
name: string;
channelType: string;
// 生命周期
setup(config: ChannelSetup): Promise<void>;
teardown(): Promise<void>;
isConnected(): boolean;
// 出站投递
deliver(platformId: string, threadId: string | null, message: OutboundMessage): Promise<void>;
// 可选
setTyping?(platformId: string, threadId: string | null): Promise<void>;
syncConversations?(): Promise<ConversationInfo[]>;
updateConversations?(conversations: ConversationConfig[]): void;
}
// 从 adapter 到 host 的入站消息
interface InboundMessage {
id: string;
kind: 'chat' | 'chat-sdk';
content: unknown; // JSON blob — NanoClaw chat 格式或 Chat SDK SerializedMessage
timestamp: string;
}
// 从 host 到 adapter 的出站消息
interface OutboundMessage {
kind: 'chat' | 'chat-sdk';
content: unknown; // JSON blob — 与 kind 对应
}
```
### Chat SDK BridgeChat SDK 桥接)
封装一个 Chat SDK adapter + Chat 实例以符合 NanoClaw `ChannelAdapter` 接口。主干代码仅内置桥接层和 channel registry通道注册表 — 平台特定的 Chat SDK adapterDiscord、Slack、Telegram 等)和原生 adapterWhatsApp/Baileys`/add-<channel>` 技能从 `channels` 分支安装。
```typescript
function createChatSdkBridge(
adapter: Adapter,
chatConfig: { concurrency?: ConcurrencyStrategy }
): ChannelAdapter {
let chat: Chat;
let hostCallbacks: ChannelSetup;
return {
name: adapter.name,
channelType: adapter.name,
async setup(config) {
hostCallbacks = config;
chat = new Chat({
adapters: { [adapter.name]: adapter },
state: new SqliteStateAdapter(),
concurrency: chatConfig.concurrency ?? 'concurrent',
});
// 订阅已注册的对话
for (const conv of config.conversations) {
if (conv.agentGroupId) {
await chat.state.subscribe(conv.platformId);
}
}
// 已订阅的线程 → 转发所有消息
chat.onSubscribedMessage(async (thread, message) => {
const channelId = adapter.channelIdFromThreadId(thread.id);
config.onInbound(channelId, thread.id, {
id: message.id,
kind: 'chat-sdk',
content: message.toJSON(),
timestamp: message.metadata.dateSent.toISOString(),
});
});
// 未订阅线程中的 @mention → 发现
chat.onNewMention(async (thread, message) => {
const channelId = adapter.channelIdFromThreadId(thread.id);
config.onInbound(channelId, thread.id, {
id: message.id,
kind: 'chat-sdk',
content: message.toJSON(),
timestamp: message.metadata.dateSent.toISOString(),
});
// 订阅以便后续接收该线程的消息
await thread.subscribe();
});
// 私信 → 始终转发
chat.onDirectMessage(async (thread, message) => {
config.onInbound(thread.id, null, {
id: message.id,
kind: 'chat-sdk',
content: message.toJSON(),
timestamp: message.metadata.dateSent.toISOString(),
});
await thread.subscribe();
});
await chat.initialize();
},
async deliver(platformId, threadId, message) {
const tid = threadId ?? platformId;
if (message.kind === 'chat-sdk') {
const content = message.content as Record<string, unknown>;
if (content.operation === 'edit') {
await adapter.editMessage(tid, content.messageId as string,
{ markdown: content.text as string });
} else if (content.operation === 'reaction') {
await adapter.addReaction(tid, content.messageId as string,
content.emoji as string);
} else {
await adapter.postMessage(tid, content as AdapterPostableMessage);
}
} else {
const content = message.content as { text: string };
await adapter.postMessage(tid, { markdown: content.text });
}
},
async setTyping(platformId, threadId) {
await adapter.startTyping(threadId ?? platformId);
},
async teardown() {
await chat.shutdown();
},
isConnected() { return true; },
updateConversations(conversations) {
// 订阅新对话,可取消订阅已移除的对话
for (const conv of conversations) {
if (conv.agentGroupId) {
chat.state.subscribe(conv.platformId);
}
}
},
};
}
```
### 原生 NanoClaw Channel不使用 Chat SDK
原生 channel 直接实现 `ChannelAdapter` 接口。WhatsApp/Baileys adapter 是典型的例子 — 它通过 `/add-whatsapp` 技能提供,不在主干代码中:
```typescript
function createWhatsAppChannel(): ChannelAdapter {
let socket: WASocket;
let config: ChannelSetup;
return {
name: 'whatsapp',
channelType: 'whatsapp',
async setup(setup) {
config = setup;
socket = await connectBaileys();
socket.on('messages.upsert', (event) => {
for (const msg of event.messages) {
const jid = msg.key.remoteJid;
const conv = config.conversations.find(c => c.platformId === jid);
// 触发器检查(原生 channel — adapter 执行,非 host
if (conv?.requiresTrigger && conv.triggerPattern) {
if (!new RegExp(conv.triggerPattern).test(msg.message?.conversation || '')) {
return; // 不匹配触发器
}
}
config.onInbound(jid, null, {
id: msg.key.id,
kind: 'chat',
content: {
sender: msg.pushName || msg.key.participant,
senderId: msg.key.participant || msg.key.remoteJid,
text: msg.message?.conversation || '',
attachments: [],
isFromMe: msg.key.fromMe,
},
timestamp: new Date(msg.messageTimestamp * 1000).toISOString(),
});
}
});
},
async deliver(platformId, threadId, message) {
const content = message.content as { text: string };
await socket.sendMessage(platformId, { text: content.text });
},
async setTyping(platformId) {
await socket.sendPresenceUpdate('composing', platformId);
},
async teardown() {
await socket.logout();
},
isConnected() { return !!socket; },
};
}
```
## Session DB会话数据库Schema 详解
### messages_in 内容示例
**`chat`** — 简洁的 NanoClaw 格式:
```json
{
"sender": "John",
"senderId": "user123",
"text": "Check this PR",
"attachments": [{ "type": "image", "url": "https://signed-url..." }],
"isFromMe": false
}
```
**`chat-sdk`** — 完整的 Chat SDK `SerializedMessage`
```json
{
"_type": "chat:Message",
"id": "msg-1",
"threadId": "slack:C123:1234.5678",
"text": "Check this PR",
"formatted": { "type": "root", "children": [...] },
"author": { "userId": "U123", "userName": "john", "fullName": "John", "isBot": false, "isMe": false },
"metadata": { "dateSent": "2024-01-01T00:00:00Z", "edited": false },
"attachments": [{ "type": "image", "url": "https://...", "name": "screenshot.png" }],
"isMention": true,
"links": []
}
```
**问题响应**(用户点击交互式卡片后):
```json
{
"sender": "John",
"senderId": "user123",
"text": "Yes",
"questionId": "q-123",
"selectedOption": "Yes",
"isFromMe": false
}
```
### messages_out 内容示例
**普通聊天消息:**
```json
{ "text": "LGTM, merging now" }
```
**Chat SDK markdown**
```json
{ "markdown": "## Review Summary\n**Status**: Approved\n\nNo issues found." }
```
**卡片:**
```json
{
"card": {
"type": "card",
"title": "Deployment Approval",
"children": [
{ "type": "text", "content": "Deploy 2.1.0 to production?" },
{ "type": "actions", "children": [
{ "type": "button", "id": "approve", "label": "Approve", "style": "primary" },
{ "type": "button", "id": "reject", "label": "Reject", "style": "danger" }
]}
]
},
"fallbackText": "Deployment Approval: Deploy 2.1.0 to production? [Approve] [Reject]"
}
```
**询问用户问题:**
```json
{
"operation": "ask_question",
"questionId": "q-123",
"title": "Failing Test",
"question": "How should we handle the failing test?",
"options": [
"Skip it",
{ "label": "Fix and retry", "selectedLabel": "✅ Fixing", "value": "fix" },
{ "label": "Abort deployment", "selectedLabel": "❌ Aborted", "value": "abort" }
]
}
```
**编辑消息:**
```json
{ "operation": "edit", "messageId": "3", "text": "Updated: LGTM with minor comments on line 42" }
```
**添加反应:**
```json
{ "operation": "reaction", "messageId": "5", "emoji": "thumbs_up" }
```
**系统操作:**
```json
{ "action": "reset_session", "payload": { "session_id": "sess-123", "reason": "Skills updated" } }
```
## Host Delivery主机投递逻辑
host 读取 `messages_out` 并根据 `kind``operation` 进行分发:
```typescript
async function deliverMessage(row: MessagesOutRow, adapter: ChannelAdapter) {
const content = JSON.parse(row.content);
// 系统操作 — host 内部处理
if (row.kind === 'system') {
await handleSystemAction(content);
return;
}
// Agent 间通信 — 写入目标 session DB
if (isAgentDestination(row)) {
await writeToAgentSession(row);
return;
}
// Channel 投递 — 委托给 adapter
await adapter.deliver(row.platform_id, row.thread_id, {
kind: row.kind,
content,
});
}
```
adapter 的 `deliver()` 方法在内部处理操作分发post vs edit vs reaction