AI 工具

用 Fireflies 汇总会议待办:按负责人追踪重复行动项

每场会议 Fireflies 都会生成待办,但散在不同的会议记录里,很难看出哪些一直没做。 这篇做 actiontracker.py:汇总指定日期以来所有会议的 summary.actionitems,按负责人整理成一张 CSV,记录每条待办首次和最近出现的时间;在 3 场以上会议中反复出现的标为“可能一直没完成”。

BINBHO · 图文教程

每场会议 Fireflies 都会生成待办,但散在不同的会议记录里,很难看出哪些一直没做。 这篇做 action_tracker.py:汇总指定日期以来所有会议的 summary.action_items,按负责人整理成一张 CSV,记录每条待办首次和最近出现的时间;在 3 场以上会议中反复出现的标为“可能一直没完成”。

先看清成品

  • 替身上 11 场会议、22 条原始待办,合并为 2 条,各在 11 场会议中出现,被标为可能拖延。
  • 发现并修复:第一版每条原始待办一行,同一件事在 CSV 里重复 11 次。
  • action_items 的格式官方没有规定:解析器支持“负责人”分组、项目符号、行尾时间戳,解析不了的行原样保留。pytest 3 项通过。

合规提醒:会议录音和转写涉及所有参会者的个人信息。录音前必须告知并取得同意(不同地区法律要求不同);只导出你有权访问的会议,导出的文件妥善保管,分享前确认参会者知情。发言时长报告只用于改进会议,不要用来考核或监控个人。本文只使用 Fireflies 官方 GraphQL API(https://api.fireflies.ai/graphql),key 在 Fireflies 的 Integrations → Fireflies API 中获取,放在 FIREFLIES_API_KEY。

运行说明:box 上没有 Fireflies 账号和 key。我用无效 key 真实调用了 GraphQL 接口(2026-10-21 20:35 和 20:53,UTC+8),得到 HTTP 500 和 {"errors":[{"friendly":true,"code":"auth_failed","message":"An error occurred while authenticating your request. Please recheck your API key and try again. …"}]}。其余响应来自我写的本地替身:字段名按官方文档,57 场会议的内容全是编的(每天 3 场,时间都是 08:00)。

做完之后你会得到什么?

成果 说明
action_tracker.py 汇总、解析、合并
ff_export.py / fake_fireflies.py 复用 A 篇的请求函数和替身
actions.csv 结果
test_actions.py 3 项测试

需要准备哪些环境?

项目 说明
Python 3.9 以上,pip install requests pytest
Fireflies key Integrations → Fireflies API 复制,export FIREFLIES_API_KEY=…

关键事实

事实 来源
summary.action_items 的类型是 String,“AI 根据逐字稿生成的待办列表”;文档没有规定格式 官方 Summary schema
摘要内容由 AI 应用(AI apps)的提示词决定,可以自定义 同上
API 不提供 Super Summary 和自定义 App 的数据 官方 API 文档
限流:Free 每分钟 30 次、每天 500 次;Pro 每分钟 60 次、每天 5000 次;每日额度按 UTC 零点(北京时间 08:00)重置 官方 Limits

因为格式没有规定,我的替身里用的“姓名 + 换行 + 事项”只是一种可能的格式,用真实账号时先打印一场会议的 action_items 看看。

文件结构

79-fireflies-B-src/
├── action_tracker.py
├── ff_export.py
├── fake_fireflies.py
├── demo_local.py
└── test_actions.py

第 1 步:脚本

"""汇总多场会议的 AI 待办(summary.action_items)到一张 CSV:按负责人分组,标出截止提示,
并把“连续多场会议都出现的同一条待办”标为可能拖延。
action_items 在官方文档里只写了 String 类型、没规定格式:解析器尽量宽松,解析不了的行原样保留。"""
import argparse, csv, os, re, sys
from collections import defaultdict
from ff_export import gql, local, FFError, PAGE

Q = """query Transcripts($limit: Int, $skip: Int, $fromDate: DateTime) {
  transcripts(limit: $limit, skip: $skip, fromDate: $fromDate) { id title date summary { action_items } } }"""

OWNER = re.compile(r"^\s*\*\*(.+?)\*\*\s*:?\s*$")
DUE = re.compile(r"(\d{4}-\d{2}-\d{2}|周[一二三四五六日天]前|本周|下周[一二三四五六日天]?|明天|今天|月底|by \w+day|\w+day)", re.I)

def parse(text):
    owner, out = "(未指明)", []
    for line in (text or "").splitlines():
        if not line.strip(): continue
        m = OWNER.match(line)
        if m: owner = m.group(1).strip(); continue
        item = re.sub(r"^\s*[-*•]\s*|\s*\(\d{1,2}:\d{2}\)\s*$", "", line).strip()
        d = DUE.search(item)
        out.append({"owner": owner, "item": item, "due_hint": d.group(1) if d else ""})
    return out

def norm(s): return re.sub(r"[\s()(),,。.]+", "", s.lower())

def collect(key, since=None, **kw):
    rows, skip = [], 0
    while True:
        v = {"limit": PAGE, "skip": skip}
        if since: v["fromDate"] = since
        page = gql(key, Q, v, **kw)["transcripts"]
        for t in page:
            for a in parse((t.get("summary") or {}).get("action_items")):
                rows.append({"date": local(t["date"]), "meeting": t["title"], "id": t["id"], **a})
        if len(page) < PAGE: return rows
        skip += PAGE

def merge(rows, stale=3):
    """同一负责人 + 同一条待办(忽略空格和标点)合并成一行,记录首次/最近出现和次数。"""
    g = {}
    for r in sorted(rows, key=lambda r: r["date"]):
        k = (r["owner"], norm(r["item"]))
        if k not in g:
            g[k] = {"owner": r["owner"], "item": r["item"], "due_hint": r["due_hint"], "first_seen": r["date"],
                    "last_seen": r["date"], "times_seen": 0, "meetings": []}
        x = g[k]; x["times_seen"] += 1; x["last_seen"] = r["date"]; x["meetings"].append(r["id"])
    out = []
    for x in g.values():
        x["flag"] = f"在 {x['times_seen']} 场会议里出现,可能一直没完成" if x["times_seen"] >= stale else ""
        x["meetings"] = " ".join(x["meetings"]); out.append(x)
    return sorted(out, key=lambda x: (x["owner"], x["first_seen"]))

def main(argv=None):
    ap = argparse.ArgumentParser(); ap.add_argument("--since"); ap.add_argument("-o", default="actions.csv")
    a = ap.parse_args(argv)
    key = os.environ.get("FIREFLIES_API_KEY") or sys.exit("请先 export FIREFLIES_API_KEY=…")
    try: raw = collect(key, a.since)
    except FFError as e: sys.exit(f"错误:{e}")
    rows = merge(raw)
    with open(a.o, "w", newline="", encoding="utf-8-sig") as f:
        w = csv.DictWriter(f, fieldnames=["owner", "item", "due_hint", "first_seen", "last_seen", "times_seen", "flag", "meetings"]); w.writeheader(); w.writerows(rows)
    print(f"{len(raw)} 条原始待办,合并后 {len(rows)} 条 → {a.o}")
    for r in rows: print(f"  {r['owner']}:{r['item']}  首次 {r['first_seen']},最近 {r['last_seen']},共 {r['times_seen']} 次" + (f"  ⚠ {r['flag']}" if r["flag"] else ""))

if __name__ == "__main__":
    main()

第 2 步:演示脚本

import os, sys
import fake_fireflies
srv = fake_fireflies.start(); os.environ["FIREFLIES_URL"] = f"http://127.0.0.1:{srv.server_port}/graphql"
import importlib, ff_export; importlib.reload(ff_export)
import action_tracker; importlib.reload(action_tracker)
action_tracker.main(sys.argv[1:])

第 3 步:测试

import action_tracker as m

def test_parse_owner_blocks_and_due():
    t = "**李强**\n补完接口文档(周三前)\n- 发会议纪要 (12:30)\n\n**Amy**\nSend the deck by Friday\n孤立一行"
    r = m.parse(t)
    assert [x["owner"] for x in r] == ["李强", "李强", "Amy", "Amy"]
    assert r[0]["due_hint"] == "周三前" and r[1]["item"] == "发会议纪要" and r[2]["due_hint"].lower() == "by friday"

def test_parse_without_owner():
    assert m.parse("Fix the login bug")[0]["owner"] == "(未指明)"
    assert m.parse(None) == []

def test_merge_counts_and_flags():
    rows = [{"date": d, "id": i, "owner": "李强", "item": it, "due_hint": ""} for d, i, it in
            [("2026-10-14 10:00", "T1", "补完接口文档"), ("2026-10-21 10:00", "T3", "补完 接口文档。"), ("2026-10-17 10:00", "T2", "补完接口文档")]]
    rows.append({"date": "2026-10-21 10:00", "id": "T3", "owner": "赵雪", "item": "补完接口文档", "due_hint": ""})
    out = m.merge(rows)
    li = [x for x in out if x["owner"] == "李强"][0]
    assert len(out) == 2 and li["times_seen"] == 3 and li["first_seen"].startswith("2026-10-14") and li["flag"]
    assert li["meetings"] == "T1 T2 T3"

第 4 步:运行

$ FIREFLIES_API_KEY=demo python demo_local.py --since 2026-10-18T00:00:00+08:00
22 条原始待办,合并后 2 条 → actions.csv
  李强:补完接口文档(周三前)  首次 2026-10-18 08:00,最近 2026-10-21 08:00,共 11 次  ⚠ 在 11 场会议里出现,可能一直没完成
  赵雪:搭好测试环境(周五前)  首次 2026-10-18 08:00,最近 2026-10-21 08:00,共 11 次  ⚠ 在 11 场会议里出现,可能一直没完成
$ cat actions.csv
owner,item,due_hint,first_seen,last_seen,times_seen,flag,meetings
李强,补完接口文档(周三前),周三前,2026-10-18 08:00,2026-10-21 08:00,11,在 11 场会议里出现,可能一直没完成,T003 T023 T043 T002 T022 T042 T001 T021 T041 T020 T040
赵雪,搭好测试环境(周五前),周五前,2026-10-18 08:00,2026-10-21 08:00,11,在 11 场会议里出现,可能一直没完成,T003 T023 T043 T002 T022 T042 T001 T021 T041 T020 T040
$ python -m pytest -q
3 passed

怎么检查结果?

  • “(未指明)”负责人的待办,需要在会上明确负责人。
  • 合并时忽略空格和标点;同一件事措辞不同(“补文档”和“补完接口文档”)不会合并,需要人看。
  • AI 生成的待办可能遗漏或理解错误,以会议中的实际约定为准。

调试示例

同一件事重复 11 行。 第一版每条原始待办写一行,再标出出现次数:

22 条待办 → actions.csv
  李强:11 条
  赵雪:11 条
  ⚠ 李强「补完接口文档(周三前)」出现在 11 场会议

CSV 里“补完接口文档”出现 11 行,内容只差会议编号,没法直接用来跟进。修复:按“负责人 + 去掉空格标点后的事项”合并成一行,记录首次出现、最近出现、次数和涉及的会议 id。22 条合并为 2 条。

常见问题

现象 原因 处理
全是“(未指明)” action_items 格式和解析规则不符 打印原文,调整 OWNER 正则
截止提示为空 原文没写日期或写法不在规则里 补充 DUE 正则
出现次数偏高 同一会议被重复记录 检查是否有重复的会议记录

模板规则

  • AI 待办只是线索,以会上的约定为准。
  • 先看真实格式,再写解析规则。
  • 同一件事合并成一行。
  • 不用待办数据考核个人。

项目资源

用途 地址
Fireflies API 文档(官方) https://docs.fireflies.ai
transcripts 查询(官方) https://docs.fireflies.ai/graphql-api/query/transcripts
限额与限流(官方) https://docs.fireflies.ai/fundamentals/limits
错误码(官方) https://docs.fireflies.ai/miscellaneous/error-codes
Summary 字段说明(官方) https://docs.fireflies.ai/schema/summary

反复出现的待办,往往最需要被提起。