Langsung ke konten utama

Membangun Plugin Channel

Panduan ini memandu Anda membangun plugin channel yang menghubungkan OpenClaw ke platform pesan. Pada akhirnya Anda akan memiliki channel yang berfungsi dengan keamanan DM, pairing, reply threading, dan pesan keluar.
Jika Anda belum pernah membangun plugin OpenClaw sebelumnya, baca Memulai terlebih dahulu untuk struktur paket dasar dan penyiapan manifes.

Cara kerja plugin channel

Plugin channel tidak memerlukan tool send/edit/react mereka sendiri. OpenClaw menjaga satu tool message bersama di inti. Plugin Anda memiliki:
  • Config — resolusi akun dan wizard penyiapan
  • Security — kebijakan DM dan allowlist
  • Pairing — alur persetujuan DM
  • Session grammar — bagaimana id percakapan khusus provider dipetakan ke chat dasar, id thread, dan fallback parent
  • Outbound — mengirim teks, media, dan polling ke platform
  • Threading — bagaimana balasan di-thread
Inti memiliki tool message bersama, prompt wiring, bentuk luar session-key, pencatatan :thread: generik, dan dispatch. Jika channel Anda menambahkan param message-tool yang membawa sumber media, tampilkan nama param tersebut melalui describeMessageTool(...).mediaSourceParams. Inti menggunakan daftar eksplisit itu untuk normalisasi path sandbox dan kebijakan akses media keluar, sehingga plugin tidak memerlukan kasus khusus shared-core untuk param avatar, lampiran, atau gambar sampul yang khusus provider. Sebaiknya kembalikan map berindeks action seperti { "set-profile": ["avatarUrl", "avatarPath"] } agar action yang tidak terkait tidak mewarisi argumen media milik action lain. Array datar tetap berfungsi untuk param yang sengaja dibagikan di setiap action yang diekspos. Jika platform Anda menyimpan cakupan tambahan di dalam id percakapan, simpan parsing itu di plugin dengan messaging.resolveSessionConversation(...). Itu adalah hook kanonis untuk memetakan rawId ke id percakapan dasar, id thread opsional, baseConversationId eksplisit, dan parentConversationCandidates. Saat Anda mengembalikan parentConversationCandidates, pertahankan urutannya dari parent yang paling sempit ke percakapan parent/dasar yang paling luas. Plugin bawaan yang memerlukan parsing yang sama sebelum registri channel aktif juga dapat mengekspos file tingkat atas session-key-api.ts dengan ekspor resolveSessionConversation(...) yang cocok. Inti menggunakan permukaan yang aman untuk bootstrap itu hanya ketika registri plugin runtime belum tersedia. messaging.resolveParentConversationCandidates(...) tetap tersedia sebagai fallback kompatibilitas lama ketika plugin hanya memerlukan fallback parent di atas id generik/raw. Jika kedua hook ada, inti menggunakan resolveSessionConversation(...).parentConversationCandidates terlebih dahulu dan hanya fallback ke resolveParentConversationCandidates(...) ketika hook kanonis mengabaikannya.

Persetujuan dan capability channel

