コンテンツにスキップ

チャットブリッジ

プロバイダー ワイヤ値 トランスポート スペース種別
Slack slack Socket Mode(外向き WSS) slack

Slack は唯一のプロバイダーです。 ChatProvider は単一値の列挙型で、cc_server は 1 つのプラグインを登録します。スキーマ、すべてのルックアップ、すべての一意インデックスは provider をキーとするため、id がプロバイダーをまたぐことはなく、プロバイダーの追加はサーバー側だけの変更で済みます。

プロバイダー側のアプリはワークスペース × プロバイダーごとに 1 つです。

コマンド名はアプリごとです(デフォルトは cc、つまり /cc)。管理者はこれを変更できます。その場合、ブリッジは最後に呼び出されたときの名前を使います。

コマンド 動作
/cc help(または動詞なしの /cc) ボットができることの一覧を表示します
/cc link <code> ワンタイムコードを引き換え、送信者のチャット ID をリンクします
/cc ticket <title> リンク済みメンバーが報告したローカルチケットを起票します
/cc ticket <title> | <description> 同じですが、最初の | より後をすべて説明として扱います
未知の動詞 未知の動詞とヘルプテキストを報告します

エージェントに到達する方法:

操作 動作
チャンネルでの @bot … Slack スレッドとそれに対応する Control Center スペースを開きます。エージェントは Slack スレッド内で返信します
ブリッジ済み Slack スレッドでの返信 同じ Control Center スペースを継続します — メンションは不要です
チャンネルへの通常のメッセージ 無視されます(ボット宛てではなく、ブリッジ済み Slack スレッドにもないため)
ボットへの DM DM 会話ごとに 1 つの Control Center スペースを作成します。Slack スレッドのタイトルと、エージェントの作業中に「考え中…」ステータスを設定します

プロバイダーディスクリプタが宣言し、それが接続ダイアログを生成し、送信内容を検証します。

フィールド ID ラベル 必須 プレフィックス 場所
botToken ボットトークン はい xoxb- OAuth & Permissions
appToken アプリレベルトークン はい xapp- Basic Information → App-Level Tokens、スコープ connections:write
configRefreshToken アプリ設定トークン いいえ xoxe api.slack.com/apps のアプリ設定トークン

configRefreshToken はアプリ管理用の資格情報です。これがなくても接続は機能しますが、Control Center からボットを作成したり編集したりすることはできません。これは使用のたびにローテーションされ、ローテーション後の値は即座に保存されます。

値はトリムされ、宣言されていないキーは破棄され、プレフィックスの不一致はクライアントとサーバーの両方で同じ文とともに拒否されます。必須フィールドは接続のたびに存在しなければなりません。任意フィールドを空のままにすると、すでに保存されている値が維持されます。

ボットトークンのスコープ:

app_mentions:read
assistant:write
channels:history
chat:write
commands
groups:history
im:history
users:read
users:read.email

ボットのイベント購読:

app_home_opened
app_mention
message.channels
message.groups
message.im

Control Center がアプリを作成または編集するときに表明する構造的設定: Socket Mode を有効化、リクエスト URL なし、メッセージタブを有効かつ書き込み可能、ボットを always_online、必要なスコープとイベントをアプリがすでに持っているものにマージ(決して差し引かない)、そして説明文 File a ticket or link your Control Center account を持つ 1 つのスラッシュコマンド — エクスポートされたマニフェストの中で Control Center 自身のコマンドを識別するマーカーです。

状態 ワイヤ 意味
未接続 disconnected このワークスペース × プロバイダーに保存された資格情報がありません
接続中… connecting 資格情報は保存済みで、トランスポートが接続または再接続中です
稼働中 connected トランスポートが稼働し、イベントを受信しています
接続エラー error 保存済みですが失敗しています。プロバイダー自身のエラーが併せて表示されます

再接続は、30 秒を上限とするジッター付きの指数バックオフです。プロバイダー主導のソケットローテーションでは、バックオフをエスカレートさせずに速やかに再接続します。拒否された資格情報(invalid_auth、not_authed、token_revoked、account_inactive、token_expired、invalid_app_id)またはスコープの欠落は終端です。ブリッジは新しい資格情報が届くまで再試行を停止します。

ブリッジは最も豊かなサーフェスを前提に書かれており、ケイパビリティごとに縮退します。

ケイパビリティ Slack 欠けている場合の動作
streaming あり 返信全体が最後に 1 回だけ投稿されます
streamingRequiresThread あり スレッドアンカーのない返信先では、返信がまとめて投稿されます
ephemeralMessages あり 使用可能な返信ハンドルを持たないスラッシュコマンドの回答は破棄されます(拒否は常にスレッド内に投稿されます)
threadStatus あり 一時的な「考え中…」行はありません
threadTitle あり チャットスレッドはデフォルトのタイトルを保ちます
slashCommands あり /cc サーフェスがありません
taskCards あり テキストのみ — 返信の上にカードがなく、「View in Control Center」リンクもありません
maxMessageLength 39,000 長い返信は Control Center へのポインター付きで切り詰められます
maxStreamChunkLength 3,800 追記は段落、行、またはスペースの境界で分割されます

