aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/models
diff options
context:
space:
mode:
author苏向夜 <fu050409@163.com>2026-03-26 09:06:56 +0800
committer苏向夜 <fu050409@163.com>2026-03-26 09:06:56 +0800
commit2412f7a3a626fc3b9e7b59ce1fc900468b792972 (patch)
tree68b2ad2b56daa1ad040a4a0df0f7db509e16d53c /packages/ui/src/models
parent788715b1ca5ab5b67fcc2e69650b74e14c953a57 (diff)
parent94b0d8e208363c802c12b56d8bdbef574dd1fb91 (diff)
downloadDropOut-2412f7a3a626fc3b9e7b59ce1fc900468b792972.tar.gz
DropOut-2412f7a3a626fc3b9e7b59ce1fc900468b792972.zip
Merge branch 'main' into refactor/fe
Diffstat (limited to 'packages/ui/src/models')
-rw-r--r--packages/ui/src/models/auth.ts87
-rw-r--r--packages/ui/src/models/instance.ts82
2 files changed, 130 insertions, 39 deletions
diff --git a/packages/ui/src/models/auth.ts b/packages/ui/src/models/auth.ts
index 10b2a0d..9c814d2 100644
--- a/packages/ui/src/models/auth.ts
+++ b/packages/ui/src/models/auth.ts
@@ -13,6 +13,10 @@ import {
} from "@/client";
import type { Account, DeviceCodeResponse } from "@/types";
+function getAuthErrorMessage(error: unknown): string {
+ return error instanceof Error ? error.message : String(error);
+}
+
export interface AuthState {
account: Account | null;
loginMode: Account["type"] | null;
@@ -68,36 +72,78 @@ export const useAuthStore = create<AuthState>((set, get) => ({
toast.warning("Failed to attch auth-progress listener");
}
- const deviceCode = await startMicrosoftLogin();
- navigator.clipboard?.writeText(deviceCode.userCode).catch((err) => {
- console.error("Failed to copy to clipboard:", err);
- });
- open(deviceCode.verificationUri).catch((err) => {
- console.error("Failed to open browser:", err);
- });
- const ms = Number(deviceCode.interval) * 1000;
- const interval = setInterval(() => {
- _pollLoginStatus(deviceCode.deviceCode, onSuccess);
- }, ms);
- set({ _pollingInterval: interval, deviceCode });
+ try {
+ const deviceCode = await startMicrosoftLogin();
+
+ navigator.clipboard?.writeText(deviceCode.userCode).catch((err) => {
+ console.error("Failed to copy to clipboard:", err);
+ });
+ open(deviceCode.verificationUri).catch((err) => {
+ console.error("Failed to open browser:", err);
+ });
+
+ const ms = Math.max(1, Number(deviceCode.interval) || 5) * 1000;
+ const interval = setInterval(() => {
+ _pollLoginStatus(deviceCode.deviceCode, onSuccess);
+ }, ms);
+
+ set({
+ _pollingInterval: interval,
+ deviceCode,
+ statusMessage: deviceCode.message ?? "Waiting for authorization...",
+ });
+ } catch (error) {
+ const message = getAuthErrorMessage(error);
+ console.error("Failed to start Microsoft login:", error);
+ set({ loginMode: null, statusMessage: `Failed to start login: ${message}` });
+ toast.error(`Failed to start Microsoft login: ${message}`);
+ }
},
_pollLoginStatus: async (deviceCode, onSuccess) => {
const { _pollingInterval, _mutex: mutex, _progressUnlisten } = get();
if (mutex.isLocked) return;
- mutex.acquire();
+
+ await mutex.acquire();
+
try {
const account = await completeMicrosoftLogin(deviceCode);
clearInterval(_pollingInterval ?? undefined);
_progressUnlisten?.();
onSuccess?.();
- set({ account, loginMode: "microsoft" });
- } catch (error) {
- if (error === "authorization_pending") {
- console.log("Authorization pending...");
- } else {
- console.error("Failed to poll login status:", error);
- toast.error("Failed to poll login status");
+ set({
+ account,
+ loginMode: "microsoft",
+ deviceCode: null,
+ _pollingInterval: null,
+ _progressUnlisten: null,
+ statusMessage: "Login successful",
+ });
+ } catch (error: unknown) {
+ const message = getAuthErrorMessage(error);
+
+ if (message.includes("authorization_pending")) {
+ set({ statusMessage: "Waiting for authorization..." });
+ return;
+ }
+
+ if (message.includes("slow_down")) {
+ set({ statusMessage: "Microsoft asked to slow down polling..." });
+ return;
}
+
+ clearInterval(_pollingInterval ?? undefined);
+ _progressUnlisten?.();
+
+ set({
+ loginMode: null,
+ deviceCode: null,
+ _pollingInterval: null,
+ _progressUnlisten: null,
+ statusMessage: `Login failed: ${message}`,
+ });
+
+ console.error("Failed to poll login status:", error);
+ toast.error(`Microsoft login failed: ${message}`);
} finally {
mutex.release();
}
@@ -111,6 +157,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}
set({
loginMode: null,
+ deviceCode: null,
_pollingInterval: null,
statusMessage: null,
_progressUnlisten: null,
diff --git a/packages/ui/src/models/instance.ts b/packages/ui/src/models/instance.ts
index b1b463e..e1eb7c1 100644
--- a/packages/ui/src/models/instance.ts
+++ b/packages/ui/src/models/instance.ts
@@ -4,10 +4,13 @@ import {
createInstance,
deleteInstance,
duplicateInstance,
+ exportInstance,
getActiveInstance,
getInstance,
+ importInstance,
listInstances,
- setActiveInstance,
+ repairInstances,
+ setActiveInstance as setActiveInstanceCommand,
updateInstance,
} from "@/client";
import type { Instance } from "@/types";
@@ -22,6 +25,9 @@ interface InstanceState {
update: (instance: Instance) => Promise<void>;
setActiveInstance: (instance: Instance) => Promise<void>;
duplicate: (id: string, newName: string) => Promise<Instance | null>;
+ exportArchive: (id: string, archivePath: string) => Promise<void>;
+ importArchive: (archivePath: string, newName?: string) => Promise<Instance | null>;
+ repair: () => Promise<void>;
get: (id: string) => Promise<Instance | null>;
}
@@ -30,14 +36,20 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
activeInstance: null,
refresh: async () => {
- const { setActiveInstance } = get();
try {
const instances = await listInstances();
- const activeInstance = await getActiveInstance();
+ let activeInstance = await getActiveInstance();
+
+ if (
+ activeInstance &&
+ !instances.some((instance) => instance.id === activeInstance?.id)
+ ) {
+ activeInstance = null;
+ }
if (!activeInstance && instances.length > 0) {
- // If no active instance but instances exist, set the first one as active
- await setActiveInstance(instances[0]);
+ await setActiveInstanceCommand(instances[0].id);
+ activeInstance = instances[0];
}
set({ instances, activeInstance });
@@ -51,35 +63,27 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
const { refresh } = get();
try {
const instance = await createInstance(name);
+ await setActiveInstanceCommand(instance.id);
await refresh();
toast.success(`Instance "${name}" created successfully`);
return instance;
} catch (e) {
console.error("Failed to create instance:", e);
- toast.error("Error creating instance");
+ toast.error(String(e));
return null;
}
},
delete: async (id) => {
- const { refresh, instances, activeInstance, setActiveInstance } = get();
+ const { refresh } = get();
try {
await deleteInstance(id);
await refresh();
- // If deleted instance was active, set another as active
- if (activeInstance?.id === id) {
- if (instances.length > 0) {
- await setActiveInstance(instances[0]);
- } else {
- set({ activeInstance: null });
- }
- }
-
toast.success("Instance deleted successfully");
} catch (e) {
console.error("Failed to delete instance:", e);
- toast.error("Error deleting instance");
+ toast.error(String(e));
}
},
@@ -96,7 +100,7 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
},
setActiveInstance: async (instance) => {
- await setActiveInstance(instance.id);
+ await setActiveInstanceCommand(instance.id);
set({ activeInstance: instance });
},
@@ -104,16 +108,56 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
const { refresh } = get();
try {
const instance = await duplicateInstance(id, newName);
+ await setActiveInstanceCommand(instance.id);
await refresh();
toast.success(`Instance duplicated as "${newName}"`);
return instance;
} catch (e) {
console.error("Failed to duplicate instance:", e);
- toast.error("Error duplicating instance");
+ toast.error(String(e));
+ return null;
+ }
+ },
+
+ exportArchive: async (id, archivePath) => {
+ try {
+ await exportInstance(id, archivePath);
+ toast.success("Instance exported successfully");
+ } catch (e) {
+ console.error("Failed to export instance:", e);
+ toast.error(String(e));
+ }
+ },
+
+ importArchive: async (archivePath, newName) => {
+ const { refresh } = get();
+ try {
+ const instance = await importInstance(archivePath, newName);
+ await setActiveInstanceCommand(instance.id);
+ await refresh();
+ toast.success(`Instance "${instance.name}" imported successfully`);
+ return instance;
+ } catch (e) {
+ console.error("Failed to import instance:", e);
+ toast.error(String(e));
return null;
}
},
+ repair: async () => {
+ const { refresh } = get();
+ try {
+ const result = await repairInstances();
+ await refresh();
+ toast.success(
+ `Repair completed: restored ${result.restoredInstances}, removed ${result.removedStaleEntries}`,
+ );
+ } catch (e) {
+ console.error("Failed to repair instances:", e);
+ toast.error(String(e));
+ }
+ },
+
get: async (id) => {
try {
return await getInstance(id);