Sebagian besar plugin channel tidak memerlukan kode khusus persetujuan.
  • Inti memiliki /approve chat yang sama, payload tombol persetujuan bersama, dan pengiriman fallback generik.
  • Sebaiknya gunakan satu objek approvalCapability pada plugin channel ketika channel memerlukan perilaku khusus persetujuan.
  • ChannelPlugin.approvals dihapus. Letakkan fakta pengiriman/native/render/auth persetujuan di approvalCapability.
  • plugin.auth hanya untuk login/logout; inti tidak lagi membaca hook auth persetujuan dari objek itu.
  • approvalCapability.authorizeActorAction dan approvalCapability.getActionAvailabilityState adalah seam auth persetujuan kanonis.
  • Gunakan approvalCapability.getActionAvailabilityState untuk ketersediaan auth persetujuan chat yang sama.
  • Jika channel Anda mengekspos persetujuan exec native, gunakan approvalCapability.getExecInitiatingSurfaceState untuk status initiating-surface/native-client ketika berbeda dari auth persetujuan chat yang sama. Inti menggunakan hook khusus exec itu untuk membedakan enabled vs disabled, memutuskan apakah channel pemicu mendukung persetujuan exec native, dan menyertakan channel itu dalam panduan fallback native-client. createApproverRestrictedNativeApprovalCapability(...) mengisi ini untuk kasus umum.
  • Gunakan outbound.shouldSuppressLocalPayloadPrompt atau outbound.beforeDeliverPayload untuk perilaku siklus hidup payload khusus channel seperti menyembunyikan prompt persetujuan lokal duplikat atau mengirim indikator mengetik sebelum pengiriman.
  • Gunakan approvalCapability.delivery hanya untuk routing persetujuan native atau penekanan fallback.
  • Gunakan approvalCapability.nativeRuntime untuk fakta persetujuan native milik channel. Jaga agar tetap lazy pada entrypoint channel yang panas dengan createLazyChannelApprovalNativeRuntimeAdapter(...), yang dapat mengimpor modul runtime Anda sesuai kebutuhan sambil tetap membiarkan inti merakit siklus hidup persetujuan.
  • Gunakan approvalCapability.render hanya ketika channel benar-benar memerlukan payload persetujuan kustom alih-alih renderer bersama.
  • Gunakan approvalCapability.describeExecApprovalSetup ketika channel ingin balasan jalur nonaktif menjelaskan knob config tepat yang dibutuhkan untuk mengaktifkan persetujuan exec native. Hook menerima { channel, channelLabel, accountId }; channel akun bernama harus merender path bercakupan akun seperti channels.<channel>.accounts.<id>.execApprovals.* alih-alih default tingkat atas.
  • Jika channel dapat menyimpulkan identitas DM mirip pemilik yang stabil dari config yang ada, gunakan createResolvedApproverActionAuthAdapter dari openclaw/plugin-sdk/approval-runtime untuk membatasi /approve chat yang sama tanpa menambahkan logika inti khusus persetujuan.
  • Jika channel memerlukan pengiriman persetujuan native, jaga agar kode channel tetap fokus pada normalisasi target plus fakta transport/presentasi. Gunakan createChannelExecApprovalProfile, createChannelNativeOriginTargetResolver, createChannelApproverDmTargetResolver, dan createApproverRestrictedNativeApprovalCapability dari openclaw/plugin-sdk/approval-runtime. Letakkan fakta khusus channel di balik approvalCapability.nativeRuntime, idealnya melalui createChannelApprovalNativeRuntimeAdapter(...) atau createLazyChannelApprovalNativeRuntimeAdapter(...), sehingga inti dapat merakit handler dan memiliki pemfilteran request, routing, dedupe, kedaluwarsa, langganan gateway, dan pemberitahuan routed-elsewhere. nativeRuntime dibagi menjadi beberapa seam yang lebih kecil:
  • availability — apakah akun dikonfigurasi dan apakah request harus ditangani
  • presentation — memetakan view model persetujuan bersama ke payload native pending/resolved/expired atau action final
  • transport — menyiapkan target plus mengirim/memperbarui/menghapus pesan persetujuan native
  • interactions — hook bind/unbind/clear-action opsional untuk tombol atau reaksi native
  • observe — hook diagnostik pengiriman opsional
  • Jika channel memerlukan objek milik runtime seperti klien, token, aplikasi Bolt, atau penerima webhook, daftarkan objek itu melalui openclaw/plugin-sdk/channel-runtime-context. Registri runtime-context generik memungkinkan inti melakukan bootstrap handler berbasis capability dari status startup channel tanpa menambahkan glue wrapper khusus persetujuan.
  • Gunakan createChannelApprovalHandler atau createChannelNativeApprovalRuntime tingkat lebih rendah hanya ketika seam berbasis capability belum cukup ekspresif.
  • Channel persetujuan native harus merutekan accountId dan approvalKind melalui helper tersebut. accountId menjaga kebijakan persetujuan multi-akun tetap bercakupan ke akun bot yang benar, dan approvalKind menjaga perilaku persetujuan exec vs plugin tetap tersedia bagi channel tanpa cabang hardcoded di inti.
  • Inti kini juga memiliki pemberitahuan pengalihan persetujuan. Plugin channel tidak boleh mengirim pesan tindak lanjut mereka sendiri seperti “persetujuan dikirim ke DM / channel lain” dari createChannelNativeApprovalRuntime; sebaliknya, tampilkan routing DM origin + approver yang akurat melalui helper capability persetujuan bersama dan biarkan inti mengagregasi pengiriman aktual sebelum memposting pemberitahuan kembali ke chat pemicu.
  • Pertahankan jenis id persetujuan yang dikirim dari awal sampai akhir. Klien native tidak boleh menebak atau menulis ulang routing persetujuan exec vs plugin dari status lokal channel.
  • Jenis persetujuan yang berbeda dapat dengan sengaja mengekspos permukaan native yang berbeda. Contoh bawaan saat ini:
    • Slack menjaga routing persetujuan native tetap tersedia untuk id exec maupun plugin.
    • Matrix menjaga routing DM/channel native dan UX reaksi yang sama untuk persetujuan exec dan plugin, sambil tetap memungkinkan auth berbeda menurut jenis persetujuan.
  • createApproverRestrictedNativeApprovalAdapter masih ada sebagai wrapper kompatibilitas, tetapi kode baru sebaiknya menggunakan builder capability dan mengekspos approvalCapability pada plugin.
