mcpField
S
starchild-auth

starchild-auth

github.com/starchild-ai-agent開発30198 回インストール6 日前に更新
リポジトリ

starchild-auth について

starchild-authは、WebアプリにStarchildのOAuthログインを導入するためのJavaScript SDKと統合ガイドです。トークン処理に加え、エージェントとのチャット、アカウント情報、コンテナ、メディア、Credits向けのAPIを備えています。

主な機能

  1. 01OAuthポップアップログインとトークン更新
  2. 02エージェントサービスの名前空間API
  3. 03SSEチャットとストリーム再接続
  4. 04スレッド、コンテナ、スキルの管理
  5. 05Creditsとウォレットの操作

活用シーン

  1. 01WebアプリへのStarchildログイン導入
  2. 02画面へのエージェント応答のストリーミング表示
  3. 03ユーザーのスレッドとコンテナの管理
  4. 04Credits残高と取引履歴の表示

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/starchild-ai-agent/official-skills --skill starchild-auth

インストール後、Claude Code で/starchild-authを入力して読み込まれたか確認します。

詳細

🔑 Starchild Auth SDK — 完全開発ガイド

Starchild OAuth ログインを任意の Web アプリケーションに統合できます。SDK は OAuth ポップアップフロー、トークンリフレッシュ、統一された StarchildAuthError、名前空間付きヘルパー(auth.chat / auth.credit …)、そして完全なエージェントアクセスのための 60 以上の API メソッドを処理します。

バージョン戦略
成果物現在のバージョンいつ bump するか
npm starchild-auth-sdk0.4.1コード / 公開 API の変更
本 Skill starchild-auth1.11.0統合ガイド / シナリオドキュメントの変更(package とは独立可能)

2 つの semver は互いに束縛されません:ドキュメントのみ変更する場合は skill のみを上げられます。実装のみ変更する場合は必ず package を上げる必要があります(skill は通常、新機能を説明するために minor/patch を同期して上げます)。


アーキテクチャ概要

第三方网站 (your-app.com)
    │
    ├─ StarchildAuth SDK (starchild-auth-sdk)
    │   ├─ auth.login()       → 弹出 starchild-web 授权页面
    │   ├─ auth.logout()      → go-api POST /v1/oauth/logout
    │   ├─ auth.bindAccount() → 跳转主站 Linked accounts 绑定正式账号
    │   └─ token refresh      → go-api POST /v1/oauth/refresh
    │
    ├─ API 调用 (chat scope)
    │   ├─ chat/stream      → clawd (SSE 流式响应)
    │   ├─ /api/clawd/*     → ai-agent (线程/消息)
    │   ├─ /api/cloud/*     → ai-agent (容器管理)
    │   └─ WebSocket        → clawd (文件同步/终端/指标)
    │
    ├─ Credits API (credit:read / credit:write)
    │   └─ https://credit.iamstarchild.com
    │       ├─ GET  /api/credits|charges|topups|usage/daily|pending|tx/{hash}
    │       ├─ POST /api/stripe/create-session | gift-cards/redeem | points/exchange
    │       ├─ GET/POST /api/kyc/* | /api/referral/* | /api/migration/reward/*
    │       └─ GET  /api/public/users/{id}/woo-bonus
    │
    └─ 用户信息
        └─ /v1/oauth/userinfo → ai-agent

アプリ登録と審査

登録フロー
  1. iamstarchild.com → More → OAuth Apps → Create App
  2. 情報を入力:
    • Name (必須): アプリ名
    • Allowed Origin (必須): サードパーティアプリ自身のページ Origin(starchild-web ではない)。例:本番 https://your-app.com;ローカル http://localhost:5173 / http://localhost:3333。origin のみ許可(scheme://host[:port]、パス / query / hash なし);localhost 以外は https:// が必須。複数設定可能。しないでください——http://localhost:6066 をサードパーティ origin として扱うこと。6066 はメインサイト web のローカルポートであり、サーバー側の静的 CORS で既に許可されています。
    • Scopes: 必要な権限をチェック
    • System Prompt (任意): エージェントの動作をカスタマイズ
  3. profile のみ選択 → 自動承認、即座に Client ID を取得
  4. chat / credit:read / credit:write を選択 → 管理者審査に入り、審査通過後にのみ Client ID が生成されます
Scope 権限体系
Scope権限範囲審査
profileユーザー名、アバター、ID の閲覧自動承認
chatエージェント対話、スレッド管理、コンテナ管理、スキル、メディア、定期実行タスク、ウォレット読み取り、課金、WebSocket審査が必要
credit:readCredits 残高とアカウント状態の閲覧審査が必要
credit:writeCredits のチャージ / 購入、Points の交換(credit:read を暗黙的に含む)審査が必要

注意: コンテナの削除操作はすべての OAuth token に対してブロックされます(403 を返します)。これはサーバー側のハード制限です。


インストール

npm / yarn / pnpm
npm install starchild-auth-sdk
# or: yarn add starchild-auth-sdk
# or: pnpm add starchild-auth-sdk
CDN(plain HTML)

<script src="https://unpkg.com/starchild-auth-sdk/dist/starchild-auth.umd.cjs"></script>


<script src="https://registry.npmmirror.com/starchild-auth-sdk/latest/files/dist/starchild-auth.umd.cjs"></script>

UMD ビルドは window.StarchildAuth をエクスポートします(コンストラクタ自体であり、namespace ではありません)。 ESM ビルド(starchild-auth.js)は <script type="module"> または bundler 用です。


初期化とログイン

import { StarchildAuth } from 'starchild-auth-sdk'

const auth = new StarchildAuth({
  clientId: 'your-client-id',          // 必填
  scope: 'profile chat credit:read credit:write', // 空格分隔;需要 Credits 时加上 credit scopes
  // clawdApiBase: 'https://preview.iamstarchild.com', // chat/stream HTTP
  // clawdWsBase: 'wss://preview.iamstarchild.com',   // /ws/sync|terminal|metrics
  // creditApiBase: 'https://credit.iamstarchild.com', // 可选,默认生产域名

  // 登录成功回调(popup 或 autoLogin 恢复 session 时触发)
  onLogin: ({ accessToken, refreshToken, expiresIn, userInfo }) => {
    console.log('Logged in:', userInfo.agentName, 'guest=', userInfo.isGuest)
    // userInfo = { userInfoId, agentName, agentAvatar, isGuest }
  },

  onLogout: () => { /* 清除本地状态 */ },
  onTokenRefresh: (newToken) => { /* 更新本地 token */ },
  onTokenRefreshFailed: () => { /* session 过期 */ },

  // 可选配置
  autoLogin: true,          // 默认 true — 从 localStorage 恢复 session
  origin: 'https://iamstarchild.com',  // Starchild 站点
  refreshInterval: 720000,  // 自动刷新间隔 (ms),默认 12 分钟
})
トークンライフサイクル
  • Access Token: 15 分間有効、12 分ごとに自動リフレッシュ;auth.getToken()
  • Refresh Token: 7 日間有効、localStorage の starchild_rt_{clientId} key に保存;auth.getRefreshToken() はメモリ内の同一値のみを公開
  • autoLogin: ページ読み込み時に refresh token で自動的に session を復元
  • visibilitychange: バックグラウンドから戻ったときに自動で token をリフレッシュ
getRefreshToken() のセキュリティモデル
  • Refresh token はもともと統合側 origin の localStorage に書き込まれています(autoLogin に必要);公開 getter は脅威面を広げず、メモリ内の読み取り可能なコピーにすぎません。
  • 優先的に SDK 自身にリフレッシュさせてください:refreshToken() / 定時 auto-refresh / visibility リフレッシュ。
  • 自前のストレージにコピーする場合は、パスワードとして扱ってください——ログ禁止、サードパーティバックエンドへの送信禁止、URL への埋め込み禁止。
  • 統合側 origin 上の XSS はもともと localStorage を読めるため、CSP を使い、inline script を避けることで緩和してください。
Guest アカウントと正式なログイン方式の紐付け

loginAsGuest() はありません。Guest と正式アカウントは同じ auth.login() メインサイト popup フローを通ります;ユーザーがメインサイトで continue-as-guest(または同等の入口)を選択したときにのみ isGuest: true を取得します。サードパーティは独自の guest ログインを作らないでください。

OAuth ログインは Guest(一時)アカウントを返す場合があります(userInfo.isGuest === true)。Guest は許可された scope を通常どおり使用できますが、永久ログイン方式(Google / X / Email / Phone / Wallet)が紐付けられていません。

