feat: oauth

This commit is contained in:
xiaose 2026-01-27 19:59:54 +08:00
parent 48f2f150c3
commit 4adb2b5543
5 changed files with 34 additions and 3156 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,8 +6,8 @@
<title>Clawdbot Control</title> <title>Clawdbot Control</title>
<meta name="color-scheme" content="dark light" /> <meta name="color-scheme" content="dark light" />
<link rel="icon" href="./favicon.ico" sizes="any" /> <link rel="icon" href="./favicon.ico" sizes="any" />
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script> <script type="module" crossorigin src="./assets/index-BCK1V83p.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css"> <link rel="stylesheet" crossorigin href="./assets/index-CjW_qQ45.css">
</head> </head>
<body> <body>
<clawdbot-app></clawdbot-app> <clawdbot-app></clawdbot-app>

View File

@ -7,7 +7,7 @@ const MINIMAX_OAUTH_CLIENT_ID = "78257093-7e40-4613-99e0-527b14b39113";
const MINIMAX_OAUTH_SCOPE = "group_id profile model.completion"; const MINIMAX_OAUTH_SCOPE = "group_id profile model.completion";
const MINIMAX_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:user_code"; const MINIMAX_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:user_code";
export type MiniMaxDeviceAuthorization = { export type MiniMaxOAuthAuthorization = {
user_code: string; user_code: string;
verification_uri: string; verification_uri: string;
expires_in: number; expires_in: number;
@ -24,9 +24,9 @@ export type MiniMaxOAuthToken = {
resourceUrl?: string; resourceUrl?: string;
}; };
type TokenPending = { status: "pending"; slowDown?: boolean }; type TokenPending = { status: "pending"; message?: string };
type DeviceTokenResult = type TokenResult =
| { status: "success"; token: MiniMaxOAuthToken } | { status: "success"; token: MiniMaxOAuthToken }
| TokenPending | TokenPending
| { status: "error"; message: string }; | { status: "error"; message: string };
@ -47,7 +47,7 @@ function generatePkce(): { verifier: string; challenge: string; state: string }
async function requestOAuthCode(params: { async function requestOAuthCode(params: {
challenge: string; challenge: string;
state: string; state: string;
}): Promise<MiniMaxDeviceAuthorization> { }): Promise<MiniMaxOAuthAuthorization> {
const response = await fetch(MINIMAX_OAUTH_CODE_ENDPOINT, { const response = await fetch(MINIMAX_OAUTH_CODE_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: {
@ -56,6 +56,7 @@ async function requestOAuthCode(params: {
"x-request-id": randomUUID(), "x-request-id": randomUUID(),
}, },
body: toFormUrlEncoded({ body: toFormUrlEncoded({
response_type:"code",
client_id: MINIMAX_OAUTH_CLIENT_ID, client_id: MINIMAX_OAUTH_CLIENT_ID,
scope: MINIMAX_OAUTH_SCOPE, scope: MINIMAX_OAUTH_SCOPE,
code_challenge: params.challenge, code_challenge: params.challenge,
@ -66,14 +67,14 @@ async function requestOAuthCode(params: {
if (!response.ok) { if (!response.ok) {
const text = await response.text(); const text = await response.text();
throw new Error(`MiniMax device authorization failed: ${text || response.statusText}`); throw new Error(`MiniMax OAuth authorization failed: ${text || response.statusText}`);
} }
const payload = (await response.json()) as MiniMaxDeviceAuthorization & { error?: string }; const payload = (await response.json()) as MiniMaxOAuthAuthorization & { error?: string };
if (!payload.user_code || !payload.verification_uri) { if (!payload.user_code || !payload.verification_uri) {
throw new Error( throw new Error(
payload.error ?? payload.error ??
"MiniMax device authorization returned an incomplete payload (missing user_code or verification_uri).", "MiniMax OAuth authorization returned an incomplete payload (missing user_code or verification_uri).",
); );
} }
if (payload.state !== params.state) { if (payload.state !== params.state) {
@ -87,7 +88,7 @@ async function requestOAuthCode(params: {
async function pollOAuthToken(params: { async function pollOAuthToken(params: {
userCode: string; userCode: string;
verifier: string; verifier: string;
}): Promise<DeviceTokenResult> { }): Promise<TokenResult> {
const response = await fetch(MINIMAX_OAUTH_TOKEN_ENDPOINT, { const response = await fetch(MINIMAX_OAUTH_TOKEN_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: {
@ -103,37 +104,34 @@ async function pollOAuthToken(params: {
}); });
if (!response.ok) { if (!response.ok) {
let payload: { error?: string; error_description?: string } | undefined; let payload: {
status?: string;
base_resp?: { status_code?: number; status_msg?: string };
} | undefined;
try { try {
payload = (await response.json()) as { error?: string; error_description?: string }; payload = (await response.json()) as typeof payload;
} catch { } catch {
const text = await response.text(); return { status: "error", message: response.statusText };
return { status: "error", message: text || response.statusText };
} }
if (payload?.error === "authorization_pending") {
return { status: "pending" };
}
if (payload?.error === "slow_down") {
return { status: "pending", slowDown: true };
}
return { return {
status: "error", status: "error",
message: payload?.error_description || payload?.error || response.statusText, message: payload?.base_resp?.status_msg ?? response.statusText,
}; };
} }
const tokenPayload = (await response.json()) as { const tokenPayload = (await response.json()) as {
status: string;
access_token?: string | null; access_token?: string | null;
refresh_token?: string | null; refresh_token?: string | null;
expires_in?: number | null; expired_in?: number | null;
token_type?: string; token_type?: string;
resource_url?: string; resource_url?: string;
}; };
if (tokenPayload.status != "success") {
return { status: "pending", message: "current user code is not authorized" };
}
if (!tokenPayload.access_token || !tokenPayload.refresh_token || !tokenPayload.expires_in) { if (!tokenPayload.access_token || !tokenPayload.refresh_token || !tokenPayload.expired_in) {
return { status: "error", message: "MiniMax OAuth returned incomplete token payload." }; return { status: "error", message: "MiniMax OAuth returned incomplete token payload." };
} }
@ -142,7 +140,7 @@ async function pollOAuthToken(params: {
token: { token: {
access: tokenPayload.access_token, access: tokenPayload.access_token,
refresh: tokenPayload.refresh_token, refresh: tokenPayload.refresh_token,
expires: Date.now() + tokenPayload.expires_in * 1000, expires: Date.now() + tokenPayload.expired_in * 1000,
resourceUrl: tokenPayload.resource_url, resourceUrl: tokenPayload.resource_url,
}, },
}; };
@ -157,13 +155,14 @@ export async function loginMiniMaxPortalOAuth(params: {
const oauth = await requestOAuthCode({ challenge, state }); const oauth = await requestOAuthCode({ challenge, state });
const verificationUrl = oauth.verification_uri; const verificationUrl = oauth.verification_uri;
await params.note( const noteLines = [
[ `Open ${verificationUrl} to approve access.`,
`Open ${verificationUrl} to approve access.`, `If prompted, enter the code ${oauth.user_code}.`,
`If prompted, enter the code ${oauth.user_code}.`, ];
].join("\n"), if (oauth.has_benefit && oauth.benefit_message) {
"MiniMax OAuth", noteLines.push("", oauth.benefit_message);
); }
await params.note(noteLines.join("\n"), "MiniMax OAuth");
try { try {
await params.openUrl(verificationUrl); await params.openUrl(verificationUrl);
@ -190,7 +189,7 @@ export async function loginMiniMaxPortalOAuth(params: {
throw new Error(`MiniMax OAuth failed: ${result.message}`); throw new Error(`MiniMax OAuth failed: ${result.message}`);
} }
if (result.status === "pending" && result.slowDown) { if (result.status === "pending") {
pollIntervalMs = Math.min(pollIntervalMs * 1.5, 10000); pollIntervalMs = Math.min(pollIntervalMs * 1.5, 10000);
} }