ストリーミングを広告することは、それを約束することではありません。Slack のストリーミングには有料プランの Agents & AI Apps 機能が必要です。呼び出し時の拒否(streaming_not_supported、feature_not_enabled、paid_only、…)はその接続の存続期間中記憶され、streamingAvailable: false として表面化します。Slack のタスクカードは同じ呼び出しに乗るため、カードの表示モードを拒否する Slack は、接続の存続期間中、ストリームを維持し、カードだけを 1 回失います。

ストリームはまた、誰に向けたものかを名指ししなければなりません。Slack はチャンネルでストリームを開く場合、受信ユーザーとそのチームの両方を要求し(DM では要求しません)、そうでなければ missing_recipient_user_id / missing_recipient_team_id で拒否します — これは呼び出しごとの拒否であるため、ブリッジは試行を続け、その間ターンは 1 つの投稿済み返信として届きます。受信者は、質問した本人であり、受信メッセージから取得されます(アプリのチームではなくその人のチームであるため、Slack Connect チャンネルも正しく解決されます)。チャットから誰も開始しなかったターン — すでにブリッジ済みの会話に対して Control Center で開始された作業 — では、ブリッジを作成したリンクのメンバーが受信者です。そのメンバーがその後チャットアカウントのリンクを解除している場合、そのターンはストリーミングではなくまとめて投稿されます。

エージェントのターンごとに 1 枚のグループ化されたカード(task_display_mode: plan)で、ステップごとに行があります。各行は id + title + status の task_update で、思考の準備ができたときには一度だけ details が付きます。同じ id の details と output は区切りなしで連結されるため、details は 1 回だけ送信され、output がストリーミングされることはありません。プランのタイトルは要求であり、回答は markdown_text チャンクを運ぶ、後続の別の追記です。

部分 内容
プランのタイトル 何が要求されたか
セットアップ行 Working on it…、次に Cloning …、次に Starting the agent… — 1 行で、タイトルが置き換わります
思考行 Thinking…。エージェントが推論を始めたときに追加されます。思考はこの行の details で、推論が終了したときに 1 回だけ送信されます
ツール行 ツールごとに 1 行(Bash which swift)。一意の id を持つため横並びに表示されます
ステータス その行の実行中は進行中。次の行が始まるかターンが終了すると完了。失敗、中断、ターン上限の場合はエラー
コールトゥアクション ターンが終了したときの末尾の行としての View in Control Center — 作業がまだ実行中のときは表示されず、サーバーに到達可能な公開 URL がないときも表示されません

カードが決して運ばないもの: ツール出力、その許可リストの外にある引数、データベースから読み取ったものは一切ありません。推論の散文は Thinking… 行の詳細(256 文字以内)だけであり、メッセージ本文ではありません。新しいことを何も言わないカードは再送されません。メンションはストリームを即座に開くため、Slack スレッドが沈黙することはありません。その後テキストをまったく生成しなかったターンは、2 つ目のメッセージを投稿するのではなく、その同じカードを短い注記で完了させます。ストリームを一度も開いていないスペース(カードがない、またはストリーミングがすでに拒否されている)での言葉のないターンでも、完成したカードと注記を載せた 1 つのメッセージを投稿します。

ストリーミングされるカードと投稿されるカードは、異なるペイロードです:

形式 形 散文
ストリーミング id をキーとする plan_update と task_update チャンク プレーン文字列、チャンクごとに 256 文字以内
投稿 task_id をキーとする task_card を包む plan ブロック リッチテキスト

障害時の処理:

状況 動作
プロバイダーが追記時にカードを拒否する その追記はカードなしで即座に再試行されます。散文は引き続きライブで届き、カードはその接続の存続期間中破棄されます
プロバイダーが散文も拒否する ストリームは閉じられ、返信はまとめて投稿されます
返信の一部が届いた後にストリームが切断される ストリームは閉じられ、何も再投稿されません

ペイロードがこのように分割されている理由は、チャットブリッジを参照してください。

カードは、メンションが受け入れられたときに開きます。クローンの前であり、最初のトークンの前です。それに続くターンは同じカードとストリームを引き継ぎます。その後のナレーションは、ドメインイベントバス上の SpaceProvisioningChanged から来ます — デスクトップのプロビジョニングバナーがスペース行から読み取るのと同じ進捗です。

