插件
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# 插件 → 后端 端到端验证
|
||||
|
||||
## 自动化验证(push 模块 → 后端链路,已验证 ✅)
|
||||
|
||||
前置:Plan 1 后端在跑(`cd d:/alibb-server/backend && docker compose up -d`,`/api/health` ok)。
|
||||
|
||||
```bash
|
||||
cd d:/alibb && node scripts/e2e-push.mjs
|
||||
# 预期:ingest status 200, inserted 1
|
||||
# 然后查:
|
||||
JWT=$(curl -s -XPOST http://localhost:8080/api/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin-password-1234"}' | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).token))")
|
||||
curl -s "http://localhost:8080/api/inquiries/E2E-TEST2" -H "Authorization: Bearer $JWT"
|
||||
# 预期:看到 inquiry + customer + _count.messages=1
|
||||
```
|
||||
|
||||
这验证 `push/payload.js` 组装的 payload 能被 Plan 1 后端 `/api/ingest` 正确接收入库(询盘+客户+消息)。
|
||||
|
||||
## 完整插件 e2e(需手动,Chrome 加载扩展)
|
||||
|
||||
1. **起后端**:`cd d:/alibb-server/backend && docker compose up -d --build`,等 `curl http://localhost:8080/api/health` → `{"ok":true}`
|
||||
2. **加载扩展**:Chrome 打开 `chrome://extensions` → 开启「开发者模式」→「加载已解压的扩展程序」→ 选 `d:/alibb`
|
||||
3. **配置推送**:点插件图标 →「后端设置」区填:
|
||||
- 服务器 URL:`http://localhost:8080`
|
||||
- Token:`dev-ingest-token-1234`
|
||||
- 勾「启用推送」
|
||||
- 点「授权域名」→ Chrome 弹框点「允许」
|
||||
- 点「保存设置」→ 看到「推送设置已保存(已启用)」
|
||||
4. **抓取**:打开 `https://message.alibaba.com/message/default.htm`(登录阿里主账号)→ 点插件「开始抓取」(detail 模式,测试 10 条)
|
||||
5. **观察**:popup 顶部状态条「推送:已推 N / 待推 M / 失败 K」随抓取增长
|
||||
6. **验证后端有数据**:
|
||||
```bash
|
||||
JWT=$(curl -s -XPOST http://localhost:8080/api/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin-password-1234"}' | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).token))")
|
||||
curl -s "http://localhost:8080/api/inquiries?size=5" -H "Authorization: Bearer $JWT" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=JSON.parse(s);console.log('total:',d.total);d.items.forEach(i=>console.log(' ',i.inquiryId,i.customer?.country,'msgs:',i.messagesCount))})"
|
||||
```
|
||||
预期:看到刚抓的询盘,带 country + messagesCount
|
||||
7. **验证幂等**:再抓同一询盘 → 后端 `messages_count` 不变(早返回生效)
|
||||
|
||||
## 断网/重试验证(防丢)
|
||||
|
||||
1. 停后端:`docker compose down`
|
||||
2. 抓一条 → popup 状态「待推 1」(入 IndexedDB 队列,未丢)
|
||||
3. 起后端:`docker compose up -d`
|
||||
4. 等 ~1 分钟(chrome.alarms 触发 worker)→ popup「已推 1」(自动补推)
|
||||
|
||||
## 收尾
|
||||
|
||||
`cd d:/alibb-server/backend && docker compose down`(volume 保留)
|
||||
@@ -0,0 +1,26 @@
|
||||
// 模拟插件推送:用 push/payload.js 组装 payload,POST 到后端 /api/ingest
|
||||
// 用法: node scripts/e2e-push.mjs (可选 SERVER/TOKEN 环境变量)
|
||||
import { buildIngestPayload } from '../push/payload.js';
|
||||
|
||||
const record = {
|
||||
inquiryId: 'E2E-TEST2', manager: 'tester',
|
||||
createTimeFromApi: '2026-07-14T10:00:00Z', updateTimeFromApi: '2026-07-14T10:00:00Z',
|
||||
status: 'new', customerId: 'c-e2e', detailCountry: 'US', detailBuyerName: 'Test Buyer',
|
||||
behavior: { validRfq: '5' },
|
||||
};
|
||||
const messages = [{
|
||||
messageId: 'e2e-test-m1', sendTime: '2026-07-14T10:00:00Z',
|
||||
sender: 'tester', original: 'hello e2e', direction: 'outgoing',
|
||||
}];
|
||||
|
||||
const payload = buildIngestPayload(record, messages);
|
||||
const SERVER = process.env.SERVER || 'http://localhost:8080';
|
||||
const TOKEN = process.env.TOKEN || 'dev-ingest-token-1234';
|
||||
|
||||
const res = await fetch(`${SERVER}/api/ingest`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
console.log('ingest status:', res.status);
|
||||
console.log('ingest body:', JSON.stringify(await res.json()));
|
||||
@@ -0,0 +1,35 @@
|
||||
// 根因验证:后端 customers.customer_id 是 VARCHAR(64)(spec §6.1),
|
||||
// 而插件对无真 customerId 的询盘用 sec_ + secTradeId 兜底(75 字符)。
|
||||
// 本脚本推两条测试数据对比:超长 sec_ vs 短 id,看后端 ingest 的实际反应。
|
||||
// 用法: node scripts/test-cid-length.mjs
|
||||
import { buildIngestPayload } from '../push/payload.js';
|
||||
|
||||
const SERVER = process.env.SERVER || 'http://219.157.255.213:23010';
|
||||
const TOKEN = process.env.TOKEN || '688cbb4b6e880b340b2360559c4ad29d01a39ccbb3c31c4d';
|
||||
|
||||
async function push(label, customerId) {
|
||||
const record = {
|
||||
inquiryId: `TEST-CID-${label}`,
|
||||
customerId,
|
||||
detailBuyerName: 'Test Buyer', detailCountry: 'US', company: 'Test Co',
|
||||
createTimeFromApi: '2026-07-20T00:00:00Z', updateTimeFromApi: '2026-07-20T00:00:00Z',
|
||||
behavior: { productViews: '1' },
|
||||
};
|
||||
const payload = buildIngestPayload(record, []);
|
||||
const res = await fetch(`${SERVER}/api/ingest`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
let body = '';
|
||||
try { body = await res.text(); } catch (e) { body = '(no body)'; }
|
||||
console.log(`[${label}] customerId(len=${customerId.length}): status=${res.status}`);
|
||||
console.log(` body=${body.slice(0, 400)}`);
|
||||
console.log(` payload.customer.customerId = ${JSON.stringify(payload.customer && payload.customer.customerId)}`);
|
||||
}
|
||||
|
||||
const longCid = 'sec_MC1IDX1yqSSuFKld-CG7npDfPBic6Iv1S_ZJecxnw8b7kTejq_GNIDRuGTmXE-sjnZMUpze';
|
||||
console.log('=== longCid length:', longCid.length, '===');
|
||||
await push('LONG75', longCid);
|
||||
await push('SHORT5', '12345');
|
||||
console.log('\n说明:若 LONG75 失败(4xx)而 SHORT5 成功(200) → 证实后端因长度拒绝 sec_ customerId');
|
||||
@@ -0,0 +1,156 @@
|
||||
// 验证聊天时间范围判定逻辑(纯函数基准实现)。
|
||||
// content.js/popup.js 为 IIFE/非 module,无法被 vitest import —— 本文件是判定逻辑的
|
||||
// 基准实现+用例锁定;实现侧照搬函数体。⚠️ 改任一侧必须同步另一侧。
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (name, cond) => { console.log((cond ? 'PASS' : 'FAIL') + ' | ' + name); cond ? pass++ : fail++; };
|
||||
|
||||
// ---- 基准实现(实现照搬区)----
|
||||
// 自然月回退:直接 setMonth(-n) 月末溢出(3-31−1月→"2-31"→3-02),先退月初再钳制天数
|
||||
function minusMonths(base, n) {
|
||||
const d = new Date(base.getFullYear(), base.getMonth(), 1,
|
||||
base.getHours(), base.getMinutes(), base.getSeconds(), base.getMilliseconds());
|
||||
d.setMonth(d.getMonth() - n);
|
||||
const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
||||
d.setDate(Math.min(base.getDate(), lastDay));
|
||||
return d;
|
||||
}
|
||||
// popup 范围计算:mode=all|1m|2m|3m|custom → {since,until}(ISO|null) 或 {error}
|
||||
function computeChatRange(mode, sinceStr, untilStr) {
|
||||
if (mode === 'all') return { since: null, until: null };
|
||||
if (mode === '1m' || mode === '2m' || mode === '3m') {
|
||||
// 整天边界:since=起始日 00:00:00(完整自然月天数),until=今日 23:59:59.999("至今"取今天整天,与 custom 止=今天一致)
|
||||
const dayStart = new Date(); dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(); dayEnd.setHours(23, 59, 59, 999);
|
||||
return { since: minusMonths(dayStart, Number(mode[0])).toISOString(), until: dayEnd.toISOString() };
|
||||
}
|
||||
if (mode === 'custom') {
|
||||
if (!sinceStr || !untilStr) return { error: '请填写自定义起止日期' };
|
||||
const s = new Date(sinceStr + 'T00:00:00.000'); // ⚠️ 必须拼时间:裸 date-only 按 UTC 解析差 8h
|
||||
const u = new Date(untilStr + 'T23:59:59.999');
|
||||
if (isNaN(s.getTime()) || isNaN(u.getTime())) return { error: '日期格式错误' };
|
||||
if (s.getTime() > u.getTime()) return { error: '起始日期不能晚于截止日期' };
|
||||
const uDay = new Date(untilStr + 'T00:00:00.000');
|
||||
const t0 = new Date(); t0.setHours(0, 0, 0, 0);
|
||||
if (uDay.getTime() > t0.getTime()) return { error: '截止日期不能晚于今天' };
|
||||
return { since: s.toISOString(), until: u.toISOString() };
|
||||
}
|
||||
return { error: '未知时间范围模式' };
|
||||
}
|
||||
// 范围过滤(null sendTime 防御性保留 —— 宁多勿漏)
|
||||
function inChatRange(sendTime, sinceMs, untilMs) {
|
||||
if (sendTime == null) return true;
|
||||
if (sinceMs != null && sendTime < sinceMs) return false;
|
||||
if (untilMs != null && sendTime > untilMs) return false;
|
||||
return true;
|
||||
}
|
||||
// 水位读取兼容:旧=纯 string(sendTime 未知) / 新={id,sendTime}
|
||||
function parseWatermark(v) {
|
||||
if (v == null || v === '') return null;
|
||||
try {
|
||||
const o = JSON.parse(v);
|
||||
if (o && typeof o === 'object' && o.id != null) return { id: String(o.id), sendTime: Number(o.sendTime) || null };
|
||||
} catch (e) { /* 旧格式继续 */ }
|
||||
return { id: String(v), sendTime: null };
|
||||
}
|
||||
// 水位只前进写入判定
|
||||
function shouldWriteWatermark(oldWm, candId, candSendTime) {
|
||||
if (candId == null || candSendTime == null) return false;
|
||||
if (oldWm == null) return true;
|
||||
if (oldWm.sendTime == null) return true; // legacy 升级,允许覆盖一次
|
||||
return candSendTime > oldWm.sendTime;
|
||||
}
|
||||
// probe 轮断言分类:'fail'(非对象/code≠200/data=null) | 'empty'(空list) | null(正常)。
|
||||
// non-JSON/HTTP/关键词由 postForm 抛 RISK,不经此函数(真风控照抛,不降级)
|
||||
function probeFailKind(json) {
|
||||
if (!json || typeof json !== 'object') return 'fail';
|
||||
if (json.code !== undefined && Number(json.code) !== 200) return 'fail';
|
||||
if (json.data == null) return 'fail';
|
||||
const list = json.data && json.data.list;
|
||||
if (!Array.isArray(list) || list.length === 0) return 'empty';
|
||||
return null;
|
||||
}
|
||||
// ISO → 毫秒,parse 容错(spec §5:非法/空 → null = 降级不限)
|
||||
function toMs(v) { const n = v ? Date.parse(v) : NaN; return Number.isFinite(n) ? n : null; }
|
||||
|
||||
// ---- 用例 ----
|
||||
ok('3-31 − 1月 = 2-28(月末钳制,setMonth 溢出防护)', (() => {
|
||||
const r = minusMonths(new Date(2026, 2, 31, 15, 30), 1);
|
||||
return r.getMonth() === 1 && r.getDate() === 28 && r.getHours() === 15;
|
||||
})());
|
||||
ok('8-25 − 1月 = 7-25(普通日期)', (() => {
|
||||
const r = minusMonths(new Date(2026, 7, 25), 1);
|
||||
return r.getMonth() === 6 && r.getDate() === 25;
|
||||
})());
|
||||
ok('12-31 − 2月 = 10-31(同年跨月)', (() => {
|
||||
const r = minusMonths(new Date(2026, 11, 31), 2);
|
||||
return r.getMonth() === 9 && r.getDate() === 31;
|
||||
})());
|
||||
ok('1-31 − 2月 = 上年 11-30(跨年+钳制)', (() => {
|
||||
const r = minusMonths(new Date(2026, 0, 31), 2);
|
||||
return r.getFullYear() === 2025 && r.getMonth() === 10 && r.getDate() === 30;
|
||||
})());
|
||||
ok('闰年:2028-03-31 − 1月 = 2028-02-29', (() => {
|
||||
const r = minusMonths(new Date(2028, 2, 31), 1);
|
||||
return r.getMonth() === 1 && r.getDate() === 29;
|
||||
})());
|
||||
|
||||
ok('all → 双 null(完全同现状)', computeChatRange('all').since === null && computeChatRange('all').until === null);
|
||||
ok('1m → since=起始日 00:00:00.000(25~32天前),until=今日 23:59:59.999(整天边界)', (() => {
|
||||
const r = computeChatRange('1m');
|
||||
const s = new Date(r.since), u = new Date(r.until);
|
||||
const ageDays = (Date.now() - s.getTime()) / 86400e3;
|
||||
const t0 = new Date(); t0.setHours(0, 0, 0, 0);
|
||||
return ageDays > 25 && ageDays < 32
|
||||
&& s.getHours() === 0 && s.getMinutes() === 0 && s.getSeconds() === 0 && s.getMilliseconds() === 0
|
||||
&& u.getTime() - t0.getTime() === 86399999; // = 今日 0 点 + 全天末毫秒
|
||||
})());
|
||||
ok('custom 本地时区(since=本地0点,非 UTC)', (() => {
|
||||
const r = computeChatRange('custom', '2026-08-25', '2026-08-25');
|
||||
const s = new Date(r.since);
|
||||
return s.getMonth() === 7 && s.getDate() === 25 && s.getHours() === 0 && s.getMinutes() === 0;
|
||||
})());
|
||||
ok('custom until=止日 23:59:59.999 本地(含全天)', (() => {
|
||||
const r = computeChatRange('custom', '2026-08-01', '2026-08-25');
|
||||
const u = new Date(r.until);
|
||||
return u.getDate() === 25 && u.getHours() === 23 && u.getMinutes() === 59 && u.getMilliseconds() === 999;
|
||||
})());
|
||||
ok('custom 起>止 → error', !!computeChatRange('custom', '2026-08-10', '2026-08-01').error);
|
||||
ok('custom 起为空 → error', !!computeChatRange('custom', '', '2026-08-01').error);
|
||||
ok('custom 止>今天 → error', !!computeChatRange('custom', '2026-01-01', '2099-01-01').error);
|
||||
ok('custom 垃圾日期 → error', !!computeChatRange('custom', 'not-a-date', '2026-08-01').error);
|
||||
ok('未知模式 → error', !!computeChatRange('6m').error);
|
||||
|
||||
const SINCE = Date.parse('2026-07-25T00:00:00'), UNTIL = Date.parse('2026-08-25T23:59:59');
|
||||
ok('since 当天消息收(含边界)', inChatRange(SINCE, SINCE, null));
|
||||
ok('since 前一天不收', !inChatRange(SINCE - 86400e3, SINCE, null));
|
||||
ok('until 末毫秒收(含边界)', inChatRange(UNTIL, null, UNTIL));
|
||||
ok('until 次日不收', !inChatRange(UNTIL + 1, null, UNTIL));
|
||||
ok('sendTime=null 防御性保留', inChatRange(null, SINCE, UNTIL));
|
||||
ok('范围不限(null)全收', inChatRange(1, null, null));
|
||||
|
||||
ok('新格式水位解析', (() => { const w = parseWatermark('{"id":"a1","sendTime":1787626659384}'); return w.id === 'a1' && w.sendTime === 1787626659384; })());
|
||||
ok('旧格式(纯string)→ legacy,sendTime 未知', (() => { const w = parseWatermark('abc123'); return w.id === 'abc123' && w.sendTime === null; })());
|
||||
ok('空值(null/undefined/空串)→ null(首次抓取必走)', parseWatermark(null) === null && parseWatermark(undefined) === null && parseWatermark('') === null);
|
||||
ok('只前进:候选6-30 不覆盖 7-28', !shouldWriteWatermark({ id: 'o', sendTime: Date.parse('2026-07-28') }, 'n', Date.parse('2026-06-30')));
|
||||
ok('只前进:候选8-25 覆盖 7-28', shouldWriteWatermark({ id: 'o', sendTime: Date.parse('2026-07-28') }, 'n', Date.parse('2026-08-25')));
|
||||
ok('legacy 允许覆盖一次(升级)', shouldWriteWatermark({ id: 'o', sendTime: null }, 'n', Date.parse('2026-06-30')));
|
||||
ok('无旧水位 → 写入', shouldWriteWatermark(null, 'n', 1));
|
||||
ok('候选缺 sendTime → 不写', !shouldWriteWatermark({ id: 'o', sendTime: 5 }, 'n', null));
|
||||
|
||||
ok('probe 正常(200+非空) → null', probeFailKind({ code: '200', data: { list: [{}] } }) === null);
|
||||
ok('probe code≠200 → fail 降级', probeFailKind({ code: '500' }) === 'fail');
|
||||
ok('probe data=null → fail 降级', probeFailKind({ code: '200', data: null }) === 'fail');
|
||||
ok('probe 非对象 → fail 降级', probeFailKind('ok') === 'fail');
|
||||
ok('probe 空 list → empty 降级(实测形态)', probeFailKind({ code: '200', data: { list: [] } }) === 'empty');
|
||||
ok('probe list 缺失 → empty 降级', probeFailKind({ code: '200', data: {} }) === 'empty');
|
||||
|
||||
ok('toMs 非法/空 → null,合法 → 毫秒', toMs('garbage') === null && toMs(null) === null && toMs('2026-08-25T00:00:00Z') === Date.parse('2026-08-25T00:00:00Z'));
|
||||
// (文档性说明,非行为锁定)until 游标 = untilMs+1:附录B实测语义=返回 sendTime < cursor,恰好覆盖 <= untilMs;真实验证靠 Task 8 #4 SW Network
|
||||
ok('文档性:until 游标 +1ms 覆盖含止日末毫秒(真实验证靠 Task 8 #4)', (() => {
|
||||
const untilMs = Date.parse('2026-06-30T23:59:59.999');
|
||||
const cursor = untilMs + 1;
|
||||
return (untilMs < cursor) && !(untilMs + 1 < cursor);
|
||||
})());
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
@@ -0,0 +1,96 @@
|
||||
// 验证 content.js postForm 的风控检测逻辑(纯函数复刻版,须与 content.js 内 postForm 保持一致)。
|
||||
// content.js 是 IIFE 闭包 + content_script 不能 ES module import,内部函数无法被 vitest 直接测,
|
||||
// 故在此复刻逻辑、跑关键 case 佐证正确性。改 content.js 的检测逻辑时务必同步本文件。
|
||||
const RISK_KEYWORDS = ['验证码', 'captcha', '滑动验证', '安全验证', 'blocked', '访问频繁', '限制访问', '风控'];
|
||||
const RISK_TOP_FIELDS = new Set(['msg', 'message', 'errmsg', 'statue', 'errormessage', 'err']);
|
||||
const hitsRiskKeyword = s => {
|
||||
const lower = String(s == null ? '' : s).toLowerCase();
|
||||
return RISK_KEYWORDS.find(kw => lower.includes(String(kw).toLowerCase()));
|
||||
};
|
||||
|
||||
// 复刻 content.js 的账号注销/管控跳过检测(与 hitsSkipKeyword 保持一致)
|
||||
const SKIP_KEYWORDS = ['对方账号已自行注销', '违反平台规则被管控'];
|
||||
const hitsSkipKeyword = s => {
|
||||
const lower = String(s == null ? '' : s).toLowerCase();
|
||||
return SKIP_KEYWORDS.find(kw => lower.includes(String(kw).toLowerCase()));
|
||||
};
|
||||
|
||||
// 复刻 postForm 的检测分支(去掉 fetch,直接吃响应 text)。返回 {risk} 或 {ok, json}。
|
||||
function detect(text) {
|
||||
let j;
|
||||
try { j = JSON.parse(text); }
|
||||
catch (e) {
|
||||
const kw = hitsRiskKeyword(text);
|
||||
if (kw) return { risk: 'RISK:关键词(' + kw + ')' };
|
||||
return { risk: 'RISK:结构异常(non-JSON)' };
|
||||
}
|
||||
if (j && typeof j === 'object') {
|
||||
for (const f of Object.keys(j)) {
|
||||
if (RISK_TOP_FIELDS.has(f.toLowerCase()) && typeof j[f] === 'string') {
|
||||
const kw = hitsRiskKeyword(j[f]);
|
||||
if (kw) return { risk: 'RISK:关键词(' + kw + '@' + f + ')' };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: true, json: j };
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{
|
||||
name: '正常 getOpMessages:正文含 captcha/blocked/验证码 不应误报(核心回归)',
|
||||
text: JSON.stringify({ code: '200', data: { list: [
|
||||
{ messageId: '1', content: 'are you blocked? the captcha 验证码 is wrong', sendTime: 1700000000000 },
|
||||
] } }),
|
||||
expectRisk: false,
|
||||
},
|
||||
{
|
||||
name: 'HTML 滑块验证页(非JSON,含关键词)→ 应报风控',
|
||||
text: '<html><body>请完成滑动验证 security check captcha required</body></html>',
|
||||
expectRisk: true,
|
||||
},
|
||||
{
|
||||
name: 'JSON 顶层 msg="访问频繁" → 应报风控',
|
||||
text: JSON.stringify({ code: '200', msg: '您访问频繁,请稍后再试' }),
|
||||
expectRisk: true,
|
||||
},
|
||||
{
|
||||
name: 'JSON 顶层 errMsg 含 captcha → 应风控(字段名大小写无关)',
|
||||
text: JSON.stringify({ errMsg: 'captcha verification failed' }),
|
||||
expectRisk: true,
|
||||
},
|
||||
{
|
||||
name: '非JSON 无关键词(登录页)→ 结构异常,应暂停',
|
||||
text: '<html><body>please login</body></html>',
|
||||
expectRisk: true,
|
||||
},
|
||||
{
|
||||
name: '正常 querySummary → 不报',
|
||||
text: JSON.stringify({ code: 200, data: { owner: { accountIdEncrypt: 'x' } } }),
|
||||
expectRisk: false,
|
||||
},
|
||||
];
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
for (const c of cases) {
|
||||
const r = detect(c.text);
|
||||
const got = !!r.risk;
|
||||
const ok = got === c.expectRisk;
|
||||
console.log((ok ? 'PASS' : 'FAIL') + ' | ' + c.name + (r.risk ? ' -> ' + r.risk : ' -> ok'));
|
||||
ok ? pass++ : fail++;
|
||||
}
|
||||
console.log(`\n--- SKIP 检测(账号注销/管控) ---`);
|
||||
const skipCases = [
|
||||
{ name: '账号注销完整提示 → 应命中 SKIP', s: '对方账号已自行注销或者因违反平台规则被管控,当前无法回复。', expect: true },
|
||||
{ name: '仅“违反平台规则被管控”片段 → 应命中 SKIP', s: '该成员因违反平台规则被管控', expect: true },
|
||||
{ name: '正常聊天 → 不应命中 SKIP', s: 'hello, please send price', expect: false },
|
||||
{ name: '含“管控”但非违规语境(管控措施) → 不应误命中', s: '我们公司有严格的管控措施', expect: false },
|
||||
];
|
||||
for (const c of skipCases) {
|
||||
const got = !!hitsSkipKeyword(c.s);
|
||||
const ok = got === c.expect;
|
||||
console.log((ok ? 'PASS' : 'FAIL') + ' | ' + c.name + ' -> ' + (got ? 'SKIP' : 'ok'));
|
||||
ok ? pass++ : fail++;
|
||||
}
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user