Files
9router/src/app/api/oauth/xiaomi-mimo/api-key/route.js
T
叶炜朋 73cb89143c feat(xiaomi-mimo): merge MiMo Desktop support into xiaomi-mimo as dual auth
Adds the Desktop-exclusive Preview models and the Xiaomi account-session
route to the existing xiaomi-mimo provider instead of a separate
xiaomi-desktop provider, so the dashboard shows one MiMo entry rather than
three overlapping ones.

Dual auth, same pattern as kimi — API key (sk-) covers the cloud API,
Desktop/OAuth adds the account session used by the Preview models:

- registry: category oauth, authModes [oauth, apikey], oauth block, the two
  mimo-x-*-preview models, and the invite signupUrl
- executor: routes Preview models to the account-service route with a Cookie
  session, everything else keeps the sourceFormat-matched transport
- oauth: custom ECDH encrypted-callback flow (X25519 -> SHA256 -> AES-256-GCM)
  with a loopback callback proxy, plus one-click import of the local Desktop
  auth.json
- usage: weekly quota from the account session

Fixes found while merging:

- the OAuth browser flow was dead: poll-status cleared the session before the
  client could POST /exchange, so every exchange returned 400
- a Claude-format client was sent to /v1/chat/completions instead of the
  declared /anthropic/v1/messages transport, because buildUrl ignored
  runtimeTransport
- stopXiaomiMimoProxy leaked every pending session (each holding an X25519
  private key) for the process lifetime
- the OAuth exchange did not persist the Desktop passToken, so the Preview
  models could never work after a browser sign-in

Removes dead code: the local engine token minting (mimoEngine, never called
on the request path), the model-catalog and usage routes, engineToken/
engineUrl plumbing, and an unread top-level usage block.

Adds tests/unit/xiaomi-mimo-{executor,oauth-session,oauth-proxy}.test.js —
the provider previously had none.
2026-09-10 23:42:41 +07:00

137 lines
4.4 KiB
JavaScript

import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
/**
* POST /api/oauth/xiaomi-mimo/api-key
* Import a Xiaomi MiMo API key manually (or from auto-import).
* The key is validated against the models endpoint, then stored.
*
* Body: { apiKey, uid?, baseUrl? }
*/
export async function POST(request) {
try {
const { apiKey, uid, baseUrl, mimoPassToken, mimoUserId, mimoCUserId } = await request.json();
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
return NextResponse.json(
{ error: "API key is required" },
{ status: 400 },
);
}
const key = apiKey.trim();
if (!key.startsWith("sk-")) {
return NextResponse.json(
{ error: "Invalid key format — expected sk- prefix" },
{ status: 400 },
);
}
const effectiveBaseUrl = (baseUrl || "https://api.xiaomimimo.com/v1").replace(/\/+$/, "");
// Validate the key against the models endpoint
let validated = false;
let modelCount = 0;
try {
const resp = await fetch(`${effectiveBaseUrl}/models`, {
method: "GET",
headers: {
Authorization: `Bearer ${key}`,
"X-Mimo-Source": "mimocode-cli",
},
signal: AbortSignal.timeout(10000),
});
if (resp.ok) {
const data = await resp.json();
modelCount = Array.isArray(data?.data) ? data.data.length : 0;
validated = true;
}
} catch {
// Network error — still allow import (key may be valid but network blocked)
}
if (!validated) {
// Soft-fail: store the key but mark as untested
console.log("[xiaomi-mimo] key validation failed, storing as untested");
}
// Dedup: if a connection with the same uid or same key already exists, update it
const { getProviderConnections, updateProviderConnection } = await import("@/models");
const existing = (await getProviderConnections()).find(
(c) => c.provider === "xiaomi-mimo" && (
(uid && c.email === `${uid}@xiaomi`) ||
c.accessToken === key
),
);
if (existing) {
const updated = await updateProviderConnection(existing.id, {
accessToken: key,
providerSpecificData: {
...existing.providerSpecificData,
uid: uid || existing.providerSpecificData?.uid || null,
baseUrl: effectiveBaseUrl,
// Per-account session credential — enables multi-account rotation.
mimoPassToken: mimoPassToken || existing.providerSpecificData?.mimoPassToken || null,
mimoUserId: mimoUserId || existing.providerSpecificData?.mimoUserId || null,
mimoCUserId: mimoCUserId || existing.providerSpecificData?.mimoCUserId || null,
modelCount,
},
testStatus: validated ? "active" : existing.testStatus,
});
return NextResponse.json({
success: true,
validated,
modelCount,
updated: true,
connection: {
id: existing.id,
provider: existing.provider,
email: existing.email,
displayName: existing.displayName,
},
});
}
const connection = await createProviderConnection({
provider: "xiaomi-mimo",
authType: "api_key",
accessToken: key,
refreshToken: null,
// API keys don't expire on a fixed schedule; use a long horizon
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
email: uid ? `${uid}@xiaomi` : null,
displayName: uid ? `Xiaomi ${uid}` : "Xiaomi MiMo",
providerSpecificData: {
uid: uid || null,
baseUrl: effectiveBaseUrl,
authMethod: "api_key",
provider: "API Key",
modelCount,
// Per-account session credential — enables multi-account rotation.
mimoPassToken: mimoPassToken || null,
mimoUserId: mimoUserId || null,
mimoCUserId: mimoCUserId || null,
},
testStatus: validated ? "active" : "untested",
});
return NextResponse.json({
success: true,
validated,
modelCount,
connection: {
id: connection.id,
provider: connection.provider,
email: connection.email,
displayName: connection.displayName,
},
});
} catch (error) {
console.log("Xiaomi MiMo API key import error:", error);
return NextResponse.json(
{ error: "API key import failed" },
{ status: 500 },
);
}
}