@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.
- package/README.md +32 -110
- package/changelog/v2.3.15.md +15 -0
- package/changelog/v2.3.16.md +11 -0
- package/docs/update-content-fix.md +135 -0
- package/package.json +1 -1
- package/src/agent/handler.ts +1 -1
- package/src/capability/doc/client.ts +239 -30
- package/src/capability/doc/schema.ts +25 -5
- package/src/capability/doc/tool.ts +246 -124
- package/src/capability/doc/types.ts +268 -0
- package/src/outbound.ts +12 -7
- package/src/runtime.ts +1 -0
- package/src/target.ts +37 -26
- package/src/transport/bot-ws/inbound.test.ts +46 -0
- package/src/transport/bot-ws/inbound.ts +23 -5
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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(
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
494
|
-
|
|
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:
|
|
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:
|
|
769
|
+
additionalProperties: true,
|
|
750
770
|
properties: {
|
|
751
771
|
update_file_member_list: docMemberListProperty,
|
|
752
772
|
del_file_member_list: delDocMemberListProperty
|