Node.js App Server SDK
本仓库提供可运行的 App Server SDK:@ni-paas-im/server-sdk(Node.js / ESM / TypeScript 类型声明 / 零运行时依赖)。它同时封装 Open API(230 个 operation,App-Key 签名)与 Admin API(85 个 operation,X-Admin-Secret),接口路径表由 IDL 生成而不是手写。Java / Go / PHP / Python 目前只有「由 OpenAPI 3.1 导出生成」的指引,没有实物。
Node.js:已交付。包 @ni-paas-im/server-sdk,npm test 15 个用例通过,并对真实服务端跑通建应用/注册/发消息/建群/翻译 1021/Admin 推送投递。
Java / Go / PHP / Python:未交付。下方给出 OpenAPI 导出、3.1→3.0 降级与签名补全步骤;生成物不会自动带鉴权。
包未发布 npm registry;仓库内用相对路径或 file: 依赖安装。
交付矩阵
| 语言 / 形态 | 交付物 | 鉴权 | 验证 |
|---|---|---|---|
| Node.js(ESM + d.ts) | sdk/server-node(AppClient / AdminClient) | 内置 SHA-1 签名 + 重试 + 错误映射 | 15 个单测 + 真实服务端冒烟 |
| Java | OpenAPI 生成(指引) | 需自写签名拦截器 | 未构建/未验证 |
| Go | OpenAPI 生成(指引) | 需自写 RoundTripper | 未构建/未验证 |
| PHP | OpenAPI 生成(指引) | 需自写 Guzzle middleware | 未构建/未验证 |
| Python | OpenAPI 生成(指引) | 需自写 requests hook | 未构建/未验证 |
安装与最小示例
# 在仓库内(相对路径)
npm install ../ni-paas-im/sdk/server-node
# 或 package.json
# "dependencies": { "@ni-paas-im/server-sdk": "file:../ni-paas-im/sdk/server-node" }import { AppClient } from '@ni-paas-im/server-sdk';
const app = new AppClient({
appKey: process.env.IM_APP_KEY, // 控制台/Admin API 创建应用后获得
appSecret: process.env.IM_APP_SECRET, // 只放服务端
baseUrl: 'http://127.0.0.1:8080',
});
await app.registerUser({ userId: 'alice', name: 'Alice' });
const { token, expireAt } = await app.issueToken({ userId: 'alice' });
const message = await app.sendPrivateMessage({
fromUserId: 'alice',
toUserId: 'bob',
content: { type: 'text', text: 'hello' },
clientMsgId: 'biz-1',
});
console.log(message.messageId, token.length, expireAt);客户端只拿 Token(JWT,7 天有效)连 WebSocket;App-Secret 永远不下发客户端。Token 的签发接口是 user.token.issue,签名规则见 鉴权与签名。
AppClient:Open API
| 能力 | SDK 方法 | operationId |
|---|---|---|
| 用户注册 / 更新 / 查询 | registerUser / updateUser / getUser / batchGetUsers | user.register / user.update / user.get / user.batch.get |
| Token | issueToken / revokeToken | user.token.issue / user.token.revoke |
| 消息发送 | sendPrivateMessage / sendGroupMessage / sendChatroomMessage / sendSystemMessage | message.*.publish |
| 历史 / 翻译 / 推送 | getHistory / translate / pushSend | message.history.get / message.translate / push.send |
| 群组 / 聊天室 | createGroup / addGroupMembers / removeGroupMembers / dismissGroup / createChatroom / destroyChatroom | group.* / chatroom.* |
| 在线状态 | batchPresence | presence.batch |
| 任意接口 | openApi(operationIdOrPath, body) | 全部 230 个 operation 或绝对路径 |
// 任意 operationId(路径来自 IDL 生成的 operations.mjs)
const history = await app.openApi('message.history.get', {
conversationType: 1, targetId: 'bob', userId: 'alice', limit: 20,
});
// 需要 requestId 时用信封版本
const envelope = await app.requestEnvelope('user.get', { userId: 'alice' });
console.log(envelope.code, envelope.requestId);AdminClient:Admin API
Admin API 用 X-Admin-Secret(环境变量 IM_ADMIN_SECRET,默认 dev-admin)。SDK 封装了应用、用户、群组、聊天室、消息、机器人、推送、翻译等最常用操作,并支持按 operationId 调用全部 85 个接口。
import { AdminClient } from '@ni-paas-im/server-sdk';
const admin = new AdminClient({
baseUrl: 'http://127.0.0.1:8080',
adminSecret: process.env.IM_ADMIN_SECRET,
});
const app = await admin.createApp({ appkey: 'demo2', name: 'Demo 2' }); // appSecret 仅本次返回
await admin.registerUser({ appkey: 'demo2', userId: 'alice', name: 'Alice' });
const users = await admin.listUsers({ appkey: 'demo2', limit: 20 });
await admin.banUser({ appkey: 'demo2', userId: 'alice', banned: true, reason: 'policy' });
// 任意 Admin operation(路径参数自动 URL 编码)
const user = await admin.call('admin.users.get', { pathParams: { appkey: 'demo2', userId: 'alice' } });
console.log(app.appkey, users.total, user.userId);签名、重试与错误映射
签名 = hex_lower(sha1(appSecret + nonce + timestamp)),Nonce 8–64 字符(SDK 用 16 字节随机 hex),Timestamp 为 epoch 毫秒,窗口 ±5 分钟,Nonce 在 5 分钟内不可重放。
| 行为 | 规则 |
|---|---|
| 重试 | 1008 限流 / 1025 配额 / 2xxx 服务端依赖 / 网络错误 / 超时;默认 2 次,指数退避 + jitter,遵守 Retry-After |
| 不重试 | 其余 1xxx(参数/权限/不存在等业务错误),避免重复写入 |
| 重新签名 | 每次重试都生成新 Nonce/Timestamp/Signature,避免 1020 |
| 错误对象 | ImApiError:code / codeName / msg / requestId / httpStatus / retryable / path |
| 错误码表 | 随包提供 43 个错误码的 name/httpStatus/retryable(来自 errors.json) |
import { ImApiError } from '@ni-paas-im/server-sdk';
try {
await app.getUser({ userId: 'ghost' });
} catch (error) {
if (error instanceof ImApiError && error.code === 1004) {
console.error(error.codeName, error.requestId); // USER_NOT_FOUND req_xxx
}
}真实运行结果(本地隔离实例)
以下为 2026-10-27 对 127.0.0.1:18291 的真实冒烟输出(节选):
health: {"status":200}
admin.createApp: {"appkey":"smoke-1793059388884","secretLength":32}
user.register(alice): {"userId":"alice","name":"Alice","banned":false}
user.token.issue(alice): {"userId":"alice","expireAt":1793664188931,"tokenLength":203}
message.private.publish: {"messageId":"m_1793059388933_aaa452563c45451192bede3cf18f1a34","seq":1}
group.create: {"groupId":"smoke-group","memberCount":2}
message.translate (expected 1021): {"code":1021,"codeName":"SERVICE_NOT_ACTIVATED","msg":"translate service not configured (IM_TRANSLATE_ENDPOINT)"}
admin.users.list: {"total":2,"userIds":["alice","bob"]}
admin.push.deliveries.create: {"requested":1,"sent":0,"status":"no_token"}可复现命令:IM_SMOKE_BASE_URL=http://127.0.0.1:18291 IM_SMOKE_ADMIN_SECRET=dev-admin npm run smoke(在 sdk/server-node 下)。翻译 1021 是预期结果:没有配置 IM_TRANSLATE_ENDPOINT。
Java / Go / PHP / Python:由 OpenAPI 3.1 导出生成
每次发布都会重写 /docs/assets/openapi-v1.json(OpenAPI 3.1.0,230 个 path)与 Postman 集合。多数代码生成器仍不支持 3.1,先用工具降级为 3.0:
# 1) 下载当前契约导出物
curl -sS -o openapi-v1.json http://127.0.0.1:8080/docs/assets/openapi-v1.json
# 2) 3.1 → 3.0(openapi-generator 等仍要求 3.0)
npx openapi-down-convert --input openapi-v1.json --output /tmp/im-openapi-3.0.json
# 3) 以 Java 为例
npx @openapitools/openapi-generator-cli generate \
-i /tmp/im-openapi-3.0.json -g java -o /tmp/im-java-sdk \
--additional-properties=library=okhttp-gson,artifactId=im-server-sdk
# Go / PHP / Python 换 -g:go / php / python| 生成物包含 | 生成物不包含(必须自己补) |
|---|---|
| 请求/响应模型、path、HTTP 方法、错误码枚举 | App-Key/Nonce/Timestamp/Signature 请求头 |
| 按 tag 分组的 API 类 | Nonce 防重放(每次请求重新生成) |
| 基础鉴权占位(apiKey) | 1008/2xxx 重试与错误码语义、requestId 记录 |
各语言签名补全(与 Node 实现等价,都输出小写 hex):
| 语言 | 签名代码 |
|---|---|
| Java | HexFormat.of().formatHex(MessageDigest.getInstance("SHA-1").digest((secret + nonce + ts).getBytes(StandardCharsets.UTF_8))) |
| Go | fmt.Sprintf("%x", sha1.Sum([]byte(secret+nonce+ts))) |
| PHP | hash('sha1', $secret.$nonce.$ts) |
| Python | hashlib.sha1((secret+nonce+ts).encode()).hexdigest() |
只靠生成器不会得到能通过鉴权的客户端:必须补签名头、Nonce 生成、时间同步与重试策略。若不想重复实现,Node 服务端可直接使用本 SDK;其它语言建议把签名封装成统一拦截器后再发请求。
维护与测试
| 命令 | 作用 |
|---|---|
npm test | 15 个用例:签名向量、重试、错误映射、Admin、与 IDL 零漂移 |
npm run typecheck | 全局 tsc 5.7.3 严格校验 index.d.ts + ESM 消费者(test/types-smoke.mts) |
npm run sync | IDL 变更后重新生成 operations.mjs(测试会拦截漂移) |
npm run smoke | 对真实 im-server 冒烟(需 IM_SMOKE_BASE_URL / IM_SMOKE_ADMIN_SECRET) |
operations.mjs 由平台契约自动生成;test/operations.test.mjs 直接对比契约,避免 SDK 路径表漂移。
限制
| 项 | 事实 |
|---|---|
| 语言覆盖 | 仅 Node.js 实物;其它语言是生成指引,仓库内没有构建/验证 |
| 接口覆盖 | openApi/call 覆盖全部 operation;便捷方法只覆盖常用子集 |
| 推送 | 无厂商发送;no_token/not_configured 是真实状态,sent 恒 0 |
| 翻译 | 依赖 IM_TRANSLATE_ENDPOINT;未配置返回 1021 |
| TypeScript | 提供 index.d.ts(手写类型声明)+ ESM 源码;已用 tsc 5.7.3 --strict 校验消费者示例,仓库未固化 tsc 依赖/CI |
| 安全 | 浏览器/客户端禁止使用(会暴露 App-Secret / Admin-Secret) |
| npm 发布 | 未发布;使用相对路径或 file: 依赖 |