超视距遥操(Teleop)示例 · JS / 浏览器
超视距遥操(Teleop)示例 · JS / 浏览器
⚠️ 尚未上线:本能力为 v1.5.0-SNAPSHOT 开发中特性,接口与协议可能调整,正式发布前请勿用于生产环境。
v1.5.0。完整可运行示例在
agentsdk_for_js/example/teleop/,包含前端页面、AudioWorklet 采集、TRRO 视频拉流,以及一个 Node 签名 / 代理服务server.mjs。本页给出最小可读的核心片段。
示例结构
| 文件 | 作用 |
|---|---|
example/teleop/server.mjs | Node:静态服务 + /api/ws-auth HMAC-SHA256 签名 + WebSocket 代理(JSON ping ↔ 协议级 Ping) |
example/teleop/main.js | 前端主逻辑:连接 → 进入遥操 → 麦克风推流 → 退出 |
example/teleop/pcm-processor.js | AudioWorklet:把麦克风重采样到 24kHz、切成 960 样本 Int16 帧 |
example/teleop/trro-client.js | 腾讯 TRRO Web SDK 封装(拉机器人现场视频 / 音频) |
example/teleop/login.js | 登录取 token(open.agibot.com) |
example/teleop/index.html / styles.css | 页面骨架 |
整体流程
- 登录:输入邮箱 + 验证码,向
open.agibot.com换取 token 存localStorage; - 连接:点击「链接」,
authProviderPOST/api/ws-auth,服务端算签名连上游、返回本地代理票据 URL,SDK 打开 WebSocket; - 进入遥操:点击「进入遥操」,
createTeleopSession+enter();如启用 TRRO,则并行取 TRRO 鉴权并开始拉流; - 推流:点击「开始录音」,
getUserMedia(24kHz 单声道)→ AudioWorklet 出 960 样本 Int16 帧 → base64 →sendAudio;停止时finishAudio; - 退出:
exit()+ 停录 + 停 TRRO; - 断开:
sdk.release()关闭连接并移出单例。
核心片段:连接 + 进入 + 推流
js
import { createAgentSdk, AgentEventType } from 'linksoul-agentsdk';
const appId = '您通过灵心开放平台创建的应用获取的appId';
const agentId = '您通过灵心平台创建的二开智能体获取的agentId';
const upstreamUrl = 'wss://open.agibot.com/api/V1/open-portal/app/wss/agent-sdk';
let sdk;
let session;
async function connect() {
// 单例:切换连接前先释放旧实例
sdk?.release();
sdk = createAgentSdk(appId, {
authProvider: async () => {
const res = await fetch('/api/ws-auth', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: upstreamUrl, appId }),
});
const { url } = await res.json();
return { url };
},
reconnect: true,
});
sdk.on('message', ({ data }) => {
let msg;
try { msg = JSON.parse(typeof data === 'string' ? data : ''); } catch { return; }
if (msg.type === AgentEventType.TeleopEnterAck) {
console.log('enter ack code =', msg.code);
} else if (msg.type === AgentEventType.TeleopExitAck) {
console.log('exit ack code =', msg.code);
}
});
sdk.on('connected', () => console.log('connected'));
sdk.on('disconnected', ({ willReconnect }) => console.log('closed, willReconnect=', willReconnect));
await sdk.connect();
}
function enterTeleop() {
session = sdk.createTeleopSession({ agentId });
session.enter(); // ACK 在上面的 message 监听里
}
// 麦克风推流:24kHz / 单声道 / Int16 PCM
async function startRecording() {
const startEventId = session.startAudio({ role: 'male', threshold: 0 });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext({ sampleRate: 24000 });
await ctx.audioWorklet.addModule('pcm-processor.js');
const src = ctx.createMediaStreamSource(stream);
const node = new AudioWorkletNode(ctx, 'pcm-processor');
node.port.onmessage = (e) => session.sendAudio(toBase64(e.data)); // e.data: ArrayBuffer(Int16)
src.connect(node);
}
function stopRecording() {
session.finishAudio();
}
function exitTeleop() {
session.exit();
}
function toBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary);
}
核心片段:服务端签名代理(server.mjs 摘要)
js
import { createHmac, randomUUID } from 'node:crypto';
app.post('/api/ws-auth', async (req, res) => {
const { url, appId } = req.body;
const appKey = process.env.APP_KEY; // 凭证只在服务端
const appSecret = process.env.APP_SECRET;
const target = new URL(url);
const timestamp = String(Date.now());
const nonce = randomUUID().replace(/-/g, '');
const signature = createHmac('sha256', appSecret)
.update(`GET\n${target.pathname}\n${timestamp}\n${nonce}`)
.digest('hex');
// 用这些头连上游网关,握手成功后签发一次性票据 t,返回本地代理 URL
const headers = {
'X-App-Id': appId,
'X-App-Key': appKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'X-Callback-Types': JSON.stringify([]),
};
const ticket = issueTicket(target, headers);
res.json({ url: `ws://localhost:${PORT}/api/ws?t=${ticket}` });
});
代理层还负责把浏览器发来的 JSON {type:"ping"} 转成 WebSocket 协议级 Ping,把网关的协议级 Pong 合成 JSON {type:"pong"} 回传,从而让 SDK 的 keep-alive 在浏览器下正常工作。
定时探活(keepAlive)
js
let online = true;
let timer;
function startKeepAlive() {
timer = setInterval(() => { if (online) session.keepAlive(); }, 10_000);
}
sdk.on('message', ({ data }) => {
let msg; try { msg = JSON.parse(String(data)); } catch { return; }
if (msg.type === AgentEventType.TeleopKeepaliveAck && msg.code !== 0) {
online = false;
clearInterval(timer);
session.exit(); // 掉线自动退出
}
});
常见改写要点
| 想做 | 改哪里 |
|---|---|
| 一次遥操中多次讲话 | 每段重新 startAudio(param) 拿新 eventId,sendAudio / finishAudio 用该 eventId;无需 exit 再 enter |
| 切换音色 / 灵敏度 | 每段 startAudio 时重填 { role, threshold } |
| 中途取消遥操 | 直接 exit(),不再补 finishAudio |
| 感知断连 / 重连 | 监听 sdk.on('disconnected'/'reconnecting'/'connected');connected 里重建 TeleopSession 并重新 enter |
| 多机器人同时遥操 | 每个 agentId 建一个 TeleopSession,复用同一 AgentSdk 连接 |
音频格式约定(示例值)
| 项 | 值 |
|---|---|
| 采样率 | 24000 Hz |
| 声道 | 单声道 |
| 位深 | 16-bit 有符号 PCM(Int16 小端) |
| 帧大小 | 960 样本 / 帧(≈ 40ms @ 24kHz,原始 1920 字节) |
| 编码 | 原始 PCM 字节 base64 后放入 sendAudio |
麦克风 /
getUserMedia/crypto.randomUUID均要求安全上下文(https 或 localhost)。示例在本地跑请用localhost。