OneSay文档

文档

JavaScript快速入门

预览版:功能和API可能会变化。

PDF由浏览器的打印对话框生成,不是托管的导出服务。

要显式提供音频并获取任务结果,选择语音任务;要实时发布和订阅音频,选择实时语音(RTC);要在房间里和Agent语音对话,选择OneSay Agent。三者都使用OneSay JavaScript SDK,但生命周期彼此独立。SDK目前处于预览阶段,尚未发布。

JavaScript + 语音任务#

从下文的公开任务示例及其使用方项目布局入手。它导出runPublicTaskSample(input, ports)(位于examples/javascript-quickstart/public-task.mjs)。如需应用准入,runAuthorizedTaskSample(input, ports)(位于examples/javascript-quickstart/authorized-task.mjs)组合了公开API和显式的用户同意决定;授权演练列出了它的输入。

在SDK发布之前,安装@onesay/sdk时使用预览版提供的包归档文件(.tgz):把它放在你的package.json旁边,用file:引用声明依赖(与examples/javascript-quickstart/package.json的做法相同),再用pnpm install --ignore-scripts安装。请提供当前的项目、环境和音频源绑定,通过SDK的Token提供器获取的短期凭据,以及已采集好的16 kHz单声道PCM s16le音频。任务客户端负责WSS/OSAF分帧、序号、ACK、背压、重放和恢复以及取消;它不采集音频,也不申请麦克风权限。为任务创建的房间不会启动RTC会话或Agent。

一次语音任务会话按以下顺序进行:

  1. 选择项目的Test环境。
  2. 在浏览器中通过tokenProvider提供短期访问Token;切勿嵌入osp_*凭据。
  3. 从Control API获取一次性Realtime票据,票据针对目标房间和项目环境。
  4. 使用onesay.realtime.v1连接,打开音频SignalStream,发送应用自己采集的16 kHz单声道PCM s16le帧。在浏览器中,AudioWorklet是可选的采集方式之一。
  5. 服务端先发送task.started,音频传输期间再发送audio.ack和artifact.delta。每一帧都要保留,直到连续的audio.ack覆盖它。
  6. 发送signal.end。服务端依次返回signal.ended、artifact.finalized和task.completed;等待任务进入这一终态。

票据只能使用一次。用同一张票据建立第二个连接时,即使第一个连接仍未关闭,服务端也会以关闭码1008 ticket_invalid拒绝它;请改为申请新票据。

在控制台试用语音任务#

想在写代码之前先看看语音任务的听写效果,可以在Staging上登录控制台,打开其中的“语音任务”一节。选好麦克风音频源和语言后开始说话:页面会实时显示陆续到达的文字,停止后显示最终文字和用量。它走的链路和上面的示例相同:以你当前登录的身份,在所选项目和环境中,通过SDK的任务客户端,经由任务WSS/OSAF运行一个OneSay听写任务;页面自己负责的部分,只是把麦克风采集成16 kHz的PCM帧。

凡是创建、发送或结束资源的步骤(房间、麦克风信号、取消听写、关闭房间),都会先准备成一项用户同意请求,只有你在页面上允许后才会执行;选择拒绝则不会执行。服务端会对每个操作重新检查,页面本身不授予任何权限。每次听写都消耗试用的音频额度,面板会在达到任务的音频上限之前自动停止听写。你也可以不停止而直接取消听写,用完后离开并关闭房间。

这一节只在Staging上提供,且仅面向已加入试用的账号。它展示的是这条链路的工作方式,不用来衡量识别质量。

JavaScript + 实时语音(RTC)#

使用examples/javascript-rtc/中的浏览器示例。其中的sample.mjs导出createRTCExample;页面使用已安装的@onesay/sdk包,并调用公开的client.rtc.connect门面接口。复制这个目录,把SDK包归档文件放在它的package.json旁边,文件名与其中file:引用的一致,然后安装并启动本地服务器,服务器会输出页面的本地URL:

Text
pnpm install --ignore-scripts
pnpm start

按Ctrl+C可停止服务器。示例不会创建API代理。完整的操作示例见examples/javascript-rtc/README.md。

页面的SDK环境是staging。连接需要你有权使用的环境、Control CORS允许的应用来源、现有的房间、应用会话和音频源资源,以及短期的用户或应用会话Token。页面只在内存中保存Token;切勿提供平台API密钥或服务商密钥。

