关于 email-enrichment
Email Enrichment 是一项技能,可将邮箱列表转换为联系人档案,也可根据人员名单查找联系方式。它通过分阶段查询,结合邮箱分类、公司与人员匹配、结果验证及后备搜索。
主要功能
- 01分类并去重邮箱输入
- 02根据邮箱域名匹配公司
- 03补充工作、教育和个人邮箱信息
- 04核验雇主与姓名匹配
- 05采用分阶段搜索后备方案
适用场景
- 01根据邮箱列表建立联系人档案
- 02为人员名单查找商务联系方式
- 03查找未匹配的个人邮箱归属
MCP 相关
该技能通过 `execute({ code })` 脚本和 `callTool` 调用 Crustdata 查询,使用 `company_identify`、`person_enrich`、`person_search` 和 `web_search_live` 等工具。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ crustdata/ skills --skill email-enrichment安装后在 Claude Code 中输入/email-enrichment确认已加载。
附带文件evals/evals.jsonREADME.md
详细介绍
电子邮件信息补全
两个方向,一项技能:
- 电子邮件转个人信息 - 将一组电子邮件地址转换为丰富的联系人资料(姓名、职称、公司、个人资料 URL)。采用六阶段瀑布式流程,优化覆盖率和准确性。
- 个人信息转电子邮件 - 为一组人员查找工作电子邮件、个人电子邮件和电话号码。使用包含个人联系信息的信息补全,并为技术人员提供基于提交历史记录的后备方案。
概览
该方法采用严格的六阶段瀑布式流程。每个阶段都会捕获前面阶段遗漏的电子邮件。各阶段按成本(先免费,再按费用从低到高)和可靠性(先精确度最高的阶段)排序。
所有 Crustdata 操作都在 execute({ code }) 脚本中运行——编写简短的纯 JavaScript 脚本,其唯一的 I/O 是 const r = await callTool("<tool>", params),然后根据 r.ok 进行分支处理。请根据 get_schema 提供的 TypeScript 类型化工具接口编写脚本,但脚本正文中不要添加任何类型注解——: Type、as 或 interface 都会导致解析错误,使整个运行失败。请参阅下方各阶段的脚本。
| 阶段 | Crustdata 工具(通过 callTool 调用) | 目标 | 费用 |
|---|---|---|---|
| 1 | company_identify | 工作邮箱 + 教育邮箱 | 免费 |
| 2 | person_enrich(business_emails)+ 后续验证 | 工作邮箱 + 教育邮箱 | 约 1-2 个积分/资料 |
| 2(个人邮箱) | batch_person_identify(异步) | 个人邮箱 | 每个匹配结果 1 个积分 |
| 3 | person_search(姓名+公司) | 遗漏的工作/教育邮箱 | 约 0.03 个积分/结果 |
| 4 | person_search(仅姓名) | 剩余个人邮箱 | 约 0.03 个积分/结果 |
| 5 | web_search_live + person_enrich | 所有剩余项 | 每个查询 1 个积分 + 信息补全费用 |
| 6 | 评分门槛 | 第 3-5 阶段的所有候选项 | 不适用 |
覆盖率:
| 类别 | 个人匹配率 | 公司匹配率 |
|---|---|---|
| 工作邮箱 | 95%+ | 95%+ |
| 教育邮箱 | 95%+ | 95%+ |
| 个人邮箱 | 95%+ | 不适用 |
| 综合 | 95%+ | 95%+ |
阶段 0:解析输入并对电子邮件分类
读取输入
接受 CSV 文件、电子表格(.xlsx/.csv)或内嵌列表。提取所有电子邮件地址并去重。
将每个电子邮件归为以下三类之一
个人邮箱域名(与此列表进行匹配):
gmail.com, yahoo.com, hotmail.com, outlook.com, aol.com, icloud.com, me.com,
live.com, protonmail.com, proton.me, msn.com, ymail.com, comcast.net, att.net,
verizon.net, mac.com, fastmail.com, hey.com, pm.me, zoho.com, gmx.com,
googlemail.com
教育邮箱域名(与这些顶级域名模式进行匹配):
.edu, .ac.uk, .ac.jp, .ac.kr, .ac.in, .ac.nz, .ac.za
工作邮箱:其他所有邮箱。
从电子邮件前缀提取姓名
按句点、下划线和连字符拆分本地部分(@ 前的部分)。移除完全由数字组成的部分。将其余部分的首字母大写。只保留长度至少为 2 个字符的部分。
import re
def extract_name_parts(email):
local = email.split("@")[0]
parts = re.split(r'[._\-]', local)
parts = [p for p in parts if not p.isdigit()]
parts = [p.capitalize() for p in parts if len(p) >= 2]
return parts
# Examples:
# "[email protected]" -> ["Daniel", "Lee"]
# "[email protected]" -> ["John", "Smith"]
# "[email protected]" -> ["Jsmith"]
# "[email protected]" -> ["Rodriguez"]
阶段 1:识别公司(免费)
识别每个非个人邮箱域名所属的公司。此阶段免费,应始终首先运行。
脚本
company_identify 接受 domains 数组,因此请先对域名去重(1,000 个工作邮箱可能只有 200 个唯一域名),然后在一次调用中解析所有域名。
// model query: identify the company behind each work/edu email domain
const domains = [...new Set(workEduEmails.map(e => e.split("@")[1].toLowerCase()))];
const r = await callTool("company_identify", { domains });
if (!r.ok) return { error: r.message };
// build domain_map: domain -> { name, primary_domain, professional_network_url }
const domainMap = {};
for (const rec of r.data) {
const info = rec.matches?.[0]?.company_data?.basic_info;
if (info) domainMap[rec.matched_on.toLowerCase()] = {
name: info.name,
primary_domain: info.primary_domain,
professional_network_url: info.professional_network_url,
};
}
return { domainMap };
返回内容
一个 { matched_on, match_type, matches: [{ confidence_score, company_data }] } 数组。从 matches[0].company_data.basic_info 中读取公司信息(name、primary_domain、website、professional_network_url、description、year_founded 等)。matched_on 是你传入的域名,因此可以直接将结果映射回输入项。
运行方式
将结果存储在 domain_map 中:domain -> { name, ... }。此 domainMap 会返回并传入后续阶段(通过 inputs),供阶段 3 验证——不要对每个域名重复调用这项免费工具。
预期结果
- 95% 以上的工作邮箱域名都能被识别
- 几乎 100% 的教育邮箱域名都能被识别(大学广为人知)
阶段 2:通过工作邮箱补全个人信息(工作/教育)
使用个人信息补全功能直接查询每个工作/教育邮箱。阶段 2 根据电子邮件类型分支处理:
- 工作/教育邮箱 -> 使用
business_emails标识符并进行后续验证(见下方分支 A) - 个人邮箱 -> 参见分支 B:使用异步
batch_person_identify流程,并以基于姓名的阶段作为后备方案
分支 A:工作/教育邮箱(business_emails)
脚本
person_enrich 接受一个 business_emails 数组(每次调用最多 25 个),因此请批量处理工作/教育邮箱。传入 fields: ["basic_profile", "experience"],以获取验证后所需的姓名和雇主信息。每次调用都会为每个输入邮箱返回一条记录;通过 matched_on 将结果映射回原邮箱。
// model query: enrich work/edu emails to find the person behind each
const r = await callTool("person_enrich", {
business_emails: workEduEmailBatch, // ≤25 emails
fields: ["basic_profile", "experience"], // name + current/past employers
});
if (!r.ok) return { error: r.message };
// r.data is an ARRAY of { matched_on, match_type, matches: [{ confidence_score, person_data }] }
const byEmail = {};
for (const rec of r.data) {
const pd = rec.matches?.[0]?.person_data;
if (pd) byEmail[rec.matched_on.toLowerCase()] = pd; // post-verify before accepting
}
return { byEmail };
如果工作/教育邮箱超过 25 个,请使用 chunk(emails, 25),然后执行 await parallelMap(batches, b => callTool("person_enrich", { business_emails: b, fields: ["basic_profile", "experience"] }))。
关键细节
business_emails接受邮箱字符串数组(每次调用最多 25 个)- 每次调用只传入一种标识符——
professional_network_profile_urls和business_emails不能在同一次调用中组合使用 - 尽管名称是“business_emails”,但教育邮箱也适用(尤其是教职员工邮箱)
- 返回的
person_data包含:basic_profile.name、basic_profile.headline、social_handles.professional_network_identifier.profile_url,以及experience.employment_details.current[]/.past[](雇主) - 付款前设置
preview: true,即可免费检查基础资料,不消耗额度
如果返回了匹配结果(matches[0].person_data 中有姓名),则在接受结果前必须通过后续验证(见下文“后续验证”部分)。
分支 B:个人邮箱(反向个人邮箱查询)
仅支持异步: 个人邮箱(gmail/yahoo/outlook)需通过异步工具
batch_person_identify解析——这是唯一能匹配个人邮箱的工具。同步工具无法做到:person_enrich只接受professional_network_profile_urls或business_emails作为 标识符,person_enrich_live只接受个人资料 URL,而person_search没有emails筛选列。操作方法: 提交
batch_person_identify({ emails: [...] })(每个作业最多 300 个),轮询batch_job_get(batch_id),直到状态变为completed,然后读取batch_results行——matches[0].person_data携带与分支 A 相同的 ID、基本资料和个人资料 URL 结构。 每个匹配的标识符消耗 1 个额度;未匹配的标识符不收费。对于无法匹配的邮箱,请依次执行:
- 阶段 4 — 使用从邮箱前缀提取的全名调用
person_search(前缀符合清晰的first.last模式时有效),然后使用返回的个人资料 URL 调用person_enrich。- 阶段 5 — 使用
web_search_live({ query: "who is EMAIL", sources: ["ai"] })确定所有者姓名,然后调用person_search→person_enrich。
后验证(Phase 2 分支 A 的每个结果都必须执行——仅限 work/edu)
人员 enrich API 可能返回错误匹配:此人在正确的公司任职,但并非该邮箱的所有者;或者此人已不再在那里任职。每个结果都必须通过以下检查。检查针对上述 enrich 脚本返回的 person_data 执行——雇主信息位于 experience.employment_details.current[] / .past[](每个条目包含公司名称 name、职位 title 和公司网站域名 company_website_domain)。
检查 1:雇主域名验证。 邮箱域名必须出现在此人当前或过去雇主的网站域名中。例如,[email protected] 的至少一个雇主域名列表中必须包含 sapphireventures.com。如果任何雇主(当前或过去)的域名都不包含该域名,则拒绝此匹配。
def verify_employer_domain(person_data, email_domain):
domain_base = email_domain.lower().split('.')[0]
emp_details = person_data.get("experience", {}).get("employment_details", {})
employers = emp_details.get("current", []) + emp_details.get("past", [])
for emp in employers:
domains = emp.get("company_website_domain") or []
if isinstance(domains, str):
domains = [domains]
for d in domains:
if email_domain.lower() in d.lower() or d.lower() in email_domain.lower():
return True, emp.get("name", "")
if len(domain_base) > 3 and domain_base in emp.get("name", "").lower().replace(" ", ""):
return True, emp.get("name", "")
return False, None
检查 2:姓名前缀匹配。 邮箱前缀应与返回人员的姓名合理匹配。例如,[email protected] 应匹配姓名中包含“alling”的人(如“Ted Alling”),而不是“Santosh Sankar”。检查个人资料姓名中的任意部分是否以与邮箱前缀相同的字符开头,或是否符合名字首字母 + 姓氏的模式。
def name_matches_prefix(profile_name, email_prefix):
pn_parts = profile_name.lower().split()
prefix = email_prefix.lower()
for part in pn_parts:
if prefix.startswith(part[:3]) or part.startswith(prefix[:3]):
return True
# First-initial + lastname pattern: "talling" = "t" + "alling"
if len(prefix) > 2:
for i in range(1, min(3, len(prefix))):
if prefix[i:] in part and len(prefix[i:]) > 2:
return True
return False
检查 3:使用 AI 修正姓名不匹配。 当雇主域名匹配但姓名不匹配时(公司正确,人员错误),使用深度研究模式的 web_search_live(sources: ["ai"])查找邮箱的实际所有者:
// model query: who actually owns [email protected]
const r = await callTool("web_search_live", {
query: "who is [email protected]",
sources: ["ai"],
});
const aiText = r.ok ? r.data.results?.map((x) => x.snippet || x.title).join(" ") : "";
return { aiText };
AI 通常会返回类似“属于 Lamp Post Group 的合伙人 Ted Alling”的信息。提取真实姓名,并使用更正后的姓名 + 公司搜索人员数据库(Phase 3 person_search)。
测试中,这一步找回了 7 个正确匹配,否则这些匹配都会丢失。
预期结果
- Work/edu(分支 A): 约 58% 的 work+edu 邮箱通过所有验证检查。约 5% 会被雇主域名检查拒绝(完全匹配错人)。约 1% 会由 AI 修正(公司正确,人员错误 -> AI 找到真实姓名)。
- Personal(分支 B): 通过异步的
batch_person_identify直接匹配;未匹配的邮箱会通过基于姓名的 Phase 4/5 在下游解析,精确度由 Phase 6 控制。
Phase 3:按姓名 + 公司搜索人员
对于 Phase 2 未匹配到的 work/edu 邮箱,尝试按姓名 + 公司搜索。从邮箱前缀中提取姓名猜测,并与 Phase 1 中识别的公司结合。
使用场景
仅适用于满足以下条件的邮箱:
- Phase 2 未返回匹配结果
- 邮箱属于 work 或 edu(不是 personal)
- Phase 1 已识别出域名(已知公司名称)
- 可以从邮箱前缀中提取至少一个姓名部分
脚本
筛选键始终为 field。人员姓名筛选使用 basic_profile.name;当前雇主名称筛选使用 experience.employment_details.current.company_name;limit 用于限制页大小。
// model query: find FirstName at CompanyName
const r = await callTool("person_search", {
filters: { op: "and", conditions: [
{ field: "basic_profile.name", type: "(.)", value: "FirstName" },
{ field: "experience.employment_details.current.company_name", type: "(.)", value: "CompanyName" },
]},
limit: 3,
fields: ["basic_profile", "social_handles"], // name + profile_url
});
if (!r.ok) return { error: r.message };
// r.data.profiles[]: read basic_profile.name and
// social_handles.professional_network_identifier.profile_url
return { profiles: (r.data.profiles ?? []).map((p) => ({
name: p.basic_profile?.name,
profile_url: p.social_handles?.professional_network_identifier?.profile_url,
})) };
验证(必须执行)
返回的个人资料姓名必须包含从邮箱前缀中提取的名字:
def verify_name_match(email_name_parts, profile_name):
if not email_name_parts or not profile_name:
return False
return email_name_parts[0].lower() in profile_name.lower()
运行方法
对于每个未匹配的 work/edu 邮箱,提取姓名,并从 domain_map 中查找公司:
// model query: find Kyle at Backswing Ventures (email [email protected])
// Name parts: ["Kyle"]; company from Phase 1 domainMap: "Backswing Ventures"
const r = await callTool("person_search", {
filters: { op: "and", conditions: [
{ field: "basic_profile.name", type: "(.)", value: "Kyle" },
{ field: "experience.employment_details.current.company_name", type: "(.)", value: "Backswing Ventures" },
]},
limit: 3,
fields: ["basic_profile", "social_handles"],
});
return r.ok ? { profiles: r.data.profiles } : { error: r.message };
检查每个返回的个人资料:basic_profile.name 中是否出现“kyle”?如果出现,即为匹配。
预期结果
- 使用姓名 + 公司组合搜索,找到 Phase 2 未匹配到的邮箱
- 对姓名格式清晰的邮箱效果最佳(john.smith@、daniel_lee@)
候选人核验(关键——不得跳过)
如果不进行核验,姓名/子串匹配会产生误报。例如,搜索“wraecca”可能会匹配到“Alessandro Racca”,因为“racca”是其中的子串。
必须对每位候选人核验以下所有内容:
步骤 1——姓名核验(所有类型的电子邮件都必须核验):
- 从电子邮件前缀中提取姓名部分
- 如果有 2 个或更多姓名部分:名和姓都必须出现在个人资料姓名中
- 如果只有 1 个姓名部分:该部分必须出现在个人资料姓名中,且长度不少于 3 个字符
步骤 2——组织核验(工作邮箱和 edu 邮箱必须核验):
- 从 domain_map(阶段 1)中查找公司/机构
- 从组织名称中提取有意义的词(跳过“the”“inc”“llc”“of”等常见词)
- 个人资料数据中的某处必须至少包含一个有意义的组织名称词(检查雇主、教育经历、标题)
- 如果没有匹配的组织名称词,即使姓名匹配也必须拒绝该结果
步骤 3——个人邮箱(仅核验姓名):
- 对于个人邮箱(gmail、yahoo 等),没有组织可供交叉核对
- 步骤 1 中的姓名匹配是唯一的判断标准
- 这意味着个人邮箱匹配的精确度较低
搜索个人资料会在 basic_profile.name 中包含姓名;核验前请将其展平。
def verify_candidate_match(email, name_parts, profile, domain_map):
profile_name = profile.get("basic_profile", {}).get("name", "").lower()
# Name verification
if len(name_parts) >= 2:
if not (name_parts[0].lower() in profile_name and name_parts[-1].lower() in profile_name):
return False
elif len(name_parts) == 1 and len(name_parts[0]) > 2:
if name_parts[0].lower() not in profile_name:
return False
else:
return False
# Org verification for work/edu
domain = email.split("@")[1]
company_info = domain_map.get(domain)
if company_info:
org_name = company_info.get("name", "")
skip = {"the", "inc", "llc", "ltd", "co", "corp", "of", "and", "for", "university", "college"}
org_words = [w.lower() for w in org_name.split() if w.lower() not in skip and len(w) > 2]
profile_str = str(profile).lower()
if org_words and not any(w in profile_str for w in org_words):
return False
return True
预期结果
- 对于工作邮箱/edu 邮箱:姓名和公司都必须通过核验。阶段 1 中未找到公司 = 自动拒绝。
- 对于个人邮箱:只需姓名匹配(精确度较低,但总比不核验好)
阶段 4:按姓名搜索个人邮箱对应的人物
对于能够从电子邮件前缀中提取出合理全名的个人邮箱,这是最后的手段。
使用时机
仅适用于满足以下条件的邮箱:
- 所有前序阶段均未返回匹配结果
- 该邮箱是个人邮箱(gmail、yahoo 等)
- 可以从电子邮件前缀中提取至少 2 个姓名部分
脚本
// model query: find FirstName LastName by name
const r = await callTool("person_search", {
filters: { op: "and", conditions: [
{ field: "basic_profile.name", type: "(.)", value: "FirstName LastName" },
]},
limit: 5,
fields: ["basic_profile", "social_handles"],
});
if (!r.ok) return { error: r.message };
return { profiles: r.data.profiles ?? [] };
验收标准
- 搜索必须返回 3 条或更少的结果(歧义较低)
- 如果返回 4 条或更多结果,则跳过——可能的匹配项过多
- 返回的姓名必须与提取出的姓名部分合理匹配
运行方法
// model query: find Joanne Bradford (email [email protected]) by name
// Name parts: ["Joanne", "Bradford"]
const r = await callTool("person_search", {
filters: { op: "and", conditions: [
{ field: "basic_profile.name", type: "(.)", value: "Joanne Bradford" },
]},
limit: 5,
fields: ["basic_profile", "social_handles"],
});
return r.ok ? { profiles: r.data.profiles } : { error: r.message };
如果返回 1–3 条结果,取第一条。如果返回 0 条或 4 条及以上,则标记为未匹配。
预期结果
- 找出剩余的、姓名格式清晰的个人邮箱,例如 first.last
- 结果上限为 3 条,可避免为常见姓名匹配到错误的人
- 必须核验:电子邮件前缀中的两个姓名部分都必须出现在返回的个人资料姓名中。不要直接接受第一条结果。
阶段 5:对所有剩余未匹配邮箱使用网页搜索作为后备方案
对于前序所有阶段均未找到匹配项的邮箱,这是最后的后备方案。通过网页搜索查找个人资料 URL,然后通过该 URL 获取更多信息。此方法可查找虚荣域名(例如 carolewainaina.com)、个人品牌域名,以及任何未被 Crustdata 数据库收录的邮箱。
使用时机
适用于阶段 1–5 后仍未匹配的任何邮箱,不论类别(工作邮箱、edu 邮箱或个人邮箱)。
步骤 1:网页搜索个人资料 URL
// model query: find a professional-network profile URL for EMAIL
const r = await callTool("web_search_live", {
query: "EMAIL linkedin",
sources: ["web"],
});
// r.data.results[]: { source, title, url, snippet, ... }
const hit = r.ok && (r.data.results ?? []).find((x) => /linkedin\.com\/in\//.test(x.url || ""));
return { profile_url: hit?.url ?? null };
检查结果中是否有包含 linkedin.com/in/ 的 URL。如果找到,则继续执行步骤 3。
步骤 2:对姓名使用深度研究模式(如果步骤 1 未找到个人资料 URL)
// model query: who owns EMAIL
const r = await callTool("web_search_live", {
query: "who is EMAIL",
sources: ["ai"],
});
const aiText = r.ok ? (r.data.results ?? []).map((x) => x.snippet || x.title).join(" ") : "";
return { aiText };
AI 的回复通常会写出类似“属于 Carole Wamuyu Wainaina”或“与 Company X 的 John Smith 相关联”的内容。提取此人的姓名,并搜索人物数据库:
// model query: find <Extracted Name> by name
const r = await callTool("person_search", {
filters: { op: "and", conditions: [
{ field: "basic_profile.name", type: "(.)", value: "Extracted Name" },
]},
limit: 3,
fields: ["basic_profile", "social_handles"],
});
// take social_handles.professional_network_identifier.profile_url off the best match
return r.ok ? { profiles: r.data.profiles } : { error: r.message };
如果 person_search 返回的结果中包含个人资料 URL(social_handles.professional_network_identifier.profile_url),则继续执行步骤 3。
步骤 3:通过个人资料 URL 获取更多信息
// model query: enrich the resolved profile URL to get the person's details
const r = await callTool("person_enrich", {
professional_network_profile_urls: ["PROFILE_URL_FROM_STEP_1_OR_2"],
fields: ["basic_profile", "experience"],
});
// r.data[0].matches[0].person_data: basic_profile.name, basic_profile.headline, employers
return r.ok ? { person: r.data?.[0]?.matches?.[0]?.person_data } : { error: r.message };
预期结果
- 找出虚荣域名/个人域名(carolewainaina.com、first-last.com)
- 找出未通过邮箱索引、但可通过网页搜索找到的人
- 深度研究模式在解决“谁拥有此邮箱”这类查询时尤其有效
第 6 阶段:评分门槛(应用于第 3–5 阶段的所有候选项)
第 3、4 和 5 阶段产生的所有候选匹配项,在被接受前都必须通过此评分门槛。第 2 阶段的结果不受此限制——分支 A 有自己的雇主域名 + 姓名后置验证,分支 B 的匹配项以电子邮件本身为依据(API 直接匹配了标识符)。
硬性要求(两项都必须满足)
- name_sim > 0.8——从电子邮件前缀提取的姓名与候选者资料姓名之间的相似度必须超过 0.8。这可以防止公司匹配完美却弥补了姓名匹配不佳的情况。
- combined_score > 0.7——综合评分(包含姓名相似度、公司匹配及其他信号)必须超过 0.7。
第 6 阶段要求同时满足 name_sim > 0.8 和 combined_score > 0.7。这可以防止公司匹配完美却弥补了姓名匹配不佳的情况。例如,如果找到的人在正确的公司任职,但姓名与电子邮件前缀不相似,即使公司匹配完美,也会被拒绝。
候选项未通过时
如果候选项未通过评分门槛,就会被拒绝,电子邮件将继续进入下一阶段。如果没有任何阶段产生通过门槛的候选项,该电子邮件将被标记为 UNMATCHED。
扩展与并发
速率限制由宿主在 execute 内处理——无需手动管理每分钟请求数表、休眠或退避。处理大型列表时,可在一个脚本中通过扇出任务来扩展:
- 批量丰富。
person_enrich和company_identify接受数组——每次调用最多传入 25 个电子邮件地址或域名。先调用chunk(emails, 25),然后执行await parallelMap(batches, b => callTool("person_enrich", { business_emails: b, fields: [...] }))。 - 并行执行相互独立的调用,使用
await parallelMap(items, fn)——不要使用for … await。保持有依赖关系的步骤(搜索 → 丰富)按顺序执行;只在同一阶段内并行处理。 - 返回最精简的投影数据——只有你
return的内容会传递给模型。
优化:在第 1 阶段对域名去重
1,000 个工作邮箱可能只有 200 个不同的域名。调用 company_identify(接受 domains 数组)之前,务必先对域名去重,然后在一次调用中解析所有不重复的域名。
进度保存与恢复
处理大型列表时,在每个阶段结束后将进度保存到 JSON 文件,以便在处理中断后恢复丰富流程。
进度文件格式
{
"phase_completed": 3,
"domain_map": {
"acme.com": {"name": "Acme Corp"},
"stanford.edu": {"name": "Stanford University"}
},
"results": {
"[email protected]": {
"name": "John Smith",
"headline": "VP Engineering at Acme",
"company": "Acme Corp",
"method": "person_enrich"
}
},
"unmatched_emails": ["[email protected]"]
}
恢复逻辑
启动时,检查是否存在进度文件。如果存在,则跳过已完成的阶段,并从中断处继续。
输出
CSV 输出
生成包含以下列的 CSV:
| 列 | 说明 |
|---|---|
email | 原始电子邮件地址 |
category | work、edu 或 personal |
person_name | 此人的全名 |
person_headline | 职位名称 / 个人简介标题 |
company_name | 公司或机构名称 |
profile_url | 职业资料 URL |
method | 找到匹配项的阶段:person_enrich、name+company、name_search、web_search |
汇总统计
最后打印汇总信息:
=== Email Enrichment Results ===
Total emails: {N}
Work: {W} | Person: {P1} ({P1%}) | Company: {C1} ({C1%})
Edu: {E} | Person: {P2} ({P2%}) | Company: {C2} ({C2%})
Personal: {R} | Person: {P3} ({P3%}) | Company: {C3} ({C3%})
Overall: {N} | Person: {PT} ({PT%}) | Company: {CT} ({CT%})
Breakdown by method:
Phase 2 (person_enrich): {count}
Phase 2B (batch_identify): {count}
Phase 3 (name+company): {count}
Phase 4 (name_search): {count}
Phase 5 (web_search): {count}
决策流程图
For each email:
|
+-- Classify: work / edu / personal
|
+-- Phase 1: Is it work or edu?
| +-- Yes -> Extract domain -> company_identify({ domains: [...] })
| | +-- Found company? -> Store in domain_map
| | +-- Not found? -> Continue (no company info for this domain)
| +-- No (personal) -> Continue to Phase 2 Branch B
|
+-- Phase 2: Branch by email type
| |
| +-- Work/edu (Branch A):
| | +-- person_enrich({ business_emails: [email], fields: ["basic_profile","experience"] })
| | +-- Found person? -> Post-verify (employer domain + name prefix + AI correction)
| | | +-- Verified? -> DONE (method=person_enrich)
| | | +-- Failed verification? -> Continue to Phase 3
| | +-- Not found? -> Continue to Phase 3
| |
| +-- Personal (Branch B): batch_person_identify({ emails: [...] }) -> poll batch_job_get -> batch_results
| +-- Matched? -> DONE (method=batch_identify)
| +-- Not matched? -> Continue to Phase 4 (name search) then Phase 5 (web search)
|
+-- Phase 3: Is it work/edu AND have company name AND name parts?
| +-- Yes -> person_search(filters: basic_profile.name + experience.employment_details.current.company_name)
| | +-- Found + name verified? -> Phase 6 scoring gate -> DONE (method=name+company)
| | +-- Not found? -> Continue to Phase 4
| +-- No -> Skip to Phase 4
|
+-- Phase 4: Is it personal AND has 2+ name parts?
| +-- Yes -> person_search(filters: basic_profile.name="FirstName LastName")
| | +-- 1-3 results returned + name verified? -> Phase 6 scoring gate -> DONE (method=name_search)
| | +-- 0 or 4+ results? -> Continue to Phase 5
| +-- No -> Continue to Phase 5
|
+-- Phase 5: Still unmatched? (any email type)
| +-- web_search_live({ query: "EMAIL linkedin", sources: ["web"] })
| | +-- Found linkedin.com/in/ URL? -> person_enrich({ professional_network_profile_urls: [URL] }) -> Phase 6 scoring gate -> DONE
| +-- No URL found? -> web_search_live({ query: "who is EMAIL", sources: ["ai"] })
| | +-- Extracted person name? -> person_search(name) -> get profile URL -> person_enrich -> Phase 6 scoring gate -> DONE
| +-- Nothing found? -> UNMATCHED
|
+-- Phase 6: Scoring gate (applied to all candidates from Phases 3-5)
+-- Requires BOTH: name_sim > 0.8 AND combined_score > 0.7
+-- Pass? -> Accept match
+-- Fail? -> Reject, continue to next phase or mark UNMATCHED
关键经验
-
验证不可妥协。 在对 1,476 封邮件进行测试时,严格验证剔除了未验证方法会返回的 149 个误报。务必进行验证。
-
第 2 阶段的后验证可捕获约 5% 的错误匹配。 人员丰富 API 有时会返回在正确公司工作的错误人员(例如另一位员工)。通过雇主域名和姓名前缀检查可以识别这些情况。
-
结合深度研究模式进行 AI 校正效果良好。 当人员丰富返回了正确的公司但错误的人员时,使用
sources: ["ai"]的web_search_live,并以 "who is EMAIL" 为查询,可以正确识别实际人员。测试中找回了 7 个匹配项。 -
未经验证时,姓名/子字符串候选项的误报率极高。 测试中:1,540 个被拒绝,37 个被接受。基于子字符串的姓名匹配会产生许多虚假匹配。严格验证姓名和公司信息至关重要。
-
个人邮箱反向查找仅支持异步方式。
person_enrich不支持个人邮箱标识符,person_search也没有emails筛选器field——唯一的直接途径是batch_person_identify(提交任务、轮询batch_job_get、读取batch_results);无法匹配的邮箱会转而进入基于姓名的第 4/5 阶段。 -
对于工作/教育邮箱,没有公司就没有匹配结果。 如果第 1 阶段未能识别某个域名对应的公司,切勿接受该域名邮箱的姓名搜索结果。没有可供验证的对象。
-
第 4 阶段必须验证姓名,不能只统计结果数量。 仅因返回结果数 ≤ 3 就接受首个结果,会产生诸如将 "Bert Zacharin" 匹配为 "Zacharie Bere" 之类的误报。邮箱中的姓名两部分都必须出现在个人资料姓名中。
-
教育邮箱可使用
person_enrich。 尽管标识符名为business_emails,但它也能匹配大学的教师和职员。 -
person_enrich每次调用只接受一种标识符类型——传入professional_network_profile_urls或business_emails,不能同时传入两者。每种都是一个数组(≤25)。付费前可使用preview: true,免费检查基础资料。 -
person_search将结果放在r.data.profiles下(所有内容首先都在r.data下)。调用失败不会中止脚本——务必根据r.ok(或unwrap)进行分支处理,否则未检查的失败会看起来像“没有结果”。 -
(.)是区分大小写的字面子字符串,不是正则表达式的交替匹配。 在人员搜索中,管道分隔的值(例如"a|b")无法匹配任何内容——有 N 个值时应使用any_of(field, [...])/in_。筛选时使用field;读取响应时应使用Returns:路径(例如social_handles.professional_network_identifier.profile_url,而不是用于筛选的experience.…字段)。 -
第 6 阶段要求
name_sim > 0.8和combined_score > 0.7两项条件同时满足。 这样可以避免完美的公司匹配弥补姓名匹配错误。没有name_sim硬性门槛时,诸如将 "Bert Zacharin" 匹配为 "Zacharie Bere" 之类的误报可能会通过。
人员到邮箱信息丰富
当输入是一组人员(姓名、个人资料 URL,或两者都有),目标是查找他们的邮箱地址时,请改用此流程。
第 1 步:解析个人资料 URL
如果输入中已有个人资料 URL,请跳过此步骤。
如果只提供姓名和公司信息,请先解析出个人资料 URL:
// model query: resolve Person Name at Company Name to a profile URL
const r = await callTool("person_search", {
filters: { op: "and", conditions: [
{ field: "basic_profile.name", type: "(.)", value: "Person Name" },
{ field: "experience.employment_details.current.company_name", type: "(.)", value: "Company Name" },
]},
limit: 3,
fields: ["basic_profile", "social_handles"],
});
if (!r.ok) return { error: r.message };
// profile URL lives at social_handles.professional_network_identifier.profile_url
return { profile_url: r.data.profiles?.[0]?.social_handles?.professional_network_identifier?.profile_url };
所需的个人资料 URL 位于 social_handles.professional_network_identifier.profile_url。
**备用方法:**如果人员数据库中未找到,请尝试网页搜索:
// model query: find Person Name's profile URL on the web
const r = await callTool("web_search_live", {
query: "Person Name Company site:linkedin.com/in/",
sources: ["web"],
});
const hit = r.ok && (r.data.results ?? []).find((x) => /linkedin\.com\/in\//.test(x.url || ""));
return { profile_url: hit?.url ?? null };
从顶部结果中提取个人资料 URL。
常见问题
- 常见姓名:务必加入公司或职务信息。"Michael Ma Liquid 2 Ventures",而不只是 "Michael Ma"。
- 姓名变体:尝试正式姓名和常用姓名,例如 "William Drevno" 与 "Will Drevno","Robert" 与 "Bob"。
- 近期更换职位:如果知道新旧公司,请用两家公司分别搜索。
第 2 步:补充商务邮箱
professional_network_profile_urls 是一个数组(每次调用 ≤25 个)。请求 contact 字段组以获取商务邮箱——默认不会返回这些邮箱。
// model query: get business emails for these profile URLs
const r = await callTool("person_enrich", {
professional_network_profile_urls: profileUrlBatch, // ≤25 URLs
fields: ["basic_profile", "contact"], // name + business_emails
});
if (!r.ok) return { error: r.message };
// r.data is an ARRAY of { matched_on, matches: [{ person_data }] }; map back via matched_on
return { results: r.data.map((rec) => ({
profile_url: rec.matched_on,
name: rec.matches?.[0]?.person_data?.basic_profile?.name,
business_emails: (rec.matches?.[0]?.person_data?.contact?.business_emails ?? []).map((b) => b.email),
})) };
重要细节
professional_network_profile_urls每次调用最多接受 25 个个人资料 URL(一个数组)- 商务邮箱会在
contact字段组中返回(contact.business_emails[].email)——在fields中请求contact - 每次调用只传入一种标识符类型——此处为
professional_network_profile_urls - 响应是一个数组。使用每条记录的
matched_on字段,将结果映射回输入的 URL。
处理大型列表
对于 25 个及以上的个人资料,先执行 chunk(profileUrls, 25),再并行分批调用:await parallelMap(batches, b => callTool("person_enrich", { professional_network_profile_urls: b, fields: ["basic_profile", "contact"] }))。只 return 精简后的 { profile_url, name, business_emails },以避免超出令牌限制。
第 3 步:补充个人邮箱和电话号码
contact 字段组会返回 business_emails、personal_emails 和 phone_numbers——请求该字段组以获取个人联系信息。先使用 preview: true(0 积分)检查个人资料是否包含个人联系数据,再决定是否付费获取。preview: true 受账户权限控制:如果返回 400 错误“Preview feature is not available for your account”,说明你的账户没有预览权限——跳过预览,直接补充信息(支付基础积分)。
// model query: get personal emails and phone numbers for these profiles
const r = await callTool("person_enrich", {
professional_network_profile_urls: profileUrlBatch, // ≤25 URLs
fields: ["basic_profile", "contact"], // personal_emails + phone_numbers
});
if (!r.ok) return { error: r.message };
return { results: r.data.map((rec) => {
const c = rec.matches?.[0]?.person_data?.contact ?? {};
return {
profile_url: rec.matched_on,
personal_emails: (c.personal_emails ?? []).map((e) => e.email ?? e),
phone_numbers: c.phone_numbers ?? [],
};
}) };
积分用量
person_enrich 的费用按附加项累加:基础费用 1,商务邮箱 +1、个人邮箱 +2、电话号码 +2、开发平台 +1——最高为 7。preview: true 不消耗积分。因此,有商务邮箱的个人资料需要 2 个积分;有个人邮箱和电话号码的则需要 5 个积分。联系潜在客户时,默认只请求商务邮箱;仅在任务需要时才请求个人邮箱或电话号码。
访问权限
个人联系信息补充受访问权限控制。并非所有账户都已启用此功能。如果返回的 personal_emails / phone_numbers 为空,可能需要为账户启用此功能(先预览可低成本检查)。
与商务邮箱一同获取
contact 组已在一次调用中返回商务邮箱、个人邮箱和电话号码,因此可以一次获取全部信息:
// model query: get business + personal emails + phone for one profile
const r = await callTool("person_enrich", {
professional_network_profile_urls: ["https://linkedin.com/in/person1"],
fields: ["basic_profile", "contact"],
});
return r.ok ? { contact: r.data?.[0]?.matches?.[0]?.person_data?.contact } : { error: r.message };
第 4 步:使用提交历史作为个人邮箱的备用方案(技术人员)
如果无法补充个人联系信息,或技术人员的补充结果为空,可改用其公开提交历史。此方法仅适用于工程师、开发者和技术创业者。
查找其 GitHub 用户名
先检查人员记录:social_handles 中包含 dev_platform_identifier(该人员的开发者账号标识)以及专业社交网络账号标识;在 person_enrich 的 fields 中添加 dev_platform_profiles(开发者附加项,+1 积分),即可在同一次个人资料调用中返回完整的开发者资料——仓库、组织成员身份、all_languages,有时还包括公开的 email。如果解析该人员时没有获取这些字段组,则使用独立的 dev_platform_enrich。请准确传入 crustdata_person_id 或 profile_url 中的一个:前者可用于已解析的人员——前面阶段的每一条匹配记录都在 person_data.crustdata_person_id 中包含此字段;后者必须是 GitHub URL(https://github.com/<username>);此处传入 LinkedIn URL 会返回 400 错误。它会返回包含 profile_url、name、bio、company_text,有时还包含公开 email 的 dev_platform_profiles[]:
// model query: find this person's developer profile (and any public email)
const r = await callTool("dev_platform_enrich", {
crustdata_person_id: PERSON_ID_FROM_EARLIER_PHASE, // person_data.crustdata_person_id
});
if (!r.ok) return { error: r.message };
// r.data.dev_platform_profiles[]: { profile_url, name, bio, company_text, email, ... }
return { dev_profiles: r.data.dev_platform_profiles ?? [] };
也可以搜索网络:
// model query: find Person Name's GitHub profile
const r = await callTool("web_search_live", {
query: "Person Name Company site:github.com",
sources: ["web"],
});
return r.ok ? { results: r.data.results } : { error: r.message };
验证 GitHub 个人资料
确认以下至少 2 项匹配:简介提及其公司或职位,个人资料名称匹配,仓库主题与其专业领域一致。dev_platform_enrich 会返回 name、bio 和 company_text,供你核对。如果 dev_platform_profiles[].email 已有内容,且不是 noreply 地址,可以直接使用并跳过下面的提交记录查询。
从提交记录中提取邮箱
dev_platform_profiles[].repos 条目已包含选择目标所需的信息——full_name、is_fork 和 github_created_at。选择 is_fork 为 false 的最早仓库;无需额外调用。只有在增强结果中没有仓库时,才使用 web_enrich_live 列出仓库(urls 参数是一个数组):
// model query: list the user's oldest repos to find a non-fork repo
const r = await callTool("web_enrich_live", {
urls: ["https://api.github.com/users/USERNAME/repos?sort=created&direction=asc&per_page=5"],
});
// r.data is [{ success, url, title, content }]; parse content[0] for repo names
return r.ok ? { content: r.data?.[0]?.content } : { error: r.message };
然后获取提交邮箱:
// model query: read the commit patch to extract the author email
const r = await callTool("web_enrich_live", {
urls: ["https://github.com/OWNER/REPO/commit/SHA.patch"],
});
return r.ok ? { content: r.data?.[0]?.content } : { error: r.message };
从补丁中的 From: 标头提取邮箱。丢弃 [email protected] 和 *@users.noreply.github.com 地址。
人员邮箱预期结果
- 商务邮箱:已知公司的专业人士中,95% 以上
- 个人邮箱:启用个人联系信息后,通过增强 API 获取,覆盖率达 95% 以上
- 电话号码:启用个人联系信息后,通过增强 API 获取,覆盖率达 95% 以上
Crustdata 工具参考
所有工具都通过 execute({ code }) 脚本中的 callTool("<tool>", params) 调用。
| 工具 | 用途 | 关键参数 |
|---|---|---|
company_identify | 根据域名识别公司(免费) | domains: [...](数组)、names、可选的 exact_match |
person_enrich | 根据邮箱查找人员,或根据个人资料 URL 查找邮箱 | business_emails: [...] 或 professional_network_profile_urls: [...] 二选一;fields: [...](分组:basic_profile、contact、experience、…);preview: true(0 cr) |
person_search | 按筛选条件搜索人员 | filters({ op, conditions: [{ field, type, value }] })、limit、cursor、fields |
web_search_live | 网页搜索(查找个人资料、深入研究“某人是谁”) | query、sources(例如 ["web"]、["ai"]) |
web_enrich_live | 获取页面内容(提交补丁) | urls: [...](数组) |
dev_platform_enrich | 开发者资料查询(Dev Platform),适用于技术人员 | profile_url(GitHub URL)或 crustdata_person_id |
MCP 服务:install.crustdata.com/mcp(Code Mode — 元工具 list_tools、get_schema、execute)。
工具依赖项
此技能需要连接位于 install.crustdata.com/mcp 的 Crustdata Code Mode MCP 服务。它提供元工具 list_tools、get_schema 和 execute;数据工具通过 execute 脚本中的 callTool(...) 调用。此技能使用的数据工具:
company_identifyperson_enrich(以及用于实时个人资料 URL 增强的person_enrich_live)person_searchweb_search_liveweb_enrich_livedev_platform_enrich