رفتن به محتوا

آوردن جلسه‌ها به ابزارهای خودتان

توکنی که recordings:read («خواندن» در گروه «جلسه‌ها») دارد می‌تواند همهٔ جلسه‌های آماده را از لُب بیرون بیاورد: به پوشه‌ای از یادداشت‌های Markdown، یک ویکی، یک نمایهٔ جست‌وجو، یا صندوق ورودی‌ای که یک دستیار کارهایش را از آن برمی‌دارد. این صفحه این کار را قدم به قدم پیش می‌برد و بعد نشان می‌دهد چطور آن را به‌روز نگه دارید.

فهرست را بخوانید و هر جلسه را بگیرید

Section titled “فهرست را بخوانید و هر جلسه را بگیرید”
  1. فهرست جلسه‌های آماده را صفحه به صفحه بخوانید، با GET /recordings/ready: از تازه‌ترین، و در هر صفحه حداکثر limit جلسه (از ۱ تا ۱۰۰، به‌طور پیش‌فرض ۲۰). مقدار nextCursor هر صفحه را، بی‌هیچ تغییری، به‌عنوان cursor برگردانید؛ در صفحهٔ آخر null است.
  2. هر جلسه‌ای را که هنوز ندارید از bundleUrl آن بگیرید: به‌صورت JSON، یا با ?format=markdown به‌صورت یک سند Markdown.
  3. آن را با id جلسه نگه دارید. این شناسه هرگز تغییر نمی‌کند.
{
"schemaVersion": 1,
"items": [
{
"id": "MEETING_ID",
"title": "Weekly product sync",
"kind": "MEETING",
"createdAt": "2026-10-19T08:31:12.000Z",
"recordedAt": "2026-10-19T07:00:00.000Z",
"durationSeconds": 2520,
"url": "https://app.lobbe.ai/m/MEETING_ID",
"bundleUrl": "https://app.lobbe.ai/api/v1/recordings/MEETING_ID/bundle",
"series": null,
"displayTitle": "Weekly product sync"
}
],
"nextCursor": "1792398672000.MEETING_ID"
}

سه نکته دربارهٔ این فهرست:

  • هر بار همهٔ فهرست را بخوانید. با رسیدن به نخستین جلسه‌ای که از پیش دارید نایستید. ترتیب فهرست ترتیب دریافت جلسه‌ها به دست لُب است و هر جلسه فقط وقتی آماده شد به فهرست می‌آید: جلسهٔ بلندی که ساعت ۱۰:۰۰ فرستاده شده ممکن است دیرتر از جلسهٔ کوتاهی که ساعت ۱۰:۰۵ فرستاده شده آماده شود و آن‌وقت پایین‌تر از آن می‌نشیند.
  • نشانگر جای خودش را نگه می‌دارد. از پس آخرین جلسهٔ همان صفحه ادامه می‌دهد، پس جلسه‌ای که میان دو صفحه از فهرست بیرون برود باعث نمی‌شود جلسهٔ بعدی را از دست بدهید.
  • فقط جلسه‌های خود شما در آن است. سری‌ای که عضوش شده‌اید در آن نیست: برای هر کدام از آن سری‌ها GET /recordings/ready?seriesId=SERIES_ID را بخوانید (سری جلسات را ببینید).

این فهرست نمی‌گوید جلسه‌ای که کپی کرده‌اید تغییر کرده است (لُبش دوباره ساخته شده، عنوانش عوض شده، رونوشتش ویرایش شده، نام گوینده‌هایش تغییر کرده یا به سری دیگری رفته) یا حذف شده است؛ اما فید رویدادها این را می‌گوید.

همگام ماندن با فید رویدادها

Section titled “همگام ماندن با فید رویدادها”

به‌جای این‌که کل فهرست را بارها از نو بخوانید، GET /events (فید رویدادها) را دنبال کنید. فید رویدادها همان جلسه‌هایی را در بر می‌گیرد که فهرست بدون seriesId دارد، یعنی جلسه‌های خود شما، در هر وضعیتی.

  1. اول یک نشانگر بگیرید، با GET /events?after=latest، و نگهش دارید.
  2. بعد یک بار کل فهرست را بخوانید، همان‌طور که بالاتر آمد، برای جلسه‌هایی که از پیش هستند.
  3. بعد فید رویدادها را از همان نشانگر بخوانید. هر چه در مدت خواندن فهرست تغییر کرده در فید هست، پس هیچ جلسه‌ای را از دست نمی‌دهید. ممکن است جلسه‌ای را دو بار ببینید، که اشکالی ندارد.

با هر رویداد چه کنید:

رویداد چه کنید
recording.ready جلسهٔ کامل آن را بگیرید
recording.updated جلسهٔ کامل آن را دوباره بگیرید: data.change می‌گوید چه چیزی تغییر کرده است
recording.deleted کپی خودتان را حذف کنید
recording.created، recording.naming_required و recording.failed کاری لازم نیست: جلسه هنوز آماده نیست
  • وقتی data.status برابر READY است، جلسه را بگیرید. جلسهٔ حذف‌شده‌ای که برگردانده شود با recording.updated و restored برمی‌گردد، با همان وضعیتی که داشت: اگر آن وضعیت READY است، آن را بگیرید.
  • ممکن است جلسهٔ کامل پاسخ 409 NOT_READY بدهد، اگر وقتی سراغش می‌روید لُبش دوباره در حال ساخته شدن باشد، یا پاسخ 404، اگر در این فاصله حذف شده باشد. از آن بگذرید: رویداد بعدی می‌گوید چه شد.
  • دو بار گرفتن یک جلسه اشکالی ندارد. فید رویدادها هر رویداد را دست‌کم یک بار می‌دهد؛ دست‌کم یک بار را ببینید.
  • نشانگری که ۳۰ روز بی‌استفاده بماند پاسخ 410 CURSOR_EXPIRED می‌گیرد: از گام ۱ از نو شروع کنید. خواندن فهرست از جلسه‌هایی که از پیش دارید می‌گذرد، پس جلسه‌ای که در غیاب شما تغییر کرده کپی قدیمی‌اش را نگه می‌دارد. برای اطمینان، کپی خودتان را خالی کنید و همه را دوباره بگیرید.

اسکریپتی که همهٔ این کارها را می‌کند

Section titled “اسکریپتی که همهٔ این کارها را می‌کند”

این اسکریپت همهٔ جلسه‌های آماده را در پوشهٔ ./meetings کپی می‌کند، برای هر جلسه یک فایل Markdown، و بعد برای همیشه فید رویدادها را دنبال می‌کند. به bash، curl و jq نیاز دارد. اگر بایستد و دوباره اجرایش کنید، از نشانگری که ذخیره کرده ادامه می‌دهد، حتی اگر در میانهٔ نخستین خواندن فهرست ایستاده باشد؛ و با هر پاسخی که انتظارش را ندارد، همان پاسخ را نشان می‌دهد و می‌ایستد.

#!/usr/bin/env bash
# Copy every finished meeting into ./meetings as Markdown, then keep up with the feed.
api=https://app.lobbe.ai/api/v1
mkdir -p meetings
# GET $1 into the file $2 and print the HTTP status (000: no answer). Waits Retry-After on a 429.
get() {
local code wait
while true; do
rm -f "$2" headers.txt
code=$(curl -sS -o "$2" -D headers.txt -w '%{http_code}' "$1" \
-H "Authorization: Bearer $LOBBE_TOKEN") || code=000
wait=$(awk -F': *' 'tolower($1) == "retry-after" { print $2 + 0 }' headers.txt 2>/dev/null)
if [ "$code" = 429 ] && [ -n "$wait" ]; then sleep "$wait"; continue; fi
echo "$code"
return
done
}
# One meeting's Markdown. 404 (deleted) and 409 (being made again) are skipped:
# a later event says what happened. Anything else stops the script.
fetch() {
case "$(get "$api/recordings/$1/bundle?format=markdown" meeting.tmp)" in
200) mv meeting.tmp "meetings/$1.md" ;;
404 | 409) rm -f meeting.tmp ;;
*) cat meeting.tmp 2>/dev/null; exit 1 ;;
esac
}
# The first run: a cursor from now, THEN the whole list, so nothing that changes meanwhile is missed.
# A run stopped during the walk keeps that first cursor, so the feed still covers what changed since.
if [ ! -s meetings/.cursor ]; then
if [ ! -s meetings/.start-cursor ]; then
[ "$(get "$api/events?after=latest" page.json)" = 200 ] || { cat page.json 2>/dev/null; exit 1; }
jq -er .nextCursor page.json >next.cursor && mv next.cursor meetings/.start-cursor || exit 1
fi
cursor=""
while true; do
[ "$(get "$api/recordings/ready?limit=100${cursor:+&cursor=$cursor}" page.json)" = 200 ] ||
{ cat page.json 2>/dev/null; exit 1; }
for id in $(jq -r '.items[].id' page.json); do
[ -f "meetings/$id.md" ] || fetch "$id"
done
cursor=$(jq -r '.nextCursor // empty' page.json)
[ -n "$cursor" ] || break
done
mv meetings/.start-cursor meetings/.cursor
fi
# Then the feed, page by page, storing the cursor only once a page is handled.
while true; do
[ "$(get "$api/events?after=$(cat meetings/.cursor)&limit=100" page.json)" = 200 ] ||
{ cat page.json 2>/dev/null; exit 1; }
jq -r '.events[] | "\(.type) \(.recordingId) \(.data.status)"' page.json >events.txt
while read -r type id status; do
if [ "$type" = recording.deleted ]; then rm -f "meetings/$id.md"
elif [ "$status" = READY ]; then fetch "$id"
fi
done <events.txt
jq -er .nextCursor page.json >next.cursor && mv next.cursor meetings/.cursor || exit 1
[ "$(jq '.events | length' page.json)" -eq 100 ] || sleep 60
done
  • پس از 410 CURSOR_EXPIRED: فایل‌های meetings/.cursor و meetings/.start-cursor را حذف کنید و اسکریپت را دوباره اجرا کنید. نشانگر تازه‌ای می‌گیرد و فهرست را دوباره می‌خواند، و فقط جلسه‌هایی را می‌گیرد که ندارد.
  • فایل‌های خود اسکریپت: دو نشانگر کنار کپی‌ها در meetings/ می‌مانند. فایل‌های page.json، events.txt و headers.txt هم در پوشه‌ای می‌مانند که اسکریپت را از آن اجرا می‌کنید، و اگر گرفتن جلسه‌ای اسکریپت را متوقف کرده باشد، meeting.tmp هم.
  • JSON به‌جای Markdown: ?format=markdown را بردارید و پسوند فایل‌ها را .json بگذارید.
  • سقف درخواست‌ها: فهرست، جلسه‌های کامل و فید رویدادها همه از سقف دستهٔ read توکن خرج می‌کنند؛ اندازهٔ این سقف در سقف درخواست‌ها آمده است. در نخستین اجرا، آرشیو بزرگ ممکن است این سقف را پر کند؛ آن‌وقت get به اندازهٔ ثانیه‌هایی که Retry-After می‌گوید صبر می‌کند.
  • هر توکن برای یک کار: این توکن فقط recordings:read لازم دارد و هرگز دقیقه‌ای خرج نمی‌کند.

