コンテンツにスキップ

Widget Channel

自社サイトや自社プロダクトの中で Agent と会話させたい時は、独立したホストページではなく widget channel を使います。

このページは埋め込み型の Widget フローを説明します。Codeer のホストページを使いたい場合は Web Channel を見てください。UI と backend を含めて自分たちで管理したい場合は API Access を使います。

Consultation Desk では、次の順番が最も安全です。

  1. 先に Agent バージョンを publish する
  2. widget channel を作成または更新する
  3. widget の基本設定と access ルールを整える
  4. widget channel を publish する
  5. embed snippet を host page に貼る
  6. 外部共有の前に live page を自分でテストする

始める前に必要なもの

  • 少なくとも一度 publish 済みの Agent
  • workspace の channel 編集権限
  • embed snippet を入れられるサイトまたは product page
  • ログイン済みユーザーをそのまま widget に入れたい場合は、host backend を更新できる担当者

完全な設定フロー

  1. 先に Agent を publish する

    • Editor で対象 Agent を開き、Publish を押します。
    • widget channel の publish だけでは Agent は publish されません。
    • Agent の publish 済み version に suggested questions があれば、widget は publish 後にそれを panel の候補質問として使います。
  2. Channels で widget channel を作成する

    • Channels を開きます。
    • New Channel を押します。
    • Widget を選びます。
    • 安定した NameSlug を設定します。

    slug は埋め込み契約の一部です

    widget channel では widgetKey が channel slug です。後で slug を変えると、再 publish だけでなく、すでに貼った embed snippet と旧 slug に依存する signed login フローも更新する必要があります。

  3. widget の基本設定を整える

    まずは次の項目を確認します。

    • Agent:この widget で応答させる publish 済み Agent
    • Launcher LabelLauncher Icon:閉じた状態の入口表示
    • Widget Title:panel header に出るタイトル
    • Widget LanguageEnglish繁體中文日本語
    • Widget Position:通常は bottom-rightbottom-left
    • Greeting:panel を開いた時の最初の案内文

    Suggested Questions は widget 側で別管理しません。現在この channel に紐づけた publish 済み Agent version から自動で使われます。

  4. access と security を設定する

    ここでは二つの判断があります。

    • 匿名アクセス
      • 訪問者が guest として会話を始めてよいなら有効にします。
      • host page が必ずログイン済み身分を渡す場合だけ無効にします。
    • origin 制限
      • 特定のサイトだけで widget を動かしたい場合は Configured origins only を使います。
      • 1 行に 1 つ、例えば https://support.example.com のような正確な origin を入れます。
      • ページ path ではなく origin だけを入れてください。

    host page からログイン済み身分を渡したい場合は、さらに login mode を選びます。

    • Disabled:widget は常に guest モード
    • Assertion:本番向けの推奨方式。host backend が身分を署名して証明します
    • Direct User ID:低セキュリティ。信頼できる環境だけで使います

    ログイン連携がないのに guest を無効にしない

    匿名アクセスを切ったのに host page が有効なログイン身分を渡さない場合、利用者は会話を開始できません。

    Direct login には origin 制限が必要です

    direct login を使う前に、origin policy を Configured origins only に切り替え、少なくとも 1 つの allowed origin を追加してください。

    Assertion の shared secret は一度しか表示されません

    Assertion を使う場合は、shared secret を生成したらすぐ host backend に保存してください。config.json には含まれず、ページを離れると完全な値は再表示されません。

  5. widget channel を publish して embed snippet をコピーする

    • 設定が整ったら widget channel を publish します。
    • Overview で生成済みの embed snippet をコピーします。
    • Auto-init は、publish 済み設定をそのまま使う通常の選択です。
    • Manual init は、script 読み込み前に host page 側で runtime override が必要な場合だけ使います。
  6. 実ページで自分で確認する

    外部共有の前に、実際の host page を開いて次を確認します。

    • launcher が想定した位置に出る
    • launcher 文言、icon、title、greeting が正しい
    • 正しい Agent が応答している
    • guest またはログイン済みフローが意図通りに動く
    • origin 制限が実ページをブロックしていない
    • 会話が Histories に出てくる

