npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zhumi/ehome-maycur-openapi-sdk

v1.1.3

Published

每刻(Maycur)开放平台 OpenAPI SDK — Node.js / TypeScript 版本,参照 Java 版 ehome-maycur-openapi-sdk 实现

Readme

@zhumi/ehome-maycur-openapi-sdk

每刻(Maycur)开放平台 OpenAPI SDK 的 Node.js / TypeScript 版本,参照 Java 版 ehome-maycur-openapi-sdk 实现,接口、模型与枚举一一对应。

  • 零运行时依赖(使用 Node 18+ 内置 fetch)
  • 完整类型声明,全部接口带中文 JSDoc
  • 同时提供 ESM 与 CommonJS 产物
  • 支持多应用(多 appId)并存、动态注册
  • 内置认证与 20 分钟认证缓存,首次请求自动登录并注入 token
  • 支持每刻的请求体 AES 加密模式

安装

npm install @zhumi/ehome-maycur-openapi-sdk

要求 Node.js >= 18.17。

快速开始

import { MaycurSdk, isMaycurSuccess } from '@zhumi/ehome-maycur-openapi-sdk';

const sdk = new MaycurSdk({
  apps: {
    AP59TWMZD0CMEI: {
      url: 'https://ng-uat.maycur.com',
      appSecret: process.env.MAYCUR_APP_SECRET!,
    },
  },
});

// 首次调用会自动完成「登录 + 注入 token」
const res = await sdk.department.getDepartmentRoot('AP59TWMZD0CMEI');

if (isMaycurSuccess(res)) {
  console.log(res.data);
} else {
  console.error(`调用失败 code=${res.code} message=${res.message}`);
}

CommonJS 同样可用:

const { MaycurSdk, isMaycurSuccess } = require('@zhumi/ehome-maycur-openapi-sdk');

绑定单个应用

不想每次都传 appId 时,用 forApp() 拿到绑定后的门面(类型会去掉第一个 appId 参数):

const app = sdk.forApp('AP59TWMZD0CMEI');

await app.department.getDepartmentRoot();
await app.employee.employeeDetails({ employeeIds: ['E001'], detailedAccountInfo: true });

在浏览器 / Chrome 扩展中使用

SDK 提供双入口,同一套 API:

| 场景 | 导入方式 | 加密实现 | 认证缓存 | | --- | --- | --- | --- | | Node.js | @zhumi/ehome-maycur-openapi-sdk | node:crypto | 文件缓存(默认,兼容 Java) | | 浏览器 / Chrome 扩展 | @zhumi/ehome-maycur-openapi-sdk/browser | WebCrypto + 纯 TS SHA-256/MD5 | 内存缓存 |

用打包器(Vite / webpack / esbuild)构建面向浏览器的产物时,也会通过 exports 的 browser 条件自动选中浏览器入口,因此直接写包名即可:

// Chrome 扩展(MV3)中
import { MaycurSdk, isMaycurSuccess } from '@zhumi/ehome-maycur-openapi-sdk';

const sdk = new MaycurSdk({
  apps: {
    AP59TWMZD0CMEI: {
      url: 'https://ng-uat.maycur.com',
      appSecret: '...', // 浏览器里没有 process.env,建议由配置面板写入 chrome.storage
    },
  },
  authCache: 'memory',
});

// 发票导入(含 OCR 识别)
const res = await sdk.invoice.invoicePicImport('AP59TWMZD0CMEI', {
  fileBase64: base64OfPdf,
  fileType: 'PDF',
  employeeId: 'E001',
});

