Skill: um-why/xhs-content-analytics (version 1.0.1)

--- name: xhs-content-analytics description: 小红书自动化运营工具-支持：搜索笔记、查看笔记详情及评论。 当用户提及 xiaohongshu、小红书、RedNote，或需要在该平台进行内容调研时使用。 license: MIT metadata: version: "1.0.1" category: - "Data&APIs" - "数据分析" tags: - "小红书关键词搜索" - "小红书笔记详情查询" - "小红书评论抓取" - "小红书内容排序筛选" - "小红书爆款选题" - "小红书竞品监控" - "小红书营销报告制作" - "小红书热点追踪" - "无需登录" capabilityBoundary: - "支持小红书公开内容抓取，不支持私密笔记/付费内容" - "评论获取上限60000条，搜索结果上限60条" - "无需登录小红书账号，无封号/风控风险" examples: - scenario: "找露营装备类高赞爆款视频笔记（内容选题）" command: "node src/xiaohongshu/search-cli.js -k 露营装备 -t 1 -s 2 -l 10" paramExplain: "-k=露营装备（搜索关键词）、-t=1（仅视频）、-s=2（最多点赞）、-l=10（返回10条）" outputExpect: "输出10条露营装备高赞视频笔记，含标题、链接、点赞/收藏数等核心数据" - scenario: "分析单篇笔记评论区情绪（用户反馈分析）" command: "node src/xiaohongshu/detail-cli.js -u 'https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy'" paramExplain: "-u=笔记PC链接（必填），默认获取6条评论" outputExpect: "返回笔记基础信息+6条评论内容，支持情绪分析" - scenario: "导出AI图文笔记收藏排序数据（营销报告）" command: "node src/xiaohongshu/search-cli.js --keyword AI --type 2 --sort 4 --limit 20 --output json" paramExplain: "--keyword=AI、--type=2（图文）、--sort=4（最多收藏）、--limit=20、--output=json" outputExpect: "生成JSON文件，含20条AI图文笔记全量数据，可用于报告可视化" --- # 📊 小红书内容分析 - 获取小红书内容表现数据 > **💡一句话价值**：一键搜索小红书笔记、互动数据，帮你做爆款选题、竞品分析。 > > **🔥核心优势** > > - 轻量: 无需部署服务，OpenClaw一键运行 > - 灵活: 支持多维度筛选、批量操作、多格式导出 > - 实用: 日志自动归档，适配营销报告 / 内容策划场景 > - 安全: 无需登录你的小红书账号，不担心风控风险 / 封号问题 ## 1. ✅ 我能帮你解决什么（10 秒判断） - 🔍 按关键词搜小红书笔记（最新/最多点赞/最多评论/最多收藏排序）：找爆款选题、分析高赞笔记规律 - 🦸 竞品监控：批量抓取对标账号所有公开作品数据，分析内容策略 - 📡 热点追流：实时获取小红书热搜，抢占流量风口 - 📊 数据导出：自动生成JSON日志，方便二次使用 ## 2. 🚀 最快上手（复制就能跑，30 秒出结果） > Note: 请先通过微信 <13395823479> 申请 API TOKEN，配置环境变量 GUAIKEI_API_TOKEN 后才能正常运行。 ### 2.1 🔎 小红书关键词搜索 ```bash node src/xiaohongshu/search-cli.js -k 数据分析 ``` ### 2.2 🔎 按点赞排序找爆款 ```bash node src/xiaohongshu/search-cli.js --keyword "AI" --sort 2 ``` ### 2.3 🔎 按最新排序找风口 ```bash node src/xiaohongshu/search-cli.js --keyword "AI" --sort 1 ``` ### 2.4 📕 小红书笔记详情查询 ```bash node src/xiaohongshu/detail-cli.js -u "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" ``` ## 3. 📌 适用场景（我该不该用？） - 你需要做小红书笔记选题 → 关键词搜索 + 点赞排序 - 你需要模仿爆款文案 → 查看高赞笔记详情 - 你需要做营销报告 → 导出结构化数据 ## 4. 🔧 参数详解表 ### 🔎 小红书关键词搜索 | 参数 | 缩写 | 作用 | 可选值 | 必填 | | :---------- | :--: | :--------- | :--------------------------------------------------------------- | :--: | | `--keyword` | `-k` | 搜索关键词 | 2-50 个汉字 | 是 | | `--type` | `-t` | 内容类型 | 0 = 全部 / 1 = 视频 / 2 = 图文 | 否 | | `--sort` | `-s` | 排序方式 | 0 = 综合 / 1 = 最新 / 2 = 最多点赞 / 3 = 最多评论 / 4 = 最多收藏 | 否 | | `--limit` | `-l` | 搜索数量 | 1-60 条 | 否 | | `--output` | `-o` | 输出格式 | json / markdown | 否 | ### 📕 小红书笔记详情获取 | 参数 | 缩写 | 类型 | 说明 | | :-------- | :--- | :------ | :----------------------- | | `--url` | `-u` | 字符串 | 必填，笔记链接 | | `--limit` | `-l` | 1-60000 | 获取笔记评论数量 (默认6) | > **💡"笔记链接"说明** > > - PC链接: <https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy> > - 短链接: <https://xhslink.com/m/xxx> ## 5. ⚠️ 重要限制（不踩坑） - 仅抓取抖音公开数据，不支持私密 / 隐藏内容 - 需要配置 GUAIKEI_API_TOKEN 才能正常运行 - 数据仅限个人 / 团队内部使用，禁止违规分发 ## 6. ❓ 常见问题（秒解决） > **💡Q：运行报错，提示无权限？** > > A：先配置环境变量：`set GUAIKEI_API_TOKEN=你的TOKEN` > > - 私有TOKEN申请后请留意使用安全，避免泄露给他人 > > **💡Q：输出文件在哪里？** > > A：自动保存在技能目录的 `logs` 文件夹下 > > - 搜索任务日志: 默认保存为「时间戳*关键词*类型*排序*数量\_search.json」 > - 详情任务日志: 默认保存为「时间戳\_笔记ID_detail.json」 > > **💡Q：支持 Windows/Mac/Linux 吗？** > > A：全平台支持，仅需安装 Node.js 环境

