超视距遥操(Teleop)示例 · JS / 浏览器

超视距遥操(Teleop)示例 · JS / 浏览器

⚠️ 尚未上线:本能力为 v1.5.0-SNAPSHOT 开发中特性,接口与协议可能调整,正式发布前请勿用于生产环境。

v1.5.0。完整可运行示例在 agentsdk_for_js/example/teleop/,包含前端页面、AudioWorklet 采集、TRRO 视频拉流,以及一个 Node 签名 / 代理服务 server.mjs。本页给出最小可读的核心片段。

示例结构

文件作用
example/teleop/server.mjsNode:静态服务 + /api/ws-auth HMAC-SHA256 签名 + WebSocket 代理(JSON ping ↔ 协议级 Ping)
example/teleop/main.js前端主逻辑:连接 → 进入遥操 → 麦克风推流 → 退出
example/teleop/pcm-processor.jsAudioWorklet:把麦克风重采样到 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页面骨架

整体流程

  1. 登录:输入邮箱 + 验证码,向 open.agibot.com 换取 token 存 localStorage;
  2. 连接:点击「链接」,authProvider POST /api/ws-auth,服务端算签名连上游、返回本地代理票据 URL,SDK 打开 WebSocket;
  3. 进入遥操:点击「进入遥操」,createTeleopSession + enter();如启用 TRRO,则并行取 TRRO 鉴权并开始拉流;
  4. 推流:点击「开始录音」,getUserMedia(24kHz 单声道)→ AudioWorklet 出 960 样本 Int16 帧 → base64 → sendAudio;停止时 finishAudio;
  5. 退出:exit() + 停录 + 停 TRRO;
  6. 断开: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。