host app での widget.js の使い方

最も簡単なのは、widget channel の Overview から生成済みの embed snippet をコピーして host page に貼る方法です。

最も簡単な埋め込み: Auto-init

publish 済み channel 設定をそのまま使いたいだけなら、通常はこれで十分です。

<script async src="https://app.codeer.ai/web-widget/widget.js?key=YOUR_WIDGET_KEY"></script>

重要なのは次の 1 点です。

  • YOUR_WIDGET_KEY は widget channel の slug

この方法では widget.js が読み込まれた後、自動で初期化され、publish 済みの config.json を使います。

進んだ埋め込み: Manual init

script 読み込み前に runtime override を入れたい場合は manual init を使います。

<script>
  window.YourWidget =
    window.YourWidget ||
    function () {
      (window.YourWidget.q = window.YourWidget.q || []).push(arguments)
    }

  window.YourWidget("init", {
    widgetKey: "YOUR_WIDGET_KEY",
    openOnInit: true,
    appearance: {
      title: "Support",
      position: "bottom-left",
      launcher: {
        label: "Need help?",
      },
    },
  })
</script>
<script async src="https://app.codeer.ai/web-widget/widget.js"></script>

ここで起きていることは次の通りです。

  • 最初の script で init コマンドを queue に積む
  • 二つ目の script で本体の widget.js を読み込む
  • widget.js の準備ができた後、queue に積んだ init が実行される
  • widget.js の準備前に複数の init を queue した場合、widget の state と callback には最後の init だけを有効なものとして扱ってください

host 側制御が不要なら、まず Auto-init から始める

runtime override や host page 側の制御が不要なら、Auto-init の方がシンプルで安定です。

設定を runtime で上書きする方法

widget.jsinit(...) 時に publish 済み設定の一部を上書きできます。preview 環境、一時的な検証、host page ごとの差し替えに向いています。

よく使う override 項目

  • configUrl
    • 別の config.json を使う
  • apiRoot
    • 別の API root を使う
  • agentId
    • 一時的に別 Agent を紐づける
  • externalUserId
    • guest / session manager 用の外部識別子を固定する
  • appearance.title
  • appearance.position
  • appearance.launcher.label
  • appearance.launcher.iconUrl
  • appearance.theme.baseColor
  • appearance.offset.x
  • appearance.offset.y
  • appearance.zIndex
  • themeMode
  • openOnInit

override の例

<script>
  window.YourWidget =
    window.YourWidget ||
    function () {
      (window.YourWidget.q = window.YourWidget.q || []).push(arguments)
    }

  window.YourWidget("init", {
    widgetKey: "support-widget",
    configUrl: "https://cdn.example.com/widget-preview/config.json",
    apiRoot: "https://api.preview.example.com",
    openOnInit: true,
    themeMode: "light",
    appearance: {
      title: "VIP Support",
      position: "bottom-left",
      launcher: {
        label: "Chat with us",
        iconUrl: "https://cdn.example.com/widget-icon.png",
      },
      theme: {
        baseColor: "green",
      },
      offset: {
        x: 24,
        y: 24,
      },
      zIndex: 9999,
    },
  })
</script>
<script async src="https://app.codeer.ai/web-widget/widget.js"></script>

override の優先順

同じ項目が複数箇所にある場合の優先順は次です。

  1. init(...) の runtime override
  2. publish 済み config.json
  3. widget の default 値

override 時の注意

  • appearance.offsetappearance.zIndex は API 専用で、Console から publish されません
  • themeModeappearance とは別の runtime option です
  • configUrl は絶対 URL でも相対 URL でも使え、相対 URL は host page の origin 基準で解決されます
  • 長期的な本番設定は publish 済み channel に寄せ、runtime override は必要最小限に留めるのが安全です

widget.js の API と例

script 読み込み後、host page は次の API を使えます。

  • window.YourWidget.init(...)
  • window.YourWidget.open()
  • window.YourWidget.close()
  • window.YourWidget.destroy()
  • window.YourWidget.loginUser(...)
  • window.YourWidget.logoutUser()