Untuk entrypoint channel yang panas, sebaiknya gunakan subpath runtime yang lebih sempit ketika Anda hanya memerlukan satu bagian dari keluarga itu:
  • openclaw/plugin-sdk/approval-auth-runtime
  • openclaw/plugin-sdk/approval-client-runtime
  • openclaw/plugin-sdk/approval-delivery-runtime
  • openclaw/plugin-sdk/approval-gateway-runtime
  • openclaw/plugin-sdk/approval-handler-adapter-runtime
  • openclaw/plugin-sdk/approval-handler-runtime
  • openclaw/plugin-sdk/approval-native-runtime
  • openclaw/plugin-sdk/approval-reply-runtime
  • openclaw/plugin-sdk/channel-runtime-context
Demikian juga, sebaiknya gunakan openclaw/plugin-sdk/setup-runtime, openclaw/plugin-sdk/setup-adapter-runtime, openclaw/plugin-sdk/reply-runtime, openclaw/plugin-sdk/reply-dispatch-runtime, openclaw/plugin-sdk/reply-reference, dan openclaw/plugin-sdk/reply-chunking ketika Anda tidak memerlukan permukaan umbrella yang lebih luas. Khusus untuk setup:
  • openclaw/plugin-sdk/setup-runtime mencakup helper setup yang aman untuk runtime: adapter patch setup yang aman diimpor (createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator), output lookup-note, promptResolvedAllowFrom, splitSetupEntries, dan builder setup-proxy terdelegasi
  • openclaw/plugin-sdk/setup-adapter-runtime adalah seam adapter sempit yang sadar env untuk createEnvPatchedAccountSetupAdapter
  • openclaw/plugin-sdk/channel-setup mencakup builder setup opsional-install ditambah beberapa primitif yang aman untuk setup: createOptionalChannelSetupSurface, createOptionalChannelSetupAdapter,
Jika channel Anda mendukung setup atau auth berbasis env dan alur startup/config generik harus mengetahui nama env tersebut sebelum runtime dimuat, deklarasikan nama itu dalam manifes plugin dengan channelEnvVars. Simpan envVars runtime channel atau konstanta lokal untuk salinan yang ditujukan kepada operator saja. createOptionalChannelSetupWizard, DEFAULT_ACCOUNT_ID, createTopLevelChannelDmPolicy, setSetupChannelEnabled, dan splitSetupEntries
  • gunakan seam openclaw/plugin-sdk/setup yang lebih luas hanya ketika Anda juga memerlukan helper setup/config bersama yang lebih berat seperti moveSingleAccountChannelSectionToDefaultAccount(...)
Jika channel Anda hanya ingin mengiklankan “instal plugin ini dulu” di permukaan setup, sebaiknya gunakan createOptionalChannelSetupSurface(...). Adapter/wizard yang dihasilkan gagal tertutup pada penulisan config dan finalisasi, dan menggunakan ulang pesan install-required yang sama di seluruh validasi, finalisasi, dan salinan tautan docs. Untuk jalur channel panas lainnya, sebaiknya gunakan helper sempit alih-alih permukaan lama yang lebih luas:
  • openclaw/plugin-sdk/account-core, openclaw/plugin-sdk/account-id, openclaw/plugin-sdk/account-resolution, dan openclaw/plugin-sdk/account-helpers untuk config multi-akun dan fallback akun default
  • openclaw/plugin-sdk/inbound-envelope dan openclaw/plugin-sdk/inbound-reply-dispatch untuk wiring route/envelope inbound dan record-and-dispatch
  • openclaw/plugin-sdk/messaging-targets untuk parsing/pencocokan target
  • openclaw/plugin-sdk/outbound-media dan openclaw/plugin-sdk/outbound-runtime untuk pemuatan media plus delegasi identitas/kirim keluar dan perencanaan payload
  • openclaw/plugin-sdk/thread-bindings-runtime untuk siklus hidup thread-binding dan pendaftaran adapter
  • openclaw/plugin-sdk/agent-media-payload hanya ketika tata letak field payload agen/media lama masih diperlukan
  • openclaw/plugin-sdk/telegram-command-config untuk normalisasi custom-command Telegram, validasi duplikat/konflik, dan kontrak config perintah yang stabil sebagai fallback
