# tsreport-sdk [English](./README.md) | [日本語](./README.ja.md) | 简体中文 | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [Tiếng Việt](./README.vi.md) | [ไทย](./README.th.md) | [Bahasa Indonesia](./README.id.md) | [Deutsch](./README.de.md) | [Français](./README.fr.md) | [Español](./README.es.md) | [Português](./README.pt.md) | [العربية](./README.ar.md) | [עברית](./README.he.md) **这是一个用于向报表服务器请求打印 PDF 并接收结果的客户端。运行时依赖包为零。设计前提是在服务器端(Node.js)使用。** tsreport-sdk 负责处理与 tsreport-editor 提供的外部打印 API 服务器之间的所有通信。基于 OAuth 2.0 的认证、打印任务的提交、等待完成、PDF 的下载,乃至浏览器预览所需素材的获取,都可以作为一个带类型的类来统一处理。 令牌的获取、有效期管理、失效时的重新获取都在库内部完成,因此使用方只需编写"向哪个模板传入哪些数据"即可。 ## 架构:存在两个认证边界 集成此 SDK 的系统由三层构成。**请先掌握整体结构。** 关于此 SDK 的大多数误解,都源于混淆了这两个认证边界。 ```mermaid flowchart LR browser["浏览器\ncreateEndpointConnector\n(不持有凭据)"] app["自有应用服务器\ncreatePreviewEndpoint + TsreportClient\n(clientSecret仅存在于此)"] editor["打印API服务器\n(tsreport-editor)"] browser -->|"认证边界A:会话认证\n(需自行实现)"| app app -->|"认证边界B:OAuth 2.0\n(SDK自动处理)"| editor ``` | 层 | 使用的 SDK 组件 | 在认证中的角色 | | --- | --- | --- | | 浏览器 | `createEndpointConnector()` | 边界 A 的**受认证方**。只发送会话 Cookie 等,完全不持有凭据 | | 自有应用服务器 | `createPreviewEndpoint()`+`TsreportClient` | **检验**边界 A 的一方(此检验由应用自行实现)。边界 B 交给 SDK 处理。`clientSecret`仅由此层持有 | | 打印 API 服务器 | (由 tsreport-editor 提供) | 检验边界 B | **SDK 只负责边界 B。** 通过`clientId`/`clientSecret`进行的令牌获取、有效期管理、重新发送,全部由`TsreportClient`在内部处理。 而另一方面,**判断边界 A——"当前操作者是谁,此人是否可以查看该报表"——的机制,在此 SDK 中并不存在。** `createPreviewEndpoint()`是完全不进行任何授权判断的直通中继。登录、会话管理、权限判定,请务必将应用自身已有的机制**放置在端点前面自行实现**。若省略此步骤,任何能访问到该 URL 的人都能读取报表和素材。 后续示例中出现的"会话确认"和`requireAppUser`并非装饰性内容。**它们是应用侧必须自行实现的边界 A 的自有代码。** ## 这个包做什么,不做什么 此包**仅**负责通信。 - **负责的事** — 认证(OAuth 2.0 client credentials)、调用打印 API、轮询任务状态、获取 PDF、获取预览素材、提供放置于应用服务器上的中继端点 - **运行位置** — `TsreportClient`和`tsreport-sdk/server`仅限服务器端使用。可在浏览器中使用的只有不持有凭据的`createEndpointConnector()` - **不负责的事** — 报表的布局或 PDF 生成(由`tsreport-core`负责)、预览画面的渲染(由`tsreport-react`负责),以及**使用者的认证与授权**(边界 A,由使用方应用负责) 设计上还有两项约定。**运行时依赖包为零**(`package.json`中没有`dependencies`),且**完全不读取环境变量**。连接目标和凭据全部作为显式参数传入,因此无论部署在什么环境中,行为都不会改变。 ## 安装 ```sh npm install tsreport-sdk ``` 可在 Node.js 18 以上版本运行。内部仅使用`fetch`和 Web Streams,不依赖 Node.js 特有的 API。 **请在服务器端使用此库。** 核心的`TsreportClient`需要`clientSecret`,若在浏览器中运行,密钥将分发给所有使用者。若想从浏览器操作报表,请采用后文"从浏览器进行预览"中说明的三层结构,在浏览器侧仅使用不持有凭据的`createEndpointConnector()`。 ## 前提条件 此库无法单独运行。前提是**tsreport-editor 的外部打印 API 服务器正在运行**。请从该服务器获取以下四项内容。 | 所需内容 | 说明 | | --- | --- | | 基础 URL | API 服务器的 URL(例如:`https://reports.example.com`)。末尾是否带斜杠均可 | | 客户端 ID | OAuth 2.0 的 client_id | | 客户端密钥 | OAuth 2.0 的 client_secret。**请仅在服务器端保存,绝不能传给浏览器** | | 工作区密钥 | 存放报表的工作区标识符(UUID 格式) | 客户端会根据用途分配相应的作用域。共有四种:`report:print`(提交打印)、`report:status`(确认状态)、`report:download`(获取 PDF)、`report:preview`(获取预览素材)。 ## 第一步:将报表作为 PDF 接收 以下是传入模板和数据后获取 PDF 字节序列的最简代码。 ```ts import { writeFileSync } from 'node:fs' import { TsreportClient } from 'tsreport-sdk' const client = new TsreportClient({ baseUrl: 'https://reports.example.com', clientId: 'my-client-id', clientSecret: 'my-client-secret', }) const pdf = await client.printAndDownload( '00000000-0000-0000-0000-000000000002', // 工作区键 'invoice.report', // 工作区内模板的路径 'v1', // 模板的标签(版本) { rows: [{ item: '部件A', amount: 12000 }] }, // 要填入报表的数据 ) writeFileSync('./invoice.pdf', pdf) ``` `printAndDownload()`会一次性执行随后说明的"提交 → 等待完成 → 下载"这三个步骤。 ## 打印任务的流程 打印是**异步**的。在提交请求的时刻 PDF 尚不存在,由服务器端的批处理依次生成。因此提交和接收被拆分为两个步骤。 ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → 会抛出PrintJobError └───────┘ ``` - `print()`返回**任务密钥**(字符串)。此时 PDF 尚未生成 - 任务状态会从`queued`(排队等待)→ `processing`(生成中)→ `completed`(已完成)依次转变。失败时变为`error` - `waitForCompletion()`会反复确认状态直到变为`completed`,若变为`error`则抛出异常 - 只有变为`completed`之后才能通过`downloadPdf()`获取 PDF ## 按用途区分的使用方法 ### 想一次调用即接收 PDF —— `printAndDownload()` 将提交、等待、下载合并执行。**通常请使用此方法。** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` 若想更改等待的间隔或上限,请在第 5 个参数中指定。 ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { intervalMs: 2000, // 检查状态的间隔 timeoutMs: 90000, // 超过此时间则抛出PollTimeoutError }) ``` ### 想将提交与接收分开 —— `print()` 与 `waitForCompletion()` 适用于将任务密钥保存到数据库、之后再去获取结果的结构。 ```ts // 只发起请求,并记下键 const key = await client.print(workspaceKey, 'invoice.report', 'v1', data) await db.jobs.insert({ key, requestedAt: new Date() }) // ── 在另一个请求或另一个进程中 ── await client.waitForCompletion(key) const pdf = await client.downloadPdf(key) ``` ### 想自行管理进度 —— `getStatus()` 不进行等待,仅获取当下瞬间的状态。适用于在画面上显示进度等场景。 ```ts const status = await client.getStatus(key) if (status.status === 'completed') { const pdf = await client.downloadPdf(key) } else if (status.status === 'error') { console.error('打印失败:', status.errorReason) } else { console.log('处理中:', status.status) // 'queued' 或 'processing' } ``` `getStatus()`仅返回状态,即使是`error`也不会抛出异常(抛出异常的是`waitForCompletion()`)。 ### 想不将大型 PDF 载入内存进行处理 —— `getPdfStream()` `downloadPdf()`会将整个 PDF 展开到内存中。对于数百页的报表等,以流的形式接收并直接输出到文件或 HTTP 响应更为安全。 ```ts import { Writable } from 'node:stream' import { createWriteStream } from 'node:fs' const stream = await client.getPdfStream(key) await stream.pipeTo(Writable.toWeb(createWriteStream('./invoice.pdf'))) ``` ### 想中途中断等待 —— `signal` 在使用者离开画面等情况下,停止轮询。 ```ts const controller = new AbortController() // 用户点击“取消”后执行 controller.abort(new Error('已取消')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` 中断后,Promise 会以传给`signal.reason`的值被 reject。 ## 从浏览器进行预览 要在浏览器中显示报表预览,需要将模板、字体、图像等素材传送到浏览器端。此时**不能将客户端密钥放在浏览器中**,因此需要采用在中间夹入自有应用服务器的三层结构。 ``` ┌──────────────┐ ①请求素材 ┌────────────────┐ ②认证并转发 ┌──────────────┐ │ 浏览器 │ ───────────────→ │ 自己的应用 │ ───────────────→ │ 打印API服务器 │ │ │ │ 服务器 │ │ │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④素材送达 │ Endpoint │ ③素材返回 │ │ └──────────────┘ └────────────────┘ └──────────────┘ 不持有凭据 clientSecret只在这里 ``` - 浏览器侧使用`createEndpointConnector()`,只需知道**自有应用的 URL**即可 - 应用服务器侧只需放置`createPreviewEndpoint()`,即可附加认证后中继到打印 API - 此中继端点直接满足`tsreport-react`的`PreviewConnector`契约,因此可以直接传给预览组件 此图中"浏览器→应用"之间是前文所述的**边界 A**(应用的会话认证・自行实现),"应用→打印API"之间是**边界 B**(OAuth・由 SDK 处理)。 ### 想在应用服务器上放置中继端点 —— `createPreviewEndpoint()` 从`tsreport-sdk/server`读取(**仅限服务器端**的子路径)。 ```ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' const client = new TsreportClient({ baseUrl: 'https://reports.example.com', clientId: 'my-client-id', clientSecret: 'my-client-secret', }) const handler = createPreviewEndpoint({ client, target: { workspace: '00000000-0000-0000-0000-000000000002', path: 'reports/invoice.report', tag: 'v1', }, }) ``` `handler`是一个标准函数,类型为`(request: Request) => Promise`。 指定`target`后,**无论浏览器请求什么,都必定返回此模板**。这样可以防止浏览器端能够指定任意模板所导致的事故,因此在公开画面中推荐指定`target`(省略时将遵从浏览器端的指定)。 > **重要**:`createPreviewEndpoint()`完全不进行任何授权判断。"该用户是否可以查看此报表"的判断是使用方的责任。请务必将其挂载在应用认证与授权的**内侧**。 ### 想集成到 Next.js App Router 将中继处理器**先用自有认证包裹**,再作为路由处理器导出。 ```ts // app/api/report-preview/route.ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' import { getSessionUser } from '@/lib/auth' // ←由应用自行实现(NextAuth、iron-session、自建会话等) const client = new TsreportClient({ baseUrl: process.env.REPORT_API_URL!, clientId: process.env.REPORT_CLIENT_ID!, clientSecret: process.env.REPORT_CLIENT_SECRET!, }) const previewHandler = createPreviewEndpoint({ client, target: { workspace: '工作区键', path: 'reports/invoice.report', tag: 'v1' }, }) export async function GET(request: Request): Promise { // 这里是应用自己的会话认证(边界A)。由于SDK不做任何检查, // 如果省略此检查,所有能访问该URL的人都能查看报表 const user = await getSessionUser(request) if (user === null) { return new Response( JSON.stringify({ message: 'unauthorized', statusCode: 401 }), { status: 401, headers: { 'content-type': 'application/json' } }, ) } return previewHandler(request) } ``` 虽然从技术上也可以直接以`export const GET = createPreviewEndpoint(...)`的方式导出,但**由于没有夹入认证的位置,不推荐这样做**。请务必按照上述形式,先通过自有认证再进行中继。 环境变量的读取属于**使用方的代码**。库本身不读取环境变量。 ### 想集成到 Express —— `toExpressHandler()` ```ts import express from 'express' import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint, toExpressHandler } from 'tsreport-sdk/server' const app = express() const client = new TsreportClient({ baseUrl, clientId, clientSecret }) const handler = createPreviewEndpoint({ client, target }) // requireAppUser是应用自己的会话认证中间件(边界A,自行实现)。 // 挂载在此位置之后即构成授权的边界 app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()`会在中继本身失败时(例如无法连接到 API 服务器等)转发给`next(error)`,因此可以在应用的错误处理器中处理。此外,**express 并非此包的依赖**。它只是接收类型形状匹配的对象。 ### 想集成到 `node:http` —— `toNodeHandler()` ```ts import { createServer } from 'node:http' import { createPreviewEndpoint, toNodeHandler } from 'tsreport-sdk/server' const previewHandler = toNodeHandler(createPreviewEndpoint({ client, target })) createServer(async (req, res) => { if (req.url?.startsWith('/api/report-preview')) { // 应用自己的会话认证(边界A,自行实现)。不得省略 if (!isAuthorizedAppUser(req)) { // ←由应用自行实现 res.statusCode = 401 res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ message: 'unauthorized', statusCode: 401 })) return } try { await previewHandler(req, res) } catch (error) { res.statusCode = 500 res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ message: 'preview failed', statusCode: 500 })) } return } res.statusCode = 404 res.end() }).listen(3000) ``` `toNodeHandler()`返回的 Promise 会在中继本身失败时 reject。请像上例那样接住它,并返回符合应用方针的响应。 ### 想从浏览器侧获取素材 —— `createEndpointConnector()` 只需传入放置在应用服务器上的中继端点的 URL 即可。 ```ts import { createEndpointConnector } from 'tsreport-sdk' const connector = createEndpointConnector({ endpoint: '/api/report-preview', fetchInit: { credentials: 'include' }, // 发送应用的会话Cookie(以通过边界A的认证) }) const payload = await connector.fetchTemplate({ workspace: '工作区键', path: 'reports/invoice.report', tag: 'v1', }) // payload.template … 模板定义 // payload.fontIds … 此模板所需的字体ID数组 for (const fontId of payload.fontIds) { const bytes = await connector.fetchFont(fontId) // 找不到则为 null } ``` 若想在 TypeScript 中为模板赋予类型,请传入类型参数。 ```ts import type { ReportTemplate } from 'tsreport-core' const connector = createEndpointConnector({ endpoint: '/api/report-preview' }) ``` 连接器有一个**需要记住的行为**。当`fetchTemplate()`成功后,会记住该模板所属的工作区和目录,并在此后的`resolveImage()`中自动附加。因此,在解析图像之前请务必先调用`fetchTemplate()`(如果是`target`固定的端点,服务器端会自动补全,因此不需要)。 ## 想在服务器端直接处理预览素材 也可以不经过浏览器,直接从 Node.js 获取素材。 ### 想获取模板 —— `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds`是该模板所需字体的列表。包括默认字体和数学公式用字体在内,**服务器都是使用与打印流水线相同的逻辑计算得出的**,因此按照此列表加载即可使预览与打印结果保持一致。 ### 想获取子报表的模板 —— `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` 与`getPreviewTemplate()`的区别在于不接受标签参数。 ### 想获取图像等文件 —— `getPreviewFile()` ```ts const bytes = await client.getPreviewFile(workspaceKey, 'assets/logo.png') ``` ### 想列出可用的字体 —— `listPreviewFonts()` ```ts const fonts = await client.listPreviewFonts() // [{ id: 'NotoSansJP', fileName: 'NotoSansJP-VariableFont_wght.ttf' }, ...] ``` ### 想获取字体实体 —— `getPreviewFont()` ```ts const fontBytes = await client.getPreviewFont('NotoSansJP') ``` ### 想自行编写中继逻辑 —— `fetchPreviewResource()` 当上述方法无法满足需求时,可携带认证信息对任意预览路径执行 GET 请求。 ```ts const response = await client.fetchPreviewResource('/api/report/preview/fonts') ``` **唯独此方法即使出错也不会抛出异常,而是原样返回`Response`。** 这是一个低层级入口,用于希望原样中继状态码或响应头的场景(`createPreviewEndpoint()`正是使用了这一点)。 ## 认证机制 使用方无需在意,但内部按以下方式运作。 1. 调用需要认证的方法时,会确认是否有有效的令牌 2. 若没有,则以`grant_type=client_credentials`向`POST /api/oauth/token`发起请求 3. 获取到的令牌**仅保存在实例的内存中**,在有效期到期前 10 秒内可复用 4. 若请求被以 401 或 403 拒绝,会丢弃令牌并重新获取,**将同一请求重发一次** 令牌不会被保存到磁盘或外部存储中。仅在明确需要令牌时才使用`getAccessToken()`。 ```ts const token = await client.getAccessToken() ``` ## 错误处理 所有错误都继承自`TsreportClientError`,因此既可以统一捕获,也可以按类型分别处理。 ```ts import { ApiError, PollTimeoutError, PrintJobError, TokenError, TsreportClientError, } from 'tsreport-sdk' try { const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) } catch (error) { if (error instanceof TokenError) { // 凭据错误、客户端已被禁用等 console.error('认证失败:', error.status, error.errorCode) } else if (error instanceof PrintJobError) { // 由于模板或数据的原因,服务器端生成失败 console.error('打印作业失败:', error.key, error.errorReason) } else if (error instanceof PollTimeoutError) { // 未在时限内完成(作业本身可能仍然存活) console.error('等待超时:', error.key, error.timeoutMs) } else if (error instanceof ApiError) { // API返回了4xx/5xx(模板不存在、权限不足等) console.error('API错误:', error.status, error.errorMessage) } else if (error instanceof TsreportClientError) { // 上述以外的情况 console.error(error.message) } } ``` | 错误类 | 发生场景 | 携带的信息 | | --- | --- | --- | | `TokenError` | 令牌获取失败(凭据有误、客户端被禁用等) | `status`(HTTP状态码)、`errorCode`、`errorDescription` | | `ApiError` | API返回了4xx/5xx(模板不存在、权限不足等) | `status`、`errorMessage`(服务器返回的消息) | | `PrintJobError` | 任务状态变为`error` | `key`、`errorReason`(服务器返回的失败原因) | | `PollTimeoutError` | `waitForCompletion()`在限定时间内未能确认完成 | `key`、`timeoutMs` | | `TsreportClientError` | 以上情况以外。所有错误的基类 | — | `PollTimeoutError`只是"客户端停止了等待",**并不意味着服务器端的任务失败了**。密钥仍然有效,之后可以通过`getStatus()`确认。 另外,唯独`createEndpointConnector()`返回的连接器例外,在素材找不到时(404)不会抛出异常而是返回`null`。这是因为即使部分素材缺失,预览也应当能够继续渲染。但`fetchTemplate()`不在此列,若模板本身无法获取,则会抛出`ApiError`(因为此时没有任何可供渲染的内容)。 ## API 参考 ### `new TsreportClient(options)` | 属性 | 类型 | 必需 | 说明 | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | API服务器的基础URL。末尾的斜杠会被自动去除 | | `clientId` | string | ✓ | OAuth 2.0的client_id | | `clientSecret` | string | ✓ | OAuth 2.0的client_secret | | `scope` | string | | 以空格分隔指定所要请求的作用域以进行限定。省略时,将赋予客户端已注册的全部作用域 | ### 方法 | 方法 | 返回值 | 说明 | | --- | --- | --- | | `print(workspace, templatePath, tag, data)` | `Promise` | 提交打印请求,返回任务密钥 | | `getStatus(key)` | `Promise` | 返回任务的当前状态(不抛出异常) | | `waitForCompletion(key, options?)` | `Promise` | 等待直到完成。失败时抛出`PrintJobError`,超时抛出`PollTimeoutError` | | `downloadPdf(key)` | `Promise` | 获取PDF的全部字节 | | `getPdfStream(key)` | `Promise>` | 以流的形式获取PDF | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | 一次性完成提交、等待、下载 | | `getAccessToken()` | `Promise` | 返回有效的访问令牌(必要时会获取) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | 获取模板定义及所需字体ID | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | 获取子报表的模板 | | `getPreviewFile(workspace, filePath)` | `Promise` | 获取工作区内的文件(图像等) | | `listPreviewFonts()` | `Promise` | 获取可用字体的列表 | | `getPreviewFont(id)` | `Promise` | 获取字体实体 | | `fetchPreviewResource(resourcePath)` | `Promise` | 携带认证信息对任意预览路径执行GET请求,返回原始`Response`(不抛出异常) | ### `WaitForCompletionOptions` | 属性 | 类型 | 必需 | 说明 | | --- | --- | --- | --- | | `intervalMs` | number | | 确认状态的间隔(毫秒)。默认:1000 | | `timeoutMs` | number | | 等待的上限(毫秒)。超过则抛出`PollTimeoutError`。默认:120000 | | `signal` | AbortSignal | | 用于中断等待的信号。中断时会以`signal.reason`进行reject | ### 返回值类型 | 类型 | 结构 | | --- | --- | | `PrintStatusResult` | `{ key: string, status: PrintJobState, errorReason?: string }` | | `PrintJobState` | `'queued'`=排队等待 / `'processing'`=生成中 / `'completed'`=已完成 / `'error'`=失败 | | `PreviewTemplateResult` | `{ template: unknown, fontIds: string[] }` | | `PreviewFontInfo` | `{ id: string, fileName: string }` | ### `createEndpointConnector(options)` | 属性 | 类型 | 必需 | 说明 | | --- | --- | --- | --- | | `endpoint` | string | ✓ | 挂载中继端点的URL(例如:`/api/report-preview`) | | `fetchInit` | RequestInit | | 附加到所有请求上的配置。若要发送会话cookie,可设为`{ credentials: 'include' }` | 返回的连接器共有以下 4 个方法。除`fetchTemplate()`外,其余在素材找不到时均返回`null`。 | 方法 | 返回值 | 说明 | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | 获取模板。`source`为`{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | 获取字体实体 | | `resolveImage(ref)` | `Promise` | 解析模板所引用的图像 | | `fetchSubreportTemplate(ref, context)` | `Promise` | 获取子报表的模板。`context`为`{ workingDirectory }` | ### `tsreport-sdk/server` | 函数 | 返回值 | 说明 | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | 创建预览素材的中继处理器 | | `toNodeHandler(handler)` | `(req, res) => Promise` | 用于`node:http`的适配器。中继本身失败时会reject | | `toExpressHandler(handler)` | `(req, res, next) => Promise` | 用于Express的适配器。中继本身的失败会转发给`next(error)` | `createPreviewEndpoint`的选项: | 属性 | 类型 | 必需 | 说明 | | --- | --- | --- | --- | | `client` | TsreportClient | ✓ | 已认证的客户端。连接目标和凭据仅从此处传入 | | `target` | `{ workspace, path, tag }` | | 要固定的模板。指定后将忽略请求方的指定,始终返回此模板。图像和子报表的基准目录也会被自动补全 | ## 示例代码 `examples/`目录中附带了可直接运行的实现示例(也包含在 npm 包中)。 | 文件 | 内容 | | --- | --- | | `examples/nextjs-route.ts` | Next.js App Router的路由处理器(附带会话认证门禁) | | `examples/express-server.ts` | 集成到Express中的方式及授权中间件的位置 | | `examples/node-server.ts` | 集成到`node:http`中的方式 | | `examples/browser-connector.ts` | 从浏览器获取预览素材 | 这些示例在`npm test`中会**实际启动服务器进行验证**,因此不会混入无法运行的代码。 ## 测试 | 命令 | 内容 | | --- | --- | | `npm test` | 单元测试・集成测试。包含实际启动`examples/`进行的验证 | | `npm run test:live` | **以正在运行的tsreport-editor及种子数据为前提**的真实服务器联调测试 | ## 运行环境 - Node.js 18 以上(`TsreportClient`和`tsreport-sdk/server`的运行环境) - 现代浏览器(仅限`createEndpointConnector()`。因不持有凭据,可安全部署) - 无运行时依赖包 - ESM / CommonJS 双支持 ## 相关项目 - [tsreport-core](https://github.com/pontasan/tsreport-core) - [tsreport-editor](https://github.com/pontasan/tsreport-editor) - [tsreport-sdk](https://github.com/pontasan/tsreport-sdk) - [tsreport-react](https://github.com/pontasan/tsreport-react) ## License tsreport-sdk 可由使用者选择以 [MIT License](./LICENSE-MIT) 或 [Apache License 2.0](./LICENSE-APACHE) 使用(SPDX:`MIT OR Apache-2.0`)。