前端 SDK

Preview

快速开始

先在应用后端配置 用户 token 接口,再使用 createNuva() 初始化客户端。

import { createNuva, Message } from "@nuva/sdk";

const nuva = createNuva({
  getAccessToken: async () => {
    const response = await fetch("/api/nuva-token", {
      method: "POST",
      credentials: "same-origin",
      // 按应用的鉴权配置添加 CSRF header。
    });
    if (!response.ok) {
      throw new Error(`Token request failed: ${response.status}`);
    }

    const data: unknown = await response.json();
    if (typeof data !== "object" || data === null ||
        !("token" in data) || typeof data.token !== "string" || !data.token) {
      throw new Error("Invalid token response");
    }
    return data.token;
  },
});

const main = await nuva.sessions.getMain();
const agent = nuva.agent({ sessionId: main.id });
const receipt = await agent.send(Message.text("开始广告创作,请先列出需要的产品资料。"));
配置项类型说明
getAccessToken() => Promise<string>返回当前登录用户的 Nuva 访问 token。获取失败时抛出错误。

每个登录用户复用一个客户端实例。退出或切换账号时调用 nuva.close(),然后为新用户创建实例。服务凭证仅配置在后端。

React 接入

在组件树中挂载 NuvaProvider。调用 useAgent({ sessionId }) 读取消息并发送操作。

import { NuvaProvider, useAgent } from "@nuva/sdk/react";
import type { NuvaClient } from "@nuva/sdk";
import { MessageList } from "./MessageList";

function App({ client, sessionId }: { client: NuvaClient; sessionId: string }) {
  return (
    <NuvaProvider client={client}>
      <Conversation sessionId={sessionId} />
    </NuvaProvider>
  );
}

function Conversation({ sessionId }: { sessionId: string }) {
  const agent = useAgent({ sessionId });
  if (agent.loadStatus === "loading") return <p>加载中…</p>;
  if (agent.loadStatus === "error") return <p role="alert">{agent.error.message}</p>;

  return (
    <>
      {agent.freshness === "stale" && <p>连接中断,正在显示已保存的内容。</p>}
      <MessageList messages={agent.messages.items} agent={agent} />
    </>
  );
}

MessageList 是应用内的消息组件。按 message.content 的顺序渲染内容,并使用 agent.respond() 提交客户端工具结果。

字段值 / 类型说明
loadStatusloading | ready | error当前会话数据的加载状态。
sessionSession会话 ID、类型、标题及所属用户;ready 时可用。
messagesPage<MessageView>已加载的消息及历史分页游标;ready 时可用。
agentStatus.stateidle | running | waiting_for_responseAgent 执行状态;等待回应时 callIds 列出相关调用。
freshnesslive | stale数据已同步,或暂时显示上次保存的内容。
errorNuvaErrorInfo加载失败时存在;ready 状态下也可能包含同步错误。

卸载组件只结束本地订阅。已开始的服务端工作继续运行。

消息与引用

发送消息

await agent.send(Message.text("继续修改第二条脚本。"));

await agent.send(
  Message.create()
    .text("修改 ")
    .mention(script)
    .text(" 的开场,保留其他内容。")
);

await agent.send(
  Message.create().replyTo(existingMessage).text("使用这一版。")
);
方法参数用途
Message.text()string创建纯文本消息。
Message.create().text()string追加文字。
.mention()RecordLink在文字中插入记录引用。
.attach()RecordLink添加消息附件。
.quote()RecordLink | MessageView, string引用记录或消息中的选定文字。
.replyTo()MessageView指定被回复的消息。

构建方法返回新的消息值。send() 成功后返回已保存消息的引用;Agent 回复通过消息订阅到达。

{
  "requestId": "request_21",
  "message": {
    "sessionId": "work_product_a",
    "messageId": "message_12"
  }
}

记录引用

将 API 返回的 RecordLink 传入 mention() 或 attach()。revision 指定记录版本。

{
  "ref": { "kind": "script", "id": "script_1", "revision": "1" },
  "label": "脚本 1"
}

接收消息

{
  "ref": { "sessionId": "work_product_a", "messageId": "message_10" },
  "role": "assistant",
  "createdAt": "2026-10-12T18:00:00Z",
  "state": "complete",
  "attachments": [],
  "content": [
    { "id": "part_text", "type": "text", "format": "markdown", "text": "脚本已生成。" },
    {
      "id": "part_script_1",
      "type": "resource",
      "record": {
        "ref": { "kind": "script", "id": "script_1", "revision": "1" },
        "label": "脚本 1"
      }
    }
  ]
}
字段说明
ref来源会话 sessionId 与消息 messageId。转发消息保留原引用。
roleuser 或 assistant。用户消息的 metadata.user 提供发送者身份。
content有序内容列表:text、mention、resource、quote、dynamic-tool 或 data-host-call。
statestreaming | complete | interrupted | failed。failed 时包含 error。
attachments消息附件列表。
replyTo可选的被回复消息引用。

按 SDK 返回的顺序显示 messages.items。流式更新会合并到原消息;无需自行拼接网络片段。

完整消息示例

客户端工具

问答、许可确认和内容审核通过消息中的 dynamic-tool 交互。使用 toolName 选择处理函数,使用 toolCallId 关联结果。

{
  "type": "dynamic-tool",
  "toolName": "nuva.review",
  "toolCallId": "call_17",
  "state": "input-available",
  "input": {
    "prompt": "请审核这条脚本。批准后将继续制作提示词。",
    "records": [{ "kind": "script", "id": "script_1", "revision": "1" }]
  }
}
状态处理方式
input-available读取完整 input,显示交互内容并等待用户回应。
output-available从 output 读取已保存的结果。
output-error从 errorText 读取失败说明。