プロビジョニング状態 ナレーション
メンションを受け付け Working on it…
ステップ repo Cloning <repo>…
ステップ prCheckout Checking out the pull request in <repo>…
ステップ script Running the setup script for <repo>…
ステップ agent Setting up <agent>…
provisioning、ステップはまだなし Preparing the workspace…
ready Starting the agent…
エージェントの推論中 Thinking…
failed Workspace setup failed — details are in Control Center.
cancelled Workspace setup was stopped — details are in Control Center.

セットアップのナレーションには、4 つの条件すべてが必要です:

条件
プロバイダーがカードを描画する
この接続でストリーミングが利用可能である
スペースにチャットリクエストが処理中である
そこですでにターンを報告しているカードがない

リクエストは、ターンが始まるかセットアップが失敗するまで処理中のままです。カードがすでに開いていない ready イベントはウォームなスペースであり、静かなままにされます。また、チャットで誰も待っていないプロビジョニング — アプリで開いた会話や、起動時に再開された取り残された会話 — は、カードをまったく生成しません。

チャット製品はリンク内で http(s) しか受け付けないため、コールトゥアクションがデスクトップのスキームを直接名指しすることはできません。

プロパティ 値
ルート GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
オリジン サーバーの公開 URL から導出されます(ws → http、wss → https)。ループバックは維持されます
レスポンス control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> へ遷移し、手動ボタンを表示する小さな no-store HTML ページ
検証 id は [A-Za-z0-9_.-]{1,128} に一致しなければなりません。それ以外は 404 です
アクセス パブリックかつ不活性です — データベースを読まないため存在オラクルにはならず、認可はこのページが開くアプリが行います
フィールド 制限 備考
appName 1〜35 文字 デフォルトは Control Center · <workspace name>
botDisplayName 1〜80 文字 デフォルトは control-center
description 140 文字以内
agentDescription 300 文字以内 ボットとの DM の上に表示されます
command 32 文字以内 正規化: 先頭のスラッシュを取り除き、小文字化し、[^a-z0-9_-] を削除。空の場合は cc にフォールバックします
agentEnabled — DM/エージェント体験を公開するかどうか

空のボックスは「そのままにする」を意味し、「消去する」ではありません。制限は Slack のものであり、モデル化されている製品の中で最も厳しいものです。

プロパティ 値
長さ 6 文字
アルファベット 23456789ABCDEFGHJKLMNPQRSTVWXYZ(0/O、1/I、U を除く)
有効期間 15 分
使用回数 1 回
ストレージ メモリ上 — サーバーの再起動で未使用のコードは無効になります
スコープ 1 つのワークスペースと 1 つのプロバイダー。新しいコードを発行すると、そのメンバーの以前のコードは無効になります

引き換えは大文字と小文字を区別せずに照合されます。未知のコード、期限切れのコード、別のワークスペース向けのコード、誤ったプロバイダーのコードは、意図的にすべて同じように失敗します。

方法 ワイヤ 仕組み
コード code メンバーが /cc link <code> を引き換えました
メール email プロバイダーが検証したメールアドレスが、すでにワークスペースのメンバーであるユーザーと一致しました

すべてワークスペーススコープです。サーバーはステートレスであるため、各呼び出しは自身の workspace_id を args に持ちます。ディスパッチャーは、それを省略したワークスペーススコープの操作を、データベースを開く前に拒否します。資格情報は入ってくるだけです — ステータスの読み取りがそれらを返すことはありません。

操作 種別 最小ロール アクションクラス
chat.providers read guest —
chat.status read guest —
chat.connect mutate admin secretAccess、networkEgress
chat.disconnect destructive admin secretAccess
chat.listUserLinks read guest —
chat.beginUserLink mutate member —
chat.unlinkUser mutate 自分自身には member、他人には admin —
chat.createApp mutate admin secretAccess、networkEgress
chat.setupLink read admin —
chat.botProfile read admin networkEgress
chat.updateBotProfile mutate admin networkEgress

読み取り操作はその種別から guest の下限を継承します。明示的な下限のない変更操作には member が必要です。

chat.setupLink はボットプロフィールからプロバイダーのアプリ作成 URL を組み立てます。シークレットを読まず、何もダイヤルしないため、アクションクラスを宣言しません — 唯一の呼び出しを行うのはユーザー自身のブラウザーです。

1 つのリアクティブクエリがサーフェスを完成させます:

クエリ 種別 送出
chat.watchUserLinks watch 名簿。リンクが作成または削除されるたびに再プッシュされます

これが存在するのは、リンクが反対側で行われるためです。メンバーはワンタイムコードをボットに入力するので、クライアントのどのレスポンスも答えを返すことはできません。設定の名簿と、開いている「アカウントをリンク」ダイアログの両方がこれを購読しており、ダイアログはメンバー自身のリンクが届いた瞬間に確認表示へ切り替わります。

