

























@@ -1,169 +1,6 @@
1-import path from "node:path";
2-import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
3-import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-model-shared";
4-import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
5-import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
6-7-const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
8-const COPILOT_EDITOR_VERSION = "vscode/1.96.2";
9-const COPILOT_USER_AGENT = "GitHubCopilotChat/0.26.7";
10-const COPILOT_EDITOR_PLUGIN_VERSION = "copilot-chat/0.35.0";
11-const COPILOT_GITHUB_API_VERSION = "2025-04-01";
12-13-export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
14-15-export type CachedCopilotToken = {
16-token: string;
17-expiresAt: number;
18-updatedAt: number;
19-};
20-21-function buildCopilotIdeHeaders(
22-params: {
23-includeApiVersion?: boolean;
24-} = {},
25-): Record<string, string> {
26-return {
27-"Editor-Version": COPILOT_EDITOR_VERSION,
28-"Editor-Plugin-Version": COPILOT_EDITOR_PLUGIN_VERSION,
29-"User-Agent": COPILOT_USER_AGENT,
30- ...(params.includeApiVersion ? { "X-Github-Api-Version": COPILOT_GITHUB_API_VERSION } : {}),
31-};
32-}
33-34-function resolveCopilotTokenCachePath(env: NodeJS.ProcessEnv = process.env) {
35-return path.join(resolveStateDir(env), "credentials", "github-copilot.token.json");
36-}
37-38-function isTokenUsable(cache: CachedCopilotToken, now = Date.now()): boolean {
39-return cache.expiresAt - now > 5 * 60 * 1000;
40-}
41-42-function parseCopilotTokenResponse(value: unknown): {
43-token: string;
44-expiresAt: number;
45-} {
46-if (!value || typeof value !== "object") {
47-throw new Error("Unexpected response from GitHub Copilot token endpoint");
48-}
49-const asRecord = value as Record<string, unknown>;
50-const token = asRecord.token;
51-const expiresAt = asRecord.expires_at;
52-if (typeof token !== "string" || token.trim().length === 0) {
53-throw new Error("Copilot token response missing token");
54-}
55-56-let expiresAtMs: number;
57-if (typeof expiresAt === "number" && Number.isFinite(expiresAt)) {
58-expiresAtMs = expiresAt < 100_000_000_000 ? expiresAt * 1000 : expiresAt;
59-} else if (typeof expiresAt === "string" && expiresAt.trim().length > 0) {
60-const parsed = Number.parseInt(expiresAt, 10);
61-if (!Number.isFinite(parsed)) {
62-throw new Error("Copilot token response has invalid expires_at");
63-}
64-expiresAtMs = parsed < 100_000_000_000 ? parsed * 1000 : parsed;
65-} else {
66-throw new Error("Copilot token response missing expires_at");
67-}
68-69-return { token, expiresAt: expiresAtMs };
70-}
71-72-function resolveCopilotProxyHost(proxyEp: string): string | null {
73-const trimmed = proxyEp.trim();
74-if (!trimmed) {
75-return null;
76-}
77-78-const urlText = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
79-try {
80-const url = new URL(urlText);
81-if (url.protocol !== "http:" && url.protocol !== "https:") {
82-return null;
83-}
84-return normalizeLowercaseStringOrEmpty(url.hostname);
85-} catch {
86-return null;
87-}
88-}
89-90-export function deriveCopilotApiBaseUrlFromToken(token: string): string | null {
91-const trimmed = token.trim();
92-if (!trimmed) {
93-return null;
94-}
95-96-const match = trimmed.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i);
97-const proxyEp = match?.[1]?.trim();
98-if (!proxyEp) {
99-return null;
100-}
101-102-const proxyHost = resolveCopilotProxyHost(proxyEp);
103-if (!proxyHost) {
104-return null;
105-}
106-const host = proxyHost.replace(/^proxy\./i, "api.");
107-108-const baseUrl = `https://${host}`;
109-return resolveProviderEndpoint(baseUrl).endpointClass === "invalid" ? null : baseUrl;
110-}
111-112-export async function resolveCopilotApiToken(params: {
113-githubToken: string;
114-env?: NodeJS.ProcessEnv;
115-fetchImpl?: typeof fetch;
116-cachePath?: string;
117-loadJsonFileImpl?: (path: string) => unknown;
118-saveJsonFileImpl?: (path: string, value: CachedCopilotToken) => void;
119-}): Promise<{
120-token: string;
121-expiresAt: number;
122-source: string;
123-baseUrl: string;
124-}> {
125-const env = params.env ?? process.env;
126-const cachePath = params.cachePath?.trim() || resolveCopilotTokenCachePath(env);
127-const loadJsonFileFn = params.loadJsonFileImpl ?? loadJsonFile;
128-const saveJsonFileFn = params.saveJsonFileImpl ?? saveJsonFile;
129-const cached = loadJsonFileFn(cachePath) as CachedCopilotToken | undefined;
130-if (cached && typeof cached.token === "string" && typeof cached.expiresAt === "number") {
131-if (isTokenUsable(cached)) {
132-return {
133-token: cached.token,
134-expiresAt: cached.expiresAt,
135-source: `cache:${cachePath}`,
136-baseUrl: deriveCopilotApiBaseUrlFromToken(cached.token) ?? DEFAULT_COPILOT_API_BASE_URL,
137-};
138-}
139-}
140-141-const fetchImpl = params.fetchImpl ?? fetch;
142-const res = await fetchImpl(COPILOT_TOKEN_URL, {
143-method: "GET",
144-headers: {
145-Accept: "application/json",
146-Authorization: `Bearer ${params.githubToken}`,
147- ...buildCopilotIdeHeaders({ includeApiVersion: true }),
148-},
149-});
150-151-if (!res.ok) {
152-throw new Error(`Copilot token exchange failed: HTTP ${res.status}`);
153-}
154-155-const json = parseCopilotTokenResponse(await res.json());
156-const payload: CachedCopilotToken = {
157-token: json.token,
158-expiresAt: json.expiresAt,
159-updatedAt: Date.now(),
160-};
161-saveJsonFileFn(cachePath, payload);
162-163-return {
164-token: payload.token,
165-expiresAt: payload.expiresAt,
166-source: `fetched:${COPILOT_TOKEN_URL}`,
167-baseUrl: deriveCopilotApiBaseUrlFromToken(payload.token) ?? DEFAULT_COPILOT_API_BASE_URL,
168-};
169-}
1+export {
2+DEFAULT_COPILOT_API_BASE_URL,
3+deriveCopilotApiBaseUrlFromToken,
4+resolveCopilotApiToken,
5+type CachedCopilotToken,
6+} from "openclaw/plugin-sdk/provider-auth";
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。