承接 bootdesk/chat-sdk-core 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

bootdesk/chat-sdk-core

Composer 安装命令:

composer require bootdesk/chat-sdk-core

包简介

Core package for the PHP Chat SDK

README 文档

README

Framework-agnostic core SDK for building chat bots in PHP.

Installation

composer require bootdesk/chat-sdk-core

Chat class

The main entry point. Accepts a state adapter, adapters, configuration, and optional ConcurrencyHandler.

use BootDesk\ChatSDK\Core\Chat;
use BootDesk\ChatSDK\Core\State\MemoryStateAdapter;
use BootDesk\ChatSDK\Core\Concurrency\DefaultConcurrencyHandler;

$chat = new Chat(
    state: new MemoryStateAdapter(),
    userName: 'MyBot',
    config: [],
    concurrencyHandler: new DefaultConcurrencyHandler($state, ['concurrency' => 'drop']),
);

// Register handlers
$chat->onNewMessage('/^hello$/i', function (MessageContext $ctx) {
    $ctx->thread->post('Hey there!');
});

$chat->onDirectMessage(function (MessageContext $ctx) {
    $ctx->thread->post('You DMd me!');
});

$chat->onNewMention(function (MessageContext $ctx) {
    $ctx->thread->post('You mentioned me!');
});

Thread

Represents a conversation thread on any platform. Retrieved by platform-specific identifier.

$thread = $chat->thread('slack:C12345');

$thread->post('Hello!');
$thread->edit('msg-id', 'Updated text');
$thread->delete('msg-id');
$thread->subscribe();
$thread->startTyping();

$thread->setState(['step' => 2]);
$state = $thread->getState();

Direct Messages (DM)

Open a DM with a user. Adapter is inferred from userId format or explicit adapter:userId prefix.

// Infer adapter from userId format:
$thread = $chat->openDM('U1234567');          // Slack (U prefix)
$thread = $chat->openDM('users/12345');       // Google Chat
$thread = $chat->openDM('29:base64string...');// Teams
$thread = $chat->openDM('8f1f3c7e-d4e1-...');// Linear (UUID v4)
$thread = $chat->openDM('1234567890');        // Discord/Telegram/GitHub (numeric)

// Explicit adapter prefix:
$thread = $chat->openDM('slack:U1234567');
$thread = $chat->openDM('telegram:123456789');

// Or pass an Author object:
$author = new Author(id: 'U1234567');
$thread = $chat->openDM($author);

$thread->post('Hello in DM!');

Throws RuntimeException when adapter can't be inferred or doesn't support DMs.

Cards

Build rich, platform-adaptive message cards with text, tables, dividers, links, buttons, and more.

Element types

Builder method Type Description
header(string) Card title (rendered as bold header)
section(callable) Section Grouped text + fields
text(string, TextStyle) Text Styled text (Bold, Muted, Plain)
divider() Divider Horizontal separator (---)
link(label, url) Link Inline hyperlink
table(headers, rows, align) Table Data table with optional column alignment
image(url, alt) Image Embedded image
actions(Button[]) Button Action buttons (triggers onAction)
linkButton(label, url, style) LinkButton URL button (opens link)

Text styles

use BootDesk\ChatSDK\Core\Cards\TextStyle;

TextStyle::Plain;  // default
TextStyle::Bold;   // rendered as **bold** or <b> depending on platform
TextStyle::Muted;  // rendered as _italic_ or muted color

Buttons

use BootDesk\ChatSDK\Core\Cards\Button;
use BootDesk\ChatSDK\Core\Cards\ButtonStyle;
use BootDesk\ChatSDK\Core\Cards\LinkButton;

// Action button — triggers onAction handler when clicked
Button::primary('Confirm', 'order_confirm', ['item' => 'Pizza']);
Button::danger('Cancel', 'order_cancel');
Button::secondary('Maybe', 'order_maybe');

// Link button — opens a URL in the client
LinkButton::primary('Open Dashboard', 'https://dash.example.com');
LinkButton::danger('Report Issue', 'https://github.com/org/repo/issues');