紐付けは Starchild メインサイトで完了する必要があり、サードパーティは独自の紐付けページを作らないでください。SDK はジャンプ用メソッドを提供します:

// 登录后检查是否为 Guest
const user = auth.getUserInfo()
// 或刷新:const user = await auth.fetchUserInfo()

if (auth.isGuest() || user?.isGuest) {
  // 打开主站 Account management → Linked accounts
  // URL: https://iamstarchild.com/?account_tab=linked-accounts
  const win = auth.bindAccount()
  if (!win) {
    // 弹窗/新标签被拦截时,可自行跳转
    window.location.href = auth.getBindAccountUrl()
  }
}

// 仅需要 URL(例如自己渲染按钮 href)
const bindUrl = auth.getBindAccountUrl()
// => `${origin}/?account_tab=linked-accounts`
メソッド戻り値説明
isGuest()boolean現在のユーザーが Guest かどうか;未ログインは false
getBindAccountUrl()stringメインサイトの紐付けページ URL(Linked accounts タブ)
bindAccount()Window | null新しいタブでメインサイトの紐付けフローを開く;ブロックされた場合は null を返す

メインサイトが開くと ?account_tab=linked-accounts に基づいてアカウント管理を自動的に開き、Linked accounts に切り替えます。ユーザーが紐付けを完了した後、サードパーティアプリは次回の fetchUserInfo() / token refresh 後に isGuest: false を確認できるはずです。


コア API 呼び出しパターン

リクエスト形式

すべての SDK メソッドは token 注入を自動的に処理します。手動でリクエストを送信する場合:

const headers = {
  'Authorization': `Bearer ${accessToken}`,
  'Content-Type': 'application/json',
}

clawd エンドポイントには fly-force-instance-id が必須:clawd(preview.iamstarchild.com)は各 Fly Machine が単一ユーザーコンテナであり、コンテナ所有権(IDOR)チェックはリクエストが現在のユーザーコンテナに到達することを要求し、そうでない場合は 403「Access denied: you do not own this resource」を返します。OAuth access token には containerId が含まれず、SDK は自動的に GET /api/cloud/containers を通じて解決し fly-force-instance-id: <container_id> header を注入します;手動 curl で clawd エンドポイントをテストする場合はこの header を明示的に付ける必要があり、そうでない場合は 403 になります。

エンドポイントアドレス:

  • ai-agent REST API: https://ai-api.iamstarchild.com(スレッド / メッセージ / コンテナ / スキルなど)
  • clawd HTTP API: https://preview.iamstarchild.com(chat/stream、scheduled-jobs、models)
  • Token エンドポイント: https://go-api.iamstarchild.com/v1(go-api)

名前空間 API(互換レイヤー)

Flat メソッドはすべて保持されます。名前空間は plan 303 スタイルのグループ別エイリアスであり、両者は等価です:

await auth.sendMessage('hi')
await auth.chat.send('hi')          // alias

await auth.getCredits()
await auth.credit.getBalance()      // alias

await auth.listThreads()
await auth.threads.list()
名前空間主要メソッド
auth.profilefetchUserInfo、getUserInfo、isGuest、bindAccount、getBindAccountUrl
auth.chatsend/sendMessage、reconnect/reconnectStream、cancelRun、getModel/setModel、WS ファクトリ
auth.threadscreate、list、get、delete、search、pin、updateTitle
auth.messageslist、delete
auth.containerslist、status、metrics、deploy、start、stop、restart、wake、rename、delete、…
auth.skillscatalog、search、detail
auth.mediauploadImage、transcribeAudio、synthesizeSpeech
auth.sharescreate、list、get、delete、fork
auth.feedbackrate、delete
auth.jobslist、create、get、pause、resume、restart
auth.walletgetPortfolio、list、create、delete、exportPrivateKey、createOnrampSession
auth.credit残高/取引履歴/Stripe/ギフトカード/Points/KYC/Referral/migration/WOO(シナリオ 8 を参照)

Points 交換、KYC、Referral は OAuth に対してすでに開放されています(credit:read / credit:write が必要)。メインサイト専用ではありません。


シナリオ 1:メッセージを送信して SSE ストリームレスポンスを読み取る

これが最も核心的なインタラクションパターンです。メッセージは SSE(Server-Sent Events)でストリーミング返却されます。

SDK 方式
const stream: Response = await auth.sendMessage('Hello, analyze this data')

// SSE 是流式响应,需要逐块读取
const reader = stream.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''

while (true) {
  const { done, value } = await reader.read()
  if (done) break

  buffer += decoder.decode(value, { stream: true })
  const lines = buffer.split('\n')
  buffer = lines.pop() || ''  // 保留未完成的行

  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const event = JSON.parse(line.slice(6))
      // event.type 决定处理方式
      handleStreamEvent(event)
    }
  }
}
ネイティブ fetch 方式(SDK を使用しない)
// POST /chat/stream — SSE 流式聊天(clawd 端点)
const response = await fetch('https://preview.iamstarchild.com/chat/stream', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    message: 'Hello, analyze this data',
    thread_id: threadId,    // 可选,不传则创建新 thread
  }),
})

// 读取 SSE 流(同上)
SSE イベントタイプ
イベントタイプ意味主要フィールド
agent_startAgent が処理を開始session_key — 以降の再接続に使用
text_deltaテキストの増分出力text — 新たに生成されたテキスト断片
tool_useツールの呼び出しtool_name、tool_input
tool_outputツールの返却結果tool_output — ツールの出力
agent_endAgent が完了stop_reason — end_turn / tool_use
errorエラーmessage — エラーの説明
function handleStreamEvent(event: any) {
  switch (event.type) {
    case 'agent_start':
      console.log('Agent started, session:', event.session_key)
      // 保存 session_key 用于断线重连
      break
    case 'text_delta':
      process.stdout.write(event.text)  // 实时输出
      break
    case 'tool_use':
      console.log(`Using tool: ${event.tool_name}(${event.tool_input})`)
      break
    case 'tool_output':
      console.log('Tool result:', event.tool_output)
      break
    case 'agent_end':
      console.log('Done, reason:', event.stop_reason)
      break
    case 'error':
      console.error('Stream error:', event.message)
      break
  }
}
SSE ストリームの再接続

SSE 接続が切断された場合(ネットワーク問題、ページ切り替えなど)、session_key で再接続します:

// POST /chat/stream/reconnect?session_key=xxx&channel=web
const stream = await auth.reconnectStream(sessionKey)
// 读取方式同 sendMessage

シナリオ 2:会話スレッドの管理

// 创建线程
const thread = await auth.createThread('My analysis')
// thread = { id: string, title: string, created_at: string, ... }

// 列出所有线程
const { threads } = await auth.listThreads()

// 获取线程消息
const { messages } = await auth.listMessages(thread.id, 50)  // 最近 50 条
// messages[0] = { id, role: 'user'|'assistant', content: [...], created_at }

// 搜索线程
const result = await auth.searchThreads('analysis')

// 删除线程
await auth.deleteThread(thread.id)

// 删除消息
await auth.deleteMessages(thread.id)

シナリオ 3:コンテナの管理

コンテナは Agent を実行する Fly.io の仮想マシンです。

// 部署新容器
const container = await auth.deployContainer()
// container = { container_id, name, state, region, ... }

// 列出所有容器
const { containers } = await auth.listContainers()

// 获取容器状态
const status = await auth.getContainerStatus(container.container_id)
// status = { state: 'started'|'stopped'|'suspended'|..., ... }

// 启动/停止/重启
await auth.startContainer(container_id)
await auth.stopContainer(container_id)
await auth.restartContainer(container_id)

// 重命名
await auth.renameContainer(container_id, 'production-agent')

// 获取指标
const metrics = await auth.getContainerMetrics(container_id)
// metrics = { cpu: { series: [...] }, memory: { series: [...] }, disk: { series: [...] } }

// ⚠️ 删除容器 — OAuth token 无法执行(服务端返回 403)
// await auth.deleteContainer(container_id)  // 总是失败

シナリオ 4:WebSocket 接続

リアルタイム指標(CPU/メモリ/ディスク)
const ws = auth.createMetricsWebSocket()

ws.onopen = () => console.log('Metrics connected')
ws.onmessage = (e) => {
  const { cpu_percent, memory_used_bytes, memory_total_bytes, disk_used_bytes } = JSON.parse(e.data)
  console.log(`CPU: ${cpu_percent}%, Mem: ${memory_used_bytes}/${memory_total_bytes}`)
}
ws.onclose = () => console.log('Disconnected — implement reconnection logic')
ファイル同期
const ws = auth.createSyncWebSocket()

