socialdatax-skills 0.2.22 → 0.2.24

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 CHANGED
@@ -73,7 +73,7 @@ Common search phrases for this skill package:
73
73
  - Sensitive Words Check MCP server name: `sensitive-check`
74
74
  - Sensitive Words Check future MCP registry name: `com.socialdatax/sensitive-check`
75
75
  - Unified MCP registry name: none; this package installs skills and calls explicit platform services.
76
- - Current public capability version: `0.2.22`
76
+ - Current public capability version: `0.2.24`
77
77
 
78
78
  ## Direct CLI
79
79
 
@@ -87,6 +87,7 @@ npx -y socialdatax-skills@latest xhs search --keyword "露营" --since-days 7 --
87
87
  npx -y socialdatax-skills@latest xhs hot-search --pretty
88
88
  npx -y socialdatax-skills@latest xhs detail --note-id "<note_id>" --pretty
89
89
  npx -y socialdatax-skills@latest xhs comments --note-id "<note_id>" --pretty
90
+ npx -y socialdatax-skills@latest xhs comments --note-id "<note_id>" --sort-type time_descending --pretty
90
91
  npx -y socialdatax-skills@latest xhs comments --note-id "<note_id>" --all --include-replies --pretty
91
92
  npx -y socialdatax-skills@latest xhs sub-comments --note-id "<note_id>" --comment-id "<comment_id>" --pretty
92
93
  npx -y socialdatax-skills@latest xhs user-info --user-id "<user_id>" --pretty
@@ -186,6 +187,10 @@ XHS, Douyin, Kuaishou, Weibo, and WeChat Channels search use `--keyword` and opt
186
187
  Kuaishou work search uses `kuaishou search --keyword` and optional `--page-token`; Kuaishou creator search uses `kuaishou user-search --keyword` and optional `--page-token`. Kuaishou search does not accept Douyin semantic filters, and `kuaishou user-search` does not support `--since-days`.
187
188
  Weibo and WeChat Channels search use `--keyword` and optional `--page-token`.
188
189
 
190
+ XHS comments accept optional `--sort-type` values: `default`,
191
+ `time_descending`, and `like_count_descending`; omit it for the platform
192
+ default comment order.
193
+
189
194
  Douyin search filters use semantic values: `--sort-type` supports `general`,
190
195
  `time_descending`, and `like_count_descending`; `--publish-time-range` supports
191
196
  `all`, `day`, `week`, and `half_year`; `--duration-range` supports `all`,
@@ -405,8 +410,8 @@ Current Sensitive Words Check workflows include:
405
410
  | `xhs_search_notes` | Search Xiaohongshu / 小红书 notes by keyword with optional sort, note type, and publish-time filters. |
406
411
  | `xhs_get_note_detail_by_note_url` | Resolve a shared XHS link, short link, or share text into structured note details. |
407
412
  | `xhs_get_note_detail_by_note_id` | Fetch structured note details when the caller already has a note ID. |
408
- | `xhs_get_note_comments_by_note_id` | Fetch paginated first-level comments when the caller already has a note ID. |
409
- | `xhs_get_note_comments_by_note_url` | Fetch paginated first-level comments directly from a shared note URL, short link, or share text. |
413
+ | `xhs_get_note_comments_by_note_id` | Fetch paginated first-level comments when the caller already has a note ID; accepts optional comment `sort_type`. |
414
+ | `xhs_get_note_comments_by_note_url` | Fetch paginated first-level comments directly from a shared note URL, short link, or share text; accepts optional comment `sort_type`. |
410
415
  | `xhs_get_note_sub_comments_by_comment_id` | Fetch paginated replies under a first-level comment by note ID and comment ID. |
411
416
  | `xhs_get_user_info_by_user_id` | Fetch creator profile data when the caller already has a user ID. |
412
417
  | `xhs_get_user_info_by_profile_url` | Resolve a profile link, short link, or share text into creator profile data. |