Table with alignment

use BootDesk\ChatSDK\Core\Cards\TableAlignment;

$card = Card::make()
    ->table(
        ['Service', 'Status', 'Uptime'],
        [
            ['API', '✅ Healthy', '99.9%'],
            ['Database', '✅ Connected', '99.8%'],
            ['Queue', '✅ Running', '100%'],
        ],
        [TableAlignment::Left, TableAlignment::Center, TableAlignment::Right],
    );

Full example

use BootDesk\ChatSDK\Core\Cards\Card;
use BootDesk\ChatSDK\Core\Cards\Button;
use BootDesk\ChatSDK\Core\Cards\ButtonStyle;
use BootDesk\ChatSDK\Core\Cards\TextStyle;
use BootDesk\ChatSDK\Core\Cards\TableAlignment;

$card = Card::make()
    ->header('System Status')
    ->text('All services are operational.', TextStyle::Bold)
    ->divider()
    ->table(
        ['Service', 'Status', 'Uptime'],
        [
            ['API', '✅ Healthy', '99.9%'],
            ['Database', '✅ Connected', '99.8%'],
            ['Queue', '✅ Running', '100%'],
        ],
        [TableAlignment::Left, TableAlignment::Center, TableAlignment::Right],
    )
    ->divider()
    ->link('View details', 'https://status.example.com')
    ->linkButton('Dashboard', 'https://dash.example.com', ButtonStyle::Primary)
    ->actions([Button::primary('Refresh', 'refresh_status')]);

$thread->post($card);

Card imageUrl

Set a header image that renders as a native image block on supported platforms:

$card = Card::make()
    ->imageUrl('https://picsum.photos/seed/demo/800/200', 'Demo banner')
    ->header('Status')
    ->text('All good!');
  • Slack: renders as a type: image Block Kit block before the header
  • Telegram: uses sendPhoto with the card text as caption
  • Discord: renders as embed.image.url

Sections with fields

$card = Card::make()
    ->header('Deploy Ready')
    ->section(fn ($s) => $s
        ->text('Build passed on main')
        ->fields(['Branch' => 'main', 'Status' => 'passing'])
    )
    ->actions([Button::primary('Deploy', 'deploy')]);

Platform rendering

Each adapter converts cards to its native format:

Adapter Format
Slack Block Kit (header, section, divider, image, actions)
Discord Embed + Action Row components
Telegram HTML text + inline keyboard
WhatsApp Interactive reply buttons or text fallback
Messenger Generic/Button template
GitHub Markdown (headings, pipe tables, links)
Linear Markdown (same as GitHub)

Attachments & File Uploads

Send URL-based media attachments or binary file uploads with any message.

URL-based Attachments

use BootDesk\ChatSDK\Core\Attachment;
use BootDesk\ChatSDK\Core\PostableMessage;

$message = new PostableMessage(
    content: 'Here is a photo:',
    attachments: [
        new Attachment('image', 'https://picsum.photos/seed/foo/800/600', 'Photo', 'image/jpeg'),
    ],
);
$thread->post($message);

All adapters support URL-based attachments. Platforms without native attachment rendering fall back to text links.

Binary File Uploads

use BootDesk\ChatSDK\Core\FileUpload;
use BootDesk\ChatSDK\Core\PostableMessage;

// From file path
$upload = FileUpload::fromFilename('/path/to/document.pdf');

// From string data
$upload = new FileUpload(file_get_contents($url), 'photo.jpg', 'image/jpeg');

$message = new PostableMessage(
    content: 'Here is your file:',
    files: [$upload],
);
$thread->post($message);

Native support: Slack (3-step API), Telegram (sendDocument), Discord (files[N] multipart).

Other platforms: Binary files are converted to URL-based attachments via a FileUploadConverter. If no converter is registered, AdapterException is thrown.

FileUploadConverter

