| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- #!/usr/bin/env node
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { runTihaoSourcing } = require('../mcp/src/tools/tihao-brief-sourcing-run');
- const { readFeedbackCsv } = require('./import-feedback-memory');
- const { readMemory, applyFeedbackRecordsToMemory } = require('../mcp/src/features/tihao-sourcing/preference-memory');
- const { buildFeedbackLoopClosureSummary } = require('./feedback-loop-closure-audit');
- async function main() {
- const out = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-feedback-loop-closure-'));
- const memoryPath = path.join(out, 'memory.json');
- const feedbackPath = path.join(out, 'feedback.csv');
- const resultPath = path.join(out, 'second-round', 'tihao-sourcing-result.json');
- const blockedNames = [
- '618母婴好物清单',
- '精致生活小鹿',
- '办公室健康零食测评'
- ];
- fs.writeFileSync(feedbackPath, [
- 'brief编号,平台,博主名称,主页链接,人工复核标签,反馈原因,归因类型,影响范围,是否升级为团队规则,下一轮动作',
- 'flower-skincare,小红书,618母婴好物清单,https://www.xiaohongshu.com/user/profile/xhs-dha-003,跑偏,护肤 Brief 不应优先推荐母婴清单号,需求解析,客户品牌级,否,剔除账号',
- 'flower-skincare,小红书,精致生活小鹿,https://www.xiaohongshu.com/user/profile/xhs-beauty-002,主页质感不符,封面下沉且主页质感不符,主页证据,团队规则,是,剔除账号',
- 'flower-skincare,小红书,办公室健康零食测评,https://www.xiaohongshu.com/user/profile/xhs-food-001,主页质感不符,封面下沉且主页质感不符,主页证据,团队规则,是,剔除账号',
- 'flower-skincare,小红书,下沉硬广一号,https://www.xiaohongshu.com/user/profile/xhs-low-001,主页质感不符,封面下沉且主页质感不符,主页证据,团队规则,是,剔除账号'
- ].join('\n'), 'utf8');
- const memory = applyFeedbackRecordsToMemory(readMemory(memoryPath), readFeedbackCsv(feedbackPath));
- fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2), 'utf8');
- const result = await runTihaoSourcing({
- collectionMode: 'sample',
- briefText: [
- '客户名称:花漾肌研',
- '平台与人数:小红书 6 人,抖音 2 人',
- '粉丝量范围:2万-50万',
- '预算范围:5000-18000',
- '偏好真实体验、素人感、成分拆解、敏感肌修护,不要硬广。'
- ].join('\n'),
- memory: memoryPath,
- output: path.join(out, 'second-round')
- });
- assert(result.status === 'ok', 'second round sourcing should return ok');
- assert(fs.existsSync(resultPath), 'second round result json should exist');
- const summary = buildFeedbackLoopClosureSummary({
- feedbackPath,
- memoryPath,
- resultPath,
- mode: 'sample',
- collectionMode: 'sample',
- generatedBy: 'feedback-loop-closure:smoke'
- });
- assert(summary.passed === true, 'feedback loop closure should pass for sample fixture');
- assert(summary.complete === false, 'sample fixture must not close direct customer proof');
- assert(summary.directCustomerProof === false, 'sample fixture must not be direct customer proof');
- assert(summary.proofLevel === 'smoke_or_local', 'sample fixture should be smoke/local proof');
- assert(summary.negativeFeedbackCount === 4, 'summary should count negative feedback rows');
- assert(summary.blockedCreatorCount === 4, 'summary should count blocked creators');
- assert(summary.blockedInMemoryCount === 4, 'summary should count blocked creators written to memory');
- assert(summary.leakedCount === 0, 'blocked creators should not leak into second-round client-ready list');
- assert(summary.teamRuleSuggestionCount >= 1, 'repeated team feedback should create team rule suggestion');
- for (const name of blockedNames) {
- assert(summary.rows.some(row => row.creatorName === name && row.inMemory && !row.leakedToSecondRound), `${name} should be blocked and absent from second round`);
- }
- const report = require('./feedback-loop-closure-audit').buildFeedbackLoopClosureSummary({
- feedbackPath,
- memoryPath,
- resultPath,
- mode: 'sample',
- collectionMode: 'sample',
- generatedBy: 'feedback-loop-closure:smoke'
- });
- assert(JSON.stringify(report).includes('directCustomerProof'), 'summary should expose proof boundary');
- assert(!/sk-[A-Za-z0-9_-]{10,}/.test(JSON.stringify(summary)), 'summary should not contain model token patterns');
- assert(!/r:[A-Za-z0-9]{20,}/.test(JSON.stringify(summary)), 'summary should not contain Parse sessionToken patterns');
- console.log(JSON.stringify({
- ok: true,
- outputDir: out,
- negativeFeedbackCount: summary.negativeFeedbackCount,
- leakedCount: summary.leakedCount,
- directCustomerProof: summary.directCustomerProof
- }, null, 2));
- }
- function assert(condition, message) {
- if (!condition) throw new Error(message);
- }
- main().catch(error => {
- console.error(error && error.stack ? error.stack : String(error));
- process.exit(1);
- });
|