feat(scripts): add fetch-meeting-artifacts orchestrator cli

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
beo3000 2026-05-06 21:06:01 +02:00
parent 51613ffe12
commit 15499cf190
2 changed files with 202 additions and 0 deletions

View File

@ -0,0 +1,114 @@
// scripts/fetch-meeting-artifacts.js
const { writeFileSync } = require('node:fs');
const {
loadEnv,
buildGraphClient,
resolveOnlineMeeting,
fetchTranscriptVtt,
fetchAiInsights,
fetchRecordingUrl
} = require('./lib/graph-meetings.js');
const { extractJoinUrlFromBody } = require('./lib/o365-calendar.js');
const { parseVtt, formatTranscript } = require('./lib/vtt-parser.js');
function parseArgs(argv) {
const out = {};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--o365-id') out.o365Id = argv[++i];
else if (a === '--join-url') out.joinUrl = argv[++i];
else if (a === '--out') out.out = argv[++i];
else if (a === '--user') out.user = argv[++i];
}
return out;
}
async function runFetch(client, userId, opts) {
const warnings = [];
let event = null;
let joinUrl = opts.joinUrl || null;
let seriesMasterId = null;
if (opts.o365Id) {
event = await client.api(`/users/${userId}/events/${opts.o365Id}`)
.select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting')
.get();
seriesMasterId = event.seriesMasterId || null;
if (!joinUrl) {
joinUrl = event.onlineMeeting?.joinUrl
|| extractJoinUrlFromBody(event.body?.content || '');
}
}
if (!joinUrl) {
warnings.push('no teams join url on event');
return {
meeting: event ? eventSummary(event) : null,
transcript: null, recap: null, recordingUrl: null, warnings
};
}
const om = await resolveOnlineMeeting(client, userId, joinUrl);
if (!om) {
warnings.push(`onlineMeeting not found for joinUrl=${joinUrl}`);
return {
meeting: event ? eventSummary(event) : null,
transcript: null, recap: null, recordingUrl: null, warnings
};
}
const [vtt, recap, recordingUrl] = await Promise.all([
fetchTranscriptVtt(client, userId, om.id).catch(e => { warnings.push(`transcript: ${e.message}`); return null; }),
fetchAiInsights(client, userId, om.id).catch(e => { warnings.push(`aiInsights: ${e.message}`); return null; }),
fetchRecordingUrl(client, userId, om.id).catch(e => { warnings.push(`recording: ${e.message}`); return null; })
]);
let transcript = null;
if (vtt) {
const cues = parseVtt(vtt, { mergeConsecutive: true });
transcript = formatTranscript(cues);
}
return {
meeting: event ? { ...eventSummary(event), onlineMeetingId: om.id, seriesMasterId } : { onlineMeetingId: om.id },
transcript,
recap,
recordingUrl,
warnings
};
}
function eventSummary(event) {
return {
id: event.id,
subject: event.subject,
start: event.start?.dateTime,
end: event.end?.dateTime,
seriesMasterId: event.seriesMasterId || null,
attendees: (event.attendees || []).map(a => ({
name: a.emailAddress?.name,
email: (a.emailAddress?.address || '').toLowerCase()
}))
};
}
async function main() {
const opts = parseArgs(process.argv);
if (!opts.o365Id && !opts.joinUrl) {
console.error('Usage: node fetch-meeting-artifacts.js --o365-id <id> [--out file.json]');
process.exit(2);
}
const env = loadEnv();
const userId = opts.user || env.AZURE_USER_EMAIL;
const client = await buildGraphClient(env);
const result = await runFetch(client, userId, opts);
const json = JSON.stringify(result, null, 2);
if (opts.out) writeFileSync(opts.out, json, 'utf-8');
else process.stdout.write(json + '\n');
}
if (require.main === module) {
main().catch(err => { console.error(err.stack || err.message); process.exit(1); });
}
module.exports = { runFetch, parseArgs };

View File