use BootDesk\ChatSDK\Core\Contracts\FileUploadConverter;
use BootDesk\ChatSDK\Core\Attachment;
use BootDesk\ChatSDK\Core\FileUpload;

class S3FileUploader implements FileUploadConverter
{
    public function upload(FileUpload $file, Adapter $adapter): Attachment
    {
        $url = $this->s3->upload($file->getData(), $file->filename);
        return new Attachment('file', $url, $file->filename, $file->mimeType);
    }
}

In Laravel, bind it in your service provider:

$this->app->bind(FileUploadConverter::class, S3FileUploader::class);

Conversations

Define multi-turn dialog flows by extending the Conversation class.

use BootDesk\ChatSDK\Core\Conversations\Conversation;
use BootDesk\ChatSDK\Core\Thread;
use BootDesk\ChatSDK\Core\Message;

class OrderConversation extends Conversation
{
    public function start(Thread $thread, Message $message): void
    {
        $this->ask($thread, 'What would you like to order?', 'handleOrder');
    }

    public function handleOrder(Thread $thread, Message $message): void
    {
        $this->say($thread, "You ordered: {$message->text}");
        $this->end($thread);
    }
}

Start a conversation:

$chat->conversationManager->start(OrderConversation::class, $thread, $message);

Emoji Resolver

Cross-platform emoji normalization for reactions and message placeholders.

EmojiValue

Immutable singleton representing a normalized emoji name.

use BootDesk\ChatSDK\Core\Support\EmojiValue;

$e = EmojiValue::get('thumbs_up');
$e->name;       // "thumbs_up"
(string) $e;    // "{{emoji:thumbs_up}}"

// Singleton — same name always returns same object
EmojiValue::get('thumbs_up') === EmojiValue::get('thumbs_up'); // true

EmojiResolver

Converts between platform formats and normalized names.

use BootDesk\ChatSDK\Core\Support\EmojiResolver;

$resolver = new EmojiResolver;

// Normalize incoming reactions
$resolver->fromSlack('+1');       // "thumbs_up"
$resolver->fromGChat('👍');       // "thumbs_up"
$resolver->fromTeams('like');     // "thumbs_up"
$resolver->fromGithub('+1');      // "thumbs_up"

// Convert to platform format for outgoing reactions
$resolver->toSlack('thumbs_up');   // "+1"
$resolver->toGChat('thumbs_up');   // "👍"
$resolver->toDiscord('thumbs_up'); // "👍"
$resolver->toGithub('thumbs_up');  // "+1"

// Match across formats
$resolver->matches('+1', 'thumbs_up');  // true
$resolver->matches('👍', 'thumbs_up');  // true

Emoji Placeholders in Messages

Use {{emoji:name}} in message text. They convert automatically to the correct platform format.

EmojiResolver::convertPlaceholders(
    'Great work! {{emoji:fire}} {{emoji:rocket}}',
    'slack',
);
// Result: "Great work! :fire: :rocket:"

EmojiResolver::convertPlaceholders(
    'Great work! {{emoji:fire}} {{emoji:rocket}}',
    'gchat',
);
// Result: "Great work! 🔥 🚀"

Adapters with EmojiResolver injection auto-convert placeholders in outgoing messages.

Reaction Events

Reaction events provide both the normalized name and the raw platform string:

$chat->onReaction(function (ReactionEvent $event) {
    $event->emoji;      // Normalized: "thumbs_up"
    $event->rawEmoji;   // Platform raw: "+1" (Slack) or "👍" (Telegram)
});

See the Emoji System guide for the full emoji map and platform details.

Middleware

Six middleware interfaces for intercepting different stages:

  • ReceivingMiddleware -- Intercept inbound messages before handlers run
  • SendingMiddleware -- Intercept outbound messages before they are delivered
  • WebhookMiddleware -- Intercept raw webhook payloads before parsing
  • SentMiddleware -- Act after a message has been sent
  • HeardMiddleware -- Fire after a pattern matches, before the handler runs
  • WebhookEventMiddleware -- Swap adapter per-event in batched webhooks