path: src/xiaohongshu/detail-cli.js
content: #!/usr/bin/env node

const constants = require("../config/constants");
const detail = require("../api/detail");
const log = require("../utils/log");
const key = require("../utils/key");
const utils = require("../utils/utils");
const validator = require("../validate/url");

function printHelp() {
  console.log(`
用法: node src/xiaohongshu/detail-cli.js <笔记链接>

示例: node src/xiaohongshu/detail-cli.js "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy"

注意: 
  - 笔记链接是小红书可公开访问的笔记链接
  - 即使用 "node src/xiaohongshu/search-cli.js <关键词>" 获取到的出参 url 值所代表的笔记链接
  - 请确保环境变量 GUAIKEI_API_TOKEN 已配置
`);
}

async function main() {
  const startTime = Date.now();
  const args = process.argv.slice(2);
  if (args.length === 0) {
    printHelp();
    return;
  }

  let url = "";
  args.forEach((arg, index) => {
    if (arg === "--url" || arg === "-u") {
      url = args[index + 1] || "";
    } else if (arg === "--help" || arg === "-h") {
      printHelp();
      process.exit(0);
    } else if (arg.startsWith("-") === false && url === "") {
      url = arg;
    }
  });
  if (url === "") {
    utils.printError(`未提供笔记链接`);
    printHelp();
    return;
  }

  utils.printBanner();
  utils.printInfo(`笔记链接: ${url}`);
  const isRight = validator.isXiaohongshuNoteUrl(url);
  if (!isRight) {
    utils.printError(
      `笔记链接格式无效, 支持: https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy, http://xhslink.com/m/xxx`,
    );
    return;
  }

  const [REDACTED_SECRET]);
  let detailTask = null;
  try {
    const status = await detail.createDetailTask(token, url);
    if (status.errcode !== 0) {
      throw new Error(
        `详情任务创建时, 遇到未知错误, 请反馈给开发者 ${status} - ${Date.now()}`,
      );
    }
    utils.printSuccess(`详情任务创建成功, 正在搜索中...`);

    detailTask = await detail.getDetailTask(token, url);
  } catch (error) {
    const errorOutput = {
      status: "error",
      url: url,
      message: error.message,
      error_code: error.code || "UNKNOWN",
      timestamp: new Date().toLocaleString(),
      results: [],
    };
    console.log(JSON.stringify(errorOutput, null, 2));
    return;
  }
  if (!detailTask) {
    utils.printError(`详情任务没有返回结果, 请稍后重试或联系开发者`);
    const emptyOutput = {
      status: "empty",
      url: url,
      message: "没有找到匹配的笔记内容",
      error_code: "NOT_FOUND",
      timestamp: new Date().toLocaleString(),
      results: [],
    };
    console.log(JSON.stringify(emptyOutput, null, 2));
    return;
  }
  // 输出搜索结果
  const finalOutput = {
    status: "success",
    url: url,
    message: "详情任务完成",
    timestamp: new Date().toLocaleString(),
    openclaw_metadata: {
      skill_version: constants.VERSION,
      runtime_version: process.versions.node,
      execution_time: Date.now() - startTime,
    },
    results: detailTask,
  };
  console.log(JSON.stringify(finalOutput, null, 2));
  utils.printSuccess(`详情任务完成, 已返回结果`);

  await log.taskWrite(
    `${startTime}_${validator.url2Name(url)}_detail.json`,
    JSON.stringify(finalOutput, null, 2),
  );
}

