@hunter-mate/sync-entities 0.1.0 → 0.2.1
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/dist/entities.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/validators.d.ts +27 -0
- package/dist/validators.js +145 -0
- package/package.json +1 -1
package/dist/entities.js
CHANGED
|
@@ -590,7 +590,7 @@ exports.SYNC_ENTITIES = [
|
|
|
590
590
|
ts('parseStatus', '解析状态'),
|
|
591
591
|
num('anomalyScore', '异常评分'),
|
|
592
592
|
dt('reportedAt', '上报时间'),
|
|
593
|
-
{ ...ts('html', 'HTML'), long: true },
|
|
593
|
+
{ ...ts('html', 'HTML'), long: true, previewMode: 'iframe' },
|
|
594
594
|
num('id', 'ID'),
|
|
595
595
|
dt('snapshotTimestamp', '快照时间'),
|
|
596
596
|
dt('createdAt', '创建时间'),
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析字段有效性校验 SSOT(FID-113-quality-issue-governance iter1)
|
|
3
|
+
*
|
|
4
|
+
* 纯函数、零框架依赖:hunter-mate 在解析落库前校验上报遥测,
|
|
5
|
+
* dev-ops 用同一套规则重扫已同步数据 —— 两端规则天然一致,防漂移。
|
|
6
|
+
*
|
|
7
|
+
* 契约:校验不通过的字段值**照常入库**(缺失走 missingFields),
|
|
8
|
+
* 这里只产出问题清单供上报/检索。value 一律先 trim,截断到 200 字符。
|
|
9
|
+
*/
|
|
10
|
+
export type QualityEntityKind = 'talent' | 'job' | 'company';
|
|
11
|
+
export interface FieldIssue {
|
|
12
|
+
/** payload 字段名(camelCase) */
|
|
13
|
+
field: string;
|
|
14
|
+
/** 实际值(trim 后,≤200 字符) */
|
|
15
|
+
value: string;
|
|
16
|
+
/** 规则 id(稳定键,dev-ops 可按规则过滤) */
|
|
17
|
+
rule: string;
|
|
18
|
+
/** 中文原因(展示用,用户心智词) */
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 校验单个实体的 payload 字段值,返回问题清单(可能为空数组)。
|
|
23
|
+
* entity: 解析侧 pageClass(talent/job/company);dev-ops 扫描侧由调用方映射。
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateEntityFields(entity: QualityEntityKind, payload: Record<string, unknown>): FieldIssue[];
|
|
26
|
+
/** dev-ops 侧:同步实体键(hm_sync_items.entity 表名)→ 校验实体种类 */
|
|
27
|
+
export declare const SYNC_ENTITY_TO_KIND: Record<string, QualityEntityKind>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 解析字段有效性校验 SSOT(FID-113-quality-issue-governance iter1)
|
|
4
|
+
*
|
|
5
|
+
* 纯函数、零框架依赖:hunter-mate 在解析落库前校验上报遥测,
|
|
6
|
+
* dev-ops 用同一套规则重扫已同步数据 —— 两端规则天然一致,防漂移。
|
|
7
|
+
*
|
|
8
|
+
* 契约:校验不通过的字段值**照常入库**(缺失走 missingFields),
|
|
9
|
+
* 这里只产出问题清单供上报/检索。value 一律先 trim,截断到 200 字符。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SYNC_ENTITY_TO_KIND = void 0;
|
|
13
|
+
exports.validateEntityFields = validateEntityFields;
|
|
14
|
+
const trimVal = (v) => String(v ?? '').trim();
|
|
15
|
+
const clip = (s) => (s.length > 200 ? `${s.slice(0, 197)}…` : s);
|
|
16
|
+
const isNonEmpty = (v) => v !== null && v !== undefined && trimVal(v) !== '';
|
|
17
|
+
/** 站点 UI 固定文案白名单(通知/导航/登录类,命中即判定抓错了元素) */
|
|
18
|
+
const UI_STRINGS = new Set([
|
|
19
|
+
'通知', '消息', '首页', '我的', '职位', '公司', '搜索', '加入', '登录', '注册',
|
|
20
|
+
'Messaging', 'Notifications', 'Jobs', 'Home', 'Sign in', 'Join now', 'Try Premium',
|
|
21
|
+
'尝试高级版', '我的职位', '职位搜索',
|
|
22
|
+
]);
|
|
23
|
+
/** 平台品牌名(值为平台名而非实体名 = 选择器抓到了站点标题) */
|
|
24
|
+
const SITE_BRANDS = new Set([
|
|
25
|
+
'LinkedIn', '领英', 'Liepin', '猎聘', '前程无忧', '51job', 'Indeed', 'Glassdoor',
|
|
26
|
+
]);
|
|
27
|
+
/** 通知角标文本(LinkedIn "0 个通知" 实证:铃铛徽标被当姓名入库) */
|
|
28
|
+
const BADGE_RE = /^\d{1,3}\s*个?(通知|消息|未读消息)$/;
|
|
29
|
+
const BADGE_RE_EN = /^\d{1,3}\s+(new\s+)?(notifications?|messages?)$/i;
|
|
30
|
+
const EMAIL_FIELDS = ['email', 'contactEmail', 'applyEmail'];
|
|
31
|
+
const URL_FIELDS = ['profileUrl', 'sourceUrl', 'url', 'companyUrl', 'applyUrl', 'website'];
|
|
32
|
+
const RULES = [
|
|
33
|
+
{
|
|
34
|
+
id: 'ui-badge-text',
|
|
35
|
+
appliesTo: [],
|
|
36
|
+
fields: ['name', 'title', 'headline'],
|
|
37
|
+
check: (v) => BADGE_RE.test(v) || BADGE_RE_EN.test(v)
|
|
38
|
+
? '命中通知角标文本(站点徽标,不是真实名称)'
|
|
39
|
+
: null,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'empty-name',
|
|
43
|
+
appliesTo: [],
|
|
44
|
+
fields: ['name', 'title'],
|
|
45
|
+
check: (v) => (v.length <= 1 ? '关键标识字段为空或过短' : null),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'ui-string',
|
|
49
|
+
appliesTo: [],
|
|
50
|
+
fields: ['name', 'title', 'headline'],
|
|
51
|
+
check: (v) => (UI_STRINGS.has(v) ? '命中站点界面固定文案' : null),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'numeric-name',
|
|
55
|
+
appliesTo: ['talent', 'company'],
|
|
56
|
+
fields: ['name'],
|
|
57
|
+
check: (v) => (/^\d+$/.test(v) ? '纯数字不可能是人名或公司名' : null),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'site-brand-name',
|
|
61
|
+
appliesTo: ['talent', 'company'],
|
|
62
|
+
fields: ['name'],
|
|
63
|
+
check: (v) => (SITE_BRANDS.has(v) ? '值为招聘平台品牌名而非实体名' : null),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'overlong-name',
|
|
67
|
+
appliesTo: [],
|
|
68
|
+
fields: ['name', 'title', 'headline'],
|
|
69
|
+
check: (v) => (v.length > 100 ? '长度过长,疑似整段描述泄漏进名称字段' : null),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'email-format',
|
|
73
|
+
appliesTo: [],
|
|
74
|
+
fields: EMAIL_FIELDS,
|
|
75
|
+
check: (v) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : '邮箱格式非法'),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'url-format',
|
|
79
|
+
appliesTo: [],
|
|
80
|
+
fields: URL_FIELDS,
|
|
81
|
+
check: (v) => {
|
|
82
|
+
try {
|
|
83
|
+
const u = new URL(v);
|
|
84
|
+
return u.protocol === 'http:' || u.protocol === 'https:' ? null : 'URL 协议非法';
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return 'URL 格式非法';
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'salary-range',
|
|
93
|
+
appliesTo: ['job'],
|
|
94
|
+
fields: [],
|
|
95
|
+
check: (_v, _field, payload) => {
|
|
96
|
+
const min = Number(payload.salaryMin);
|
|
97
|
+
const max = Number(payload.salaryMax);
|
|
98
|
+
if (!Number.isFinite(min) && !Number.isFinite(max))
|
|
99
|
+
return null;
|
|
100
|
+
if (Number.isFinite(min) && Number.isFinite(max) && min > max)
|
|
101
|
+
return '薪资下限大于上限';
|
|
102
|
+
const cap = 10_000_000;
|
|
103
|
+
if ((Number.isFinite(min) && (min < 0 || min > cap)) ||
|
|
104
|
+
(Number.isFinite(max) && (max < 0 || max > cap)))
|
|
105
|
+
return '薪资数值越界';
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
/**
|
|
111
|
+
* 校验单个实体的 payload 字段值,返回问题清单(可能为空数组)。
|
|
112
|
+
* entity: 解析侧 pageClass(talent/job/company);dev-ops 扫描侧由调用方映射。
|
|
113
|
+
*/
|
|
114
|
+
function validateEntityFields(entity, payload) {
|
|
115
|
+
if (!payload || typeof payload !== 'object')
|
|
116
|
+
return [];
|
|
117
|
+
const issues = [];
|
|
118
|
+
for (const rule of RULES) {
|
|
119
|
+
if (rule.appliesTo.length > 0 && !rule.appliesTo.includes(entity))
|
|
120
|
+
continue;
|
|
121
|
+
if (rule.fields.length === 0) {
|
|
122
|
+
// 载荷级规则(如薪资区间):对包含相关字段的实体触发一次
|
|
123
|
+
const reason = rule.check('', '', payload);
|
|
124
|
+
if (reason)
|
|
125
|
+
issues.push({ field: '(payload)', value: '', rule: rule.id, reason });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const field of rule.fields) {
|
|
129
|
+
const raw = payload[field];
|
|
130
|
+
if (!isNonEmpty(raw))
|
|
131
|
+
continue; // 缺失交给 missingFields,这里只管"值本身非法"
|
|
132
|
+
const value = trimVal(raw);
|
|
133
|
+
const reason = rule.check(value, field, payload);
|
|
134
|
+
if (reason)
|
|
135
|
+
issues.push({ field, value: clip(value), rule: rule.id, reason });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return issues;
|
|
139
|
+
}
|
|
140
|
+
/** dev-ops 侧:同步实体键(hm_sync_items.entity 表名)→ 校验实体种类 */
|
|
141
|
+
exports.SYNC_ENTITY_TO_KIND = {
|
|
142
|
+
talent_profiles: 'talent',
|
|
143
|
+
job_postings: 'job',
|
|
144
|
+
company_profiles: 'company',
|
|
145
|
+
};
|
package/package.json
CHANGED