Wave 2.2: 流式安装输出、软件内授权四模式、本机环境一键装与总览缓存
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Copy, ExternalLink, Loader2, ShieldCheck, X } from "lucide-react";
|
||||
import { authorize, cancelAuthorize, onCliAuth } from "../ipc";
|
||||
import type { AuthFlowEvent, AuthModeInfo } from "../ipc/types";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
/** 官方授权方式中文名 */
|
||||
function authModeLabel(mode: string): string {
|
||||
switch (mode) {
|
||||
case "browser_oauth":
|
||||
return "账号授权(浏览器)";
|
||||
case "device_code":
|
||||
return "设备码";
|
||||
case "api_key":
|
||||
return "API Key";
|
||||
case "local_tui":
|
||||
return "本机终端授权";
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
function modeButtonLabel(mode: string): string {
|
||||
switch (mode) {
|
||||
case "browser_oauth":
|
||||
return "开始授权";
|
||||
case "device_code":
|
||||
return "开始授权";
|
||||
case "local_tui":
|
||||
return "打开终端授权";
|
||||
case "api_key":
|
||||
return "用已保存的 Key 授权";
|
||||
default:
|
||||
return "去授权";
|
||||
}
|
||||
}
|
||||
|
||||
interface FlowState {
|
||||
mode: string;
|
||||
events: AuthFlowEvent[];
|
||||
device: { code: string; url: string } | null;
|
||||
done: boolean | null; // null=进行中
|
||||
}
|
||||
|
||||
/** 软件内授权面板(Wave 2.2 Req 2):把「说明文字」升级为可操作授权流程。
|
||||
* 覆盖 API Key / 浏览器 / 设备码 / 本机终端四类,事件流经 cli-auth-event 实时回传。 */
|
||||
export function AuthPanel({
|
||||
id,
|
||||
authModes,
|
||||
onAuthChanged,
|
||||
}: {
|
||||
id: string;
|
||||
authModes: AuthModeInfo[];
|
||||
onAuthChanged: () => void;
|
||||
}) {
|
||||
const [flow, setFlow] = useState<FlowState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
onCliAuth((ev) => {
|
||||
if (ev.cli_id !== id) return;
|
||||
setFlow((f) => {
|
||||
if (!f) return f;
|
||||
const next: FlowState = { ...f, events: [...f.events, ev] };
|
||||
if (ev.kind === "device_code" && ev.user_code && ev.verification_url) {
|
||||
next.device = { code: ev.user_code, url: ev.verification_url };
|
||||
}
|
||||
if (ev.kind === "done") {
|
||||
next.done = ev.authorized ?? false;
|
||||
onAuthChanged();
|
||||
}
|
||||
if (ev.kind === "error" || ev.kind === "cancelled") {
|
||||
next.done = false;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
return () => unlisten?.();
|
||||
}, [id, onAuthChanged]);
|
||||
|
||||
function start(mode: string) {
|
||||
setFlow({ mode, events: [], device: null, done: null });
|
||||
void authorize(id, mode);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (flow) void cancelAuthorize(id, flow.mode);
|
||||
}
|
||||
|
||||
function close() {
|
||||
setFlow(null);
|
||||
}
|
||||
|
||||
if (authModes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="auth-modes">
|
||||
{authModes.map((m) => (
|
||||
<li key={m.mode} className="auth-mode">
|
||||
<span className="auth-mode-name">{authModeLabel(m.mode)}</span>
|
||||
{m.notes_zh && <span className="auth-mode-note">{m.notes_zh}</span>}
|
||||
<button type="button" className="btn btn-secondary auth-mode-action" onClick={() => start(m.mode)}>
|
||||
{modeButtonLabel(m.mode)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{flow && (
|
||||
<Modal
|
||||
title={`${authModeLabel(flow.mode)}授权 · ${id}`}
|
||||
onClose={flow.done == null ? undefined : close}
|
||||
footer={
|
||||
flow.done == null ? (
|
||||
<button type="button" className="btn btn-secondary" onClick={cancel}>
|
||||
<X size={14} strokeWidth={1.5} aria-hidden="true" /> 取消授权
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="btn btn-primary" onClick={close}>
|
||||
完成
|
||||
</button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<AuthFlowBody flow={flow} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthFlowBody({ flow }: { flow: FlowState }) {
|
||||
const { device, events, done } = flow;
|
||||
return (
|
||||
<div className="auth-flow">
|
||||
{/* 设备码:大号验证码 + 一键复制 + 打开授权网页 */}
|
||||
{device && (
|
||||
<div className="auth-device">
|
||||
<div className="auth-device-label">在浏览器打开验证链接,输入以下设备码</div>
|
||||
<div className="auth-device-code">
|
||||
<span className="auth-device-code-text">{device.code}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary auth-copy"
|
||||
onClick={() => void navigator.clipboard.writeText(device.code)}
|
||||
>
|
||||
<Copy size={14} strokeWidth={1.5} aria-hidden="true" /> 复制
|
||||
</button>
|
||||
</div>
|
||||
<a className="btn btn-primary auth-open" href={device.url} target="_blank" rel="noreferrer">
|
||||
<ExternalLink size={14} strokeWidth={1.5} aria-hidden="true" /> 打开授权网页
|
||||
</a>
|
||||
<div className="auth-device-hint">请在弹出的浏览器里完成确认,本软件会轮询授权结果。</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 状态行 */}
|
||||
<div className="auth-status">
|
||||
{done == null ? (
|
||||
<span className="auth-waiting">
|
||||
<Loader2 size={14} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||
{device ? "等待浏览器确认…(可随时取消)" : "授权进行中,等待浏览器确认…(可随时取消)"}
|
||||
</span>
|
||||
) : done ? (
|
||||
<span className="auth-done">
|
||||
<ShieldCheck size={14} strokeWidth={1.5} aria-hidden="true" /> 已授权 ✓
|
||||
</span>
|
||||
) : (
|
||||
<span className="auth-failed">
|
||||
<X size={14} strokeWidth={1.5} aria-hidden="true" /> 授权未完成,请重试或改用其它方式
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 实时输出(脱敏后) */}
|
||||
{events.length > 0 && (
|
||||
<div className="auth-log">
|
||||
{events
|
||||
.filter((e) => e.kind === "line" || e.kind === "waiting")
|
||||
.map((e, i) => (
|
||||
<div key={i} className={`auth-log-line ${e.kind}`}>
|
||||
{e.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user