main().catch((error) => {
  utils.printError(error);
  process.exit(1);
});


path: src/xiaohongshu/search-cli.js
content: #!/usr/bin/env node

const constants = require("../config/constants");
const key = require("../utils/key");
const log = require("../utils/log");
const search = require("../api/search");
const utils = require("../utils/utils");
const validator = require("../validate/keyword");

function printHelp() {
  console.log(`
用法: node src/xiaohongshu/search-cli.js <关键词> [选项]

选项:
  --keyword -k \t<关键词> \t搜索关键词
  --type -t \t<类型> \t内容类型, 0: 全部(默认), 1: 视频, 2: 图文
  --sort -s \t<排序> \t排序规则, 0: 综合(默认), 1: 最新, 2: 最多点赞, 3: 最多评论, 4: 最多收藏
  --limit -l \t<数量> \t搜索数量 (默认 10, 最大 60)
  --output -o \t<格式> \t输出格式, json, markdown (默认 json)
  --help -h \t显示帮助信息

示例1: node src/xiaohongshu/search-cli.js AI
示例2: node src/xiaohongshu/search-cli.js "AI 模型"
示例3: node src/xiaohongshu/search-cli.js --keyword AI --type 0 --sort 0 --limit 10 --output json
示例4: node src/xiaohongshu/search-cli.js --keyword "AI 模型" --type 1 --sort 2 --limit 20 --output markdown

注意: 
  - 关键词建议 2-50 个汉字，避免特殊符号
  - 请确保环境变量 GUAIKEI_API_TOKEN 已配置
  - 所有参数都会自动清洗和验证
`);
}