连接时显式指定初始接收集合,再通过对应的控件请求发布和播放。订阅为空表示拒绝接收任何音频。RTC连接不会创建任务或Agent;创建和停止Agent是需要单独授权的操作。在本地关闭会话,不能确认服务端已撤销连接。

JavaScript + OneSay Agent#

OneSay Agent是一个语音参与者:它加入你的房间,收听你选定的发布,并用语音回答。你可以打断它:在它回答时开口,它就会停下来听你说。不想写代码时,登录控制台,使用其中的“语音通话试用”部分即可试用(仅限Staging;你的账号需要已开通试用)。

在代码中,按RTC一节加入房间并发布麦克风音频,然后用显式的输入集合和时长启动Agent:

TypeScript
const idempotencyKey = crypto.randomUUID();
const agent = await client.rtc.agents.create({
  roomId, appSessionId, mode: "realtime", publicationIds: [microphonePublicationId],
  maxDurationSeconds: 120, idempotencyKey,
});

每个创建、更改或结束资源的调用都需要用户确认(用户同意):在用户确认之前,服务器会返回consent_required。确认后,用同一个idempotencyKey重试。

请保留幂等键:如果响应丢失,用同一个键再次调用会返回同一个Agent,而不会再启动一个。用client.rtc.agents.get(roomId, agent.agentExecutionId)读取Agent,最多每2秒一次,直到playback.facts.outputPublicationId给出它的输出,然后订阅这个发布即可听到它。

observedState含义
pending已接受;Agent尚未运行
runningAgent已开始运行
failed在你停止之前,Agent自行结束了
unknownOneSay无法确认Agent的状态(它不再上报、在你停止后结束,或达到了最长时长)
stopped已确认停止

截至2026-10-26,Staging在Agent工作期间返回running(你请求停止后也是如此),Agent结束后返回unknown(如果在任何停止请求之前自行结束,则返回failed)。如果迟到的上报表明Agent是先自行结束的,unknown仍可能变为failed。OneSay目前还不会返回stopped;effectiveStoppedAt为null表示停止尚未得到确认。

desiredState是你请求的期望状态;observedState是OneSay观察到的实际状态。收听期间,RTC会话的audio.agentStates会把Agent输出的状态报告为initializing、idle、listening、thinking、speaking或unknown:这是低频的界面提示,不能证明Agent做了什么。用client.rtc.agents.stop(roomId, agentExecutionId, { idempotencyKey })停止它;停止被接受只表示请求已提交,不代表Agent已经离开。

截至2026-10-26,Staging标准试用:每次对话最长120秒,并有费用预算;被打断的回答同样会消耗预算。账号能进行多少次对话由它的免费额度决定,而不是按次数限制;新账号的自动开通尚未上线。更长的对话(特别长的回答会被提前截断,但至少会先说约30秒)已经实现,将随后续的Staging更新提供。

使用已安装包的公开任务示例#

入口是runPublicTaskSample(input, ports) (../../examples/javascript-quickstart/public-task.mjs)。它导入公开的@onesay/sdk入口。它不会创建身份、项目环境、房间、音频源、用户同意或授权,也不会自行签发连接票据。调用方必须已持有已授权的资源ID,以及针对目标房间和项目环境新签发的一次性票据。房间的访问权限由服务端签发的这张票据承载;此函数不接收单独的roomId参数,也不会解码票据来给自己授权。

准备使用方项目布局#

使用一个单独的项目,并按语音任务一节的说明安装@onesay/sdk。不要用工作区别名替代它,也不要删除它声明的依赖。

public-task.mjs从./support/导入两个支持模块:pcm-session.mjs为示例PCM分帧,并观测ACK、背压和重放;task-usage-readback.mjs通过公开API重新读取任务及其用量。把它们放进support/文件夹,与public-task.mjs位于同一目录:

Text
consumer/
  package.json
  node_modules/@onesay/sdk/
  public-task.mjs
  support/
    pcm-session.mjs
    task-usage-readback.mjs

调用导出的函数#

下面是放在public-task.mjs旁边的应用代码。参数值由你已获授权的应用提供;代码不嵌入也不输出任何Token。下面的observeAuthorizedTask是本指南写的包装函数,不是OneSay SDK额外导出的接口。

JavaScript
import { buildRealtimeConnectPlan } from "@onesay/sdk";
import { runPublicTaskSample } from "./public-task.mjs";