Channel khusus auth biasanya dapat berhenti pada jalur default: inti menangani persetujuan dan plugin hanya mengekspos capability outbound/auth. Channel persetujuan native seperti Matrix, Slack, Telegram, dan transport chat kustom harus menggunakan helper native bersama alih-alih membuat siklus hidup persetujuan mereka sendiri.

Kebijakan mention inbound

Pertahankan penanganan mention inbound tetap terpisah dalam dua lapisan:
  • pengumpulan bukti milik plugin
  • evaluasi kebijakan bersama
Gunakan openclaw/plugin-sdk/channel-mention-gating untuk keputusan kebijakan mention. Gunakan openclaw/plugin-sdk/channel-inbound hanya ketika Anda memerlukan barrel helper inbound yang lebih luas. Cocok untuk logika lokal plugin:
  • deteksi balasan-ke-bot
  • deteksi kutipan-bot
  • pemeriksaan partisipasi thread
  • pengecualian pesan layanan/sistem
  • cache native platform yang diperlukan untuk membuktikan partisipasi bot
Cocok untuk helper bersama:
  • requireMention
  • hasil mention eksplisit
  • allowlist mention implisit
  • bypass perintah
  • keputusan skip final
Alur yang disarankan:
  1. Hitung fakta mention lokal.
  2. Oper fakta tersebut ke resolveInboundMentionDecision({ facts, policy }).
  3. Gunakan decision.effectiveWasMentioned, decision.shouldBypassMention, dan decision.shouldSkip di gerbang inbound Anda.
import {
  implicitMentionKindWhen,
  matchesMentionWithExplicit,
  resolveInboundMentionDecision,
} from "openclaw/plugin-sdk/channel-inbound";

const mentionMatch = matchesMentionWithExplicit(text, {
  mentionRegexes,
  mentionPatterns,
});

const facts = {
  canDetectMention: true,
  wasMentioned: mentionMatch.matched,
  hasAnyMention: mentionMatch.hasExplicitMention,
  implicitMentionKinds: [
    ...implicitMentionKindWhen("reply_to_bot", isReplyToBot),
    ...implicitMentionKindWhen("quoted_bot", isQuoteOfBot),
  ],
};

const decision = resolveInboundMentionDecision({
  facts,
  policy: {
    isGroup,
    requireMention,
    allowedImplicitMentionKinds: requireExplicitMention ? [] : ["reply_to_bot", "quoted_bot"],
    allowTextCommands,
    hasControlCommand,
    commandAuthorized,
  },
});

if (decision.shouldSkip) return;
api.runtime.channel.mentions mengekspos helper mention bersama yang sama untuk plugin channel bawaan yang sudah bergantung pada injeksi runtime:
  • buildMentionRegexes
  • matchesMentionPatterns
  • matchesMentionWithExplicit
  • implicitMentionKindWhen
  • resolveInboundMentionDecision
Jika Anda hanya memerlukan implicitMentionKindWhen dan resolveInboundMentionDecision, impor dari openclaw/plugin-sdk/channel-mention-gating untuk menghindari memuat helper runtime inbound lain yang tidak terkait. Helper lama resolveMentionGating* tetap ada di openclaw/plugin-sdk/channel-inbound hanya sebagai ekspor kompatibilitas. Kode baru sebaiknya menggunakan resolveInboundMentionDecision({ facts, policy }).

Panduan langkah demi langkah

1
2

Paket dan manifes

Buat file plugin standar. Field channel di package.json adalah yang menjadikan ini plugin channel. Untuk permukaan metadata paket lengkap, lihat Penyiapan dan Config Plugin:
{
  "name": "@myorg/openclaw-acme-chat",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "setupEntry": "./setup-entry.ts",
    "channel": {
      "id": "acme-chat",
      "label": "Acme Chat",
      "blurb": "Hubungkan OpenClaw ke Acme Chat."
    }
  }
}
3

Bangun objek plugin channel

