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.md 与
quickstart-commerce-guide.md。
进阶路径(个人/单用户集成):用户在 pouchy.ai 登录状态下通过
POST /api/companion/keys 生成 Personal Access Token(暂无应用内面板),
自选愿意授予的 scopes 与 modalities(省略 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.write、call、files和敏感 scopes。
6. 下一步
- 把你后端的数据交给 Agent——View(读你的权威状态)、Action(经用户 同意改它)、Event(变化时通知它),纯签名 HTTPS,背后接什么数据库都行: 快速上手:Agent 数据面(中文版)。这是 上面客户端工具调用覆盖不到的服务端平面——客户端零改动。
- 规范/消息类型:
companion-sdk-protocol.md - Scope 词表:
src/lib/types/companion-scopes.ts - 活参考(每个调用、原始线报文):playground,
/app/admin/timing#playground