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

10 KiB
Raw Permalink Blame History

NanoClaw API 详解

架构的实现级细节。高层设计请参见 architecture.md

Channel Adapter 接口

NanoClaw Channel 接口

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 分支安装。

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 技能提供,不在主干代码中:

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 格式:

{
  "sender": "John",
  "senderId": "user123",
  "text": "Check this PR",
  "attachments": [{ "type": "image", "url": "https://signed-url..." }],
  "isFromMe": false
}

chat-sdk — 完整的 Chat SDK SerializedMessage

{
  "_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": []
}

问题响应(用户点击交互式卡片后):

{
  "sender": "John",
  "senderId": "user123",
  "text": "Yes",
  "questionId": "q-123",
  "selectedOption": "Yes",
  "isFromMe": false
}

messages_out 内容示例

普通聊天消息:

{ "text": "LGTM, merging now" }

Chat SDK markdown

{ "markdown": "## Review Summary\n**Status**: Approved\n\nNo issues found." }

卡片:

{
  "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]"
}

询问用户问题:

{
  "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" }
  ]
}

编辑消息:

{ "operation": "edit", "messageId": "3", "text": "Updated: LGTM with minor comments on line 42" }

添加反应:

{ "operation": "reaction", "messageId": "5", "emoji": "thumbs_up" }

系统操作:

{ "action": "reset_session", "payload": { "session_id": "sess-123", "reason": "Skills updated" } }

Host Delivery主机投递逻辑

host 读取 messages_out 并根据 kindoperation 进行分发:

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