浏览器入口的差异

  • 不导出依赖 Node 的能力:createFileAuthCache、loadAppsFromFile、 maycurFileFromPath、encryptCbcMode / decryptCbcMode / deriveAesKey(返回 Buffer 的版本)。 等价能力请使用 MaycurFile.fromBlob / MaycurFile.fromBuffer、 deriveAesKeyBytes,认证缓存用内存实现。
  • Chrome 扩展需要声明目标域名的 host_permissions(例如 https://*.maycur.com/*)。
  • MV3 的 extension_pages 无需为 WASM 调整 CSP —— 浏览器入口不使用 WASM。

为什么能跨运行时

| 关注点 | 处理方式 | | --- | --- | | SHA-256 / MD5 | 纯 TypeScript 实现(同步、零依赖),与 node:crypto 逐字节一致 | | AES-CBC | Node 用 node:crypto;浏览器用 WebCrypto,并运行时探测该实现是否自动做 PKCS#7 填充,两种行为都能与 Java 服务端互通 | | Base64 / UTF-8 | 自带实现,不依赖 Buffer / btoa | | 认证缓存 | AuthCache 接口 + 入口注入的工厂(文件 / 内存) | | 文件与流 | MaycurFile 支持 Uint8Array / ArrayBuffer / Blob;流支持 Web ReadableStream 与 Node 可读流 |

npm run build 后可用 npm run test:dist 校验浏览器产物不含任何 node: 内置模块引用:

构建产物检查:
  dist/browser.js 无 Node 内置模块依赖 ✓
  dist/index.js 使用 node:crypto ✓
构建产物检查通过:browser 入口可在 Chrome 扩展中直接打包使用。

配置

构造选项

new MaycurSdk({
  apps: {
    [appId]: {
      url: 'https://ng-uat.maycur.com', // 必填,网关地址
      appSecret: 'xxx',                 // 必填,应用密钥
      needAes: false,                   // 可选,是否启用请求体 AES 加密
      dataSecret: 'xxx',                // 可选,needAes 为 true 时必填
      authCacheFile: './cache-#appCode.json', // 可选,支持 #appCode 占位符
      describe: '测试环境',              // 可选,备注
    },
  },

  autoAuth: true,                       // 默认 true:首次请求自动登录
  authCache: 'file',                    // 'file'(默认,兼容 Java)| 'memory'
  authCacheDir: './.ehome-maycur-cache',// 可选,覆盖缓存文件目录
  authCacheFile: './ehome.maycur.auth_cache-#appCode.json', // 可选,全局模板
  timeoutMs: 30_000,                    // 默认 30s
  uploadFieldName: 'file',              // 默认 file
  logLevel: 'silent',                   // silent(默认)| error | warn | info | debug
  logger: myLogger,                     // 可选,自定义 logger(优先于 logLevel)
  fetch: myFetch,                       // 可选,注入自定义 fetch(测试用)
});

从 JSON 文件加载应用配置

兼容 application.yml 的结构,同时也接受 { apps: ... } 或直接给出 apps 映射:

{
  "ehome": {
    "maycur": {
      "apps": {
        "AP59TWMZD0CMEI": {
          "url": "https://ng-uat.maycur.com",
          "appSecret": "xxx",
          "needAes": false,
          "authCacheFile": "./ehome.maycur.auth_cache-#appCode.json"
        }
      }
    }
  }
}
import { MaycurSdk, loadAppsFromFile } from '@zhumi/ehome-maycur-openapi-sdk';

const sdk = new MaycurSdk({ apps: loadAppsFromFile('./ehome.maycur.json') });

运行时增删应用

sdk.addApp('OTHER_APP_ID', { url: 'https://ng.maycur.com', appSecret: 'xxx' });
sdk.listAppIds();          // ['AP59TWMZD0CMEI', 'OTHER_APP_ID']
sdk.hasClient('OTHER_APP_ID'); // true
sdk.removeApp('OTHER_APP_ID');

自定义日志

const sdk = new MaycurSdk({
  apps: { /* ... */ },
  logger: {
    error: (...a) => myLogger.error(...a),
    warn: (...a) => myLogger.warn(...a),
    info: (...a) => myLogger.info(...a),
    debug: (...a) => myLogger.debug(...a),
  },
});

认证

自动认证(默认)

autoAuth 默认为 true:客户端在每次请求前检查是否持有 token,没有则调用 /api/openapi/auth/login 登录并注入 tokenId / entCode。并发请求会合并为一次登录。

Java 版需要业务侧显式调用 authService.login() 再 maycurClientManager.getClient(appid).refreshToken(...);Node 版默认免去这两步。

手工认证

const login = await sdk.auth.login('AP59TWMZD0CMEI');
sdk.getClient('AP59TWMZD0CMEI').refreshToken(login.data!.tokenId!, login.data!.entCode!);

// 关闭自动认证,完全自行控制
const manual = new MaycurSdk({ apps: { /* ... */ }, autoAuth: false });

// 退出 / 清理缓存
await sdk.auth.logout('AP59TWMZD0CMEI');

签名算法与 Java 版完全一致:sha256(appSecret + ":" + appId + ":" + 毫秒时间戳),小写十六进制。

认证结果按 20 分钟 有效期缓存,默认写入文件(以文件 mtime 判断有效期,与 Java 版相同)。 无文件系统的运行环境(Serverless、只读容器)请使用 authCache: 'memory'。


文件上传

上传使用 multipart/form-data,中文文件名以原始 UTF-8 字节写入 Content-Disposition,与 Java 版 HttpMultipartMode.BROWSER_COMPATIBLE + UTF-8 行为一致, 因此 银行回单.pdf 不会乱码。

import { MaycurFile } from '@zhumi/ehome-maycur-openapi-sdk';

// 从磁盘读取
const file = await MaycurFile.fromPath('./银行回单.pdf');

// 或从内存 / Blob 构造
const fromMemory = MaycurFile.fromBuffer(Buffer.from('%PDF-1.4 ...'), '回单.pdf');
const fromBlob = MaycurFile.fromBlob(new Blob([bytes]), '回单.pdf');

// 上传 -> 拿到 fileKey
const uploaded = await sdk.thirdPaymentWithPaybillMode.uploadReceipt('AP59...', file);

// 用 fileKey 绑定支付流水
await sdk.thirdPaymentWithPaybillMode.associateReceiptToPaymentOrder(
  'AP59...',
  paymentNo,
  uploaded.data!.fileKey!,
  file.fileName,
);

也支持流式上传(必须显式给出文件名):

import { createReadStream } from 'node:fs';

await sdk.thirdPaymentWithPaybillMode.uploadReceipt(
  'AP59...',
  createReadStream('./回单.pdf'),
  '回单.pdf',
);

AES 请求体加密

当应用配置 needAes: true 且提供 dataSecret 时:

  • 请求体加密为 {"encryptedBody":"<Base64>"},并带上 securityType: AES 头
  • 响应若为 {"encryptedBody":"..."} 信封或整体密文,会自动解密
  • 若服务端对某接口返回的是明文 JSON,会按明文处理而不会强行解密

密钥派生与加解密完全对齐 Java 版:

  1. 密钥 = Base64(dataSecret) 的前 16 个 ASCII 字符,不足 16 字节补 0(故实际为 AES-128)
  2. IV = 密钥本身(每刻平台的既有约定,为保持互通未做改动)
  3. 明文 UTF-8 编码,密文标准 Base64,分组填充 PKCS7(等价于 Java 的 PKCS5Padding)

仓库内的 test/aes.test.ts 使用 真实 JVM 运行 Java 算法产出的向量 (tools/codegen/AesVector.java → tools/codegen/aes-vectors.json)做逐字节校验。

安全性提示:IV 固定且等于密钥是每刻开放平台的接口约定,SDK 为保证与 Java 版、 与服务端互通而必须沿用,请勿据此评估其密码学强度。


错误处理

SDK 区分「传输层失败」与「业务失败」:

| 情况 | 行为 | | --- | --- | | HTTP 请求失败 / 响应非 JSON / 响应为空 | 抛 MaycurHttpError(含 status、apiPath、responseText) | | 请求超时 | 抛 MaycurTimeoutError(继承自 MaycurHttpError) | | 登录失败或未拿到 token | 抛 MaycurAuthError | | 配置缺失或非法 | 抛 MaycurConfigError | | 接口返回 success: false | 不抛异常,原样返回响应(与 Java 版只打 warn 的行为一致) |

import { isMaycurSuccess, MaycurHttpError } from '@zhumi/ehome-maycur-openapi-sdk';

try {
  const res = await sdk.department.getDepartmentRoot('AP59TWMZD0CMEI');
  if (!isMaycurSuccess(res)) {
    // 业务失败:res.code / res.message / res.requestId
  }
} catch (error) {
  if (error instanceof MaycurHttpError) {
    console.error(error.status, error.apiPath, error.responseText);
  }
}

isMaycurSuccess() 与 Java 的 MaycurResponse.isSuccess() 语义一致:仅当 success === true 为真。注意部分接口的文档描述 code === '0' 表示成功, 但 Java 版判定的是 success 字段,本 SDK 保持一致。


接口一览

所有服务挂在 sdk 上,方法签名与 Java 版一致,第一个参数为 appId,返回 Promise<MaycurResponse<T>>。共 23 个服务、219 个方法。

| SDK 属性 | Java 服务类 | 方法数 | | --- | --- | --- | | sdk.auth | MaycurAuthService | 认证登录 / 缓存管理 | | sdk.billApproveWebhook | BillApproveWebhookService | 9 | | sdk.cascade | CascadeService | 3 | | sdk.department | DepartmentService | 7 | | sdk.employee | EmployeeService | 10 | | sdk.expense | ExpenseService | 11 | | sdk.formContract | FormContractService | 13 | | sdk.formLoan | FormLoanService | 14 | | sdk.formPayment | FormPaymentService | 6 | | sdk.formPreConsume | FormPreConsumeService | 16 | | sdk.formReimburse | FormReimburseService | 14 | | sdk.formRepayment | FormRepaymentService | 8 | | sdk.formSubType | FormSubTypeService | 1 | | sdk.installment | InstallmentService | 6 | | sdk.invoice | InvoiceService | 7 | | sdk.legalEntity | LegalEntityService | 9 | | sdk.location | LocationService | 1 | | sdk.referenceDataDetail | ReferenceDataDetailService | 7 | | sdk.role | RoleService | 3 | | sdk.thirdPaymentWithoutPaybillMode | ThirdPaymentWithoutPaybillModeService | 9 | | sdk.thirdPaymentWithPaybillMode | ThirdPaymentWithPaybillModeService | 16 | | sdk.tradingPartner | TradingPartnerService | 8 | | sdk.userGroup | UserGroupService | 15 | | sdk.voucher | VoucherService | 25 |

请求/响应模型位于 src/model/**,与 Java 的 model 包逐文件对应, 共 336 个类型(含 33 个枚举),全部带中文 TSDoc,IDE 里可直接看到字段说明。

import type {
  BatchSaveDepartmentRequest,
  DepartmentListSearchRequest,
} from '@zhumi/ehome-maycur-openapi-sdk';

const departments: BatchSaveDepartmentRequest[] = [
  { businessCode: 'D001', name: '研发部', parentBizCode: 'ROOT', enabled: true },
];

const search: DepartmentListSearchRequest = { pageNo: 1, pageSize: 20, keyword: '' };

枚举

Java 枚举在 TypeScript 中按形态映射,取值与 Java 序列化到 JSON 的值一致:

import {
  FormType, FormTypeDescriptions,           // 单参数:取值 = 常量名,额外提供说明文字映射
  DepartmentErrorCode,                      // 双参数(code, description):取值 = code
  VoucherFinanceCodeEnum,                   // 多字段:常量对象注册表
  voucherFinanceCodeEnumFromFinanceCode,    // Java 静态工厂方法对应的查找助手
} from '@zhumi/ehome-maycur-openapi-sdk';

FormType.REIMBURSE;                        // 'REIMBURSE'
FormTypeDescriptions[FormType.REIMBURSE];  // '报销单'
DepartmentErrorCode.SUCCESS;               // 'ACK'
VoucherFinanceCodeEnum.SAP_SAP76;          // { name: 'SAP', financeVersion: 'SAP7.6', ... }
voucherFinanceCodeEnumFromFinanceCode('F2501')?.name; // 'SAP'

Java 的嵌套类型(如 BatchSaveDepartmentRequest.SpecifyDeptAndUser)通过 declare namespace 合并保留同名引用方式:

import type { BatchSaveDepartmentRequest } from '@zhumi/ehome-maycur-openapi-sdk';

const item: BatchSaveDepartmentRequest.SpecifyDeptAndUser = {
  type: 'department',
  bizCode: 'D002',
};

与 Java 版的差异

除语言差异(异步 Promise、构造函数替代 Spring 注入)外,有以下有意的行为差异:

| 项 | Java 版 | 本 SDK | 原因 | | --- | --- | --- | --- | | 认证 | 需业务侧显式 login() + refreshToken() | autoAuth 默认自动完成(可关闭) | 免去样板代码,等价于 Java 版声明但未实现的 AOP 切面意图 | | GET 查询参数 | 循环中误用 fieldNames().next(),只有第一个字段生效 | 拼接全部字段 | 修复 Java 版的缺陷 | | 空请求体 | POST 时发送字面量 null | 不发送请求体 | 更符合 HTTP 语义,服务端行为一致 | | 上传来源 | java.io.File / InputStream | MaycurFile / 可读流 | Node 无对应类型 || 响应解密字符集 | new String(bytes) 使用平台默认字符集(Windows 上为 GBK,中文会乱码) | 固定 UTF-8 | 修复 Java 版在非 UTF-8 平台的缺陷 | | 认证缓存 | 仅文件缓存 | 文件缓存(默认)+ 内存缓存 | 适配 Serverless / 只读文件系统 | | 服务层日志 | 每个方法 log.info(...) | 统一在客户端按 logLevel 输出请求/响应 | 避免生成代码里堆积 Java 风格字符串拼接 |

已知上游问题(本 SDK 保持与 Java 版一致,未擅自修改)

ExpenseService.expenseThirdPartyQuery 的接口常量在 Java 版中是 /openapi/expense/thirdParty/%s,缺少 /api 前缀(同文件其它常量均为 /api/openapi/...)。 本 SDK 按原样保留以避免与 Java 版行为分叉。若确认是笔误,请同时修正 Java 版与本 SDK 的生成源(tools/codegen)。


代码生成与维护

src/model/** 与 src/service/** 由 Java 源码自动生成,请勿手工修改。 生成器位于 tools/codegen,从 Java 版 SDK 的源码解析出中间表示后产出 TypeScript。

# 重新生成(默认读取 ../ehome-maycur-openapi-sdk 的源码)
npm run gen

# 校验生成结果与 Java 源码一致(动词 / 路径 / 参数个数)
npm run verify

# 类型检查 / 测试 / 构建
npm run typecheck
npm test
npm run build

tools/codegen 各文件职责:

| 文件 | 作用 | | --- | --- | | cst.mjs | java-parser CST 遍历工具、顶层逗号切分 | | java-ir.mjs | Java 源码 → 中间表示(类/枚举/字段/方法/嵌套类型) | | types.mjs | Java 类型 → TypeScript 类型映射 | | tsdoc.mjs | Javadoc → TSDoc | | emit-model.mjs | 中间表示 → interface / enum 模块 | | emit-service.mjs | 中间表示 → 服务类模块 | | overrides.mjs | 少量无法自动翻译的方法(Java 重载、便捷方法、上传)的手工实现 | | index.mjs | 生成入口 | | verify.mjs | 独立校验脚本 | | AesVector.java | 在真实 JVM 上产出 AES 互通测试向量 |

生成结果覆盖情况(npm run gen 会输出):

model 文件 : 311
service 文件: 23
顶层类型   : 336(其中 enum 33)
嵌套类型   : 331
字段       : 3819
服务方法   : 219

开发与测试

npm install
npm run gen          # 生成代码
npm run verify       # 与 Java 源码交叉校验
npm run typecheck    # tsc --noEmit(覆盖 src / test / examples)
npm test             # vitest:单元测试 + mock 服务器端到端测试
npm run build        # 产出 ESM + CJS + .d.ts

测试分层:

  • test/aes.test.ts — AES 与 真实 JVM 产出的向量 逐字节互通校验
  • test/config.test.ts — 配置解析、#appCode 占位符、AES 配置校验、JSON 加载
  • test/auth-cache.test.ts — 文件缓存 TTL(mtime 语义)与内存缓存
  • test/client.test.ts — 请求头、GET 查询串、AES 请求/响应、超时、错误映射、multipart 中文文件名
  • test/models.test.ts — 枚举各形态、嵌套类型命名空间、泛型模型、字段类型映射
  • test/e2e.test.ts — 本地 mock HTTP 服务器跑通签名、自动登录、token 注入、AES 全链路、上传与错误传播

示例位于仓库的 examples/ 目录,与 Java 版 Demo 一一对应(该目录不随 npm 包发布):

| 示例 | Java Demo | | --- | --- | | examples/01-auth.ts | BaseDemo | | examples/02-department.ts | DepartmentDemo | | examples/03-employee-account.ts | EmployeeAccountDemo | | examples/04-employer.ts | EmployerDemo | | examples/05-legal-entity.ts | LegalEngityDemo | | examples/06-preconsume-import.ts | PreConsumeImportDemo | | examples/07-receipt-bind.ts | ReceiptBindDemo | | examples/08-reference-data-detail.ts | ReferenceDataDetailDemo | | examples/09-reimburse-invoice.ts | ReimburseInvoiceDemo | | examples/10-third-payment.ts | ThirdPaymentDemo | | examples/11-trading-partner.ts | TradingPartnerDemo | | examples/12-voucher.ts | VoucherDemo |

运行方式(需要先设置 MAYCUR_BASE_URL / MAYCUR_APP_SECRET 等环境变量,详见 examples/README.md):

npm run example examples/02-department.ts

发布

发布到 npm 的只有编译产物 dist/(Node 与浏览器两个入口,均为 ESM + CJS + 类型声明), 源码、示例与代码生成器保留在仓库中,不随包发布。

npm run clean && npm run gen && npm run verify && npm run typecheck && npm test && npm run build && npm run test:dist
npm publish

prepublishOnly 已把上述步骤串起来。首次发布到 npmjs 需要指定 scope 的访问权限:

npm publish --access public

发布后建议用 npm pack 安装到临时工程验证三种入口都能工作:

npm pack
# 在临时工程中
npm install ./zhumi-ehome-maycur-openapi-sdk-1.0.0.tgz
node -e "console.log(Object.keys(require('@zhumi/ehome-maycur-openapi-sdk')).length)"   # CJS
node --input-type=module -e "import('@zhumi/ehome-maycur-openapi-sdk').then(m=>console.log(typeof m.MaycurSdk))"  # ESM

许可

MIT