从采集矩阵 → 真实 API 调度 + 数据归一化 + 去重 + 假设打标 —— 这是整个工作流的"事实地基"。
输入:P3 的采集矩阵(30-50 关键词 × 批次)
输出:
docs/<品类>/raw/xhs/*.json(每关键词一个文件)docs/<品类>/raw/douyin/*.jsondocs/<品类>/raw/_merged.json(统一格式合并)docs/<品类>/raw/comments-flat.jsonl(行级流,便于 grep)docs/<品类>/raw/audit.log(采集审计日志)scripts/tools/<品类>-collect.js结构骨架(300-500 行):
// ==========================================================
// 1. 常量 + 路径
// ==========================================================
const ROOT = path.resolve(__dirname, '..', '..');
const RAW = path.join(ROOT, 'docs', '<品类>', 'raw');
const XHS_DIR = path.join(RAW, 'xhs');
const DY_DIR = path.join(RAW, 'douyin');
const MERGED = path.join(RAW, '_merged.json');
const AUDIT = path.join(RAW, 'audit.log');
// ==========================================================
// 2. API 凭据加载
// ==========================================================
function loadTikHubKey() { ... } // ~/.openclaw/skills/xiaohongshu-search-notes/api-config.json
function loadVocToken() { ... } // ~/.openclaw/voc-credentials.json
// ==========================================================
// 3. 批次定义(BATCHES)
// ==========================================================
const BATCHES = {
1: {
xhs: [
{ keyword: '本品名', max_notes: 15, max_comments_per: 10, product: '本品' },
// ...
],
douyin: [
{ keyword: '本品名', max_items: 25, product: '本品' },
// ...
],
},
2: { ... },
3: { ... },
};
// ==========================================================
// 4. XHS 采集函数(TikHub)
// ==========================================================
async function collectXhs(task) {
// 4.1 检查缓存
if (existsCache(task.keyword) && !FORCE) return;
// 4.2 调 TikHub search_notes
const notes = await tikhubSearchNotes(task.keyword, task.max_notes);
// 4.3 逐笔记调 note_comments
const items = [];
for (const note of notes) {
const comments = await tikhubNoteComments(note.id, task.max_comments_per);
for (const c of comments) {
items.push(buildItem({
platform: 'xhs',
product: task.product,
keyword: task.keyword,
type: 'comment',
nickname: c.user_nickname,
ip: c.ip_location,
content: c.content,
likes: c.like_count,
note_id: note.id,
note_title: note.title,
// ...
}));
}
}
// 4.4 落盘 + 审计
fs.writeFileSync(path.join(XHS_DIR, `${safeName(task.keyword)}.json`),
JSON.stringify(items, null, 2));
logAudit(`OK ${task.keyword} → ${items.length}`);
}
// ==========================================================
// 5. 抖音采集函数(VOC Skill API)
// ==========================================================
async function collectDouyin(task) {
// 调 hotSoonTopVideo / video_comments
// ...
}
// ==========================================================
// 6. 统一 Item 构造
// ==========================================================
function buildItem({ platform, product, keyword, type, nickname, ip, content, likes, ...rest }) {
const id = hash(platform + content + nickname);
return {
id, platform, product, keyword, type, nickname, ip, content,
likes: Number(likes) || 0,
tags: inferTags(content),
hypotheses: inferHypotheses(content, keyword),
sentiment: inferSentiment(content),
collected_at: Date.now(),
...rest,
};
}
// ==========================================================
// 7. 假设标注
// ==========================================================
function inferHypotheses(content, keyword) {
const tags = [];
if (/冷链|处方|医生开/.test(content)) tags.push('H1');
if (/食品级|菌株|效果|没用|不靠谱/.test(content)) tags.push('H2');
if (/便秘|积食|挑食|腹泻|宝宝|孩子/.test(content)) tags.push('H3');
if (/入园|开学|抗生素|换季/.test(content)) tags.push('H4');
if (/喂|吐出|苦|难吃|咀嚼|水果味/.test(content)) tags.push('H5');
if (/包装|颜值|独立|一次一|礼盒/.test(content)) tags.push('H6');
if (/吸收|营养糖|瓶瓶罐罐|DHA|补钙/.test(content)) tags.push('H7');
if (/副作用|长期|依赖|糖|添加/.test(content)) tags.push('H8');
return tags;
}
// ==========================================================
// 8. 合并函数
// ==========================================================
function mergeAll() {
const items = [];
scanDir(XHS_DIR, 'xhs', items);
scanDir(DY_DIR, 'douyin', items);
// 去重(同 id 合并)
const dedup = new Map();
for (const it of items) dedup.set(it.id, it);
const merged = {
meta: {
count: dedup.size,
platforms: { xhs: ..., douyin: ... },
hypotheses: tally H1-H8,
collected_at: Date.now(),
},
items: [...dedup.values()],
};
fs.writeFileSync(MERGED, JSON.stringify(merged, null, 2));
// 同时生成 JSONL 流
fs.writeFileSync(path.join(RAW, 'comments-flat.jsonl'),
[...dedup.values()].map(x => JSON.stringify(x)).join('\n'));
}
// ==========================================================
// 9. 主入口 + CLI
// ==========================================================
async function main() {
const args = process.argv.slice(2);
const batch = args.find(a => a.startsWith('--batch='))?.split('=')[1];
if (args.includes('--merge')) return mergeAll();
if (batch === 'all') { for (let i of [1,2,3]) await runBatch(i); }
else if (batch) await runBatch(Number(batch));
else if (args.includes('--test')) await testSingle();
mergeAll();
}
main().catch(console.error);
详见 templates/collect.template.js。
凭据:~/.openclaw/skills/xiaohongshu-search-notes/api-config.json
{
"currentToken": "gqsZHfMW..."
}
接口:
POST https://api.tikhub.io/api/v1/xiaohongshu/web_v1/search_notes
{ keyword, sort_type: "general", note_type: "normal", total_number: N }POST https://api.tikhub.io/api/v1/xiaohongshu/web_v1/note_comments
{ note_id, cursor: "" }关键响应字段:
id, user.nickname, title, desc, liked_count, comment_count, covercontent, like_count, user_info.nickname, ip_location, sub_comment_countRate Limit:
凭据:~/.openclaw/voc-credentials.json(Parse session token,余额 ≥ 1)
接口:通过 VOC Skill 封装(参考 ~/.openclaw/skills/social-*-comments/)
关键调用(伪代码):
const videos = await vocApiSearch('抖音', keyword, { max: 20 });
for (const video of videos) {
const comments = await vocApiComments('抖音', video.id, { max: 10 });
// ...
}
Token 预飞:跑 node ~/.openclaw/tools/voc-token-preflight.js 确认 status=valid 才能跑。
凭据:同 TikHub token(TikHub 也提供 Amazon 接口)
接口:
POST https://api.tikhub.io/api/v1/amazon/...场景:肝纯片项目用了 Amazon 采集海外竞品(milk thistle / liver detox)
.json 凭据文件到 gitconst tokenSnippet = (token || '').slice(0, 8) + '...';
console.log(` TikHub token: ✓ ${tokenSnippet}`);
node ~/.openclaw/tools/voc-token-preflight.js
5 种 status:
| status | 含义 | 处理 |
|---|---|---|
| valid | 正常 | 继续跑采集 |
| missing | 凭据文件不存在 | node ~/.openclaw/tools/set-voc-token.js <token> |
| expired | token 过期 | 去 apig-pay 登录拿新 token |
| insufficient_balance | 余额 < 1 | 去 apig-pay 充值 |
| error | 网络/API 异常 | 等 5 分钟重试 |
# 1.1 凭据
Test-Path ~/.openclaw/skills/xiaohongshu-search-notes/api-config.json
Test-Path ~/.openclaw/voc-credentials.json
# 1.2 Token 预飞
node ~/.openclaw/tools/voc-token-preflight.js
# 1.3 目录
mkdir -p docs/<品类>/raw/xhs docs/<品类>/raw/douyin
node scripts/tools/<品类>-collect.js --test
# 预期:采 1-2 个 kw 出 10-30 条真实评论
# Batch 1(P0 基础盘)
node scripts/tools/<品类>-collect.js --batch=1
# 预期:20-30 分钟,~500-1200 条
# Batch 2(P1 长尾 + 场景)
node scripts/tools/<品类>-collect.js --batch=2
# 预期:15-25 分钟,~400-800 条
# Batch 3(P2 决策 + 相邻)
node scripts/tools/<品类>-collect.js --batch=3
# 预期:12-20 分钟,~300-600 条
node scripts/tools/<品类>-collect.js --merge
# 生成 _merged.json + comments-flat.jsonl
Get-Content docs/<品类>/raw/audit.log
# 应该看到 OK 行,不能有 ERR 未处理
如果 P5 发现 H6 / H7 覆盖不足:
// 在 BATCHES[4] 追加新 kw
BATCHES[4] = {
xhs: [
{ keyword: '儿童无糖益生菌', max_notes: 10, max_comments_per: 8, product: '补采·H6' },
{ keyword: '宝宝专用', max_notes: 8, max_comments_per: 5, product: '补采·H6' },
],
};
node scripts/tools/<品类>-collect.js --batch=4 --force
node scripts/tools/<品类>-collect.js --merge
node scripts/tools/gen-<品类>.js
采完后必须跑的 5 项检查:
node -e "
const d = require('./docs/<品类>/raw/_merged.json');
const items = d.items;
console.log('总量:', items.length);
console.log('平台分布:', JSON.stringify(d.meta.platforms));
console.log('H 假设分布:');
for (let i=1; i<=8; i++) {
const n = items.filter(it => (it.hypotheses||[]).includes('H'+i)).length;
console.log(' H'+i+':', n, n < 100 ? '⚠️ 不足' : '✓');
}
console.log('空内容:', items.filter(it => !it.content || it.content.length < 5).length);
console.log('重复 content 前 40 字:',
Array.from(new Map(items.map(it => [it.content.slice(0,40), it])).values()).length,
'/', items.length);
"
--merge + 查看 _merged.json 验证格式buildItem 时用 hash(platform + content + nickname) 做 id + merge 时 Map 去重--force 过于激进--force,白白消耗 API 额度--force,日常调试用缓存OK/SKIP/ERR/keyword/count/msinferSentiment 辅助(差评 / 好评 / 中性)Phase 4 完成的 6 个条件:
_merged.json 可加载,结构符合 {meta, items}audit.log 无 ERR 未处理