All add*Middleware() methods accept an optional int $priority parameter (default 0). Higher values execute earlier. When omitted or equal, insertion order is preserved.

$chat->addReceivingMiddleware($auditLog, priority: 100);  // runs first
$chat->addReceivingMiddleware($transform);                  // runs second (priority 0)

Extending Adapters

All adapters use protected members for extensibility. Extend any adapter to customize behavior:

use BootDesk\ChatSDK\Telegram\TelegramAdapter;

class MyTelegramAdapter extends TelegramAdapter
{
    protected function apiCall(string $method, array $params): array
    {
        // Add custom logging, retry logic, etc.
        return parent::apiCall($method, $params);
    }

    protected function buildMessageParams(PostableMessage $message): array
    {
        $params = parent::buildMessageParams($message);

        // Add custom parameters
        $params['disable_web_page_preview'] = true;

        return $params;
    }
}

Register your custom adapter via AdapterRegistry:

use BootDesk\ChatSDK\Core\Support\AdapterRegistry;

// Register in a service provider or bootstrap file

// Replace an existing adapter
AdapterRegistry::register('telegram', MyTelegramAdapter::class);

// Or register as a new adapter
AdapterRegistry::register('telegram-custom', MyTelegramAdapter::class);

With AdapterResolver: Dynamic resolution tries resolver first (tenant-specific), then falls back to static adapters from config (global default). This allows tenants to override specific adapters while using global defaults for others.

StateAdapter interface

The state adapter handles persistence, pub/sub, locking, and queuing. Methods:

Method Purpose
connect Establish connection to state store
disconnect Close connection
subscribe Subscribe to a channel
unsubscribe Unsubscribe from a channel
acquireLock Acquire a named lock with TTL (returns Lock or null)
releaseLock Release a previously acquired lock
extendLock Extend a lock's TTL (returns bool)
get Retrieve a value by key
set Store a value by key with optional TTL
delete Remove a value by key
enqueue Add item to a queue (max size configurable)
dequeue Remove and return item from a queue
queueDepth Get current queue size

Locks are used for concurrency control (drop/queue/debounce strategies). Queues store pending messages when concurrency: queue is set.

Concurrency is pluggable via ConcurrencyHandler. The core provides DefaultConcurrencyHandler (sync/blocking with usleep for debounce). Framework packages (e.g., Laravel) replace it with async implementations that dispatch jobs to workers. ConcurrencyHandler::process() accepts an optional ?ServerRequestInterface $request — framework packages can serialize it for jobs so AdapterResolver receives the original request in queued context. Adapters can declare sync/async preference via RequiresSyncResponse and RequiresAsyncResponse marker interfaces.

MessageContext

Passed to every event handler.

  • Properties: thread, message, transcripts
  • Methods: skip(), setState(), getState()

Transcripts

Per-user message history. Configure via $transcripts param on Chat constructor. Requires an IdentityResolver.

use BootDesk\ChatSDK\Core\Contracts\IdentityResolver;
use BootDesk\ChatSDK\Core\Contracts\TranscriptsApi;

// Pass an IdentityResolver + config array — creates DefaultTranscriptsApi
$chat = new Chat(
    state: $state,
    identity: new class implements IdentityResolver {
        public function resolve(Author $author): ?string { return $author->id; }
    },
    transcripts: ['max_messages' => 100, 'ttl_ms' => 2592000000],
);

// Or pass a pre-built TranscriptsApi instance (for custom impls)
$chat = new Chat(
    state: $state,
    identity: $resolver,
    transcripts: new MyCustomTranscriptsApi($state),
);
  • Incoming messages auto-recorded in dispatchIncomingMessage()
  • Outgoing replies auto-recorded via TranscriptSentMiddleware (wired automatically)
  • Access via $chat->getTranscripts() or $context->transcripts in handlers
  • Methods: append(userKey, Message), appendOutgoing(userKey, SentMessage, text), list(userKey), count(userKey), delete(userKey)