package/cli.mjs CHANGED
@@ -17,7 +17,7 @@ import { dirname, join, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
19
  const PACKAGE_NAME = "socialdatax-skills";
20
- const PACKAGE_VERSION = "0.2.22";
20
+ const PACKAGE_VERSION = "0.2.24";
21
21
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
22
22
  const LOG_PREFIX = `[${PACKAGE_NAME}]`;
23
23
  const MIN_NODE_VERSION = "20.18.1";
@@ -29,6 +29,37 @@ const MEDIA_DOWNLOAD_TIMEOUT_MS = 10 * 60 * 1000;
29
29
  const MEDIA_DOWNLOAD_MAX_BYTES = 2 * 1024 * 1024 * 1024;
30
30
  const MEDIA_DECRYPT_CHUNK_SIZE = 64 * 1024;
31
31
  const MEDIA_DOWNLOAD_MAX_REDIRECTS = 5;
32
+ const MP4_BOX_HEADER_SIZE = 8;
33
+ const MP4_EXTENDED_BOX_HEADER_SIZE = 16;
34
+ const MP4_MAX_BOX_SCAN_DEPTH = 16;
35
+ const MP4_FULL_BOX_HEADER_SIZE = 4;
36
+ const MP4_STSD_ENTRY_COUNT_SIZE = 4;
37
+ const MP4_VISUAL_SAMPLE_ENTRY_HEADER_SIZE = 78;
38
+ const MP4_VVC_NAL_LENGTH_SIZE_BYTE_OFFSET = 4;
39
+ const MP4_CONTAINER_BOX_TYPES = new Set([
40
+ "dinf",
41
+ "edts",
42
+ "mdia",
43
+ "meta",
44
+ "minf",
45
+ "moof",
46
+ "moov",
47
+ "mfra",
48
+ "stbl",
49
+ "traf",
50
+ "trak",
51
+ "udta",
52
+ ]);
53
+ const MP4_VISUAL_SAMPLE_ENTRY_TYPES = new Set([
54
+ "av01",
55
+ "avc1",
56
+ "avc3",
57
+ "encv",
58
+ "hev1",
59
+ "hvc1",
60
+ "vvc1",
61
+ "vvi1",
62
+ ]);
32
63
  // Keep this local helper scoped to WeChat media host trees; require k for the decrypt key.
33
64
  const WECHAT_MEDIA_DOWNLOAD_HOST_SUFFIXES = [
34
65
  ".wxapp.tc.qq.com",
@@ -146,6 +177,7 @@ const XHS_DIRECT_ACTION_OPTIONS = {
146
177
  "all",
147
178
  "maxItems",
148
179
  "includeReplies",
180
+ "sortType",
149
181
  "pretty",
150
182
  ],
151
183
  "sub-comments": [
@@ -197,6 +229,11 @@ const XHS_SEARCH_SORT_TYPES = [
197
229
  "comment_count_descending",
198
230
  "collect_count_descending",
199
231
  ];
232
+ const XHS_COMMENT_SORT_TYPES = [
233
+ "default",
234
+ "time_descending",
235
+ "like_count_descending",
236
+ ];
200
237
  const XHS_LEGACY_SEARCH_SORT_TYPE_ALIASES = {
201
238
  popularity_descending: "like_count_descending",
202
239
  comment_descending: "comment_count_descending",
@@ -521,12 +558,13 @@ const PLATFORMS = {
521
558
  },
522
559
  {
523
560
  name: "xhs_get_note_comments_by_note_id",
524
- description: "Fetch paginated first-level comments by note ID.",
561
+ description:
562
+ "Fetch paginated first-level comments by note ID with optional sort.",
525
563
  },
526
564
  {
527
565
  name: "xhs_get_note_comments_by_note_url",
528
566
  description:
529
- "Fetch paginated first-level comments from a note URL, short link, or share text.",
567
+ "Fetch paginated first-level comments from a note URL, short link, or share text with optional sort.",
530
568
  },
531
569
  {
532
570
  name: "xhs_get_note_sub_comments_by_comment_id",
@@ -2061,6 +2099,8 @@ function printHelp() {
2061
2099
  console.log(" For comments commands, also fetch nested second-level replies for each returned first-level comment.");
2062
2100
  console.log(" --sort-type <general|time_descending|like_count_descending|comment_count_descending|collect_count_descending>");
2063
2101
  console.log(" XHS sort meanings: general=default, time_descending=newest, like_count_descending=most liked, comment_count_descending=most commented, collect_count_descending=most collected.");
2102
+ console.log(" --sort-type <default|time_descending|like_count_descending>");
2103
+ console.log(" XHS comments sort meanings: default=platform default, time_descending=newest, like_count_descending=most liked.");
2064
2104
  console.log(" --note-type <all|image|video> XHS search note type filter; default is all.");
2065
2105
  console.log(" --publish-time-range <all|day|week|half_year>");
2066
2106
  console.log(" XHS search publish-time filter; default is all.");
@@ -2355,6 +2395,7 @@ function buildXhsOperation(action, options) {
2355
2395
  idDisplay: "--note-id",
2356
2396
  urlDisplay: "--url",
2357
2397
  pageToken: options.pageToken,
2398
+ extraArguments: buildXhsCommentsExtraArguments(options),
2358
2399
  })
2359
2400
  );
2360
2401
  case "sub-comments":
@@ -2936,6 +2977,7 @@ export async function decryptWechatMediaCommand(
2936
2977
  encryptedLength,
2937
2978
  decodeKey,
2938
2979
  });
2980
+ await patchWechatVvcMp4NalLengthMetadata(tempPath);
2939
2981
  decrypted = true;
2940
2982
  }
2941
2983
  await rename(tempPath, outputPath);
@@ -3185,6 +3227,158 @@ async function decryptWechatMediaFilePrefix(
3185
3227
  }
3186
3228
  }
3187
3229
 
3230
+ async function patchWechatVvcMp4NalLengthMetadata(filePath) {
3231
+ const file = await openFile(filePath, "r+");
3232
+ try {
3233
+ const stats = await file.stat();
3234
+ await patchMp4ChildBoxes(file, 0, stats.size, 0);
3235
+ } finally {
3236
+ await file.close();
3237
+ }
3238
+ }
3239
+
3240
+ async function patchMp4ChildBoxes(file, startOffset, endOffset, depth) {
3241
+ if (
3242
+ depth > MP4_MAX_BOX_SCAN_DEPTH ||
3243
+ startOffset < 0 ||
3244
+ endOffset - startOffset < MP4_BOX_HEADER_SIZE
3245
+ ) {
3246
+ return;
3247
+ }
3248
+
3249
+ let offset = startOffset;
3250
+ while (offset + MP4_BOX_HEADER_SIZE <= endOffset) {
3251
+ const box = await readMp4BoxHeader(file, offset, endOffset);
3252
+ if (!box) {
3253
+ return;
3254
+ }
3255
+ if (box.type === "vvcC") {
3256
+ await patchVvcConfigurationNalLengthSize(file, box);
3257
+ } else {
3258
+ const childRange = mp4ChildBoxRange(box);
3259
+ if (childRange) {
3260
+ await patchMp4ChildBoxes(
3261
+ file,
3262
+ childRange.startOffset,
3263
+ childRange.endOffset,
3264
+ depth + 1
3265
+ );
3266
+ }
3267
+ }
3268
+ offset = box.endOffset;
3269
+ }
3270
+ }
3271
+
3272
+ async function readMp4BoxHeader(file, offset, parentEndOffset) {
3273
+ const baseHeader = Buffer.allocUnsafe(MP4_BOX_HEADER_SIZE);
3274
+ const baseRead = await file.read(
3275
+ baseHeader,
3276
+ 0,
3277
+ MP4_BOX_HEADER_SIZE,
3278
+ offset
3279
+ );
3280
+ if (baseRead.bytesRead !== MP4_BOX_HEADER_SIZE) {
3281
+ return undefined;
3282
+ }
3283
+
3284
+ const size32 = baseHeader.readUInt32BE(0);
3285
+ const type = baseHeader.toString("ascii", 4, 8);
3286
+ let headerSize = MP4_BOX_HEADER_SIZE;
3287
+ let size;
3288
+ if (size32 === 0) {
3289
+ size = parentEndOffset - offset;
3290
+ } else if (size32 === 1) {
3291
+ if (offset + MP4_EXTENDED_BOX_HEADER_SIZE > parentEndOffset) {
3292
+ return undefined;
3293
+ }
3294
+ const extendedSizeBuffer = Buffer.allocUnsafe(8);
3295
+ const extendedRead = await file.read(
3296
+ extendedSizeBuffer,
3297
+ 0,
3298
+ 8,
3299
+ offset + MP4_BOX_HEADER_SIZE
3300
+ );
3301
+ if (extendedRead.bytesRead !== 8) {
3302
+ return undefined;
3303
+ }
3304
+ const extendedSize = extendedSizeBuffer.readBigUInt64BE(0);
3305
+ if (extendedSize > BigInt(Number.MAX_SAFE_INTEGER)) {
3306
+ return undefined;
3307
+ }
3308
+ size = Number(extendedSize);
3309
+ headerSize = MP4_EXTENDED_BOX_HEADER_SIZE;
3310
+ } else {
3311
+ size = size32;
3312
+ }
3313
+
3314
+ if (
3315
+ size < headerSize ||
3316
+ offset + size > parentEndOffset ||
3317
+ !isMp4BoxType(type)
3318
+ ) {
3319
+ return undefined;
3320
+ }
3321
+
3322
+ return {
3323
+ type,
3324
+ startOffset: offset,
3325
+ headerSize,
3326
+ payloadOffset: offset + headerSize,
3327
+ endOffset: offset + size,
3328
+ };
3329
+ }
3330
+
3331
+ function isMp4BoxType(type) {
3332
+ return /^[\x20-\x7e]{4}$/.test(type);
3333
+ }
3334
+
3335
+ function mp4ChildBoxRange(box) {
3336
+ if (box.type === "stsd") {
3337
+ const childStart =
3338
+ box.payloadOffset + MP4_FULL_BOX_HEADER_SIZE + MP4_STSD_ENTRY_COUNT_SIZE;
3339
+ return childStart < box.endOffset
3340
+ ? { startOffset: childStart, endOffset: box.endOffset }
3341
+ : undefined;
3342
+ }
3343
+ if (box.type === "meta") {
3344
+ const childStart = box.payloadOffset + MP4_FULL_BOX_HEADER_SIZE;
3345
+ return childStart < box.endOffset
3346
+ ? { startOffset: childStart, endOffset: box.endOffset }
3347
+ : undefined;
3348
+ }
3349
+ if (MP4_VISUAL_SAMPLE_ENTRY_TYPES.has(box.type)) {
3350
+ const childStart = box.payloadOffset + MP4_VISUAL_SAMPLE_ENTRY_HEADER_SIZE;
3351
+ return childStart < box.endOffset
3352
+ ? { startOffset: childStart, endOffset: box.endOffset }
3353
+ : undefined;
3354
+ }
3355
+ if (MP4_CONTAINER_BOX_TYPES.has(box.type)) {
3356
+ return box.payloadOffset < box.endOffset
3357
+ ? { startOffset: box.payloadOffset, endOffset: box.endOffset }
3358
+ : undefined;
3359
+ }
3360
+ return undefined;
3361
+ }
3362
+
3363
+ async function patchVvcConfigurationNalLengthSize(file, box) {
3364
+ const targetOffset =
3365
+ box.payloadOffset + MP4_VVC_NAL_LENGTH_SIZE_BYTE_OFFSET;
3366
+ if (targetOffset >= box.endOffset) {
3367
+ return;
3368
+ }
3369
+ const buffer = Buffer.allocUnsafe(1);
3370
+ const { bytesRead } = await file.read(buffer, 0, 1, targetOffset);
3371
+ if (bytesRead !== 1) {
3372
+ return;
3373
+ }
3374
+ const patched = (buffer[0] & 0xf8) | 0x07;
3375
+ if (patched === buffer[0]) {
3376
+ return;
3377
+ }
3378
+ buffer[0] = patched;
3379
+ await writeAllToFile(file, buffer, targetOffset);
3380
+ }
3381
+
3188
3382
  function isAbortLikeError(error) {
3189
3383
  return error?.name === "AbortError" || error?.name === "TimeoutError";
3190
3384
  }
@@ -3500,6 +3694,7 @@ function buildOneOfCall(
3500
3694
  idDisplay,
3501
3695
  urlDisplay,
3502
3696
  pageToken,
3697
+ extraArguments = {},
3503
3698
  }
3504
3699
  ) {
3505
3700
  const idValue = options[idOption];
@@ -3516,9 +3711,23 @@ function buildOneOfCall(
3516
3711
  if (pageToken) {
3517
3712
  toolArguments.page_token = pageToken;
3518
3713
  }
3714
+ Object.assign(toolArguments, extraArguments);
3519
3715
  return { tool, toolArguments };
3520
3716
  }
3521
3717
 
3718
+ function buildXhsCommentsExtraArguments(options) {
3719
+ const toolArguments = {};
3720
+ if (options.sortType !== undefined) {
3721
+ toolArguments.sort_type = parseAllowedStringOption(
3722
+ options.sortType,
3723
+ "--sort-type",
3724
+ XHS_COMMENT_SORT_TYPES,
3725
+ XHS_COMMENT_SORT_TYPES.join(", ")
3726
+ );
3727
+ }
3728
+ return toolArguments;
3729
+ }
3730
+
3522
3731
  function buildTranscriptCall(
3523
3732
  options,
3524
3733
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socialdatax-skills",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "description": "SocialDataX / 社媒数据助手 skill installer and direct CLI helper for hosted platform social media MCP services.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -142,6 +142,7 @@ npx -y socialdatax-skills@latest wechat replies \
142
142
  Optional arguments:
143
143
 
144
144
  - XHS `--note-id <note_id>`: use the complete 24-character lowercase hexadecimal `note_id` returned from search, detail, comments, or creator note lists; do not pass only a prefix.
145
+ - XHS comments `--sort-type <default|time_descending|like_count_descending>`: optional first-level comment sort order; omit it for the platform default order.
145
146
  - Douyin `--aweme-id <aweme_id>`: preferred when the video ID is already known and should anchor the comment thread.
146
147
  - `--url <url_or_share_text>`: use for a content page URL, short link, or share text for first-level comments.
147
148
  - Douyin URL safety: do not pass `video.play_url`; use a Douyin content page URL, short link, or share text instead.
@@ -188,8 +189,8 @@ MCP tools matching the direct CLI commands above:
188
189
  - `wechat_get_video_comment_replies_by_comment_id`
189
190
 
190
191
  If MCP tools are already available in the current agent, use one of these tools:
191
- - `xhs_get_note_comments_by_note_id`: use when the complete 24-character lowercase hexadecimal `note_id` is known; do not pass only a prefix.
192
- - `xhs_get_note_comments_by_note_url`: use for note URLs, short links, or share text.
192
+ - `xhs_get_note_comments_by_note_id`: use when the complete 24-character lowercase hexadecimal `note_id` is known; do not pass only a prefix; optional `sort_type` accepts `default`, `time_descending`, or `like_count_descending`.
193
+ - `xhs_get_note_comments_by_note_url`: use for note URLs, short links, or share text; optional `sort_type` accepts `default`, `time_descending`, or `like_count_descending`.
193
194
  - `xhs_get_note_sub_comments_by_comment_id`: use when the complete 24-character lowercase hexadecimal `note_id` and first-level comment ID are known; do not pass only a note ID prefix.
194
195
  - `douyin_get_video_comments_by_aweme_id`: use when the aweme_id is known.
195
196
  - `douyin_get_video_comments_by_url`: use for Douyin content page URLs, short links, or share text; do not pass playback URLs such as `video.play_url`.
@@ -436,7 +436,8 @@ Optional arguments:
436
436
  - Kuaishou `user-search` supports `--pages <n>` and `--max-items <n>` for bounded creator candidate discovery; it does not support `--since-days`.
437
437
  - `--since-days <1-365>`: keep search or creator content-list items whose public `publish_time` is within the last N days. Search stays bounded by `--pages`; creator content lists continue until the publish-time boundary when `--pages` is omitted.
438
438
  - `--include-replies`: for first-level comments, also fetch nested second-level replies under each returned comment.
439
- - `--sort-type`, `--note-type`, `--publish-time-range`, `--duration-range`, and `--content-type`: apply only to XHS, Douyin, or WeChat Channels keyword research commands when the user asks for filtering; Kuaishou and Weibo search currently use `--keyword` and optional `--page-token`.
439
+ - XHS comments `--sort-type <default|time_descending|like_count_descending>`: optional first-level comment sort order; omit it for the platform default order.
440
+ - Keyword research filters: use the documented XHS, Douyin, and WeChat Channels search `--sort-type` values; `--note-type`, `--publish-time-range`, `--duration-range`, and `--content-type` apply only where supported. Kuaishou and Weibo search currently use `--keyword` and optional `--page-token`.
440
441
  - `--comment-id`: required for reply/sub-comment commands together with the content ID.
441
442
  - `--pretty`: output formatting only.
442
443
  - `--source-client socialdatax-skills --source-platform npm --source-skill socialdatax-content-research-assistant`: usage attribution for this Agent Skill; keep these values unchanged when running examples from this Skill.