ws.onopen = () => {
  // 订阅文件变更
  ws.send(JSON.stringify({
    type: 'sync:subscribe',
    payload: { paths: ['/src'] }
  }))
}

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  switch (msg.type) {
    case 'sync:connected':
      console.log('Sync ready, session:', msg.payload.sessionId)
      break
    case 'file:created':
      console.log('New file:', msg.payload.path)
      break
    case 'file:updated':
      // msg.payload = { path, type: 'file'|'directory', triggeredBy: 'watcher'|'agent'|'user' }
      console.log('Changed:', msg.payload.path, 'by', msg.payload.triggeredBy)
      break
    case 'file:deleted':
      console.log('Deleted:', msg.payload.path)
      break
    case 'file:moved':
      console.log('Moved:', msg.payload.oldPath, '→', msg.payload.newPath)
      break
  }
}
ターミナル
// sessionId 从 SSE chat stream 的 terminal:connected 事件获取
const ws = auth.createTerminalWebSocket(sessionId)

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  switch (msg.type) {
    case 'connected':
      console.log('Terminal ready, session:', msg.sessionId)
      break
    case 'output':
      process.stdout.write(msg.data)
      break
    case 'error':
      console.error('Terminal error:', msg.message)
      break
  }
}

// 发送命令
ws.send(JSON.stringify({ type: 'input', data: 'ls -la\n' }))
// 调整终端大小
ws.send(JSON.stringify({ type: 'resize', cols: 120, rows: 40 }))
WebSocket の再接続

ブラウザの WebSocket は自動再接続をサポートしていないため、自身で実装する必要があります:

class WSReconnect {
  private ws: WebSocket | null = null
  private attempts = 0
  private maxAttempts = 5

  connect(factory: () => WebSocket) {
    this.ws = factory()
    this.ws.onclose = () => {
      if (this.attempts < this.maxAttempts) {
        const delay = Math.min(1000 * 2 ** this.attempts, 30000)
        setTimeout(() => {
          this.attempts++
          this.connect(factory)
        }, delay)
      }
    }
    this.ws.onopen = () => { this.attempts = 0 }
  }
}

シナリオ 5:スキルとメディア

// 浏览技能目录
const catalog = await auth.getSkillsCatalog()
// catalog = { official: [{ source, name, description, ... }], community: [...], installed: [...] }

// 搜索技能
const results = await auth.searchSkills('trading')

// 获取技能详情
const detail = await auth.getSkillDetail('official', 'orderly-trading')

// 上传图片
const image = await auth.uploadImage(base64data, 'image/png')
// image = { url: string, filename: string }

// 语音转文字
const text = await auth.transcribeAudio(audioBase64)
// text = { text: string }

// 文字转语音
const audio = await auth.synthesizeSpeech('Hello world')
// audio = { audio_base64: string, format: 'mp3' }

シナリオ 6:共有とフィードバック

// 创建对话分享
const share = await auth.createShare(threadId)
// share = { share_id: string, share_url: string }

const share = await auth.createShare(threadId, ['msg-1', 'msg-2'])  // 指定消息

// 列出分享
const { shares } = await auth.listShares()

// 删除分享
await auth.deleteShare(shareId)

// 复制分享
await auth.forkShare(shareId)

// 点赞/踩消息
await auth.rateMessage(messageId, 'like')
await auth.rateMessage(messageId, 'dislike', 'Not accurate')

// 取消反馈
await auth.deleteFeedback(messageId)

シナリオ 7:ウォレットと課金

// 获取投资组合
const portfolio = await auth.getPortfolio()
// portfolio = { total_value_usd, tokens: [...] }

// 列出钱包
const { wallets } = await auth.listWallets()

// 创建/删除钱包
const wallet = await auth.createWallet()
await auth.deleteWallet(walletAddress)

// Coinbase Onramp
const session = await auth.createOnrampSession({
  amount: '100',  // USD
  currency: 'USD',
})
// session = { url: string } — redirect user to this URL

シナリオ 8:Credits(残高 / チャージ / Points / KYC / Referral)

サービス: starchild-credit-api
Base: creditApiBase、デフォルト https://credit.iamstarchild.com
認証: Authorization: Bearer <oauth_access_token>
Scope:

  • credit:read — すべての GET(残高、取引履歴、pending、tx、points 残高、KYC ステータス、referral 照会、migration ステータス)
  • credit:write — 書き込み操作(Stripe セッション、ギフトカード、points 交換、KYC 書き込み、referral bind、migration claim)。read を暗黙的に含みます

OAuth App の登録時に対応する scope をチェックする必要があり、審査通過後に token に付与されます。

開放範囲:残高/取引履歴、Stripe、ギフトカード、Points 交換、KYC、Referral、migration reward、WOO bonus(public)はいずれも OAuth に対して開放済みです。名前空間での記法:auth.credit.*。

初期化
const auth = new StarchildAuth({
  clientId: 'your-client-id',
  scope: 'profile chat credit:read credit:write',
  // creditApiBase: 'https://credit.iamstarchild.com', // 默认值,本地可改
  onLogin: ({ userInfo }) => console.log(userInfo),
})
残高と取引履歴(credit:read)
// 当前余额
const bal = await auth.getCredits()
// bal.credit_balance — 可用余额
// bal.pending_credit — 待入账
// bal.total_recharged / bal.total_used
// bal.daily_balance — 订阅日额度(若有)
// bal.container_id / bal.user_id

// 扣费记录(默认近 24h,可分页 + 时间窗)
const charges = await auth.getCreditCharges({
  page: 1,
  page_size: 20,
  start_time: '2026-01-01T00:00:00Z', // 可选 ISO8601
  end_time: '2026-01-31T23:59:59Z',
})
// charges.charges[]: { amount, api_type, balance_after, description, created_at, ... }
// charges.pagination: { page, page_size, has_more }
// charges.time_range: { start_time, end_time }

// 充值记录
const topups = await auth.getCreditTopups({ page: 1, page_size: 20 })
// topups.topups[]: { amount, chain, tx_hash, balance_after, created_at, ... }

// 每日用量
const usage = await auth.getCreditDailyUsage({ days: 7 })
// usage.daily[] / usage.by_api[]

// 待入账
const pending = await auth.getPendingCredit()
// pending.status === 'no_pending' | 'pending_sync' | (有 machine 时直接带 pending_credit)

// 轮询链上/支付 tx
const tx = await auth.getCreditTxStatus(txHash)
// 1) status==='not_detected' && !credited && !pending → 继续轮询
// 2) pending && !container_id → 已进 pending_credit,可停
// 3) pending && container_id → 等 flush,继续轮询
// 4) credited === true → 已入账,停
フィールド早見表:CreditBalance(GET /api/credits)
フィールド型説明
user_idstring?ユーザー ID
container_idstring関連コンテナ(空の場合があります)
credit_balancenumber利用可能な Credits
daily_balancenumber?サブスクリプションの日次枠の残り
total_rechargednumber累計チャージ額
total_usednumber累計消費額
pending_creditnumber?入金待ち
ipv6 / name / is_active / status / hintoptionalマシン/ステータス情報
Stripe チャージ(credit:write)
const { url } = await auth.createStripeSession({
  amount_usd: 20,
  success_url: 'https://your-app.com/billing?ok=1',
  cancel_url: 'https://your-app.com/billing?cancel=1',
})
window.location.href = url
// 支付完成后可用 getCreditTxStatus / getCredits / getPendingCredit 确认到账
リクエストフィールド型説明
amount_usdnumber米ドル金額。例:10 = $10
success_urlstring支払い成功時のリダイレクト先絶対 URL
cancel_urlstringキャンセル時のリダイレクト先絶対 URL

レスポンス:{ url: string } — Stripe Checkout のアドレス。

ギフトカード(credit:write)
const r = await auth.redeemGiftCard('GIFT-CODE-XXX')
// 或 auth.redeemGiftCard({ code: 'GIFT-CODE-XXX' })
// r.amount, r.credited_to: 'machine' | 'pending'
// r.new_balance / r.pending_credit
Points で Credits に交換(read で残高照会 / write で交換)
const pts = await auth.getPointsExchangeBalance()
// pts.available_points, pts.exchange_rate, pts.exchanged_credits, ...

