@perk-net/perk-pushplus-sdk 1.2.0 → 1.2.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 CHANGED
@@ -90,7 +90,7 @@ await client.send({
90
90
  template: Template.MARKDOWN,
91
91
  });
92
92
 
93
- // push 表单:template=form 时需传 pushId(表单编码)
93
+ // push 表单 / 文档 / 表格:templateform/doc/excel 时需传 pushId(对应编码)
94
94
  await client.send(
95
95
  sendRequest()
96
96
  .title('表单通知')
@@ -99,6 +99,22 @@ await client.send(
99
99
  .pushId('表单编码')
100
100
  .build(),
101
101
  );
102
+ await client.send(
103
+ sendRequest()
104
+ .title('本周工作同步')
105
+ .content('请查收')
106
+ .template(Template.DOC)
107
+ .pushId('文档编码')
108
+ .build(),
109
+ );
110
+ await client.send(
111
+ sendRequest()
112
+ .title('销售日报')
113
+ .content('请查收')
114
+ .template(Template.EXCEL)
115
+ .pushId('表格编码')
116
+ .build(),
117
+ );
102
118
  ```
103
119
 
104
120
  ### 3. 多渠道发送
@@ -151,10 +167,18 @@ const qr = await client.topic.qrCode(123, 86400, -1);
151
167
 
152
168
  // 群组用户
153
169
  await client.topicUser.editRemark(456, '老张');
170
+ await client.topicUser.addBlacklist(10);
171
+ const topicBlacklist = await client.topicUser.blacklistList({
172
+ current: 1,
173
+ pageSize: 20,
174
+ params: { topicId: 1 },
175
+ });
154
176
 
155
177
  // 好友
156
178
  const myQr = await client.friend.getQrCode({ content: 'welcome' });
157
179
  const friends = await client.friend.list({ current: 1, pageSize: 20 });
180
+ await client.friend.addBlacklist(friends.list[0].friendId);
181
+ const friendBlacklist = await client.friend.blacklistList({ current: 1, pageSize: 20 });
158
182
 
159
183
  // webhook 渠道
160
184
  import { WebhookType } from '@perk-net/perk-pushplus-sdk';
@@ -195,20 +219,38 @@ await client.form.save({
195
219
  });
196
220
  const published = await client.form.publish(form.id!);
197
221
  console.log(published.fillUrl);
222
+ await client.send({
223
+ title: published.title,
224
+ content: '请花1分钟完成填写',
225
+ template: Template.FORM,
226
+ pushId: published.formCode,
227
+ });
198
228
 
199
229
  // push 文档
200
- const doc = await client.doc.create('本周工作同步');
201
- await client.doc.saveContent(doc.docCode!, '<h1>本周工作同步</h1><p>需求评审。</p>');
230
+ import { readFile } from 'node:fs/promises';
231
+ const doc = await client.doc.importWord(await readFile('本周工作同步.docx'), '本周工作同步.docx');
202
232
  await client.doc.updateShare(doc.docCode!, 1, 0);
203
233
  await client.doc.publish(doc.docCode!);
234
+ await client.send({
235
+ title: doc.title,
236
+ content: '请查收',
237
+ template: Template.DOC,
238
+ pushId: doc.docCode,
239
+ });
204
240
 
205
241
  // push 表格
206
- const sheet = await client.excel.create('销售日报');
242
+ const sheet = await client.excel.importExcel(await readFile('销售日报.xlsx'), '销售日报.xlsx');
207
243
  await client.excel.writeCells(sheet.docCode!, 'A1', [
208
244
  ['日期', '销售额'],
209
245
  ['2026-08-13', 12800],
210
246
  ], 'Sheet1');
211
247
  await client.excel.publish(sheet.docCode!);
248
+ await client.send({
249
+ title: sheet.title,
250
+ content: '请查收',
251
+ template: Template.EXCEL,
252
+ pushId: sheet.docCode,
253
+ });
212
254
  ```
213
255
 
214
256
  ### 图片服务
package/dist/index.cjs CHANGED
@@ -375,6 +375,26 @@ var AbstractApi = class {
375
375
  }
376
376
  return parseApiResponse(resp);
377
377
  }
