skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
starchild-ai-agent/official-skills194 installs

starchild-auth

Starchild Auth SDK: add OAuth login to any web app with one SDK. Use when integrating Starchild login into a project (e.g. add Starchild sign-in to my React app, set up OAuth with iamstarchild.com, implement login/logout with Starchild Auth SDK). Also use for local OAuth/CORS testing guidance (localhost:6066 web, third-party localhost origins, browser vs Node).

How do I install this agent skill?

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

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    The skill is a comprehensive developer integration guide for the Starchild Auth SDK. It provides instructions on implementing OAuth login, managing API requests for agent services, and handling authentication tokens. All referenced resources, including npm packages and API endpoints, belong to the official starchild vendor or are hosted on well-known, reputable public registries.

  • Socketwarn

    1 alert: gptSecurity

  • Snykwarn

    Risk: MEDIUM · 3 issues

What does this agent skill do?

🔑 Starchild Auth SDK — 完整开发指南

Integrate Starchild OAuth login into any web application. The SDK handles OAuth popup flow, token refresh, unified StarchildAuthError, namespaced helpers (auth.chat / auth.credit …), and 60+ API methods for full Agent access.

版本策略

产物当前版本何时 bump
npm starchild-auth-sdk0.4.1代码 / 公开 API 变更
本 Skill starchild-auth1.11.0集成指南 / 场景文档变更(可与 package 独立)

两套 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 (可选): 自定义 Agent 行为
  3. 仅选 profile → 自动通过,立即获得 Client ID
  4. 选了 chat / credit:read / credit:write → 进入管理员审核,审核通过后才生成 Client ID

Scope 权限体系

Scope权限范围审核
profile查看用户名、头像、ID自动通过
chatAgent 对话、线程管理、容器管理、技能、媒体、定时任务、钱包读取、计费、WebSocket需审核
credit:read查看 Credits 余额和账户状态需审核
credit:write充值/购买 Credits、兑换 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)

<!-- UMD build — use with plain <script> tags -->
<script src="https://unpkg.com/starchild-auth-sdk/dist/starchild-auth.umd.cjs"></script>

<!-- China mirror -->
<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 分钟
})

Token 生命周期

  • 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 tab)
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()
Namespace主要方法
auth.profilefetchUserInfo, getUserInfo, isGuest, bindAccount, getBindAccountUrl
auth.chatsend/sendMessage, reconnect/reconnectStream, cancelRun, getModel/setModel, WS factories
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(见场景八)

Points 兑换、KYC、Referral 已对 OAuth 开放(需 credit:read / credit:write),不是主站专属。


场景一:发送消息并读取 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 事件类型

Event Type含义关键字段
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

场景二:管理对话线程

// 创建线程
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)

场景三:管理容器

容器是运行 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)  // 总是失败

场景四: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 }
  }
}

场景五:技能与媒体

// 浏览技能目录
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' }

场景六:分享与反馈

// 创建对话分享
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)

场景七:钱包与计费

// 获取投资组合
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

场景八: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. 两套 Origin,不要混

角色Origin 示例谁配置
主站 web(授权 popup)http://localhost:6066 与 https://localhost:6066服务端静态 CORS 已统一放行(go-api CORS_ORIGINS / ai-agent CORS_ALLOWED_ORIGINS / clawd CORS_ORIGINS;生产 env 也应包含这两项)
第三方应用页(集成 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-api按 transparent-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

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/starchild-ai-agent/official-skills/starchild-auth">View starchild-auth on skillZs</a>