chat.beginUserLink は常に呼び出し元自身のアイデンティティ用のコードを発行します。chat.updateBotProfile は、ステップが残っている場合(通常は再インストール)にのみ remaining_step を返します。それがなければ、編集は反映済みです。

chat.providers は、提供されているプロバイダーごとに 1 つのエントリを返します — ディスクリプタと、存在する場合は接続ステータスです。設定 UI が描画するものはすべてそのペイロードから来るため、プロバイダーの追加はサーバー側だけの変更で済みます。

対象 場所
接続資格情報 <dataDir>/<workspaceId>/chat_credentials/<provider>.json
途中まで進んだガイド付きセットアップ <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
レガシー Slack ファイル <dataDir>/<workspaceId>/slack_credentials.json と slack_app_setup.json。一度読み込まれ、その場で移行されます

資格情報はワークスペースのデータベースの隣にあり、その中にはありません。ワークスペースを削除するとそのチャットトークンも一緒に削除され、workspace.export やバックアップは workspace.db をコピーします — したがって、他人に渡されたワークスペースに有効なボットトークンは含まれません。書き込みはアトミック(一時ファイルとリネーム)で、所有者のみに制限されます。

どちらもワークスペースデータベースにあり、そこはスカッシュされた v1 ベースラインを持つため、ワークスペースをまたぐ読み取りはコンパイルされません。

chat_space_links — 1 つの外部会話 ↔ 1 つの Control Center スペース:

列 備考
id UUID v4
workspace_id ワークスペーススコープ
provider ChatProvider.wire
external_team_id Slack チーム T…。この概念を持たないプロバイダーでは空
external_channel_id Slack チャンネル C…、グループ G…、DM D…
external_thread_id Slack スレッドアンカー。会話全体がアンカーである場合(ボット DM)は null
cc_space_id REFERENCES spaces(id) ON DELETE CASCADE
created_by_user_id、created_at、last_activity_at

(workspace_id, provider, external_channel_id, external_thread_id) に一意で、スレッド id が null の場合にも同じ制約を課す部分インデックスが付きます(SQLite は NULL を別個のものとして扱います)。

chat_user_links — 1 つの外部メンバー ↔ 1 つの Control Center ユーザー:

列 備考
id UUID v4
workspace_id ワークスペーススコープ
provider ChatProvider.wire
external_team_id、external_user_id Slack メンバー U…
user_id Control Center ユーザー(グローバルなので外部キーなし)
method code または email
linked_at

(workspace_id, provider, external_team_id, external_user_id) に一意、および (workspace_id, provider, user_id) に一意 — 双方向で単一値の帰属であり、一方で 1 人が複数のプロバイダーでリンクされることは可能です。

リンクはアイデンティティの解決であり、アクセス権の付与ではありません。メンバーシップはすべての受信イベントで再確認されるため、その後削除されたメンバーへのリンクは拒否に解決されます。

ブリッジされたスペースは、プロバイダーの SpaceKind(slack)で作成されます。これは通常の人間向けスペースのように振る舞い — topic と同様に通知され、セクション分けされ — 、isExternalChat があらゆる振る舞い(ミラーリング、バッジ付け、保持)が通るチェックです。

ブリッジが取り込むすべてのメッセージは metadata['chat'] スタンプを持ちます:

{
"chat": {
"provider": "slack",
"teamId": "T024BE7LD",
"channelId": "C0G9QF9GW",
"threadId": "1700000000.000100",
"messageId": "1700000000.000200",
"userId": "U024BE7LH",
"userLabel": "sam"
}
}

このスタンプはエコーガードでもあります。外向きのミラーはこのスタンプを持つメッセージをスキップし、これが受信したチャットメッセージがそのまま投稿し返されるのを防ぎます。

振る舞い 詳細
外向きに中継されるもの エージェントのテキストのみ、およびタスクカードのステータス行。推論の散文とツール出力は Control Center にとどまります
ストリーミングの間隔 ターンの実行中は 1 秒に 1 回のフラッシュにスロットルされます。カードとそれが説明するテキストは同じ呼び出しで運ばれます
Control Center で入力された人間のメッセージ リンクされたスレッドにミラーリングされ、作成者の表示名が先頭に付きます
空のターン 沈黙ではなく短い注記(実行の失敗、中断、ターン上限での停止)として報告されます
重複排除 プロバイダー自身のイベント id をキーとし、直近 512 件が記憶されます
フロントドアエージェント 新しいブリッジ済み会話は、ワークスペースの CEO エージェントを、なければ最初のエージェントを、それもなければ誰も起こしません
スペースの命名 Slack チャンネルスレッドには #channel · <request>、DM には Slack DM · <request>。タイトルは 48 文字で切り詰められます
障害の分離 1 つの不良イベントがブリッジを落とすことはありません。接続は他のすべての会話へのサービスを続けます