378
+ /**
379
+ * 执行带二进制请求体的请求并返回原始 ApiResponse(不进行 code 校验)。
380
+ * 用于 multipart 上传等场景。
381
+ */
382
+ async executeRaw(method, path, headers, body) {
383
+ const url = this.resolveUrl(path);
384
+ const resp = await callExecuteRaw(this.http, {
385
+ method,
386
+ url,
387
+ headers: headers != null ? headers : void 0,
388
+ body
389
+ });
390
+ if (!isSuccessfulHttpStatus(resp.statusCode)) {
391
+ throw new PushPlusError(
392
+ `PushPlus \u63A5\u53E3 HTTP \u8C03\u7528\u5931\u8D25: status=${resp.statusCode}, body=${resp.body}`,
393
+ resp.statusCode
394
+ );
395
+ }
396
+ return parseApiResponse(resp);
397
+ }
378
398
  /** 执行请求并直接返回 data;非 200 抛出异常。 */
379
399
  async executeForData(method, path, headers, body) {
380
400
  var _a;
@@ -505,6 +525,39 @@ var _OpenAbstractApi = class _OpenAbstractApi extends AbstractApi {
505
525
  (_b = resp.code) != null ? _b : -1
506
526
  );
507
527
  }
528
+ /** 以 multipart 上传文件(自动携带 access-key;code=401 时刷新后重试一次)。 */
529
+ executeOpenMultipart(path, multipart) {
530
+ return this.executeOpenRaw("POST", path, multipart.body, {
531
+ "Content-Type": multipart.contentType
532
+ });
533
+ }
534
+ /**
535
+ * 执行带二进制 body 的开放接口请求;当返回 code=401 时自动刷新 key 并重试一次。
536
+ */
537
+ async executeOpenRaw(method, path, body, extraHeaders) {
538
+ var _a, _b;
539
+ const headers = { ...await this.headersWithAccessKey(), ...extraHeaders != null ? extraHeaders : {} };
540
+ const resp = await this.executeRaw(method, path, headers, body);
541
+ if (isApiSuccess(resp)) {
542
+ return resp.data;
543
+ }
544
+ if (resp.code === _OpenAbstractApi.CODE_ACCESS_KEY_INVALID) {
545
+ this.accessKeyManager.invalidate();
546
+ const retryHeaders = { ...await this.headersWithAccessKey(), ...extraHeaders != null ? extraHeaders : {} };
547
+ const retry = await this.executeRaw(method, path, retryHeaders, body);
548
+ if (isApiSuccess(retry)) {
549
+ return retry.data;
550
+ }
551
+ throw new PushPlusError(
552
+ `PushPlus \u5F00\u653E\u63A5\u53E3\u4E1A\u52A1\u5931\u8D25(\u91CD\u8BD5\u540E): code=${retry.code}, msg=${retry.msg}`,
553
+ (_a = retry.code) != null ? _a : -1
554
+ );
555
+ }
556
+ throw new PushPlusError(
557
+ `PushPlus \u5F00\u653E\u63A5\u53E3\u4E1A\u52A1\u5931\u8D25: code=${resp.code}, msg=${resp.msg}`,
558
+ (_b = resp.code) != null ? _b : -1
559
+ );
560
+ }
508
561
  };
509
562
  _OpenAbstractApi.HEADER_ACCESS_KEY = "access-key";
510
563
  /** PushPlus AccessKey 失效相关的业务码(用于触发自动重试)。 */
@@ -567,6 +620,50 @@ var ClawBotApi = class extends OpenAbstractApi {
567
620
  }
568
621
  };
569
622
 