@ -0,0 +1,88 @@
// scripts/test/fetch-meeting-artifacts.test.js
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { runFetch } = require('../fetch-meeting-artifacts.js');
function makeClient(map) {
return {
api(path) {
const h = map[path];
const builder = {
query() { return builder; },
select() { return builder; },
version() { return builder; },
async get() {
if (h === undefined) throw Object.assign(new Error('nf'), { statusCode: 404 });
return typeof h === 'function' ? h() : h;
}
};
return builder;
}
};
}
describe('runFetch', () => {
it('returns full artifact bundle for happy path', async () => {
const userId = 'u@krah.de';
const client = makeClient({
[`/users/${userId}/events/E1`]: {
id: 'E1', subject: 'Jour Fixe IT Team', seriesMasterId: 'S1',
start: { dateTime: '2026-05-06T09:00:00.0000000', timeZone: 'UTC' },
end: { dateTime: '2026-05-06T10:00:00.0000000', timeZone: 'UTC' },
attendees: [{ emailAddress: { name: 'Christian Kauer', address: 'c.kauer@krah-gruppe.de' } }],
body: { contentType: 'html', content: '<a href="https://teams.microsoft.com/l/meetup-join/19%3aXYZ/0">Join</a>' }
},
[`/users/${userId}/onlineMeetings`]: { value: [{ id: 'M1', joinWebUrl: 'https://teams.microsoft.com/l/meetup-join/19%3aXYZ/0' }] },
[`/users/${userId}/onlineMeetings/M1/transcripts`]: { value: [{ id: 'T1', createdDateTime: '2026-05-06T10:01:00Z' }] },
[`/users/${userId}/onlineMeetings/M1/transcripts/T1/content`]: 'WEBVTT\n\n00:00:01.000 --> 00:00:02.000\n<v Christian Kauer>Hallo.</v>\n',
[`/users/${userId}/onlineMeetings/M1/aiInsights`]: { value: [{ id: 'I1', meetingNotes: [{ title: 'Topic', text: 'Notiz' }] }] },
[`/users/${userId}/onlineMeetings/M1/recordings`]: { value: [{ id: 'R1', recordingContentUrl: 'https://teams/play/R1', createdDateTime: '2026-05-06T10:30:00Z' }] }
});
const result = await runFetch(client, userId, { o365Id: 'E1' });
assert.equal(result.meeting.id, 'E1');
assert.equal(result.meeting.seriesMasterId, 'S1');
assert.equal(result.meeting.onlineMeetingId, 'M1');
assert.ok(result.transcript.includes('Christian Kauer: Hallo.'));
assert.equal(result.recap.id, 'I1');
assert.equal(result.recordingUrl, 'https://teams/play/R1');
});
it('handles missing transcript gracefully', async () => {
const userId = 'u@krah.de';
const client = makeClient({
[`/users/${userId}/events/E1`]: {
id: 'E1', subject: 'X', seriesMasterId: null,
start: { dateTime: '2026-05-06T09:00:00.0000000' },
end: { dateTime: '2026-05-06T10:00:00.0000000' },
attendees: [],
body: { contentType: 'html', content: '<a href="https://teams.microsoft.com/l/meetup-join/19%3aabc/0">J</a>' }
},
[`/users/${userId}/onlineMeetings`]: { value: [{ id: 'M1', joinWebUrl: 'https://teams.microsoft.com/l/meetup-join/19%3aabc/0' }] },
[`/users/${userId}/onlineMeetings/M1/transcripts`]: { value: [] },
[`/users/${userId}/onlineMeetings/M1/recordings`]: { value: [] }
});
const result = await runFetch(client, userId, { o365Id: 'E1' });
assert.equal(result.transcript, null);
assert.equal(result.recap, null);
assert.equal(result.recordingUrl, null);
});
it('returns error info when event has no teams join url', async () => {
const userId = 'u@krah.de';
const client = makeClient({
[`/users/${userId}/events/E1`]: {
id: 'E1', subject: 'X', seriesMasterId: null,
start: { dateTime: '2026-05-06T09:00:00.0000000' },
end: { dateTime: '2026-05-06T10:00:00.0000000' },
attendees: [],
body: { contentType: 'text', content: 'no join url here' }
}
});
const result = await runFetch(client, userId, { o365Id: 'E1' });
assert.equal(result.transcript, null);
assert.equal(result.recap, null);
assert.ok(result.warnings.some(w => /join url/i.test(w)));
});
});