@yanhaidao/wecom 2.3.14 → 2.3.150

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.
@@ -20,7 +20,7 @@ function mapDocTypeLabel(docType: number): string {
20
20
  function summarizeDocInfo(info: any = {}) {
21
21
  const docName = readString(info.doc_name) || "未命名文档";
22
22
  const docType = mapDocTypeLabel(Number(info.doc_type));
23
- return `${docType}“${docName}”信息已获取`;
23
+ return `${docType}"${docName}"信息已获取`;
24
24
  }
25
25
 
26
26
  function summarizeDocAuth(result: any = {}) {
@@ -77,7 +77,7 @@ function buildDocAuthDiagnosis(result: any = {}, requesterSenderId = "") {
77
77
  ];
78
78
  const recommendations: string[] = [];
79
79
  if (likelyAnonymousLinkFailure) {
80
- recommendations.push("当前更像是仅企业内可访问;匿名浏览器或未登录企业微信环境通常会显示“文档不存在”。");
80
+ recommendations.push('当前更像是仅企业内可访问;匿名浏览器或未登录企业微信环境通常会显示"文档不存在"。');
81
81
  }
82
82
  if (requester) {
83
83
  if (requesterIsCollaborator) {
@@ -176,7 +176,7 @@ function buildShareLinkDiagnosis(params: { shareUrl: string; finalUrl: string; s
176
176
  ];
177
177
  const recommendations: string[] = [];
178
178
  if (likelyUnavailableToGuest) {
179
- recommendations.push("当前链接对 guest/未登录企业微信环境返回 blankpage,外部访问会表现为打不开或像“文档不存在”。");
179
+ recommendations.push('当前链接对 guest/未登录企业微信环境返回 blankpage,外部访问会表现为打不开或像"文档不存在"。');
180
180
  }
181
181
  if (shareCode) {
182
182
  recommendations.push(`当前链接带有分享码 scode=${shareCode}。如分享码过期或未生效,外部访问会失败。`);
@@ -269,7 +269,7 @@ function summarizeDocAccess(result: any = {}) {
269
269
 
270
270
  function summarizeFormInfo(result: any = {}) {
271
271
  const title = readString(result.formInfo?.form_title) || "未命名收集表";
272
- return `收集表“${title}”信息已获取`;
272
+ return `收集表"${title}"信息已获取`;
273
273
  }
274
274
 
275
275
  function summarizeFormAnswer(result: any = {}) {
@@ -346,7 +346,7 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
346
346
  const action = params.action;
347
347
  switch (action) {
348
348
  case "create": {
349
- const collaborators = resolveCreateCollaborators({ toolContext, requestParams: params });
349
+ const explicitCollaborators = Array.isArray(params.collaborators) ? [...params.collaborators] : [];
350
350
  const result = await docClient.createDoc({
351
351
  agent: account,
352
352
  docName: params.docName,
@@ -356,122 +356,215 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
356
356
  adminUsers: params.adminUsers,
357
357
  });
358
358
 
359
+ // Auto-set security rules for better default permissions (internal users can edit)
360
+ try {
361
+ await docClient.setDocJoinRule({
362
+ agent: account,
363
+ docId: result.docId,
364
+ request: {
365
+ enable_corp_internal: true,
366
+ corp_internal_auth: 2, // 2 = edit permission
367
+ enable_corp_external: false,
368
+ ban_share_external: false,
369
+ },
370
+ });
371
+ } catch (err) {
372
+ // Non-fatal: document created, just default permissions may be read-only
373
+ }
374
+
359
375
  // Handle initial content (title/body separation) if provided
376
+ // Supports: string (text) or {type: "text"|"image", content/url: string}
360
377
  let contentResult: any = null;
361
378
  if (Array.isArray(params.init_content) && params.init_content.length > 0) {
362
379
  try {
363
- // 1. Get initial content to find paragraph boundaries
364
- const initContent = await docClient.getDocContent({
365
- agent: account,
366
- docId: result.docId,
367
- });
368
-
369
- // We assume a new doc has 1 empty paragraph.
370
- // We will insert content sequentially.
371
- // Note: WeCom API indices shift after insertion.
372
- // Strategy:
373
- // - Insert Para 1 (Title) at 0.
374
- // - Insert Paragraph Break (creates new para).
375
- // - Insert Para 2 (Content) at new index.
376
- // To be safe and follow "Correct Flow", we will do it in a loop or calculate carefully.
377
- // Since batch_update is atomic, indices are relative to start of batch? NO, usually sequential in batch.
378
- // But user says "Must call get_content".
379
- // So we will do it step-by-step for safety as per user instruction.
380
-
381
- let currentContent = initContent;
382
- let requests: UpdateRequest[] = [];
383
-
384
- // If we have content, we treat the first item as "Title" (or first paragraph)
385
- // The doc starts with one empty paragraph.
386
-
387
- // Step 1: Insert first paragraph text at index 0
380
+ // Helper: check if content item is an image
381
+ const isImageItem = (item: any): boolean => {
382
+ if (typeof item === "object" && item !== null) {
383
+ return item.type === "image" || (item.url && !item.content);
384
+ }
385
+ if (typeof item === "string") {
386
+ // Detect image URLs
387
+ return item.startsWith("http") &&
388
+ (item.includes(".png") || item.includes(".jpg") ||
389
+ item.includes(".jpeg") || item.includes(".gif") ||
390
+ item.includes("qpic.cn") || item.includes("weixin.qq.com"));
391
+ }
392
+ return false;
393
+ };
394
+
395
+ // Helper: get image URL from content item
396
+ const getImageUrl = (item: any): string => {
397
+ if (typeof item === "object" && item !== null) {
398
+ return item.url || item.content || "";
399
+ }
400
+ return String(item);
401
+ };
402
+
403
+ // Helper: download image and convert to base64
404
+ const downloadImageAsBase64 = async (url: string): Promise<string> => {
405
+ const response = await fetch(url);
406
+ if (!response.ok) {
407
+ throw new Error(`Failed to download image: ${url}`);
408
+ }
409
+ const arrayBuffer = await response.arrayBuffer();
410
+ return Buffer.from(arrayBuffer).toString("base64");
411
+ };
412
+
413
+ // Helper: get text from content item
414
+ const getText = (item: any): string => {
415
+ if (typeof item === "object" && item !== null) {
416
+ return item.content || item.text || "";
417
+ }
418
+ return String(item);
419
+ };
420
+
421
+ // Step 1: Insert first paragraph (title) at index 0
388
422
  if (params.init_content[0]) {
389
- const titleText = String(params.init_content[0]);
390
- await docClient.updateDocContent({
391
- agent: account,
392
- docId: result.docId,
393
- requests: [{
394
- insert_text: {
395
- text: titleText,
396
- location: { index: 0 }
397
- }
398
- }]
399
- });
423
+ const firstItem = params.init_content[0];
424
+ if (isImageItem(firstItem)) {
425
+ // First item is image - upload first, then insert at index 0
426
+ const imgUrl = getImageUrl(firstItem);
427
+
428
+ try {
429
+ // Upload image to WeCom to get proper image_id
430
+ const base64 = await downloadImageAsBase64(imgUrl);
431
+ const uploadResult = await docClient.uploadDocImage({
432
+ agent: account,
433
+ docId: result.docId,
434
+ base64_content: base64,
435
+ });
400
436
 
401
- // Apply Title Styling (Bold)
402
- // We assume the title is at the start (0) and has the length of the text.
403
- if (titleText.length > 0) {
437
+ // Insert image using uploaded URL
438
+ // Note: version is optional, API handles concurrency
439
+ await docClient.updateDocContent({
440
+ agent: account,
441
+ docId: result.docId,
442
+ requests: [{
443
+ insert_image: {
444
+ image_id: uploadResult.url,
445
+ location: { index: 0 },
446
+ width: uploadResult.width,
447
+ height: uploadResult.height
448
+ }
449
+ }]
450
+ });
451
+ } catch (uploadErr) {
452
+ console.error(`Failed to upload first image ${imgUrl}:`, uploadErr);
453
+ throw new Error(`First image upload failed: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
454
+ }
455
+ } else {
456
+ const titleText = getText(firstItem);
404
457
  await docClient.updateDocContent({
405
458
  agent: account,
406
459
  docId: result.docId,
407
460
  requests: [{
408
- update_text_property: {
409
- text_property: { bold: true },
410
- ranges: [{ start_index: 0, length: titleText.length }]
461
+ insert_text: {
462
+ text: titleText,
463
+ location: { index: 0 }
411
464
  }
412
465
  }]
413
466
  });
467
+
468
+ // Apply Title Styling (Bold)
469
+ if (titleText.length > 0) {
470
+ await docClient.updateDocContent({
471
+ agent: account,
472
+ docId: result.docId,
473
+ requests: [{
474
+ update_text_property: {
475
+ text_property: { bold: true },
476
+ ranges: [{ start_index: 0, length: titleText.length }]
477
+ }
478
+ }]
479
+ });
480
+ }
414
481
  }
415
482
  }
416
483
 
417
- // Step 2: For subsequent paragraphs, we need to append.
484
+ // Step 2: For subsequent items, append with proper paragraph handling
485
+ // Per API spec: must get latest version and index before each batch_update
418
486
  for (let i = 1; i < params.init_content.length; i++) {
419
- const text = String(params.init_content[i]);
420
- if (!text) continue;
487
+ const item = params.init_content[i];
421
488
 
422
- // Refresh content to get latest end position
423
- currentContent = await docClient.getDocContent({
424
- agent: account,
425
- docId: result.docId,
426
- });
427
-
428
- // Find the end of the document (or last paragraph)
429
- // We use 'end' directly as the insertion point for appending.
430
- // Note: WeCom 'end' is exclusive [begin, end).
431
- // If we insert at 'end', we append after the last element.
432
- let docEndIndex = currentContent.document.end;
433
-
434
- // Safety adjustment: If the document has a final mandatory newline/EOF that we can't append after,
435
- // we might need to insert *before* it.
436
- // However, creating a NEW paragraph usually happens at the end.
437
- // If we are unsure, we try 'end - 1' if 'end' fails, but 'end' is the standard "append" index.
438
- // Given the user analysis "Paragraph 2 (5-117)" where 5 was the end of Para 1,
439
- // it suggests we insert AT the boundary.
440
-
441
- // We use insert_paragraph to create a split
442
- await docClient.updateDocContent({
443
- agent: account,
444
- docId: result.docId,
445
- requests: [{
446
- insert_paragraph: {
447
- location: { index: docEndIndex }
448
- }
449
- }]
450
- });
451
-
452
- // Now insert text into the new paragraph
453
- // We need to refresh again or assume index shifted by 1
454
- currentContent = await docClient.getDocContent({
455
- agent: account,
456
- docId: result.docId,
457
- });
458
-
459
- // The new paragraph should be at the end.
460
- // We want to insert text *into* this new paragraph.
461
- // The insert_paragraph likely created a new Paragraph node.
462
- // We insert at the new end (which is inside the new paragraph).
463
- const newParaIndex = currentContent.document.end;
464
-
465
- await docClient.updateDocContent({
489
+ // Refresh content to get latest document structure and version
490
+ // API requires: version difference ≤ 100 from latest
491
+ const currentContent = await docClient.getDocContent({
466
492
  agent: account,
467
493
  docId: result.docId,
468
- requests: [{
469
- insert_text: {
470
- text: text,
471
- location: { index: newParaIndex }
472
- }
473
- }]
474
494
  });
495
+
496
+ // Get the end index of the document
497
+ const docEndIndex = currentContent.document.end;
498
+ const currentVersion = currentContent.version;
499
+
500
+ if (isImageItem(item)) {
501
+ // Insert image: upload first, then create paragraph, then insert image
502
+ const imgUrl = getImageUrl(item);
503
+
504
+ try {
505
+ // Step 1: Download and upload image to WeCom
506
+ const base64 = await downloadImageAsBase64(imgUrl);
507
+ const uploadResult = await docClient.uploadDocImage({
508
+ agent: account,
509
+ docId: result.docId,
510
+ base64_content: base64,
511
+ });
512
+
513
+ // Step 2: Create new paragraph and insert image in one batch (2 operations ≤ 30)
514
+ // Per API spec: all indices are based on the same document snapshot
515
+ // insert_paragraph at docEndIndex creates a new paragraph
516
+ // insert_image at docEndIndex + 1 inserts into the newly created paragraph
517
+ await docClient.updateDocContent({
518
+ agent: account,
519
+ docId: result.docId,
520
+ version: currentVersion, // Pass version for concurrency control
521
+ requests: [
522
+ {
523
+ insert_paragraph: {
524
+ location: { index: docEndIndex }
525
+ }
526
+ },
527
+ {
528
+ insert_image: {
529
+ image_id: uploadResult.url,
530
+ location: { index: docEndIndex + 1 },
531
+ width: uploadResult.width,
532
+ height: uploadResult.height
533
+ }
534
+ }
535
+ ]
536
+ });
537
+ } catch (uploadErr) {
538
+ console.error(`Failed to upload image ${imgUrl}:`, uploadErr);
539
+ throw new Error(`Image upload failed: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
540
+ }
541
+ } else {
542
+ const text = getText(item);
543
+ if (!text) continue;
544
+
545
+ // Insert text: create paragraph and insert text in one batch (2 operations ≤ 30)
546
+ // Per API spec: all indices are based on the same document snapshot
547
+ // insert_paragraph at docEndIndex creates a new paragraph
548
+ // insert_text at docEndIndex + 1 inserts into the newly created paragraph
549
+ await docClient.updateDocContent({
550
+ agent: account,
551
+ docId: result.docId,
552
+ version: currentVersion, // Pass version for concurrency control
553
+ requests: [
554
+ {
555
+ insert_paragraph: {
556
+ location: { index: docEndIndex }
557
+ }
558
+ },
559
+ {
560
+ insert_text: {
561
+ text: text,
562
+ location: { index: docEndIndex + 1 }
563
+ }
564
+ }
565
+ ]
566
+ });
567
+ }
475
568
  }
476
569
  contentResult = "init_content_populated";
477
570
  } catch (err) {
@@ -480,13 +573,13 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
480
573
  }
481
574
 
482
575
  let accessResult: any = null;
483
- if ((Array.isArray(params.viewers) && params.viewers.length > 0) || collaborators.length > 0) {
576
+ if ((Array.isArray(params.viewers) && params.viewers.length > 0) || explicitCollaborators.length > 0) {
484
577
  try {
485
578
  accessResult = await docClient.grantDocAccess({
486
579
  agent: account,
487
580
  docId: result.docId,
488
581
  viewers: params.viewers,
489
- collaborators,
582
+ collaborators: explicitCollaborators,
490
583
  });
491
584
  } catch (err) {
492
585
  return buildToolResult({
@@ -499,7 +592,7 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
499
592
  docId: result.docId,
500
593
  title: readString(params.docName),
501
594
  url: result.url || undefined,
502
- summary: `已创建${mapDocTypeLabel(result.docType)}“${readString(params.docName)}”(docId: ${result.docId}),但权限授予失败`,
595
+ summary: `已创建${mapDocTypeLabel(result.docType)}"${readString(params.docName)}"(docId: ${result.docId}),但权限授予失败`,
503
596
  usageHint: buildDocIdUsageHint(result.docId) || undefined,
504
597
  error: err instanceof Error ? err.message : String(err),
505
598
  raw: { create: result.raw },
@@ -516,8 +609,8 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
516
609
  title: readString(params.docName),
517
610
  url: result.url || undefined,
518
611
  summary: accessResult
519
- ? `已创建${mapDocTypeLabel(result.docType)}“${readString(params.docName)}”(docId: ${result.docId});${summarizeDocAccess(accessResult)}` + (contentResult ? `;内容填充: ${contentResult}` : "")
520
- : `已创建${mapDocTypeLabel(result.docType)}“${readString(params.docName)}”(docId: ${result.docId})` + (contentResult ? `;内容填充: ${contentResult}` : ""),
612
+ ? `已创建${mapDocTypeLabel(result.docType)}"${readString(params.docName)}"(docId: ${result.docId});${summarizeDocAccess(accessResult)}` + (contentResult ? `;内容填充: ${contentResult}` : "")
613
+ : `已创建${mapDocTypeLabel(result.docType)}"${readString(params.docName)}"(docId: ${result.docId})` + (contentResult ? `;内容填充: ${contentResult}` : ""),
521
614
  usageHint: buildDocIdUsageHint(result.docId) || undefined,
522
615
  raw: accessResult ? { create: result.raw, access: accessResult.raw } : result.raw,
523
616
  });
@@ -534,7 +627,7 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
534
627
  accountId: account.accountId,
535
628
  docId: result.docId,
536
629
  title: result.newName,
537
- summary: `文档已重命名为“${result.newName}”`,
630
+ summary: `文档已重命名为"${result.newName}"`,
538
631
  raw: result.raw,
539
632
  });
540
633
  }
@@ -733,18 +826,22 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
733
826
  });
734
827
  }
735
828
  case "update_content": {
829
+ const batchMode = params.batchMode === true;
830
+
736
831
  const result = await docClient.updateDocContent({
737
832
  agent: account,
738
833
  docId: params.docId,
739
834
  requests: params.requests,
740
835
  version: params.version,
836
+ batchMode: batchMode,
741
837
  });
838
+
742
839
  return buildToolResult({
743
840
  ok: true,
744
841
  action: "update_content",
745
842
  accountId: account.accountId,
746
843
  docId: params.docId,
747
- summary: "文档内容已更新",
844
+ summary: `文档内容已更新(${batchMode ? '批量' : '顺序'}模式)`,
748
845
  raw: result.raw,
749
846
  });
750
847
  }
@@ -811,22 +908,47 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
811
908
  });
812
909
  }
813
910
  case "create_collect": {
814
- const result = await docClient.createCollect({
815
- agent: account,
816
- formInfo: params.formInfo,
817
- spaceId: params.spaceId,
818
- fatherId: params.fatherId,
819
- });
820
- const title = readString(result.title);
821
- return buildToolResult({
822
- ok: true,
823
- action: "create_collect",
824
- accountId: account.accountId,
825
- formId: result.formId,
826
- title: title || undefined,
827
- summary: title ? `已创建收集表“${title}”` : "收集表已创建",
828
- raw: result.raw,
829
- });
911
+ // 创建收集表(表单)
912
+ // 参考 API 规范文档:E8_AF_B7_E4_B8_A5_E6_A0_BC_E6_8C_89_E7_85_A7_E4_BB_A5_E4_B8_---099c30ec-70bd-4e5b-ae03-212de0226a25.docx
913
+ try {
914
+ const result = await docClient.createCollect({
915
+ agent: account,
916
+ formInfo: params.formInfo,
917
+ spaceId: params.spaceId,
918
+ fatherId: params.fatherId,
919
+ });
920
+ const title = readString(result.title);
921
+ return buildToolResult({
922
+ ok: true,
923
+ action: "create_collect",
924
+ accountId: account.accountId,
925
+ formId: result.formId,
926
+ title: title || undefined,
927
+ summary: title ? `已创建收集表"${title}"(formId: ${result.formId})` : `已创建收集表(formId: ${result.formId})`,
928
+ raw: result.raw,
929
+ });
930
+ } catch (err) {
931
+ // 提供更详细的错误提示
932
+ const errorMsg = err instanceof Error ? err.message : String(err);
933
+ const hint = `
934
+ 创建收集表失败。请检查以下必填项:
935
+ - form_title: 收集表标题(必填)
936
+ - form_question.items: 问题数组(必填,≤200 个)
937
+ - 每个问题必须包含:question_id, title, pos, reply_type, must_reply
938
+ - 单选/多选/下拉列表必须提供 option_item 数组
939
+ - reply_type 对照表:1 文本,2 单选,3 多选,5 位置,9 图片,10 文件,11 日期,14 时间,15 下拉列表,16 体温,17 签名,18 部门,19 成员,22 时长
940
+
941
+ 错误详情:${errorMsg}`;
942
+ return buildToolResult({
943
+ ok: false,
944
+ action: "create_collect",
945
+ accountId: account.accountId,
946
+ error: errorMsg,
947
+ summary: "创建收集表失败",
948
+ hint: hint.trim(),
949
+ raw: {},
950
+ });
951
+ }
830
952
  }
831
953
  case "modify_collect": {
832
954
  const result = await docClient.modifyCollect({
@@ -843,7 +965,7 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
843
965
  formId: result.formId,
844
966
  title: title || undefined,
845
967
  summary: title
846
- ? `收集表已更新(${result.oper}):“${title}”`
968
+ ? `收集表已更新(${result.oper}):"${title}"`
847
969
  : `收集表已更新(${result.oper})`,
848
970
  raw: result.raw,
849
971
  });
@@ -909,7 +1031,10 @@ export function registerWecomDocTools(api: OpenClawPluginApi) {
909
1031
  const result = await docClient.editSheetData({
910
1032
  agent: account,
911
1033
  docId: params.docId,
912
- request: params.request,
1034
+ sheetId: params.sheetId,
1035
+ startRow: params.startRow ?? 0,
1036
+ startColumn: params.startColumn ?? 0,
1037
+ gridData: params.gridData,
913
1038
  });
914
1039
  return buildToolResult({
915
1040
  ok: true,