From a726bf2ff3f467120446b4050e6206106f642afe Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 3 Nov 2026 21:31:12 +0800 Subject: [PATCH] fix: find a GitLab record by the recording's file name too winlab-helper can file a recording under a booking it matched by group and time; that record carries the booking's key, not the recording's, but always names the file [] .mp4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014sgfmRNRtGuDLrKY4AjgHi --- lib/actions/recordings.ts | 20 +++++++++--- lib/gitlab.ts | 67 +++++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/lib/actions/recordings.ts b/lib/actions/recordings.ts index 85668af..52d1cba 100644 --- a/lib/actions/recordings.ts +++ b/lib/actions/recordings.ts @@ -381,12 +381,24 @@ function recordKeys(rec: Recording) { if (rec.booking) return [rec.booking] const owner = rec.group || rec.opener if (!owner) return [] - const day = new Intl.DateTimeFormat("en-CA", { - timeZone: "Asia/Taipei", - }).format(rec.startedAt) + const day = taipeiDay(rec.startedAt) return [`meet:${owner}:${day}:${rec.id}`, `meet:${owner}:${day}`] } +const taipeiDay = (date: Date) => + new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Taipei" }).format(date) + +/** The name winlab-helper gives the recording in its record. */ +function recordFile(rec: Recording) { + const owner = rec.group || rec.opener + if (!owner) return undefined + return { + fileName: `[${owner}] ${taipeiDay(rec.startedAt)} ${rec.title}.mp4`, + title: rec.title, + since: rec.startedAt, + } +} + export const getRecording = defineAction({ name: "get_recording", title: "會議紀錄", @@ -410,7 +422,7 @@ export const getRecording = defineAction({ }) : null, gitlabConfigured() - ? recordUrl(recordKeys(rec)).catch((error) => { + ? recordUrl(recordKeys(rec), recordFile(rec)).catch((error) => { emitErrorLog(error, { source: "gitlab record" }) return null }) diff --git a/lib/gitlab.ts b/lib/gitlab.ts index 692ba38..e6ac249 100644 --- a/lib/gitlab.ts +++ b/lib/gitlab.ts @@ -11,32 +11,51 @@ export const gitlabConfigured = () => Boolean(process.env.GITLAB_API_TOKEN) type Epic = { web_url: string; description: string | null; state: string } -/** The record's web URL for the first key found, or null. Searches the - * lab's epics for each key in turn: the record links the recording on - * Nextcloud, not on meet.winlab.tw, so the key is the one text it is sure - * to hold. */ -export async function recordUrl(keys: string[]) { - const token = process.env.GITLAB_API_TOKEN - if (!token) return null +async function searchEpics(search: string, since?: Date) { + const token = process.env.GITLAB_API_TOKEN ?? "" + const query = new URLSearchParams({ + search, + include_descendant_groups: "true", + state: "all", + per_page: "100", + }) + if (since) query.set("updated_after", since.toISOString()) + const response = await fetch( + `${base()}/api/v4/groups/${encodeURIComponent(group())}/epics?${query}`, + { + headers: { "private-token": token }, + cache: "no-store", + signal: AbortSignal.timeout(8000), + } + ) + if (!response.ok) throw new Error(`GitLab ${response.status}`) + return (await response.json()) as Epic[] +} + +/** The record's web URL, or null. First by key: each record carries its + * `winlab:booking request="<key>"` marker. Then by the recording's file name: + * the helper may file a recording under a booking it matched by group and + * time, whose record carries the booking's key instead, but its Recording + * row always names the file `[<owner>] <YYYY-MM-DD> <title>.mp4`. */ +export async function recordUrl( + keys: string[], + recording?: { fileName: string; title: string; since: Date } +) { + if (!gitlabConfigured()) return null for (const key of keys) { - const query = new URLSearchParams({ - search: key, - include_descendant_groups: "true", - state: "all", - per_page: "20", - }) - const response = await fetch( - `${base()}/api/v4/groups/${encodeURIComponent(group())}/epics?${query}`, - { - headers: { "private-token": token }, - cache: "no-store", - signal: AbortSignal.timeout(8000), - } - ) - if (!response.ok) throw new Error(`GitLab ${response.status}`) - const epics = (await response.json()) as Epic[] const marker = `request="${key}"` - const found = epics.find((e) => e.description?.includes(marker)) + const found = (await searchEpics(key)).find((e) => + e.description?.includes(marker) + ) + if (found) return found.web_url + } + // GitLab searches only for three characters or more. + if (recording && recording.title.length >= 3) { + const marker = `[${recording.fileName}]` + const since = new Date(recording.since.getTime() - 86_400_000) + const found = (await searchEpics(recording.title, since)).find((e) => + e.description?.includes(marker) + ) if (found) return found.web_url } return null