async function main() {
  const startTime = Date.now();
  const args = process.argv.slice(2);
  if (args.length === 0) {
    printHelp();
    return;
  }

  let keyword = "",
    type = 0,
    sort = 0,
    limit = 10,
    output = "json";
  args.forEach((arg, index) => {
    if (arg === "--keyword" || arg === "-k") {
      keyword = args[index + 1] || "";
    } else if (arg === "--type" || arg === "-t") {
      type = args[index + 1] || 0;
      type = Number(type);
    } else if (arg === "--sort" || arg === "-s") {
      sort = args[index + 1] || 0;
      sort = Number(sort);
    } else if (arg === "--limit" || arg === "-l") {
      limit = args[index + 1] || 10;
      limit = Number(limit);
    } else if (arg === "--output" || arg === "-o") {
      output = args[index + 1] || "json";
    } else if (arg === "--help" || arg === "-h") {
      printHelp();
      process.exit(0);
    } else if (arg.startsWith("-") === false && keyword === "") {
      keyword = arg;
    }
  });
  if (keyword === "") {
    utils.printError(`未提供关键词`);
    printHelp();
    return;
  }

  utils.printBanner();
  utils.printInfo(`原始关键词: ${keyword}`);
  const isRight = validator.isKeywordValid(keyword);
  if (!isRight) {
    return;
  }
  keyword = validator.cleanKeyword(keyword);
  utils.printInfo(`清洗后关键词: ${keyword}`);

  [type, sort, limit, output] = validator.optionFormat(
    type,
    sort,
    limit,
    output,
  );
  utils.printInfo(
    `内容类型: ${type}, 排序规则: ${sort}, 数量: ${limit}, 输出格式: ${output}`,
  );

  const [REDACTED_SECRET]);
  let searchTask = null;
  try {
    const status = await search.createSearchTask(
      token,
      keyword,
      type,
      sort,
      limit,
    );
    if (status.errcode !== 0) {
      throw new Error(
        `搜索任务创建时, 遇到未知错误, 请反馈给开发者 ${status} - ${Date.now()}`,
      );
    }
    utils.printSuccess(`搜索任务创建成功, 正在搜索中...`);

    searchTask = await search.getSearchTask(token, keyword, type, sort, limit);
  } catch (error) {
    const errorOutput = {
      status: "error",
      keyword: keyword,
      message: error.message,
      error_code: error.code || "UNKNOWN",
      type: type,
      sort: sort,
      limit: limit,
      output_format: output,
      timestamp: new Date().toLocaleString(),
      results: [],
    };
    console.log(JSON.stringify(errorOutput, null, 2));
    return;
  }
  if (!searchTask || !Array.isArray(searchTask) || searchTask.length === 0) {
    utils.printError(`搜索任务没有返回结果, 请稍后重试或联系开发者`);
    const emptyOutput = {
      status: "empty",
      keyword: keyword,
      message: "没有找到匹配的视频或图文内容",
      error_code: "NO_MATCH",
      type: type,
      sort: sort,
      limit: limit,
      output_format: output,
      timestamp: new Date().toLocaleString(),
      results: [],
    };
    console.log(JSON.stringify(emptyOutput, null, 2));
    return;
  }

  // 输出搜索结果
  const finalOutput = {
    status: "success",
    keyword: keyword,
    message: "搜索任务完成",
    type: type,
    sort: sort,
    limit: limit,
    output_format: output,
    total: searchTask.length,
    timestamp: new Date().toLocaleString(),
    openclaw_metadata: {
      skill_version: constants.VERSION,
      runtime_version: process.versions.node,
      execution_time: Date.now() - startTime,
    },
    results: searchTask,
  };
  if (output === "markdown") {
    const message = validator.formatMessage(keyword, searchTask);
    utils.printInfo(message);
    utils.printSuccess(`搜索任务完成, 共返回 ${finalOutput.total} 条结果`);
  } else {
    console.log(JSON.stringify(finalOutput, null, 2));
    utils.printSuccess(`搜索任务完成, 共返回 ${finalOutput.total} 条结果`);
  }

  await log.taskWrite(
    `${startTime}_${keyword}_${type}_${sort}_${limit}_search.json`,
    JSON.stringify(finalOutput, null, 2),
  );
}

main().catch((error) => {
  utils.printError(error);
  process.exit(1);
});


path: src/validate/keyword.js
content: const utils = require("../utils/utils");

/**
 * 检查搜索关键词是否符合要求
 */