init(...)

widget を初期化します。最低限必要なのは:

  • widgetKey

必要に応じて次も渡せます。

  • configUrl
  • apiRoot
  • agentId
  • externalUserId
  • appearance
  • themeMode
  • openOnInit
  • onConfigLoaded
  • onError
  • onStateChange

例:

<script>
  async function startWidget() {
    await window.YourWidget.init({
      widgetKey: "support-widget",
      openOnInit: true,
      onConfigLoaded: (config) => {
        console.log("Widget config loaded", config)
      },
      onError: (error) => {
        console.error("Widget error", error)
      },
      onStateChange: (snapshot) => {
        console.log("Widget state", snapshot)
      },
    })
  }
</script>

重要な挙動:

  • widget runtime は singleton なので、init(...) を繰り返しても別々の widget instance は増えず、同じ instance が再設定されます
  • 複数の init(...) が重なった場合、widget の state と onConfigLoaded / onError のような callback を決めるのは最後の呼び出しだけです
  • 先に始まった init(...) の promise が後から resolve することはありますが、古い state の再適用や古い callback の発火は行われません

open() / close()

独自ボタンで widget を開閉したい場合:

<button onclick="window.YourWidget.open()">Open support</button>
<button onclick="window.YourWidget.close()">Close support</button>

destroy()

host app が SPA で、route 変更時に現在の widget instance を残したくない場合:

<script>
  window.YourWidget.destroy()
</script>

よくある用途:

  • route 変更後にクリーンな状態で再初期化したい
  • 一部ページでは widget を残したくない
  • 現在の widget 設定を完全に入れ替えたい

User login と注意事項

widget の login は内蔵ログイン画面ではありません。host app がログイン済み身分を widget に渡します。

3 つのモード

  • Disabled
    • widget は host app からのログイン済み身分を受け付けない
    • 利用者は guest としてのみ開始する
  • Assertion
    • 本番向けの推奨方式
    • host backend が短命の assertion を署名し、身分を証明する
  • Direct User ID
    • host app が externalUserId をそのまま送る
    • 低セキュリティ。信頼できる環境のみで使う

Assertion モードの例

Assertion モードでは host app は次のように呼びます。

<script>
  async function loginWidgetUser() {
    await window.YourWidget.loginUser({
      getAssertion: async () => {
        const response = await fetch("/api/widget/assertion", {
          method: "POST",
          credentials: "include",
        })
        const data = await response.json()
        return data.assertion
      },
    })
  }
</script>

重要な点:

  • assertion は browser 側で署名せず、host backend から返す
  • widget channel の shared secret は自分たちの backend に保存する
  • browser 側の getAssertion() は、自分たちの backend API を呼んで assertion を受け取るだけにする
  • shared secret を browser コードに置かない
  • assertion は必要な都度、新しく生成する

Assertion モードで本当に必要な実装

Assertion を使う場合、実際の流れは次の通りです。

  1. Codeer の widget channel で shared secret を生成する
  2. その shared secret を自分たちの backend に保存する
  3. 自分たちの backend に assertion 発行 API を実装する
  4. host page の getAssertion() からその API を呼ぶ
  5. backend が shared secret で assertion を署名する
  6. 返ってきた assertion を window.YourWidget.loginUser(...) に渡す

つまり:

  • Codeer は assertion を検証する側
  • shared secret の保管と assertion 署名は自分たちの backend 側
  • browser は shared secret を知ってはいけない

Assertion のルール

Assertion は HS256 で署名した短命 JWT にする必要があります。payload には少なくとも次を含めます。

  • sub
    • widget にログインさせたいユーザー識別子
  • channel_slug
    • widget channel の slug と完全一致する値
  • iat
    • 発行時刻。Unix seconds
  • exp
    • 有効期限。Unix seconds
  • jti
    • リプレイ防止のため、毎回新しい一意値

channel に issuer 設定がある場合は、次も入れます。

  • iss

さらに注意:

  • expiat より後であること
  • expiat から最大 5 分以内であること
  • 毎回新しい jti を作ること
  • widget channel の slug を変えたら channel_slug も更新すること