export async function observeAuthorizedTask({
  projectId,
  environmentId,
  sourceId,
  issuedSingleUseTicket,
  signal,
  ports,
}) {
  return runPublicTaskSample(
    {
      projectId,
      environmentId,
      sourceId,
      plan: buildRealtimeConnectPlan("staging", issuedSingleUseTicket),
      signal,
      timeoutMs: 5000,
    },
    ports,
  );
}

buildRealtimeConnectPlan(environment, ticket)是公开的SDK函数。它规划WSS连接,不做票据交换。示例从Staging Control环境重新读取用量;它不是生产环境的端点配置。只在你有权使用的环境中运行。

四个端口都是应用必须提供的回调,不是SDK方法:

端口回调形式和职责
webSocketFactory(url, protocols, options) => socket。返回与SDK兼容的WebSocket传输。遵守传入的连接和大小上限,禁用重定向,不要放宽证书和主机名校验。
confirmedControlsFactory(client, { signal }) => controls,或返回controls的Promise。通过公开接口约定,接入应用中显式的授权与用户同意选择;绝不默认确认。
readbackTokenProvider({ signal }) => shortLivedToken,或返回该Token的Promise。提供一个当前有效、有权在所请求范围内读取任务和用量的主体。长期有效的平台密钥或服务商密钥不能放在这里。
readbackTransport(url, init) => Promise<Response>。提供与Fetch兼容的传输,遵守init.signal,并保留SDK的请求头、禁止重定向策略和范围。

confirmedControlsFactory返回的controls必须实现以下形式:

Text
send(type, data, messageId, { signal })
flush({ signal })
observe(event, { signal })

send和flush可以返回Promise;observe是同步的事件观测器。用户同意的选择是应用显式做出的决定,由服务端最终裁决。返回这些方法并不授予权限:服务端仍会校验票据、音频源、操作、授权和用户同意。

PCM、完成和失败行为#

本示例恰好生成两个16 kHz、640个采样的PCM16LE单声道帧,即80毫秒的固定合成输入。它会有意触发有上限的背压,并通过SDK重放第一个未确认的帧。重复的重放既不是新的音频输入,也不是第二个任务。示例中没有麦克风采集、AudioWorklet、操作系统依赖或媒体权限请求。

如果应用要录制实时音频,采集和权限由调用方负责。AudioWorklet只是可选的采集方式之一;SDK没有音频采集导出。任务PCM的这一边界不影响RTC的publishMicrophone行为。

示例会等待连续ACK、匹配的任务和产物终态,以及有上限的公开任务和用量查询,并且只在SDK观察到传输关闭之后才返回结果。结果只含计数:pcm报告已发送、已确认和已重放的帧数,客户端背压拒绝次数,丢弃的帧数和ACK延迟百分位;readback报告观察到的任务数、扫描的用量事件数和读取的页数。结果不含音频、转写文本、票据或Token,也不是计费账单。

HTTP 401/403会直接结束本次调用,不会自动重试。用量缺失、重复或属于其他范围,任务与产物关联错误,缺少ACK,调用方取消,截止时间已过期,以及清理失败,都会让本次调用失败。再次调用前,先通过应用的显式流程重新获取授权;不要重放已消费的票据。

timeoutMs限制整个操作的时长,也单独限制等待关闭的时长。示例会把中止信号传给异步的factory、control、Token和HTTP端口。回调必须遵守传入的signal,并释放自己占用的资源。有上限的等待无法强制停止不配合的回调,也无法撤回它对外部造成的副作用;同步回调还必须避免阻塞事件循环。排查失败时,切勿把连接计划URL、票据、Token、原始PCM或转写文本写入日志。

通过现有公开API完成应用授权#

runAuthorizedTaskSample(input, ports)(位于examples/javascript-quickstart/authorized-task.mjs)是一个应用示例:它自行获取连接材料,并显式确认受保护的操作。它组合runPublicTaskSample,不替代SDK的任务生命周期、ACK、重放、背压或取消逻辑。这是示例导出,不是新的OneSay SDK API。

把authorized-task.mjs放在public-task.mjs旁边。它还导入一个支持模块support/confirmed-controls-core.mjs:这是一个有上限的队列,每条受保护的命令都要等确认完成后才会发出。它从不自行确认任何操作。

