Twenty Partner Program 技能实践:twenty-partner-recap 如何把 Fireflies 会议自动沉淀为 CRM 伙伴档案
本文围绕 Twenty 伙伴计划应用(packages/twenty-apps/internal/twenty-partners)内置的 twenty-partner-recap 技能文档展开:完整拆解其"拉取 Fireflies 会议 → 按邮箱/域名匹配 Partner 记录 → 转录优先生成结构化纪要 → 以 Note 注入 CRM → 汇报 → 可选清理录音"的六阶段管线,并结合共享 API 参考文件补齐凭证、GraphQL 查询与幂等设计等实操细节,读完可复现一套"会议记录自动回流 CRM"的 Agent 技能工作流。
一、技能定位:网络维护,不是线索主路径
twenty-partners 应用把 Twenty 的 CRM 变成了伙伴计划的运营系统:接入符合伙伴资格的交易(Opportunity)、将其与已验证的市场伙伴匹配,并端到端跟踪匹配管道(见 README 与 AGENTS.md)。该应用下打包了一组 Claude 技能,位于 skills 目录:
| 技能 | 角色 |
|---|---|
twenty-lead-brief |
线索主路径第 1 步:把通话变成伙伴级简报并写入 Opportunity |
twenty-partner-shortlist |
第 2 步:只读地筛选适配伙伴并停下等人工复核 |
twenty-partner-intro |
第 3 步:写入 Application 记录并打开预填邮件 |
twenty-partner-triage |
网络维护:对 APPLICATION 阶段申请人做净新增价值排序 |
twenty-partner-recap |
本文主角:网络维护,把伙伴通话纪要沉淀回 CRM |
技能文档 SKILL.md 的开篇定调:这是"network upkeep, not the lead path"——在一批伙伴通话结束后运行:拉取 Fireflies 会议、判断每场会议属于哪个伙伴、总结通话内容、把总结作为 Note 挂到伙伴档案上,全程端到端执行、无需逐条确认。
它写出的纪要同时是下游技能的输入:twenty-partner-shortlist 会读取这些 Note 来区分"伙伴真实交付过的能力"与"伙伴自述的营销文案"(见 shortlist 技能 中"Read the partner's notes… A Partner call recap: note carries what was actually said on a call, which beats self-written copy")。按文档描述,约 5 个已验证伙伴已有一条这样的纪要,因此每次运行都在让后续短名单更准。
可选参数 --prune:在纪要已安全落入 CRM 之后,删除对应的 Fireflies 录音以释放存储,删除前必须人工确认(见第六阶段)。
二、基础设施:共享凭证与 API 参考
文档明确声明:凭证、gql() 辅助函数和所有查询都集中在 partner-api.md(即 SKILL.md 中 ../_shared/partner-api.md 的仓库内实际路径),本技能需要全部三个 key。
2.1 凭证
所有技能统一读取 ~/.twenty/credentials.env:
TWENTY_PARTNERS_API_URL=https://partners.twenty.com
TWENTY_PARTNERS_API_KEY=<key>
FIREFLIES_API_KEY=<key>
Partners 的 key 存放在仓库内 gitignored 的 .env.prod 中,首次使用时复制到 ~/.twenty/credentials.env;Fireflies 的 key 是个人的。共享参考里还有一张"哪个技能需要哪些 key"的矩阵,twenty-partner-recap 一行标注为"Partners URL + key: yes;Fireflies key: yes"。硬性规则:任一所需 key 缺失时,停下并明确指出缺哪个,绝不允许半套凭证继续跑。
2.2 gql() 辅助函数
import os, json, urllib.request
creds = {}
for line in open(os.path.expanduser("~/.twenty/credentials.env")):
line = line.strip()
if line and "=" in line and not line.startswith("#"):
k, v = line.split("=", 1); creds[k] = v.strip()
def gql(url, key, query, variables=None):
body = json.dumps({"query": query, "variables": variables or {}}).encode()
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + key,
"User-Agent": "Mozilla/5.0"})
return json.load(urllib.request.urlopen(req, timeout=90))
两个实现细节值得注意:
User-Agent头不是可选的——Fireflies API 会拒绝 urllib 的默认 UA,这是共享参考里明确标注的"真实限制";- 端点约定:Partners 用
$TWENTY_PARTNERS_API_URL/graphql,Fireflies 固定为https://api.fireflies.ai/graphql。
2.3 本技能用到的 GraphQL 查询
匹配用:拉取全部伙伴及联系人 + 域名(分页,endCursor 作为 $a 传入,直到 hasNextPage 为 false):
query($a:String){ partners(after:$a){
pageInfo{ hasNextPage endCursor }
edges{ node{
id name slug validationStage
persons{ edges{ node{ name{ firstName lastName } emails{ primaryEmail } } } }
company{ name domainName{ primaryLinkUrl } } } } } }
Note 读写(第四阶段的核心):
query($pid:UUID!){ noteTargets(filter:{ targetPartnerId:{ eq:$pid } }){
edges{ node{ note{ id title bodyV2{ markdown } createdAt } } } } }
mutation($d:NoteCreateInput!){ createNote(data:$d){ id title } }
mutation($d:NoteTargetCreateInput!){ createNoteTarget(data:$d){ id targetPartnerId } }
mutation($id:UUID!,$d:NoteUpdateInput!){ updateNote(id:$id,data:$d){ id } }
Fireflies 三件套(注意:date 是 epoch 毫秒;limit 上限为 50,超过会收到硬性的 invalid_arguments 400,而不是软截断):
query{ transcripts(limit:50){ id title date duration participants
meeting_attendees{ displayName email } } }
query($id:String!){ transcript(id:$id){
title date duration participants host_email organizer_email
meeting_attendees{ displayName email }
summary{ overview short_summary keywords }
sentences{ speaker_name text } } }
mutation($id:String!){ deleteTranscript(id:$id){ id title } }
共享参考还给出 Fireflies URL 的解析规则:app.fireflies.ai/view/<slug>::<ID>,其中 ID 是尾部 01K… 开头的那段——这是用户直接粘贴 URL 时提取 transcript id 的依据。
三、阶段一:拉取会议
默认时间窗:最近 2 天,覆盖"昨天"的场景;用户可每次运行时覆盖——"last week"、某个日期,或直接粘贴 Fireflies URL 与 ID。
执行步骤:
- 列出最近的 transcripts(即上面
transcripts(limit:50)查询); - 按
date(epoch 毫秒)过滤出时间窗内的记录; - 对每条保留的 transcript 逐条拉取
transcript(id:)详情(含sentences与summary)。
去重规则:若两条 transcript 属于同一伙伴、同一天(Fireflies 有时会重复录制),保留句子数更多的那条。
四、阶段二:把每场会议匹配到 Partner 记录
核心原则:只有能对应到现有 Partner 的会议才进入处理。线索通话(lead)或 discovery 通话没有 Partner 匹配:跳过,并在末尾报告中列出。
匹配算法分三步:
- 一次分页遍历所有伙伴,构建两张映射表:
email → partner:来自每个伙伴persons.edges.node.emails.primaryEmail;domain → [partners]:来自每家公司的company.domainName.primaryLinkUrl。
- 对每场会议,取参会人邮箱,先剔除
@twenty.com域名邮箱以及 host 与 organizer(这是 Twenty 自己一方)。对每位剩余参会人:- 精确邮箱匹配优先——这是最强信号,命中即定;
- 否则域名匹配,但跳过免费邮箱服务商(
gmail.com、outlook.com、hotmail.com、yahoo.com、icloud.com、proton.me等)。该域名下恰好一个伙伴才算匹配;多个伙伴共享该域名时,在报告中标记(ambiguous)并跳过,绝不猜测。
- 会议标题只是次级线索(历史上曾沿用
Partner intro between … and <name>这类命名约定),永远不作为主匹配器。
五、阶段三:转录优先的纪要生成
总纪律:先判断内容质量,绝不从空内容或噪声里写出纪要。
- 转录"可用"的判定门槛:约 15 句以上 且 平均每句 4 词以上。几行单字词(
Platform.、Opportunity.、Background.)是 ASR 乱码而非转录,即使数组非空也视为不可用。 - 转录可用 → 由 Agent 自己基于转录写 recap。只要转录存在这就是默认路径。
- 没有可用转录,但 Fireflies 已生成 summary → 回退顺序为
summary.overview→ 更丰富的字段 →short_summary,并在 source 行注明本次走的是 fallback。 - 两者皆无 → 通话还在处理中或从未被录制:跳过,记录为 "skipped: content not ready",继续下一条。当天的通话常会长时间停留在这里。永不注入空纪要或占位纪要。
5.1 纪要模板与 Source 行的"承重"作用
纪要用英文撰写、结构化、不用破折号(改用冒号或逗号)。模板如下(小节标题沿用原文档的法语标签):
**TL;DR:** one-line verdict / state of the relationship.
**Profil:** team size, location, languages, structure.
**Compétences Twenty:** deployment (cloud / self-host), data model, migrations, what they've
actually shipped.
**Contexte:** background, how they found Twenty, motivation, target clients, current
partnerships.
**Next steps:** concrete follow-ups (who owes what).
**Flags:** risks, unknowns, ASR artifacts to double-check.
Source: Fireflies <transcript-id> (call <YYYY-MM-DD>, transcript|summary).
Source: Fireflies <transcript-id> 这一行是承重墙:它既是重跑时的去重键(dedup key),也是阶段六 prune 的匹配键。真实 transcript id 必须始终写入。
Compétences Twenty 是下游真正看重的小节——它是唯一记录"经验证的能力"的地方,区别于伙伴自己写的自我介绍。纪律是:只写他们实际交付过的东西(what they've actually shipped),而不是声称能做的东西。
六、阶段四:注入 Note(幂等写回)
对每场已匹配的会议,先检查是否已经记录过:读取该伙伴的 notes(noteTargets 按 targetPartnerId 过滤),在正文中查找包含 Fireflies <transcript-id> 的条目。
- 无此 Note → 用
bodyV2.markdown调createNote创建,再用createNoteTarget把noteId关联到targetPartnerId。标题格式:Partner call recap: <Partner name> (<YYYY-MM-DD>)。 - 已有 Note → 重新生成 recap,与现有正文做 diff,只把净新增信息以带日期的
**Update <YYYY-MM-DD>:**块通过updateNote追加。没有新增内容就原样不动。
没有确认环节:匹配、总结、写入一气呵成。但写完必须读回验证——把 note 读回来,确认与伙伴的关联已正确解析。
这套"先查后写 + 净新增追加"的设计让技能天然可重跑:重复运行不会产生重复 Note,只会把新信息并入已有纪要。
七、阶段五:报告
输出一张表,收尾给计数:
| Meeting (date · title) | Attendee matched | Partner | Action |
|---|
Action 的取值域固定为六选一:
createdupdated (appended)unchangedskipped: no partner matchskipped: ambiguous domain (N partners)skipped: content not ready
八、阶段六:Prune 清理(仅 --prune 显式开启)
动机:Fireflies 存储会填满,而内容已安全进入 CRM 的录音就是死重。删除不可逆且发生在外部服务上,所以删除前必须确认。
一条录音被判定可安全清理,当且仅当其 recap Note 满足以下之一:
- 本次运行中已确认写入(
created或updated); - CRM 中已存在、且其正文包含本 transcript 的
Fireflies <id>标记。
永不清理被跳过的会议、没有 Note 的会议、或无法验证 Note 的会议:一旦丢录音,唯一副本就没了。
执行三步:
- 从本次运行的安全会议集合构建 prune set;若用户要求"再释放一些",则补充:那些
Fireflies <id>仍能解析到现存 transcript 的既有 recap Note; - 展示精确清单(伙伴、transcript id、日期)并获取明确确认;除非另有指示,默认保留最近一次;
- 对每条已确认的 transcript 调
deleteTranscript(id:),然后重新列表验证 id 已消失。报告deleted N/M及剩余数量。
历史数据兼容:这个技能上线前写下的老 Note 正文里只有日期、没有 id,因此回链 transcript 时退化为:用会议标题里的人名约定(Partner intro between … and <name>、… - <name> x Rashad)去对 Note 标题中的伙伴名。而本技能写出的 Note 都带 Fireflies <id>,此后映射是精确的。
九、端到端视角:为什么这套设计值得借鉴
把 SKILL.md 通读下来,可以提炼出四条贯穿始终的工程纪律,对任何"外部 SaaS 数据回流 CRM"的 Agent 技能都有参考价值:
- 单一事实源。凭证与查询只存在于 partner-api.md 一处,五个技能(
twenty-lead-brief、twenty-partner-shortlist、twenty-partner-intro、twenty-partner-recap、twenty-partner-triage)全部引用同一份参考——"修一处,所有技能受益",避免每个 SKILL.md 各自复制一份会漂移的 GraphQL; - 承重键设计。
Source: Fireflies <transcript-id>一行同时承担去重(阶段四读回比对)与清理(阶段六 prune 匹配)两个职责,使"重跑安全"与"删得放心"共享同一标识,而不是两套规则; - 保守的失败策略。域名歧义不猜、内容不足不写、Note 未验证不删、key 缺失即停——所有分支都显式落到
Action枚举里,让运行结果完全可审计; - 读写边界的技能化拆分。对照同目录的其他技能:
twenty-partner-shortlist刻意只读、绝不发信,而twenty-partner-recap的写入是"无确认端到端"的——因为它的产物(内部纪要 Note)低风险且幂等,而 intro 技能的写生产库动作才需要人工复核。文档把"哪些动作需要人"作为技能划分的一等设计决策写进了各自 SKILL.md。
十、适用前提与限制
- 该技能运行在 Twenty 伙伴工作区(示例环境
partners.twenty.com)之上,依赖 Twenty 应用提供的Partner、Note/NoteTarget等对象模型与 GraphQL 端点,凭证文件位于~/.twenty/credentials.env; - Fireflies 查询有硬上限:
transcripts的limit最大 50,超限是 400 错误而非截断; - 域名匹配依赖伙伴公司档案里已维护
domainName.primaryLinkUrl,邮箱匹配依赖persons的primaryEmail,数据质量决定匹配召回; --prune会删除外部 Fireflies 账户中的录音,属破坏性操作,只在纪要确认落库后才具备执行条件;- 本文所述 29 个已验证伙伴、约 5 条 recap Note 等数字来自技能文档的时点描述,实际规模随伙伴计划增长而变化。
相关文档索引:twenty-partner-recap/SKILL.md、共享 API 参考、shortlist 技能、triage 技能、模块架构说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00