Antarmuka ChannelPlugin memiliki banyak permukaan adaptor opsional. Mulailah dengan yang minimum — id dan setup — lalu tambahkan adaptor sesuai kebutuhan.Buat src/channel.ts:
src/channel.ts
import {
  createChatChannelPlugin,
  createChannelPluginBase,
} from "openclaw/plugin-sdk/channel-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/channel-core";
import { acmeChatApi } from "./client.js"; // klien API platform Anda

type ResolvedAccount = {
  accountId: string | null;
  token: string;
  allowFrom: string[];
  dmPolicy: string | undefined;
};

function resolveAccount(
  cfg: OpenClawConfig,
  accountId?: string | null,
): ResolvedAccount {
  const section = (cfg.channels as Record<string, any>)?.["acme-chat"];
  const token = section?.token;
  if (!token) throw new Error("acme-chat: token is required");
  return {
    accountId: accountId ?? null,
    token,
    allowFrom: section?.allowFrom ?? [],
    dmPolicy: section?.dmSecurity,
  };
}

export const acmeChatPlugin = createChatChannelPlugin<ResolvedAccount>({
  base: createChannelPluginBase({
    id: "acme-chat",
    setup: {
      resolveAccount,
      inspectAccount(cfg, accountId) {
        const section =
          (cfg.channels as Record<string, any>)?.["acme-chat"];
        return {
          enabled: Boolean(section?.token),
          configured: Boolean(section?.token),
          tokenStatus: section?.token ? "available" : "missing",
        };
      },
    },
  }),

  // Keamanan DM: siapa yang dapat mengirim pesan ke bot
  security: {
    dm: {
      channelKey: "acme-chat",
      resolvePolicy: (account) => account.dmPolicy,
      resolveAllowFrom: (account) => account.allowFrom,
      defaultPolicy: "allowlist",
    },
  },

  // Pairing: alur persetujuan untuk kontak DM baru
  pairing: {
    text: {
      idLabel: "username Acme Chat",
      message: "Kirim kode ini untuk memverifikasi identitas Anda:",
      notify: async ({ target, code }) => {
        await acmeChatApi.sendDm(target, `Pairing code: ${code}`);
      },
    },
  },

  // Threading: bagaimana balasan dikirim
  threading: { topLevelReplyToMode: "reply" },

  // Outbound: kirim pesan ke platform
  outbound: {
    attachedResults: {
      sendText: async (params) => {
        const result = await acmeChatApi.sendMessage(
          params.to,
          params.text,
        );
        return { messageId: result.id };
      },
    },
    base: {
      sendMedia: async (params) => {
        await acmeChatApi.sendFile(params.to, params.filePath);
      },
    },
  },
});
Alih-alih mengimplementasikan antarmuka adaptor tingkat rendah secara manual, Anda meneruskan opsi deklaratif dan builder akan menyusunnya:
OptionWhat it wires
security.dmResolver keamanan DM bercakupan dari field config
pairing.textAlur pairing DM berbasis teks dengan pertukaran kode
threadingResolver mode reply-to (tetap, bercakupan akun, atau kustom)
outbound.attachedResultsFungsi kirim yang mengembalikan metadata hasil (ID pesan)
Anda juga dapat meneruskan objek adaptor mentah alih-alih opsi deklaratif jika memerlukan kontrol penuh.
4

Hubungkan entry point

Buat index.ts:
index.ts
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { acmeChatPlugin } from "./src/channel.js";

export default defineChannelPluginEntry({
  id: "acme-chat",
  name: "Acme Chat",
  description: "Plugin channel Acme Chat",
  plugin: acmeChatPlugin,
  registerCliMetadata(api) {
    api.registerCli(
      ({ program }) => {
        program
          .command("acme-chat")
          .description("Manajemen Acme Chat");
      },
      {
        descriptors: [
          {
            name: "acme-chat",
            description: "Manajemen Acme Chat",
            hasSubcommands: false,
          },
        ],
      },
    );
  },
  registerFull(api) {
    api.registerGatewayMethod(/* ... */);
  },
});
Letakkan descriptor CLI milik channel di registerCliMetadata(...) agar OpenClaw dapat menampilkannya di bantuan root tanpa mengaktifkan runtime channel penuh, sementara muatan penuh normal tetap mengambil descriptor yang sama untuk pendaftaran perintah nyata. Pertahankan registerFull(...) untuk pekerjaan yang hanya runtime. Jika registerFull(...) mendaftarkan metode gateway RPC, gunakan prefix khusus plugin. Namespace admin inti (config.*, exec.approvals.*, wizard.*, update.*) tetap dicadangkan dan selalu di-resolve ke operator.admin. defineChannelPluginEntry menangani pemisahan mode registrasi secara otomatis. Lihat Entry Points untuk semua opsi.
5