function isKeywordValid(keyword) {
  keyword = keyword.trim();
  if (keyword.length < 2) {
    utils.printError(`搜索关键词长度不能小于 2 个字符`);
    return false;
  }
  if (keyword.length > 50) {
    utils.printError(`搜索关键词长度不能超过 50 个字符`);
    return false;
  }
  if (/[<>\"'&]/g.test(keyword)) {
    utils.printError(`搜索关键词包含特殊字符, 请输入普通关键词, 例如: 新媒体`);
    return false;
  }
  if (keyword.includes("http")) {
    utils.printError(
      `搜索关键词包含 http 链接, 请输入普通关键词, 例如: 新媒体`,
    );
    return false;
  }
  return true;
}

/**
 * 清洗搜索关键词
 */
function cleanKeyword(keyword) {
  keyword = keyword.trim();
  return keyword.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s.,!?# ，。！？]/g, "");
}

/**
 * 格式化搜索选项, 并检查是否有效
 */
function optionFormat(type, sort, limit, output) {
  type = type || 0;
  sort = sort || 0;
  limit = limit || 10;
  output = output || "json";
  if (type !== 0 && type !== 1 && type !== 2) {
    utils.printError(`内容类型 ${type} 无效, 请使用 0, 1, 2。 默认值为 0`);
    type = 0;
  }
  if (sort !== 0 && sort !== 1 && sort !== 2 && sort !== 3 && sort !== 4) {
    utils.printError(
      `排序规则 ${sort} 无效, 请使用 0, 1, 2, 3, 4。 默认值为 0`,
    );
    sort = 0;
  }
  if (limit < 1 || limit > 60) {
    utils.printError(`搜索数量 ${limit} 无效, 请使用 1-60。 默认值为 10`);
    limit = 10;
  }
  if (output !== "json" && output !== "markdown") {
    utils.printError(
      `输出格式 ${output} 无效, 请使用 json, markdown。 默认值为 json`,
    );
    output = "json";
  }
  return [type, sort, limit, output];
}

function formatMessage(keyword, result) {
  let message = `### 📊 关于 "${keyword}" 的小红书数据简报\n`;
  message += `共找到 **${result.length}** 条高互动笔记（样本量）。\n\n`;
  message += "#### 🏆 爆款前三名\n";
  result.slice(0, 3).forEach((item, index) => {
    message += `${index + 1}. **[${item.title?.substring(0, 20)}...](${item.url})** `;
    message += `🔥${item.liked_count}点赞 `;
    message += `💬${item.comment_count}评论\n`;
  });
  message += "\n---\n\n";
  message += "#### 📝 详细列表\n";

  message += "-".repeat(35) + "\n\n";
  for (let i = 0; i < result.length; i++) {
    const item = result[i];
    message += `**${i + 1} .** ${item.title || "[无标题]"}\n`;
    message += `**发布时间**: ${item.publish_time || "[未知]"}\n`;
    message += `**链接**: ${item.url || "[未知]"}\n`;
    message += `**发布人**: ${item.user.nickname || "[未知]"}\n`;
    if (item.image_list && item.image_list.length > 0) {
      message += `**图文**: ${item.image_list.slice(0, 3).join(", ")}...\n`;
    }
    message += `**点赞**: ${item.liked_count || 0}\t`;
    message += `**评论**: ${item.comment_count || 0}\t`;
    message += `**收藏**: ${item.collected_count || 0}\t`;
    message += `**分享**: ${item.shared_count || 0}\n`;
    message += "\n";
  }
  message += "-".repeat(35) + "\n";
  message += `**共 ${result.length} 条结果**\n`;
  return message;
}

module.exports = {
  cleanKeyword,
  formatMessage,
  isKeywordValid,
  optionFormat,
};


path: src/validate/url.js
content: const utils = require("../utils/utils");

/**
 * 检查笔记链接是否符合要求
 */
function isXiaohongshuNoteUrl(url) {
  url = url.trim();
  url = url.replace("http://", "https://");
  if (url.indexOf("https://") !== 0) {
    utils.printError(`笔记链接必须以 https:// 开头`);
    return false;
  }
  if (url.indexOf(" ") !== -1) {
    utils.printError(`笔记链接不能包含空格`);
    return false;
  }
  if (url.indexOf("https://www.xiaohongshu.com/explore/") !== -1) {
    if (url.indexOf("?xsec_token=") === -1) {
      utils.printError(`笔记链接必须包含 xsec_token 参数`);
      return false;
    }
  } else if (url.indexOf("https://xhslink.com/m/") !== -1) {
  } else {
    utils.printError(`笔记链接格式无效`);
    return false;
  }
  return true;
}

function url2Name(url) {
  url = url.substring(0, url.indexOf("?"));
  return url
    .replace(/^https?:\/\//, "")
    .replace(/www\.xiaohongshu\.com\/explore\/|xhslink\.com\/m\//, "")
    .replace(/[\/?=&\-]/g, "_");
}

module.exports = {
  isXiaohongshuNoteUrl,
  url2Name,
};


path: src/utils/key.js
content: /**
 * TOKEN管理模块
 */
const utils = require("./utils");

function isValidToken(token) {
  if (!token || typeof token !== "string") {
    return false;
  }

  if (token.length !== 32) {
    return false;
  }

  const hexPattern = /^[0-9a-fA-F]{32}$/;
  return hexPattern.test(token);
}

function skillKey(token) {
  if (!isValidToken(token)) {
    utils.printWarn(
      "警告: 你的 GUAIKEI_API_TOKEN 未配置或已失效,技能功能已暂停服务. \n" +
        "请添加微信: 13395823479,获取专属私有TOKEN,一键配置即可恢复全部功能,永久稳定可用,不影响日常办公!",
    );
    return "";
  }

  utils.printInfo("已使用配置的私有TOKEN");
  return token;
}

module.exports = {
  skillKey,
};


path: src/utils/log.js
content: const fs = require("fs");
const path = require("path");
const utils = require("./utils");

async function taskWrite(filename, content) {
  const safeFilename = filename.replace(/[\\/:*?"<>|]/g, "_");
  const outputFilename = path.join(
    path.dirname(__filename),
    "..",
    "..",
    "logs",
    safeFilename,
  );

  try {
    await fs.promises.mkdir(path.dirname(outputFilename), { recursive: true });
    await fs.promises.writeFile(outputFilename, content);
    utils.printSuccess(`  → 已保存到 ${outputFilename}`);
  } catch (error) {
    utils.printError(`日志写入失败: ${error.message}`);
  }
}

module.exports = {
  taskWrite,
};


path: src/utils/request.js
content: const https = require("https");
const querystring = require("querystring");
const constants = require("../config/constants");
const utils = require("./utils");

async function request(options, data = null) {
  return new Promise((resolve, reject) => {
    const req = https.request(
      { ...options, timeout: constants.REQUEST_TIMEOUT },
      (res) => {
        let body = "";
        res.on("data", (chunk) => (body += chunk));
        res.on("end", () => {
          if (res.statusCode === 200) {
            const jsonBody = JSON.parse(body);
            if (jsonBody.errcode === 0) {
              resolve(jsonBody);
            } else {
              reject(new Error(jsonBody.errmsg || "请求失败"));
              return;
            }
          } else if (res.statusCode === 401 || res.statusCode === 403) {
            reject(
              new Error(
                `GUAIKEI_API_TOKEN 无效, 请检查环境变量 或 联系微信: 13395823479 获取解决方案`,
              ),
            );
          } else {
            reject(new Error(`请求失败, 状态码: ${res.statusCode}`));
          }
        });
      },
    );
    req.on("error", reject);
    req.on("timeout", () => reject(new Error("请求超时")));
    if (data) req.write(data);
    req.end();
  });
}

async function postJson(path, params, data) {
  const fullPath = `${path}?${querystring.stringify(params)}`;
  const options = {
    host: constants.BASE_URL,
    path: fullPath,
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Content-Length": Buffer.byteLength(JSON.stringify(data)),
    },
  };
  return await request(options, JSON.stringify(data));
}

async function getJson(path, params) {
  const fullPath = `${path}?${querystring.stringify(params)}`;
  const options = {
    host: constants.BASE_URL,
    path: fullPath,
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  };
  return await request(options);
}

module.exports = { getJson, postJson };


path: src/utils/retry.js
content: const constants = require("../config/constants");

async function withRetry(fn, maxAttempts, errorHandler) {
  let lastError;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn(attempt);
    } catch (error) {
      lastError = error;
      if (errorHandler) errorHandler(attempt, error);
      if (attempt < maxAttempts - 1) {
        await new Promise((resolve) =>
          setTimeout(resolve, constants.RETRY_INTERVAL),
        );
      }
    }
  }
  throw lastError || new Error(`重试${maxAttempts}次后失败`);
}

module.exports = { withRetry };


path: src/utils/utils.js
content: /**
 * 通用工具函数模块
 */

function printBanner() {
  console.log("╔════════════════════════════════════════════╗");
  console.log("║                                            ║");
  console.log("║       📕 小红书运营全链路数据工具           ║");
  console.log("║                                            ║");
  console.log("╚════════════════════════════════════════════╝");
  console.log("");
}

function printLog(level, message) {
  const colorMap = {
    INFO: "\x1b[34m",
    SUCCESS: "\x1b[32m",
    WARN: "\x1b[33m",
    ERROR: "\x1b[31m",
  };
  console.log(
    `${colorMap[level] || ""}[${new Date().toLocaleString()}] [${level}] ${message}\x1b[0m`,
  );
}

module.exports = {
  printBanner,
  printInfo: (msg) => printLog("INFO", msg),
  printSuccess: (msg) => printLog("SUCCESS", msg),
  printError: (msg) => printLog("ERROR", msg),
  printWarn: (msg) => printLog("WARN", msg),
};


path: src/config/constants.js
content: module.exports = {
  BASE_URL: "www.guaikei.com",
  REQUEST_TIMEOUT: 20000,
  CREATE_MAX_ATTEMPTS: 3,
  QUERY_MAX_ATTEMPTS: 60,
  RETRY_INTERVAL: 2000,
  VERSION: "1.0.1",
};


path: src/api/detail.js
content: /**
 * 小红书笔记详情和评论模块
 */
const constants = require("../config/constants");
const { postJson, getJson } = require("../utils/request");
const { withRetry } = require("../utils/retry");
const utils = require("../utils/utils");

/**
 * 创建小红书笔记详情及评论任务
 * @param {string} token - API令牌
 * @param {string} url - 笔记链接
 * @returns {Promise<Object>} 详情任务状态
 * @throws {Error} API调用失败时抛出错误
 */
async function createDetailTask(token, url) {
  return await withRetry(
    async () => {
      return await postJson(
        "/api/xiaohongshu/detail/url",
        { _: Date.now(), token: token },
        { url: url },
      );
    },
    constants.CREATE_MAX_ATTEMPTS,
    (attempt, err) => {
      utils.printError(
        `【创建任务重试】 ${attempt + 1}/${constants.CREATE_MAX_ATTEMPTS} 次 - ${err.message}`,
      );
    },
  );
}

/**
 * 获取小红书笔记详情及评论任务结果
 * @param {string} token - API令牌
 * @param {string} url - 笔记链接
 * @returns {Promise<Object>} 详情结果对象
 * @throws {Error} API调用失败时抛出错误
 */
async function getDetailTask(token, url) {
  return await withRetry(
    async () => {
      const res = await getJson("/api/xiaohongshu/detail/info", {
        _: Date.now(),
        token: token,
        url: url,
      });
      if (res.errcode === 0) {
        if (res.data.id && res.data.xsec_token) {
          res.data.url =
            "https://www.xiaohongshu.com/explore/" +
            res.data.id +
            "?xsec_token=" +
            res.data.xsec_token;
        }
        if (
          res.data.user &&
          res.data.user.user_id &&
          res.data.user.xsec_token
        ) {
          res.data.user.url =
            "https://www.xiaohongshu.com/user/profile/" +
            res.data.user.user_id +
            "?xsec_token=" +
            res.data.user.xsec_token;
        }
        return res.data;
      } else {
        throw new Error(`请求错误信息: ${res.errmsg || "请求失败"}`);
      }
      return null;
    },
    constants.QUERY_MAX_ATTEMPTS,
    (attempt, err) => {
      utils.printError(
        `【查询任务重试】 ${attempt + 1}/${constants.QUERY_MAX_ATTEMPTS} 次 - ${err.message}`,
      );
    },
  );
}

module.exports = {
  createDetailTask,
  getDetailTask,
};


path: src/api/search.js
content: /**
 * 小红书笔记搜索模块
 */
const constants = require("../config/constants");
const { postJson, getJson } = require("../utils/request");
const { withRetry } = require("../utils/retry");
const utils = require("../utils/utils");

/**
 * 创建小红书笔记搜索任务
 * @param {string} token - API令牌
 * @param {string} keyword - 搜索关键词
 * @param {number} type - 内容类型, 0: 全部, 1: 视频, 2: 图文
 * @param {number} sort - 排序规则, 0: 综合, 1: 最新, 2: 最多点赞, 3: 最多评论, 4: 最多收藏
 * @param {number} limit - 搜索数量, 1-60
 * @returns {Promise<Object>} 搜索任务状态
 * @throws {Error} API调用失败时抛出错误
 */
async function createSearchTask(token, keyword, type, sort, limit) {
  return await withRetry(
    async () => {
      return await postJson(
        "/api/xiaohongshu/note-search/keyword",
        { _: Date.now(), token: token },
        { keyword, type, sort, limit },
      );
    },
    constants.CREATE_MAX_ATTEMPTS,
    (attempt, err) => {
      utils.printError(
        `【创建任务重试】 ${attempt + 1}/${constants.CREATE_MAX_ATTEMPTS} 次 - ${err.message}`,
      );
    },
  );
}

/**
 * 获取小红书笔记搜索任务结果
 * @param {string} token - API令牌
 * @param {string} keyword - 搜索关键词
 * @param {number} type - 内容类型, 0: 全部, 1: 视频, 2: 图文
 * @param {number} sort - 排序规则, 0: 综合, 1: 最新, 2: 最多点赞, 3: 最多评论, 4: 最多收藏
 * @param {number} limit - 搜索数量, 1-60
 * @returns {Promise<Array>} 搜索结果数组
 * @throws {Error} API调用失败时抛出错误
 */
async function getSearchTask(token, keyword, type, sort, limit) {
  return await withRetry(
    async () => {
      const res = await getJson("/api/xiaohongshu/note-search/info", {
        _: Date.now(),
        token: token,
        keyword,
        type,
        sort,
        limit,
      });
      if (res.errcode === 0) {
        for (let i = 0; i < res.data.length; i++) {
          const item = res.data[i];
          if (item.id && item.xsec_token) {
            res.data[i].url =
              "https://www.xiaohongshu.com/explore/" +
              item.id +
              "?xsec_token=" +
              item.xsec_token;
          }
          if (item.user && item.user.user_id && item.user.xsec_token) {
            res.data[i].user.url =
              "https://www.xiaohongshu.com/user/profile/" +
              item.user.user_id +
              "?xsec_token=" +
              item.user.xsec_token;
          }
        }
        return res.data;
      } else {
        throw new Error(`请求错误信息: ${res.errmsg || "请求失败"}`);
      }
    },
    constants.QUERY_MAX_ATTEMPTS,
    (attempt, err) => {
      utils.printError(
        `【查询任务重试】 ${attempt + 1}/${constants.QUERY_MAX_ATTEMPTS} 次 - ${err.message}`,
      );
    },
  );
}

module.exports = {
  createSearchTask,
  getSearchTask,
};


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)