@yanhaidao/wecom 2.3.160 → 2.3.190
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 +294 -379
- package/SKILLS_CAL.md +895 -0
- package/SKILLS_DOC.md +2288 -0
- package/changelog/v2.3.18.md +22 -0
- package/changelog/v2.3.19.md +73 -0
- package/index.ts +39 -3
- package/package.json +2 -3
- package/src/agent/handler.event-filter.test.ts +11 -0
- package/src/agent/handler.ts +732 -643
- package/src/app/account-runtime.ts +46 -20
- package/src/app/index.ts +20 -1
- package/src/capability/bot/stream-orchestrator.ts +1 -1
- package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
- package/src/capability/calendar/client.ts +815 -0
- package/src/capability/calendar/index.ts +3 -0
- package/src/capability/calendar/schema.ts +417 -0
- package/src/capability/calendar/tool.ts +417 -0
- package/src/capability/calendar/types.ts +309 -0
- package/src/capability/doc/client.ts +788 -64
- package/src/capability/doc/schema.ts +419 -318
- package/src/capability/doc/tool.ts +1517 -1178
- package/src/capability/doc/types.ts +130 -14
- package/src/capability/mcp/index.ts +10 -0
- package/src/capability/mcp/schema.ts +107 -0
- package/src/capability/mcp/tool.ts +170 -0
- package/src/capability/mcp/transport.ts +394 -0
- package/src/channel.ts +70 -28
- package/src/config/index.ts +7 -1
- package/src/config/media.test.ts +113 -0
- package/src/config/media.ts +133 -6
- package/src/config/schema.ts +74 -102
- package/src/outbound.test.ts +250 -15
- package/src/outbound.ts +155 -30
- package/src/runtime/reply-orchestrator.test.ts +35 -2
- package/src/runtime/reply-orchestrator.ts +14 -2
- package/src/runtime/routing-bridge.test.ts +115 -0
- package/src/runtime/routing-bridge.ts +26 -1
- package/src/runtime/session-manager.ts +20 -6
- package/src/runtime/source-registry.ts +165 -0
- package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
- package/src/transport/bot-ws/media.test.ts +44 -0
- package/src/transport/bot-ws/media.ts +272 -0
- package/src/transport/bot-ws/reply.test.ts +216 -18
- package/src/transport/bot-ws/reply.ts +116 -21
- package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
- package/src/transport/bot-ws/sdk-adapter.ts +89 -12
- package/src/types/config.ts +3 -0
- package/.claude/settings.local.json +0 -11
- package/docs/update-content-fix.md +0 -135
|
@@ -196,25 +196,112 @@ export class WecomDocClient {
|
|
|
196
196
|
const normalizedFatherId = readString(fatherId);
|
|
197
197
|
if (normalizedSpaceId) payload.spaceid = normalizedSpaceId;
|
|
198
198
|
if (normalizedFatherId) payload.fatherid = normalizedFatherId;
|
|
199
|
+
|
|
200
|
+
// admin_users is required for smart_table to ensure proper permissions
|
|
199
201
|
const normalizedAdminUsers = Array.isArray(adminUsers)
|
|
200
202
|
? adminUsers.map((item) => readString(item)).filter(Boolean)
|
|
201
203
|
: [];
|
|
202
204
|
if (normalizedAdminUsers.length > 0) {
|
|
203
205
|
payload.admin_users = normalizedAdminUsers;
|
|
204
206
|
}
|
|
207
|
+
|
|
205
208
|
const json = await this.postWecomDocApi({
|
|
206
209
|
path: "/cgi-bin/wedoc/create_doc",
|
|
207
210
|
actionLabel: "create_doc",
|
|
208
211
|
agent,
|
|
209
212
|
body: payload,
|
|
210
213
|
});
|
|
211
|
-
|
|
214
|
+
|
|
215
|
+
const result = {
|
|
212
216
|
raw: json,
|
|
213
217
|
docId: readString(json.docid),
|
|
214
218
|
url: readString(json.url),
|
|
215
219
|
docType: normalizedDocType,
|
|
216
220
|
docTypeLabel: mapDocTypeLabel(normalizedDocType),
|
|
217
221
|
};
|
|
222
|
+
|
|
223
|
+
// Auto-initialize smart_table: clean up default fields and records
|
|
224
|
+
if (normalizedDocType === 10) {
|
|
225
|
+
await this.initializeSmartTable({ agent, docId: result.docId });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Initialize smart_table after creation:
|
|
233
|
+
* 1. Get default sheet (smartsheet type)
|
|
234
|
+
* 2. Get default fields (usually 5 fields)
|
|
235
|
+
* 3. Delete 4 default fields, keep 1 as primary key
|
|
236
|
+
* 4. Get default records (usually 5 empty records)
|
|
237
|
+
* 5. Delete all default records
|
|
238
|
+
*/
|
|
239
|
+
private async initializeSmartTable(params: { agent: ResolvedAgentAccount; docId: string }) {
|
|
240
|
+
const { agent, docId } = params;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Step 1: Get sheet list to find the default smartsheet
|
|
244
|
+
const sheetsResult = await this.smartTableGetSheets({ agent, docId });
|
|
245
|
+
const defaultSheet = sheetsResult.sheets.find((s: any) => s.type === "smartsheet");
|
|
246
|
+
if (!defaultSheet) return; // No smartsheet found, skip initialization
|
|
247
|
+
|
|
248
|
+
const sheetId = (defaultSheet as any).sheet_id;
|
|
249
|
+
|
|
250
|
+
// Step 2: Get default fields
|
|
251
|
+
const fieldsResult = await this.smartTableGetFields({ agent, docId, sheetId });
|
|
252
|
+
const fields = fieldsResult.fields || [];
|
|
253
|
+
|
|
254
|
+
if (fields.length > 1) {
|
|
255
|
+
// Keep the last field as primary key, delete the rest
|
|
256
|
+
// Primary key capable types: TEXT, NUMBER, DATE_TIME, URL, PROGRESS, EMAIL, PHONE_NUMBER, FORMULA, LOCATION, CURRENCY, AUTONUMBER, TITLE, WWGROUP
|
|
257
|
+
const primaryKeyCapableTypes = [
|
|
258
|
+
'FIELD_TYPE_TEXT', 'FIELD_TYPE_NUMBER', 'FIELD_TYPE_DATE_TIME',
|
|
259
|
+
'FIELD_TYPE_URL', 'FIELD_TYPE_PROGRESS', 'FIELD_TYPE_EMAIL',
|
|
260
|
+
'FIELD_TYPE_PHONE_NUMBER', 'FIELD_TYPE_LOCATION', 'FIELD_TYPE_CURRENCY',
|
|
261
|
+
'FIELD_TYPE_AUTONUMBER', 'FIELD_TYPE_WWGROUP'
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
// Find a field that can be primary key (prefer the last one)
|
|
265
|
+
let fieldToDelete: string[] = [];
|
|
266
|
+
let fieldToKeep: string | null = null;
|
|
267
|
+
|
|
268
|
+
for (let i = fields.length - 1; i >= 0; i--) {
|
|
269
|
+
const field = fields[i] as any;
|
|
270
|
+
if (!fieldToKeep && primaryKeyCapableTypes.includes(field.field_type)) {
|
|
271
|
+
fieldToKeep = field.field_id;
|
|
272
|
+
} else if (field.field_id) {
|
|
273
|
+
fieldToDelete.push(field.field_id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If no primary key capable field found, keep the last one anyway
|
|
278
|
+
if (!fieldToKeep && fields.length > 0) {
|
|
279
|
+
const lastField = fields[fields.length - 1] as any;
|
|
280
|
+
fieldToKeep = lastField.field_id;
|
|
281
|
+
fieldToDelete = fields.slice(0, -1).map((f: any) => f.field_id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Delete the fields
|
|
285
|
+
if (fieldToDelete.length > 0) {
|
|
286
|
+
await this.smartTableDelFields({ agent, docId, sheetId, field_ids: fieldToDelete });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Step 3: Get default records
|
|
291
|
+
const recordsResult = await this.smartTableGetRecords({ agent, docId, sheetId, limit: 100 });
|
|
292
|
+
const records = recordsResult.records || [];
|
|
293
|
+
|
|
294
|
+
if (records.length > 0) {
|
|
295
|
+
// Delete all default empty records
|
|
296
|
+
const recordIds = records.map((r: any) => r.record_id).filter(Boolean);
|
|
297
|
+
if (recordIds.length > 0) {
|
|
298
|
+
await this.smartTableDelRecords({ agent, docId, sheetId, record_ids: recordIds });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
// Non-fatal: smart_table created, just default cleanup failed
|
|
303
|
+
console.error(`[WecomDocClient] initializeSmartTable failed:`, err);
|
|
304
|
+
}
|
|
218
305
|
}
|
|
219
306
|
|
|
220
307
|
async renameDoc(params: { agent: ResolvedAgentAccount; docId: string; newName: string }) {
|
|
@@ -391,19 +478,23 @@ export class WecomDocClient {
|
|
|
391
478
|
// Need to check current auth status
|
|
392
479
|
try {
|
|
393
480
|
const currentAuth = await this.getDocAuth({ agent, docId });
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const newCollaboratorUserIds = normalizeDocMemberEntryList(collaborators)
|
|
400
|
-
.map(e => e.userid)
|
|
401
|
-
.filter(Boolean) as string[];
|
|
481
|
+
// Build a map of viewer entries with their full structure (preserving type and other fields)
|
|
482
|
+
const viewerMap = new Map<string, any>();
|
|
483
|
+
(currentAuth.docMembers || [])
|
|
484
|
+
.filter((m: any) => m.userid)
|
|
485
|
+
.forEach((m: any) => viewerMap.set(m.userid, m));
|
|
402
486
|
|
|
403
|
-
//
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
487
|
+
// Normalize new collaborators to get their userids
|
|
488
|
+
const newCollaboratorEntries = normalizeDocMemberEntryList(collaborators);
|
|
489
|
+
|
|
490
|
+
// Auto-add viewers who are being promoted to collaborators, preserving their original structure
|
|
491
|
+
const autoRemoveViewers = newCollaboratorEntries
|
|
492
|
+
.filter(entry => entry.userid && viewerMap.has(entry.userid))
|
|
493
|
+
.map(entry => {
|
|
494
|
+
// Preserve the original viewer's full structure (type, userid, etc.)
|
|
495
|
+
const originalViewer = viewerMap.get(entry.userid!);
|
|
496
|
+
return { ...originalViewer };
|
|
497
|
+
});
|
|
407
498
|
|
|
408
499
|
if (autoRemoveViewers.length > 0) {
|
|
409
500
|
finalRemoveViewers = autoRemoveViewers;
|
|
@@ -488,6 +579,16 @@ export class WecomDocClient {
|
|
|
488
579
|
throw new Error("问题数量不能超过 200 个");
|
|
489
580
|
}
|
|
490
581
|
|
|
582
|
+
// Auto-fill status fields for questions and options
|
|
583
|
+
questions.forEach((q: any) => {
|
|
584
|
+
if (q.status === undefined) q.status = 1;
|
|
585
|
+
if (Array.isArray(q.option_item)) {
|
|
586
|
+
q.option_item.forEach((opt: any) => {
|
|
587
|
+
if (opt.status === undefined) opt.status = 1;
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
491
592
|
// Validate each question
|
|
492
593
|
questions.forEach((q: any, index: number) => {
|
|
493
594
|
if (!q.question_id || !Number.isInteger(q.question_id) || q.question_id < 1) {
|
|
@@ -505,6 +606,9 @@ export class WecomDocClient {
|
|
|
505
606
|
if (q.must_reply === undefined || typeof q.must_reply !== 'boolean') {
|
|
506
607
|
throw new Error(`第${index + 1}个问题:must_reply 必填且必须为布尔值`);
|
|
507
608
|
}
|
|
609
|
+
if (q.status !== undefined && ![1, 2].includes(q.status)) {
|
|
610
|
+
throw new Error(`第${index + 1}个问题:status 必须为 1(正常) 或 2(删除)`);
|
|
611
|
+
}
|
|
508
612
|
|
|
509
613
|
// Validate option_item for single/multiple/dropdown questions
|
|
510
614
|
const requiresOptions = [2, 3, 15].includes(q.reply_type); // 单选/多选/下拉列表
|
|
@@ -520,6 +624,9 @@ export class WecomDocClient {
|
|
|
520
624
|
if (!opt.value || readString(opt.value).length === 0) {
|
|
521
625
|
throw new Error(`第${index + 1}个问题的第${optIndex + 1}个选项:value 必填`);
|
|
522
626
|
}
|
|
627
|
+
if (opt.status !== undefined && ![1, 2].includes(opt.status)) {
|
|
628
|
+
throw new Error(`第${index + 1}个问题的第${optIndex + 1}个选项:status 必须为 1(正常) 或 2(删除)`);
|
|
629
|
+
}
|
|
523
630
|
});
|
|
524
631
|
}
|
|
525
632
|
|
|
@@ -546,6 +653,13 @@ export class WecomDocClient {
|
|
|
546
653
|
console.warn("警告:timed_finish 与 timed_repeat_info 互斥,若都填优先定时重复");
|
|
547
654
|
}
|
|
548
655
|
|
|
656
|
+
// Validate timed_repeat_info.enable=true requires fill_in_range
|
|
657
|
+
if (formSetting.timed_repeat_info?.enable) {
|
|
658
|
+
if (!formSetting.fill_in_range || (!formSetting.fill_in_range.userids?.length && !formSetting.fill_in_range.departmentids?.length)) {
|
|
659
|
+
throw new Error("timed_repeat_info 开启时,fill_in_range 必填(需指定 userids 或 departmentids)");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
549
663
|
// Build payload
|
|
550
664
|
const payload: Record<string, unknown> = {
|
|
551
665
|
form_info: {
|
|
@@ -563,8 +677,8 @@ export class WecomDocClient {
|
|
|
563
677
|
if (normalizedFatherId) payload.fatherid = normalizedFatherId;
|
|
564
678
|
|
|
565
679
|
const json = await this.postWecomDocApi({
|
|
566
|
-
path: "/cgi-bin/wedoc/
|
|
567
|
-
actionLabel: "
|
|
680
|
+
path: "/cgi-bin/wedoc/create_form",
|
|
681
|
+
actionLabel: "create_form",
|
|
568
682
|
agent,
|
|
569
683
|
body: payload,
|
|
570
684
|
});
|
|
@@ -605,6 +719,16 @@ export class WecomDocClient {
|
|
|
605
719
|
throw new Error("问题数量不能超过 200 个");
|
|
606
720
|
}
|
|
607
721
|
|
|
722
|
+
// Auto-fill status fields for questions and options
|
|
723
|
+
questions.forEach((q: any) => {
|
|
724
|
+
if (q.status === undefined) q.status = 1;
|
|
725
|
+
if (Array.isArray(q.option_item)) {
|
|
726
|
+
q.option_item.forEach((opt: any) => {
|
|
727
|
+
if (opt.status === undefined) opt.status = 1;
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
608
732
|
// Validate each question (same as createCollect)
|
|
609
733
|
questions.forEach((q: any, index: number) => {
|
|
610
734
|
if (!q.question_id || !Number.isInteger(q.question_id) || q.question_id < 1) {
|
|
@@ -658,8 +782,8 @@ export class WecomDocClient {
|
|
|
658
782
|
}
|
|
659
783
|
|
|
660
784
|
const json = await this.postWecomDocApi({
|
|
661
|
-
path: "/cgi-bin/wedoc/
|
|
662
|
-
actionLabel: "
|
|
785
|
+
path: "/cgi-bin/wedoc/modify_form",
|
|
786
|
+
actionLabel: "modify_form",
|
|
663
787
|
agent,
|
|
664
788
|
body: payload,
|
|
665
789
|
});
|
|
@@ -696,6 +820,12 @@ export class WecomDocClient {
|
|
|
696
820
|
.map((item) => Number(item))
|
|
697
821
|
.filter((item) => Number.isFinite(item))
|
|
698
822
|
: [];
|
|
823
|
+
|
|
824
|
+
// Official API limit: ≤100 answer IDs
|
|
825
|
+
if (normalizedAnswerIds.length > 100) {
|
|
826
|
+
throw new Error(`answer_ids 不能超过 100 个,当前:${normalizedAnswerIds.length}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
699
829
|
const payload: Record<string, unknown> = {
|
|
700
830
|
repeated_id: normalizedRepeatedId,
|
|
701
831
|
};
|
|
@@ -724,6 +854,32 @@ export class WecomDocClient {
|
|
|
724
854
|
if (payload.length === 0) {
|
|
725
855
|
throw new Error("requests required");
|
|
726
856
|
}
|
|
857
|
+
|
|
858
|
+
// Validate each request per official API
|
|
859
|
+
payload.forEach((req: any, index: number) => {
|
|
860
|
+
const reqType = Number(req.req_type);
|
|
861
|
+
|
|
862
|
+
// req_type=2: Get submitted list - requires start_time and end_time (same day timestamps)
|
|
863
|
+
if (reqType === 2) {
|
|
864
|
+
if (!req.start_time || !req.end_time) {
|
|
865
|
+
throw new Error(`第${index + 1}个请求:req_type=2 时必须提供 start_time 和 end_time(当天时间戳)`);
|
|
866
|
+
}
|
|
867
|
+
// Validate timestamps are numbers
|
|
868
|
+
if (!Number.isFinite(Number(req.start_time)) || !Number.isFinite(Number(req.end_time))) {
|
|
869
|
+
throw new Error(`第${index + 1}个请求:start_time 和 end_time 必须是有效时间戳`);
|
|
870
|
+
}
|
|
871
|
+
// Validate end_time >= start_time
|
|
872
|
+
if (Number(req.end_time) < Number(req.start_time)) {
|
|
873
|
+
throw new Error(`第${index + 1}个请求:end_time 必须大于等于 start_time`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Validate repeated_id is present
|
|
878
|
+
if (!req.repeated_id) {
|
|
879
|
+
throw new Error(`第${index + 1}个请求:repeated_id 必填`);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
727
883
|
const json = await this.postWecomDocApi({
|
|
728
884
|
path: "/cgi-bin/wedoc/get_form_statistic",
|
|
729
885
|
actionLabel: "get_form_statistic",
|
|
@@ -758,30 +914,90 @@ export class WecomDocClient {
|
|
|
758
914
|
}
|
|
759
915
|
|
|
760
916
|
async updateDocContent(params: { agent: ResolvedAgentAccount; docId: string; requests: UpdateRequest[]; version?: number; batchMode?: boolean }) {
|
|
761
|
-
const { agent, docId, requests, version
|
|
917
|
+
const { agent, docId, requests, version } = params;
|
|
762
918
|
|
|
763
919
|
// Validate requests structure basic check
|
|
764
920
|
const requestList = readArray(requests);
|
|
765
921
|
if (requestList.length === 0) {
|
|
766
922
|
throw new Error("requests list cannot be empty");
|
|
767
923
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
docid: readString(docId),
|
|
771
|
-
requests: requestList,
|
|
772
|
-
};
|
|
773
|
-
// version is optional but recommended for concurrency control
|
|
924
|
+
|
|
925
|
+
// Validate version difference (≤100 per official API)
|
|
774
926
|
if (version !== undefined && version !== null) {
|
|
775
|
-
|
|
927
|
+
const currentContent = await this.getDocContent({ agent, docId });
|
|
928
|
+
const versionDiff = Math.abs(currentContent.version - version);
|
|
929
|
+
if (versionDiff > 100) {
|
|
930
|
+
throw new Error(`version 与最新版本差值不能超过 100(当前版本:${currentContent.version},传入版本:${version},差值:${versionDiff})`);
|
|
931
|
+
}
|
|
776
932
|
}
|
|
777
933
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
934
|
+
// Validate each request's ranges count (≤10 per official API)
|
|
935
|
+
requestList.forEach((req: any, index: number) => {
|
|
936
|
+
if (req.replace_text?.ranges && req.replace_text.ranges.length > 10) {
|
|
937
|
+
throw new Error(`第${index + 1}个操作:replace_text.ranges 不能超过 10 个`);
|
|
938
|
+
}
|
|
939
|
+
if (req.update_text_property?.ranges && req.update_text_property.ranges.length > 10) {
|
|
940
|
+
throw new Error(`第${index + 1}个操作:update_text_property.ranges 不能超过 10 个`);
|
|
941
|
+
}
|
|
942
|
+
// Validate insert_table limits
|
|
943
|
+
if (req.insert_table) {
|
|
944
|
+
const { rows, cols } = req.insert_table;
|
|
945
|
+
if (rows > 100) throw new Error(`第${index + 1}个操作:insert_table 行数不能超过 100`);
|
|
946
|
+
if (cols > 60) throw new Error(`第${index + 1}个操作:insert_table 列数不能超过 60`);
|
|
947
|
+
if (rows * cols > 1000) throw new Error(`第${index + 1}个操作:insert_table 单元格总数不能超过 1000`);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Official API limit: ≤30 operations per batch
|
|
952
|
+
const MAX_OPERATIONS = 30;
|
|
953
|
+
if (requestList.length <= MAX_OPERATIONS) {
|
|
954
|
+
// Single batch
|
|
955
|
+
const body: Record<string, unknown> = {
|
|
956
|
+
docid: readString(docId),
|
|
957
|
+
requests: requestList,
|
|
958
|
+
};
|
|
959
|
+
if (version !== undefined && version !== null) {
|
|
960
|
+
body.version = Number(version);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const json = await this.postWecomDocApi({
|
|
964
|
+
path: "/cgi-bin/wedoc/document/batch_update",
|
|
965
|
+
actionLabel: "update_doc_content",
|
|
966
|
+
agent,
|
|
967
|
+
body,
|
|
968
|
+
}) as BatchUpdateDocResponse;
|
|
969
|
+
return { raw: json, batches: 1 };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Auto-batch: split into multiple requests
|
|
973
|
+
// Note: Each batch updates the version, so we need to get latest version for each batch
|
|
974
|
+
const batches: BatchUpdateDocResponse[] = [];
|
|
975
|
+
for (let i = 0; i < requestList.length; i += MAX_OPERATIONS) {
|
|
976
|
+
const batchRequests = requestList.slice(i, i + MAX_OPERATIONS);
|
|
977
|
+
|
|
978
|
+
// Get latest version before each batch (except first if version provided)
|
|
979
|
+
let currentVersion = version;
|
|
980
|
+
if (i > 0 || currentVersion === undefined || currentVersion === null) {
|
|
981
|
+
const content = await this.getDocContent({ agent, docId });
|
|
982
|
+
currentVersion = content.version;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const body: Record<string, unknown> = {
|
|
986
|
+
docid: readString(docId),
|
|
987
|
+
requests: batchRequests,
|
|
988
|
+
version: currentVersion,
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const json = await this.postWecomDocApi({
|
|
992
|
+
path: "/cgi-bin/wedoc/document/batch_update",
|
|
993
|
+
actionLabel: `update_doc_content_batch_${Math.floor(i / MAX_OPERATIONS) + 1}`,
|
|
994
|
+
agent,
|
|
995
|
+
body,
|
|
996
|
+
}) as BatchUpdateDocResponse;
|
|
997
|
+
batches.push(json);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return { raw: batches[batches.length - 1], batches: batches.length, allBatches: batches };
|
|
785
1001
|
}
|
|
786
1002
|
|
|
787
1003
|
// --- Spreadsheet Operations ---
|
|
@@ -824,8 +1040,9 @@ export class WecomDocClient {
|
|
|
824
1040
|
startRow?: number;
|
|
825
1041
|
startColumn?: number;
|
|
826
1042
|
gridData?: any;
|
|
1043
|
+
requests?: any[]; // For direct batch_update with multiple operations
|
|
827
1044
|
}) {
|
|
828
|
-
const { agent, docId, sheetId, startRow = 0, startColumn = 0, gridData } = params;
|
|
1045
|
+
const { agent, docId, sheetId, startRow = 0, startColumn = 0, gridData, requests } = params;
|
|
829
1046
|
|
|
830
1047
|
// Validate required docId
|
|
831
1048
|
const normalizedDocId = readString(docId);
|
|
@@ -839,15 +1056,72 @@ export class WecomDocClient {
|
|
|
839
1056
|
throw new Error('sheetId is required');
|
|
840
1057
|
}
|
|
841
1058
|
|
|
1059
|
+
// Handle direct requests (for multiple operations)
|
|
1060
|
+
if (requests && requests.length > 0) {
|
|
1061
|
+
// Official API limit: ≤5 operations per batch
|
|
1062
|
+
const MAX_OPERATIONS = 5;
|
|
1063
|
+
|
|
1064
|
+
// Validate each request
|
|
1065
|
+
requests.forEach((req: any, index: number) => {
|
|
1066
|
+
if (req.update_range_request?.grid_data?.rows) {
|
|
1067
|
+
const rows = req.update_range_request.grid_data.rows;
|
|
1068
|
+
const rowCount = rows.length;
|
|
1069
|
+
const rowWidths = rows.map((row: any) => row.values?.length || 0);
|
|
1070
|
+
const columnCount = rowWidths.length > 0 ? Math.max(...rowWidths) : 0;
|
|
1071
|
+
const totalCells = rowWidths.reduce((sum: number, width: number) => sum + width, 0);
|
|
1072
|
+
|
|
1073
|
+
if (rowCount > 1000) throw new Error(`第${index + 1}个操作:行数不能超过 1000`);
|
|
1074
|
+
if (columnCount > 200) throw new Error(`第${index + 1}个操作:列数不能超过 200`);
|
|
1075
|
+
if (totalCells > 10000) throw new Error(`第${index + 1}个操作:单元格总数不能超过 10000`);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
if (requests.length > MAX_OPERATIONS) {
|
|
1080
|
+
throw new Error(`单次批量更新最多${MAX_OPERATIONS}个操作,当前:${requests.length}`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const body = {
|
|
1084
|
+
docid: normalizedDocId,
|
|
1085
|
+
requests: requests
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const json = await this.postWecomDocApi({
|
|
1089
|
+
path: "/cgi-bin/wedoc/spreadsheet/batch_update",
|
|
1090
|
+
actionLabel: "spreadsheet_batch_update",
|
|
1091
|
+
agent, body,
|
|
1092
|
+
});
|
|
1093
|
+
return {
|
|
1094
|
+
raw: json,
|
|
1095
|
+
docId: normalizedDocId,
|
|
1096
|
+
operations: requests.length
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Handle single gridData update
|
|
1101
|
+
if (!gridData) {
|
|
1102
|
+
throw new Error('gridData or requests is required');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
842
1105
|
// Build GridData per official API
|
|
843
1106
|
// gridData.rows[i].values[j] must be: {cell_value: {text} | {link: {text, url}}, cell_format?: {...}}
|
|
844
|
-
const rows = (gridData
|
|
1107
|
+
const rows = (gridData.rows || []).map((row: any) => ({
|
|
845
1108
|
values: (row.values || []).map((cell: any) => {
|
|
846
1109
|
// If already CellData format, use as-is
|
|
847
1110
|
if (cell && typeof cell === 'object' && cell.cell_value) {
|
|
848
1111
|
return cell;
|
|
849
1112
|
}
|
|
850
|
-
//
|
|
1113
|
+
// Support link simplified format: { url: '...', text: '...' }
|
|
1114
|
+
if (cell && typeof cell === 'object' && cell.url) {
|
|
1115
|
+
return {
|
|
1116
|
+
cell_value: {
|
|
1117
|
+
link: {
|
|
1118
|
+
url: String(cell.url),
|
|
1119
|
+
text: String(cell.text ?? cell.url)
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
// Otherwise wrap primitive as CellValue with text
|
|
851
1125
|
return { cell_value: { text: String(cell ?? '') } };
|
|
852
1126
|
})
|
|
853
1127
|
}));
|
|
@@ -874,8 +1148,7 @@ export class WecomDocClient {
|
|
|
874
1148
|
rows: rows
|
|
875
1149
|
};
|
|
876
1150
|
|
|
877
|
-
// Build batch_update request per official API
|
|
878
|
-
// Note: requests array length ≤ 5 per API spec
|
|
1151
|
+
// Build batch_update request per official API (single operation)
|
|
879
1152
|
const body = {
|
|
880
1153
|
docid: normalizedDocId,
|
|
881
1154
|
requests: [{
|
|
@@ -893,7 +1166,7 @@ export class WecomDocClient {
|
|
|
893
1166
|
});
|
|
894
1167
|
return {
|
|
895
1168
|
raw: json,
|
|
896
|
-
docId:
|
|
1169
|
+
docId: normalizedDocId,
|
|
897
1170
|
updatedCells: json.data?.responses?.[0]?.update_range_response?.updated_cells || 0
|
|
898
1171
|
};
|
|
899
1172
|
}
|
|
@@ -978,13 +1251,18 @@ export class WecomDocClient {
|
|
|
978
1251
|
return { raw: json, docId };
|
|
979
1252
|
}
|
|
980
1253
|
|
|
981
|
-
async smartTableGetSheets(params: { agent: ResolvedAgentAccount; docId: string }) {
|
|
982
|
-
const { agent, docId } = params;
|
|
1254
|
+
async smartTableGetSheets(params: { agent: ResolvedAgentAccount; docId: string; sheet_id?: string; need_all_type_sheet?: boolean }) {
|
|
1255
|
+
const { agent, docId, sheet_id, need_all_type_sheet } = params;
|
|
1256
|
+
const payload: Record<string, unknown> = {
|
|
1257
|
+
docid: readString(docId),
|
|
1258
|
+
};
|
|
1259
|
+
if (sheet_id) payload.sheet_id = sheet_id;
|
|
1260
|
+
if (need_all_type_sheet !== undefined) payload.need_all_type_sheet = need_all_type_sheet;
|
|
983
1261
|
const json = await this.postWecomDocApi({
|
|
984
1262
|
path: "/cgi-bin/wedoc/smartsheet/get_sheet",
|
|
985
1263
|
actionLabel: "smartsheet_get_sheet",
|
|
986
1264
|
agent,
|
|
987
|
-
body:
|
|
1265
|
+
body: payload,
|
|
988
1266
|
});
|
|
989
1267
|
return {
|
|
990
1268
|
raw: json,
|
|
@@ -1006,34 +1284,329 @@ export class WecomDocClient {
|
|
|
1006
1284
|
const { agent, docId, sheetId, title } = params;
|
|
1007
1285
|
return this.smartTableOperate({ agent, docId, operation: "update_sheet", bodyData: { properties: { sheet_id: sheetId, title } } });
|
|
1008
1286
|
}
|
|
1009
|
-
async smartTableAddView(params: {
|
|
1010
|
-
|
|
1011
|
-
|
|
1287
|
+
async smartTableAddView(params: {
|
|
1288
|
+
agent: ResolvedAgentAccount;
|
|
1289
|
+
docId: string;
|
|
1290
|
+
sheetId: string;
|
|
1291
|
+
view_title: string;
|
|
1292
|
+
view_type: string;
|
|
1293
|
+
property?: any; // ViewProperty: sort_spec, filter_spec, group_spec, etc.
|
|
1294
|
+
property_gantt?: any; // Deprecated, use property instead
|
|
1295
|
+
property_calendar?: any; // Deprecated, use property instead
|
|
1296
|
+
}) {
|
|
1297
|
+
const { agent, docId, sheetId, view_title, view_type, property, property_gantt, property_calendar } = params;
|
|
1298
|
+
const payload: Record<string, unknown> = {
|
|
1299
|
+
docid: readString(docId),
|
|
1300
|
+
sheet_id: readString(sheetId),
|
|
1301
|
+
view_title: readString(view_title),
|
|
1302
|
+
view_type: readString(view_type),
|
|
1303
|
+
};
|
|
1304
|
+
if (property && typeof property === 'object') {
|
|
1305
|
+
payload.property = property;
|
|
1306
|
+
}
|
|
1307
|
+
// Support deprecated property_gantt/property_calendar for backward compatibility
|
|
1308
|
+
if (property_gantt) payload.property_gantt = property_gantt;
|
|
1309
|
+
if (property_calendar) payload.property_calendar = property_calendar;
|
|
1310
|
+
|
|
1311
|
+
const json = await this.postWecomDocApi({
|
|
1312
|
+
path: "/cgi-bin/wedoc/smartsheet/add_view",
|
|
1313
|
+
actionLabel: "smartsheet_add_view",
|
|
1314
|
+
agent,
|
|
1315
|
+
body: payload,
|
|
1316
|
+
});
|
|
1317
|
+
return {
|
|
1318
|
+
raw: json,
|
|
1319
|
+
view: json.view,
|
|
1320
|
+
};
|
|
1012
1321
|
}
|
|
1013
1322
|
|
|
1014
|
-
async smartTableUpdateView(params: {
|
|
1015
|
-
|
|
1016
|
-
|
|
1323
|
+
async smartTableUpdateView(params: {
|
|
1324
|
+
agent: ResolvedAgentAccount;
|
|
1325
|
+
docId: string;
|
|
1326
|
+
sheetId: string;
|
|
1327
|
+
view_id: string;
|
|
1328
|
+
view_title?: string;
|
|
1329
|
+
property?: any; // ViewProperty: sort_spec, filter_spec, group_spec, etc.
|
|
1330
|
+
property_gantt?: any; // Deprecated, use property instead
|
|
1331
|
+
property_calendar?: any; // Deprecated, use property instead
|
|
1332
|
+
}) {
|
|
1333
|
+
const { agent, docId, sheetId, view_id, view_title, property, property_gantt, property_calendar } = params;
|
|
1334
|
+
const payload: Record<string, unknown> = {
|
|
1335
|
+
docid: readString(docId),
|
|
1336
|
+
sheet_id: readString(sheetId),
|
|
1337
|
+
view_id: readString(view_id),
|
|
1338
|
+
};
|
|
1339
|
+
if (view_title) payload.view_title = readString(view_title);
|
|
1340
|
+
if (property && typeof property === 'object') {
|
|
1341
|
+
payload.property = property;
|
|
1342
|
+
}
|
|
1343
|
+
// Support deprecated property_gantt/property_calendar for backward compatibility
|
|
1344
|
+
if (property_gantt) payload.property_gantt = property_gantt;
|
|
1345
|
+
if (property_calendar) payload.property_calendar = property_calendar;
|
|
1346
|
+
|
|
1347
|
+
const json = await this.postWecomDocApi({
|
|
1348
|
+
path: "/cgi-bin/wedoc/smartsheet/update_view",
|
|
1349
|
+
actionLabel: "smartsheet_update_view",
|
|
1350
|
+
agent,
|
|
1351
|
+
body: payload,
|
|
1352
|
+
});
|
|
1353
|
+
return {
|
|
1354
|
+
raw: json,
|
|
1355
|
+
view: json.view,
|
|
1356
|
+
};
|
|
1017
1357
|
}
|
|
1018
1358
|
|
|
1019
1359
|
async smartTableDelView(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; view_ids: string[] }) {
|
|
1020
1360
|
const { agent, docId, sheetId, view_ids } = params;
|
|
1021
|
-
|
|
1361
|
+
|
|
1362
|
+
if (!Array.isArray(view_ids) || view_ids.length === 0) {
|
|
1363
|
+
throw new Error("view_ids 必须是非空数组");
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
return this.postWecomDocApi({
|
|
1367
|
+
path: "/cgi-bin/wedoc/smartsheet/delete_views",
|
|
1368
|
+
actionLabel: "smartsheet_del_view",
|
|
1369
|
+
agent,
|
|
1370
|
+
body: {
|
|
1371
|
+
docid: readString(docId),
|
|
1372
|
+
sheet_id: readString(sheetId),
|
|
1373
|
+
view_ids: view_ids,
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1022
1376
|
}
|
|
1023
1377
|
|
|
1024
|
-
async
|
|
1378
|
+
async smartTableGetViews(params: {
|
|
1379
|
+
agent: ResolvedAgentAccount;
|
|
1380
|
+
docId: string;
|
|
1381
|
+
sheetId: string;
|
|
1382
|
+
view_ids?: string[];
|
|
1383
|
+
offset?: number;
|
|
1384
|
+
limit?: number;
|
|
1385
|
+
}) {
|
|
1386
|
+
const { agent, docId, sheetId, view_ids, offset, limit } = params;
|
|
1387
|
+
const payload: Record<string, unknown> = {
|
|
1388
|
+
docid: readString(docId),
|
|
1389
|
+
sheet_id: readString(sheetId),
|
|
1390
|
+
};
|
|
1391
|
+
if (view_ids && Array.isArray(view_ids)) payload.view_ids = view_ids;
|
|
1392
|
+
if (offset !== undefined) payload.offset = offset;
|
|
1393
|
+
if (limit !== undefined) payload.limit = limit;
|
|
1394
|
+
|
|
1395
|
+
const json = await this.postWecomDocApi({
|
|
1396
|
+
path: "/cgi-bin/wedoc/smartsheet/get_views",
|
|
1397
|
+
actionLabel: "smartsheet_get_views",
|
|
1398
|
+
agent,
|
|
1399
|
+
body: payload,
|
|
1400
|
+
});
|
|
1401
|
+
return {
|
|
1402
|
+
raw: json,
|
|
1403
|
+
views: readArray(json.views),
|
|
1404
|
+
total: json.total,
|
|
1405
|
+
has_more: json.has_more,
|
|
1406
|
+
next: json.next,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async smartTableAddFields(params: {
|
|
1411
|
+
agent: ResolvedAgentAccount;
|
|
1412
|
+
docId: string;
|
|
1413
|
+
sheetId: string;
|
|
1414
|
+
fields: any[];
|
|
1415
|
+
autoCleanupDefaultField?: boolean; // Auto-delete leftover default field after adding new fields
|
|
1416
|
+
}) {
|
|
1417
|
+
const { agent, docId, sheetId, fields, autoCleanupDefaultField = true } = params;
|
|
1418
|
+
|
|
1419
|
+
// Validate fields per official API spec
|
|
1420
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
1421
|
+
throw new Error("fields 必须是非空数组");
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Validate each field has required field_title and field_type
|
|
1425
|
+
fields.forEach((field: any, index: number) => {
|
|
1426
|
+
if (!field.field_title) {
|
|
1427
|
+
throw new Error(`第${index + 1}个字段:field_title 必填`);
|
|
1428
|
+
}
|
|
1429
|
+
if (!field.field_type) {
|
|
1430
|
+
throw new Error(`第${index + 1}个字段:field_type 必填`);
|
|
1431
|
+
}
|
|
1432
|
+
// Validate field_type is valid enum value
|
|
1433
|
+
const validFieldTypes = [
|
|
1434
|
+
'FIELD_TYPE_TEXT', 'FIELD_TYPE_NUMBER', 'FIELD_TYPE_CHECKBOX',
|
|
1435
|
+
'FIELD_TYPE_DATE_TIME', 'FIELD_TYPE_IMAGE', 'FIELD_TYPE_ATTACHMENT',
|
|
1436
|
+
'FIELD_TYPE_USER', 'FIELD_TYPE_URL', 'FIELD_TYPE_SELECT',
|
|
1437
|
+
'FIELD_TYPE_CREATED_USER', 'FIELD_TYPE_MODIFIED_USER', 'FIELD_TYPE_CREATED_TIME',
|
|
1438
|
+
'FIELD_TYPE_MODIFIED_TIME', 'FIELD_TYPE_PROGRESS', 'FIELD_TYPE_PHONE_NUMBER',
|
|
1439
|
+
'FIELD_TYPE_EMAIL', 'FIELD_TYPE_SINGLE_SELECT', 'FIELD_TYPE_REFERENCE',
|
|
1440
|
+
'FIELD_TYPE_LOCATION', 'FIELD_TYPE_CURRENCY', 'FIELD_TYPE_WWGROUP',
|
|
1441
|
+
'FIELD_TYPE_AUTONUMBER', 'FIELD_TYPE_PERCENTAGE', 'FIELD_TYPE_BARCODE'
|
|
1442
|
+
];
|
|
1443
|
+
if (!validFieldTypes.includes(field.field_type)) {
|
|
1444
|
+
throw new Error(`第${index + 1}个字段:field_type 必须是有效的字段类型(见 FieldType 枚举)`);
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
const json = await this.postWecomDocApi({
|
|
1449
|
+
path: "/cgi-bin/wedoc/smartsheet/add_fields",
|
|
1450
|
+
actionLabel: "smartsheet_add_fields",
|
|
1451
|
+
agent,
|
|
1452
|
+
body: {
|
|
1453
|
+
docid: readString(docId),
|
|
1454
|
+
sheet_id: readString(sheetId),
|
|
1455
|
+
fields: fields,
|
|
1456
|
+
},
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
const result = {
|
|
1460
|
+
raw: json,
|
|
1461
|
+
fields: readArray(json.fields),
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// Auto-cleanup: delete leftover default field after successfully adding new fields
|
|
1465
|
+
// This handles the case where initializeSmartTable kept 1 default field
|
|
1466
|
+
if (autoCleanupDefaultField) {
|
|
1467
|
+
await this.cleanupLeftoverDefaultField({ agent, docId, sheetId, newlyAddedFieldCount: result.fields.length });
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
return result;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Cleanup leftover default field after adding new fields
|
|
1475
|
+
* When user adds new fields, we can safely delete the leftover default field from initialization
|
|
1476
|
+
*/
|
|
1477
|
+
private async cleanupLeftoverDefaultField(params: {
|
|
1478
|
+
agent: ResolvedAgentAccount;
|
|
1479
|
+
docId: string;
|
|
1480
|
+
sheetId: string;
|
|
1481
|
+
newlyAddedFieldCount: number;
|
|
1482
|
+
}) {
|
|
1483
|
+
const { agent, docId, sheetId, newlyAddedFieldCount } = params;
|
|
1484
|
+
|
|
1485
|
+
try {
|
|
1486
|
+
// Get all fields to find the leftover default field
|
|
1487
|
+
const fieldsResult = await this.smartTableGetFields({ agent, docId, sheetId, limit: 100 });
|
|
1488
|
+
const allFields = fieldsResult.fields || [];
|
|
1489
|
+
|
|
1490
|
+
// After adding N new fields to a table with 1 default field, we should have N+1 fields
|
|
1491
|
+
// If total = newlyAdded + 1, then there's 1 leftover default field to delete
|
|
1492
|
+
if (allFields.length === newlyAddedFieldCount + 1 && newlyAddedFieldCount > 0) {
|
|
1493
|
+
// Find the field that looks like a leftover default
|
|
1494
|
+
// Default fields typically have generic titles like "文本", "数字", "日期", "单选", "人员"
|
|
1495
|
+
const defaultFieldTitles = ['文本', '数字', '日期', '单选', '人员', '文本 1', '数字 1', '日期 1', '单选 1', '人员 1'];
|
|
1496
|
+
const defaultFieldTypes = ['FIELD_TYPE_TEXT', 'FIELD_TYPE_NUMBER', 'FIELD_TYPE_DATE_TIME', 'FIELD_TYPE_SINGLE_SELECT', 'FIELD_TYPE_USER'];
|
|
1497
|
+
|
|
1498
|
+
const leftoverField = allFields.find((field: any) => {
|
|
1499
|
+
const isDefaultTitle = defaultFieldTitles.includes(field.field_title);
|
|
1500
|
+
const isDefaultType = defaultFieldTypes.includes(field.field_type);
|
|
1501
|
+
return isDefaultTitle && isDefaultType;
|
|
1502
|
+
}) as any;
|
|
1503
|
+
|
|
1504
|
+
if (leftoverField && leftoverField.field_id) {
|
|
1505
|
+
await this.smartTableDelFields({ agent, docId, sheetId, field_ids: [leftoverField.field_id] });
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
// Non-fatal: new fields added, just cleanup failed
|
|
1510
|
+
console.error(`[WecomDocClient] cleanupLeftoverDefaultField failed:`, err);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
async smartTableUpdateFields(params: {
|
|
1515
|
+
agent: ResolvedAgentAccount;
|
|
1516
|
+
docId: string;
|
|
1517
|
+
sheetId: string;
|
|
1518
|
+
fields: any[];
|
|
1519
|
+
}) {
|
|
1025
1520
|
const { agent, docId, sheetId, fields } = params;
|
|
1026
|
-
|
|
1521
|
+
|
|
1522
|
+
// Validate fields per official API spec
|
|
1523
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
1524
|
+
throw new Error("fields 必须是非空数组");
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Validate each field has required field_id and field_type
|
|
1528
|
+
fields.forEach((field: any, index: number) => {
|
|
1529
|
+
if (!field.field_id) {
|
|
1530
|
+
throw new Error(`第${index + 1}个字段:field_id 必填`);
|
|
1531
|
+
}
|
|
1532
|
+
if (!field.field_type) {
|
|
1533
|
+
throw new Error(`第${index + 1}个字段:field_type 必填`);
|
|
1534
|
+
}
|
|
1535
|
+
// field_title is optional for update, but at least one of field_title or property_* must be provided
|
|
1536
|
+
if (!field.field_title && !Object.keys(field).some(key => key.startsWith('property_'))) {
|
|
1537
|
+
throw new Error(`第${index + 1}个字段:field_title 或 property_* 属性至少提供一个`);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
const json = await this.postWecomDocApi({
|
|
1542
|
+
path: "/cgi-bin/wedoc/smartsheet/update_fields",
|
|
1543
|
+
actionLabel: "smartsheet_update_fields",
|
|
1544
|
+
agent,
|
|
1545
|
+
body: {
|
|
1546
|
+
docid: readString(docId),
|
|
1547
|
+
sheet_id: readString(sheetId),
|
|
1548
|
+
fields: fields,
|
|
1549
|
+
},
|
|
1550
|
+
});
|
|
1551
|
+
return {
|
|
1552
|
+
raw: json,
|
|
1553
|
+
fields: readArray(json.fields),
|
|
1554
|
+
};
|
|
1027
1555
|
}
|
|
1028
1556
|
|
|
1029
1557
|
async smartTableDelFields(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; field_ids: string[] }) {
|
|
1030
1558
|
const { agent, docId, sheetId, field_ids } = params;
|
|
1031
|
-
|
|
1559
|
+
|
|
1560
|
+
if (!Array.isArray(field_ids) || field_ids.length === 0) {
|
|
1561
|
+
throw new Error("field_ids 必须是非空数组");
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
return this.postWecomDocApi({
|
|
1565
|
+
path: "/cgi-bin/wedoc/smartsheet/delete_fields",
|
|
1566
|
+
actionLabel: "smartsheet_del_fields",
|
|
1567
|
+
agent,
|
|
1568
|
+
body: {
|
|
1569
|
+
docid: readString(docId),
|
|
1570
|
+
sheet_id: readString(sheetId),
|
|
1571
|
+
field_ids: field_ids,
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1032
1574
|
}
|
|
1033
1575
|
|
|
1034
|
-
async
|
|
1035
|
-
|
|
1036
|
-
|
|
1576
|
+
async smartTableGetFields(params: {
|
|
1577
|
+
agent: ResolvedAgentAccount;
|
|
1578
|
+
docId: string;
|
|
1579
|
+
sheetId: string;
|
|
1580
|
+
view_id?: string;
|
|
1581
|
+
field_ids?: string[];
|
|
1582
|
+
field_titles?: string[];
|
|
1583
|
+
offset?: number;
|
|
1584
|
+
limit?: number;
|
|
1585
|
+
}) {
|
|
1586
|
+
const { agent, docId, sheetId, view_id, field_ids, field_titles, offset, limit } = params;
|
|
1587
|
+
const payload: Record<string, unknown> = {
|
|
1588
|
+
docid: readString(docId),
|
|
1589
|
+
sheet_id: readString(sheetId),
|
|
1590
|
+
};
|
|
1591
|
+
if (view_id) payload.view_id = view_id;
|
|
1592
|
+
if (field_ids && Array.isArray(field_ids)) payload.field_ids = field_ids;
|
|
1593
|
+
if (field_titles && Array.isArray(field_titles)) payload.field_titles = field_titles;
|
|
1594
|
+
if (offset !== undefined) payload.offset = offset;
|
|
1595
|
+
if (limit !== undefined) payload.limit = limit;
|
|
1596
|
+
|
|
1597
|
+
const json = await this.postWecomDocApi({
|
|
1598
|
+
path: "/cgi-bin/wedoc/smartsheet/get_fields",
|
|
1599
|
+
actionLabel: "smartsheet_get_fields",
|
|
1600
|
+
agent,
|
|
1601
|
+
body: payload,
|
|
1602
|
+
});
|
|
1603
|
+
return {
|
|
1604
|
+
raw: json,
|
|
1605
|
+
fields: readArray(json.fields),
|
|
1606
|
+
total: json.total,
|
|
1607
|
+
has_more: json.has_more,
|
|
1608
|
+
next: json.next,
|
|
1609
|
+
};
|
|
1037
1610
|
}
|
|
1038
1611
|
|
|
1039
1612
|
async smartTableAddGroup(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; name: string; children?: string[] }) {
|
|
@@ -1066,14 +1639,123 @@ export class WecomDocClient {
|
|
|
1066
1639
|
return this.smartTableOperate({ agent, docId, operation: "update_external_records", bodyData: { sheet_id: sheetId, records } });
|
|
1067
1640
|
}
|
|
1068
1641
|
|
|
1069
|
-
async smartTableAddRecords(params: {
|
|
1070
|
-
|
|
1071
|
-
|
|
1642
|
+
async smartTableAddRecords(params: {
|
|
1643
|
+
agent: ResolvedAgentAccount;
|
|
1644
|
+
docId: string;
|
|
1645
|
+
sheetId: string;
|
|
1646
|
+
records: any[];
|
|
1647
|
+
key_type?: string;
|
|
1648
|
+
}) {
|
|
1649
|
+
const { agent, docId, sheetId, records, key_type } = params;
|
|
1650
|
+
|
|
1651
|
+
// Validate records format per official API spec (doc2.txt line 1594-1601)
|
|
1652
|
+
if (!Array.isArray(records) || records.length === 0) {
|
|
1653
|
+
throw new Error("records 必须是非空数组");
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Strict validation: require correct format based on field type
|
|
1657
|
+
// Do NOT auto-convert ambiguous values to avoid corrupting user intent
|
|
1658
|
+
const validatedRecords = records.map((record: any, recordIndex: number) => {
|
|
1659
|
+
if (!record.values || typeof record.values !== 'object' || Array.isArray(record.values)) {
|
|
1660
|
+
throw new Error(`第${recordIndex + 1}条记录:values 必须是非空对象`);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const validatedValues: Record<string, any> = {};
|
|
1664
|
+
for (const [key, value] of Object.entries(record.values)) {
|
|
1665
|
+
// Accept both array and non-array formats based on field type
|
|
1666
|
+
// Array types: TEXT, USER, SELECT, SINGLE_SELECT, CHECKBOX, PHONE_NUMBER, EMAIL, URL, LOCATION, BARCODE, ATTACHMENT, IMAGE
|
|
1667
|
+
// Non-array types: NUMBER, DATE_TIME, PROGRESS, CURRENCY, PERCENTAGE
|
|
1668
|
+
|
|
1669
|
+
if (Array.isArray(value)) {
|
|
1670
|
+
// Array format - validate structure
|
|
1671
|
+
if (value.length === 0) {
|
|
1672
|
+
throw new Error(`第${recordIndex + 1}条记录字段 "${key}": 数组不能为空`);
|
|
1673
|
+
}
|
|
1674
|
+
validatedValues[key] = value;
|
|
1675
|
+
} else if (typeof value === 'number') {
|
|
1676
|
+
// Non-array number - valid for NUMBER, PROGRESS, CURRENCY, PERCENTAGE
|
|
1677
|
+
validatedValues[key] = value;
|
|
1678
|
+
} else if (typeof value === 'string' && /^\d{13}$/.test(value)) {
|
|
1679
|
+
// Non-array 13-digit string - valid for DATE_TIME (millisecond timestamp)
|
|
1680
|
+
validatedValues[key] = value;
|
|
1681
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1682
|
+
// Object format - wrap in array for types like USER, TEXT object
|
|
1683
|
+
// This allows {user_id: "..."} to become [{user_id: "..."}]
|
|
1684
|
+
validatedValues[key] = [value];
|
|
1685
|
+
} else {
|
|
1686
|
+
// Reject ambiguous primitives (plain strings, booleans)
|
|
1687
|
+
// Users should explicitly use array format: [{type: "text", text: "..."}]
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
`第${recordIndex + 1}条记录字段 "${key}": 值格式不明确。` +
|
|
1690
|
+
`数字/日期类型直接写值 (25, "1704067200000"),` +
|
|
1691
|
+
`文本/成员/选项类型用数组 ([{"type": "text", "text": "..."}], [{"user_id": "..."}])`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return {
|
|
1697
|
+
...record,
|
|
1698
|
+
values: validatedValues,
|
|
1699
|
+
};
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
const bodyData: Record<string, unknown> = {
|
|
1703
|
+
sheet_id: readString(sheetId),
|
|
1704
|
+
records: validatedRecords,
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
if (key_type) {
|
|
1708
|
+
bodyData.key_type = key_type;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
return this.smartTableOperate({ agent, docId, operation: "add_records", bodyData });
|
|
1072
1712
|
}
|
|
1073
1713
|
|
|
1074
1714
|
async smartTableUpdateRecords(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; records: any[] }) {
|
|
1075
1715
|
const { agent, docId, sheetId, records } = params;
|
|
1076
|
-
|
|
1716
|
+
|
|
1717
|
+
// Strict validation: same as addRecords
|
|
1718
|
+
if (!Array.isArray(records) || records.length === 0) {
|
|
1719
|
+
throw new Error("records 必须是非空数组");
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const validatedRecords = records.map((record: any, recordIndex: number) => {
|
|
1723
|
+
if (!record.record_id) {
|
|
1724
|
+
throw new Error(`第${recordIndex + 1}条记录缺少 record_id`);
|
|
1725
|
+
}
|
|
1726
|
+
if (!record.values || typeof record.values !== 'object' || Array.isArray(record.values)) {
|
|
1727
|
+
throw new Error(`第${recordIndex + 1}条记录:values 必须是非空对象`);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const validatedValues: Record<string, any> = {};
|
|
1731
|
+
for (const [key, value] of Object.entries(record.values)) {
|
|
1732
|
+
if (Array.isArray(value)) {
|
|
1733
|
+
if (value.length === 0) {
|
|
1734
|
+
throw new Error(`第${recordIndex + 1}条记录字段 "${key}": 数组不能为空`);
|
|
1735
|
+
}
|
|
1736
|
+
validatedValues[key] = value;
|
|
1737
|
+
} else if (typeof value === 'number') {
|
|
1738
|
+
validatedValues[key] = value;
|
|
1739
|
+
} else if (typeof value === 'string' && /^\d{13}$/.test(value)) {
|
|
1740
|
+
validatedValues[key] = value;
|
|
1741
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1742
|
+
validatedValues[key] = [value];
|
|
1743
|
+
} else {
|
|
1744
|
+
throw new Error(
|
|
1745
|
+
`第${recordIndex + 1}条记录字段 "${key}": 值格式不明确。` +
|
|
1746
|
+
`数字/日期类型直接写值 (25, "1704067200000"),` +
|
|
1747
|
+
`文本/成员/选项类型用数组 ([{"type": "text", "text": "..."}], [{"user_id": "..."}])`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
return {
|
|
1753
|
+
...record,
|
|
1754
|
+
values: validatedValues,
|
|
1755
|
+
};
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
return this.smartTableOperate({ agent, docId, operation: "update_records", bodyData: { sheet_id: sheetId, records: validatedRecords } });
|
|
1077
1759
|
}
|
|
1078
1760
|
|
|
1079
1761
|
async smartTableDelRecords(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; record_ids: string[] }) {
|
|
@@ -1081,9 +1763,51 @@ export class WecomDocClient {
|
|
|
1081
1763
|
return this.smartTableOperate({ agent, docId, operation: "delete_records", bodyData: { sheet_id: sheetId, record_ids } });
|
|
1082
1764
|
}
|
|
1083
1765
|
|
|
1084
|
-
async smartTableGetRecords(params: {
|
|
1085
|
-
|
|
1086
|
-
|
|
1766
|
+
async smartTableGetRecords(params: {
|
|
1767
|
+
agent: ResolvedAgentAccount;
|
|
1768
|
+
docId: string;
|
|
1769
|
+
sheetId: string;
|
|
1770
|
+
view_id?: string;
|
|
1771
|
+
record_ids?: string[];
|
|
1772
|
+
key_type?: string;
|
|
1773
|
+
field_titles?: string[];
|
|
1774
|
+
field_ids?: string[];
|
|
1775
|
+
sort?: any[];
|
|
1776
|
+
offset?: number;
|
|
1777
|
+
limit?: number;
|
|
1778
|
+
ver?: number;
|
|
1779
|
+
filter_spec?: any;
|
|
1780
|
+
}) {
|
|
1781
|
+
const { agent, docId, sheetId, view_id, record_ids, key_type, field_titles, field_ids, sort, offset, limit, ver, filter_spec } = params;
|
|
1782
|
+
const payload: Record<string, unknown> = {
|
|
1783
|
+
docid: readString(docId),
|
|
1784
|
+
sheet_id: readString(sheetId),
|
|
1785
|
+
};
|
|
1786
|
+
if (view_id) payload.view_id = view_id;
|
|
1787
|
+
if (record_ids && Array.isArray(record_ids)) payload.record_ids = record_ids;
|
|
1788
|
+
if (key_type) payload.key_type = key_type;
|
|
1789
|
+
if (field_titles && Array.isArray(field_titles)) payload.field_titles = field_titles;
|
|
1790
|
+
if (field_ids && Array.isArray(field_ids)) payload.field_ids = field_ids;
|
|
1791
|
+
if (sort && Array.isArray(sort)) payload.sort = sort;
|
|
1792
|
+
if (offset !== undefined) payload.offset = offset;
|
|
1793
|
+
if (limit !== undefined) payload.limit = limit;
|
|
1794
|
+
if (ver !== undefined) payload.ver = ver;
|
|
1795
|
+
if (filter_spec && typeof filter_spec === 'object') payload.filter_spec = filter_spec;
|
|
1796
|
+
|
|
1797
|
+
const json = await this.postWecomDocApi({
|
|
1798
|
+
path: "/cgi-bin/wedoc/smartsheet/get_records",
|
|
1799
|
+
actionLabel: "smartsheet_get_records",
|
|
1800
|
+
agent,
|
|
1801
|
+
body: payload,
|
|
1802
|
+
});
|
|
1803
|
+
return {
|
|
1804
|
+
raw: json,
|
|
1805
|
+
records: readArray(json.records),
|
|
1806
|
+
total: json.total,
|
|
1807
|
+
has_more: json.has_more,
|
|
1808
|
+
next: json.next,
|
|
1809
|
+
ver: json.ver,
|
|
1810
|
+
};
|
|
1087
1811
|
}
|
|
1088
1812
|
|
|
1089
1813
|
// --- Smartsheet Content Permissions ---
|
|
@@ -1173,13 +1897,13 @@ export class WecomDocClient {
|
|
|
1173
1897
|
});
|
|
1174
1898
|
}
|
|
1175
1899
|
|
|
1176
|
-
async getDocAdvancedAccountList(params: { agent: ResolvedAgentAccount;
|
|
1177
|
-
const { agent,
|
|
1900
|
+
async getDocAdvancedAccountList(params: { agent: ResolvedAgentAccount; cursor?: number; limit?: number }) {
|
|
1901
|
+
const { agent, cursor, limit } = params;
|
|
1178
1902
|
return this.postWecomDocApi({
|
|
1179
1903
|
path: "/cgi-bin/meeting/vip/get_vip_user_list",
|
|
1180
1904
|
actionLabel: "get_advanced_account_list",
|
|
1181
1905
|
agent,
|
|
1182
|
-
body: { cursor:
|
|
1906
|
+
body: { cursor: cursor !== undefined ? String(cursor) : undefined, limit: limit ?? 100 },
|
|
1183
1907
|
});
|
|
1184
1908
|
}
|
|
1185
1909
|
|