const ex = await auth.exchangePoints(
  { points: 1000 },
  crypto.randomUUID(), // 推荐传 Idempotency-Key,防重试双花
)
// 也可 auth.exchangePoints(1000, idemKey)
// ex.credits_received, ex.new_credit_balance, ex.idempotent?
KYC(points の大口交換で要求される場合があります)
const kyc = await auth.getKycStatus()
// kyc.verified / kyc.exempt / kyc.exchanged_credits / kyc.threshold_credits

if (!kyc.verified && !kyc.exempt) {
  const intent = await auth.createKycSetupIntent()
  // intent.client_secret → 交给 Stripe.js / Payment Element 完成绑卡
  // 成功后:
  await auth.verifyKyc(intent.setup_intent_id)
}
Referral
const ref = await auth.getReferralStatus()
// ref.my_referral_code, ref.can_bind, ref.has_bound_inviter, ref.invited_by

if (ref.can_bind) {
  await auth.bindReferralCode('INVITE-CODE') // credit:write,仅一次
}

const invitees = await auth.getReferralInvitees()
// invitees.invitees[], invitees.total_bonus_earned, ...
Migration 報酬
const st = await auth.getMigrationRewardStatus()
if (st.eligible && !st.already_claimed) {
  const claim = await auth.claimMigrationReward() // credit:write
  // claim.amount, claim.credited_to, claim.new_balance
}
WOO Staking Bonus(公開インターフェース)
const woo = await auth.getWooBonus() // 默认当前登录 userInfoId
// woo.bonus_percent, woo.max_staked_woo, woo.matched_wallet
フィールド早見表:その他のよく使うレスポンス
CreditChargeItem / getCreditCharges
字段类型说明
amountnumber扣费 Credits
api_typestring计费 API 类别
balance_afternumber扣费后余额
descriptionstring描述
created_atstringISO8601
machine_ipv6string机器 IPv6
call_type / agent_idstring?可选调用元数据
pagination.has_moreboolean是否还有下一页
CreditTopupItem / getCreditTopups
字段类型说明
amountnumber充值金额
chainstring链 / 渠道(含 stripe)
tx_hashstring交易哈希
balance_afternumber入账后余额
created_atstringISO8601
CreditTxStatus / getCreditTxStatus
字段类型说明
creditedboolean是否已入账
pendingboolean是否处理中
status'not_detected'?未检测到链上 tx
balance_afternumber?入账后余额
amount / chain / tx_hashoptional检测到后的详情
container_idstring?空字符串表示无容器、进 pending
RedeemGiftCardResponse
字段类型说明
codestring礼品卡码
amountnumber到账 Credits
credited_to'machine' | 'pending'入账目标
new_balance / pending_creditnumber | null对应余额
PointsExchangeBalance / PointsExchangeResponse
字段类型说明
available_pointsnumber可兑换积分
exchange_ratestring汇率文案
points_spentnumber本次消耗积分
credits_receivednumber本次获得 Credits
new_credit_balancenumber兑换后余额
idempotentboolean?幂等重放
KycStatus / KycVerifyResponse
字段类型说明
verifiedboolean是否已 KYC
exemptboolean是否豁免
exchanged_credits / threshold_creditsnumber已兑 / 阈值
card_last4 / card_brandstring验卡结果
ReferralStatus / ReferralInviteesResponse
字段类型说明
my_referral_codestring | null自己的邀请码
can_bindboolean是否还能绑邀请人
invitee_countnumber邀请人数
total_bonus_earnednumber累计 referral bonus
MigrationRewardClaimResponse
字段类型说明
amountnumber奖励 Credits
credited_to'machine' | 'pending' | 'none'入账位置
already_claimedboolean是否已领过
messagestring状态说明
WooBonusResponse
字段类型说明
bonus_percentnumber额外 credit 百分比
max_staked_woonumber最高质押 WOO
matched_walletstring命中档位的钱包
wallets_checked[]{ wallet, staked_woo }检查明细

完整 TypeScript 定义与字段 JSDoc 见 SDK:starchild-auth-sdk/src/types.ts(构建后 dist/index.d.ts)。

Scope 不足时

服务端返回 403,body 类似:

{ "detail": "Insufficient scope: credit:read or credit:write is required." }

写接口缺 credit:write 时:

{ "detail": "Insufficient scope: credit:write is required for this operation." }

集成方应引导用户重新 login() 并申请完整 credit scopes,或在 OAuth App 控制台勾选后重新授权。


场景九:消息排队与注入(Agent 运行中发送新消息)

当 Agent 正在处理消息时(SSE 流未结束),用户可能发送新消息。这时不能直接调用 /chat/stream,而是将消息加入排队队列。

前端实现模式(参考 starchild-web)
// 1. 检查 Agent 是否正在运行
const isAgentActive = isStreaming || !!agentBackgroundRunning[threadId]

if (isAgentActive) {
  // 2. 消息加入本地队列(不立即发送到后端)
  const queuedId = `queued-${Date.now()}`
  dispatch(addToMessageQueue({
    threadId,
    message: {
      id: queuedId,
      content: message,
      images: images,      // 可选:base64 图片
      files: files,        // 可选:已上传文件引用
      quote: quoteOptions, // 可选:引用的消息
      status: 'pending',   // pending → sending → sent
      createdAt: Date.now(),
    },
  }))

  // 3. 显示 "当前消息已排队,Agent 完成后自动发送" 提示
  dispatch(updateQueuedMessageStatus({ threadId, messageId: queuedId, status: 'sending' }))

  // 4. 后端在 /chat/stream 完成后,检查 messageQueue
  //    取出 FIFO 的第一条,调用下一个 /chat/stream
} else {
  // Agent 空闲,直接发送
  await sendToChatStream(message, images, files)
}
/chat/stream 请求体格式(完整)
{
  "message": "Hello, analyze this data",
  "thread_id": "thread-uuid",
  "channel": "web",
  "message_id": "queued-1234567890",
  "model": "claude-3-5-sonnet-20241022",
  "images": [
    {
      "base64_data": "...",
      "media_type": "image/png"
    }
  ],
  "files": [
    {
      "name": "data.csv",
      "workspace_path": "/workspace/data.csv",
      "mime_type": "text/csv",
      "size": 1024
    }
  ],
  "quote": {
    "source_message_id": "msg-abc123",
    "quoted_text": "The original message text...",
    "source_role": "user"
  }
}
/chat/stream 响应流程
POST /chat/stream → SSE 连接建立
  ← event: agent_start    { session_key: "sess-xxx" }
  ← event: text_delta     { text: "I'll analyze..." }
  ← event: tool_use       { tool_name: "read_file", tool_input: {...} }
  ← event: tool_output    { tool_output: "file content..." }
  ← event: text_delta     { text: "Based on the data..." }
  ← event: agent_end      { stop_reason: "end_turn" }
SSE 连接关闭
→ 后端检查 messageQueue[threadId]
→ 如果有排队消息 → 自动开始下一个 /chat/stream
SSE 事件类型详解
事件含义payload 示例
agent_startAgent 开始处理,返回 session_key 用于重连{session_key: "sess-abc123"}
text_delta增量文本输出(逐 token){text: "Hello"}
tool_useAgent 调用工具{tool_name: "read_file", tool_input: {path: "/a.txt"}}
tool_output工具返回结果{tool_output: "file contents..."}
agent_endAgent 完成一轮对话{stop_reason: "end_turn" | "tool_use"}
error流错误{message: "Error description"}
agent:interruptedAgent 被中断(用户取消/超时){reason: "..."}
/chat/runs/cancel 取消运行
// POST /chat/runs/cancel?thread_id=xxx
await auth.cancelRun(threadId)
// 这会中断当前正在运行的 SSE 流
// SSE 连接会收到 agent_end 或 agent:interrupted 事件后关闭
/chat/stream/reconnect 断线重连

当 SSE 连接意外断开(网络问题、页面切换),用 session_key 重连:

// POST /chat/stream/reconnect?session_key=sess-xxx&channel=web
const stream = await auth.reconnectStream(sessionKey)

// reconnect 返回的 SSE 事件格式相同
// 它会从中断点继续推送剩余的事件
// 如果 Agent 已完成,会立即收到 agent_end

场景十:Agent 集成(直接 token 注入)

Agent 可以在没有浏览器 popup 的情况下使用 SDK:

import { StarchildAuth } from 'starchild-auth-sdk'

// Agent 从环境变量或 OAuth 回调获取 token
const accessToken = process.env.STARCHILD_TOKEN!

const auth = new StarchildAuth({
  clientId: process.env.CLIENT_ID!,
  scope: 'profile chat',
  autoLogin: false,  // 不弹出浏览器窗口
  onLogin: () => {},
})