JavaScript
import { runAuthorizedTaskSample } from "./authorized-task.mjs";

export async function runApplicationTask(resources, signal, ports) {
  return runAuthorizedTaskSample({
    organizationId: resources.organizationId,
    projectId: resources.projectId,
    environmentId: resources.environmentId,
    deviceId: resources.deviceId,
    roomId: resources.roomId,
    sourceId: resources.sourceId,
    signal,
    timeoutMs: 60000,
    delegationTtlSeconds: 120,
  }, ports);
}

所有资源都必须已存在于目标环境中。激活、登录、设备注册、凭据和能力策略仍由应用现有的已授权流程负责。本示例不会创建授权、修改权限,也不会给自己授予访问权限。

应用端口职责
humanTokenProvider({ signal })提供当前有效的短期用户Token,它须有权执行公开读取、委托管理和交互式用户同意决定。
deviceTokenProvider({ signal })为目标设备另外提供当前有效的短期设备Token。不要拿用户Token顶替,也不要传入设备长期有效的引导密钥。
confirmConsent(snapshot, { signal })展示经过校验、不可更改的服务端呈现内容,在用户明确决定后只返回"allow"或"deny"。遵守取消请求。绝不默认允许。
fetch(url, init)保留SDK的请求头、范围、中止信号和重定向限制。
webSocketFactory(url, protocols, options)提供兼容的传输,执行证书和主机名检查,并使用有上限的连接选项。

应用必须实现这些回调。确认回调只负责交互体验:兼容的第三方客户端会受到完全相同的服务端检查。交互式用户同意还需要服务端认可的、受信任且已签名的OIDC客户端身份;身为所有者、返回"allow"或伪造Origin请求头都不能代替它。这个Node示例不会添加Origin请求头。应用的浏览器登录和CSRF要求仍由其既有的浏览器流程负责。

示例通过公开的Control操作读取/me以及项目、环境、设备、房间和音频源。这些读取使用用户Token;连接由设备Token请求。加入房间后,示例先读取实际的参与者,确认它属于所配置的设备,然后才发送受保护的操作。这些观测不能替代服务端当前的授权检查。

对每条受保护的命令,示例都会显式确认具体的委托管理操作和具体的设备操作。两次确认都使用公开的用户同意辅助函数,以及服务端快照、摘要、措辞版本和执行绑定。每次调用的命令标识从确认到实际发送保持一致。确认被拒绝、已过期、已更改或不匹配时,本次调用会终止。

连接响应中的websocket_url包含短期票据。响应中没有单独的ticket字段或应用会话字段。示例通过buildRealtimeConnectPlan校验这个URL,不会改写它。示例使用Staging环境。

结果在任务示例结果的基础上增加authorization_observation:执行过哪些公开检查,创建了多少个委托(retained_delegations),以及请求的TTL(requested_ttl_seconds)。它不是授权证明。客户端会等待传输清理完毕,但服务端已创建的委托会一直保留,直到在有限期限内过期,或经单独确认后撤销。客户端关闭连接不会撤销委托。请求的委托TTL默认值和上限都是120秒;任何清理步骤都不会自动确认撤销。

身份、范围、HTTP 401/403、用户同意被拒、过期、截止时间和传输等常规失败,都会让本次调用失败,且不会自动重试。公开错误使用固定的阶段消息,只保留SDK支持的错误元数据;原始原因、Token、票据URL、资源ID、呈现内容和服务商详情都会省略。请根据这次有上限操作的应用上下文,决定下一步的显式操作。切勿转储传输错误或凭据。

整个授权和任务操作共用调用方的取消信号和截止时间。示例单独限制等待关闭的时长。端口必须遵守中止信号;有上限的等待无法撤回服务端已接受的操作,也无法强制停止不配合的回调。如果失败发生在创建委托之后,委托的有限TTL依然有效;失败不会撤销委托。

查看原始Markdown
# JavaScript快速入门

