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。统一包版本不随组件版本或其标签自动变化。
目录
- 安装与运行要求
- 选择引入入口
- App:连接与宿主协作
- Session Client:数据与文件
- React 数据 hooks
- Session React:会话界面
- Markdown 与样式
- 数据源与 adapters
- 三个模块一起使用
- 迁移、错误与安全边界
安装与运行要求
本文对应 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。
