fix(scripts): resolve UPN to OID, fix transcript headers, move aiInsights to /copilot path

- OnlineMeetings API + Application Access Policy match users by OID;
  UPN routing returns 404. Resolve UPN -> OID before all Graph calls.
- Transcript content endpoint rejects default Accept '*/*';
  add ?$format=text/vtt + Accept header + responseType('text').
- aiInsights moved to /copilot/users/{oid}/onlineMeetings/{mid}/aiInsights (Beta).
  List call returns metadata only; fetch detail per insight for
  meetingNotes/actionItems/mentions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
beo3000 2026-05-07 07:46:32 +02:00
parent 18e7737db5
commit 69c7f86fac
3 changed files with 38 additions and 8 deletions

View File

@ -99,8 +99,12 @@ async function main() {
process.exit(2); process.exit(2);
} }
const env = loadEnv(); const env = loadEnv();
const userId = opts.user || env.AZURE_USER_EMAIL; const upn = opts.user || env.AZURE_USER_EMAIL;
const client = await buildGraphClient(env); const client = await buildGraphClient(env);
// Resolve UPN -> Object ID. OnlineMeetings API + Application Access Policy
// match users by OID; UPN routing returns 404 even with correct policy.
const me = await client.api('/users/' + upn).select('id').get();
const userId = me.id;
const result = await runFetch(client, userId, opts); const result = await runFetch(client, userId, opts);
const json = JSON.stringify(result, null, 2); const json = JSON.stringify(result, null, 2);
if (opts.out) writeFileSync(opts.out, json, 'utf-8'); if (opts.out) writeFileSync(opts.out, json, 'utf-8');

View File

@ -62,14 +62,37 @@ async function fetchTranscriptVtt(client, userId, meetingId) {
new Date(b.createdDateTime) - new Date(a.createdDateTime) new Date(b.createdDateTime) - new Date(a.createdDateTime)
)[0]; )[0];
const contentPath = `/users/${userId}/onlineMeetings/${meetingId}/transcripts/${latest.id}/content`; const contentPath = `/users/${userId}/onlineMeetings/${meetingId}/transcripts/${latest.id}/content`;
return await safeGet(client, contentPath); // Transcript content endpoint returns text/vtt; default Accept */* is rejected.
try {
let req = client.api(contentPath).query({ $format: 'text/vtt' });
if (typeof req.header === 'function') req = req.header('Accept', 'text/vtt');
if (typeof req.responseType === 'function') req = req.responseType('text');
const resp = await req.get();
if (typeof resp === 'string') return resp;
if (resp && typeof resp.text === 'function') return await resp.text();
return resp;
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 403) return null;
throw err;
}
} }
async function fetchAiInsights(client, userId, meetingId) { async function fetchAiInsights(client, userId, meetingId) {
const path = `/users/${userId}/onlineMeetings/${meetingId}/aiInsights`; // Copilot recap moved under /copilot/... (beta) ca. 2025-2026.
const resp = await safeGet(client, path, { version: 'beta' }); // List call returns metadata only; detail call required for meetingNotes/actionItems.
const items = resp?.value || []; const listPath = `/copilot/users/${userId}/onlineMeetings/${meetingId}/aiInsights`;
return items[0] || null; const list = await safeGet(client, listPath, { version: 'beta' });
const items = list?.value || [];
if (items.length === 0) return null;
const details = [];
for (const item of items) {
const detail = await safeGet(client, `${listPath}/${item.id}`, { version: 'beta' });
if (detail) details.push(detail);
}
if (details.length === 0) return null;
// Return single object if one insight, otherwise wrap.
if (details.length === 1) return details[0];
return { id: details[0].id, insights: details };
} }
async function fetchRecordingUrl(client, userId, meetingId) { async function fetchRecordingUrl(client, userId, meetingId) {

View File

@ -71,8 +71,11 @@ describe('fetchTranscriptVtt', () => {
describe('fetchAiInsights', () => { describe('fetchAiInsights', () => {
it('returns insights when available', async () => { it('returns insights when available', async () => {
const client = fakeClient({ const client = fakeClient({
"/users/u/onlineMeetings/M1/aiInsights": { "/copilot/users/u/onlineMeetings/M1/aiInsights": {
value: [{ id: 'I1', actionItems: [], meetingNotes: [], mentions: [] }] value: [{ id: 'I1' }]
},
"/copilot/users/u/onlineMeetings/M1/aiInsights/I1": {
id: 'I1', actionItems: [], meetingNotes: [], mentions: []
} }
}); });
const out = await fetchAiInsights(client, 'u', 'M1'); const out = await fetchAiInsights(client, 'u', 'M1');