要显式提供音频并获取任务结果,选择[语音任务](#javascript-task-voice);要实时发布和订阅音频,选择[实时语音(RTC)](#javascript-realtime-rtc);要在房间里和Agent语音对话,选择[OneSay Agent](#javascript-onesay-agent)。三者都使用OneSay JavaScript SDK,但生命周期彼此独立。SDK目前处于预览阶段,尚未发布。

## JavaScript + 语音任务

从下文的[公开任务示例](#public-task-sample-using-the-installed-package)及其使用方项目布局入手。它导出`runPublicTaskSample(input, ports)`(位于`examples/javascript-quickstart/public-task.mjs`)。如需应用准入,`runAuthorizedTaskSample(input, ports)`(位于`examples/javascript-quickstart/authorized-task.mjs`)组合了公开API和显式的用户同意决定;[授权演练](#application-authorization-through-existing-public-apis)列出了它的输入。

在SDK发布之前,安装`@onesay/sdk`时使用预览版提供的包归档文件(`.tgz`):把它放在你的`package.json`旁边,用`file:`引用声明依赖(与`examples/javascript-quickstart/package.json`的做法相同),再用`pnpm install --ignore-scripts`安装。请提供当前的项目、环境和音频源绑定,通过SDK的Token提供器获取的短期凭据,以及已采集好的16 kHz单声道PCM s16le音频。任务客户端负责WSS/OSAF分帧、序号、ACK、背压、重放和恢复以及取消;它不采集音频,也不申请麦克风权限。为任务创建的房间不会启动RTC会话或Agent。

一次语音任务会话按以下顺序进行:

1. 选择项目的Test环境。
2. 在浏览器中通过`tokenProvider`提供短期访问Token;切勿嵌入`osp_*`凭据。
3. 从Control API获取一次性Realtime票据,票据针对目标房间和项目环境。
4. 使用`onesay.realtime.v1`连接,打开音频SignalStream,发送应用自己采集的16 kHz单声道PCM s16le帧。在浏览器中,AudioWorklet是可选的采集方式之一。
5. 服务端先发送`task.started`,音频传输期间再发送`audio.ack`和`artifact.delta`。每一帧都要保留,直到连续的`audio.ack`覆盖它。
6. 发送`signal.end`。服务端依次返回`signal.ended`、`artifact.finalized`和`task.completed`;等待任务进入这一终态。

票据只能使用一次。用同一张票据建立第二个连接时,即使第一个连接仍未关闭,服务端也会以关闭码1008 `ticket_invalid`拒绝它;请改为申请新票据。

### 在控制台试用语音任务

想在写代码之前先看看语音任务的听写效果,可以在Staging上登录[控制台](../developer/CONSOLE.md),打开其中的“语音任务”一节。选好麦克风音频源和语言后开始说话:页面会实时显示陆续到达的文字,停止后显示最终文字和用量。它走的链路和上面的示例相同:以你当前登录的身份,在所选项目和环境中,通过SDK的任务客户端,经由任务WSS/OSAF运行一个OneSay听写任务;页面自己负责的部分,只是把麦克风采集成16 kHz的PCM帧。

凡是创建、发送或结束资源的步骤(房间、麦克风信号、取消听写、关闭房间),都会先准备成一项用户同意请求,只有你在页面上允许后才会执行;选择拒绝则不会执行。服务端会对每个操作重新检查,页面本身不授予任何权限。每次听写都消耗试用的音频额度,面板会在达到任务的音频上限之前自动停止听写。你也可以不停止而直接取消听写,用完后离开并关闭房间。

这一节只在Staging上提供,且仅面向已加入试用的账号。它展示的是这条链路的工作方式,不用来衡量识别质量。

## JavaScript + 实时语音(RTC)

使用`examples/javascript-rtc/`中的浏览器示例。其中的`sample.mjs`导出`createRTCExample`;页面使用已安装的`@onesay/sdk`包,并调用公开的`client.rtc.connect`门面接口。复制这个目录,把SDK包归档文件放在它的`package.json`旁边,文件名与其中`file:`引用的一致,然后安装并启动本地服务器,服务器会输出页面的本地URL:

```text
pnpm install --ignore-scripts
pnpm start
```

按Ctrl+C可停止服务器。示例不会创建API代理。完整的操作示例见`examples/javascript-rtc/README.md`。

页面的SDK环境是`staging`。连接需要你有权使用的环境、Control CORS允许的应用来源、现有的房间、应用会话和音频源资源,以及短期的用户或应用会话Token。页面只在内存中保存Token;切勿提供平台API密钥或服务商密钥。

连接时显式指定初始接收集合,再通过对应的控件请求发布和播放。订阅为空表示拒绝接收任何音频。RTC连接不会创建任务或Agent;创建和停止Agent是需要单独授权的操作。在本地关闭会话,不能确认服务端已撤销连接。

## JavaScript + OneSay Agent

OneSay Agent是一个语音参与者:它加入你的房间,收听你选定的发布,并用语音回答。你可以打断它:在它回答时开口,它就会停下来听你说。不想写代码时,登录[控制台](../developer/CONSOLE.md),使用其中的“语音通话试用”部分即可试用(仅限Staging;你的账号需要已开通试用)。

在代码中,按RTC一节加入房间并发布麦克风音频,然后用显式的输入集合和时长启动Agent:

```ts
const idempotencyKey = crypto.randomUUID();
const agent = await client.rtc.agents.create({
  roomId, appSessionId, mode: "realtime", publicationIds: [microphonePublicationId],
  maxDurationSeconds: 120, idempotencyKey,
});
```

每个创建、更改或结束资源的调用都需要用户确认(用户同意):在用户确认之前,服务器会返回`consent_required`。确认后,用同一个`idempotencyKey`重试。

请保留幂等键:如果响应丢失,用同一个键再次调用会返回同一个Agent,而不会再启动一个。用`client.rtc.agents.get(roomId, agent.agentExecutionId)`读取Agent,最多每2秒一次,直到`playback.facts.outputPublicationId`给出它的输出,然后订阅这个发布即可听到它。

| `observedState` | 含义 |
|---|---|
| `pending` | 已接受;Agent尚未运行 |
| `running` | Agent已开始运行 |
| `failed` | 在你停止之前,Agent自行结束了 |
| `unknown` | OneSay无法确认Agent的状态(它不再上报、在你停止后结束,或达到了最长时长) |
| `stopped` | 已确认停止 |

截至2026-10-26,Staging在Agent工作期间返回`running`(你请求停止后也是如此),Agent结束后返回`unknown`(如果在任何停止请求之前自行结束,则返回`failed`)。如果迟到的上报表明Agent是先自行结束的,`unknown`仍可能变为`failed`。OneSay目前还不会返回`stopped`;`effectiveStoppedAt`为null表示停止尚未得到确认。

`desiredState`是你请求的期望状态;`observedState`是OneSay观察到的实际状态。收听期间,RTC会话的`audio.agentStates`会把Agent输出的状态报告为`initializing`、`idle`、`listening`、`thinking`、`speaking`或`unknown`:这是低频的界面提示,不能证明Agent做了什么。用`client.rtc.agents.stop(roomId, agentExecutionId, { idempotencyKey })`停止它;停止被接受只表示请求已提交,不代表Agent已经离开。

截至2026-10-26,Staging标准试用:每次对话最长120秒,并有费用预算;被打断的回答同样会消耗预算。账号能进行多少次对话由它的免费额度决定,而不是按次数限制;新账号的自动开通尚未上线。更长的对话(特别长的回答会被提前截断,但至少会先说约30秒)已经实现,将随后续的Staging更新提供。

## 使用已安装包的公开任务示例

入口是[`runPublicTaskSample(input, ports)`](../../examples/javascript-quickstart/public-task.mjs)。它导入公开的`@onesay/sdk`入口。它不会创建身份、项目环境、房间、音频源、用户同意或授权,也不会自行签发连接票据。调用方必须已持有已授权的资源ID,以及针对目标房间和项目环境新签发的一次性票据。房间的访问权限由服务端签发的这张票据承载;此函数不接收单独的`roomId`参数,也不会解码票据来给自己授权。

### 准备使用方项目布局

使用一个单独的项目,并按[语音任务](#javascript-task-voice)一节的说明安装`@onesay/sdk`。不要用工作区别名替代它,也不要删除它声明的依赖。

`public-task.mjs`从`./support/`导入两个支持模块:`pcm-session.mjs`为示例PCM分帧,并观测ACK、背压和重放;`task-usage-readback.mjs`通过公开API重新读取任务及其用量。把它们放进`support/`文件夹,与`public-task.mjs`位于同一目录:

```text
consumer/
  package.json
  node_modules/@onesay/sdk/
  public-task.mjs
  support/
    pcm-session.mjs
    task-usage-readback.mjs
```

### 调用导出的函数

下面是放在`public-task.mjs`旁边的应用代码。参数值由你已获授权的应用提供;代码不嵌入也不输出任何Token。下面的`observeAuthorizedTask`是本指南写的包装函数,不是OneSay SDK额外导出的接口。

```js
import { buildRealtimeConnectPlan } from "@onesay/sdk";
import { runPublicTaskSample } from "./public-task.mjs";

export async function observeAuthorizedTask({
  projectId,
  environmentId,
  sourceId,
  issuedSingleUseTicket,
  signal,
  ports,
}) {
  return runPublicTaskSample(
    {
      projectId,
      environmentId,
      sourceId,
      plan: buildRealtimeConnectPlan("staging", issuedSingleUseTicket),
      signal,
      timeoutMs: 5000,
    },
    ports,
  );
}
```

`buildRealtimeConnectPlan(environment, ticket)`是公开的SDK函数。它规划WSS连接,不做票据交换。示例从Staging Control环境重新读取用量;它不是生产环境的端点配置。只在你有权使用的环境中运行。

四个端口都是应用必须提供的回调,不是SDK方法:

| 端口 | 回调形式和职责 |
| --- | --- |
| `webSocketFactory` | `(url, protocols, options) => socket`。返回与SDK兼容的WebSocket传输。遵守传入的连接和大小上限,禁用重定向,不要放宽证书和主机名校验。 |
| `confirmedControlsFactory` | `(client, { signal }) => controls`,或返回controls的Promise。通过公开接口约定,接入应用中显式的授权与用户同意选择;绝不默认确认。 |
| `readbackTokenProvider` | `({ signal }) => shortLivedToken`,或返回该Token的Promise。提供一个当前有效、有权在所请求范围内读取任务和用量的主体。长期有效的平台密钥或服务商密钥不能放在这里。 |
| `readbackTransport` | `(url, init) => Promise<Response>`。提供与Fetch兼容的传输,遵守`init.signal`,并保留SDK的请求头、禁止重定向策略和范围。 |

`confirmedControlsFactory`返回的controls必须实现以下形式:

```text
send(type, data, messageId, { signal })
flush({ signal })
observe(event, { signal })
```

`send`和`flush`可以返回Promise;`observe`是同步的事件观测器。用户同意的选择是应用显式做出的决定,由服务端最终裁决。返回这些方法并不授予权限:服务端仍会校验票据、音频源、操作、授权和用户同意。

### PCM、完成和失败行为

本示例恰好生成两个16 kHz、640个采样的PCM16LE单声道帧,即80毫秒的固定合成输入。它会有意触发有上限的背压,并通过SDK重放第一个未确认的帧。重复的重放既不是新的音频输入,也不是第二个任务。示例中没有麦克风采集、AudioWorklet、操作系统依赖或媒体权限请求。

如果应用要录制实时音频,采集和权限由调用方负责。AudioWorklet只是可选的采集方式之一;SDK没有音频采集导出。任务PCM的这一边界不影响RTC的`publishMicrophone`行为。

示例会等待连续ACK、匹配的任务和产物终态,以及有上限的公开任务和用量查询,并且只在SDK观察到传输关闭之后才返回结果。结果只含计数:`pcm`报告已发送、已确认和已重放的帧数,客户端背压拒绝次数,丢弃的帧数和ACK延迟百分位;`readback`报告观察到的任务数、扫描的用量事件数和读取的页数。结果不含音频、转写文本、票据或Token,也不是计费账单。

HTTP 401/403会直接结束本次调用,不会自动重试。用量缺失、重复或属于其他范围,任务与产物关联错误,缺少ACK,调用方取消,截止时间已过期,以及清理失败,都会让本次调用失败。再次调用前,先通过应用的显式流程重新获取授权;不要重放已消费的票据。

`timeoutMs`限制整个操作的时长,也单独限制等待关闭的时长。示例会把中止信号传给异步的factory、control、Token和HTTP端口。回调必须遵守传入的signal,并释放自己占用的资源。有上限的等待无法强制停止不配合的回调,也无法撤回它对外部造成的副作用;同步回调还必须避免阻塞事件循环。排查失败时,切勿把连接计划URL、票据、Token、原始PCM或转写文本写入日志。

### 通过现有公开API完成应用授权

`runAuthorizedTaskSample(input, ports)`(位于`examples/javascript-quickstart/authorized-task.mjs`)是一个应用示例:它自行获取连接材料,并显式确认受保护的操作。它组合`runPublicTaskSample`,不替代SDK的任务生命周期、ACK、重放、背压或取消逻辑。这是示例导出,不是新的OneSay SDK API。

把`authorized-task.mjs`放在`public-task.mjs`旁边。它还导入一个支持模块`support/confirmed-controls-core.mjs`:这是一个有上限的队列,每条受保护的命令都要等确认完成后才会发出。它从不自行确认任何操作。

```js
import { runAuthorizedTaskSample } from "./authorized-task.mjs";

export async function runApplicationTask(resources, signal, ports) {
  return runAuthorizedTaskSample({
    organizationId: resources.organizationId,
    projectId: resources.projectId,
    environmentId: resources.environmentId,
    deviceId: resources.deviceId,
    roomId: resources.roomId,
    sourceId: resources.sourceId,
    signal,
    timeoutMs: 60000,
    delegationTtlSeconds: 120,
  }, ports);
}
```

所有资源都必须已存在于目标环境中。激活、登录、设备注册、凭据和能力策略仍由应用现有的已授权流程负责。本示例不会创建授权、修改权限,也不会给自己授予访问权限。

| 应用端口 | 职责 |
| --- | --- |
| `humanTokenProvider({ signal })` | 提供当前有效的短期用户Token,它须有权执行公开读取、委托管理和交互式用户同意决定。 |
| `deviceTokenProvider({ signal })` | 为目标设备另外提供当前有效的短期设备Token。不要拿用户Token顶替,也不要传入设备长期有效的引导密钥。 |
| `confirmConsent(snapshot, { signal })` | 展示经过校验、不可更改的服务端呈现内容,在用户明确决定后只返回`"allow"`或`"deny"`。遵守取消请求。绝不默认允许。 |
| `fetch(url, init)` | 保留SDK的请求头、范围、中止信号和重定向限制。 |
| `webSocketFactory(url, protocols, options)` | 提供兼容的传输,执行证书和主机名检查,并使用有上限的连接选项。 |

应用必须实现这些回调。确认回调只负责交互体验:兼容的第三方客户端会受到完全相同的服务端检查。交互式用户同意还需要服务端认可的、受信任且已签名的OIDC客户端身份;身为所有者、返回`"allow"`或伪造Origin请求头都不能代替它。这个Node示例不会添加Origin请求头。应用的浏览器登录和CSRF要求仍由其既有的浏览器流程负责。

示例通过公开的Control操作读取`/me`以及项目、环境、设备、房间和音频源。这些读取使用用户Token;连接由设备Token请求。加入房间后,示例先读取实际的参与者,确认它属于所配置的设备,然后才发送受保护的操作。这些观测不能替代服务端当前的授权检查。

对每条受保护的命令,示例都会显式确认具体的委托管理操作和具体的设备操作。两次确认都使用公开的用户同意辅助函数,以及服务端快照、摘要、措辞版本和执行绑定。每次调用的命令标识从确认到实际发送保持一致。确认被拒绝、已过期、已更改或不匹配时,本次调用会终止。

连接响应中的`websocket_url`包含短期票据。响应中没有单独的`ticket`字段或应用会话字段。示例通过`buildRealtimeConnectPlan`校验这个URL,不会改写它。示例使用Staging环境。

结果在任务示例结果的基础上增加`authorization_observation`:执行过哪些公开检查,创建了多少个委托(`retained_delegations`),以及请求的TTL(`requested_ttl_seconds`)。它不是授权证明。客户端会等待传输清理完毕,但服务端已创建的委托会一直保留,直到在有限期限内过期,或经单独确认后撤销。客户端关闭连接不会撤销委托。请求的委托TTL默认值和上限都是120秒;任何清理步骤都不会自动确认撤销。

身份、范围、HTTP 401/403、用户同意被拒、过期、截止时间和传输等常规失败,都会让本次调用失败,且不会自动重试。公开错误使用固定的阶段消息,只保留SDK支持的错误元数据;原始原因、Token、票据URL、资源ID、呈现内容和服务商详情都会省略。请根据这次有上限操作的应用上下文,决定下一步的显式操作。切勿转储传输错误或凭据。

整个授权和任务操作共用调用方的取消信号和截止时间。示例单独限制等待关闭的时长。端口必须遵守中止信号;有上限的等待无法撤回服务端已接受的操作,也无法强制停止不配合的回调。如果失败发生在创建委托之后,委托的有限TTL依然有效;失败不会撤销委托。