@yanhaidao/wecom 2.3.141 → 2.3.160

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.
@@ -383,11 +383,42 @@ export class WecomDocClient {
383
383
  authLevel?: number;
384
384
  }) {
385
385
  const { agent, docId, viewers, collaborators, removeViewers, removeCollaborators, authLevel } = params;
386
+
387
+ // Auto-detect: if adding collaborators, check if they are already viewers and need to be removed
388
+ // This prevents the "user is viewer but not collaborator" issue
389
+ let finalRemoveViewers = removeViewers;
390
+ if (collaborators && !removeViewers) {
391
+ // Need to check current auth status
392
+ try {
393
+ const currentAuth = await this.getDocAuth({ agent, docId });
394
+ const viewerUserIds = new Set(
395
+ (currentAuth.docMembers || [])
396
+ .filter((m: any) => m.type === 1 && m.userid)
397
+ .map((m: any) => m.userid)
398
+ );
399
+ const newCollaboratorUserIds = normalizeDocMemberEntryList(collaborators)
400
+ .map(e => e.userid)
401
+ .filter(Boolean) as string[];
402
+
403
+ // Auto-add viewers who are being promoted to collaborators
404
+ const autoRemoveViewers = newCollaboratorUserIds
405
+ .filter(userid => viewerUserIds.has(userid))
406
+ .map(userid => ({ userid, type: 1 }));
407
+
408
+ if (autoRemoveViewers.length > 0) {
409
+ finalRemoveViewers = autoRemoveViewers;
410
+ }
411
+ } catch (err) {
412
+ // If we can't check auth, proceed without auto-removal
413
+ // The caller can explicitly pass removeViewers if needed
414
+ }
415
+ }
416
+
386
417
  const payload = buildDocMemberAuthRequest({
387
418
  docId,
388
419
  viewers,
389
420
  collaborators,
390
- removeViewers,
421
+ removeViewers: finalRemoveViewers,
391
422
  removeCollaborators,
392
423
  authLevel,
393
424
  });
@@ -436,16 +467,101 @@ export class WecomDocClient {
436
467
 
437
468
  async createCollect(params: { agent: ResolvedAgentAccount; formInfo: any; spaceId?: string; fatherId?: string }) {
438
469
  const { agent, formInfo, spaceId, fatherId } = params;
470
+
471
+ // Validate form_info structure per API spec
472
+ if (!formInfo || typeof formInfo !== 'object') {
473
+ throw new Error("formInfo 必须是非空对象");
474
+ }
475
+
476
+ // Validate required fields
477
+ if (!formInfo.form_title || readString(formInfo.form_title).length === 0) {
478
+ throw new Error("form_title 必填");
479
+ }
480
+
481
+ if (!formInfo.form_question || !formInfo.form_question.items || !Array.isArray(formInfo.form_question.items)) {
482
+ throw new Error("form_question.items 必填且必须为数组");
483
+ }
484
+
485
+ // Validate questions count ≤ 200
486
+ const questions = formInfo.form_question.items;
487
+ if (questions.length > 200) {
488
+ throw new Error("问题数量不能超过 200 个");
489
+ }
490
+
491
+ // Validate each question
492
+ questions.forEach((q: any, index: number) => {
493
+ if (!q.question_id || !Number.isInteger(q.question_id) || q.question_id < 1) {
494
+ throw new Error(`第${index + 1}个问题:question_id 必填且必须从 1 开始`);
495
+ }
496
+ if (!q.title || readString(q.title).length === 0) {
497
+ throw new Error(`第${index + 1}个问题:title 必填`);
498
+ }
499
+ if (!q.pos || !Number.isInteger(q.pos) || q.pos < 1) {
500
+ throw new Error(`第${index + 1}个问题:pos 必填且必须从 1 开始`);
501
+ }
502
+ if (q.reply_type === undefined || !Number.isInteger(q.reply_type)) {
503
+ throw new Error(`第${index + 1}个问题:reply_type 必填`);
504
+ }
505
+ if (q.must_reply === undefined || typeof q.must_reply !== 'boolean') {
506
+ throw new Error(`第${index + 1}个问题:must_reply 必填且必须为布尔值`);
507
+ }
508
+
509
+ // Validate option_item for single/multiple/dropdown questions
510
+ const requiresOptions = [2, 3, 15].includes(q.reply_type); // 单选/多选/下拉列表
511
+ if (requiresOptions) {
512
+ if (!Array.isArray(q.option_item) || q.option_item.length === 0) {
513
+ throw new Error(`第${index + 1}个问题:单选/多选/下拉列表必须提供 option_item 数组`);
514
+ }
515
+ // Validate option keys are sequential from 1
516
+ q.option_item.forEach((opt: any, optIndex: number) => {
517
+ if (!opt.key || !Number.isInteger(opt.key) || opt.key < 1) {
518
+ throw new Error(`第${index + 1}个问题的第${optIndex + 1}个选项:key 必填且从 1 开始`);
519
+ }
520
+ if (!opt.value || readString(opt.value).length === 0) {
521
+ throw new Error(`第${index + 1}个问题的第${optIndex + 1}个选项:value 必填`);
522
+ }
523
+ });
524
+ }
525
+
526
+ // Validate image/file upload limits
527
+ if ([9, 10].includes(q.reply_type)) { // 图片/文件
528
+ const setting = q.question_extend_setting;
529
+ if (setting) {
530
+ const limit = setting.image_setting?.upload_image_limit || setting.file_setting?.upload_file_limit;
531
+ if (limit) {
532
+ if (limit.count !== undefined && (limit.count < 1 || limit.count > 9)) {
533
+ throw new Error(`第${index + 1}个问题:图片/文件上传数量限制必须在 1-9 之间`);
534
+ }
535
+ if (limit.max_size !== undefined && limit.max_size > 3000) {
536
+ throw new Error(`第${index + 1}个问题:单个文件大小限制最大 3000MB`);
537
+ }
538
+ }
539
+ }
540
+ }
541
+ });
542
+
543
+ // Validate timed_repeat_info and timed_finish are mutually exclusive
544
+ const formSetting = formInfo.form_setting || {};
545
+ if (formSetting.timed_repeat_info?.enable && formSetting.timed_finish) {
546
+ console.warn("警告:timed_finish 与 timed_repeat_info 互斥,若都填优先定时重复");
547
+ }
548
+
549
+ // Build payload
439
550
  const payload: Record<string, unknown> = {
440
- form_info: readObject(formInfo),
551
+ form_info: {
552
+ form_title: readString(formInfo.form_title),
553
+ form_desc: formInfo.form_desc ? readString(formInfo.form_desc) : undefined,
554
+ form_header: formInfo.form_header ? readString(formInfo.form_header) : undefined,
555
+ form_question: formInfo.form_question,
556
+ form_setting: formSetting,
557
+ },
441
558
  };
442
- if (Object.keys(payload.form_info as object).length === 0) {
443
- throw new Error("formInfo required");
444
- }
559
+
445
560
  const normalizedSpaceId = readString(spaceId);
446
561
  const normalizedFatherId = readString(fatherId);
447
562
  if (normalizedSpaceId) payload.spaceid = normalizedSpaceId;
448
563
  if (normalizedFatherId) payload.fatherid = normalizedFatherId;
564
+
449
565
  const json = await this.postWecomDocApi({
450
566
  path: "/cgi-bin/wedoc/create_collect",
451
567
  actionLabel: "create_collect",
@@ -461,16 +577,86 @@ export class WecomDocClient {
461
577
 
462
578
  async modifyCollect(params: { agent: ResolvedAgentAccount; oper: string; formId: string; formInfo: any }) {
463
579
  const { agent, oper, formId, formInfo } = params;
464
- const payload = {
465
- oper: readString(oper),
466
- formid: readString(formId),
467
- form_info: readObject(formInfo),
468
- };
469
- if (!payload.oper) throw new Error("oper required");
470
- if (!payload.formid) throw new Error("formId required");
471
- if (Object.keys(payload.form_info).length === 0) {
472
- throw new Error("formInfo required");
580
+
581
+ // Validate oper parameter
582
+ const operNum = Number(oper);
583
+ if (!operNum || ![1, 2].includes(operNum)) {
584
+ throw new Error("oper 必填且必须为 1 或 2:1=全量修改问题,2=全量修改设置");
473
585
  }
586
+
587
+ const normalizedFormId = readString(formId);
588
+ if (!normalizedFormId) throw new Error("formId required");
589
+
590
+ // Build payload based on oper type
591
+ const payload: Record<string, unknown> = {
592
+ oper: operNum,
593
+ formid: normalizedFormId,
594
+ };
595
+
596
+ if (operNum === 1) {
597
+ // 全量修改问题:必须提供完整的 form_question 数组
598
+ if (!formInfo || !formInfo.form_question || !Array.isArray(formInfo.form_question.items)) {
599
+ throw new Error("oper=1 时,必须提供 form_question.items 数组(包含所有问题,缺失的问题将被删除)");
600
+ }
601
+
602
+ // Validate questions count ≤ 200
603
+ const questions = formInfo.form_question.items;
604
+ if (questions.length > 200) {
605
+ throw new Error("问题数量不能超过 200 个");
606
+ }
607
+
608
+ // Validate each question (same as createCollect)
609
+ questions.forEach((q: any, index: number) => {
610
+ if (!q.question_id || !Number.isInteger(q.question_id) || q.question_id < 1) {
611
+ throw new Error(`第${index + 1}个问题:question_id 必填且必须从 1 开始`);
612
+ }
613
+ if (!q.title || readString(q.title).length === 0) {
614
+ throw new Error(`第${index + 1}个问题:title 必填`);
615
+ }
616
+ if (!q.pos || !Number.isInteger(q.pos) || q.pos < 1) {
617
+ throw new Error(`第${index + 1}个问题:pos 必填且必须从 1 开始`);
618
+ }
619
+ if (q.reply_type === undefined || !Number.isInteger(q.reply_type)) {
620
+ throw new Error(`第${index + 1}个问题:reply_type 必填`);
621
+ }
622
+ if (q.must_reply === undefined || typeof q.must_reply !== 'boolean') {
623
+ throw new Error(`第${index + 1}个问题:must_reply 必填且必须为布尔值`);
624
+ }
625
+
626
+ // Validate option_item for single/multiple/dropdown questions
627
+ const requiresOptions = [2, 3, 15].includes(q.reply_type);
628
+ if (requiresOptions) {
629
+ if (!Array.isArray(q.option_item) || q.option_item.length === 0) {
630
+ throw new Error(`第${index + 1}个问题:单选/多选/下拉列表必须提供 option_item 数组`);
631
+ }
632
+ q.option_item.forEach((opt: any, optIndex: number) => {
633
+ if (!opt.key || !Number.isInteger(opt.key) || opt.key < 1) {
634
+ throw new Error(`第${index + 1}个问题的第${optIndex + 1}个选项:key 必填且从 1 开始`);
635
+ }
636
+ if (!opt.value || readString(opt.value).length === 0) {
637
+ throw new Error(`第${index + 1}个问题的第${optIndex + 1}个选项:value 必填`);
638
+ }
639
+ });
640
+ }
641
+ });
642
+
643
+ payload.form_info = { form_question: formInfo.form_question };
644
+
645
+ } else if (operNum === 2) {
646
+ // 全量修改设置:必须提供完整的 form_setting 对象
647
+ if (!formInfo || !formInfo.form_setting || typeof formInfo.form_setting !== 'object') {
648
+ throw new Error("oper=2 时,必须提供 form_setting 对象(缺失的设置项将被重置为默认值)");
649
+ }
650
+
651
+ // Validate timed_repeat_info and timed_finish are mutually exclusive
652
+ const formSetting = formInfo.form_setting;
653
+ if (formSetting.timed_repeat_info?.enable && formSetting.timed_finish) {
654
+ console.warn("警告:timed_finish 与 timed_repeat_info 互斥,若都填优先定时重复");
655
+ }
656
+
657
+ payload.form_info = { form_setting: formSetting };
658
+ }
659
+
474
660
  const json = await this.postWecomDocApi({
475
661
  path: "/cgi-bin/wedoc/modify_collect",
476
662
  actionLabel: "modify_collect",
@@ -479,9 +665,9 @@ export class WecomDocClient {
479
665
  });
480
666
  return {
481
667
  raw: json,
482
- formId: payload.formid,
483
- oper: payload.oper,
484
- title: readString((payload.form_info as any).form_title),
668
+ formId: payload.formid as string,
669
+ oper: payload.oper as string,
670
+ title: formInfo?.form_title ? readString(formInfo.form_title) : undefined,
485
671
  };
486
672
  }
487
673
 
@@ -571,8 +757,8 @@ export class WecomDocClient {
571
757
  };
572
758
  }
573
759
 
574
- async updateDocContent(params: { agent: ResolvedAgentAccount; docId: string; requests: UpdateRequest[]; version?: number }) {
575
- const { agent, docId, requests, version } = params;
760
+ async updateDocContent(params: { agent: ResolvedAgentAccount; docId: string; requests: UpdateRequest[]; version?: number; batchMode?: boolean }) {
761
+ const { agent, docId, requests, version, batchMode } = params;
576
762
 
577
763
  // Validate requests structure basic check
578
764
  const requestList = readArray(requests);
@@ -655,22 +841,41 @@ export class WecomDocClient {
655
841
 
656
842
  // Build GridData per official API
657
843
  // gridData.rows[i].values[j] must be: {cell_value: {text} | {link: {text, url}}, cell_format?: {...}}
844
+ const rows = (gridData?.rows || []).map((row: any) => ({
845
+ values: (row.values || []).map((cell: any) => {
846
+ // If already CellData format, use as-is
847
+ if (cell && typeof cell === 'object' && cell.cell_value) {
848
+ return cell;
849
+ }
850
+ // Otherwise wrap primitive as CellValue
851
+ return { cell_value: { text: String(cell ?? '') } };
852
+ })
853
+ }));
854
+
855
+ // Validate range limits per API spec
856
+ const rowCount = rows.length;
857
+ const rowWidths = rows.map((row: any) => row.values?.length || 0);
858
+ const columnCount = rowWidths.length > 0 ? Math.max(...rowWidths) : 0;
859
+ const totalCells = rowWidths.reduce((sum: number, width: number) => sum + width, 0);
860
+
861
+ if (rowCount > 1000) {
862
+ throw new Error(`行数不能超过 1000,当前:${rowCount}`);
863
+ }
864
+ if (columnCount > 200) {
865
+ throw new Error(`列数不能超过 200,当前:${columnCount}`);
866
+ }
867
+ if (totalCells > 10000) {
868
+ throw new Error(`单元格总数不能超过 10000,当前:${totalCells}`);
869
+ }
870
+
658
871
  const finalGridData = {
659
872
  start_row: startRow,
660
873
  start_column: startColumn,
661
- rows: (gridData?.rows || []).map((row: any) => ({
662
- values: (row.values || []).map((cell: any) => {
663
- // If already CellData format, use as-is
664
- if (cell && typeof cell === 'object' && cell.cell_value) {
665
- return cell;
666
- }
667
- // Otherwise wrap primitive as CellValue
668
- return { cell_value: { text: String(cell ?? '') } };
669
- })
670
- }))
874
+ rows: rows
671
875
  };
672
876
 
673
877
  // Build batch_update request per official API
878
+ // Note: requests array length ≤ 5 per API spec
674
879
  const body = {
675
880
  docid: normalizedDocId,
676
881
  requests: [{
@@ -686,7 +891,11 @@ export class WecomDocClient {
686
891
  actionLabel: "spreadsheet_batch_update",
687
892
  agent, body,
688
893
  });
689
- return { raw: json, docId: body.docid as string };
894
+ return {
895
+ raw: json,
896
+ docId: body.docid as string,
897
+ updatedCells: json.data?.responses?.[0]?.update_range_response?.updated_cells || 0
898
+ };
690
899
  }
691
900
 
692
901
  /**
@@ -488,10 +488,30 @@ export const wecomDocToolSchema = {
488
488
  },
489
489
  init_content: {
490
490
  type: "array",
491
- description: "可选:初始文档内容(段落列表)。插件会自动处理段落分隔,确保标题和正文分离。",
491
+ description: "可选:初始文档内容(段落列表)。支持纯文本字符串或图片对象。插件会自动处理段落分隔,确保标题和正文分离。",
492
492
  items: {
493
- type: "string",
494
- description: "段落内容",
493
+ oneOf: [
494
+ {
495
+ type: "string",
496
+ description: "段落文本内容",
497
+ },
498
+ {
499
+ type: "object",
500
+ additionalProperties: false,
501
+ required: ["type", "url"],
502
+ properties: {
503
+ type: {
504
+ type: "string",
505
+ const: "image",
506
+ description: "内容类型:image 表示图片",
507
+ },
508
+ url: {
509
+ type: "string",
510
+ description: "图片 URL(支持 http/https 链接)",
511
+ },
512
+ },
513
+ },
514
+ ],
495
515
  },
496
516
  },
497
517
  },
@@ -737,7 +757,7 @@ export const wecomDocToolSchema = {
737
757
  },
738
758
  {
739
759
  type: "object",
740
- additionalProperties: false,
760
+ additionalProperties: true,
741
761
  required: ["action", "docId", "request"],
742
762
  properties: {
743
763
  action: { const: "set_member_auth" },
@@ -746,7 +766,7 @@ export const wecomDocToolSchema = {
746
766
  request: {
747
767
  type: "object",
748
768
  description: "mod_doc_member 请求体。插件会自动补 docid。",
749
- additionalProperties: false,
769
+ additionalProperties: true,
750
770
  properties: {
751
771
  update_file_member_list: docMemberListProperty,
752
772
  del_file_member_list: delDocMemberListProperty