Widget Channel
自社サイトや自社プロダクトの中で Agent と会話させたい時は、独立したホストページではなく widget channel を使います。
このページは埋め込み型の Widget フローを説明します。Codeer のホストページを使いたい場合は Web Channel を見てください。UI と backend を含めて自分たちで管理したい場合は API Access を使います。
Consultation Desk では、次の順番が最も安全です。
- 先に Agent バージョンを publish する
- widget channel を作成または更新する
- widget の基本設定と access ルールを整える
- widget channel を publish する
- embed snippet を host page に貼る
- 外部共有の前に live page を自分でテストする
始める前に必要なもの
- 少なくとも一度 publish 済みの Agent
- workspace の channel 編集権限
- embed snippet を入れられるサイトまたは product page
- ログイン済みユーザーをそのまま widget に入れたい場合は、host backend を更新できる担当者
完全な設定フロー
-
先に Agent を publish する
Editorで対象 Agent を開き、Publishを押します。- widget channel の publish だけでは Agent は publish されません。
- Agent の publish 済み version に
suggested questionsがあれば、widget は publish 後にそれを panel の候補質問として使います。
-
Channelsで widget channel を作成するChannelsを開きます。New Channelを押します。Widgetを選びます。- 安定した
NameとSlugを設定します。
slug は埋め込み契約の一部です
widget channel では
widgetKeyが channel slug です。後で slug を変えると、再 publish だけでなく、すでに貼った embed snippet と旧 slug に依存する signed login フローも更新する必要があります。 -
widget の基本設定を整える
まずは次の項目を確認します。
Agent:この widget で応答させる publish 済み AgentLauncher LabelとLauncher Icon:閉じた状態の入口表示Widget Title:panel header に出るタイトルWidget Language:English、繁體中文、日本語Widget Position:通常はbottom-rightかbottom-leftGreeting:panel を開いた時の最初の案内文
Suggested Questionsは widget 側で別管理しません。現在この channel に紐づけた publish 済み Agent version から自動で使われます。 -
access と security を設定する
ここでは二つの判断があります。
- 匿名アクセス
- 訪問者が guest として会話を始めてよいなら有効にします。
- host page が必ずログイン済み身分を渡す場合だけ無効にします。
- origin 制限
- 特定のサイトだけで widget を動かしたい場合は
Configured origins onlyを使います。 - 1 行に 1 つ、例えば
https://support.example.comのような正確な origin を入れます。 - ページ path ではなく origin だけを入れてください。
- 特定のサイトだけで widget を動かしたい場合は
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には含まれず、ページを離れると完全な値は再表示されません。 - 匿名アクセス
-
widget channel を publish して embed snippet をコピーする
- 設定が整ったら widget channel を publish します。
Overviewで生成済みの embed snippet をコピーします。Auto-initは、publish 済み設定をそのまま使う通常の選択です。Manual initは、script 読み込み前に host page 側で runtime override が必要な場合だけ使います。
-
実ページで自分で確認する
外部共有の前に、実際の 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.js は init(...) 時に publish 済み設定の一部を上書きできます。preview 環境、一時的な検証、host page ごとの差し替えに向いています。
よく使う override 項目
configUrl- 別の
config.jsonを使う
- 別の
apiRoot- 別の API root を使う
agentId- 一時的に別 Agent を紐づける
externalUserId- guest / session manager 用の外部識別子を固定する
appearance.titleappearance.positionappearance.launcher.labelappearance.launcher.iconUrlappearance.theme.baseColorappearance.offset.xappearance.offset.yappearance.zIndexthemeModeopenOnInit
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 の優先順
同じ項目が複数箇所にある場合の優先順は次です。
init(...)の runtime override- publish 済み
config.json - widget の default 値
override 時の注意
appearance.offsetとappearance.zIndexは API 専用で、Console から publish されませんthemeModeはappearanceとは別の 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
必要に応じて次も渡せます。
configUrlapiRootagentIdexternalUserIdappearancethemeModeopenOnInitonConfigLoadedonErroronStateChange
例:
<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をそのまま送る - 低セキュリティ。信頼できる環境のみで使う
- host app が
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 を使う場合、実際の流れは次の通りです。
- Codeer の widget channel で shared secret を生成する
- その shared secret を自分たちの backend に保存する
- 自分たちの backend に assertion 発行 API を実装する
- host page の
getAssertion()からその API を呼ぶ - backend が shared secret で assertion を署名する
- 返ってきた 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
さらに注意:
expはiatより後であることexpはiatから最大 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は使わない