623
+ // src/multipart.ts
624
+ function buildFileMultipart(fileName, contentType, fileBytes) {
625
+ if (fileBytes == null || fileBytes.byteLength === 0) {
626
+ throw new PushPlusError("\u4E0A\u4F20\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
627
+ }
628
+ const safeName = fileName && fileName.trim() ? fileName : "file";
629
+ const mime = contentType && contentType.trim() ? contentType : "application/octet-stream";
630
+ const boundary = "----PushPlusBoundary" + randomBoundarySuffix();
631
+ const crlf = "\r\n";
632
+ const enc = new TextEncoder();
633
+ const head = enc.encode(
634
+ `--${boundary}${crlf}Content-Disposition: form-data; name="file"; filename="${escapeFileName(safeName)}"${crlf}Content-Type: ${mime}${crlf}${crlf}`
635
+ );
636
+ const tail = enc.encode(`${crlf}--${boundary}--${crlf}`);
637
+ const body = new Uint8Array(head.byteLength + fileBytes.byteLength + tail.byteLength);
638
+ body.set(head, 0);
639
+ body.set(fileBytes, head.byteLength);
640
+ body.set(tail, head.byteLength + fileBytes.byteLength);
641
+ return { contentType: `multipart/form-data; boundary=${boundary}`, body };
642
+ }
643
+ async function toFileBytes(file) {
644
+ if (file instanceof Uint8Array) {
645
+ return file;
646
+ }
647
+ if (file instanceof ArrayBuffer) {
648
+ return new Uint8Array(file);
649
+ }
650
+ if (typeof Blob !== "undefined" && file instanceof Blob) {
651
+ const ab = await file.arrayBuffer();
652
+ return new Uint8Array(ab);
653
+ }
654
+ throw new PushPlusError(`\u4E0D\u652F\u6301\u7684\u4E0A\u4F20\u6587\u4EF6\u7C7B\u578B: ${Object.prototype.toString.call(file)}`);
655
+ }
656
+ function escapeFileName(name) {
657
+ return name.replace(/"/g, "_").replace(/\r/g, " ").replace(/\n/g, " ");
658
+ }
659
+ function randomBoundarySuffix() {
660
+ let s = "";
661
+ for (let i = 0; i < 32; i++) {
662
+ s += Math.floor(Math.random() * 16).toString(16);
663
+ }
664
+ return s;
665
+ }
666
+
570
667
  // src/api/doc-api.ts
571
668
  var DocApi = class extends OpenAbstractApi {
572
669
  constructor(config, http, mgr) {
@@ -584,6 +681,19 @@ var DocApi = class extends OpenAbstractApi {
584
681
  create(title) {
585
682
  return this.executeOpen("POST", "/push/api/open/doc/create", { title });
586
683
  }
684
+ /**
685
+ * 导入 Word(.docx)创建文档。
686
+ *
687
+ * 标题默认取文件名;创建后默认关闭分享,需再调用 publish 才会同步到分享页。
688
+ */
689
+ async importWord(file, fileName = "document.docx") {
690
+ const bytes = await toFileBytes(file);
691
+ const name = fileName && fileName.trim() ? fileName : "document.docx";
692
+ return this.executeOpenMultipart(
693
+ "/push/api/open/doc/import",
694
+ buildFileMultipart(name, guessDocxContentType(name), bytes)
695
+ );
696
+ }
587
697
  /** 获取文档元信息与 HTML 草稿正文。 */
588
698
  content(docCode) {
589
699
  return this.executeOpen(
@@ -625,6 +735,9 @@ var DocApi = class extends OpenAbstractApi {
625
735
  return this.executeOpen("POST", "/push/api/open/doc/updateShare", body);
626
736
  }
627
737
  };
738
+ function guessDocxContentType(name) {
739
+ return name.toLowerCase().endsWith(".docx") ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "application/octet-stream";
740
+ }
628
741
 
629
742
  // src/api/excel-api.ts
630
743
  var ExcelApi = class extends OpenAbstractApi {
@@ -643,6 +756,19 @@ var ExcelApi = class extends OpenAbstractApi {
643
756
  create(title) {
644
757
  return this.executeOpen("POST", "/push/api/open/excel/create", { title });
645
758
  }
759
+ /**
760
+ * 导入 Excel(.xlsx / .xls)创建表格。
761
+ *
762
+ * 标题默认取文件名;创建后默认关闭分享,需再调用 publish 才会同步到分享页。
763
+ */
764
+ async importExcel(file, fileName = "workbook.xlsx") {
765
+ const bytes = await toFileBytes(file);
766
+ const name = fileName && fileName.trim() ? fileName : "workbook.xlsx";
767
+ return this.executeOpenMultipart(
768
+ "/push/api/open/excel/import",
769
+ buildFileMultipart(name, guessExcelContentType(name), bytes)
770
+ );
771
+ }
646
772
  /** 获取表格元信息与整表 JSON 草稿。 */
647
773
  content(docCode) {
648
774
  return this.executeOpen(
@@ -713,6 +839,16 @@ function stringifyJsonContent(content) {
713
839
  throw new PushPlusError(`\u5E8F\u5217\u5316\u8868\u683C\u5185\u5BB9\u5931\u8D25: ${e.message}`, -1, { cause: e });
714
840
  }
715
841
  }
842
+ function guessExcelContentType(name) {
843
+ const lower = name.toLowerCase();
844
+ if (lower.endsWith(".xlsx")) {
845
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
846
+ }
847
+ if (lower.endsWith(".xls")) {
848
+ return "application/vnd.ms-excel";
849
+ }
850
+ return "application/octet-stream";
851
+ }
716
852
 
717
853
  // src/api/form-api.ts
718
854
  var FormApi = class extends OpenAbstractApi {
@@ -808,6 +944,40 @@ var FriendApi = class extends OpenAbstractApi {
808
944
  async editRemark(id, remark) {
809
945
  await this.executeOpen("POST", "/api/open/friend/editRemark", { id, remark });
810
946
  }
947
+ /**
948
+ * 5. 将好友加入黑名单。
949
+ *
950
+ * 加入后将解除双方好友关系,对方无法再添加你。不能将自己加入黑名单,仅可将已有好友加入黑名单。
951
+ *
952
+ * @param friendId 好友 id(好友列表中的 friendId 字段)
953
+ */
954
+ async addBlacklist(friendId) {
955
+ await this.executeOpen(
956
+ "POST",
957
+ this.appendQuery("/api/open/friend/addBlacklist", { friendId })
958
+ );
959
+ }
960
+ /** 6. 好友黑名单列表。 */
961
+ blacklistList(query) {
962
+ return this.executeOpen(
963
+ "POST",
964
+ "/api/open/friend/blacklistList",
965
+ query != null ? query : {}
966
+ );
967
+ }
968
+ /**
969
+ * 7. 解除好友黑名单。
970
+ *
971
+ * 解除后不会自动恢复好友关系,需重新扫码添加。
972
+ *
973
+ * @param id 黑名单记录 ID(黑名单列表中的 id 字段)
974
+ */
975
+ async removeBlacklist(id) {
976
+ await this.executeOpen(
977
+ "POST",
978
+ this.appendQuery("/api/open/friend/removeBlacklist", { id })
979
+ );
980
+ }
811
981
  };
812
982
 
813
983
  // src/api/image-api.ts
@@ -856,7 +1026,7 @@ var ImageApi = class extends OpenAbstractApi {
856
1026
  }
857
1027
  const fileName = options.fileName || "file";
858
1028
  const contentType = options.contentType || guessContentTypeByName(fileName) || "application/octet-stream";
859
- const boundary = "----PushPlusBoundary" + randomBoundarySuffix();
1029
+ const boundary = "----PushPlusBoundary" + randomBoundarySuffix2();
860
1030
  const body = buildMultipartBody(boundary, uploadToken, fileName, contentType, bytes);
861
1031
  const resp = await callExecuteRaw(this.http, {
862
1032
  method: "POST",
@@ -942,7 +1112,7 @@ function buildMultipartBody(boundary, uploadToken, fileName, contentType, fileBy
942
1112
  const crlf = "\r\n";
943
1113
  const enc = new TextEncoder();
944
1114
  const head = enc.encode(
945
- `--${boundary}${crlf}Content-Disposition: form-data; name="token"${crlf}${crlf}${uploadToken}${crlf}--${boundary}${crlf}Content-Disposition: form-data; name="file"; filename="${escapeFileName(fileName)}"${crlf}Content-Type: ${contentType}${crlf}${crlf}`
1115
+ `--${boundary}${crlf}Content-Disposition: form-data; name="token"${crlf}${crlf}${uploadToken}${crlf}--${boundary}${crlf}Content-Disposition: form-data; name="file"; filename="${escapeFileName2(fileName)}"${crlf}Content-Type: ${contentType}${crlf}${crlf}`
946
1116
  );
947
1117
  const tail = enc.encode(`${crlf}--${boundary}--${crlf}`);
948
1118
  const out = new Uint8Array(head.byteLength + fileBytes.byteLength + tail.byteLength);
@@ -951,7 +1121,7 @@ function buildMultipartBody(boundary, uploadToken, fileName, contentType, fileBy
951
1121
  out.set(tail, head.byteLength + fileBytes.byteLength);
952
1122
  return out;
953
1123
  }
954
- function escapeFileName(name) {
1124
+ function escapeFileName2(name) {
955
1125
  return name.replace(/"/g, "_").replace(/\r/g, " ").replace(/\n/g, " ");
956
1126
  }
957
1127
  function guessContentTypeByName(name) {
@@ -964,7 +1134,7 @@ function guessContentTypeByName(name) {
964
1134
  if (lower.endsWith(".svg")) return "image/svg+xml";
965
1135
  return null;
966
1136
  }
967
- function randomBoundarySuffix() {
1137
+ function randomBoundarySuffix2() {
968
1138
  let s = "";
969
1139
  for (let i = 0; i < 32; i++) {
970
1140
  s += Math.floor(Math.random() * 16).toString(16);
@@ -1307,6 +1477,42 @@ var TopicUserApi = class extends OpenAbstractApi {
1307
1477
  async editRemark(id, remark) {
1308
1478
  await this.executeOpen("POST", "/api/open/topicUser/editRemark", { id, remark });
1309
1479
  }
1480
+ /**
1481
+ * 4. 将订阅人加入黑名单。
1482
+ *
1483
+ * 加入后将移出群组,对方无法再加入该群组。积分群组不支持黑名单。不能将自己加入黑名单。
1484
+ *
1485
+ * @param topicRelationId 用户编号(订阅人列表中的 id 字段)
1486
+ */
1487
+ async addBlacklist(topicRelationId) {
1488
+ const path = this.appendQuery("/api/open/topicUser/addBlacklist", { topicRelationId });
1489
+ await this.executeOpen("POST", path);
1490
+ }
1491
+ /**
1492
+ * 5. 订阅人黑名单列表。
1493
+ *
1494
+ * `query.params.topicId` 必填。
1495
+ */
1496
+ blacklistList(query) {
1497
+ return this.executeOpen(
1498
+ "POST",
1499
+ "/api/open/topicUser/blacklistList",
1500
+ query
1501
+ );
1502
+ }
1503
+ /**
1504
+ * 6. 解除订阅人黑名单。
1505
+ *
1506
+ * 解除后不会自动恢复群组订阅,对方可重新加入该群组。
1507
+ *
1508
+ * @param id 黑名单记录 ID(黑名单列表中的 id 字段)
1509
+ */
1510
+ async removeBlacklist(id) {
1511
+ await this.executeOpen(
1512
+ "POST",
1513
+ this.appendQuery("/api/open/topicUser/removeBlacklist", { id })
1514
+ );
1515
+ }
1310
1516
  };
1311
1517
 
1312
1518
  // src/api/user-api.ts
@@ -1371,7 +1577,7 @@ function resolveConfig(input) {
1371
1577
  logRequest: (_g = cfg.logRequest) != null ? _g : false,
1372
1578
  rateLimitGuardEnabled: (_h = cfg.rateLimitGuardEnabled) != null ? _h : true,
1373
1579
  rateLimitCooldownMs: (_i = cfg.rateLimitCooldownMs) != null ? _i : 0,
1374
- userAgent: (_j = cfg.userAgent) != null ? _j : `@perk-net/perk-pushplus-sdk/1.2.0`
1580
+ userAgent: (_j = cfg.userAgent) != null ? _j : `@perk-net/perk-pushplus-sdk/1.2.1`
1375
1581
  };
1376
1582
  }
1377
1583