// 注入 token(绕过 popup 流程)
;(auth as any)._accessToken = accessToken

// 现在可以调用所有方法
const threads = await auth.listThreads()
const message = await auth.sendMessage('Summarize my threads')

场景十一:推荐 Chat UI 组件集成

Auth SDK 是纯 API 层(无 UI 组件)。以下推荐两个开源 React Chat 组件库,并给出与 SDK SSE 流对接的完整适配器代码,帮助第三方应用快速搭建 ChatGPT 风格的对话界面。

官方文档:

选型对比
维度assistant-uiMUI X Chat
包名@assistant-ui/react@mui/x-chat
集成难度低 — 一个 async *run() generator 即可中 — 需将 SSE 转为 ReadableStream<ChatMessageChunk>
UI 依赖框架无关(shadcn / Tailwind 风格,无主题绑定)强绑 MUI Material 主题(带入 @mui/material + @emotion/*)
React Native支持(@assistant-ui/react-native)不支持
流式协议yield 累积内容(每次替换上一次)chunk 协议(start → text-delta → finish)
工具调用yield { type: 'tool-call', ... } 直观tool-call-* chunk 序列
多会话RemoteThreadListAdapter 或 AssistantCloudlistConversations + 内置侧边栏
许可证MITMIT(全功能免费,无 Pro/Premium)
社区规模1.4M+ 周下载量,YC 背书,Anthropic / LangChain 在用较新,MUI 生态背书
推荐首选 — 适合大多数第三方应用备选 — 仅推荐已在用 MUI Material 的项目
共享:SSE 流解析 helper

两个适配器都需要解析 SDK sendMessage() 返回的 SSE Response。提取为共享函数:

// lib/starchild-sse.ts
import type { SSEEvent } from 'starchild-auth-sdk'

/**
 * Parse Starchild SSE Response into an async iterable of events.
 * Stops early if the abort signal fires.
 *
 * SDK sendMessage() returns a raw Response whose body is an SSE stream.
 * Each line starting with "data: " contains a JSON event:
 *   agent_start | text_delta | tool_use | tool_output | agent_end | error
 */
export async function* parseStarchildSSE(
  response: Response,
  signal?: AbortSignal,
): AsyncGenerator<SSEEvent> {
  if (!response.ok) {
    const body = await response.text().catch(() => '')
    throw new Error(`Chat API ${response.status}: ${body.slice(0, 200)}`)
  }
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ''

  while (true) {
    if (signal?.aborted) {
      reader.cancel()
      break
    }
    const { done, value } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split('\n')
    buffer = lines.pop() || ''

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue
      try {
        yield JSON.parse(line.slice(6)) as SSEEvent
      } catch {
        // Skip malformed JSON lines
      }
    }
  }
}
方案 A:assistant-ui(推荐)
安装
# 推荐使用 pnpm(更快、磁盘占用更小);yarn / bun 亦可
pnpm add @assistant-ui/react
# 或: yarn add @assistant-ui/react
# 或: npm install @assistant-ui/react

# 脚手架生成 Thread 组件(shadcn / Tailwind 风格)
npx assistant-ui init
npx assistant-ui add thread
ChatModelAdapter 实现

assistant-ui 的 LocalRuntime 只需实现一个 ChatModelAdapter.run() 函数(async * generator)。在循环中 yield 累积内容(每次替换上一次,不是 delta):

// runtime/starchild-adapter.ts
import type { ChatModelAdapter } from '@assistant-ui/react'
import { StarchildAuth } from 'starchild-auth-sdk'
import { parseStarchildSSE } from '@/lib/starchild-sse'

/**
 * Create an assistant-ui ChatModelAdapter backed by Starchild Auth SDK.
 * The auth instance is initialized in the Provider and passed in.
 */
export function createStarchildAdapter(auth: StarchildAuth): ChatModelAdapter {
  return {
    async *run({ messages, abortSignal, unstable_threadId }) {
      // 1. Extract last user message text
      const lastUser = [...messages].reverse().find(m => m.role === 'user')
      const text = lastUser?.content
        .filter(c => c.type === 'text')
        .map(c => c.text)
        .join('\n') ?? ''

      if (!text) {
        yield { content: [{ type: 'text', text: '' }] }
        return
      }

      // 2. Call SDK sendMessage — returns SSE Response
      //    threadId: continue existing thread, or omit to create new
      const response = await auth.sendMessage(text, {
        threadId: unstable_threadId,
      })

      // 3. Parse SSE stream and yield cumulative content
      let fullText = ''
      const toolCalls = new Map<string, any>()

      for await (const event of parseStarchildSSE(response, abortSignal)) {
        switch (event.type) {
          case 'agent_start':
            // event.session_key — save for reconnect if needed
            break

          case 'text_delta':
            fullText += event.text
            yield {
              content: [
                ...(fullText ? [{ type: 'text' as const, text: fullText }] : []),
                ...Array.from(toolCalls.values()),
              ],
            }
            break

          case 'tool_use': {
            // Accumulate tool calls outside the loop (per assistant-ui best practice)
            const id = event.tool_use_id || crypto.randomUUID()
            toolCalls.set(id, {
              type: 'tool-call' as const,
              toolCallId: id,
              toolName: event.tool_name,
              args: event.tool_input,
              argsText: JSON.stringify(event.tool_input),
            })
            yield {
              content: [
                ...(fullText ? [{ type: 'text' as const, text: fullText }] : []),
                ...Array.from(toolCalls.values()),
              ],
            }
            break
          }

          case 'tool_output': {
            // Update the matching tool call with its result.
            // IMPORTANT: create a new object (not mutate) so React detects the change.
            const id = event.tool_use_id
            if (id && toolCalls.has(id)) {
              const existing = toolCalls.get(id)
              toolCalls.set(id, { ...existing, result: event.tool_output })
              yield {
                content: [
                  ...(fullText ? [{ type: 'text' as const, text: fullText }] : []),
                  ...Array.from(toolCalls.values()),
                ],
              }
            }
            break
          }

          case 'agent_end':
            // Stream complete — final yield already done above
            break

          case 'error':
            throw new Error(event.message)

          case 'agent:interrupted':
            // User cancelled or timeout — stop yielding
            break
        }
      }

      // Ensure at least one yield with content
      if (!fullText && toolCalls.size === 0) {
        yield { content: [{ type: 'text', text: '' }] }
      }
    },
  }
}
RuntimeProvider 组装
// runtime/StarchildRuntimeProvider.tsx
'use client'
import type { ReactNode } from 'react'
import {
  AssistantRuntimeProvider,
  useLocalRuntime,
} from '@assistant-ui/react'
import { StarchildAuth } from 'starchild-auth-sdk'
import { createStarchildAdapter } from './starchild-adapter'

// Singleton auth instance — SDK handles token refresh internally
const auth = new StarchildAuth({
  clientId: process.env.NEXT_PUBLIC_STARCHILD_CLIENT_ID!,
  scope: 'profile chat',
  onLogin: ({ userInfo }) => console.log('Logged in:', userInfo.agentName),
  onTokenRefreshFailed: () => {
    // Session expired — redirect to login or show login button
    window.location.reload()
  },
})

export function StarchildRuntimeProvider({
  children,
}: Readonly<{ children: ReactNode }>) {
  const runtime = useLocalRuntime(createStarchildAdapter(auth))
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {children}
    </AssistantRuntimeProvider>
  )
}
多线程列表(可选)

如需左侧线程列表(类似 ChatGPT 侧边栏),实现 RemoteThreadListAdapter 并传入 useLocalRuntime 的 adapters.threadList。

⚠️ 接口验证提醒:RemoteThreadListAdapter 的方法签名可能随 assistant-ui 版本变化。以下示例基于公开文档的常见模式,集成前请务必查阅最新文档:https://www.assistant-ui.com/docs/runtimes/concepts/threads

import { useLocalRuntime } from '@assistant-ui/react'
import type { RemoteThreadListAdapter } from '@assistant-ui/react'

// Thread list adapter — bridges SDK thread API to assistant-ui sidebar
const threadListAdapter: RemoteThreadListAdapter = {
  // Called on mount — load thread list from SDK
  async getThreads() {
    const { threads } = await auth.listThreads()
    return threads.map(t => ({
      id: t.thread_id,
      title: t.title || 'New Chat',
      createdAt: t.created_at ? new Date(t.created_at) : new Date(),
    }))
  },

  // Called when user clicks a thread in the sidebar
  async switchToThread(threadId: string) {
    // assistant-ui will call this to switch the active thread.
    // Messages are loaded separately via the adapter's run() or initialMessages.
    // If you need to pre-load history, use auth.listMessages(threadId)
    // and pass them as initialMessages to the runtime.
  },

  // Called when user creates a new thread
  async createThread() {
    const thread = await auth.createThread()
    return { id: thread.thread_id }
  },

  // Called when user deletes a thread
  async deleteThread(threadId: string) {
    await auth.deleteThread(threadId)
  },
}

