Companion SDK — 集成快速上手(中文版)

English version: Integrator Quickstart。两份内容 同步维护;以英文版为准绳,中文版跟随更新。

把用户的 Pouchy 伙伴嵌进你的应用/游戏/网站——它带着对用户的了解而来(记忆、 名字、偏好),能文字或语音聊天、能图、能在你的应用里行动、能对 正在发生的事实时反应。本文是动手指南;线协议规范见 companion-sdk-protocol.md

想先看每个调用在真实后端上跑起来的样子?应用内 playground (/app/admin/timing#playground)端到端驱动每个平面并展示原始线报文—— 它就是下文一切的参考实现。


1. 拿到 token(30 秒)

标准路径(开发者/自带用户体系的应用):在 dashboard 创建项目,铸一枚 Secret Key (dashboard → Keys),由你的后端用它换取每用户的会话 token—— 首次出现的 external_user_id 自动开通,你的用户什么都不用配置:

POST https://pouchy.ai/v1/sessions
Authorization: Bearer pchy_sk_…
{ "agent": "<agentId>", "external_user_id": "user_4211" }

→ { "session_token": "pchy_…", "expires_in": 3600, "instance": { … } }

session_token 就是下文每段代码里的 token。这条流程的两个端到端完整案例: quickstart-romance-companion.mdquickstart-commerce-guide.md

进阶路径(个人/单用户集成):用户在 pouchy.ai 登录状态下通过 POST /api/companion/keys 生成 Personal Access Token(暂无应用内面板), 自选愿意授予的 scopesmodalities(省略 scopes 取安全默认包), 然后粘贴进你的应用。一枚 token = 一个用户 = 处处同一个伙伴与记忆。本指南其余 部分对两种 token 完全一致。

Authorization: Bearer pchy_<secret>

常用 scopes(未授予则不可达):

Scope 解锁
chat 文字轮次
events.subscribe 必需——接收回复(事件流)
worldstate.write 流式送入实时上下文(§4——陪伴通道)
call 语音通话
files 发送图片(多模态)
memory.read:core 伙伴能召回用户的真实记忆(名字、偏好)
memory.read:app / memory.write:app 你应用自己的沙箱记忆命名空间
wallet.spend · social.message · skills.execute 代用户行动(敏感)

token 没有某个 scope 就到不了那个能力——从最小集开始。


2. 安装与连接

import { createCompanion } from '@pouchy_ai/companion-sdk'; // or src/lib/companion-sdk

const companion = createCompanion({
  baseUrl: 'https://pouchy.ai',
  token: PCHY_TOKEN,
  surface: 'my-rpg',          // one resumable session per surface
  modalities: ['text', 'call', 'files'],
  contextKinds: ['game.player.*', 'game.event.*'], // what you'll stream (§4)
});

const ack = await companion.connect();   // { session, grantedScopes, modalities, resumeCursor }
companion.start();                        // open the auto-reconnecting event stream

companion.onMessage((text) => render(text));          // assistant replies
await companion.sendText('hey, I just logged in');     // → reply via onMessage

start() 需要 events.subscribe;没有它回复永远不会到达(SDK 会发出 control.error { code: 'stream_unauthorized' } 提醒你)。stop() 关闭流。


3. 五个平面

文字 + 图片

await companion.sendText('what do you think of this?', { images: [dataUrl] }); // needs `files`
companion.onMessage((text) => …);

照片先在客户端缩到 ≈1024px JPEG 再发——原始相机图会超请求体上限。

语音通话

const creds = await companion.startCall({ locale: navigator.language });
// creds is discriminated by provider — open WebRTC directly to it:
//  'elevenlabs-convai' → { token, agentId, instructions, voice?, language? }
//  'openai-realtime'   → { clientSecret, model, voice, expiresAt }

Pouchy 完全不在音频路径里;你与 provider 点对点直连。传 locale,通话 就用用户的语言和克隆声线开启。可运行的连接示例见 playground 的 startVoiceCall

工具——让伙伴在你的应用里行动

createCompanion({ …, tools: [{
  name: 'give_item',
  description: 'Give the player an item.',
  parameters: { type: 'object', properties: { item: { type: 'string' } }, required: ['item'] },
}]});

companion.onToolCall(async ({ id, name, argsJson }) => {
  if (name === 'give_item') {
    grantItem((argsJson as { item: string }).item);   // argsJson = pre-parsed args
    await companion.sendToolResult(id, { ok: true, result: 'done' });
  }
});

伙伴只会调用你声明过 token 授权过的工具。一轮的全部工具结果就位后, 它继续并回复。

记忆

const facts = await companion.recall({ limit: 8 });          // ranked, scope-filtered
await companion.remember({ content: 'Plays as a healer main', importance: 60 });

World-state——陪伴通道(见 §4)

companion.sendWorldState({ type: 'game.scene', data: 'rainy tavern', retained: true });

4. 该往里流什么(怎么流)——陪伴得好的关键

这个问题决定伙伴是"在场"还是"茫然"。双向模式下你的应用流式送入正在发生 的实时画面;伙伴对它作出反应——聊天里在下一轮,带 voiceRelevant通话中当场开口。只有 type 必填;其余服务端补全。

两种口味

retained: true ——状态 retained: false(默认)——时刻
含义 最新值,"现在是什么" 离散事件,"刚发生了什么"
例子 hp、场景、位置、正在播放的歌、页码、心率 升级、Boss 出现、进球、切歌、破 PR
服务端 type 合并(每帧变的值不会吵醒 agent) 作为事件投递,按 salience 排序
用途 被问到时它知道("我血量怎样?") 它会反应("漂亮,10 级了!")

时刻的旋钮:salience(0–1,该多抢注意力)、voiceRelevant: true(推进 进行中的通话让它现在说)、ttl(retained 值保鲜毫秒数)。

按应用类型的信号菜单

应用 流为状态(retained) 流为时刻(salience / voiceRelevant)
游戏 game.player.hp, game.player.location, game.scene game.event.boss_spawned (0.9, voice), game.event.level_up (0.7), game.event.death (0.95, voice)
视频/直播 media.title, media.scene ("a car chase") media.event.plot_twist (0.8, voice), media.event.ended (0.6)
音乐 music.now_playing, music.mood music.event.skipped, music.event.liked (0.5)
阅读 read.book, read.chapter, read.highlight read.event.finished_chapter (0.6)
健身/穿戴 fit.heart_rate, fit.activity ("running"), fit.steps fit.event.goal_hit (0.8, voice), fit.event.set_done (0.6)
购物 shop.cart, shop.viewing shop.event.price_drop (0.7), shop.event.checkout (0.6)
效率 work.current_doc, work.focus_mode work.event.task_done (0.5), work.event.deadline_near (0.8, voice)
环境/IoT home.room, env.weather home.event.doorbell (0.9, voice)

要 / 不要

  • 用稳定的点分命名空间起名(game.player.hp)并在 contextKinds 里 声明——服务端据此区分环境值与触发器。
  • 只把真正值得打断的少数时刻标 voiceRelevant——这是通话里的魔法 ("Boss 来了——先加血!")。保持稀少;它会盖过用户说话。
  • 让高频变化的东西走 retained(位置、hp、播放进度)。合并的,便宜。
  • 不要把逐帧原始遥测当时刻流——会被限流并浪费用户配额;服务端只会注入 紧凑的滚动摘要,绝不是火线。
  • 不要把机密/PII 放进 data——它可能到达模型与记忆。

最小"陪玩"循环

// ambient state — coalesced, consulted when relevant
companion.sendWorldState({ type: 'game.player.hp', data: { hp: 80 }, retained: true, ttl: 5000 });

// a moment worth reacting to, out loud, mid-call
companion.sendWorldState({
  type: 'game.event.boss_spawned',
  data: { boss: 'Dragon' },
  salience: 0.9,
  voiceRelevant: true,
});
// → in a call, the companion says: "Boss incoming — heal up first!"

5. 可靠性

  • 恢复:流从 resumeCursor 自动重连;错过的出站事件按 cursor 重放、 按 id 去重。你不用管。
  • 错误:失败以 control.error { code, message } 出现在流上(用 companion.on('control.error', …) 订阅),REST 调用直接 reject 带消息。 永久性的流鉴权失败会停掉重试循环并发出 stream_unauthorized,不会静默 空转。
  • 从最小开始:先只上 chat + events.subscribe,确认回复通了,再按需 加 worldstate.writecallfiles 和敏感 scopes。

6. 下一步

  • 把你后端的数据交给 Agent——View(读你的权威状态)、Action(经用户 同意改它)、Event(变化时通知它),纯签名 HTTPS,背后接什么数据库都行: 快速上手:Agent 数据面(中文版)。这是 上面客户端工具调用覆盖不到的服务端平面——客户端零改动。
  • 规范/消息类型:companion-sdk-protocol.md
  • Scope 词表:src/lib/types/companion-scopes.ts
  • 活参考(每个调用、原始线报文):playground,/app/admin/timing#playground