@unifyport/sdk-node 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -7
- package/package.json +6 -6
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
# `@unifyport/sdk-node`
|
|
2
2
|
|
|
3
|
-
面向 Node.js/TypeScript 的 UnifyPort Device API SDK
|
|
4
|
-
`
|
|
5
|
-
|
|
3
|
+
面向 Node.js/TypeScript 的 UnifyPort Device API SDK。它提供类型安全的
|
|
4
|
+
`UnifyPortDeviceClient`、统一的成功响应与错误模型,以及 timeout、安全重试、取消和 cursor
|
|
5
|
+
分页能力。
|
|
6
|
+
|
|
7
|
+
SDK 使用固定的 `X-Api-Key` 认证边界。`/v1/accounts/...` operation 属于 Device API,统一由
|
|
8
|
+
`UnifyPortDeviceClient` 提供。
|
|
9
|
+
|
|
10
|
+
## 功能范围
|
|
11
|
+
|
|
12
|
+
当前公开契约中的全部 operation 都有同名 SDK 方法,主要覆盖:
|
|
13
|
+
|
|
14
|
+
- workspace 与 provider region 查询;
|
|
15
|
+
- provider 账号资源、授权流程与运行状态;
|
|
16
|
+
- 会话、联系人和群组;
|
|
17
|
+
- 消息发送与消息动作;
|
|
18
|
+
- API Key 与 webhook endpoint 管理。
|
|
19
|
+
|
|
20
|
+
方法名与 OpenAPI `operationId` 保持一致。每个 operation 的参数、请求体、返回字段和 TypeScript
|
|
21
|
+
示例见 [API Reference](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/api-reference/README.md);
|
|
22
|
+
HTTP path、重试和 MCP 策略见
|
|
23
|
+
[API 覆盖表](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/api-coverage.md)。
|
|
24
|
+
|
|
25
|
+
## 环境要求
|
|
26
|
+
|
|
27
|
+
- Node.js `>=22.12.0`
|
|
28
|
+
- TypeScript 项目或支持 ESM 的 Node.js 项目
|
|
29
|
+
|
|
30
|
+
该 package 只提供 ESM `import` 和 TypeScript 声明文件,不提供 CommonJS `require()` 入口。
|
|
6
31
|
|
|
7
32
|
## 安装
|
|
8
33
|
|
|
@@ -10,10 +35,270 @@
|
|
|
10
35
|
npm install @unifyport/sdk-node
|
|
11
36
|
```
|
|
12
37
|
|
|
13
|
-
|
|
14
|
-
[项目 README](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs#readme)。公开发布前必须运行:
|
|
38
|
+
也可以使用 pnpm:
|
|
15
39
|
|
|
16
40
|
```bash
|
|
17
|
-
pnpm
|
|
18
|
-
|
|
41
|
+
pnpm add @unifyport/sdk-node
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 快速开始
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { UnifyPortDeviceClient } from "@unifyport/sdk-node";
|
|
48
|
+
|
|
49
|
+
function requiredEnv(name: string): string {
|
|
50
|
+
const value = process.env[name];
|
|
51
|
+
if (!value) {
|
|
52
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 凭据只从运行环境读取,避免进入源码、构建产物或版本控制。
|
|
58
|
+
const device = new UnifyPortDeviceClient({
|
|
59
|
+
baseUrl: requiredEnv("UNIFYPORT_DEVICE_API_BASE_URL"),
|
|
60
|
+
apiKey: requiredEnv("UNIFYPORT_DEVICE_API_KEY")
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await device.getWorkspace();
|
|
64
|
+
|
|
65
|
+
console.log(result.data.data.name);
|
|
66
|
+
console.log(result.status, result.requestId);
|
|
19
67
|
```
|
|
68
|
+
|
|
69
|
+
`ApiResult.data` 是完整 API JSON envelope,因此资源数据通常位于 `result.data.data`。
|
|
70
|
+
|
|
71
|
+
## Client 配置
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const device = new UnifyPortDeviceClient({
|
|
75
|
+
baseUrl: "https://device.example.com",
|
|
76
|
+
apiKey: () => requiredEnv("UNIFYPORT_DEVICE_API_KEY"),
|
|
77
|
+
timeoutMs: 15_000,
|
|
78
|
+
maxResponseBytes: 4 * 1024 * 1024,
|
|
79
|
+
retry: {
|
|
80
|
+
maxRetries: 2,
|
|
81
|
+
baseDelayMs: 200,
|
|
82
|
+
maxDelayMs: 5_000
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| 配置项 | 必填 | 说明 |
|
|
88
|
+
| ------------------- | ---- | --------------------------------------------------------------------- |
|
|
89
|
+
| `baseUrl` | 是 | Device API 的绝对 URL;远程地址必须使用 HTTPS |
|
|
90
|
+
| `apiKey` | 是 | 字符串,或返回字符串的同步/异步 provider |
|
|
91
|
+
| `fetch` | 否 | 自定义 `fetch` 实现,签名为 `(request: Request) => Promise<Response>` |
|
|
92
|
+
| `timeoutMs` | 否 | 默认 `30000`;每次 operation 的总超时 |
|
|
93
|
+
| `maxResponseBytes` | 否 | 默认 8 MiB,最大 64 MiB;同时限制 wire body 与规范化后的 JSON |
|
|
94
|
+
| `retry` | 否 | 安全 operation 的重试次数和 full-jitter 退避配置 |
|
|
95
|
+
| `allowInsecureHttp` | 否 | 默认 `false`;仅允许显式开启的 loopback HTTP 开发环境 |
|
|
96
|
+
|
|
97
|
+
`baseUrl` 不应包含 username、password、query 或 fragment。API key 由 client 统一注入,单次 operation
|
|
98
|
+
不能覆盖认证 header。
|
|
99
|
+
|
|
100
|
+
## 调用 operation
|
|
101
|
+
|
|
102
|
+
方法统一使用 `(request?, execution?)`:
|
|
103
|
+
|
|
104
|
+
- 没有参数的 operation 可以省略 `request`;
|
|
105
|
+
- path 和 query 参数放在 `request.params`;
|
|
106
|
+
- JSON 请求体放在 `request.body`;
|
|
107
|
+
- 单次 timeout、重试收紧和取消信号放在 `execution`。
|
|
108
|
+
|
|
109
|
+
### 无参数读取
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const workspace = await device.getWorkspace();
|
|
113
|
+
console.log(workspace.data.data);
|
|
114
|
+
|
|
115
|
+
const accounts = await device.listAccounts();
|
|
116
|
+
for (const account of accounts.data.data) {
|
|
117
|
+
console.log(account.id, account.provider, account.status);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Path 参数
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const regions = await device.listProviderRegions({
|
|
125
|
+
params: {
|
|
126
|
+
path: { provider: "whatsapp" }
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
for (const region of regions.data.data.regions) {
|
|
131
|
+
console.log(region.region, region.supported, region.allocatable);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Path 与 query 参数
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const contacts = await device.listContacts({
|
|
139
|
+
params: {
|
|
140
|
+
path: { account_id: "acc_xxx" },
|
|
141
|
+
query: { limit: 100, q: "Alice" }
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log(contacts.data.data.items);
|
|
146
|
+
console.log(contacts.data.data.next_cursor);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### JSON 请求体
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const message = await device.sendMessage({
|
|
153
|
+
body: {
|
|
154
|
+
account_id: "acc_xxx",
|
|
155
|
+
to: { id: "recipient_xxx", type: "user" },
|
|
156
|
+
message: { type: "text", text: "Hello from UnifyPort" }
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
console.log(message.data.data.message_id, message.data.data.status);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
写操作不会因为 HTTP method 或调用方配置而自动获得重试能力。消息正文等敏感输入不应写入日志。
|
|
164
|
+
|
|
165
|
+
## 成功响应
|
|
166
|
+
|
|
167
|
+
所有 operation 都返回 `ApiResult<T>`:
|
|
168
|
+
|
|
169
|
+
| 字段 | 说明 |
|
|
170
|
+
| ----------- | ------------------------------------------------------- |
|
|
171
|
+
| `data` | 按公开契约解析后的完整 API JSON envelope |
|
|
172
|
+
| `status` | HTTP status |
|
|
173
|
+
| `requestId` | 可选的服务端 request ID |
|
|
174
|
+
| `response` | 底层 `Response`,用于读取正常响应的 header 等传输元数据 |
|
|
175
|
+
|
|
176
|
+
`response` 的 body 已由 SDK 解析,不应依赖再次读取;业务数据使用类型化的 `data`。
|
|
177
|
+
|
|
178
|
+
## Timeout、取消与重试
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
|
|
183
|
+
const workspace = await device.getWorkspace(
|
|
184
|
+
{},
|
|
185
|
+
{
|
|
186
|
+
timeoutMs: 5_000,
|
|
187
|
+
retry: { maxRetries: 1 },
|
|
188
|
+
signal: controller.signal
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
单次 `execution.retry` 只能关闭或收紧 client 级重试:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
await device.getWorkspace({}, { retry: false });
|
|
197
|
+
await device.getWorkspace({}, { retry: { maxRetries: 0 } });
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
自动重试只适用于策略明确允许的安全 operation,处理网络失败和 `408`、`429`、`502`、`503`、
|
|
201
|
+
`504` 等临时状态,并遵守合法的 `Retry-After`。普通写操作、账号授权提交和一次性 secret operation
|
|
202
|
+
不会自动重试。
|
|
203
|
+
|
|
204
|
+
## Cursor 分页
|
|
205
|
+
|
|
206
|
+
`paginateCursor` 会逐项迭代分页数据,并提供最大页数、重复 cursor 检测与 `AbortSignal` 支持:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { paginateCursor } from "@unifyport/sdk-node";
|
|
210
|
+
|
|
211
|
+
const accountId = "acc_xxx";
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
|
|
214
|
+
for await (const contact of paginateCursor(
|
|
215
|
+
async (cursor, pageSignal) => {
|
|
216
|
+
// exactOptionalPropertyTypes 下不把 undefined 写入可选 query 字段。
|
|
217
|
+
const query = cursor === undefined ? { limit: 100 } : { cursor, limit: 100 };
|
|
218
|
+
const result = await device.listContacts(
|
|
219
|
+
{
|
|
220
|
+
params: {
|
|
221
|
+
path: { account_id: accountId },
|
|
222
|
+
query
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
pageSignal === undefined ? {} : { signal: pageSignal }
|
|
226
|
+
);
|
|
227
|
+
return result.data.data;
|
|
228
|
+
},
|
|
229
|
+
{ maxPages: 100, signal: controller.signal }
|
|
230
|
+
)) {
|
|
231
|
+
console.log(contact.id, contact.display_name);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
分页 helper 默认最多读取 10,000 页。业务代码通常应根据自身任务量设置更小的 `maxPages`。
|
|
236
|
+
|
|
237
|
+
## 错误处理
|
|
238
|
+
|
|
239
|
+
所有公开错误都继承 `UnifyPortError`。建议只记录经过筛选的 `status`、`code`、`requestId`、`api` 和
|
|
240
|
+
`operationId`,不要直接序列化完整错误、请求、响应或 header。
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { UnifyPortAbortError, UnifyPortApiError, UnifyPortTimeoutError } from "@unifyport/sdk-node";
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await device.getWorkspace();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof UnifyPortApiError) {
|
|
249
|
+
console.error({
|
|
250
|
+
status: error.status,
|
|
251
|
+
code: error.code,
|
|
252
|
+
requestId: error.requestId,
|
|
253
|
+
operationId: error.operationId
|
|
254
|
+
});
|
|
255
|
+
} else if (error instanceof UnifyPortTimeoutError) {
|
|
256
|
+
console.error({ timeoutMs: error.timeoutMs, operationId: error.operationId });
|
|
257
|
+
} else if (error instanceof UnifyPortAbortError) {
|
|
258
|
+
console.error({ aborted: true, operationId: error.operationId });
|
|
259
|
+
} else {
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
| 错误类型 | 场景 |
|
|
266
|
+
| ----------------------------- | --------------------------------------- |
|
|
267
|
+
| `UnifyPortConfigurationError` | client 配置无效 |
|
|
268
|
+
| `UnifyPortNetworkError` | 网络或自定义 credential provider 失败 |
|
|
269
|
+
| `UnifyPortTimeoutError` | operation 超时 |
|
|
270
|
+
| `UnifyPortAbortError` | 调用方通过 `AbortSignal` 取消 |
|
|
271
|
+
| `UnifyPortApiError` | API 返回非成功 HTTP status |
|
|
272
|
+
| `UnifyPortResponseParseError` | 成功响应无法安全解析 |
|
|
273
|
+
| `UnifyPortPaginationError` | cursor 缺失、重复、超出页数或分页被取消 |
|
|
274
|
+
|
|
275
|
+
## TypeScript 类型
|
|
276
|
+
|
|
277
|
+
根入口同时导出公开契约类型:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import type { DeviceApiComponents, DeviceApiOperations, DeviceApiPaths } from "@unifyport/sdk-node";
|
|
281
|
+
|
|
282
|
+
type Workspace = DeviceApiComponents["schemas"]["Workspace"];
|
|
283
|
+
type SendMessageOperation = DeviceApiOperations["sendMessage"];
|
|
284
|
+
type DevicePaths = DeviceApiPaths;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
为避免 JavaScript 精度丢失,SDK 会把超出安全范围的整数保留为字符串。因此 `uint64`/`int64` 风格
|
|
288
|
+
字段可能是 `number | string`;不要在没有范围保证时无条件调用 `Number(...)`。
|
|
289
|
+
|
|
290
|
+
## 安全注意事项
|
|
291
|
+
|
|
292
|
+
- API key 应从 secret manager 或运行环境提供,不要硬编码或提交到版本控制;
|
|
293
|
+
- 生产环境必须使用 HTTPS;`allowInsecureHttp` 只接受显式开启的 loopback 地址;
|
|
294
|
+
- 不要记录认证 header、授权 code/password/session、消息正文或一次性 secret;
|
|
295
|
+
- 不要从外部输入动态覆盖 `baseUrl`;client 会把凭据限制在初始化时确定的 origin/path;
|
|
296
|
+
- 对写操作自行设计业务幂等和失败恢复,不要把读取 operation 的重试假设套用到写入。
|
|
297
|
+
|
|
298
|
+
更多设计细节见[安全边界](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/security.md)。
|
|
299
|
+
|
|
300
|
+
## 项目与许可证
|
|
301
|
+
|
|
302
|
+
- [GitHub 仓库](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs)
|
|
303
|
+
- [项目 README](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs#readme)
|
|
304
|
+
- [MIT License](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifyport/sdk-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "面向 Node.js 的 UnifyPort Device API 类型安全 SDK。",
|
|
6
6
|
"type": "module",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"sideEffects": false,
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"openapi-fetch": "0.17.0",
|
|
36
|
-
"openapi-typescript-helpers": "0.1.0"
|
|
37
|
-
},
|
|
38
34
|
"scripts": {
|
|
39
35
|
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
40
36
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
41
37
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"openapi-fetch": "0.17.0",
|
|
41
|
+
"openapi-typescript-helpers": "0.1.0"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 UnifyPort
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|