@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.
Files changed (49) hide show
  1. package/README.md +294 -379
  2. package/SKILLS_CAL.md +895 -0
  3. package/SKILLS_DOC.md +2288 -0
  4. package/changelog/v2.3.18.md +22 -0
  5. package/changelog/v2.3.19.md +73 -0
  6. package/index.ts +39 -3
  7. package/package.json +2 -3
  8. package/src/agent/handler.event-filter.test.ts +11 -0
  9. package/src/agent/handler.ts +732 -643
  10. package/src/app/account-runtime.ts +46 -20
  11. package/src/app/index.ts +20 -1
  12. package/src/capability/bot/stream-orchestrator.ts +1 -1
  13. package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
  14. package/src/capability/calendar/client.ts +815 -0
  15. package/src/capability/calendar/index.ts +3 -0
  16. package/src/capability/calendar/schema.ts +417 -0
  17. package/src/capability/calendar/tool.ts +417 -0
  18. package/src/capability/calendar/types.ts +309 -0
  19. package/src/capability/doc/client.ts +788 -64
  20. package/src/capability/doc/schema.ts +419 -318
  21. package/src/capability/doc/tool.ts +1517 -1178
  22. package/src/capability/doc/types.ts +130 -14
  23. package/src/capability/mcp/index.ts +10 -0
  24. package/src/capability/mcp/schema.ts +107 -0
  25. package/src/capability/mcp/tool.ts +170 -0
  26. package/src/capability/mcp/transport.ts +394 -0
  27. package/src/channel.ts +70 -28
  28. package/src/config/index.ts +7 -1
  29. package/src/config/media.test.ts +113 -0
  30. package/src/config/media.ts +133 -6
  31. package/src/config/schema.ts +74 -102
  32. package/src/outbound.test.ts +250 -15
  33. package/src/outbound.ts +155 -30
  34. package/src/runtime/reply-orchestrator.test.ts +35 -2
  35. package/src/runtime/reply-orchestrator.ts +14 -2
  36. package/src/runtime/routing-bridge.test.ts +115 -0
  37. package/src/runtime/routing-bridge.ts +26 -1
  38. package/src/runtime/session-manager.ts +20 -6
  39. package/src/runtime/source-registry.ts +165 -0
  40. package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
  41. package/src/transport/bot-ws/media.test.ts +44 -0
  42. package/src/transport/bot-ws/media.ts +272 -0
  43. package/src/transport/bot-ws/reply.test.ts +216 -18
  44. package/src/transport/bot-ws/reply.ts +116 -21
  45. package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
  46. package/src/transport/bot-ws/sdk-adapter.ts +89 -12
  47. package/src/types/config.ts +3 -0
  48. package/.claude/settings.local.json +0 -11
  49. 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
- return {
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
- 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[];
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
- // 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 }));
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/create_collect",
567
- actionLabel: "create_collect",
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/modify_collect",
662
- actionLabel: "modify_collect",
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, batchMode } = params;
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
- const body: Record<string, unknown> = {
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
- body.version = Number(version);
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
- const json = await this.postWecomDocApi({
779
- path: "/cgi-bin/wedoc/document/batch_update",
780
- actionLabel: "update_doc_content",
781
- agent,
782
- body,
783
- }) as BatchUpdateDocResponse;
784
- return { raw: json };
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?.rows || []).map((row: any) => ({
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
- // Otherwise wrap primitive as CellValue
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: body.docid as string,
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: { docid: readString(docId) },
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: { agent: ResolvedAgentAccount; docId: string; sheetId: string; view_title: string; view_type: string; property_gantt?: any; property_calendar?: any }) {
1010
- const { agent, docId, sheetId, view_title, view_type, property_gantt, property_calendar } = params;
1011
- return this.smartTableOperate({ agent, docId, operation: "add_view", bodyData: { sheet_id: sheetId, view_title, view_type, property_gantt, property_calendar } });
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: { agent: ResolvedAgentAccount; docId: string; sheetId: string; view_id: string; view_title?: string; property_gantt?: any; property_calendar?: any }) {
1015
- const { agent, docId, sheetId, view_id, view_title, property_gantt, property_calendar } = params;
1016
- return this.smartTableOperate({ agent, docId, operation: "update_view", bodyData: { sheet_id: sheetId, view_id, view_title, property_gantt, property_calendar } });
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
- return this.smartTableOperate({ agent, docId, operation: "delete_views", bodyData: { sheet_id: sheetId, view_ids } });
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 smartTableAddFields(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; fields: any[] }) {
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
- return this.smartTableOperate({ agent, docId, operation: "add_fields", bodyData: { sheet_id: sheetId, fields } });
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
- return this.smartTableOperate({ agent, docId, operation: "delete_fields", bodyData: { sheet_id: sheetId, field_ids } });
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 smartTableUpdateFields(params: { agent: ResolvedAgentAccount; docId: string; sheetId: string; fields: any[] }) {
1035
- const { agent, docId, sheetId, fields } = params;
1036
- return this.smartTableOperate({ agent, docId, operation: "update_fields", bodyData: { sheet_id: sheetId, fields } });
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: { agent: ResolvedAgentAccount; docId: string; sheetId: string; records: any[] }) {
1070
- const { agent, docId, sheetId, records } = params;
1071
- return this.smartTableOperate({ agent, docId, operation: "add_records", bodyData: { sheet_id: sheetId, records } });
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
- return this.smartTableOperate({ agent, docId, operation: "update_records", bodyData: { sheet_id: sheetId, records } });
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: { agent: ResolvedAgentAccount; docId: string; sheetId: string; record_ids?: string[]; offset?: number; limit?: number }) {
1085
- const { agent, docId, sheetId, record_ids, offset, limit } = params;
1086
- return this.smartTableOperate({ agent, docId, operation: "get_records", bodyData: { sheet_id: sheetId, record_ids, offset, limit } });
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; offset?: number; limit?: number }) {
1177
- const { agent, offset, limit } = params;
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: offset ? String(offset) : undefined, limit: limit ?? 100 },
1906
+ body: { cursor: cursor !== undefined ? String(cursor) : undefined, limit: limit ?? 100 },
1183
1907
  });
1184
1908
  }
1185
1909