جلسهٔ کامل چه چیزهایی دارد

Section titled “جلسهٔ کامل چه چیزهایی دارد”

GET /recordings/{id}/bundle یک جلسهٔ آماده را برمی‌گرداند. به‌صورت JSON چهار بخش دارد:

بخش چه چیزی در آن است
recording خود جلسه: شناسه، عنوان‌ها (title، displayTitle و subtitle)، kind، source، زمان برگزاری (recordedAt) و زمان دریافت به دست لُب (createdAt)، مدت، url آن در برنامه، series آن، و access: owner، یا member برای سری‌ای که عضوش هستید
transcript آنچه گفته شد: زبان، آدم‌ها (speakers، با نام‌هایی که به آن‌ها داده شده)، نوبت‌ها (segments، هر کدام با گوینده، زمان‌ها و متنش)، کل متن، و رونوشت به‌صورت Markdown
lobbe لُب جلسه: قالب آن (type)، زبانش، content آن به‌صورت بخش‌ها، و کل لُب به‌صورت Markdown
stats عددهای جلسه که از روی رونوشت شمرده شده‌اند: زمان صحبت، نوبت‌ها و سؤال‌ها، برای هر نفر

بخش‌های لُب به ترتیب خواندن می‌آیند و kind هر بخش می‌گوید چه فیلدهایی دارد: summary، decision_list، action_item_list و بقیه. مرجع API فیلدهای هر نوع را فهرست کرده است.

با ?format=markdown همان جلسه یک سند text/markdown است: عنوان به‌صورت تیتر اصلی، مشخصات جلسه (زمان، مدت، سری و گوینده‌ها)، بخش‌های لُب و سپس رونوشت. طوری ساخته شده که همان‌طور که هست در یک یادداشت یا در ورودی یک دستیار هوش مصنوعی بنشیند. همان دسترسی و خطاهای نسخهٔ JSON را دارد: 404 برای جلسه‌ای که نمی‌توانید بخوانید و 409 NOT_READY برای جلسه‌ای که هنوز آماده نیست.

  • ساختار جلسهٔ کامل در v1 ثابت می‌ماند. schemaVersion همان 1 می‌ماند و تغییرهای درون v1 فقط چیزی اضافه می‌کنند: هیچ فیلدی حذف نمی‌شود یا تغییر نام نمی‌دهد.
  • پس آنچه را نمی‌شناسید نادیده بگیرید. فیلدی که نمی‌شناسید ممکن است در هر بخشی بیاید: از آن بگذرید و هرگز به خاطرش خطا ندهید. بخشی از لُب هم که kind آن را نمی‌شناسید همین‌طور.
  • کپی خود را با recording.id نگه دارید و آن را بر اساس recordedAt، یعنی زمان برگزاری جلسه، مرتب کنید.