Assertion payload の例

{
  "sub": "member@example.com",
  "channel_slug": "support-widget",
  "iat": 1760000000,
  "exp": 1760000240,
  "jti": "8f4d0e6d-2a63-4d17-a5a3-2be6f4a34862",
  "iss": "https://customer.example.com"
}

Node.js 例: backend で assertion を作る

次の例は、自分たちの Node.js backend で assertion を生成する方法です。

import crypto from "node:crypto"
import jwt from "jsonwebtoken"

export function buildWidgetAssertion({
  externalUserId,
  channelSlug,
  sharedSecret,
  issuer,
}) {
  const now = Math.floor(Date.now() / 1000)

  const payload = {
    sub: externalUserId,
    channel_slug: channelSlug,
    iat: now,
    exp: now + 240,
    jti: crypto.randomUUID(),
  }

  if (issuer) {
    payload.iss = issuer
  }

  return jwt.sign(payload, sharedSecret, {
    algorithm: "HS256",
  })
}

backend API にする場合の返し方の例:

app.post("/api/widget/assertion", async (req, res) => {
  const signedInUser = req.user

  const assertion = buildWidgetAssertion({
    externalUserId: signedInUser.email,
    channelSlug: "support-widget",
    sharedSecret: process.env.WIDGET_SHARED_SECRET,
    issuer: "https://customer.example.com",
  })

  res.json({ assertion })
})

そして browser 側の getAssertion() からこの API を呼びます。

<script>
  async function loginWidgetUser() {
    await window.YourWidget.loginUser({
      getAssertion: async () => {
        const response = await fetch("/api/widget/assertion", {
          method: "POST",
          credentials: "include",
        })
        const data = await response.json()
        return data.assertion
      },
    })
  }
</script>

Assertion モードの注意

  • shared secret は自分たちの backend にだけ保存する
  • shared secret を frontend bundle、HTML、public config に置かない
  • 古い assertion を再利用しない
  • channel_slug は現在の widget channel slug と一致させる
  • issuer を設定したなら、対応する iss も必ず入れる
  • 本番では Direct User ID より Assertion を優先する

Direct User ID モードの例

この使い方は channel が明示的に Direct User ID に設定されている時だけにしてください。

<script>
  async function loginWidgetUser() {
    await window.YourWidget.loginUser({
      externalUserId: "member@example.com",
    })
  }
</script>

logout の例

<script>
  async function logoutWidgetUser() {
    await window.YourWidget.logoutUser()
  }
</script>

login 関連の注意

  • 本番では Assertion を推奨します
  • Direct User ID は、host page 上の任意の JavaScript が別の externalUserId を試せるため低セキュリティです
  • Direct User ID を使う前に、Configured origins only に切り替え、少なくとも 1 つの allowed origin を追加してください
  • widget.js 自体は assertion を生成しません。必ず backend で処理してください
  • channel が Assertion の場合は loginUser({ getAssertion }) を呼びます
  • channel が Direct User ID の場合は loginUser({ externalUserId }) を呼びます
  • logoutUser() の後に guest として続けたい場合は、widget を閉じて再度開くのが最も安定です
  • widget を開いたまま guest session が期限切れになった場合も、通常は閉じて開き直す必要があります

rollout 前の最終確認

  • host page に貼った snippet は Overview からコピーした最新のもの
  • widgetKey は現在の channel slug と一致している
  • slug を変えた場合、embed snippet と login flow も更新済み
  • 本番では強い理由がない限り Assertion を使っている
  • allowed origins に実際に widget を読み込む全サイトが入っている
  • 社内利用者または少数 stakeholder が end-to-end で実テスト済み

制御された rollout

初回の live rollout では、まず surface を狭く保ちます。

  • 最初は allowed origins に staging か社内サイトを 1 つだけ入れる
  • まずは少人数の stakeholder に共有する
  • workspace レベルの制御も必要なら Audience Access を併用する
  • 明確な理由がない限り、広い公開 rollout で Direct User ID は使わない

関連ガイド