npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

qwenwork-open-sdk

v0.2.0

Published

QwenWork App and Session SDK

Downloads

1,098

Readme

QwenWork Open SDK(统一包)

一个安装包,按子入口使用 App 通信、Desktop 应用开发、Session 数据和 React 会话 UI。 Public Kit 的 App、Host、Protocol、Desktop、Desktop Open 与 qwapp 编译器均在包内。 这是多入口 ESM 包,不会因为使用 Session 数据核心而初始化 App 连接或加载 React UI。

包名为 qwenwork-open-sdk,通过下方列出的子路径按需导入。 准确的统一包版本见已安装包的 package.json,组件版本与摘要见 components.json。统一包版本不随组件版本或其标签自动变化。

目录

安装与运行要求

本文对应 0.2.0:Desktop / Open 2.1.0、Standard App / Host / Protocol 0.3.1, Session 双包仍为 0.1.7。正式版使用 latest;与 0.2.0-preview.1 的 SDK 实现、类型和资源一致。 准确的发布时间和标签以 npm registry 为准,本 README 不作为发布回执。

在已有 package.json 的业务项目执行:

npm install qwenwork-open-sdk

若项目配置了其他 registry,可在本次安装时指定 npm 官方源:

npm install qwenwork-open-sdk --registry=https://registry.npmjs.org/

使用 pnpm 的项目执行:

pnpm add qwenwork-open-sdk

安装后提交业务项目的 package.json 和锁文件;后续使用 npm ci 或 pnpm install --frozen-lockfile 恢复锁定的依赖。无需下载 Kit 或运行配套安装器。

  • 只使用 App 或 Session 数据核心时,不需要业务创建 React Provider。 核心入口不加载 React;这不等于 npm 一定不会因传递 peer 自动安装 React。
  • 使用 UI 或 React hooks 时,业务应提供匹配的 React / React DOM 17、18 或 19, TypeScript 项目使用相同主版本的类型声明。已有匹配依赖无需重复安装。
  • 默认 UI 和 /session-client/react/core 不需要 React Query;只有旧 /session-client/react 入口需要 @tanstack/react-query@5,且仅支持 React 18/19。
  • 浏览器或 SSR 环境需有 fetch、AbortController 和可取消的流读取;上传还需 Blob / File。 示例中的 crypto.randomUUID() 也需由运行环境提供。React 版本兼容不等于旧浏览器兼容。
  • App 连接应在浏览器页面挂载后建立,不在 SSR 中调用 qw.connect()。 使用框架的客户端组件边界承载带 hooks 的示例;每个 SSR 请求独立管理身份与缓存。

选择引入入口

根路径 qwenwork-open-sdk 没有聚合导出,请按需使用以下子路径。 普通会话 UI 只需从 /session-react 导入组件,并在应用入口引入一次 /session-react/styles.css; 完整 Markdown 渲染已内置,不需要另外选择或引入 Markdown 入口。

| 引入路径 | 内容与用途 | | --- | --- | | qwenwork-open-sdk/app | 默认 qw、显式 App、页面工具、协议类型与宿主协作 | | qwenwork-open-sdk/app/browser | 浏览器 Transport、连接来源策略及 Portal 辅助能力 | | qwenwork-open-sdk/qwenwork-app | 规范 App 入口;与 /app 指向同一个模块 | | qwenwork-open-sdk/qwenwork-app/browser | 规范浏览器入口;与 /app/browser 指向同一个模块 | | qwenwork-open-sdk/qwenwork-app-host | 宿主接收器、AppBridge 与能力接线 | | qwenwork-open-sdk/qwenwork-app-host/browser | 浏览器宿主 Transport 与 Relay | | qwenwork-open-sdk/qwenwork-app-protocol | App / Host 共用的协议与类型 | | qwenwork-open-sdk/qwenwork-app-desktop-open | H5 页面调用获授权的 Desktop Open 能力 | | qwenwork-open-sdk/qwenwork-app-desktop-open/protocol | Desktop Open 协议定义 | | qwenwork-open-sdk/qwenwork-app-desktop | Desktop 公共 SDK 类型及编译器注入入口 | | qwenwork-open-sdk/qwenwork-app-desktop/config | 应用配置与权限声明 | | qwenwork-open-sdk/qwenwork-app-desktop/contract | UI / Main / Service 通信契约 | | qwenwork-open-sdk/qwenwork-app-desktop/ui | UI API 类型与版本信息 | | qwenwork-open-sdk/qwenwork-app-desktop/api | App API 类型 | | qwenwork-open-sdk/qwenwork-app-desktop/main | Main 定义、工具与生命周期 | | qwenwork-open-sdk/qwenwork-app-desktop/service | Service 定义、工具与生命周期 | | qwenwork-open-sdk/qwenwork-app-desktop/package-target | 平台包目标类型 | | qwenwork-open-sdk/qwenwork-app-desktop/globals/qw/ui | UI 的 qw 全局类型 | | qwenwork-open-sdk/qwenwork-app-desktop/globals/qw/main | Main 的 qw 全局类型 | | qwenwork-open-sdk/qwenwork-app-desktop/globals/qw/service | Service 的 qw 全局类型 | | qwenwork-open-sdk/qwenwork-app-desktop/globals/qwenwork/ui | UI 的 qwenwork 全局类型 | | qwenwork-open-sdk/qwenwork-app-desktop/globals/qwenwork/main | Main 的 qwenwork 全局类型 | | qwenwork-open-sdk/qwenwork-app-desktop/globals/qwenwork/service | Service 的 qwenwork 全局类型 | | qwenwork-open-sdk/qwenwork-app-desktop/internal/qwapp-standard-protocol | Public Kit 宿主适配协议;非普通页面入口 | | qwenwork-open-sdk/qwenwork-app-desktop/internal/standard-page-connector | Public Kit 页面连接适配;非普通页面入口 | | qwenwork-open-sdk/qwenwork-app-desktop/internal/standard-app-native | Public Kit 原生连接适配;非普通页面入口 | | qwenwork-open-sdk/session-client | SessionClient、Session/Drive 请求、上传、状态及类型 | | qwenwork-open-sdk/session-client/react/core | React 17/18/19 数据 Provider 与订阅 hooks,无 Query 依赖 | | qwenwork-open-sdk/session-client/react | 兼容旧 Query 5 hooks;React 18/19,生成的查询接口为 MA-only | | qwenwork-open-sdk/session-react | Provider、Feed、Composer、文件预览、主题和渲染扩展 | | qwenwork-open-sdk/session-react/styles.css | 会话 UI、Markdown 与 KaTeX 样式及字体引用 |