Tambahkan entri setup

Buat setup-entry.ts untuk pemuatan ringan selama onboarding:
setup-entry.ts
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { acmeChatPlugin } from "./src/channel.js";

export default defineSetupPluginEntry(acmeChatPlugin);
OpenClaw memuat ini alih-alih entri penuh saat channel dinonaktifkan atau belum dikonfigurasi. Ini menghindari menarik kode runtime berat selama alur setup. Lihat Setup dan Config untuk detail.Channel workspace bawaan yang memisahkan ekspor aman-setup ke modul sidecar dapat menggunakan defineBundledChannelSetupEntry(...) dari openclaw/plugin-sdk/channel-entry-contract ketika juga memerlukan setter runtime eksplisit saat setup.
6

Tangani pesan inbound

Plugin Anda perlu menerima pesan dari platform dan meneruskannya ke OpenClaw. Pola yang umum adalah webhook yang memverifikasi request dan mendispatch-nya melalui handler inbound channel Anda:
registerFull(api) {
  api.registerHttpRoute({
    path: "/acme-chat/webhook",
    auth: "plugin", // auth dikelola plugin (verifikasi signature sendiri)
    handler: async (req, res) => {
      const event = parseWebhookPayload(req);

      // Handler inbound Anda mendispatch pesan ke OpenClaw.
      // Wiring tepatnya bergantung pada SDK platform Anda —
      // lihat contoh nyata di paket plugin Microsoft Teams atau Google Chat bawaan.
      await handleAcmeChatInbound(api, event);

      res.statusCode = 200;
      res.end("ok");
      return true;
    },
  });
}
Penanganan pesan inbound bersifat khusus channel. Setiap plugin channel memiliki pipeline inbound-nya sendiri. Lihat plugin channel bawaan (misalnya paket plugin Microsoft Teams atau Google Chat) untuk pola nyata.
7
8

Uji

Tulis test yang diletakkan berdampingan di src/channel.test.ts:
src/channel.test.ts
import { describe, it, expect } from "vitest";
import { acmeChatPlugin } from "./channel.js";

describe("plugin acme-chat", () => {
  it("me-resolve akun dari config", () => {
    const cfg = {
      channels: {
        "acme-chat": { token: "test-token", allowFrom: ["user1"] },
      },
    } as any;
    const account = acmeChatPlugin.setup!.resolveAccount(cfg, undefined);
    expect(account.token).toBe("test-token");
  });

  it("memeriksa akun tanpa mematerialisasikan secret", () => {
    const cfg = {
      channels: { "acme-chat": { token: "test-token" } },
    } as any;
    const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined);
    expect(result.configured).toBe(true);
    expect(result.tokenStatus).toBe("available");
  });

  it("melaporkan config yang hilang", () => {
    const cfg = { channels: {} } as any;
    const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined);
    expect(result.configured).toBe(false);
  });
});
pnpm test -- <bundled-plugin-root>/acme-chat/
Untuk helper test bersama, lihat Testing.

Struktur file

<bundled-plugin-root>/acme-chat/
├── package.json              # metadata openclaw.channel
├── openclaw.plugin.json      # Manifes dengan skema config
├── index.ts                  # defineChannelPluginEntry
├── setup-entry.ts            # defineSetupPluginEntry
├── api.ts                    # Ekspor publik (opsional)
├── runtime-api.ts            # Ekspor runtime internal (opsional)
└── src/
    ├── channel.ts            # ChannelPlugin melalui createChatChannelPlugin
    ├── channel.test.ts       # Test
    ├── client.ts             # Klien API platform
    └── runtime.ts            # Penyimpanan runtime (jika diperlukan)

Topik lanjutan

Opsi threading

Mode balasan tetap, bercakupan akun, atau kustom

Integrasi tool message

describeMessageTool dan penemuan action

Resolusi target

inferTargetChatType, looksLikeId, resolveTarget

Helper runtime

TTS, STT, media, subagen melalui api.runtime
Beberapa seam helper bawaan masih ada untuk pemeliharaan plugin bawaan dan kompatibilitas. Itu bukan pola yang disarankan untuk plugin channel baru; sebaiknya gunakan subpath channel/setup/reply/runtime generik dari permukaan SDK umum kecuali Anda memang memelihara keluarga plugin bawaan itu secara langsung.

Langkah berikutnya