// In provider:
const runtime = useLocalRuntime(createStarchildAdapter(auth), {
  adapters: { threadList: threadListAdapter },
})
页面中使用
// app/chat/page.tsx
import { Thread } from '@/components/assistant-ui/thread'
import { StarchildRuntimeProvider } from '@/runtime/StarchildRuntimeProvider'

export default function ChatPage() {
  return (
    <StarchildRuntimeProvider>
      <Thread />
    </StarchildRuntimeProvider>
  )
}
方案 B:MUI X Chat

仅推荐给已在用 MUI Material 主题的项目。@mui/x-chat 会强制带入 @mui/material + @emotion/* 依赖树,非 MUI 项目引入成本高。

安装
# 推荐使用 pnpm(更快、磁盘占用更小);yarn / bun 亦可
pnpm add @mui/x-chat @mui/material @emotion/react @emotion/styled
# 或: yarn add @mui/x-chat @mui/material @emotion/react @emotion/styled
# 或: npm install @mui/x-chat @mui/material @emotion/react @emotion/styled
ChatAdapter 实现

MUI X Chat 的 ChatAdapter.sendMessage() 必须返回 Promise<ReadableStream<ChatMessageChunk>>。需要将 SDK 的 SSE 流转换为 MUI 的 chunk 协议(start → text-start → text-delta → text-end → finish):

// adapters/starchild-mui-adapter.ts
import type { ChatAdapter, ChatMessageChunk } from '@mui/x-chat/headless'
import { StarchildAuth } from 'starchild-auth-sdk'
import { parseStarchildSSE } from '@/lib/starchild-sse'

// Module-level state for reconnect / cancel
let lastSessionKey = ''
let currentThreadId = ''

export function createStarchildMuiAdapter(auth: StarchildAuth): ChatAdapter {
  return {
    async sendMessage({ message, signal }) {
      // Extract text from user message.
      // ChatMessage.content type varies by MUI version — handle both
      // string and structured content arrays defensively.
      const text = typeof message.content === 'string'
        ? message.content
        : Array.isArray(message.content)
            ? (message.content as Array<{ type: string; text?: string }>)
                .filter(c => c.type === 'text' && c.text)
                .map(c => c.text!)
                .join('\n')
            : String(message.content ?? '')

      // Call SDK sendMessage — returns SSE Response
      const response = await auth.sendMessage(text, {
        threadId: currentThreadId || undefined,
      })

      const messageId = crypto.randomUUID()
      const textId = crypto.randomUUID()

      // Transform SSE → MUI ReadableStream<ChatMessageChunk>
      return new ReadableStream<ChatMessageChunk>({
        async start(controller) {
          controller.enqueue({ type: 'start', messageId })

          let textStarted = false
          try {
            for await (const event of parseStarchildSSE(response, signal)) {
              switch (event.type) {
                case 'agent_start':
                  lastSessionKey = event.session_key
                  if (event.thread_id) currentThreadId = event.thread_id
                  break

                case 'text_delta':
                  if (!textStarted) {
                    controller.enqueue({ type: 'text-start', id: textId })
                    textStarted = true
                  }
                  controller.enqueue({
                    type: 'text-delta',
                    id: textId,
                    delta: event.text,
                  })
                  break

                case 'tool_use': {
                  // ⚠️ Tool-call chunk types are MUI-version-specific.
                  // The names below follow the MUI X Chat streaming protocol
                  // convention but may differ — always verify against:
                  // https://mui.com/x/react-chat/behavior/streaming/
                  const toolId = event.tool_use_id || crypto.randomUUID()
                  controller.enqueue({
                    type: 'tool-call-start',
                    id: toolId,
                    toolName: event.tool_name,
                  } as ChatMessageChunk)
                  controller.enqueue({
                    type: 'tool-call-input-available',
                    id: toolId,
                    input: JSON.stringify(event.tool_input),
                  } as ChatMessageChunk)
                  break
                }

                case 'tool_output':
                  // ⚠️ Same caveat as tool_use — verify chunk type name.
                  controller.enqueue({
                    type: 'tool-result',
                    id: event.tool_use_id || '',
                    result: event.tool_output,
                  } as ChatMessageChunk)
                  break

                case 'agent_end':
                  if (textStarted) {
                    controller.enqueue({ type: 'text-end', id: textId })
                    textStarted = false
                  }
                  break

                case 'error':
                  // Error chunks: the runtime wraps thrown errors into
                  // ChatError automatically. Throwing here is also valid
                  // and may be cleaner — see MUI error handling docs.
                  controller.enqueue({
                    type: 'error',
                    message: event.message,
                  } as ChatMessageChunk)
                  break
              }
            }
            // Close any open text stream
            if (textStarted) {
              controller.enqueue({ type: 'text-end', id: textId })
            }
            controller.enqueue({ type: 'finish', messageId })
          } catch (e) {
            // On error/abort, emit abort chunk and let runtime handle cleanup.
            // The 'abort' chunk type ends the stream per MUI protocol.
            controller.enqueue({ type: 'abort', messageId })
          } finally {
            controller.close()
          }
        },
      })
    },

    // Optional: conversation list — powers the built-in sidebar
    async listConversations() {
      const { threads } = await auth.listThreads()
      return {
        conversations: threads.map(t => ({
          id: t.thread_id,
          title: t.title || 'Untitled',
          createdAt: t.created_at ? new Date(t.created_at) : new Date(),
          updatedAt: t.updated_at ? new Date(t.updated_at) : new Date(),
        })),
        hasMore: false,
      }
    },

    // Optional: message history on conversation switch
    async listMessages({ conversationId }) {
      const { messages } = await auth.listMessages(conversationId)
      return {
        messages: messages.map(m => ({
          id: m.message_id,
          role: m.role,
          content: (m.content_blocks || [])
            .filter(b => b.type === 'text')
            .map(b => b.text)
            .join('\n'),
          createdAt: m.created_at ? new Date(m.created_at) : new Date(),
        })),
        hasMore: false,
      }
    },

    // Optional: reconnect interrupted stream
    async reconnectToStream({ messageId, signal }) {
      if (!lastSessionKey) return null
      const response = await auth.reconnectStream(lastSessionKey)
      if (!response.ok) return null
      // Reuse the same SSE → chunk transform logic as sendMessage
      // (extract to a shared helper for production use)
      return new ReadableStream<ChatMessageChunk>({
        async start(controller) {
          controller.enqueue({ type: 'start', messageId })
          let textStarted = false
          const textId = crypto.randomUUID()
          for await (const event of parseStarchildSSE(response, signal)) {
            if (event.type === 'text_delta') {
              if (!textStarted) {
                controller.enqueue({ type: 'text-start', id: textId })
                textStarted = true
              }
              controller.enqueue({ type: 'text-delta', id: textId, delta: event.text })
            } else if (event.type === 'agent_end' && textStarted) {
              controller.enqueue({ type: 'text-end', id: textId })
              textStarted = false
            }
          }
          if (textStarted) controller.enqueue({ type: 'text-end', id: textId })
          controller.enqueue({ type: 'finish', messageId })
          controller.close()
        },
      })
    },

    // Optional: server-side cancel (when abort signal is not enough)
    stop() {
      if (currentThreadId) {
        auth.cancelRun(currentThreadId)
      }
    },
  }
}
ChatBox 使用
// pages/chat.tsx
import { ChatBox } from '@mui/x-chat'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import { StarchildAuth } from 'starchild-auth-sdk'
import { createStarchildMuiAdapter } from '@/adapters/starchild-mui-adapter'

const auth = new StarchildAuth({
  clientId: process.env.NEXT_PUBLIC_STARCHILD_CLIENT_ID!,
  scope: 'profile chat',
})

const theme = createTheme() // 或你的自定义 MUI 主题
const adapter = createStarchildMuiAdapter(auth)

export default function ChatPage() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <ChatBox
        adapter={adapter}
        features={{ conversationList: true }} // 启用内置会话侧边栏
        sx={{ height: 600 }}
      />
    </ThemeProvider>
  )
}
集成注意事项
要点说明
Thread 管理SDK sendMessage 不传 threadId 时自动创建新线程;传 threadId 续接已有对话。agent_start 事件会返回 thread_id,适配器应保存用于后续请求。
取消 / 中断SDK sendMessage 不接受 AbortSignal 参数(SendMessageOptions 无 signal 字段)。两个适配器都在 parseStarchildSSE 循环中检查 abortSignal.aborted 后 reader.cancel() 停止读取流;MUI 额外用 stop() 调 auth.cancelRun(threadId) 做服务端取消。注意:fetch 请求本身不会被 abort,只是停止消费响应流。
断线重连保存 agent_start 事件的 session_key,断线后调 auth.reconnectStream(sessionKey) 获取新的 SSE 流。
消息历史auth.listMessages(threadId) 返回 content_blocks[],需映射为各库的消息格式:text block → string / text part,tool_use block → tool-call part。
图片 / 文件SDK sendMessage 支持 images / files / quote 参数。在适配器中从 messages 的 attachments 提取后传入 SendMessageOptions。
ScopeChat 集成需要 scope: 'profile chat'。如需在 Chat 界面中显示 Credits 余额,加 credit:read。
Token 刷新SDK 自动每 12 分钟刷新 token,适配器无需关心 token 过期。onTokenRefreshFailed 触发时引导用户重新 login()。
工具调用 chunk 类型MUI X Chat 的完整 tool-call chunk 协议参考官方文档:https://mui.com/x/react-chat/behavior/streaming/ 。上述代码使用 tool-call-start / tool-call-input-available / tool-result,以最新文档为准。

错误处理

登录 popup
try {
  await auth.login()
} catch (err: any) {
  if (err.message?.includes('cancelled')) {
    // 用户关闭了弹窗
  } else if (err.message?.includes('blocked')) {
    // 浏览器拦截了弹窗 — 必须在用户点击事件中调用 login()
  }
}
JSON API:StarchildAuthError

getCredits / listThreads / wallet / credit 等 JSON helper 在非 2xx 时 throw StarchildAuthError(已从 starchild-auth-sdk 导出):

import { StarchildAuth, StarchildAuthError } from 'starchild-auth-sdk'

try {
  await auth.credit.getBalance()
} catch (e) {
  if (e instanceof StarchildAuthError) {
    // e.status / e.code / e.detail / e.path / e.insufficientScope / e.response
    if (e.insufficientScope) {
      // 引导重新 login() 申请完整 scopes,或检查 OAuth App 审核 scope
      await auth.login()
    }
  }
}
字段说明
statusHTTP 状态;无响应时为 0
code可选机器码
detail原始 body / detail
path请求路径
insufficientScopeOAuth scope 不足类 403 的启发式标记
response原始 Response(若有)
SSE / 原始 Response

sendMessage / reconnectStream 仍返回原始 Response,需自行检查 ok:

const stream = await auth.sendMessage('hello')
// 或 auth.chat.send('hello')
if (!stream.ok) {
  const error = await stream.json()
  console.error('API error:', error.detail)
  if (stream.status === 401) {
    // token 过期 — SDK 会尝试自动刷新;仍失败则 onTokenRefreshFailed
  } else if (stream.status === 403) {
    // 权限不足 — scope 不满足
  }
}

快速参考:所有 API 端点

分类端点方法Scope
Auth/v1/oauth/userinfoGETprofile
/v1/private/oauth/authorizePOST— (go-api)
/v1/oauth/refreshPOST— (go-api)
/v1/oauth/logoutPOST— (go-api)
Bind (SDK)auth.bindAccount() → 主站 /?account_tab=linked-accounts—profile
auth.getBindAccountUrl() / auth.isGuest()—profile
Token (SDK)getToken() / getRefreshToken() / refreshToken()——
Namespaceauth.profile / chat / threads / messages / containers / skills / media / shares / feedback / jobs / wallet / credit—同 flat
Chat/chat/streamPOSTchat
/chat/stream/reconnectPOSTchat
/chat/runs/cancelPOSTchat
/chat/modelGET/POSTchat
Threads/api/clawd/threadsGET/POSTchat
/api/clawd/threads/{id}GET/DELETEchat
/api/clawd/threads/searchGETchat
/api/clawd/threads/{id}/pinPOSTchat
/api/clawd/threads/{id}/titlePOSTchat
Messages/api/clawd/messagesGET/DELETEchat
Containers/api/cloud/containersGETchat
/api/cloud/containers/deployPOSTchat
/api/cloud/containers/startPOSTchat
/api/cloud/containers/stopPOSTchat
/api/cloud/containers/restartPOSTchat
/api/cloud/containers/wakePOSTchat
/api/cloud/containers/renamePUTchat
/api/cloud/containers/statusGETchat
/api/cloud/containers/metricsGETchat
/api/cloud/containers/compute-configPOSTchat
/api/cloud/containers/{id}/updatePOSTchat
Skills/api/skills/catalogGETchat
/api/skills/catalog/searchGETchat
/api/skills/catalog/{source}/{name}GETchat
Media/api/clawd/images/uploadPOSTchat
/api/audio/transcribePOSTchat
/v1/synthesizePOSTchat
Shares/api/clawd/sharesGET/POSTchat
/api/clawd/shares/{id}GET/DELETEchat
/api/clawd/shares/{id}/forkPOSTchat
Feedback/api/clawd/feedbackPUT/DELETEchat
Jobs/scheduled-jobsGET/POSTchat
Wallet/api/cloud/containers/portfolio/evmGETchat
/api/cloud/containers/walletsGETchat
/api/cloud/containers/walletPOST/DELETEchat
/wallet/exportPOSTchat
Billing/coinbase/onramp-sessionPOSTchat
WebSocket/ws/syncWSchat
/ws/terminal/{id}WSchat
/ws/metricsWSchat
Notifications/v1/agentx/notificationsGET/POSTchat
/v1/agentx/notifications/unread-countGETchat
Models/chat/modelsGETchat
Free quota/v1/free-quotaGETchat
Credits balanceGET {creditApiBase}/api/creditsGETcredit:read
Credits chargesGET .../api/chargesGETcredit:read
Credits topupsGET .../api/topupsGETcredit:read
Credits usageGET .../api/usage/dailyGETcredit:read
Credits pendingGET .../api/pendingGETcredit:read
Credits txGET .../api/tx/{hash}GETcredit:read
Stripe sessionPOST .../api/stripe/create-sessionPOSTcredit:write
Gift redeemPOST .../api/gift-cards/redeemPOSTcredit:write
Points balanceGET .../api/points/balanceGETcredit:read
Points exchangePOST .../api/points/exchangePOSTcredit:write
KYCGET/POST .../api/kyc/**read/write
ReferralGET/POST .../api/referral/**read/write
Migration rewardGET/POST .../api/migration/reward/**read/write
WOO bonusGET .../api/public/users/{id}/woo-bonusGETpublic

ローカルテストガイド(Agent 必読)

Agent 向けの実行原則:OAuth の login() は必ず実際のブラウザページでテストしてください(popup + ユーザー操作 + ページの Origin が必要です)。token を取得した後は、API を curl/Node スクリプトでテストできます。「純粋な Node で Origin がなくても popup ログインは通る」と仮定してはいけません。

1. 2 種類の Origin を混同しないこと
役割Origin の例設定する人
メインサイト web(認可 popup)http://localhost:6066 および https://localhost:6066サーバー側の静的 CORS で統一して許可済み(go-api CORS_ORIGINS / ai-agent CORS_ALLOWED_ORIGINS / clawd CORS_ORIGINS。本番 env にもこの 2 つを含めるべきです)
サードパーティアプリページ(SDK を統合したサイト)例:http://localhost:3333、http://localhost:5173その OAuth App の allowed_origins に記載する必要があります(登録/審査時に記入。ページのアドレスバーと完全に一致させること。scheme とポートを含みます)
  • Popup が開くのはメインサイト(ローカル web または https://iamstarchild.com)で、ブラウザの Origin はメインサイトです。
  • SDK はサードパーティページで動作し、API リクエストの Origin はサードパーティ origin です → OAuth client によって動的に CORS にマージされます。
  • authorize 時:Header Origin = メインサイト(trusted web)、body の origin = サードパーティ origin(client のホワイトリストで検証)。
2. 本番のデフォルト Base URL(SDK 0.4.x)

starchild-web と揃えており、デフォルトのままオンラインです。ローカル demo では通常変更不要です:

設定項目デフォルト
originhttps://iamstarchild.com(popup のメインサイト)
apiBasehttps://go-api.iamstarchild.com/v1
chatApiBasehttps://ai-api.iamstarchild.com
clawdApiBasehttps://preview.iamstarchild.com(HTTP chat/stream、jobs)
clawdWsBasewss://preview.iamstarchild.com(WS)
creditApiBasehttps://credit.iamstarchild.com

ローカルでバックエンド一式を連携する場合は、明示的に http://127.0.0.1:8000/v1、http://127.0.0.1:8008、http://127.0.0.1:8009 などに変更してください(リポジトリの clinerules ポート表を参照)。

3. シナリオ A — サードパーティのローカルページ + オンライン API(最も一般的)

目標:http://localhost:<port> で統合側のページを動かし、ログインと API は本番を叩く。

  1. OAuth App の allowed_origins にページの Origin(例:http://localhost:3333)が含まれ、status=approved、scopes が足りていること。
  2. ページで SDK を使用:clientId + 必要な scope。サードパーティ origin を 6066 に設定してはいけません。
  3. ブラウザでサードパーティページを開く → ユーザーがクリック → auth.login() → popup はオンラインの https://iamstarchild.com(デフォルトの origin)へ。
  4. ログイン成功後、同じページで auth.chat / auth.credit / SSE / WS を呼び出す。
  5. CORS が失敗する場合:サードパーティ origin が client のホワイトリストにあるか、本番の go-api/ai-agent/clawd の静的 CORS にメインサイト関連のドメインが含まれているか(ローカル web で popup をテストする場合のみ 6066 が必要)を確認してください。

リポジトリ内の参考:

  • SDK demo:starchild-auth-sdk/example/index.html、test-sdk-full.html(デフォルトは本番 URL)
  • Orderly の例:orderly-test-dex + starchild-orderly-plugin
4. シナリオ B — ローカルのメインサイト web(6066)+ ローカルまたはオンライン API

目標:starchild-web を改修する / authorize の仲介、Guest バインディングなどをテストする。

  1. starchild-web を起動:デフォルトは http://localhost:6066(https のローカル証明書を使う場合は https://localhost:6066 も可)。
  2. API 側の静的 CORS に以下が含まれることを確認:
    • http://localhost:6066
    • https://localhost:6066
    • (任意)https://starchild.dev:6066
  3. Env 名:
    • go-api:CORS_ORIGINS(設定するとコードのデフォルトをテーブル全体で置き換えるため、オンラインドメイン + 上記の 6066 を同時に保持する必要があります)
    • ai-agent:CORS_ALLOWED_ORIGINS(同上)
    • clawd:CORS_ORIGINS(entrypoint のデフォルトに 6066 の http/https が既に含まれています)
  4. サードパーティは引き続き自分の origin で登録します。ローカル web は popup/仲介のみを担当します。
5. シナリオ C — ブラウザ vs Node/スクリプト
ステップブラウザcurl / Node
login() popup必須不可(ウィンドウなし、実際のページ Origin なし)
token を持って userinfo / threads / credits を呼ぶ可能可能(Authorization: Bearer)
CORS の自動付与ブラウザが実行スクリプトに CORS はない。直結でよい
CORS が許可されるかテストDevTools / ページリクエストOPTIONS + Origin ヘッダで preflight を模擬

Agent 推奨フロー:

  1. ブラウザでログインを完了し、onLogin / DevTools / auth.getToken() から access token を取得(refresh はデバッグ用の getRefreshToken() のみ。外部に漏らさないこと)。
  2. 次に curl でマトリクス(scope ゲート、403、credits など)を実行。
  3. popup/CORS のリグレッションが必要な場合は、Playwright/実際のページを使用し、ページ URL の origin が必ず client のホワイトリストにあるようにしてください。
6. CORS プリフライト(Agent が直接実行可能)
# 期望:ACAO 回显同一 Origin(静态主站 web)
curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: http://localhost:6066' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type' \
  | tr -d '\r' | grep -i access-control-allow-origin

curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: https://localhost:6066' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type' \
  | tr -d '\r' | grep -i access-control-allow-origin

# 第三方本地 origin(须已在该 OAuth client allowed_origins,且 client approved)
curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: http://localhost:3333' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type' \
  | tr -d '\r' | grep -i access-control-allow-origin

# 期望:无 ACAO(拒绝)
curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: https://evil.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  | tr -d '\r' | grep -i access-control-allow-origin || echo 'DENY_OK'

ローカルバックエンドでは host を http://127.0.0.1:8000 / :8008 / :8009 に置き換えるだけです。

7. ログイン後の API スモーク(token 取得後)
TOKEN='<access_token from browser login>'
# profile
curl -s -H "Authorization: Bearer $TOKEN" https://ai-api.iamstarchild.com/v1/oauth/userinfo
# chat(无 chat scope 应 403)
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN" \
  https://ai-api.iamstarchild.com/api/clawd/threads
# credits(需 credit:read)
curl -s -H "Authorization: Bearer $TOKEN" https://credit.iamstarchild.com/api/credits
8. ローカルフルスタックのポート(リポジトリ開発)
サービスポート
starchild-webhttp://localhost:6066
go-apihttp://127.0.0.1:8000
ai-agenthttp://127.0.0.1:8008
clawdhttp://127.0.0.1:8009
credit-apitransparent-proxy のデプロイに準拠(本番は credit.iamstarchild.com)

動的 CORS:go-api / ai-agent / clawd は定期的に approved + active な OAuth client の allowed_origins を取得してホワイトリストにマージします(デフォルトで約 5 分。origin を変更した後反映されない場合は、サービスを再起動するか更新を待ってください)。

9. 安全注意(测试时)
  • 静态放行仅限精确 http://localhost:6066 与 https://localhost:6066,不是任意 localhost 端口。
  • 第三方每个端口/scheme 都要单独进 allowed_origins。
  • 不要把 refresh token 写进日志、issue、或发给用户聊天。
  • 生产 env 覆盖 CORS 时必须整表包含线上域名 + 需要的本地 web origin,避免只配 localhost 导致主站跨域全挂。
10. Agent 检查清单(测 SDK / OAuth 前勾选)
  • OAuth client allowed_origins 精确等于第三方测试页 Origin
  • client status=approved 且 is_active,scopes 含要测的能力
  • login() 在浏览器 click 路径调用
  • 主站本地 popup 时,API CORS 含 http://localhost:6066 与 https://localhost:6066
  • SDK base URL:打生产用默认;打本地后端再改 apiBase/chatApiBase/clawd*
  • CORS 失败先分清是「静态主站 origin」还是「OAuth 第三方 origin」
  • scope 矩阵:profile-only 不能 threads;credit:read 不能 write
  • 脚本测试只在已有 token 后进行

故障排查

问题原因解决
弹窗被拦截浏览器要求用户手势触发确保 login() 在 click handler 中调用
Origin 不匹配Allowed Origin 配置不符检查 OAuth Apps 的 origin 与第三方页面完全一致(含 http/https 与端口)
本地 web popup CORS6066 未进静态 CORS确认 go-api/ai-agent/clawd(及生产 env)含 http://localhost:6066 与 https://localhost:6066
第三方本地 CORSorigin 未进 client 或不在动态列表写入 allowed_origins 并 approved;等 CORS 刷新或重启 API
Node 脚本无法 loginpopup 依赖浏览器浏览器登录取 token 后再用脚本调 API
onLogin 不触发用户未确认授权或 popup 被关闭检查 onAuthCancelled 和 onAuthError
SSE 流中断网络波动或容器重启用 session_key 调用 reconnectStream()
WS 断开连接超时或服务器重启实现指数退避自动重连
403 on container deleteOAuth token 不允许删除容器这是预期行为,无法绕过
401 after token refreshrefresh token 过期引导用户重新 login()
Guest 需绑定正式账号userInfo.isGuest === true调用 auth.bindAccount() 跳转主站 Linked accounts,勿自建 /guest/bind;Guest 登录本身走 login() 主站 popup,无 loginAsGuest
需要 refresh token自建刷新或调试用 getRefreshToken();优先 SDK refreshToken();勿日志/外传
JSON API throw非 2xxcatch StarchildAuthError,看 insufficientScope
403 Insufficient scope credit:*token 未含 credit scope重新 login 并请求 credit:read/credit:write;检查 OAuth App 是否已审核通过
Credits 调不通 / CORSorigin 未在 client allowed origins与 chat 相同,origin 必须在 OAuth client 白名单
Stripe 成功但余额未变仍在 pending 或 tx 未 credited轮询 getCreditTxStatus / getPendingCredit / getCredits

コメント