只需自定义非 React 界面时选数据核心;需要现成聊天界面时从 ManagedSessionProvider 开始。 需要与宿主协作时再引入 App。App 连接和 Session API 鉴权彼此独立,没有隐式消息转发或账号共享。 不提供单独 /protocol 或 internal-navigation;协议使用规范 /qwenwork-app-protocol。 三个 Desktop internal/* 路径只是既有 Public Kit 的宿主接线契约,不包含 first-party 或 private SDK。

Desktop 应用迁移

内网 App 系列的包名替换规则:保留 qwenwork-app… 部分,仅将原来的 @ali 前缀 换成 qwenwork-open-sdk;包名后的子路径不变。 例如原 Desktop SDK 的 /config 对应 /qwenwork-app-desktop/config, 不是 /qwenwork-app/config。/app 和 /app/browser 继续兼容,不要求现有使用方改代码。

import { defineQwenWorkApp } from 'qwenwork-open-sdk/qwenwork-app-desktop/config';
export default defineQwenWorkApp({
  id: 'com.example.hello', name: 'Hello', version: '1.0.0',
  runtime: { minVersion: '2.1.0' }, sdk: { global: 'qw' },
  main: { entry: './src/main.ts', environment: 'node', scope: 'installation' },
  views: [{ id: 'main', title: 'Hello', entry: './src/index.html' }],
});

qwapp validate、qwapp build、配置字段、权限、方法和 qw 全局均保持不变。 移除旧 SDK 依赖及指向它们的 overrides,更新锁文件;不要同时安装内网分包和大包。 旧 Demo 的 @qwenwork/app-sdk 本地别名实际指向 Desktop,也应换成 /qwenwork-app-desktop。

Desktop 的普通入口保持 types-only,由 qwapp 为配置、UI、Main、Service 注入实际实现, 不是可以直接用 Node import() 执行业务的库。UI / Main / Service 的 globals 声明应分别 放进各自的 tsconfig,不要在同一个类型工程同时引入不同环境。编译器只提供 public Profile。 需要 Node >=22.12;Main / Service 是应用角色,不代表 Electron 主进程。

App:连接与宿主协作

普通业务页面

宿主需要先登记并打开页面、准备接收器。直接嵌入宿主的页面通常使用默认 qw:

import qw from 'qwenwork-open-sdk/qwenwork-app';

export async function connectPage() {
  // 在连接前登记需要保留的回调和页面工具。
  qw.onteardown = () => ({});
  try {
    await qw.connect({ timeout: 15_000 });
    return { host: qw.getHostVersion(), close: () => qw.close() };
  } catch (error) {
    await qw.close().catch(() => {});
    throw error;
  }
}

默认 Direct 使用实际父窗口,并校验来源、origin 和本次连接关联;它不代替业务认证。 业务通过 CSP frame-ancestors 等部署策略控制谁能嵌入。独立打开普通浏览器 Tab 没有可信 宿主端点时会超时,不能靠放宽来源或伪造 Session ID 建连。 同一 Document 统一管理一个默认 qw,不要让多个组件各自连接和关闭它。

浏览器连接策略与显式 App

需要固定部署来源时,从 /app/browser 配置策略;以下两种按部署选择一种, 在首次 qw.connect() 前设置,参数来自可信部署配置,不从任意查询参数读取:

import { configureQwConnection } from 'qwenwork-open-sdk/qwenwork-app/browser';

export function configureDirect(hostOrigin: string) {
  configureQwConnection({ type: 'direct', mode: 'window', hostOrigins: [hostOrigin] });
}

export function configurePortal(portalOrigin: string, hostOrigin: string) {
  configureQwConnection({ type: 'relay', portalOrigin, hostOrigin });
}

Origin 必须精确到协议、域名和端口,不能用 *。Portal 中的嵌套 iframe 需要 Portal 已接好 Relay;单独传入 qw.connect({ portalOrigin }) 不会创建中继或授权任意 iframe。 MessagePort / 原生端口也需要真实的宿主接线,不会自动退回另一种宿主或连接方式。

已有可信 Transport、需要显式持有实例时使用 App,与默认 qw 二选一管理当前连接:

import { App, type Transport } from 'qwenwork-open-sdk/qwenwork-app';

export async function connectApp(verifiedTransport: Transport) {
  const app = new App({ name: 'report-panel', version: '1.0.0' }, {}, {
    initializationTimeoutMs: 5_000, closeTimeoutMs: 500,
  });
  try {
    await app.connect(verifiedTransport);
    return app; // 调用方在页面结束时 await app.close()。
  } catch (error) {
    await app.close().catch(() => {});
    throw error;
  }
}

Transport 和公开协议类型可从 /app 获取,不需要另装 Protocol。 类型存在不代表每个宿主都已实现对应能力;显式 App 不负责发现宿主或获得登录身份。

输入、消息与模型上下文

连接成功后,先查宿主能力,再在用户触发的操作中调用;能力声明不等于执行许可:

| 方法 | 实际含义 | | --- | --- | | qw.ui.input.prefill | 填入宿主草稿,不发送;if-empty 保护已有输入 | | qw.ui.input.addAttachment | 向宿主输入区添加附件/文本材料,不发送 | | qw.sendMessage | 提交本次显式消息,不读取或发送宿主输入框草稿 | | qw.updateModelContext | 完整替换本 App 的模型上下文,不追加、不触发回复 |

import qw from 'qwenwork-open-sdk/qwenwork-app';

export async function fillDraft(text: string) {
  const capability = qw.getHostCapabilities()?.input?.prefill;
  if (!capability?.content.text || !capability.modes.includes('if-empty')) {
    throw new Error('当前宿主不支持安全填入草稿');
  }
  return qw.ui.input.prefill({ content: [{ type: 'text', text }], mode: 'if-empty' });
  // 调用方处理 { applied: false, reason },不要直接当作写入成功。
}

export async function attachSelection(text: string) {
  if (!qw.getHostCapabilities()?.input?.addAttachment?.content.text) {
    throw new Error('当前宿主不支持文本附件');
  }
  await qw.ui.input.addAttachment({ content: [{ type: 'text', text }] });
}

export async function tellHost(text: string, signal: AbortSignal) {
  if (!qw.getHostCapabilities()?.message?.text) throw new Error('当前宿主不支持文本消息');
  const result = await qw.sendMessage({ role: 'user', content: [{ type: 'text', text }] }, {
    timeout: 5_000, signal,
  });
  if (result.isError) throw new Error('宿主未接受消息');
}

export async function replaceContext(text: string) {
  if (!qw.getHostCapabilities()?.updateModelContext?.text) throw new Error('当前宿主不支持文本上下文');
  await qw.updateModelContext({ content: [{ type: 'text', text }] });
}

消息被接受不等于 Agent 已执行或已经回复。超时、取消和断连也不保证远端没有产生副作用, 不要因此自动重发。批注等扩展只有宿主明确提供时才能使用,不能当作所有页面的默认功能。

从 0.2.0-preview.0 升级

导入路径及 /app 兼容别名不变。本版跟随最新 Public TGZ,不恢复上游已移除的 API: Standard 0.3.1 不再提供 ui.input.references、ReferencesApi 或 references extension。 使用这些旧接口的项目需要调整调用,不能保证仅更换版本即可迁移。 Desktop UI 的历史引用类型依照上游保留;其 0.1.1 兼容协议已内置为包内私有模块, 不新增公开子入口,也不授予引用管理权限。新版 Native 能力需要 Runtime 2.1.0。 Session 的 API、配置、默认 backend 与入口保持不变。

页面工具与环境订阅

import qw from 'qwenwork-open-sdk/qwenwork-app';

export function registerSelectionTool(readSelection: () => string) {
  const tool = qw.registerTool('get_selection', {
    title: '读取当前选区', description: '读取用户在当前页面选中的文本',
    inputSchema: { type: 'object', properties: {}, additionalProperties: false },
  }, async (_args, extra) => {
    extra.signal.throwIfAborted();
    return { content: [{ type: 'text', text: readSelection() }] };
  });
  return () => tool.remove();
}

export function observeEnvironment(notify: () => void) {
  const offTheme = qw.ui.theme.onChange(notify);
  const offLanguage = qw.ui.language.onChange(notify);
  return {
    read: () => ({ theme: qw.ui.theme.get(), language: qw.ui.language.get() }),
    dispose: () => { offTheme(); offLanguage(); },
  };
}

工具可在连接前注册,名称在当前 App 内唯一;移除时使用返回句柄的 remove()。 业务 handler 应校验输入、当前页面权限并遵守 extra.signal。注册成功不保证模型一定能看到或调用工具, 工具发现、授权和执行入口仍由宿主负责。 主题和语言共用 HostContext 缓存;更完整的变化可订阅 hostcontextchanged,Agent 事件可订阅 agentevent,销毁时用同一回调 removeEventListener。接收事件不自动同步到 Session Client。

关闭页面先释放业务订阅、工具和其他资源,再 await qw.close();需要重连时串行等待关闭完成。 不要并发建立多条默认连接。onbeforeclose 用于关闭前业务确认,onteardown 用于资源清理, 它们不替代宿主自己的页面和会话生命周期管理。

Session Client:数据与文件

主站:创建并订阅

下面使用已登录、支持 Cookie 鉴权的同源 API 代理。主站必须显式设置 backend: 'qwenwork' 与 API 前缀;直接调用公开 API 时使用后面的 Bearer 配置。

import { SessionClient } from 'qwenwork-open-sdk/session-client';

export async function startSession(text: string) {
  const client = new SessionClient({ config: {
    backend: 'qwenwork', baseUrl: '/v1', auth: { kind: 'cookie' },
  } });
  try {
    const created = await client.createSession({ initial_prompt: { text } });
    const session = client.forSession(created.session_id);
    const unsubscribe = session.subscribe(() => {
      const snapshot = session.getSnapshot();
      console.log({ phase: snapshot.phase, eventCount: snapshot.events.length });
      // 业务 UI 另行处理 snapshot.error,不将原始会话内容写入日志。
    });
    void session.start();
    return { session, dispose: () => { unsubscribe(); session.destroy(); } };
  } finally {
    client.destroy(); // forSession 返回独立实例,不受临时 client 销毁影响。
  }
}

initial_prompt 已提交首轮,不要在订阅或挂载 UI 时再发一遍。 start() 持续运行历史加载和实时 SSE;不要 await start() 来判断连接就绪, 应订阅快照。取得的绑定 session 可调用 sendPrompt({ text });不再使用时调用 dispose()。

后端与鉴权

| 配置/能力 | QwenWork 主站 | Managed Agents(MA) | | --- | --- | --- | | backend | 必须为 'qwenwork' | 'ma';省略时保持旧 MA 默认 | | 标准 baseUrl | 显式 /v1 | /v1/managed-agent | | 创建参数 | initial_prompt | agent_id,Workspace 按业务作用域传入 | | Prompt 文件引用 | { path } | { file_id } 或 MA 存储内的 { path } | | context / resource | 支持云端会话 | 不支持,误传会失败 |

SDK 不从 URL、Session ID 或请求字段猜测后端,也不在失败时自动回退。 baseUrl 可以是同源前缀或完整 API 地址,不包含凭据、查询参数和片段。

import { SessionClient, type SessionSdkConfig } from 'qwenwork-open-sdk/session-client';

export function createApiClient(apiBaseUrl: string, getToken: () => Promise<string>) {
  return new SessionClient({ config: {
    backend: 'qwenwork', baseUrl: apiBaseUrl, auth: { kind: 'bearer', getToken },
  } });
}

export const maConfig: SessionSdkConfig<'ma'> = {
  backend: 'ma', baseUrl: '/v1/managed-agent', auth: { kind: 'cookie' },
};

export async function createManagedSession(workspaceId: string, agentId: string) {
  const client = new SessionClient({ config: maConfig });
  try {
    const created = await client.createSession({ workspace_id: workspaceId, agent_id: agentId });
    return client.forSession(created.session_id); // 调用方 start,并最终 destroy。
  } finally { client.destroy(); }
}

令牌由宿主获得和刷新;getToken 每次请求读取同一身份的最新令牌,不是登录 API。 Cookie 模式使用 credentials: 'include';auth: { kind: 'none' } 仅表示不附加 Bearer,不能绕过鉴权。 fetcher 可注入 JSON / SSE 传输,需支持流读取与取消。身份、环境、租户变化时销毁并重建 client, 不要轮换全局 Token;普通同身份 Token 刷新无需重建。

常用方法与生命周期

| 方法 | 用途 | | --- | --- | | createSession(input, options?) | 创建会话,不自动绑定实例 | | forSession(id) | 创建独立绑定实例,不切换当前实例 | | sendPrompt(sessionId, input, options?) | 应用级发送;绑定实例用 sendPrompt(input, options?) | | start() / refresh() | 启动历史与实时流 / 单独刷新详情 | | subscribe(listener) / getSnapshot() | 监听变化 / 获取当前快照 | | cancelTurn(key?) / removeQueuedPrompt(promptId, key?) | 请求中断执行 / 移除待执行 Prompt | | answerQuestion(input, key?) | 回答当前待处理问题或权限请求 | | stop() / destroy() | 中止本地工作,可重启 / 永久释放实例 |

也可直接 new SessionClient({ sessionId, config }) 绑定已有会话。创建、应用级发送、上传和文件读取 不要求先开 SSE;父、子和兄弟 client 生命周期独立。stop() 保留快照与可重试上传任务, destroy() 后不能复用旧实例或任务;两者都不会删除远端会话。

createSession / sendPrompt 的选项支持 { signal, headers },也兼容字符串幂等键; 实例方法不接受单次 { sdk } 配置覆盖。写请求不自动重试,MA 去重重试需复用原幂等键, 主站创建/发送不承诺相同保证。中止本地请求不是撤销服务端操作,accepted/queued 不是执行完成。 独立请求函数如 getSession、listSessions、listEvents、createSession、sendPrompt 也从 /session-client 导出,通过最后一个选项 { sdk: config } 指定连接;它们不管理长连接。

Prompt 上下文与 Skill

import type { SessionClient, SessionSkillRef } from 'qwenwork-open-sdk/session-client';

export function createReport(client: SessionClient<'qwenwork'>, skills: readonly SessionSkillRef[]) {
  return client.createSession({
    initial_prompt: { text: '总结当前报表', context: ['仅分析当前选区', '金额单位为万元'] },
    resource: skills,
  });
}

export function continueReport(session: SessionClient<'qwenwork'>) {
  return session.sendPrompt({ text: '再看同比变化', context: ['金额单位为万元'] });
}

context 每次独立传入,最多 10 个非空白字符串,含 SDK 包装标签后的 UTF-8 总大小不超过 100000 字节。SDK 将它编码为 hidden_text,不显示为普通输入/附件,不自动带入下一轮。 隐藏不是保密或更高指令权限,不能放密钥。 resource 是创建时追加的 Skill 引用,结构为 { resource_type: 'skill', resource_origin: 'public' | 'org' | 'user', resource_id }, ID 必须是非零 UUID,最多 100 项;不替换默认资源,不改变安装状态,也不保证一定调用。 服务端最终验证可用性与授权。泛型只缩窄类型,不会改变实例实际后端。

文件上传、重试与取消

import type { SessionClient, ManagedFileUploadScope } from 'qwenwork-open-sdk/session-client';

export async function sendFile(client: SessionClient<'qwenwork'>, sessionId: string, file: File) {
  const uploaded = await client.uploadFile(file, {
    path: `/uploads/${crypto.randomUUID()}-${file.name}`,
    onProgress: ({ phase, loadedBytes, totalBytes }) => console.log({ phase, loadedBytes, totalBytes }),
  });
  if (uploaded.backend !== 'qwenwork') throw new Error('文件与会话后端不匹配');
  return client.sendPrompt(sessionId, { text: '分析这个文件', files: [uploaded.promptFile] });
}

export function uploadManaged(client: SessionClient<'ma'>, file: File, scope: ManagedFileUploadScope) {
  return client.uploadFile(file, { path: `attachments/${crypto.randomUUID()}-${file.name}`, scope });
}

主站使用 Drive 绝对路径及对应上传权限。MA 使用相对路径,每次提供完整授权作用域:Workspace 为 { fileKind: 'workspace', orgId, workspaceId };Namespace 附件为 { fileKind: 'attachment', orgId, workspaceId, resourceNamespaceKey, resourceNamespaceId }。 不要从会话 ID 猜作用域。两端都直接使用结果的 promptFile,不要混用 ID、存储路径和签名 URL。

  • 上传经历 creating → uploading → completing → completed;必须等 Promise 成功,传完字节不等于登记完成。
  • 需要显式重试时保存 client.createUpload(file, input) 的句柄,重复调用同一个 task.upload({ signal }), 复用原文件、目标、身份和幂等键;它不是刷新页面后的持久化续传。
  • 放弃上传时先 abort,并等待在途 upload() 结束,再 await task.cancel()。取消不删除已完成文件, UPLOAD_ID_UNKNOWN 不能当作远端清理成功。
  • 主站 single / multipart 上限为 512 MiB / 10 GiB,MA 为 64 MiB / 1 GiB,还受配额限制; 两端只创建新文件,不覆盖同名目标。mode: 'auto' 默认选择模式。
  • 自定义 uploads.uploadTransport 时遵守取消、禁止重定向和 credentials: 'omit', 仅使用服务端提供的存储 headers,不携带业务 API Token。自定义代理前缀可用 uploads.baseUrl 明确指定。

读取文件与下载地址

import { SessionClient, type ManagedFileReadScope } from 'qwenwork-open-sdk/session-client';

export async function getReportUrl(client: SessionClient<'qwenwork'>, signal: AbortSignal) {
  const file = await client.getFile({ path: '/Documents/report.md' }, { signal });
  if (!file.download_url) throw new Error('文件当前没有可用的下载地址');
  return file.download_url; // 短时能力,不记录日志或存入长期缓存。
}

export function createManagedReader(sessionId: string, scope: ManagedFileReadScope) {
  return new SessionClient({ sessionId, config: {
    backend: 'ma', baseUrl: '/v1/managed-agent', auth: { kind: 'cookie' }, files: { scope },
  } }); // 调用方负责 destroy。
}

getFile({ path }) 和 getFile({ file_id }) 二选一,按已配置后端返回原生文件信息,不下载字节:

  • 主站:调用公开 Drive,返回可选 download_url,不是私有 presigned_url。
  • MA:配置经宿主验证的 files.scope: { orgId, workspaceId } 后返回 presigned_url; namespace 场景还需成对配置业务 key 与 ID。支持 { mode: 'preview' | 'download' },默认 download。 可读授权的 UUID / storage_path,产物路径按当前会话精确唯一匹配;不兜底成其他会话或沙箱文件。 旧 getManagedFile 仅保留 MA 兼容,主站调用会失败。

barn://... 标识、barnId 和沙箱 filePath 都不是可直接打开的网页地址。 获取签名 URL 的 API 请求使用当前身份;后续对象存储 GET 不携带业务 API Token。 stop() / destroy() 和单次 signal 可取消读取。签名会过期,打开/重试时重新解析; 内置 UI 已接入这些默认解析,无需额外手写同一套请求。

React 数据 hooks

只做自定义 UI 时使用 /session-client/react/core,不需要 CSS 或 React Query:

import type { SessionSdkConfig } from 'qwenwork-open-sdk/session-client';
import { SessionClientProvider, useSessionSnapshot } from 'qwenwork-open-sdk/session-client/react/core';

const config: SessionSdkConfig<'qwenwork'> = {
  backend: 'qwenwork', baseUrl: '/v1', auth: { kind: 'cookie' },
};
function Status() {
  const snapshot = useSessionSnapshot<'qwenwork'>();
  return <p>连接:{snapshot.phase};事件数:{snapshot.events.length}</p>;
}
export function StatusPanel({ sessionId }: { sessionId: string }) {
  return <SessionClientProvider sessionId={sessionId} config={config}><Status /></SessionClientProvider>;
}

Provider 自动启动、切换会话并清理连接。可用 hooks 还包括 useSessionClient、useSessionEvents、 useSessionStatus、useSessionPhase、useSessionNotifications、useCurrentSessionId。 config 引用应稳定;身份改变时用身份维度的 key 重建 Provider,并隔离/清理宿主缓存。 <'qwenwork'> / <'ma'> 只缩窄 TypeScript 类型,必须匹配 Provider 的实际配置。

React 17/18/19 都使用以上写法。自行管理挂载时,17 使用 react-dom 的 render, 18/19 使用 react-dom/client 的 createRoot;由框架管理挂载时无需额外调用。 SDK 已接入 React 17 所需的订阅 shim,不要再创建一套 Context。

旧 Query 入口(仅 React 18/19、MA)

已有项目可以保留 /session-client/react。其中生成的查询 hooks 如 useGetSession 是 MA-only,不是主站订阅接口,需要业务安装 @tanstack/react-query@5 并提供 QueryClient:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { SessionSdkConfig } from 'qwenwork-open-sdk/session-client';
import { useGetSession } from 'qwenwork-open-sdk/session-client/react';

type DetailProps = {
  sessionId: string; config: SessionSdkConfig<'ma'>; identityKey: string; workspaceId: string;
};
function Detail({ sessionId, config, identityKey, workspaceId }: DetailProps) {
  const query = useGetSession(sessionId, {
    request: { sdk: { ...config, cacheScope: [identityKey, workspaceId] } },
  });
  return <p>{query.isPending ? '加载中' : query.isError ? '加载失败' : '详情已加载'}</p>;
}
export function LegacyDetail({ queryClient, ...props }: DetailProps & { queryClient: QueryClient }) {
  return <QueryClientProvider client={queryClient}>
    <Detail {...props} />
  </QueryClientProvider>;
}

identityKey 与 workspaceId 由宿主提供,用 cacheScope 隔离查询缓存,也可显式设置分区 queryKey。 QueryClient 由宿主持有和清理,不能跨身份复用未隔离缓存;不要把 Token 用作键。 主站用 core 订阅 hooks,或用业务 useQuery 包装数据核心请求函数,不把长连接 SSE 当一次性查询。

Session React:会话界面

可直接使用的主站面板

ManagedSessionProvider 组合数据 Provider、SDK adapter 和 UI Provider,并非仅支持 MA。 先用数据 API 创建会话,再传入 ID;应用入口导入一次样式:

import type { FileUploadInput, SessionSdkConfig } from 'qwenwork-open-sdk/session-client';
import { useSessionSnapshot } from 'qwenwork-open-sdk/session-client/react/core';
import { ManagedSessionProvider, SessionFeed, SessionComposer } from 'qwenwork-open-sdk/session-react';
import 'qwenwork-open-sdk/session-react/styles.css';

const config: SessionSdkConfig<'qwenwork'> = {
  backend: 'qwenwork', baseUrl: '/v1', auth: { kind: 'cookie' },
};
const getUploadInput = (file: File): FileUploadInput => ({
  path: `/uploads/${crypto.randomUUID()}-${file.name}`,
});
function ConnectionStatus() {
  const snapshot = useSessionSnapshot<'qwenwork'>();
  return <p>连接阶段:{snapshot.phase}</p>;
}
export function ChatPanel({ sessionId, identityKey }: { sessionId: string; identityKey: string }) {
  return <ManagedSessionProvider key={identityKey} sessionId={sessionId} config={config}
    getUploadInput={getUploadInput} className="my-session-panel">
    <ConnectionStatus />
    <SessionFeed />
    <SessionComposer accept="image/*,.pdf,.txt" />
  </ManagedSessionProvider>;
}

此例依赖已授权的同源 Cookie API;其他鉴权方式见后端与鉴权。 identityKey 是宿主提供的非敏感身份标识。Provider 自动管理会话切换、历史、SSE 和卸载清理; 不要再嵌套第二个 SessionClientProvider 或创建另一份 client 来读取状态。 config、getUploadInput 和自定义回调保持引用稳定。

SessionFeed 展示消息、工具、规划、问题、权限请求和错误;SessionComposer 处理输入、 提交反馈、附件与中断。在子组件用 useSessionClient(UI 入口或 /react/core)取得同一 client; 用 UI 入口的 useSessionItems / useSessionStatus 取得已投影的条目/状态。 UI 的 useSessionStatus 和数据 hooks 的同名函数返回不同模型,按所需层选择导入。

给外栏明确高度、最小宽度和布局,SDK 不接管整个页面:

.my-session-panel {
  display: flex;
  flex-direction: column;
  width: 400px;
  max-width: 100%;
  height: min(756px, 85dvh);
  min-width: 0;
  overflow: hidden;
}

上传与提交行为

上例的 getUploadInput 启用默认文件选择和拖放;去掉它且不提供 uploadFile 时, Composer 保留文本发送,不显示默认文件选择按钮。MA 的 UI 用法相同,但必须改为 MA 配置、 相对上传路径和前述完整 scope。accept 仅是选择器提示,不是服务端校验。

已有上传服务时用 AttachmentUploader 覆盖;以下组件放在主站 Provider 内:

import { useCallback } from 'react';
import { SessionComposer, type AttachmentUploader, type AttachmentUploadOptions } from 'qwenwork-open-sdk/session-react';

type HostUpload = (file: File, options: AttachmentUploadOptions) => Promise<{ id: string; path: string }>;
export function CustomUploadComposer({ upload }: { upload: HostUpload }) {
  const uploadFile = useCallback<AttachmentUploader>(async (file, options) => {
    const result = await upload(file, options);
    return { id: result.id, name: file.name, sizeBytes: file.size, mimeType: file.type,
      promptFile: { path: result.path } };
  }, [upload]);
  return <SessionComposer uploadFile={uploadFile} />;
}

uploadFile 完全替代默认上传,不再调用 getUploadInput。宿主实现必须遵守 options.signal, 重试复用 options.idempotencyKey,可用 options.onProgress 报告进度;完成登记后才 resolve。 MA 实现返回 { file_id },不能把主站路径传给 MA。

  • 上传中或失败时禁止提交,保留可编辑草稿;重试复用任务,迟到结果不进入新会话。
  • 提交成功清除内部草稿/附件,失败保留;受控 attachments 由宿主在 onSubmitted 后自行清理。
  • 移除附件、切换会话和卸载会中止本地上传,不代表远端取消或删除了文件。
  • onSelectFiles 可接已有选择器;onRemoveFile 处理受控移除;不要重复接两套发送逻辑。
  • 主站可传 <SessionComposer context={selection.trim() ? [selection] : []} />; 每次发送独立编码为隐藏上下文,限制与数据核心相同,MA 不接受此参数。
  • 程序化 client.sendPrompt() 的防重复提交、就绪检查和错误提示由调用方负责。

文件预览与接管

默认 adapter 把产物投影为独立文件卡;点击后在当前会话栏预览,返回保留草稿与消息位置。 主站自动用 getFile 取 download_url;MA 配好经宿主验证的 config.files.scope 后也有默认解析。 没有作用域的 MA 或自定义 source 不会猜身份,不能直接打开 Barn 标识。

支持图片、音视频、PDF、Markdown、HTML、CSV/TSV、代码与文本;编码支持取决于浏览器。 Office/未知格式只提供下载,不向第三方转换服务发送文件或签名 URL。 文本上限 2 MiB,PDF 32 MiB,其他类型按已知大小限制为 200 MiB;超限可下载。 HTML 在独立 origin 的 sandbox 中运行,禁止宿主访问、表单提交和外部网络资源。

通过 Provider 的 files: SessionFileHandlers 覆盖:

| 回调 | 用途 | | --- | --- | | resolveUrl(file, { sessionId, signal }) | 替换默认签名 URL 解析 | | resolvePreview(file, context) | 返回 { url, mimeType?, sizeBytes? },优先用于预览 | | resolveDownloadUrl(file, context) | 下载点击时单独刷新 URL;遵守取消和失败重试 | | onOpen(file, context) | 接管打开行为,内置栏内预览不再同时打开 |

例如接入宿主已有弹窗,由宿主管理弹窗关闭与内容:

import { useMemo } from 'react';
import type { SessionSdkConfig } from 'qwenwork-open-sdk/session-client';
import { ManagedSessionProvider, SessionFeed, SessionComposer,
  type SessionFileHandlers } from 'qwenwork-open-sdk/session-react';

export function HostPreviewChat({ sessionId, config, onOpen }: {
  sessionId: string; config: SessionSdkConfig<'qwenwork'>; onOpen: NonNullable<SessionFileHandlers['onOpen']>;
}) {
  const files = useMemo<SessionFileHandlers>(() => ({ onOpen }), [onOpen]);
  return <ManagedSessionProvider sessionId={sessionId} config={config} files={files}>
    <SessionFeed /><SessionComposer />
  </ManagedSessionProvider>;
}

只提供 onOpen 时保留默认解析;自定义弹窗可在同一 Provider 内复用 SessionFilePreview。 宿主应在身份/会话切换时关闭弹窗并中止旧请求。覆盖 resolveUrl 时不会继承 MA 的专用下载解析, 需要独立下载授权则同时提供 resolveDownloadUrl。不要长期缓存签名地址或复用预览签名冒充下载能力。

渲染扩展与主题

Provider 的 registry: RendererRegistry 限定当前面板:overrides 包装/替换内置行, presentations 渲染自定义卡片,renderItem 返回 undefined 继续默认渲染、返回 null 隐藏。 theme={{ defaultMode: 'system' }} 选择初始模式,theme.mode 可受控,theme.tokens 覆盖 --mus-* 变量;主题限制在 .mus-root,无需 Tailwind,也不修改全局页面主题。

Markdown 与样式

默认 Feed 和 Markdown 文件预览使用完整 CommonMark/GFM、代码高亮与 KaTeX。 styles.css 已包含完整 Markdown 样式和字体引用,不要重复引入另一份 KaTeX CSS。 应用入口仍需显式导入一次 qwenwork-open-sdk/session-react/styles.css;组件 JS 不会自动加载 CSS。 流式内容由会话组件传递状态并合并解析,结束时立即处理最终文本;不是任意长文本的增量解析器。 原始 HTML 不执行,默认链接/图片校验 URL,公式关闭信任扩展;自定义 renderer 仍由宿主保证安全。

统一包不再提供 /session-react/markdown、/session-react/markdown/light 和 /session-react/markdown.css 三个独立子入口。此调整不删除会话组件内部的完整渲染能力, 不改变主入口的其他现有导出,也不改变原分包的入口。 需要定制展示时,继续使用当前 Provider 的 registry;例如只包装工具行,保留默认 Markdown:

import { SessionProvider, SessionFeed, type SessionSource, type RendererRegistry } from 'qwenwork-open-sdk/session-react';
import 'qwenwork-open-sdk/session-react/styles.css';

const registry: RendererRegistry = {
  overrides: { toolCall: ({ item, Default }) => <div className="tool-frame"><Default item={item} /></div> },
};
export function CustomizedFeed({ source }: { source: SessionSource }) {
  return <SessionProvider source={source} registry={registry}><SessionFeed /></SessionProvider>;
}

ManagedSessionProvider 也接受此 registry。宿主需要替换正文渲染时可提供自己的 registry.markdown 组件,接收 { text, streaming? };替换仅限当前 Provider。 无需另建数据 client,也不承诺因此移除默认渲染的依赖。

数据源与 adapters

本包 SessionClient → SessionSource

常规接入直接使用 ManagedSessionProvider。已由宿主管理 client 或数据 Provider 时,可分层接入: 适配函数直接从 /session-react 主入口导入,无需独立 adapter 子入口。

import { useMemo } from 'react';
import type { SessionClient } from 'qwenwork-open-sdk/session-client';
import {
  SessionProvider, SessionFeed, SessionComposer, createSessionSdkSource, useSessionSdkSource,
} from 'qwenwork-open-sdk/session-react';

export function ExistingClientPanel({ client }: { client: SessionClient }) {
  const source = useMemo(() => createSessionSdkSource(client), [client]);
  return <SessionProvider source={source}><SessionFeed /><SessionComposer /></SessionProvider>;
}

// 另一种接法:此组件必须放在已有 SessionClientProvider 内。
export function DataProviderPanel() {
  const source = useSessionSdkSource();
  return <SessionProvider source={source}><SessionFeed /><SessionComposer /></SessionProvider>;
}

第一种接法的 client 必须已绑定会话,由宿主 start() 并最终 destroy();adapter 不接管连接。 SessionProvider 自身只提供 UI Context,不能据此调用数据 useSessionClient()。 第二种从已有数据 Provider 取得 client,不创建第二条连接。需要只读展示时设置 adapter 的 readOnly: true;需要默认上传时提供 getUploadInput。未知命令显式失败,不会猜测业务接口。

自定义 SessionSource

完全自定义数据源实现 SessionSource:getItems、subscribeItems、getStatus、subscribeStatus 以及命令提交等接口。通知之间 getItems() / getStatus() 必须返回稳定引用,订阅返回清理函数; 不支持的命令返回明确失败。将其传给 SessionProvider,文件解析、状态投影、权限和撤回过滤由该 source 负责。

三个模块一起使用

App 负责与宿主通信,Client 负责 Session API,React 负责展示。它们共用一个安装包, 不代表自动共享会话、鉴权或消息。下面接收前文已连接的 App,显式点击才通知宿主:

import { useState } from 'react';
import type { App } from 'qwenwork-open-sdk/qwenwork-app';
import type { SessionSdkConfig } from 'qwenwork-open-sdk/session-client';
import { useSessionClient, useSessionSnapshot } from 'qwenwork-open-sdk/session-client/react/core';
import { ManagedSessionProvider, SessionFeed, SessionComposer } from 'qwenwork-open-sdk/session-react';
import 'qwenwork-open-sdk/session-react/styles.css';

function NotifyHost({ app }: { app: App }) {
  const client = useSessionClient<'qwenwork'>();
  const snapshot = useSessionSnapshot<'qwenwork'>();
  const [busy, setBusy] = useState(false);
  const [failed, setFailed] = useState(false);
  async function notify() {
    setBusy(true); setFailed(false);
    try {
      if (!app.getHostCapabilities()?.message?.text) throw new Error('不支持文本消息');
      const result = await app.sendMessage({
        role: 'user', content: [{ type: 'text', text: `当前会话:${client.sessionId}` }],
      }, { timeout: 5_000 });
      if (result.isError) throw new Error('消息未被接受');
    } catch { setFailed(true); }
    finally { setBusy(false); }
  }
  return <section aria-label="宿主协作">
    <p>连接阶段:{snapshot.phase}</p>
    <button type="button" disabled={busy} onClick={() => void notify()}>告知宿主当前会话</button>
    {failed && <p role="alert">未能通知宿主,请确认连接与权限。</p>}
  </section>;
}
export function UnifiedPanel({ app, sessionId, config, identityKey }: {
  app: App; sessionId: string; config: SessionSdkConfig<'qwenwork'>; identityKey: string;
}) {
  return <ManagedSessionProvider key={`${identityKey}:${sessionId}`} sessionId={sessionId} config={config}>
    <NotifyHost app={app} /><SessionFeed /><SessionComposer />
  </ManagedSessionProvider>;
}

App 的创建与关闭由外层负责,Provider 内使用同一个 Session client。 不要把每条 Session 事件自动转发给 app.sendMessage,也不要把宿主事件自动回发给 sendPrompt, 否则可能重复提交、形成循环或越过用户的发送意图。

迁移、错误与安全边界

  • 从旧 @qwenwork/sdk 统一候选迁移时,先移除旧依赖及相关 overrides, 把所有导入前缀换成 qwenwork-open-sdk,并按下列说明迁移已移除的子入口; 其余类名和方法保持不变,不混装两个包名。
  • 从分包迁移时保持类名和方法,将 App 的导入改为 /qwenwork-app,数据包改为 /session-client, UI 改为 /session-react,React 子入口映射见入口表。 不同时安装并混用旧分包与大包,避免两个 SDK Context、连接或样式版本。
  • 两个 /session-react/adapters/* 子入口不再公开。原 /adapters/session-sdk 的函数与类型 改从 /session-react 导入;旧 /adapters/session-client 的 SessionManagerLike 迁移能力 不随统一包提供。已有 Manager 集成需先迁移到 SessionClient 或自行实现 SessionSource, 原 Session 分包的兼容入口不变。
  • 从旧统一候选升级时,移除对三个独立 Markdown 子路径的导入;默认会话界面保留 /session-react 和 /session-react/styles.css 即可,不再单独导入或注册内置 Markdown 渲染器。
  • 请求错误用 try/catch,长连接错误看 snapshot.error 或 client 的 error 事件; 连接正常不等于当前轮次成功。数据核心的 classifySessionFailure / eventSessionFailure 可提供结构化恢复建议,不直接向终端用户展示原始上游错误,不自动重试被内容策略拒绝的输入。
  • 历史和实时合并、重复与乱序由数据 SDK 处理;手动分页按 has_more / next_cursor 继续, 不能因过滤后条目为空就提前结束,也不能假设 SSE 序号连续。
  • hidden_text 控制默认展示,不删除服务端数据。自定义 UI 还需用 resolveContentSafety(snapshot) 执行内容撤回过滤;raw 事件可能保留被撤回内容。默认 UI adapter 已处理,业务审核责任不因此消失。
  • API 跨域访问、存储 PUT 和签名内容 GET 分别需要正确 CORS;更换 Bearer 不会修复 CORS。 宿主代理须验证身份、目标与大小,不能是任意 URL 代理;不要向存储或第三方携带业务 Token。
  • SSR 不跨请求共享 client、身份或 Query 缓存;App 在客户端挂载后连接。React Native 等环境需自行 提供支持流与取消的 Fetcher,兼容类型不等于已做设备验收。
  • 当前主站范围是 cloud;完整生成类型不代表 remote、Connector / Expert Kit 已接线。 Session 模块不提供登录、授权授予、会话列表导航或沙箱生命周期;Host 通信入口不代替产品宿主。
  • 本 README 示例会对最终 TGZ 做类型验证,消费矩阵覆盖 core 与 React 17/18/19; 模拟后端/DOM 检查不等于真实宿主、媒体编码、生产会话和跨域文件链路验收。

本包的原创 SDK 代码采用 Apache-2.0。第三方组件保留各自许可证, 包括 KaTeX CSS 的 MIT 和字体的 SIL OFL 1.1;完整声明见 THIRD_PARTY_NOTICES.txt。