Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/renderer/ChatGPTConnection.tsx

tsx155 行查看原文
import { useEffect, useRef, useState } from "react";
import type { ProviderRecord, PublicSettings } from "../shared/bridge.ts";
import { SettingsHelp } from "./SettingsHelp.tsx";
import { unwrap } from "./unwrap.ts";
import { userErrorMessage } from "./user-error.ts";

export function ChatGPTConnection({
  provider,
  onSettings,
  onBusy,
  onError,
  onNotice,
}: {
  provider?: ProviderRecord;
  onSettings: (settings: PublicSettings, providerId: string) => void;
  onBusy: (busy: boolean) => void;
  onError: (error: string) => void;
  onNotice: (notice: string) => void;
}) {
  const pending = useRef<string | undefined>(undefined);
  const mounted = useRef(true);
  const busyRef = useRef(false);
  const onBusyRef = useRef(onBusy);
  onBusyRef.current = onBusy;
  const [login, setLogin] = useState<{ loginId: string; url: string }>();
  const [callback, setCallback] = useState("");
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    mounted.current = true;
    return () => {
      mounted.current = false;
      if (pending.current) void window.zpi.cancelChatGPTLogin(pending.current);
      if (busyRef.current) {
        busyRef.current = false;
        onBusyRef.current(false);
      }
    };
  }, []);
  const signIn = async () => {
    setBusy(true);
    busyRef.current = true;
    onBusy(true);
    onError("");
    onNotice("");
    try {
      const attempt = unwrap(await window.zpi.beginChatGPTLogin(provider?.id ?? null));
      if (!mounted.current) {
        void window.zpi.cancelChatGPTLogin(attempt.loginId);
        return;
      }
      pending.current = attempt.loginId;
      setLogin(attempt);
      const result = unwrap(await window.zpi.completeChatGPTLogin(attempt.loginId));
      if (!mounted.current || pending.current !== attempt.loginId) return;
      pending.current = undefined;
      setLogin(undefined);
      setCallback("");
      onSettings(result.settings, result.providerId);
      onNotice(result.warning ?? "ChatGPT 已登录,模型列表已自动获取。");
    } catch (error) {
      if (mounted.current && pending.current !== "cancelled") onError(userErrorMessage(error));
    } finally {
      if (mounted.current) {
        pending.current = undefined;
        setLogin(undefined);
        setBusy(false);
        busyRef.current = false;
        onBusy(false);
      }
    }
  };
  const cancel = () => {
    const id = pending.current;
    pending.current = "cancelled";
    if (id) void window.zpi.cancelChatGPTLogin(id);
  };
  const disconnect = async () => {
    if (!provider) return;
    setBusy(true);
    busyRef.current = true;
    onBusy(true);
    onError("");
    try {
      const result = unwrap(await window.zpi.disconnectChatGPT(provider.id));
      if (mounted.current) {
        onSettings(result.settings, provider.id);
        onNotice(result.warning ?? "已退出 ChatGPT。");
      }
    } catch (error) {
      if (mounted.current) onError(userErrorMessage(error));
    } finally {
      if (mounted.current) {
        setBusy(false);
        busyRef.current = false;
        onBusy(false);
      }
    }
  };
  return (
    <section className="chatgpt-connection" aria-label="ChatGPT 授权">
      {provider?.chatgptAccount?.connected ? (
        <p>已登录:{provider.chatgptAccount.label}</p>
      ) : (
        <p className="settings-label">
          使用 ChatGPT 套餐{" "}
          <SettingsHelp label="ChatGPT 登录" text="通过浏览器登录并授权,完成后自动获取账号可用模型。" />
        </p>
      )}
      <div className="chatgpt-actions">
        <button className="primary" disabled={busy} onClick={() => void signIn()}>
          {provider?.chatgptAccount?.connected ? "重新登录" : "Continue with ChatGPT"}
        </button>
        {provider?.chatgptAccount?.connected && (
          <button className="secondary" disabled={busy} onClick={() => void disconnect()}>
            退出登录
          </button>
        )}
      </div>
      {login && (
        <div className="chatgpt-login-pending">
          <p role="status">正在等待浏览器完成授权…</p>
          <button className="secondary" onClick={() => void window.zpi.openExternal(login.url)}>
            重新打开登录页面
          </button>
          <button className="secondary" onClick={cancel}>
            取消登录
          </button>
          <label>
            回调地址(浏览器未能自动返回时)
            <input
              aria-label="ChatGPT 回调地址"
              value={callback}
              onChange={(event) => setCallback(event.target.value)}
              placeholder="http://127.0.0.1:…/auth/callback?…"
            />
          </label>
          <button
            className="secondary"
            disabled={!callback.trim()}
            onClick={() => {
              void window.zpi
                .submitChatGPTCallback(login.loginId, callback)
                .then(unwrap)
                .then(() => setCallback(""))
                .catch((error) => onError(userErrorMessage(error)));
            }}
          >
            提交回调地址
          </button>
        </div>
      )}
    </section>
  );
}