Event handlers

Method Pattern Description
onNewMessage regex Match text messages
onDirectMessage - DM-only messages
onNewMention - Bot was mentioned
onSubscribedMessage - Subscribed thread messages
onReaction emoji Reaction added/removed
onAction actionId Button/action triggered
onSlashCommand command Slash command
onModalSubmit callbackId Modal form submitted
onModalClose callbackId Modal form closed
onOptionsLoad actionId External select options requested
onAssistantThreadStarted - Slack assistant thread created
onAssistantContextChanged - Slack assistant context changed
onAppHomeOpened - Slack App Home tab opened
onMemberJoinedChannel - User joined a channel

Modals

Build and open platform-agnostic modal forms.

Value Objects

use BootDesk\ChatSDK\Core\Modals\Modal;
use BootDesk\ChatSDK\Core\Modals\TextInput;
use BootDesk\ChatSDK\Core\Modals\Select;
use BootDesk\ChatSDK\Core\Modals\ExternalSelect;
use BootDesk\ChatSDK\Core\Modals\RadioSelect;
use BootDesk\ChatSDK\Core\Modals\SelectOption;

$modal = new Modal(
    callbackId: 'feedback',
    title: 'Submit Feedback',
    submitLabel: 'Send',
    closeLabel: 'Cancel',
    notifyOnClose: true,
    children: [
        new TextInput(
            id: 'comment',
            label: 'Comment',
            placeholder: 'Enter your feedback...',
            multiline: true,
        ),
        new ExternalSelect(
            id: 'category',
            label: 'Category',
            placeholder: 'Start typing...',
            minQueryLength: 1,
        ),
    ],
);

Opening Modals from Handlers

Both ActionEvent and SlashCommandEvent expose openModal() via the OpensModals trait:

use BootDesk\ChatSDK\Core\Modals\Modal;
use BootDesk\ChatSDK\Core\Modals\TextInput;

$chat->onAction('feedback', function (ActionEvent $event) {
    $event->openModal(new Modal(
        callbackId: 'feedback',
        title: 'Submit Feedback',
        submitLabel: 'Send',
        children: [
            new TextInput(id: 'comment', label: 'Comment', multiline: true),
        ],
    ));
});

Modal context (thread info) is stored server-side and restored when the modal is submitted or closed, so handlers have access to $event->relatedThread.

External Selects / Options Load

When using ExternalSelect, Slack sends block_suggestion events as the user types. Handle them with onOptionsLoad:

$chat->onOptionsLoad(function (OptionsLoadEvent $event) {
    $prefix = strtolower($event->query);
    $all = [
        ['text' => 'Bug Report', 'value' => 'bug'],
        ['text' => 'Feature Request', 'value' => 'feature'],
    ];

    return $prefix === ''
        ? $all
        : array_values(array_filter($all, fn ($o) => str_starts_with(strtolower($o['text']), $prefix)));
});

The return value must be an array of ['text' => string, 'value' => string]. The adapter converts to platform format.

Modal Events

$chat->onModalSubmit(function (ModalSubmitEvent $event) {
    $event->relatedThread?->post("Form submitted: " . json_encode($event->values));
});

$chat->onModalClose(function (ModalCloseEvent $event) {
    $event->relatedThread?->post("Form closed without submitting.");
});
  • ModalSubmitEvent: callbackId, values (map of actionId → value), user, viewId, relatedThread
  • ModalCloseEvent: callbackId, user, viewId, relatedThread

Documentationn

Full API documentation: https://bootdesk.github.io/chat-sdk

License

MIT

bootdesk/chat-sdk-core 适用场景与选型建议

bootdesk/chat-sdk-core 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 354 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 18 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 bootdesk/chat-sdk-core 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 bootdesk/chat-sdk-core 我们能提供哪些服务?
定制开发 / 二次开发

基于 bootdesk/chat-sdk-core 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 354
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 24
  • 依赖项目数: 13
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-18