mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
fix(m365): BizChat invocation shape drift + HAR-import UX + Antigravity alias note (#11069)
⭐5 — M365 Copilot (BizChat) individual/consumer path — 3 itens: (1) forma de invocação do #10718 derivou de novo (2026-08-21 capture): optionsSets 14→34, allowedMessageTypes 6→30, tone "magic"→"Magic", plugins []→[{BingWebSearch}], disconnectBehavior em todos os tiers, +8 keys de clientInfo; verificado contra conta real com round-trip WebSocket (ping-then-close → resposta real). (2) Aviso sobre o alias Antigravity gemini-3.1-pro-high ainda não publicado (3.8.49 pré-data). (3) Botão "Import .har file" no modal de credencial M365. Conflito resolvido em copilot-m365-frames.ts (board vs release tip): mantive o forwarding de opts.plugins/toolChoice/customInstructions do HEAD com os NOVOS defaults da captura (BingWebSearch builtin, tone "Magic"). Alinhei 3 testes pré-existentes que afirmavam o contrato antigo (m365-bizchat-frames-4042 clientInfo, m365-tone-model-variants tone, copilot-m365-tool-calls plugins) — propagação de contrato, não mascaramento. Rebaselinei AddApiKeyModal 1073→1080 (crescimento próprio da parte 3, ~Har import button) com anotação. Validação: typecheck limpo, 142/142 testes m365/copilot verdes, changelog-integrit/file-size/eslint OK.
This commit is contained in:
116
src/shared/utils/m365HarImport.ts
Normal file
116
src/shared/utils/m365HarImport.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Pure, transport-free helpers that extract a `copilot-m365-web` credential
|
||||
* (`access_token=...; chathubPath=...`) directly from a DevTools HAR export,
|
||||
* so the "Add connection" / "Edit connection" dialogs can offer a one-click
|
||||
* "Import .har file" button instead of the user hand-extracting the token
|
||||
* from the WebSocket URL (see `HarImportButton.tsx`).
|
||||
*
|
||||
* The token rides in the query string of the BizChat ChatHub WebSocket URL
|
||||
* (`wss://substrate.office.com/m365Copilot/Chathub/<oid>@<tenant>?...
|
||||
* access_token=...`) — the same URL the executor itself connects to (see
|
||||
* `open-sse/executors/copilot-m365-connection.ts`). No network calls here;
|
||||
* everything operates on the HAR text already in the browser.
|
||||
*/
|
||||
|
||||
/** Prefix identifying the BizChat ChatHub WebSocket request in a HAR entry. */
|
||||
export const M365_CHATHUB_WS_PREFIX = "wss://substrate.office.com/m365Copilot/Chathub/";
|
||||
|
||||
export type M365HarImportResult =
|
||||
| { ok: true; apiKey: string; chathubPath: string; expiresAt: number | null }
|
||||
| { ok: false; error: string };
|
||||
|
||||
interface HarEntryLike {
|
||||
request?: { url?: unknown };
|
||||
}
|
||||
|
||||
interface HarLike {
|
||||
log?: { entries?: unknown };
|
||||
}
|
||||
|
||||
/** Decode a JWT payload WITHOUT verification — exp is a display hint only. */
|
||||
function decodeJwtExpiry(token: string): number | null {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
||||
const json = JSON.parse(atob(padded)) as { exp?: unknown };
|
||||
return typeof json.exp === "number" ? json.exp * 1000 : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the `copilot-m365-web` credential from raw HAR file text.
|
||||
*
|
||||
* Scans every request in `log.entries` for the ChatHub WebSocket URL and
|
||||
* uses the LAST match (a HAR may contain several turns of the same session;
|
||||
* the most recent one carries the freshest token). Returns a structured
|
||||
* error — never throws — so callers can render it directly.
|
||||
*/
|
||||
export function extractM365CredentialFromHar(text: string): M365HarImportResult {
|
||||
let har: HarLike;
|
||||
try {
|
||||
har = JSON.parse(text) as HarLike;
|
||||
} catch {
|
||||
return { ok: false, error: "notJson" };
|
||||
}
|
||||
|
||||
const entries = har.log?.entries;
|
||||
if (!Array.isArray(entries)) {
|
||||
return { ok: false, error: "noEntries" };
|
||||
}
|
||||
|
||||
let matchedUrl: string | null = null;
|
||||
for (const entry of entries as HarEntryLike[]) {
|
||||
const url = entry?.request?.url;
|
||||
if (typeof url === "string" && url.startsWith(M365_CHATHUB_WS_PREFIX)) {
|
||||
matchedUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedUrl) {
|
||||
return { ok: false, error: "noChathubUrl" };
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(matchedUrl);
|
||||
} catch {
|
||||
return { ok: false, error: "unparsableUrl" };
|
||||
}
|
||||
|
||||
const token = parsed.searchParams.get("access_token");
|
||||
const chathubPath = decodeURIComponent(parsed.pathname.split("/m365Copilot/Chathub/")[1] || "");
|
||||
|
||||
if (!token || !chathubPath) {
|
||||
return { ok: false, error: "missingFields" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
apiKey: `access_token=${token}; chathubPath=${chathubPath}`,
|
||||
chathubPath,
|
||||
expiresAt: decodeJwtExpiry(token),
|
||||
};
|
||||
}
|
||||
|
||||
export type HarImportExpiryTone = "ok" | "warn" | "bad" | "unknown";
|
||||
|
||||
export interface HarImportExpiryStatus {
|
||||
tone: HarImportExpiryTone;
|
||||
minutesRemaining: number | null;
|
||||
}
|
||||
|
||||
/** Classify a decoded token expiry for the inline status hint. */
|
||||
export function describeHarImportExpiry(
|
||||
expiresAt: number | null,
|
||||
now: number = Date.now()
|
||||
): HarImportExpiryStatus {
|
||||
if (expiresAt === null) return { tone: "unknown", minutesRemaining: null };
|
||||
const minutesRemaining = Math.round((expiresAt - now) / 60000);
|
||||
if (minutesRemaining <= 0) return { tone: "bad", minutesRemaining };
|
||||
if (minutesRemaining < 15) return { tone: "warn", minutesRemaining };
|
||||
return { tone: "ok", minutesRemaining };
|
||||
}
|
||||
Reference in New Issue
Block a user