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 个单测 + 真实服务端冒烟
JavaOpenAPI 生成(指引)需自写签名拦截器未构建/未验证
GoOpenAPI 生成(指引)需自写 RoundTripper未构建/未验证
PHPOpenAPI 生成(指引)需自写 Guzzle middleware未构建/未验证
PythonOpenAPI 生成(指引)需自写 requests hook未构建/未验证

安装与最小示例​

Shell
# 在仓库内(相对路径)
npm install ../ni-paas-im/sdk/server-node

# 或 package.json
# "dependencies": { "@ni-paas-im/server-sdk": "file:../ni-paas-im/sdk/server-node" }
JavaScript
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 / batchGetUsersuser.register / user.update / user.get / user.batch.get
TokenissueToken / revokeTokenuser.token.issue / user.token.revoke
消息发送sendPrivateMessage / sendGroupMessage / sendChatroomMessage / sendSystemMessagemessage.*.publish
历史 / 翻译 / 推送getHistory / translate / pushSendmessage.history.get / message.translate / push.send
群组 / 聊天室createGroup / addGroupMembers / removeGroupMembers / dismissGroup / createChatroom / destroyChatroomgroup.* / chatroom.*
在线状态batchPresencepresence.batch
任意接口openApi(operationIdOrPath, body)全部 230 个 operation 或绝对路径
JavaScript
// 任意 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 个接口。

JavaScript
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)
JavaScript
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 的真实冒烟输出(节选):

Text
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:

Shell
# 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):

语言签名代码
JavaHexFormat.of().formatHex(MessageDigest.getInstance("SHA-1").digest((secret + nonce + ts).getBytes(StandardCharsets.UTF_8)))
Gofmt.Sprintf("%x", sha1.Sum([]byte(secret+nonce+ts)))
PHPhash('sha1', $secret.$nonce.$ts)
Pythonhashlib.sha1((secret+nonce+ts).encode()).hexdigest()
生成 ≠ 可用

只靠生成器不会得到能通过鉴权的客户端:必须补签名头、Nonce 生成、时间同步与重试策略。若不想重复实现,Node 服务端可直接使用本 SDK;其它语言建议把签名封装成统一拦截器后再发请求。

维护与测试​

命令作用
npm test15 个用例:签名向量、重试、错误映射、Admin、与 IDL 零漂移
npm run typecheck全局 tsc 5.7.3 严格校验 index.d.ts + ESM 消费者(test/types-smoke.mts)
npm run syncIDL 变更后重新生成 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: 依赖

相关页面​