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.
This commit is contained in:
叶炜朋
2026-09-10 23:42:41 +07:00
parent 83af3f1853
commit 73cb89143c
21 changed files with 1828 additions and 4 deletions
@@ -0,0 +1,276 @@
"use client";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Modal, Button } from "@/shared/components";
/**
* Xiaomi MiMo Auth Modal
*
* Auto-imports credentials from the local Xiaomi MiMo Desktop auth.json (~/.local/share/mimocode/auth.json).
* If auto-import fails, offers a one-click browser OAuth fallback.
* Reached only via the "Connect with OAuth" button — the API-key path uses the
* standard Add API Key modal, since Xiaomi MiMo supports both auth modes.
*/
export default function XiaomiMimoAuthModal({ isOpen, onSuccess, onClose }) {
const [phase, setPhase] = useState("detecting"); // detecting | found | not-found | importing | error
const [detectResult, setDetectResult] = useState(null);
const [error, setError] = useState(null);
const [oauthUrl, setOauthUrl] = useState(null);
const [oauthState, setOauthState] = useState(null);
// Auto-detect local credentials when modal opens
useEffect(() => {
if (!isOpen) return;
let cancelled = false;
(async () => {
setPhase("detecting");
setError(null);
setDetectResult(null);
setOauthUrl(null);
try {
const res = await fetch("/api/oauth/xiaomi-mimo/auto-import");
const data = await res.json();
if (cancelled) return;
if (data.found && data.apiKey) {
setDetectResult(data);
setPhase("found");
} else {
setPhase("not-found");
setError(data.error || "Xiaomi MiMo Desktop credentials not found on this machine.");
}
} catch {
if (!cancelled) {
setPhase("not-found");
setError("Failed to read local Xiaomi MiMo Desktop credentials.");
}
}
})();
return () => { cancelled = true; };
}, [isOpen]);
// Import the auto-detected key
const handleImport = async () => {
if (!detectResult?.apiKey) return;
setPhase("importing");
setError(null);
try {
const res = await fetch("/api/oauth/xiaomi-mimo/api-key", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apiKey: detectResult.apiKey,
uid: detectResult.uid,
baseUrl: detectResult.baseUrl,
mimoPassToken: detectResult.mimoPassToken || null,
mimoUserId: detectResult.mimoUserId || null,
mimoCUserId: detectResult.mimoCUserId || null,
}),
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.error || "Import failed");
}
onSuccess?.(data.connection);
onClose();
} catch (err) {
setPhase("found");
setError(err.message);
}
};
// Start browser OAuth fallback
const handleStartOAuth = async () => {
setError(null);
try {
const state = crypto.randomUUID();
const res = await fetch(`/api/oauth/xiaomi-mimo/authorize?state=${state}`);
const data = await res.json();
if (data.authorizeUrl) {
setOauthUrl(data.authorizeUrl);
setOauthState(data.state);
window.open(data.authorizeUrl, "_blank", "width=600,height=700");
} else {
throw new Error(data.error || "Failed to start OAuth");
}
} catch (err) {
setError(err.message);
}
};
// Poll OAuth result
const handlePollOAuth = async () => {
if (!oauthState) return;
setError(null);
try {
const res = await fetch(`/api/oauth/xiaomi-mimo/poll-status?state=${oauthState}`);
const data = await res.json();
if (data.status === "done" && data.result) {
// Exchange to create the connection
const exRes = await fetch("/api/oauth/xiaomi-mimo/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: oauthState }),
});
const exData = await exRes.json();
if (exData.success) {
onSuccess?.(exData.connection);
onClose();
} else {
throw new Error(exData.error || "Exchange failed");
}
} else if (data.status === "error") {
throw new Error(data.error || "OAuth failed");
} else {
setError("Authorization not completed yet. Finish in the browser, then click Check Again.");
}
} catch (err) {
setError(err.message);
}
};
return (
<Modal isOpen={isOpen} title="Connect Xiaomi MiMo" onClose={onClose}>
<div className="flex flex-col gap-4">
{/* Detecting */}
{phase === "detecting" && (
<div className="text-center py-6">
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
progress_activity
</span>
</div>
<h3 className="text-lg font-semibold mb-2">Reading local credentials...</h3>
<p className="text-sm text-text-muted">
Checking ~/.local/share/mimocode/auth.json
</p>
</div>
)}
{/* Found — one-click import */}
{phase === "found" && detectResult && (
<>
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg border border-green-200 dark:border-green-800">
<div className="flex gap-2">
<span className="material-symbols-outlined text-green-600 dark:text-green-400">check_circle</span>
<div className="text-sm text-green-800 dark:text-green-200">
<p className="font-medium">Xiaomi MiMo Desktop credentials found!</p>
<p className="mt-1 opacity-80">
UID: {detectResult.uid || "—"} · Source: {detectResult.source?.split(/[\\/]/).pop()}
</p>
</div>
</div>
</div>
{error && (
<div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<div className="flex gap-2">
<Button onClick={handleImport} fullWidth>
Connect with Local Credentials
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
Cancel
</Button>
</div>
</>
)}
{/* Importing */}
{phase === "importing" && (
<div className="text-center py-6">
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
progress_activity
</span>
</div>
<h3 className="text-lg font-semibold mb-2">Connecting...</h3>
</div>
)}
{/* Not found — offer OAuth fallback */}
{phase === "not-found" && (
<>
<div className="bg-amber-50 dark:bg-amber-900/20 p-3 rounded-lg border border-amber-200 dark:border-amber-800">
<div className="flex gap-2 items-start">
<span className="material-symbols-outlined text-amber-600 dark:text-amber-400">info</span>
<div className="text-sm text-amber-800 dark:text-amber-200">
<p className="font-medium">Local credentials not found</p>
<p className="mt-1 opacity-80">{error}</p>
<p className="mt-2 opacity-80">
Make sure Xiaomi MiMo Desktop is installed and you are signed in, then retry.
Or sign in via browser below.
</p>
</div>
</div>
</div>
{!oauthUrl ? (
<div className="flex gap-2">
<Button
onClick={() => {
setPhase("detecting");
// Re-trigger detect
fetch("/api/oauth/xiaomi-mimo/auto-import")
.then((r) => r.json())
.then((data) => {
if (data.found && data.apiKey) {
setDetectResult(data);
setPhase("found");
} else {
setPhase("not-found");
setError(data.error || "Still not found.");
}
})
.catch(() => setPhase("not-found"));
}}
variant="outline"
fullWidth
>
Retry Local Detect
</Button>
<Button onClick={handleStartOAuth} fullWidth>
Sign in via Browser
</Button>
</div>
) : (
<div className="flex flex-col gap-2">
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
Browser opened. Complete the Xiaomi sign-in, then click{" "}
<strong>Check Again</strong>.
</p>
</div>
<div className="flex gap-2">
<Button onClick={handlePollOAuth} fullWidth>
Check Again
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
Cancel
</Button>
</div>
</div>
)}
</>
)}
</div>
</Modal>
);
}
XiaomiMimoAuthModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onSuccess: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
+1
View File
@@ -28,6 +28,7 @@ export { default as KiroAuthModal } from "./KiroAuthModal";
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal";
export { default as CursorAuthModal } from "./CursorAuthModal";
export { default as XiaomiMimoAuthModal } from "./XiaomiMimoAuthModal";
export { default as IFlowCookieModal } from "./IFlowCookieModal";
export { default as GitLabAuthModal } from "./GitLabAuthModal";
export { default as EditConnectionModal } from "./EditConnectionModal";