























@@ -22,6 +22,7 @@ const COPILOT_HEADERS = {
2222const INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2;
2323const SLOW_DOWN_POLL_INTERVAL_MULTIPLIER = 1.4;
2424const COPILOT_ROUTER_ID_PREFIX = "accounts/";
25+const COPILOT_REQUEST_TIMEOUT_MS = 30_000;
25262627type DeviceCodeResponse = {
2728device_code: string;
@@ -50,6 +51,10 @@ type CopilotModelListEntry = {
5051type?: unknown;
5152};
5253};
54+type CopilotRequestOptions = {
55+signal?: AbortSignal;
56+timeoutMs?: number;
57+};
53585459export function normalizeDomain(input: string): string | null {
5560const trimmed = input.trim();
@@ -107,29 +112,88 @@ export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: strin
107112return "https://api.individual.githubcopilot.com";
108113}
109114110-async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
111-const response = await fetch(url, init);
115+function formatCopilotRequestError(
116+operation: string,
117+error: unknown,
118+options: Required<Pick<CopilotRequestOptions, "timeoutMs">> & {
119+signal?: AbortSignal;
120+},
121+): Error {
122+if (options.signal?.aborted) {
123+return new Error("Login cancelled");
124+}
125+if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
126+return new Error(`GitHub Copilot ${operation} timed out after ${options.timeoutMs}ms`);
127+}
128+return error instanceof Error
129+ ? error
130+ : new Error(`GitHub Copilot ${operation} failed: ${String(error)}`);
131+}
132+133+function buildCopilotRequestSignal(options: CopilotRequestOptions): AbortSignal {
134+const timeoutSignal = AbortSignal.timeout(options.timeoutMs ?? COPILOT_REQUEST_TIMEOUT_MS);
135+if (!options.signal) {
136+return timeoutSignal;
137+}
138+return AbortSignal.any([options.signal, timeoutSignal]);
139+}
140+141+async function fetchResponse(
142+url: string,
143+init: RequestInit,
144+operation: string,
145+options: CopilotRequestOptions = {},
146+): Promise<Response> {
147+const timeoutMs = options.timeoutMs ?? COPILOT_REQUEST_TIMEOUT_MS;
148+try {
149+return await fetch(url, {
150+ ...init,
151+signal: buildCopilotRequestSignal({ ...options, timeoutMs }),
152+});
153+} catch (error) {
154+throw formatCopilotRequestError(operation, error, {
155+signal: options.signal,
156+ timeoutMs,
157+});
158+}
159+}
160+161+async function fetchJson(
162+url: string,
163+init: RequestInit,
164+operation: string,
165+options: CopilotRequestOptions = {},
166+): Promise<unknown> {
167+const response = await fetchResponse(url, init, operation, options);
112168if (!response.ok) {
113169const text = await response.text();
114170throw new Error(`${response.status} ${response.statusText}: ${text}`);
115171}
116172return response.json();
117173}
118174119-async function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {
175+async function startDeviceFlow(
176+domain: string,
177+options: CopilotRequestOptions = {},
178+): Promise<DeviceCodeResponse> {
120179const urls = getUrls(domain);
121-const data = await fetchJson(urls.deviceCodeUrl, {
122-method: "POST",
123-headers: {
124-Accept: "application/json",
125-"Content-Type": "application/x-www-form-urlencoded",
126-"User-Agent": "GitHubCopilotChat/0.35.0",
180+const data = await fetchJson(
181+urls.deviceCodeUrl,
182+{
183+method: "POST",
184+headers: {
185+Accept: "application/json",
186+"Content-Type": "application/x-www-form-urlencoded",
187+"User-Agent": "GitHubCopilotChat/0.35.0",
188+},
189+body: new URLSearchParams({
190+client_id: CLIENT_ID,
191+scope: "read:user",
192+}),
127193},
128-body: new URLSearchParams({
129-client_id: CLIENT_ID,
130-scope: "read:user",
131-}),
132-});
194+"device code request",
195+options,
196+);
133197134198if (!data || typeof data !== "object") {
135199throw new Error("Invalid device code response");
@@ -205,19 +269,24 @@ async function pollForGitHubAccessToken(
205269const waitMs = Math.min(Math.ceil(intervalMs * intervalMultiplier), remainingMs);
206270await abortableSleep(waitMs, signal);
207271208-const raw = await fetchJson(urls.accessTokenUrl, {
209-method: "POST",
210-headers: {
211-Accept: "application/json",
212-"Content-Type": "application/x-www-form-urlencoded",
213-"User-Agent": "GitHubCopilotChat/0.35.0",
272+const raw = await fetchJson(
273+urls.accessTokenUrl,
274+{
275+method: "POST",
276+headers: {
277+Accept: "application/json",
278+"Content-Type": "application/x-www-form-urlencoded",
279+"User-Agent": "GitHubCopilotChat/0.35.0",
280+},
281+body: new URLSearchParams({
282+client_id: CLIENT_ID,
283+device_code: deviceCode,
284+grant_type: "urn:ietf:params:oauth:grant-type:device_code",
285+}),
214286},
215-body: new URLSearchParams({
216-client_id: CLIENT_ID,
217-device_code: deviceCode,
218-grant_type: "urn:ietf:params:oauth:grant-type:device_code",
219-}),
220-});
287+"device token request",
288+{ signal },
289+);
221290222291if (
223292raw &&
@@ -267,17 +336,23 @@ async function pollForGitHubAccessToken(
267336export async function refreshGitHubCopilotToken(
268337refreshToken: string,
269338enterpriseDomain?: string,
339+options: CopilotRequestOptions = {},
270340): Promise<OAuthCredentials> {
271341const domain = enterpriseDomain || "github.com";
272342const urls = getUrls(domain);
273343274-const raw = await fetchJson(urls.copilotTokenUrl, {
275-headers: {
276-Accept: "application/json",
277-Authorization: `Bearer ${refreshToken}`,
278- ...COPILOT_HEADERS,
344+const raw = await fetchJson(
345+urls.copilotTokenUrl,
346+{
347+headers: {
348+Accept: "application/json",
349+Authorization: `Bearer ${refreshToken}`,
350+ ...COPILOT_HEADERS,
351+},
279352},
280-});
353+"token refresh request",
354+options,
355+);
281356282357if (!raw || typeof raw !== "object") {
283358throw new Error("Invalid Copilot token response");
@@ -306,22 +381,28 @@ async function enableGitHubCopilotModel(
306381token: string,
307382modelId: string,
308383enterpriseDomain?: string,
384+options: CopilotRequestOptions = {},
309385): Promise<boolean> {
310386const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
311387const url = `${baseUrl}/models/${modelId}/policy`;
312388313389try {
314-const response = await fetch(url, {
315-method: "POST",
316-headers: {
317-"Content-Type": "application/json",
318-Authorization: `Bearer ${token}`,
319- ...COPILOT_HEADERS,
320-"openai-intent": "chat-policy",
321-"x-interaction-type": "chat-policy",
390+const response = await fetchResponse(
391+url,
392+{
393+method: "POST",
394+headers: {
395+"Content-Type": "application/json",
396+Authorization: `Bearer ${token}`,
397+ ...COPILOT_HEADERS,
398+"openai-intent": "chat-policy",
399+"x-interaction-type": "chat-policy",
400+},
401+body: JSON.stringify({ state: "enabled" }),
322402},
323-body: JSON.stringify({ state: "enabled" }),
324-});
403+"model policy request",
404+options,
405+);
325406return response.ok;
326407} catch {
327408return false;
@@ -331,21 +412,23 @@ async function enableGitHubCopilotModel(
331412async function listGitHubCopilotModelIds(
332413token: string,
333414enterpriseDomain?: string,
415+options: CopilotRequestOptions = {},
334416): Promise<string[]> {
335417const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
336418const url = `${baseUrl}/models`;
337419try {
338-const response = await fetch(url, {
339-headers: {
340-Accept: "application/json",
341-Authorization: `Bearer ${token}`,
342- ...COPILOT_HEADERS,
420+const raw = await fetchJson(
421+url,
422+{
423+headers: {
424+Accept: "application/json",
425+Authorization: `Bearer ${token}`,
426+ ...COPILOT_HEADERS,
427+},
343428},
344-});
345-if (!response.ok) {
346-return [];
347-}
348-const raw = await response.json();
429+"model list request",
430+options,
431+);
349432const data = raw && typeof raw === "object" ? (raw as { data?: unknown }).data : undefined;
350433if (!Array.isArray(data)) {
351434return [];
@@ -425,7 +508,7 @@ export async function loginGitHubCopilot(options: {
425508}
426509const domain = enterpriseDomain || "github.com";
427510428-const device = await startDeviceFlow(domain);
511+const device = await startDeviceFlow(domain, { signal: options.signal });
429512options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
430513431514const githubAccessToken = await pollForGitHubAccessToken(
@@ -479,5 +562,7 @@ export const githubCopilotOAuthProvider: OAuthProviderInterface = {
479562};
480563481564export const testing = {
565+ enableGitHubCopilotModel,
482566 listGitHubCopilotModelIds,
567+ startDeviceFlow,
483568};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。