提交结果

将收到的工具调用和结果传入 respond()。结果必须符合该工具的 outputSchema。

// part 是消息中 state 为 input-available 的工具调用。
const receipt = await agent.respond(part, {
  decision: "reject",
  text: "先展示产品,再讲困扰。",
});

nuva.review 使用 decision: "approve" | "reject" 和 text。无补充说明时传入空字符串。方法在服务端保存结果后返回:

{
  "requestId": "request_22",
  "call": {
    "type": "dynamic-tool",
    "toolName": "nuva.review",
    "toolCallId": "call_17",
    "state": "output-available",
    "input": {
      "prompt": "请审核这条脚本。批准后将继续制作提示词。",
      "records": [{ "kind": "script", "id": "script_1", "revision": "1" }]
    },
    "output": { "decision": "reject", "text": "先展示产品,再讲困扰。" }
  }
}

后续 Agent 回复继续通过消息订阅到达。仍在等待回应的调用可从历史消息或转发消息中提交。

提交执行错误

无法取得有效结果时调用 fail()。用户拒绝审核应作为正常结果传给 respond()。

await agent.fail(part, {
  code: "EDITOR_UNAVAILABLE",
  message: "脚本编辑器加载失败。",
});

返回值包含 requestId 和状态为 output-error 的 call。

Host call

通过 onHostCall() 订阅已注册的网站交互。将 toolName 映射到应用的页面、编辑器或其他操作。

const unsubscribe = agent.onHostCall((part) => {
  if (part.type === "dynamic-tool") {
    if (part.state !== "input-available") return;
    host.receive(part.toolName, part.input, part);
  } else {
    host.receive(part.data.toolName, part.data.input, part);
  }
});

// 在组件卸载的清理函数中调用 unsubscribe()。

host.receive 是应用提供的处理函数。需要结果的 dynamic-tool 使用 respond() 或 fail() 回传结果。

不要求结果的 host call 使用 data-host-call。按其 input 执行应用操作,无需回传:

{
  "type": "data-host-call",
  "id": "host_19",
  "data": {
    "toolName": "workspace.showScripts",
    "input": {
      "records": [{ "kind": "script", "id": "script_1", "revision": "1" }]
    }
  }
}

Host call 同时保存在消息内容中。历史渲染只展示保存的内容;自动执行时按调用 ID 去重。

在服务端授权配置中启用对应 UI skill。前端处理函数使用该 skill 中注册的方法名和输入格式。

方法参考

方法返回类型用途
nuva.sessions.getMain()Promise<Session>读取当前用户的主会话。
nuva.sessions.list(query?)Promise<Page<Session>>分页读取可访问会话。
nuva.sessions.get(sessionId)Promise<Session>读取指定会话。
nuva.sessions.history(sessionId, query?)Promise<Page<MessageView>>读取已保存的消息历史。
nuva.sessions.getMessage(ref)Promise<MessageView>按 MessageRef 读取消息。
nuva.agent({ sessionId })AgentHandle取得该会话的操作句柄。
agent.send(message, options?)Promise<SendReceipt>保存用户消息。
agent.respond(part, output, options?)Promise<ResponseReceipt>保存客户端工具结果。
agent.fail(part, failure, options?)Promise<HostFailureReceipt>保存客户端工具执行错误。
agent.subscribe(listener)Unsubscribe立即读取当前状态,并接收后续状态更新。
agent.onHostCall(listener)Unsubscribe接收客户端工具及 host call。
agent.loadEarlierMessages()Promise<void>加载更早消息并更新 messages。
agent.listPendingCalls(query?)Promise<Page<DynamicToolUIPart>>读取 input-available 状态的调用,包括尚未加载的历史。
agent.getHostCall(id)Promise<DynamicToolUIPart | NotificationHostCall>按 toolCallId 或 host call ID 读取调用。
agent.refresh()Promise<void>重新同步状态;不会重发操作。
nuva.close()void关闭本地连接并清理客户端状态。

query 接受可选的 cursor 和 limit。返回页包含 items 和 nextCursor;nextCursor 为 null 时已到末页。options 接受可选的 requestId。

错误处理

操作失败时 Promise 抛出 NuvaError。读取 code、message、retryable 和可选的 requestId。

错误码处理方式
UNAUTHENTICATED检查 token 和应用登录状态。
FORBIDDEN检查用户授权范围。
NOT_FOUND核对会话、消息或记录 ID。
INVALID_INPUT按输入 Schema 修正参数。
REVISION_CONFLICT读取最新记录,核对后重新提交。
CALL_OBSOLETE读取当前有效的调用,重新确认待提交内容。
CALL_ALREADY_RESOLVED读取该调用已保存的结果。
REQUEST_ID_CONFLICT检查 requestId 是否被用于不同的输入。
RATE_LIMITED降低调用频率,按 retryable 决定是否重试。
UNSUPPORTED_CONTENT检查消息类型和 SDK 版本。
CONNECTION_ERROR / SERVICE_ERROR保留用户输入,检查接受状态后恢复或重试。

命令错误中的 acceptance 为 not_accepted 或 unknown。unknown 表示尚不能确认服务端是否已保存操作。

重试同一操作时复用相同 requestId 和输入。缺省 requestId 由 SDK 生成;发生状态不明的错误时,保留错误中的 requestId。send() 或 respond() 返回成功仅表示该次输入已保存,后续生成结果从消息订阅读取。